首页 > 解决方案 > 在定期更改数据库密码时实现连接重建机制

问题描述

我们正在使用具有AWS RDS IAM 授权功能的 PostgreSQL 数据库——这意味着我们的应用程序需要每 10 分钟左右刷新一次授权令牌(因为令牌的有效期为 15 分钟)。此令牌用作数据库密码,我需要定期更新它。我们正在使用 Dropwizard 框架,该框架利用了处理连接池的Apache Commons DBCP 组件。

我能够增强配置类,以便它执行 AWS API 调用来获取令牌,而不是从配置文件中读取密码。但是,这仅在应用程序启动期间工作一次,持续 15 分钟。我想定期为令牌调用 AWS API 并处理连接的创建以及使旧连接无效。

import org.jooq.Configuration;
import org.jooq.impl.DefaultConfiguration;
import io.dropwizard.setup.Environment;
import org.example.myapp.ApplicationConfiguration;
// more less relevant imports...

@Override
public void run(ApplicationConfiguration configuration, Environment environment) {
    Configuration postgresConfiguration = new DefaultConfiguration().set(configuration.getDbcp2Configuration()
                                                                                      .getDataSource())
                                                                    .set(SQLDialect.POSTGRES_10)
                                                                    .set(new Settings().withExecuteWithOptimisticLocking(true));

    // this DSLContext object needs to be refreshed/recreated every 10 minutes with the new password!
    KeysDAO.initialize(DSL.using(postgresConfiguration));

    // rest of the app's config
}

我怎样才能实现这样的连接重建机制?org.jooq.ConnectionProvider 看起来很有希望,但我需要更多关于如何定期注入密码的指导(并实现自定义 ConnectionProvider)。任何提示将不胜感激。

编辑:今天早上,我能够确认在重新部署后数据库交互是可能的,并且恰好在 15 分钟后,我得到了第一个例外:

org.postgresql.util.PSQLException: FATAL: PAM authentication failed for user "jikg_service"
    at org.postgresql.core.v3.ConnectionFactoryImpl.doAuthentication(ConnectionFactoryImpl.java:514)
    at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:141)
    at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:192)
    at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:49)
    at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:195)
    at org.postgresql.Driver.makeConnection(Driver.java:454)
    at org.postgresql.Driver.connect(Driver.java:256)
    at org.apache.commons.dbcp2.DriverConnectionFactory.createConnection(DriverConnectionFactory.java:39)
    at org.apache.commons.dbcp2.PoolableConnectionFactory.makeObject(PoolableConnectionFactory.java:256)
    at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:868)
    at org.apache.commons.pool2.impl.GenericObjectPool.ensureIdle(GenericObjectPool.java:927)
    at org.apache.commons.pool2.impl.GenericObjectPool.ensureMinIdle(GenericObjectPool.java:906)
    at org.apache.commons.pool2.impl.BaseGenericObjectPool$Evictor.run(BaseGenericObjectPool.java:1046)
    at java.base/java.util.TimerThread.mainLoop(Timer.java:556)
    at java.base/java.util.TimerThread.run(Timer.java:506)
    Suppressed: org.postgresql.util.PSQLException: FATAL: pg_hba.conf rejects connection for host "172.30.19.218", user "my_db_user", database "my_db_development", SSL off
        at org.postgresql.core.v3.ConnectionFactoryImpl.doAuthentication(ConnectionFactoryImpl.java:514)
        at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:141)
        at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:201)
        ... 12 common frames omitted

这些异常每分钟都会重复。

标签: postgresqlamazon-web-servicesdropwizardjooqapache-commons-dbcp

解决方案


我欠大家一个解释。我忘了提到一个重要的细节——我们实际上使用的是内部开发的 Dropwizard 的修改版本,它使用捆绑的 Apache Commons DBCP(afaik 不是 Dropwizard 的正式一部分)以及其他组件。我最终放弃了 Apache Commons DBCP 以支持HikariCP——这使得在运行时更新池配置成为可能。虽然没有得到官方支持,但该库的创建者暗示它可能会起作用,并且在我们的场景中它确实有效。下面是一个示例解决方案。

import org.jooq.Configuration;
import org.jooq.impl.DefaultConfiguration;
import io.dropwizard.setup.Environment;
import org.example.myapp.ApplicationConfiguration;
// more less relevant imports...

@Override
public void run(ApplicationConfiguration configuration, Environment environment) {

    HikariDataSource hikariDataSource = loadDatabaseConfiguration(configuration.getDatabaseConfiguration());
    new DbConfigurationLoader(hikariDataSource).start();
    // this DSLContext object now has the reference to DataSource object that has an always-fresh password!
    KeysDAO.initialize(DSL.using(hikariDataSource, SQLDialect.POSTGRES_10, new Settings().withExecuteWithOptimisticLocking(true)));

    // rest of the app's config
}

private HikariDataSource loadDatabaseConfiguration(DatabaseConfiguration configuration) {
    HikariDataSource hikariDataSource = new HikariDataSource();
    hikariDataSource.setJdbcUrl(configuration.getJdbcUrl());
    hikariDataSource.setDriverClassName(configuration.getDriverClassName());
    hikariDataSource.setMinimumIdle(configuration.getMinimumIdle());
    hikariDataSource.setMaximumPoolSize(configuration.getMaximumPoolSize());
    hikariDataSource.setUsername(configuration.getJdbcUser());
    return hikariDataSource;
}

private class DbConfigurationLoader extends Thread {
    private final HikariDataSource hikariDataSource;
    private final RdsTokenProvider rdsTokenProvider;

    public DbConfigurationLoader(HikariDataSource hikariDataSource) {
        this.rdsTokenProvider = new RdsTokenProvider();
        this.hikariDataSource = hikariDataSource;
    }

    @Override
    public void run() {
        while (true) {
            hikariDataSource.setPassword(rdsTokenProvider.getToken());
            try {
                Thread.sleep(/* token is valid for 15 minutes, so it makes sense to refresh it more often */);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

希望这可以在将来节省一些时间。


推荐阅读