偶然发现了《扛住 100 亿次请求——如何做一个“有把握”的春晚红包系统》这篇文章。读后感慨万千,受益匪浅。图片来自Pexels。据说他山之石可以攻玉。这篇文章虽然是2015年发表的,已经很久没有看到了,但是里面的思想还是可以作为很多后端设计的参考。同时,作为一个微信后台工程师,看完之后,再三思考。学习了这样一篇文章,能不能给自己的工作带来一些实践经验呢?想要修炼,能不能自己修炼100亿红包请求?不然看完之后脑子里就只剩100亿、1400万QPS整改之类的字眼了,接下来的文章会展示作者是如何使用这个目标进程在本地环境中进行模拟的。实现目标:单机支持100万连接,模拟摇红包、发红包的过程,单机峰值QPS6万,支持业务流畅。注:本文及作者所有内容仅代表个人理解和实践。过程与微信团队无关。真正的在线系统也不同。只是从一些技术点上进行练习。请读者加以区分。背景知识QPS:Queriespersecond(每秒的请求数)。PPS:Packetspersecond(每秒的数据包数)。摇红包:客户端发送摇红包请求,如果系统有红包则返回,用户拿到红包。发红包:生成一个红包,内含一定金额。红包指定几个用户。每个用户都会收到红包信息。用户可以发送请求打开红包获取部分金额。确定目标在启动任何系统之前,我们应该弄清楚我们的系统在完成后应该具有什么样的负载能力。用户总数通过文中可以了解到,接入服务器有638台,服务上限约为14.3亿用户。因此,单次负载的用户上限约为14.3亿/638台=228万用户/台。但目前中国绝对不会有14亿用户同时在线。参考:http://qiye.qianzhan.com/show/detail/160818-b8d1c700.html2016年Q2,微信用户约8亿,月活跃用户约5.4亿。所以2015年春节期间,虽然用户会很多,但同时在线的用户肯定不到5.4亿。总共有638台服务器。按照正常的运维设计,相信不会所有的服务器都完全在线,会有一定的硬件冗余,防止硬件突发故障。假设总共有600台接入服务器。单台服务器要支持的负载数每台服务器支持的用户数:5.4亿/600=90万。即平均单机支持90万用户。如果realcase超过900,000,模拟的case可能会有偏差,但我觉得QPS在这个实验中更重要。单机峰值QPS文章明确写为1400万QPS。这个数值很高,但是因为有600台服务器,所以单机的QPS是1400万/600=大约23000个QPS。文章中曾经提到系统可以支持4000万的QPS,那么系统的QPS至少要有4000万/600=66000左右,大约是当前值的3倍,短期内不会触及。但我认为应该做相应的压力测试。红包发放中提到系统以50000个/秒的速度发送,所以单机发送速度为50000/600=83个/秒,即单机系统要保证83个/秒第二个被发送。.最后,考虑到系统的真实性,至少会有用户登录这个动作,真正的系统还会包含聊天等服务。最后看一下100亿摇红包的整体需求,假设春晚4小时内均匀发生。那么服务器的QPS应该是10000000000/600/3600/4.0=1157。也就是单机每秒能做1000多次,其实不高。如果10000000000/(1400*10000)=714秒完全被1400万的峰值速度消化掉,也就是说只需要11分钟的峰值时间就可以完成所有的请求。可以看出,互联网产品的特点之一就是峰值很高,持续时间不是很长。总结:从单台服务器的角度来看,需要满足以下条件。①支持至少100万连接用户。②每秒至少可以处理23000个QPS。这里我们把目标定的高一些,分别定为30000和60000。③摇红包:红包发放速度为每秒83个,也就是说每秒有23000个摇红包请求,其中83个请求可以摇到红包,剩下的22900个请求就知道了那他们没有动摇。当然,客户端收到红包后,还需要保证客户端和服务端的红包数量与红包中的金额一致。因为没有支付模块,我们也把要求提高了一倍,达到每秒200个红包的分发速度。④支持用户间互发红包业务,并确保发送方和接收方红包数量与红包内金额一致。我们也设定了每秒200个红包的分发速度作为我们的目标。完全模拟整个系统太难了。首先需要大量的服务器,其次需要数以亿计的模拟客户端。这对我来说是不可能的,但是可以肯定的是,整个系统是可以横向扩展的,所以我们可以模拟100万个客户端,然后再模拟一个服务器,那么1/600的模拟就完成了。与现有系统的差异:与大多数高QPS测试不同,该系统具有不同的重点。两者做了一些比较:基础软硬件软件Golang1.8r3、Shell、Python(开发没有使用C++而是使用Golang,因为最初使用Golang的原型满足了系统要求。虽然Golang还有一些问题,但是相比之下以开发效率来说,这种损失是可以接受的)。服务器操作系统:Ubuntu12.04。客户端操作系统:debian5.0。硬件环境服务器:dellR2950。8核物理机,非独占其他业务,16G内存。这个硬件是7年前的,对性能要求应该不是很高。服务器硬件版本:服务器CPU信息:客户端:配置4核5G内存的esxi5.0虚拟机。一共17台机器,每台机器有60,000个连接到服务器,完成了100万个客户端模拟。技术分析比较简单,实现单机100万用户连接。笔者几年前就完成了单机百万用户的开发和运营。现代服务器可以支持数百万用户。相关内容可以查看Github代码及相关文档、系统配置和优化文档。参考链接:https://github.com/xiaojiaqi/C1000kPracticeGuidehttps://github.com/xiaojiaqi/C1000kPracticeGuide/tree/master/docs/cn30000QPS的问题需要分两部分看client端和服务器端。①客户端QPS为30000,因为有100万个连接连接到服务器。这意味着每个连接需要每33秒向服务器发送一次摇红包的请求。因为单个IP可以建立的连接数在6万左右,17台服务器同时模拟客户端行为。我们要做的就是保证每秒有这么多的请求发送到服务器。技术点是客户协作。但是各个客户端的启动时间和连接建立时间不一致,仍然会出现断网重连等情况。每个客户端如何确定何时需要发送请求以及应该发送多少请求?我是这样解决的:使用NTP服务同步所有服务器时间,客户端通过时间戳来判断此时需要发送多少个请求。算法实现简单:假设有100万用户,用户id为0-999999。要求的QPS是5万,客户端知道QPS是5万,总用户数是100万。计算100万/5万=20,所有用户应该分成20组。如果time()%20==userid%20,那么此时该id的用户应该发起请求,从而实现多客户端协同。每个client只需要知道用户总数和QPS就可以自己做出准确的请求。延伸思考:如果QPS是一个不能被30000整除的数,怎么办?如何保证每个客户端发送的请求数量尽可能均衡?②服务器QPS服务器端的QPS比较简单,只需要处理客户端的请求即可。但是为了客观的了解处理情况,我们还需要做2件事。第一:需要记录每秒处理的请求数,这就需要在代码中嵌入一个计数器。第二:需要对网络进行监控,因为网络的吞吐量可以客观反映QPS的真实数据。为此,我使用Python脚本结合ethtool工具编写了一个简单的工具,通过它我们可以直观的监控网络的数据包是如何通过的。它可以客观地显示我们的网络上发生了多少数据传输。工具截图:摇红包业务摇红包业务非常简单。首先,服务器以一定的速度产生红包。红包不带走,就堆在里面。服务器接收来自客户端的请求。如果服务端有红包,就告诉客户端有,否则提示没有红包。因为单机每秒3万个请求,大部分请求都会失败。只需要处理锁的问题。为了减少竞争,我把所有用户分成了不同的桶。这减少了对锁的争用。如果以后有更高的性能需求,还可以使用高性能队列-Disruptor进一步提升性能。注意我的测试环境缺少支付这个核心服务,所以实现难度大大降低。另外提供一组数据:2016年淘宝双11交易高峰仅为12万/秒,微信红包发放速度为5万/秒。要做到这一点非常困难。参考链接:http://mt.sohu.com/20161111/n472951708.shtml发红包发红包的业务很简单。系统随机生成一些红包,并随机选择一些用户,系统提示这些用户有红包。这些用户只需要发出打开红包的请求,系统就可以从红包中随机分出一部分金额分发给用户,从而完成交易。这里也没有为这项核心服务付费。监控最后,我们需要一个监控系统来了解系统的状态。我借用了我另一个项目的部分代码来完成这个监控模块。使用这个监听,服务端和客户端会将当前的计数器内容发送给监听。监控需要整合展示各个客户端的数据。同时记录日志,为日后分析提供原始数据。线上系统较多使用opentsdb等时序数据库,资源有限,所以采用原方案。参考链接:https://github.com/xiaojiaqi/fakewechat监控显示日志大致是这样的:代码实现与分析代码方面,用到的技巧不多,主要是设计思路和Golang本身的一些问题需要解决被考虑。首先,Golang中的goroutine数量是受控的。因为至少有100万个连接,按照一般的设计方案,至少需要200万、300万个goroutine才能工作,这会给系统本身造成沉重的负担。二是100万个连接的管理。不管是人脉还是生意,都会造成一些精神上的负担。我的设计是这样的:①首先,将100万个连接分成多个不同的SET,每个SET是一个独立的并行对象。每个SET只管理几千个连接,如果单个SET工作正常,我只需要添加SET来增加系统吞吐量。②其次,仔细设计每个SET中数据结构的大小,保证每个SET的压力不会太大,不会出现消息堆积。③再次减少groutine数量,每个connection只使用一个goroutine,一个SET中只有一个gcrooutine负责发送消息,节省了100万个goroutine。这样整个系统只需要预留1,000,000个gcrooutines就可以完成业务。显着节省CPU和内存。系统的工作流程大致是:每个客户端连接成功后,系统会分配一个goroutine来读取客户端的消息。当消息被读取时,会转化为消息对象放入SET的接收消息队列中,然后返回获取下一条消息。在SET内部,有一个workergoroutine只是做一些非常简单和高效的事情,它所做的是以下内容。查看SET的接受消息,会收到3种消息:客户端的摇红包请求消息。来自客户端的其他消息,例如聊天好友。服务器对客户端消息的响应。对于第一类消息,就是这样处理的。获取client的红包请求消息,尝试从SET的红包队列中获取一个红包,获取到则返回红包信息给client,否则构造一个没有被摇过的红包。消息返回给相应的客户端。对于第二种消息,只需从队列中取出消息转发到后端聊天服务队列,其他服务再转发该消息。对于第三种消息,SET只需要根据消息中的用户id,找到SET中预留的用户连接对象,发回即可。对于红包生成服务来说,它的工作非常简单,只需要将红包对象依次放入各个SET的红包生成队列中即可。这样一来可以保证每个SET中的公平性,二来它的工作强度很低,可以保证业务的稳定性。参考链接:https://github.com/xiaojiaqi/10billionhongbaos实践过程分为三个阶段:阶段1分别启动服务器和监控,然后一一启动17个客户端,让它们建立100万个链接。在服务端,使用ss命令统计每个客户端与服务端建立了多少个连接。命令如下:Aliass2=Ss–ant|grep1025|grepEST|awk–F:“{print\$8}”|sort|uniq–c'结果如下:第2阶段使用客户端的HTTP接口调整所有client的QPS为30000,让client发送一个3WQPS强度的请求。运行如下命令:观察网络监控和监控端的反馈,发现QPS达到了预期的数据,并对网络监控进行截图:在服务端启动一个产生红包的服务,并这个服务会以每秒200个的速度发出红包,总共40000个。这时候在监视器上观察客户端的日志,你会发现红包基本都是以每秒200个的速度获取的。红包全部发出后,开始红包服务。该服务系统将产生20,000个红包,每秒200个。每个红包会随机指定3个用户,给这3个用户发送一条消息。客户端会自动上门领红包,最后红包全部拿走。Phase3使用客户端的HTTP接口,将所有客户端的QPS调整为60000,让客户端发送一个强度为6WQPS的请求。同理,在服务器端,启动一个生成红包的服务。该服务将以每秒200个的速度发送红包,共计40,000个。这时候在监视器上观察客户端的日志,你会发现红包基本都是以每秒200个的速度获取的。红包全部发出后,开始红包服务。该服务系统将产生20,000个红包,每秒200个。每个红包会随机指定3个用户,给这3个用户发送一条消息。客户端会自动上门领红包,最后红包全部拿走。最后,练习完成。分析数据在实践中,服务端和客户端都会将自己内部的计数器记录发送给监控端,成为日志。我们使用一个简单的Python脚本和gnuplt绘图工具来可视化实践过程来验证运行过程。第一个是客户端发送的QPS数据:这张图的横坐标是时间,单位是秒,纵坐标是QPS,表示此时客户端发送的所有请求的QPS。在图表的第一个区间中,几个小峰由100万个客户端连接。图表的第二个区间是30,000QPS区间。我们可以看到数据在30000这个区间是比较稳定的。最后是60,000QPS区间。但是从整体上看,QPS并没有完美的保持在我们希望的直线上。这主要是以下几个原因造成的:①当很多goroutine同时运行时,依赖sleep的时机不准确,会出现offset。我认为这是Golang自身调度造成的。当然,如果CPU更强大,这种现象就会消失。②由于网络的影响,客户端在发起连接时可能会出现延迟,导致前1秒内无法完成连接。③服务器负载大时,1000M网络已经出现丢包现象,可以通过ifconfig命令观察到,所以会出现QPS波动。二是服务端处理的QPS图:对应客户端,服务端也有3个区间,和客户端的情况很接近。但是我们看到在22:57左右,系统的处理能力明显下降,然后又急剧上升,这说明代码还是有待优化的地方。从整体观察可以发现,在30000QPS范围内,服务器QPS比较稳定,60000QSP时,服务器处理不稳定。我相信这与我的代码有关,如果我继续优化它,我应该能够有更好的结果。合并两张图:基本一致,也证明系统符合预期设计。这是产生红包数的状态变化图:很稳定。这是客户端每秒获取摇红包的状态:可以发现在30000QPS的范围内,客户端每秒获取的红包数量基本在200左右,而在60000QPS时,抖动严重发生时,不能保证在200数值以内。我觉得主要是在6万QPS的时候,网络抖动加剧,导致红包数量抖动。最后是Golang自带的pprof信息。其中GC时间超过10ms。考虑到这是一个7年的老硬件,而且是非独占模式,还是可以接受的。总结根据设计目标,我们模拟设计了一个支持100万用户的系统,可以支持最少30000QPS,最高60000QPS/s。我们简单模拟了微信摇一摇发红包的过程,可以说是达到了预期目的。如果这600台主机每台都能支持60000QPS,那么完成100亿次红包摇一摇请求只需要7分钟。虽然这个原型只是简单的完成了预设的业务,但是它和真正的服务有什么区别呢?我列出来了:参考资料:单机百万实践https://github.com/xiaojiaqi/C1000kPracticeGuideAWS100万用户压力https://github.com/xiaojiaqi/fakewechat/wiki/Stress-Testing-in-the-Cloud搭建自己的微信类系统https://github.com/xiaojiaqi/fakewechat/wiki/Designhttp://techblog.cloudperf.net/2016/05/2-million-packets-per-second-on-public.html@火锦注http://huoding.com/2013/10/30/296https://gobyexample.com/non-blocking-channel-operations
