写点什么

解 Bug 之路 - 主从切换”未成功”?

发布于: 2021 年 04 月 08 日
解Bug之路-主从切换”未成功”?

解 Bug 之路-主从切换”未成功”?

前言

数据库主从切换是个非常有意思的话题。能够稳定的处理主从切换是保证业务连续性的必要条件。今天笔者就来讲讲主从切换过程中一个小小的问题。

故障场景

最近线上进行主从切换,大部分应用都切过去了,但是某些应用的连接确还在老的主(新的从)上面。

这让对应应用的开发百思不得其解,于是求助了笔者一探究竟。

怎么发现的

应用开发收到 Cat 监控告警,发现这个应用(A)中的请求在好几台机器中一直稳定失败。联想到昨晚刚做过数据库主从切换演练,于是上机器 netstat -anp 下,发现机器一直连的是旧的从库!

netstat -anp | grep 1521tcp 0 0 1.2.3.4:54100 1.1.1.1:1521 ESTABLISHED
复制代码

开发感觉肯定是主从没有切换过去导致请求失败。乍一看,好像非常有道理的样子。

着手调查

神马情况?距离切换成功已经 8 个小时了,为什么连接还连在上面呢?于是笔者 ping 了下对应数据库的域名:

ping db.prd64byres from db.prd (2.2.2.2): icmp_seq=1 ttl=64 time=0.02ms
复制代码


好奇怪,DNS 已经切换过去了。应用怎么还连到老库呢?

第一个猜想,DNS 延迟

最先想到的是主从切换到 DNS 反应过来有延迟。例如主从切换完,DNS 在 2min 后才能生效,所以在此期间新建的连接还是到从库。

这种情况很正常,对于这种情况需要 DBA 将旧主的连接全都杀掉即可。咨询了下 DBA,他们反馈他们已经把连接全部杀掉了。而且当场给我看了下数据库的统计连接 SQL,确实没有对应机器的连接。这就奇怪了,应用机器上的连接是 ESTABLISHED 状态啊!

应用大部分机器都连的是老库!

这时候,开发向笔者反应,这个应用对应的大部分机器都是连的老库!如果是 DNS 延迟,不可能这么巧吧,40 多台呢!

而且这些机器的 DNS 都是指向新库的。

DB 没有 kill 连接?

难道是 DBA 漏了 kill 连接的步骤?但是和他和我展示的 DB 统计信息矛盾啊。于是笔者让 DBA 在对应老库的机器上 netstat 了一把。发现,连接还真的存在!

netstat -anp | grep 1.2.3.4tcp 0 0 1.1.1.1:1521 1.2.3.4:54100 ESTABLISHED
复制代码

难道统计信息真的有问题?


获取连接创建时间

为了验证笔者对于 DNS 延迟的猜想,就通过一些技巧来获取这个连接的创建时间。首先 netstat -anp | grep 1.2.3.4 找出来这个连接。由于明显是属于应用 java 进程的,所以直接找到进程 pid:8299

netstat -anp | grep 1521tcp 0 0 1.2.3.4:54100 1.1.1.1:1521 ESTABLISHED
netstat -anp | grep javaabc 8299 java
复制代码

既然有了进程 pid,我们直接 cat /proc/8299/net/tcp,直接获取到其所有的连接信息,然后在其中 grep 1521 的 16 进制 05F1(当前机器上 1521 的连接只有一个)

...... local_address rem_address inode     ............ xxx:D345      xxx:05F1    23456789  ......
复制代码

找到这个 socket(1.2.3.4:54100<->1.1.1.1:1521)对应的 inode 号。有了这个 inode 号就简单了,我们直接

ls -all -h /proc/8299/fd | grep 23456789 (inode号)...... Jan 29 17:43 222 -> socket:[23456789]
复制代码

这么一看,这个连接是 1 月 29 日创建的。但是主从切换的时间点确是 3 月 19 日,这个连接已经建了 2 个月了!那么就不可能是笔者所说的 DNS 失效问题了。因为连接就没有重连过。

DB 都重启了,怎么还有旧的连接保持?

看到这个连接创建时间,笔者第一反应,DBA 确定杀连接了吗?问了下 DBA 有没有可能是统计问题。DBA 听了后,告诉笔者,他们都重启过数据库了,怎么可能还有连接存在呢?看了下 DB 进程的创建时间。

ps -eo lstart,cmd | grep db进程名Mar 19 17:52:32 2021 db进程名
复制代码

从进程启动时间来看,真的是在 3 月 19 日启动的。而这个诡异的连接还确实属于这个 3 月 19 日启动的进程。这个怎么看逻辑上都不通啊。

但是,既然 linux 的统计信息在这(还是要先暂时认为是靠谱的),那肯定是又有什么其它的诡异逻辑在里面了。

子进程继承了父进程的连接

稍微思考了一会,笔者就找到了一种可能。父进程先新建了连接进行处理,在创建子进程 fork 的时候,子进程会继承父进程的连接,这时候父进程退出,只保留子进程的话。就会出现连接在进程启动之前就已经存在的诡异现象。

为了验证这个问题,笔者自己写了段简单的 C 程序,执行了一下确实如此。代码例子为:

main.c......int main(int argc,char* argv[]){    ......    if((client_fd = accept(sockfd,(struct sockaddr*)&remote_addr,&sin_size)) == -1){        printf("accept error!\n");    }    printf("Received a connection \n");    // 制造两分钟延迟,以造成上面的现象    sleep(2 * 60);    if(!fork()){        // 子进程保持        while(1){           sleep(100000);        }    }else{        // 父进程关闭连接        close(client_fd);    }    return 0;}
复制代码

问了下 DBA,他们不会 kill -9 所有进程,都是按照标准的数据库重启流程来操作的(kill -9 所有进程的同时会关闭这些进程所拥有的连接,但这么暴力的操作明显不敢用在 DB 上)如果我们使用的商业数据库用了上图的机制,那就会造成前面的现象。但是由于 DB 本身保持的 session 都已经没了,那么这个连接在数据库维度肯定是已经 gg 了(这也是数据库统计不出来的原因)。既然还保留在上面,这个连接肯定再也没有处理过请求!不然肯定出错了。


业务代码逻辑

如果按照上面的论断的话,那么没有执行过请求,也就不会有报错喽?如果按照这个逻辑的话,那岂不是只有出现业务报错的才会有新的正常连接。笔者去报错的机器看了下,既然报错了,那肯定是执行过 SQL 了,然后触发 Druid 丢弃连接再新建连接。果然,一直报错的机器上连接都连到新库了(但应用开发发现其它机器还是连到老库,所以找到了我求助),而且创建时间是 3 月 29 日,而不报错的应用的连接挂在老库上面,挑了几台看一下,这些挂在老库的连接依旧是 1 月 29 日创建的。

但为什么还在报错?

既然连接都正常了(到新库了),为何还在报错呢?难道说业务代码写的有问题,一旦报错,就永远错下去?于是笔者直接翻起了应用的源码。其使用这个数据库的连接用来获取(sequence)序列号。然后细细分析了源码后发现。其在数据库报错之后没有处理好,走了一个有问题的代码分支,导致永远不会再从数据库获取 sequence(业务代码就不放上来了)。

为什么只有几台机器报错?

因为这个序列号是取一段很大的范围到机器的内存中使用的,不耗尽之前不会执行 SQL。所以只有一些内存中序列耗尽的机器才会运行到那一段有问题的代码分支。


为什么心跳没有检测出来?

到这里大家可能会疑问?没有心跳检测么?确实没有,应用采用的是 Druid 数据源,而他们使用的那个版本的 Druid 是没有定时心跳检测的。

主从切换到底有没有成功呢?

主从切换当然是成功的。这从其它的应用切过去之后运行良好可以判断出来。主从切换当中的数据库流量损失是我们可预期的正常现象。但是,数据库切换完之后,应用确恢复不回来,那就要仔细看看应用代码本身有什么问题了。

总结

数据库主从切换是个频繁而又重要的动作,是保证业务连续性的必要条件。这不仅要看 DBA 的努力,还需要我们在应用层写出健壮的代码,才能够让我们的产线更加稳定。


发布于: 2021 年 04 月 08 日阅读数: 33
用户头像

公众号: <<解Bug之路>> 2019.02.11 加入

无论多么艰苦的时刻,都不要忘记。辉煌的未来,在你的眼中闪耀!

评论 (1 条评论)

发布
用户头像
自己顶
2021 年 04 月 08 日 17:40
回复
没有更多了
解Bug之路-主从切换”未成功”?