当前位置: 首页 > 知识库问答 >
问题:

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

经骁
2023-03-14

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

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

我以这种方式创建了我的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个租户,我想认为带有Aurora的db.r4.large(2vcore,15,25GB RAM和快速磁盘访问)应该足够了(大约150€/月)。

根据计算连接池大小的公式:

connections = ((core_count * 2) + effective_spindle_count)

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

据我所知,这应该是池中的最大连接数,以最大限度地提高数据库实例的性能。

第一种解决方案

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

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

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

如果您有10,000个前端用户,那么拥有10,000个连接池将是剪切疯狂。1000仍然很可怕。即使是100个连接,也太过分了。您最多想要一个只有几十个连接的小池子,并且您希望其余的应用程序线程被阻塞在池子上等待连接。

第二种解决方案

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

这是最大限度地提高性能和缩短应用程序响应时间的正确方法吗?

你对如何使用Spring、Hibernate、Mysql(托管在AWS RDS Aurora上)管理这个场景有更好的了解吗?

共有2个答案

苏彭薄
2023-03-14

遵循前面的问题

策略2:每个租户在单个数据库中都有自己的模式和自己的连接池

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

我建议把HikariCP的公式放在一边,用更少的租户数量作为10(动态大小?)低连接池大小为2。

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

10是个不错的整数。看起来很低?试一试,我们敢打赌,在这样的设置下,你可以轻松处理3000名前端用户,以6000 TPS的速度运行简单的查询。

另请参阅注释,说明100个实例太多

,但这必须是一个巨大的负载,需要100秒。

通过@EssexBoy

南宫浩皛
2023-03-14

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

>

  • 因此,第一步是根据一些预测找到负荷或预测负荷。

    确定可接受的延迟时间、突发峰值时间流量等

    最后来看看你需要多少连接,并决定需要多少实例。例如,如果您的峰值时间使用率为每秒10k,并且每个查询需要10ms,那么您将需要100个打开的连接,等待1s。

    实现它时不需要对用户进行任何绑定。i、 e.所有人共享同一个池。除非你有一个案例可以分组,比如说高级/基本用户可以说有一套两个池等等

    最后,在AWS中执行此操作时,如果您需要基于第3点的多个实例,请查看是否可以根据负载自动缩放以节省成本。

    看看这些比较指标

    从需求激增的角度来看,这一次可能是最有趣的

    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

  •  类似资料:
    • 我们正在尝试使用nodejs/mongo原生驱动程序实现下面演示(幻灯片13-18)中概述的策略。 https://www.slideshare.net/mongodb/securing-mongodb-to-serve-an-awsbased-multitenant-securityfanatic-saas-application 总结: 从node.js.创建到mongoDB的连接池 对于租户

    • 我正在尝试设置一个多租户web应用程序,在理想情况下,可以同时使用数据库分离和模式分离的方法。虽然我将从模式分离开始。我们目前正在使用: Spring 4.0。0 冬眠4.2。8 Hibernate-c3p04.2。8(使用c3p0-0.9.2.1) 以及PostgreSQL 9.3(我怀疑它对整个体系结构是否真的重要) 大多数时候,我遵循这个线程(因为的解决方案)。但是我有点迷失在实现中。在SO

    • 有人知道我们是否可以增加1000s用户池的限制吗?

    • 问题内容: 我正在尝试建立一个多租户的Web应用程序,同时(理想情况下)可以同时使用数据库分隔和模式分隔的方法。尽管我将从模式分离开始。我们目前正在使用: Spring 4.0.0 Hibernate 4.2.8 Hibernate-c3p0 4.2.8 (which uses c3p0-0.9.2.1) and PostgreSQL 9.3 (which I doubt it really ma

    • 我们正在开发一个多租户应用程序。在体系结构方面,我们为业务逻辑设计了共享中间层,为数据持久性设计了每个租户一个数据库。也就是说,业务层将与每个租户的数据库服务器建立一组连接(连接池)。这意味着应用程序为每个租户维护单独的连接池。如果我们预计约有5000个租户,那么这个解决方案需要高资源利用率(每个租户的应用服务器和数据库服务器之间的连接),这会导致性能问题。 我们已经通过保持公共连接池解决了这个问

    • 我正在构建一个应用程序,它应该服务于多个租户,并存储他们非常敏感的数据。每个租户都有多个用户。我的后端堆栈是用spring boot Hibernate5构建的。我希望hibernate处理多租户问题,所以我有两个问题要问专家: 如果我使用的是每租户模式范式,如何在登录时将用户名与租户关联?每个租户都有自己的用户表,那么我如何知道用户名属于哪个租户呢? 在注册时,什么是正确的方法来动态创建新模式?