我正在使用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上)管理这种情况有更好的了解吗?
答案 0 :(得分:3)
大多数情况下,每个租户打开连接绝对不是一个好主意。您所需要做的就是在所有用户之间共享一个连接池。
因此第一步将是找到负载或根据一些预测来预测负载。
确定可接受的延迟时间是多少,突发峰值时间流量如何等
最后,您将需要此连接数,并确定所需的实例数。例如,如果您的高峰时间使用率为10k / s,每个查询需要10ms,那么您将需要100个打开的连接,等待时间为1s。
实施时不绑定任何用户。即同一共享池在所有人之间共享。除非您有一个案例将高级/基本用户归为一组,例如说有两个池,等等。
最后,如果您需要基于第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