多租户应用程序中的连接池。共享池与每个租户的池

时间:2018-10-09 20:12:12

标签: java spring amazon-web-services amazon-rds hikaricp

我正在使用Spring 2.x,Hibernate 5.x,Spring Data REST,Mysql 5.7构建多租户REST服务器应用程序。 Spring 2.x使用Hikari进行连接池。

我将为每个租户使用 DB ,因此每个租户都将拥有自己的数据库。

我以这种方式创建了我的MultiTenantConnectionProvider:

@Component
@Profile("prod")
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
    private static final long serialVersionUID = 3193007611085791247L;
    private Logger log = LogManager.getLogger();

    private Map<String, HikariDataSource> dataSourceMap = new ConcurrentHashMap<String, HikariDataSource>();

    @Autowired
    private TenantRestClient tenantRestClient;

    @Autowired
    private PasswordEncrypt passwordEncrypt;

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        connection.close();
    }

    @Override
    public Connection getAnyConnection() throws SQLException {
        Connection connection = getDataSource(TenantIdResolver.TENANT_DEFAULT).getConnection();
        return connection;

    }

    @Override
    public Connection getConnection(String tenantId) throws SQLException {
        Connection connection = getDataSource(tenantId).getConnection();
        return connection;
    }

    @Override
    public void releaseConnection(String tenantId, Connection connection) throws SQLException {
        log.info("releaseConnection " + tenantId);
        connection.close();
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return false;
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return null;
    }

    public HikariDataSource getDataSource(@NotNull String tentantId) throws SQLException {
        if (dataSourceMap.containsKey(tentantId)) {
            return dataSourceMap.get(tentantId);
        } else {
            HikariDataSource dataSource = createDataSource(tentantId);
            dataSourceMap.put(tentantId, dataSource);
            return dataSource;
        }
    }

    public HikariDataSource createDataSource(String tenantId) throws SQLException {
        log.info("Create Datasource for tenant {}", tenantId);
        try {
            Database database = tenantRestClient.getDatabase(tenantId);
            DatabaseInstance databaseInstance = tenantRestClient.getDatabaseInstance(tenantId);
            if (database != null && databaseInstance != null) {
                HikariConfig hikari = new HikariConfig();
                String driver = "";
                String options = "";
                switch (databaseInstance.getType()) {
                case MYSQL:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                    break;

                default:
                    driver = "jdbc:mysql://";
                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";
                }

                hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + "/" + database.getName() + options);
                hikari.setUsername(database.getUsername());
                hikari.setPassword(passwordEncrypt.decryptPassword(database.getPassword()));

                // MySQL optimizations, see
                // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
                hikari.addDataSourceProperty("cachePrepStmts", true);
                hikari.addDataSourceProperty("prepStmtCacheSize", "250");
                hikari.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
                hikari.addDataSourceProperty("useServerPrepStmts", "true");
                hikari.addDataSourceProperty("useLocalSessionState", "true");
                hikari.addDataSourceProperty("useLocalTransactionState", "true");
                hikari.addDataSourceProperty("rewriteBatchedStatements", "true");
                hikari.addDataSourceProperty("cacheResultSetMetadata", "true");
                hikari.addDataSourceProperty("cacheServerConfiguration", "true");
                hikari.addDataSourceProperty("elideSetAutoCommits", "true");
                hikari.addDataSourceProperty("maintainTimeStats", "false");
                hikari.setMinimumIdle(3);
                hikari.setMaximumPoolSize(5);

                hikari.setIdleTimeout(30000);
                hikari.setPoolName("JPAHikari_" + tenantId);
                // mysql wait_timeout 600seconds
                hikari.setMaxLifetime(580000);
                hikari.setLeakDetectionThreshold(60 * 1000);

                HikariDataSource dataSource = new HikariDataSource(hikari);


                return dataSource;

            } else {
                throw new SQLException(String.format("DB not found for tenant %s!", tenantId));
            }
        } catch (Exception e) {
            throw new SQLException(e.getMessage());
        }
    }

}

在我的实现中,我阅读了tenantId并从中央管理系统获取有关数据库实例的信息。 我为每个租户创建一个新池,并缓存该池,以避免每次都重新创建它。

我读了interesting question,但是我的问题却大不相同。 我正在考虑使用AWS(用于服务器实例和RDS数据库实例)。

让我们假设一个具体的场景,其中有100个租户。 该应用程序是管理/销售点软件。只能从代理商那里使用。假设每个租户平均每个时刻有3个代理同时工作。

牢记这个数字,并根据this article,我首先意识到的是,很难为每个租户提供一个游泳池

对于100位租户,我想拥有Aurora的db.r4.large(2vcore,15,25GB RAM和快速磁盘访问权限)就足够了(约150欧元/月)。

根据公式确定连接池的大小:

connections = ((core_count * 2) + effective_spindle_count)

我应该在池中拥有2core * 2 +1 = 5个连接。

从我得到的结果来看,这应该是池中的最大连接数,以最大化该数据库实例的性能。

第一个解决方案

所以我的第一个问题很简单:我应该为每个租户创建一个单独的连接池,因为我总共只能使用5个连接?

对我来说似乎不可能。即使我为每个租户分配2个连接,我也将有200个与DBMS的连接!

根据this question,在一个db.r4.large实例上,我最多可以有1300个连接,因此该实例似乎应该很好地承受负载。 但是根据我之前提到的文章,使用数百个数据库连接似乎是一种不好的做法:

  

如果您有10,000个前端用户,则连接池为10,000会造成精神错乱。 1000仍然很恐怖。甚至有100个连接,过度杀伤力。您想要一个最多只有几十个连接的小型池,并且希望池中等待连接的其余应用程序线程被阻塞。

第二个解决方案

我想到的第二个解决方案是为同一DMBS上的租户共享一个连接池。这意味着所有100个租户将使用5个连接的同一个Hikari池(老实说,这对我来说似乎很低)。

这是最大化性能并减少应用程序响应时间的正确方法吗?

您对如何使用Spring,Hibernate,Mysql(托管在AWS RDS Aurora上)管理这种情况有更好的了解吗?

2 个答案:

答案 0 :(得分:3)

大多数情况下,每个租户打开连接绝对不是一个好主意。您所需要做的就是在所有用户之间共享一个连接池。

  1. 因此第一步将是找到负载或根据一些预测来预测负载。

  2. 确定可接受的延迟时间是多少,突发峰值时间流量如何等

  3. 最后,您将需要此连接数,并确定所需的实例数。例如,如果您的高峰时间使用率为10k / s,每个查询需要10ms,那么您将需要100个打开的连接,等待时间为1s。

  4. 实施时不绑定任何用户。即同一共享池在所有人之间共享。除非您有一个案例将高级/基本用户归为一组,例如说有两个池,等等。

  5. 最后,如果您需要基于第3点的多个实例,那么您将在AWS中执行此操作-查看是否可以基于负载自动扩展/缩小以节省成本。

查看这些以获取一些比较指标

就峰值需求而言,这可能是最有趣的

https://github.com/brettwooldridge/HikariCP/blob/dev/documents/Welcome-To-The-Jungle.md

更多...

https://github.com/brettwooldridge/HikariCP

https://www.wix.engineering/blog/how-does-hikaricp-compare-to-other-connection-pools

答案 1 :(得分:1)

按照previous Q&A,针对多租户环境选择的策略将(令人惊讶地)使用每个租户的连接池

  

策略2:每个租户在一个数据库中都有自己的架构和连接池

     

策略2更灵活,更安全:每个租户都不能消耗超过给定数量的连接(如果需要,可以为每个租户配置此数量)

我建议将HikariCP的公式放在这里,并使用较少的租户数作为10(动态大小?),而将连接池的大小减小为2。

要更加关注您期望的流量,请注意,HikariCP Pool Size中的10个连接池大小注释可能就足够了:

  

10作为一个很好的整数。看起来低吗?试试看,我们打赌您可以轻松地处理3000个前端用户,在这种设置下以6000 TPS的速度运行简单查询。

另请参见comment表示100个实例太多

  

,但要负担100秒钟,这将是一个巨大的负担。

通过@EssexBoy