Thoughts, stories and ideas.

事故带来的技术思考

事故说明

2016 年 3 月 4 日下午 14 点 46 左右,BearyChat 服务器突然无法连接,很快就处于不可用状态,30 分钟之后服务恢复,具体的时间点:

14:46: 线上在线数开始急剧下降,聊天服务器异常报告增多(此时越来越多的用户处于无法连接的状态)

14:50: 通过日志发现聊天服务器触发了一个概率极小的 Bug,导致当前节点启动了自恢复流程,主动断开了用户连接

14:55: 大概经过 5 分钟的恢复,有部分用户已经可以连上,这个时候 RDS 集群发出了报警,CPU 超过阈值

14:55: 在监控看到,当前的 QPS 达到了之前峰值的 20 倍,而且大部分请求直接到达了 RDS,导致雪崩

15:00: 工程师提交聊天服务器修复 Patch,同时断开所有流量,提高 RDS 配置

15:14: RDS 恢复完毕,所有服务启动完毕

15:15: 在线数逐步恢复,BearyChat 服务逐渐恢复

15:23: 监控到所有请求和服务都已正常

从经过看起来,那个概率极小的 Bug 是始作俑者,但其实后面的问题更应该被揭露,首先说一下这个 Bug 引起的第一个反应:聊天服务器节点自恢复流程

Erlang Supervisor

BearyChat 的聊天服务器是 Erlang 实现的,Erlang 的一个特点是 fault-tolerance,它倡导 「Let it crash」。不熟悉 Erlang 的朋友可能觉得这个哲学很吓人,其实并不是那样的,在 OTP 框架下,通常一个基于 Erlang 的系统,都是由 Supervisor 和 Worker 进程组成的一棵监督树,一个典型的实例图如下:

Supervisor 不但负责启动子进程,同时会监控子进程的运行状态。那 Supervisor 怎么做到「Let it crash」呢?Supervisor 启动的有一个 strategy 参数,它决定了这个 Supervisor 的几种工作模式:

  • one_for_one: 如果 ASupervisor 发现其中 W1 停止运行了,它会自动重启 W1。
  • one_for_all: 如果 ASupervisor 发现其中 W1 停止运行了,那它会让 W2 和 W3 也停止运行,然后重启他们(W1~3)。
  • rest_for_one: 如果 ASupervisor 发现其中 W1 停止运行了,那它会让在 W1 之后启动的 Worker 停止运行,然后重启他们。

嗯,通过上面,大家可能觉得有点似曾相识,也就是当年我们当「电脑修理工」时回答最多的问题:「电脑坏了?重启一下试试」。不过大家可能会发现如果选择了 one_for_all 或者 rest_for_one,在大部分场景里面都不适用,BearyChat 也是采用了 one_for_one 模式,但还有一个决定了 Worker 重启策略的参数 -- restart:

  • permanent: 停止的 Worker 总会重启
  • transient: 只有异常停止的 Worker 才会重启
  • temporary: Worker 不会重启

问题就出在这里,我们在一个不该选择 transient 的 Supervisor 上选择了 transient 策略,Worker 触发 Bug 之后就会不断重启,在短时间内超过了 Supervisor 的最大重启次数,会让该 Supervisor 的其他 Worker 都终止,并且导致 Supervisor 停止工作,引发到更上层的 Supervisor 活动,中断了用户连接。虽然聊天服务器本身在这个过程中最终恢复了正常的运行,但却因为主动中断了大量用户连接,导致其他系统的崩溃。我们依然觉得 Erlang/OTP 的 Let it crash 很强大,只是在使用的时候需要更仔细,尤其是要考虑到整套系统的 fault-tolerance 能力。

聊天服务的自恢复引发了故障的第二个环节:RDS 倒下了

服务雪崩

在聊天服务自恢复之后,大量的用户请求到达了 API 层面(20 倍平时的峰值流量),而且很多都越过了缓存, RDS 服务没有扛下来,最后不得已停止了所有服务,调整 RDS 集群的配置。这里问题更明显一点,需要做 流控,在完成并测试通过之前就先不说了。

Tl;Dr,这次事故踩了几个可能大部分朋友也会踩到的坑,所以也在这里分享一下,我们也会在今后规避这样的常见问题,改善服务稳定性,给大家提供一个靠谱的 BearyChat。(圣斗士之所以强大,是因为不会败在同一招下两次

Jenkins 自动化 BearyChat 开发流程

BearyChat 的开发流程

在一熊科技,我们非常重视代码质量。虽然我们目前还没有专职的测试,但不同项目的同学经常互相比较,谁的 UT 写的多,谁的 assert 多,个个都是 DevTester;同时,我们对每行代码都进行严格的 code review (这里要感谢 GitHub 给我们提供这么好的平台)。简单说说我们目前的开发流程:

1. 产品出需求文档
2. 开发实现
3. 开发自己写测试
4. 开发提 Pull Request
5. Reviewer 进行 code review
6. Reviewer 合并到 master
7. 上线到 Stage 环境,并进行验收 

在 code review 过程中,我们的 Reviewer 往往会把代码拉下来,自己跑一边测试,保证测试不挂。但是这种方式效率不够高,存在很多重复劳动(拉代码,跑测试),并且本地环境经常和线上环境有不同,很有可能本地跑的挺好的,放到线上就挂了。这时候我们就需要一个自动化工具来帮我们做这些事, Jenkins 可以很好的完成这个任务。

Jenkins

Jenkins CI 是目前使用最多的开源持续集成平台。BearyChat 主要用他来做两个事情:

  • 每个 Pull Request触发测试脚本,并且把测试结果发到 BearyChat 和 GitHub。
  • Pull Request merge 之后会触发自动部署,自动将最新的代码部署到 Stage 环境

所以我们需要将 Jenkins、GitHub 和 BearyChat 进行结合。

Jenkins + GitHub

  • 更新 Jenkins 插件源,系统管理 -> 管理插件 -> 高级 -> 立即获取(在右下角)。
  • 安装 GitHub pull request builder plugin 插件。
  • 重启 Jenkins。
  • 按照下图配置 Jenkins, 具体可以参考官方教程: 图1 图2 图3
  • 在 GitHub 上添加一个 Webhook, 如图: 图7

  • 通过上面的配置,你就应该可以试试提个 pr, 看看效果了,如果成功的话,应该能在 GitHub 上面看到如下图的效果:

UT 过了的样子: 图4

UT 挂了的样子: 图5


BearyChat + Jenkins

接下来就是将上面的 UT 结果第一时间发送到 BearyChat 里面,让大家看到了,配置非常简单:

  • 在 BearyChat 中添加一个 Jenkins 机器人
  • 按照里面的教程一步步设置就可以了。

直接上最后的效果图: 图6

然后要做到 Merge Pull Request 之后自动部署,就需要安装 Jenkins 的另外一个插件:GitHub Plugin, 配置可以参考官方文档

BearyChat 还集成了好多工具: Trello, Sentry 等等,会在以后的博文中一一介绍。