Я создаю мультитенантное серверное приложение REST с Spring 2.x, Hibernate 5.x, Spring Data REST, Mysql 5.7.Spring 2.x использует Hikari для пула соединений.
Я собираюсь использовать БД на подход каждого арендатора , поэтому у каждого арендатора будет своя собственная база данных.
Iсоздал свой 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 и получаю информацию об экземпляре базы данных из центральной системы управления.Я создаю новый пул для каждого арендатора и кэширую пул, чтобы не создавать его заново каждый раз.
Я прочитал этот интересный вопрос , но мой вопрос совершенно другой.Я думаю использовать AWS (как для экземпляра сервера, так и для экземпляра RDS db).
Давайте предположим конкретный сценарий, в котором у меня 100 арендаторов.Приложение представляет собой программное обеспечение для управления / продажи.Он будет использоваться только от агентов.Допустим, у каждого арендатора в среднем одновременно работают 3 агента.
С учетом этих цифр и в соответствии с этой статьей первое, что я понимаю, это то, что кажется труднымиметь пул для каждого арендатора .
Для 100 арендаторов я хотел бы думать, что db.r4.large (2vcore, 15,25GB RAM и быстрый дискдоступа) с Aurora должно быть достаточно (около 150 € / месяц).
В соответствии с формулой для определения размера пула соединений:
connections = ((core_count * 2) + effective_spindle_count)
У меня должно быть 2core * 2 + 1 = 5соединений в пуле.
Из того, что я получил, это должно быть максимальное количество соединений в пуле для максимизации производительности на этом экземпляре БД.
1-е решение
Итак, мой первый вопрос довольно прост: как мне создать отдельный пул соединений для каждого арендатора, который видел, что я должен использовать всего 5 соединений?
Мне кажется, это невозможно.Даже если я назначу 2 подключения каждому арендатору, у меня будет 200 подключений к СУБД !!
Согласно этому вопросу , в случае db.r4.large
у меня может быть максимум 1300 подключенийТаким образом, кажется, что экземпляр должен выдерживать нагрузку.Но согласно статье, о которой я упоминал ранее, кажется плохой практикой использовать сотни соединений с БД:
Если у вас есть 10 000 интерфейсных пользователей, наличие пула соединений в 10 000 будет безумным сдвигом.1000 все еще ужасно.Даже 100 соединений, перебор.Вам нужен небольшой пул, состоящий максимум из нескольких десятков соединений, и хотите, чтобы остальные потоки приложений были заблокированы в пуле в ожидании соединений.
2-е решение
Второе решение, которое я имею в виду, - это совместное использование пула соединений для арендаторов в одной и той же DMBS.Это означает, что все 100 арендаторов будут использовать один и тот же пул Hikari из 5 соединений (честно говоря, мне это кажется довольно низким).
Должен ли это быть верным способом максимизировать производительность и сократить время отклика приложения?
У вас есть лучшее представление о том, как управлять этим сценарием с помощью Spring, Hibernate, Mysql (размещенного на AWS RDS Aurora)?