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

dre*_*nda 10 java spring amazon-web-services amazon-rds hikaricp

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

\n\n

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

\n\n

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

\n\n
@Component\n@Profile("prod")\npublic class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {\n    private static final long serialVersionUID = 3193007611085791247L;\n    private Logger log = LogManager.getLogger();\n\n    private Map<String, HikariDataSource> dataSourceMap = new ConcurrentHashMap<String, HikariDataSource>();\n\n    @Autowired\n    private TenantRestClient tenantRestClient;\n\n    @Autowired\n    private PasswordEncrypt passwordEncrypt;\n\n    @Override\n    public void releaseAnyConnection(Connection connection) throws SQLException {\n        connection.close();\n    }\n\n    @Override\n    public Connection getAnyConnection() throws SQLException {\n        Connection connection = getDataSource(TenantIdResolver.TENANT_DEFAULT).getConnection();\n        return connection;\n\n    }\n\n    @Override\n    public Connection getConnection(String tenantId) throws SQLException {\n        Connection connection = getDataSource(tenantId).getConnection();\n        return connection;\n    }\n\n    @Override\n    public void releaseConnection(String tenantId, Connection connection) throws SQLException {\n        log.info("releaseConnection " + tenantId);\n        connection.close();\n    }\n\n    @Override\n    public boolean supportsAggressiveRelease() {\n        return false;\n    }\n\n    @Override\n    public boolean isUnwrappableAs(Class unwrapType) {\n        return false;\n    }\n\n    @Override\n    public <T> T unwrap(Class<T> unwrapType) {\n        return null;\n    }\n\n    public HikariDataSource getDataSource(@NotNull String tentantId) throws SQLException {\n        if (dataSourceMap.containsKey(tentantId)) {\n            return dataSourceMap.get(tentantId);\n        } else {\n            HikariDataSource dataSource = createDataSource(tentantId);\n            dataSourceMap.put(tentantId, dataSource);\n            return dataSource;\n        }\n    }\n\n    public HikariDataSource createDataSource(String tenantId) throws SQLException {\n        log.info("Create Datasource for tenant {}", tenantId);\n        try {\n            Database database = tenantRestClient.getDatabase(tenantId);\n            DatabaseInstance databaseInstance = tenantRestClient.getDatabaseInstance(tenantId);\n            if (database != null && databaseInstance != null) {\n                HikariConfig hikari = new HikariConfig();\n                String driver = "";\n                String options = "";\n                switch (databaseInstance.getType()) {\n                case MYSQL:\n                    driver = "jdbc:mysql://";\n                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";\n                    break;\n\n                default:\n                    driver = "jdbc:mysql://";\n                    options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false";\n                }\n\n                hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + "/" + database.getName() + options);\n                hikari.setUsername(database.getUsername());\n                hikari.setPassword(passwordEncrypt.decryptPassword(database.getPassword()));\n\n                // MySQL optimizations, see\n                // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration\n                hikari.addDataSourceProperty("cachePrepStmts", true);\n                hikari.addDataSourceProperty("prepStmtCacheSize", "250");\n                hikari.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");\n                hikari.addDataSourceProperty("useServerPrepStmts", "true");\n                hikari.addDataSourceProperty("useLocalSessionState", "true");\n                hikari.addDataSourceProperty("useLocalTransactionState", "true");\n                hikari.addDataSourceProperty("rewriteBatchedStatements", "true");\n                hikari.addDataSourceProperty("cacheResultSetMetadata", "true");\n                hikari.addDataSourceProperty("cacheServerConfiguration", "true");\n                hikari.addDataSourceProperty("elideSetAutoCommits", "true");\n                hikari.addDataSourceProperty("maintainTimeStats", "false");\n                hikari.setMinimumIdle(3);\n                hikari.setMaximumPoolSize(5);\n\n                hikari.setIdleTimeout(30000);\n                hikari.setPoolName("JPAHikari_" + tenantId);\n                // mysql wait_timeout 600seconds\n                hikari.setMaxLifetime(580000);\n                hikari.setLeakDetectionThreshold(60 * 1000);\n\n                HikariDataSource dataSource = new HikariDataSource(hikari);\n\n\n                return dataSource;\n\n            } else {\n                throw new SQLException(String.format("DB not found for tenant %s!", tenantId));\n            }\n        } catch (Exception e) {\n            throw new SQLException(e.getMessage());\n        }\n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

我读到了这个有趣的问题,但我的问题完全不同。\n我正在考虑使用 AWS(既用于服务器实例,又用于 RDS 数据库实例)。

\n\n

让我们假设一个具体场景,其中我有 100 个租户。\n该应用程序是一个管理/销售点软件。它将仅由代理使用。假设每个租户平均每个时刻有 3 个代理同时工作。

\n\n

考虑到这些数字并根据本文,我意识到的第一件事是似乎很难为每个租户提供一个游泳池。

\n\n

对于 100 个租户,我认为带有 Aurora 的db.r4.large(2vcore、15,25GB RAM 和快速磁盘访问)应该足够了(大约 150\xe2\x82\xac/月)。

\n\n

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

\n\n
connections = ((core_count * 2) + effective_spindle_count)\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

根据我得到的信息,这应该是池中的最大连接数,以最大限度地提高该数据库实例的性能。

\n\n

第一个解决方案

\n\n

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

\n\n

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

\n\n

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

\n\n
\n

如果您有 10,000 个前端用户,那么拥有 10,000 个连接池将是疯狂的。1000还是太恐怖了 即使是 100 个连接,也太过分了。您需要一个最多包含几十个连接的小型池,并且希望其余应用程序线程阻塞在池中等待连接。

\n
\n\n

第二种解决方案

\n\n

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

\n\n

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

\n\n

您是否更清楚如何使用 Spring、Hibernate、Mysql(托管在 AWS RDS Aurora 上)管理此场景?

\n

Sid*_*ani 5

毫无疑问,为每个租户打开连接是一个非常糟糕的主意。您所需要的只是一个在所有用户之间共享的连接池。

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

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

  3. 最后确定所需的连接数并决定所需的实例数。例如,如果您的峰值时间使用率为每秒 10k,并且每个查询需要 10 毫秒,那么您将需要 100 个打开的连接来实现 1 秒的延迟。

  4. 实现它时无需对用户进行任何绑定。即所有人共享同一个池。除非你有一个案例来分组高级/基本用户说有一组两个池等

  5. 最后,当您在 AWS 中执行此操作时,如果您根据第 3 点需要超过 1 个实例,请查看是否可以根据负载自动向上/向下扩展以节省成本。

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

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

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