继一怒之下我写出了 Vivian(详见“测试驱动开发 Nginx 配置”)之后。又在等待客户审批流程的时间里自己写了一个流量测试工具。
背景
客户的站点是通过 Wordpress 搭建的,这个应用放在一台 EC2 虚拟机上。奇葩的是,这个应用的 MySQL 数据库也在这台虚拟机上,之前做过一次 RDS 迁移,失败了,原因未知。看起来这个应用和数据库就像筷子兄弟一样,不离不弃,而且没有办法通过 AutoScaling Group 进行水平扩展。也就是说,所有的东西都在一台虚拟机上。
我所要做的,就是把这个架构重新变成可自动水平扩展且高可用高性能有缓存低消耗具备监控和更加安全且有版本控制并可以通过持续交付流水线来半自动部署的架构。你可以重新读一下上一句加粗文字的内容。没错,目前他们连版本控制都没有,所有的操作在服务器上通过 mv 之间 scp 进行。
很不巧的时候,这个“筷子兄弟”应用在上周开始,晚上随机的 Down 机,表现为数据库被删。但通过日志可以发现,是由于内存资源不足导致的 MySQL 数据引擎加载不了导致的。
由于需要做“筷子兄弟”拆分手术,目的是要把数据库和应用程序分开,并且需要进行一些服务的重启和拆分。这些操作中会导致停机时间,为了能够度量这个停机时间,便于做出更好的决策,客户希望在测试环境上能够通过模拟生产环境的工作状态来完成这个任务。我设计了方案,包括以下几点:
知道每一个可能引起停机的操作引起停机的时长。
测试 RDS 能带来多少的性能提升。
找出整个架构引起停机的根本问题。
在 500 个并发用户访问的情况下,会出现的性能拐点。
能够度量应用的资源损耗。
客户已经购买了 NewRelic 和 Flood.io (我在 17 期技术雷达里提交的条目,叉会腰。)但是 Flood.io 的账号分配需要一个额外的审批才可以使用,也就是说,我得等到第二天才能使用。
我想,也许 github 上会有这样的工具能够满足我这个简单的需求,搜了一圈,没有合适的。
于是,一怒之下,我用了大概两个小时的时间用 Python 编写了这样一个测试工具。
工具的设计
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
在计算机科学里,缓存失效和命名事物是两件最困难的事情。于是,为了纪念这个事情,一开始我用提出这个需求的客户的名字(Dave)来命名它,但可能不太好记忆。所以最后还是用 Wade (Web Application Downtime Estimation)作为这个工具的名字。它很简单,可以在https://github.com/wizardbyron/wade 找到。
如果我需要知道停机时长,我必须要先能够持续不断的发出 http 请求,并记录下相应 状态不是 200 OK 的返回。我并不希望应用是一个死循环,因此我需要能够加入时间控制。我期望用下面的这样的方式来使用:
wade -t 10 -u https://www.baidu.com
其中,-t 代表时间,10 代表持续分钟,-u 表示要测试的 url。我期望这个工具能够连续的帮我输出每次请求的时间和 HTTP 状态字。
例如:
[2018-07-05 22:30:57]status:200
[2018-07-05 22:31:08]status:200
[2018-07-05 22:31:15]status:200
这实际上是在构造一个命令行工具的 End-2-End 测试的设计,#我们可以用 BDD 的方式来构造命令行工具的 End-2-End 自动化测试#。这个需求其实很简单,我大概花了半个小时就完成了。
在实际应用中,你需要先运行这个程序,然后再执行那些可能引起停机的操作。于是我准备让它运行30分钟,因为这些操作会执行多久我并不清楚。
很好,经过测试,这些操作只会引起 5 秒左右的停机。
不过,我好像忘了一个重要的事情……
那就是这个应用是单进程的,也就是说,这个实际上和真实的场景相差很远。我需要能够有 500 个并发 HTTP 请求,于是我把它改造成了多进程的。我期望用下面的这样的方式来使用:
wade -t 10 -n 5 -u https://www.baidu.com
其中,-n 代表进程数量。
有了多进程,我就需要改变这些应用的输出。对于多进程
应用,输出需要知道每个进程的执行情况并且要能够汇总。因此,我期望应用能够这样输出:
{'Thread': 0, '2XX': 2, '3XX': 0, '4XX': 0, '5XX': 0}
{'Thread': 3, '2XX': 2, '3XX': 0, '4XX': 0, '5XX': 0}
{'Thread': 1, '2XX': 4, '3XX': 0, '4XX': 0, '5XX': 0}
{'Thread': 4, '2XX': 4, '3XX': 0, '4XX': 0, '5XX': 0}
{'Thread': 2, '2XX': 4, '3XX': 0, '4XX': 0, '5XX': 0}
因为,其实3XX 类的返回值在某些情况下也应该算是正确,而 4XX 和 5XX 类的返回值应该分开统计。因此,我改进了一下这个工具。
在改进后我重新测试,我找到了问题的答案:
我成功的把数据库迁移到了 RDS 上,并在测试环境实例上停止了 MySQL 进程,带来了 40 倍的性能提升。发现这个应用的数据库需要最少 10 GB 的内存才能正常工作。
当我以 500 个进程去持续请求的时候,我把服务器弄挂了。输入的响应很慢,且执行命令会返回-bash: fork: Cannot allocate memory
的错误。通过减少进程数,我发现一个用户请求会占用 110 MB 左右内存,要满足 500 个用户的并发访问,主机需要最少 64 GB 的内存。
由于并发用户增长的同时,内存也在增长,物理内存用完之后会使用 Swap 区的虚拟内存空间。当 Swap 区的空间占满后,这个时候因为没有可分配内存,所以应用响应奇慢。即便是我终止了测试请求,仍然没有缓解,我猜之前的请求已经在 HTTP 端排队,在请求没有结束或者超时释放资源,后续的请求会继续排队。
那个…… 好像,我对这个服务器进行了一次 DoS (拒绝服务)攻击。
加载了 NewRelic,我发现这个应用在加载首页的时候性能是最低的,而大部分的资源都消耗在了 select 查询上。因此,我判断其中的表或者数据有问题,会进行大量加载。其次,可以通过给首页增加页面缓存,或者在数据库库端加入缓存,来缓解资源占用。毕竟,首页的访问时最频繁的。
最后,我们可以把 wade 在测试中度量到的数据当做是架构演进中的验收测试或者冒烟测试,集成在持续部署流水线中,在变更基础设施或者部署应用之后执行。我们需要非功能的架构级别的自动化测试来保护应用架构的重构。
反思 - 少即是多
如果没有这个工具,想得到以上的答案。我需要同时在三个服务(AWS CloudWatch, NewRelic, Flood.io)之间来回切换,并且搜集到需要的数据。那么多的数据,找到一个简单直接能反应问题的数据也很困难。而在等待账号审批的过程中,我就写下了这个工具。这个工具覆盖了客户会关心的基本场景和数据之间的关系。而这三个工具不能同时都满足(其实NewRelic 其实就差一点点)。虽然每个工具在各自领域 和所面对的客户都是非常强大的工具,而一个真实客户需求的场景 - 找到在正常压力下影响停机时间的因素 - 却很难被满足。
所以,对于非目标的用户和使用场景,产品丰富的功能和数据有可能是需要被过滤的噪音。一个产品所要面对的用户场景越多样,它所引入的噪音就会越多。而更多的增值服务和高价值服务,则被淹没在了这样的噪音里。
支付宝和银行的手机端应用就有这样的问题,什么都做的事情,一般什么都做不好。
最后
作为一个两个小时之内完成的工具,wade 缺乏各种自动化测试。但是,从 wade 设计过程我们可以看出,虽然我没有写自动化测试,但是设定期望并完成期望的结果是一致的。从这个角度上讲,TDD 也是把大脑中对程序的设计过程记载下来的一个活动。
如果对这个工具感兴趣,欢迎 PR:https://github.com/wizardbyron/wade