在定期更改数据库密码时实现连接重建机制

Implementing a connection recreation mechanism on periodic DB password change

我们正在使用 PostgreSQL 数据库 AWS RDS IAM authorization feature – which means that our application needs to refresh the authorization token every 10 minutes or so (since the token is valid for 15 minutes). This token is used as a database password and I need to periodically update it. We are using the Dropwizard framework which is taking advantage of Apache Commons DBCP Component 来处理连接池。

我能够增强配置 class 以便它执行 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

这些异常每分钟重复一次。

这个我欠你们一个解释。我忘了提到一个重要的细节——我们实际上使用的是内部开发的 Dropwizard 的修改版本,它使用捆绑的 Apache Commons DBCP(afaik 不是 Dropwizard 的正式部分)以及其他组件。我最终放弃了 Apache Commons DBCP,转而使用 HikariCP - which made it possible to update the pool configuration at runtime. Although not officially supported, the creator of the library hinted that it might work,在我们的场景中它确实有效。下面是一个示例解决方案。

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();
            }
        }
    }
}

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