一、背景
近期测试反馈关于系统可靠性测试提出的问题令人感到头疼,一来这类问题有时候属于“偶发”现象,难以在环境上快速复现;二来则是可靠性问题的定位链条有时候变得很长,极端情况下可能要从A服务追踪到Z服务,或者是从应用代码追溯到硬件层面。
架构
首先,系统以MySQL作为主要的数据存储部件。整一个是典型的微服务架构(SpringBoot+SpringCloud),持久层则采用了如下几个组件:
mybatis,实现SQL-Method的映射hikaricp,实现数据库连接池mariadb-java-client,实现JDBC驱动在MySQL服务端部分,后端采用了双主架构,前端以keepalived结合浮动IP(VIP)做一层高可用。如下:
说明
MySQL部署两台实例,设定为互为主备的关系。为每台MySQL实例部署一个keepalived进程,由keepalived提供VIP高可用的故障切换。实际上,keepalived和MySQL都实现了容器化,而VIP端口则映射到VM上的nodePort服务端口上。
业务服务一律使用VIP进行数据库访问。Keepalived是基于VRRP协议实现了路由层转换的,在同一时刻,VIP只会指向其中的一个虚拟机(master)。当主节点发生故障时,其他的keepalived会检测到问题并重新选举出新的master,此后VIP将切换到另一个可用的MySQL实例节点上。这样一来,MySQL数据库就拥有了基础的高可用能力。
另外一点,Keepalived还会对MySQL实例进行定时的健康检查,一旦发现MySQL实例不可用会将自身进程杀死,进而再触发VIP的切换动作。
问题现象
持续以较小的压力向业务服务发起访问,随后将其中一台MySQL的容器实例(master)重启。按照原有的评估,业务可能会产生很小的抖动,但其中断时间应该保持在秒级。
然而经过多次的测试后发现,在重启MySQL主节点容器之后,有一定的概率会出现业务却再也无法访问的情况!
二、分析过程
在发生问题之后,第一反应是MySQL的高可用机制出了问题。由于此前曾经出现过由于keepalived配置不当导致VIP未能及时切换的问题,因此对其已经有所戒备。
先是经过一通的排查,然后并没有找到keepalived任何配置上的毛病。
然后在没有办法的情况下,重新测试了几次,问题又复现了。
紧接着,我们提出了几个疑点:
1.Keepalived会根据MySQL实例的可达性进行判断,会不会是健康检查出了问题?
但在本次测试场景中,MySQL容器销毁会导致keepalived的端口探测产生失败,这同样会导致keepalived失效。如果keepalived也发生了中止,那么VIP应该能自动发生抢占。而通过对比两台虚拟机节点的信息后,发现VIP的确发生了切换。
2.业务进程所在的容器是否发生了网络不可达的问题?
尝试进入容器,对当前发生切换后的浮动IP、端口执行telnet测试,发现仍然能访问成功。
陷入焦灼
在代码分析之余,也注意到当前使用的hikariCP版本为3.4.5,而环境上出问题的业务服务却是2.7.9版本,这仿佛预示着什么…让我们再次假设hikariCP2.7.9版本存在某种未知的BUG,导致了问题的产生。
为了进一步分析连接池对于服务端故障的行为处理,我们尝试在本地机器上进行模拟,这一次使用了hikariCP2.7.9版本进行测试,并同时将hikariCP的日志级别设置为DEBUG。
模拟场景中,会由由本地应用程序连接本机的MySQL数据库进行操作,步骤如下:
1.初始化数据源,此时连接池min-idle设置为10;
2.每隔50ms执行一次SQL操作,查询当前的元数据表;
3.将MySQL服务停止一段时间,观察业务表现;
4.将MySQL服务重新启动,观察业务表现。
三、解决方案
在了解了事情的来龙去脉之后,我们主要考虑从两方面进行优化:
优化一,增加HirakiPool中AddConnectionExecutor线程的数量,这样即使第一个线程出现挂死,还有其他的线程能参与建链任务的分配。优化二,出问题的socketRead是一种同步阻塞式的调用,可通过SO_TIMEOUT来避免长时间挂死。对于优化点一,我们一致认为用处并不大,如果连接出现了挂死那么相当于线程资源已经泄露,对服务后续的稳定运行十分不利,而且hikariCP在这里也已经将其写死了。因此关键的方案还是避免阻塞式的调用。