JDBC连接池测试查询"SELECT 1"未捕获AWS RDS Writer/Reader故障转移

Ber*_*enz 7 java datasource connection-pooling amazon-web-services amazon-rds

我们在集群中运行AWS RDS Aurora/MySQL数据库,其中包含编写器和读取器实例,其中编写器被复制到读取器.

访问数据库的应用程序是使用HikariCP连接池的标准Java应用程序.池配置为"SELECT 1"在结帐时使用测试查询.

我们注意到的是,RDS偶尔将编写器故障转移给读者.也可以通过单击AWS控制台中的"实例操作/故障转移"手动复制故障转移.

连接池无法检测到故障转移以及它现在已连接到读取器数据库的事实,因为"SELECT 1"测试查询仍然成功.但是,任何后续数据库更新都会因"java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement"

似乎"SELECT 1"连接池不是测试查询,而是通过使用测试查询检测到它现在已连接到阅读器"SELECT count(1) FROM test_table WHERE 1 = 2 FOR UPDATE".

  1. 有没有人遇到过同样的问题?
  2. "FOR UPDATE"在测试查询中使用是否有任何缺点?
  3. 是否有任何替代或更好的方法来处理AWS RDS集群编写器/读取器故障转移?

非常感谢您的帮助

伯尼

kdg*_*ory 8

自原始回复以来的两个月中,我一直在思考这个问题。


Aurora端点如何工作

启动Aurora群集时,您会获得多个主机名来访问该群集。出于此答案的目的,我们只关心两个是读写的“集群端点”和(您猜对了)只读的“只读端点”。您在集群中的每个节点上都有一个端点,但是访问节点直接破坏了使用Aurora的目的,因此我不再赘述。

例如,如果创建一个名为“ example”的集群,则将获得以下端点:

  • 集群端点: example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
  • 只读端点: example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

您可能会认为这些端点将引用诸如Elastic Load Balancer之类的东西,它足够聪明,可以在故障转移时重定向流量,但是您错了。实际上,它们只是DNS CNAME条目,而且存活时间很短:

dig example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com


; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40120
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-18-209-198-76.compute-1.amazonaws.com.
ec2-18-209-198-76.compute-1.amazonaws.com. 7199 IN A 18.209.198.76

;; Query time: 54 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:12:08 EST 2018
;; MSG SIZE  rcvd: 178
Run Code Online (Sandbox Code Playgroud)

发生故障转移时,CNAME会更新(从exampleexample-us-east-1a):

; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27191
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-3-81-195-23.compute-1.amazonaws.com.
ec2-3-81-195-23.compute-1.amazonaws.com. 7199 IN A 3.81.195.23

;; Query time: 158 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:15:33 EST 2018
;; MSG SIZE  rcvd: 187
Run Code Online (Sandbox Code Playgroud)

故障转移期间发生的另一件事是,与“集群”端点的所有连接都被关闭,这将使任何正在进行的事务都失败(假设您已设置了合理的查询超时)。

与“只读”端点的连接不会关闭,这意味着,提升了任何节点的权限,除只读流量外,还将获得读写流量(当然,假设您的应用程序不只是发送对集群端点的所有请求)。由于只读连接通常用于相对昂贵的查询(例如,报告),因此这可能会导致您的读写操作出现性能问题。

问题:DNS缓存

发生故障转移时,所有进行中的事务都会失败(同样,假设您已设置查询超时)。由于连接池在完成恢复之前会尝试连接到同一主机,因此所有新连接也将在短时间内失败。以我的经验,故障转移大约需要15秒钟,在此期间,您的应用程序不应期望获得连接。

15秒(或大约15秒)之后,一切都会恢复正常:您的连接池尝试连接到群集端点,它解析为新的读写节点的IP地址,一切正常。但是,如果有什么阻止解决该CNAME链的问题,您可能会发现您的连接池建立了到只读终结点的连接,一旦尝试更新操作,该终结点就会失败。

对于OP,他拥有自己的CNAME,超时时间更长。因此,与其直接连接到集群端点,不如直接连接到database.example.com。在您将手动故障转移到副本数据库的世界中,这是一种有用的技术。我怀疑它对Aurora没什么用。无论如何,如果您使用自己的CNAME来引用数据库端点,则需要它们具有短的生存时间值(一定不超过5秒)。

在我最初的回答中,我还指出Java在某些情况下会永久缓存DNS查找。该缓存的行为取决于(我相信)Java的版本,以及是否在安装安全管理器的情况下运行。使用OpenJDK 8作为应用程序运行时,JVM似乎将委派所有命名查找,而不是自己缓存任何内容。但是,您应该熟悉此Oracle文档此SO问题中networkaddress.cache.ttl所述的系统属性。

但是,即使您消除了任何意外的缓存,在某些情况下仍可能会将集群端点解析为只读节点。剩下的问题是如何处理这种情况。

不太好的解决方案:在结帐时使用只读测试

OP希望使用数据库连接测试来验证其应用程序是否在只读节点上运行。这很难做到:大多数连接池(包括OP正在使用的HikariCP)只是简单地验证测试查询是否成功执行;无法查看返回的结果。这意味着任何测试查询都必须抛出异常才能失败。

我还没有想出一种方法可以使MySQL仅通过独立查询引发异常。我想出的最好的办法是创建一个函数:

DELIMITER EOF

CREATE FUNCTION throwIfReadOnly() RETURNS INTEGER
BEGIN
    IF @@innodb_read_only THEN
        SIGNAL SQLSTATE 'ERR0R' SET MESSAGE_TEXT = 'database is read_only';
    END IF;
    RETURN 0;
END;
EOF

DELIMITER ;
Run Code Online (Sandbox Code Playgroud)

然后在测试查询中调用该函数:

select throwIfReadOnly() 
Run Code Online (Sandbox Code Playgroud)

大多数情况下,这有效。在运行测试程序时,我会看到一系列“无法验证连接”消息,但是,莫名其妙地,更新查询将以只读连接运行。Hikari没有调试消息来指示它发出的连接,因此我无法确定它是否已通过验证。

但是除了可能出现的问题外,此实现还有一个更深层次的问题:它掩盖了存在问题的事实。用户发出请求,可能要等待30秒才能获得响应。日志中没有任何内容(除非您启用了Hikari的调试日志记录),没有给出造成这种延迟的原因。

而且,虽然数据库不可访问,但是Hikari却疯狂地尝试建立连接:在我的单线程测试中,它将每100毫秒尝试一次新的连接。这些是真正的联系,它们只是去了错误的主机。放入具有几十个或一百个线程的应用服务器中,这可能会对数据库造成严重的连锁反应。

更好的解决方案:通过包装器在结帐时使用只读测试 Datasource

与其让Hikari静静地重试连接,不如将其包装HikariDataSource在自己的DataSource实现中并自己测试/重试。这样的好处是,您实际上可以查看测试查询的结果,这意味着您可以使用独立的查询,而不用调用单独安装的函数。它还使您可以使用首选的日志级别记录问题,让您在两次尝试之间暂停,并有机会更改池配置。

private static class WrappedDataSource
implements DataSource
{
    private HikariDataSource delegate;

    public WrappedDataSource(HikariDataSource delegate) {
        this.delegate = delegate;
    }

    @Override
    public Connection getConnection() throws SQLException {
        while (true) {
            Connection cxt = delegate.getConnection();
            try (Statement stmt = cxt.createStatement()) {
                try (ResultSet rslt = stmt.executeQuery("select @@innodb_read_only")) {
                    if (rslt.next() && ! rslt.getBoolean(1)) {
                        return cxt;
                    }
                }
            }
            // evict connection so that we won't get it again
            // should also log here
            delegate.evictConnection(cxt);
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException ignored) {
                // if we're interrupted we just retry
            }
        }
    }

    // all other methods can just delegate to HikariDataSource
Run Code Online (Sandbox Code Playgroud)

该解决方案仍然遭受将延迟引入用户请求的问题。的确,您知道它正在发生(您没有通过on-checkout测试),并且您可以引入超时(限制通过循环的次数)。但是它仍然表示不良的用户体验。

最佳(imo)解决方案:切换到“维护模式”

用户非常不耐烦:如果要花几秒钟以上的时间才能得到答复,他们可能会尝试重新加载页面,或再次提交表单,或者执行无济于事并且可能会造成伤害的事情

因此,我认为最好的解决方案是迅速失败,并让他们知道某些错误。在调用堆栈顶部附近的某个位置,您应该已经有一些响应异常的代码。也许您现在只返回了一个通用的500页,但是您可以做得更好:查看异常,如果是只读数据库异常,则返回“抱歉,暂时不可用,请在几分钟后重试”页面。

同时,您应该向操作人员发送通知:这可能是正常的维护窗口故障转移,或者可能是更严重的事情(但是除非您有某种方式知道更严重的情况,否则请不要唤醒它们) )。

  • 我遇到了[这篇文章](https://www.yenlo.com/blog/using-amazon-aurora-as-a-datasource-with-high-availability-in-yenlos-wso2-cloud-solution-connext)建议使用以下 SQL 语句来检查连接是否属于只读端点:``Select case when @@read_only + @@innodb_read_only = 0 then 1 else (select table_name from information_schema.tables) end as `1` `` 这个想法是导致错误,因为返回不止一行是出乎意料的。 (2认同)