一、前言
2020 年初,突如其来的疫情使各行各业都进入紧张状态,互联网产品也各尽其能投入到战“疫”中。健康信息收集是抗疫过程中最基础的工作之一,腾讯问卷平台结合其信息采集、统计分析的特性,快速迭代出“健康打卡”“复学码”等功能。
然而,健康码关系着用户能否正常进出社区、公司或校园,计算的时效性和准确性必须有保证。加之用户量大、打卡时间点集中,在开发时间和资源都紧张的情况下,我们是如何应对访问高并发、数据量陡增,从而保障系统的稳定?
图 1
本文结合腾讯问卷后台的优化和改造工作,记录过程中的思考,整理提升 Web 应用负载能力的关注点。作为一个刚接触海量用户场景的小白,希望借此跟大家交流探讨。
图 2
实践围绕图2的四个点,从被动到主动,从发现到治理。这几个方法远远谈不上全面,但可能是短时间内性价比最高的实践。
二、思考
1. 理解和关注高并发
1.1 What & Why
“高并发(High Concurrency)通常是指,通过设计保证系统能够同时并行处理很多请求”……Web 应用需求变更快,需要持续改进、持续运营以应对潜在的用户量突增。
Web 应用负载能力不足会有怎样的表现?
- 速度慢
比如打开页面持续 loading/白屏,需要等待或者重新进入,给用户带来很差的使用体验。
- 系统奔溃
系统崩溃可能表现在服务无响应、组件不可用、用户数据丢失等。在问卷系统中假设用户提交的数据丢失,健康码会因为中断打卡由绿变红,给用户带来困扰,同时也给开发运维带来修复的工作量。
所幸通过项目组的合力改造,系统的抗压能力提升了一个数量级。但也引发了新的思考,假如我们的系统要支持更高的并发,如何复盘当前的工作,以及准备下一步的工作?
图3借用《海量运维、运营规划之道》一书中的负载与架构演进图:
图 3
作者认为,Web 架构的生命周期要求我们要杜绝过度设计,又要保证前瞻性。而在实践中我也形成了一种认知,高并发是相对而不是绝对的,在业务每个阶段都需要有敏捷支持相对于前一阶段更高负载的能力,做性价比最高的事情。
1.2 Who
作为应用开发我也曾陷入这样的误区,认为高并发是架构设计师、运维工程师才需要考虑的事情,实践中发现项目组的各个角色都可以为高并发能力做点什么。
- 产品策划和交互设计
当一个功能被设计出来时,就可以预见是否存在高并发瓶颈,通过一些产品策略或者交互方式的转变,往往能缓解问题。
图 4
比如在“健康打卡”这个功能点,以往设计了在每天早上七点推送提醒,无形中给系统制造了高并发的场景,实际上推送的时间点可以交给客户设置、并且分散到不同时间段,从而合理利用机器资源。
- 开发人员
即使架构师设计了良好的分布式方案、运维工程师保障了系统能稳定扩容,负载的瓶颈还会是被开发人员“制造”出来(更别提在我们的业务每个人都身兼数职)。后续的案例也可以体现开发在支持高并发过程中的职责。
2. 理解和关注业务
2.1 When
什么时候需要重点关注业务的并发能力?最好的时间点当然是在设计之初,但往往我们是中途加入项目,下表中这几个时机可以作为参考:
软件熵这里可以做一个简单的科普,熵是一个来自物理学的概念,指的是某个系统中的 “无序” 的总量,当软件中的无序增长时,我们称之为“软件腐烂”。
图5中举了个例子,问卷中有个打卡统计功能,在开发的初期只需要展示谁没有打卡,重实现轻设计。在逐渐新增交叉分析、筛选等功能,以及在修过几次统计不准确的 Bug 后,打卡统计越来越耗性能,以致于在并发量高时耗尽计算资源而挂掉。
图 5
所有的问题都是建立在历史问题的基础上,往往这个时候对问题的处理就没有第一个版本那么完美。实践的经验是代码开始腐烂的位置往往也是存在高并发风险的位置。这时候就需要尽早关注。
2.2 Where
“设计一个大型高并发网站的架构,首先必须以了解业务特点作为出发点,架构的目的是支撑业务”……
Web 应用的架构往往并不复杂,在项目中我们主要关注两个方面:
1)流量链路
- 负载均衡
- 网络连通
图 6
2)业务架构
- 组件的选型是否合理
- 依赖的服务是否稳定
图 7
图7粗略展示了腾讯问卷的技术栈,实践中其中某一个环节出问题都可能导致负载提升不上去,后文也将围绕各个环节来做分析。
三、实践
1. 洞察
1.1 监控
监控是为了第一时间发现、定位并记录异常。当前腾讯问卷系统虽谈不上立体化,但也在不断完善,保证了从业务层、系统层、网络层都有迹可循。
图 8
1.1.1 监控的指标选择
监控时我们往往最关注两个内容:时间和行为(路径),例如在应用层,左图记录了任务执行的耗时,右图记录了在任务执行过程中经历了哪些事件,二者结合便于判断性能的瓶颈、或者 bug 潜在的位置。同理对前端、对接入层也用了类似的指标。
图 9
1.1.2 监控的呈现
接下来是把指标可视化,将关键日志进行聚合,利用 Dashboard 呈现。
图 10
1.1.3 监控的监控
在异常爆发时,自建的监控系统在也会接收大量的上报,如果监控系统自己都挂掉了,对业务的监控也就失去意义,因此对监控系统也需要做保护,例如对错误上报做限流、对监控做额外的健康探测等。
1.2 压测
1.2.1 预测
- 预估可能存在的瓶颈位
各个位置的带宽是否跑满?
组件的 CPU 或者内存使用率是过高?
业务机的 CPU 或者是磁盘 IO 是否跑满?
- 预设目标
需求角度出发:实际总用户量可能是多少?同时会有多少人使用(打卡)功能?
现状角度出发:想达到的目标是当前负载能力的 5 倍?10 倍?
1.2.2 实施
我们常会使用 apache bench(简称ab) 测试接口,但到业务场景中,单接口的压测有局限性。在测试同学的带领下我们尝试了 Locust。
- 如何模拟用户并发
首先要理解一个用户同时发起一万次请求,跟一万个用户同时发起一次请求,对系统造成的压力是不一样的。实测过程中我们预留了一千万个 ID 用于模拟真实用户,同时避免测试数据难以清理。
图 11
- 如何模拟行为并发
这里先介绍一个用户使用问卷进行打卡的流程:打开问卷链接->登录->答题->提交答案。也就是说一个打卡场景实际涉及多个请求,在做压测的时候我们尽可能还原。在写完每个接口的测试方法后,Locust 可以使用 TaskSet 进行任务组合,并且支持权重配比。这个比例正好可以从我们的流量监控获得。
图 12
1.2.3 定位瓶颈
压测的目的是在于找到系统的瓶颈,一定是要确定系统某个方面达到瓶颈了,压力测试才算是基本完成…
- 纵向:按流量链路逐层“断点” 回顾系统的链路,流量从前端到接入层、再到应用层、缓存层、存储层,每个环节都有被“撑爆”风险。只有逐层排查和改造,才能了解到链路中潜在的瓶颈位。
案例 1. 流量被前端拦住,并没有到后台
图 13
案例 2. 缓存层带宽不足
图 14
- 横向:对照实验 经过逐层压测,系统的性能瓶颈定位到 MySQL 上,这也符合我们对业务的认知,因为系统重度依赖数据库并且缺乏针对性的优化。但是数据库的读写来源有接口服务和任务队列,我们采用了对照实验,调整 fpm worker 与 queue worker 数量比例,表格记录每一轮测试的效果,进而判断是哪个影响更大
1.2.4 压测小结
压力测试不仅是孤立测试各个软硬件的性能指标,而是要尽可能模拟真实的业务场景,从而充分评估系统上线后可能发生的情况,同时也为我们后续制定问题应对措施、制定改造计划提供了依据。
2. 响应
2.1 扩容
当用户量开始增多,现有系统无法处理那么大访问流量,最快速的响应方案是扩容。但是你的系统真的能快速扩容吗?
2.1.1 扩容的方式
- 垂直扩展
软件上,通过调参能否充分利用硬件资源
硬件上,是否有升配的能力
- 水平扩展
业务服务器加机器
组件集群加节点/分片
2.1.2 怎样支持扩容
对于老业务,我们往往要先解决怎样支持扩容的问题。
- 解耦 业务中依赖了大量的公司内部服务,但由于网络策略或安全因素,它们影响了业务的扩容能力。实践中我们把这些依赖剥离出来,通过代理等形式,形成了局部单体、整体可扩容的架构。
- 上云 以往的单体应用架构制约了扩容能力:比如需要在应用层加一台物理机,需要从装系统开始做起;给 MongoDB 加一个节点,需要大量的运维操作。借助云服务的灵活性可以大大提高系统的扩容能力。
腾讯问卷上云主要包括以下三个方面:
在上云过程中多多少少会踩坑,这里暂不展开讨论,后续再开新篇深度记录。
2.1.3 扩容还需要注意什么
- 依赖的组件是否会扛不住
- 依赖的远程服务是否会因为扩容被拖垮
在压测过程中可能会在测试环境 Mock 掉一些远程请求,导致在线上扩容业务服务后,问题才暴露出来,需要引以为戒。
图 15
显示了在并发量逐步上升的过程中,QPS 出现下降再上升的情况,排查发现是依赖的远程服务容量不足导致的性能下降,所以在压测过程中不能仅关注系统本身。
2.2 优化
扩容往往只是一种缓解措施,从运维的角度提升了系统的负载能力,但随之而来的是服务器成本的上升,我们需要通过改造和优化使资源的利用更合理。
2.2.1 关注代码逻辑
- 慢执行
为了功能快速上线,我们往往忽略了代码里耗时的方法和逻辑,当大量请求涌入,慢执行就会阻塞后续的请求,拖慢系统的其他接口,甚至造成雪崩。
CPU 密集型的比如 web 应用常会防止 xss 攻击,一般会用正则过滤标签,在面对大文本、高并发、或者是循环的时候,都会大量占用 CPU,使业务服务器的压力上升。
IO 密集型的比如上传下载文件时过度扫描目录,任务的大部分时间都在等待 IO 操作完成,响应时间变长,这时候业务服务器的 CPU 使用率不高,而磁盘读写压力大。
对于PHP接口通过记录和查询 fpm_slow_log 可以分析耗时的方法。同时我们把慢执行数量进行监控,在新功能上线时需要密切关注。
- 慢查询
无论是在生产环境还是压测过程,造成 MySQL 压力的很多情况是大量的慢查询,通过查 slow_log 或者从腾讯云管理后台可以进行慢查询分析。
很多资料都会提到使用索引来提升查询速度(先抛开I/O、锁这些不讲),然而即使有索引也可能踩坑。比如 “隐式类型转换” 的问题在腾讯问卷系统中就存在。
图 16
MySQL 会根据需要自动将数字转换为字符串,比如 openid 为 varchar 类型,当使用 where openid=123456 的查找时,实际进行了全表扫描,解决方案是查询前在代码中做转换,或者使用 CAST 方法转换。
2.2.2 合理使用组件
- 是否有性能更高的替代
专业的事情交给专业的人干,在组件选型同样适用。在改造过程中,Elasticsearch 组件的引入,减少了在 MySQL 做搜索或聚合,提高了前面案例提到的打卡统计的计算能力。
- 是否有更低成本的替代
Web 应用在使用缓存时,往往不注意容量和带宽的成本,回顾前面的压力测试,我们发现将大量的问卷静态数据存在 Redis 后,Redis 很快成为负载瓶颈,实际上只需要稍加改造,就可以找到合适的存储替代。
四、小结与展望
本文整理了 Web 应用高并发改造过程中的一些关注点,实际上每个加粗的小点都应该展开来更深入地探讨,提供对比方案和详细效果。但作为开发的我往往感觉到,自己不缺乏解决单个问题的能力,而缺乏系统性思维和全局观,本文也算是为高并发实践记录一些思路和方向,后续应继续补充完善。
回顾这段时间的改造效果,在使用资源不到两倍的情况下实现了5-10倍的负载能力,并且使继续扩容成为可能。虽然我们用效率最高的手段,让系统顺利支持了疫情期间的并发量,但如果要实现十倍甚至百倍的负载能力,当前的工作必定是不够的,或许更完善的分布式架构、或者当前火热的微服务架构,是我们系统下一步的考虑方向。