什么是全链路压测?
基于实际的生产业务场景、系统环境,模拟海量的用户请求和数据对整个业务链进行压力测试,并持续调优的过程
主要特征:
真实流量
线上环境
实时监控和过载保护
全链路压测组成
单链路指一个业务线。
全链路压测是一个模拟线上环境的完整闭环,由5大核心要素组成:
- 压测环境:对应用户真实的线上环境,具备数据与流量隔离能力的生产环境; 原则:能够用中间件解决的问题,绝不对业务系统进行改造,系统所需做的是升级中间件,这一原则极大提高了工作效率。
- 压测基础数据:构造满足高峰场景的核心基础相关数据,影子库里构造相同量级的数据; 真实线上数据筛选脱敏。
- 压测流量(模型、数据):成百上千的接口组合,到复杂的接口之间的参数传递,复杂的条件判断来构造精准的全局流量模型,和真实业务情况保持一致;
压测引擎的三层结构:
协议支持
请求发送:CGroup资源隔离,异步Reactor模型发送请求,链路间线程池隔离
集群协作: Master,Slave长连接; Cristian算法同步网络延迟,Slave动作一致;
4. 流量发起:模拟全国各地真实的用户请求访问,探测站点能力;
5. 问题定位:多维度的监控和报表,服务端可通过其他生态产品协助定位。
翻译构造能力的体现:便捷的构造全局业务场景和流量数据的能力。
原子因素:链路(被压测的最小单位) 指令: 思考时间、集合点、条件跳转、cookie存取、全局准备、并发用户限制等
原子因素->串行链路->场景
全链路压测解决什么问题?
- 验证峰值流量下服务的稳定性和伸缩性
- 验证新上线功能的稳定性
- 进行降级、报警等故障演练
- 对线上服务进行更准确的容量评估
什么时机下需要?
- 业务发展速度
- 在可以预期的一段时间(最好是半年,一个季度有点晚)内,业务会有较快速的发展,线上机器必须要大幅度扩容
- 扩容有的时候并不是线性的,从两台扩展到四台,你得服务能力或者能提高两倍
- 继续扩容服务能力就有可能提高不上去了,因为要受限于其他的模块,比如 DB、公共组建、中间件等
- ps:业务的不断发展,依赖的模块不断增多。需要找出短板来进行解决
- 链路的复杂程度在扩张
- 一般而言,随着业务的发展,我们的接口会越来越多,系统会逐渐的做分布式
- 业务线内部的模块越抽象越多,业务线跟其他业务线的交互也越来越多
- 我们无法单纯的根据自己系统的处理能力来评估接口的服务能力
- ps:接口的服务能力取决于模块中最低的那个—木桶理论
- 对单机压测结果越来越没有自信
- 一个很好的指标,一般而言,我们都会压一下我们自己的模块
- 单机的压测不代表真实的线上场景,内心会越来越虚,这个时候,就要考虑全链路了
如何展开全链路压测?
压力测试方式的几个阶段
- 对线上的单机或集群发起服务调用
- 将线上流量进行录制,然后在单台机器上进行回放
- 通过修改权重的方式进行引流压测
- 全链路压测
梳理核心链路和边界
- 核心链路是一个业务的核心,这一块应该可以很快梳理清楚
- 难点在于梳理清楚链路的边界
- 千万不要污染正常数据:认真梳理数据处理的每一个环节,确保 mock 数据的处理结果不会写入到正常库里面
- 在核心链路的基础上,我们会有很多的分支业务,而这些分支业务有的可以参与压测,有的不能参与压测
- 比如给用户下放 push 消息
- 短信 / 支付 / 微信 Oauth 授权
首先应该明确的是:全链路压测针对的是现代越来越复杂的业务场景和全链路的系统依赖。所以首先应该将核心业务和非核心业务进行拆分,确认流量高峰针对的是哪些业务场景和模块,针对性的进行扩容准备,而不是为了解决海量流量冲击而所有的系统服务集群扩容几十倍,这样会造成不必要的成本投入。
数据模型构建
- 数据的真实性和可用性:可以从生产环境完全移植一份当量的数据包,作为压测的基础数据,然后基于基础数据,通过分析历史数据增长趋势,预估当前可能的数据量
- 数据脱敏:基于生产环境的全链路压测,必须考虑的一点是不能产生脏数据,以免对生产造成影响,影响用户体验等,因此在数据准备时需要进行数据脱敏
- 数据隔离:千万不要污染正常数据:认真梳理数据处理的每一个环节,可以考虑通过压测数据隔离处理,落入影子库,mock 对象等手段,来防止数据污染
流量平台搭建
- jmeter、Ngrinder、locust,提供分布式压测的方式(饿了么 的流量平台是基于 jmeter 改造的)、压测机中的机器数据能够实时的收集查看到、可以随时停止压测、一定时间内实时错误率达到阈值自动熔断。考虑到压测量较大的情况下回传测试结果会对 agent 本身造成一定资源占用,可以考虑异步上传,甚至事务补偿机制。
- 业务代码改造:压测请求上会打上特殊的标记,这个标记会随着请求的依赖调用一直传递下去。写请求写到影子区域(比如header头中做标记,存储、缓存、消息、日志等一系列的状态数据)、依赖的外部服务做 mock 处理(短信、邮件、push 等等)
全链路压测应对的都是海量的用户请求冲击,可以使用分布式压测的手段来进行用户请求模拟,目前有很多的开源工具可以提供分布式压测的方式,比如jmeter、Ngrinder、locust等。
可以基于这些压测工具进行二次开发,由Contorller机器负责请求分发,agent机器进行压测,然后测试结果上传Contorller机器。
考虑到压测量较大的情况下回传测试结果会对agent本身造成一定资源占用,可以考虑异步上传,甚至事务补偿机制。
- 真实流量蓄水池,分批释放
- 逐步压测
全链路压测核心功能
数据构造
回放业务高峰期产生的流量
- HTTP: Nginx Access Log分析
- RPC:对部分机器录制
通过以上两种方式生成压测词表(词表分片处理,方便后续批量加载)
压测隔离
- 添加压测标识,各服务和中间件依据标识来进行压测服务的分组和影子表方案的实施。
- 在请求头添加特殊标识,但需要保证线程间和跨服务间透传。
- 链路诊断功能方便定位问题。
服务隔离
- 通常选择深夜低峰压测,同时隔离正常流量和测试流量。 隔离策略:基于IP,机器数,百分比
数据隔离
- 影子表(阿里):使用线上同一个数据库,包括共享数据库中的内存资源,只在写入数据时写进另一个影子表。 KV的处理类似,MQ则选择生产端或消费端忽略消息。
调度中心
资源计算预估
- 压测期望到达的QPS
- 压测请求的平均响应时间和请求/响应体大小
- 压测的词表大小、分片数
- 压测类型
事件注入机制
根据系统的实际情况对压力进行相应调整。
- 调整QPS
- 触发熔断
- 开启事故注入
- 开启代码级性能分析
代码设计:观察者模式(会触发的事件)和责任链模式(执行事件)
熔断保护机制
客户端熔断: 根据业务自定义的熔断阈值,实时分析监控数据,当达到熔断阈值时,任务调度器会向压测引擎发送降低QPS或者直接中断压测的指令,防止系统被压挂。
容量规划
为什么需要容量规划
容量规划的目的在于让每一个业务系统能够清晰地知道:什么时候该加机器、什么时候应该减机器?双11等大促场景需要准备多少机器,既能保障系统稳定性、又能节约成本
ps:什么时候增减机器、保障系统稳定性、节约成本
容量规划四步走
业务流量预估阶段:通过历史数据分析未来某一个时间点业务的访问量会有多大
系统容量评估阶段:初步计算每一个系统需要分配多少机器
容量的精调阶段:通过全链路压测来模拟大促时刻的用户行为,在验证站点能力的同时对整个站点的容量水位进行精细调整
流量控制阶段:对系统配置限流阈值等系统保护措施,防止实际的业务流量超过预估业务流量的情况下,系统无法提供正常服务流量控制阶段:对系统配置限流阈值等系统保护措施,防止实际的业务流量超过预估业务流量的情况下,系统无法提供正常服务
获取单台机器的服务能力
为了精准地获取到单台机器的服务能力,压力测试都是直接在生产环境进行,这有两个非常重要的原因:单机压测既需要保证环境的真实性,又要保证流量的真实性
生产环境进行单台机器压力测试的 4 个方法
模拟请求:通过对生产环境的一台机器发起模拟请求调用来达到压力测试的目的
工具:apache ab、webbench、httpload、jmeter、loadrunner
适用场景:新系统上线或者访问量不大的系统采用这种方式来进行单机压测
缺点:模拟请求和真实业务请求之间存在的差异,会对压力测试的结果造成影响 另一个缺点在于写请求的处理比较麻烦,因为写请求可能会对业务数据造成污染,这个污染要么接受、要么需要做特殊的处理(比如将压测产生的数据进行隔离)
ps:和真实请求有差异、写请求需要处理、适合新系统上线或访问量不大的
复制请求:通过将一台机器的请求复制多份发送到指定的压测机器
适用场景:系统调用量比较小的场景
优点:为了使得压测的请求跟真实的业务请求更加接近,在压测请求的来源方式上,我们尝试从真实的业务流量进行录制和回放,采用请求复制的方式来进行压力测试
缺点:同样也面临着处理写请求脏数据的问题 另外一个缺点复制的请求必须要将响应拦截下来,所以被压测的这台机器需要单独提供,且不能提供正常的服务(不能把响应给到真实的用户了,比如涉及到发短信邮件之类的)
请求转发:将分布式环境中多台机器的请求转发到一台机器
适用场景:系统调用量比较大的场景
优点:请求的引流转发方式不仅压测结果非常精准、不会产生脏数据、而且操作起来也非常方便快捷,在阿里巴巴也是用的非常广泛的一种单机压测方式,这种方式怎么测试出当前系统最大能抗的流量是多少呢?
调整负载均衡:修改负载均衡设备的权重,让压测的机器分配更多的请求
适用场景:系统调用量比较大的场景
优点:调整负载均衡方式活的的压测结果非常准确、并且不会产生脏数据
ps:单机压测可以基于上面的4种压测方式基础上,构件一套自动化的压测系统,可以配置定时任务定期对系统进行压测,也可以在任意想压测的时间点手动触发一次压测
在进行压测的同时,实时探测压测机器的系统负载,一旦系统负载达到预设的阈值即立刻停止压测,同时输出一份压测报告 通过单机压测获取的单机服务能力值也是容量规划一个非常重要的参考依据
最小机器数 = 预估的业务访问量 / 单机能力
单机压测问题
系统可用性问题
经常由下面一些不确定性因素引起:
- 系统容量
- 业务性能
- 基础设施瓶颈
- 中间件瓶颈
- 系统直接的依赖影响
传统线上单机与单系统压测的四种方式
- 模拟调用者压测生产环境:读请求+写请求(需要特定处理)
- 流量录制和回放:快速率回放对单机压测;从流量分配的角度,将流量集中到某台机器(这两种方式要求访问流量不能太小):
- 请求流量转发
- 改变负载均衡的权重
单系统压测的问题
- 在做单个系统的容量规划时,所有的依赖环节能力是无限的,进而使得我们获取的单机能力值是偏乐观的;
- 采用单系统规划时,无法保证所有系统均一步到位,大多数精力都集中核心少数核心系统;
- 部分问题只有在真正大流量下才会暴露,比如网络带宽等等。
流程举例
- 业务中需要区分流量(正常流量/压测流量)
- 压测时需要在 http header 里加入 X-Shadow 的key ,值为 true 的代表压测,key 不存在或者 key 值不等于 true 代表非压测流量
- 接收和发送 http / grpc 等请求时
- 在向下游服务发起请求时,如果是压测流量把 header 头中的标记字段往下透传,下游继续在业务中往下透传
- 接收到如果是压测流量,使用相应的压测数据
- 依赖的模块
- MySQL 使用影子表,将压测流量对 MySQL 的读写打到影子表上。即正常的业务表名为A,则影子表名为Ashadow
- 所有涉及到的业务表都需要建一份影子表
- 便于事后数据的清理
- MongoDB 使用影子 collection ,将压测流量对 MongoDB 的读写打到影子表上。即正常的业务表名为A,则影子表名为 Ashadow
- 同 MySQL
- Redis 使用影子 key ,将压测流量对 redis 的读写打到影子 key 上。 如 set key value 会变成 set key_shadow value
- 同 MySQL
- kafka 使用影子 topic,key 后拼接 _shadow
- 同 MySQL
- MySQL 使用影子表,将压测流量对 MySQL 的读写打到影子表上。即正常的业务表名为A,则影子表名为Ashadow
- 不需要参与压测的做 mock 处理
- 给用户发push、短信、支付、微信 Oauth 授权
全链路压测应用
1 阿里分享
2013年为了双11提前预演而诞生,该服务已提供在阿里云PTS铂金版。
- 系统稳定性保障核武器——全链路压测
- 双11核武器——全链路压测详解
2 京东分享
ForgeBot, 2016年开发
京东全链路压测军演系统(ForceBot)架构解密
最早基于开源的NGrinder,能胜任单业务压测。Controller功能耦合重,支持的Agent数量有限。 之后开发了ForgeBot。
2.1 主要功能模块
- Controller:任务分配
- Task Service:负载任务下发,支持横向扩展。提供任务交互和注册服务。(gRPC:HTTP2+protobuf3)
- Agent:注册心跳,拉取任务、更新任务状态、 执行和停止worker process(采用Docker容器部署)
- Monitor Service:接受并转发压测数据给JMQ
- DataFlow:对压测数据做流式计算(输出TPS,TP999,TP99,TP90,TP50,MAX,MIN),将计算结果存到DB(ES)
在管理端创建测试场景,Controller扫描发现场景,寻找空闲Agent资源。
任务分配时,Controller计算每个间隔的执行时间点和递增的虚拟用户数,由Agent动态加压减压。
在多个组件使用了gRPC框架通讯
分读压测和写压测
2.2 一些解决问题的思路
问题:如何模拟在某一个瞬间压力达到峰值?
解决方案:通过集合点功能实现,提前开启峰值所需足够数量的线程,通过计算确定各个时间点上不需要执行任务的线程数量,通过条件锁让这些线程阻塞。当压力需要急剧变化时,我们从这些阻塞的线程中唤醒足够数量的线程,使更多的线程在短时间进入对目标服务压测的任务。
问题:为了计算整体的 TPS,需要每个Agent把每次调用的性能数据上报,会产生大量的数据,如果进行有效的传输?
解决方案:对每秒的性能数据进行了必要的合并,组提交到监控服务
3 饿了么分享
饿了么全链路压测平台的实现与原理
3.1 业务模型的梳理
- 是否关键路径
- 业务的调用关系
- 业务的提供的接口列表
- 接口类型(http、thrift、soa等)
- 读接口还是写接口?
- 各接口之间的比例关系
3.2 数据模型的构建
3.2.1 写请求
压测方法:
- 用户、商户、菜品等在数量上与线上等比例缩放;
- 对压测流量进行特殊标记;
- 根据压测标记对支付,短信等环节进行mock;
- 根据压测标记进行数据清理;
读请求
压测方法:拉取线上日志,根据真实接口比例关系进行回放
3.2.2 无日志服务
压测方法:
- 构建压测数据使缓存命中率为0%时,服务接口性能,数据库性能;
- 缓存命中率为100%时,服务接口性能;
- 缓存命中率达到业务预估值时,服务接口性能;
3.3 压测工具
定制JMeter
3.4 压测指标监控和收集
- 应用层面
- 服务器资源
- 基础服务:中间件和数据库
要点:
- 响应时间不要用平均响应时间,关注95线;
- 吞吐量和响应时间挂钩
- 吞吐量和成功率挂钩
3.5 具体实现
SpringBoot+AngularJS.
测试期间产生的冷数据(用例数据、结果数据)持久化至MongoDB,热数据(实时数据)持久化至InfluxDB并定期清理。
分布式测试:重新实现JMeter的分布式调度
测试状态流转:各种流程形成闭环,要考虑各种异常。
主要流程:配置 -> 触发 -> 运行 -> 结果收集 -> 清理。
整个状态流转的实现,采用异步Job机制实现了类似状态机的概念,状态属性持久化到数据库中,便于恢复。
3.6 安全保障
由于是在线上真实环境,需要避免测试引起的服务不可用和事故。
- 权限管理:用户权限分级管理,不能随意触发他人的测试用例,同时高峰期和禁止发布期,不允许执行任何测试。
- 停止功能:这是面向用户的手动停止功能,用户可以随时点击运行状态下的测试用例上的停止按钮,后台会直接kill掉所有运行该测试用例的测试机上的JMeter进程。
- 熔断功能:系统会根据实时信息中的错误率进行判断,当一定时间内的实时错误率达到或超过某个阈值时,该次测试将被自动熔断,无需用户干预。
- 兜底脚本:最极端的情况,当整个系统不可用,而此时需要停止测试时,我们提供了一份外部脚本直接进行停止。