本文是第五届中间件性能挑战赛的赛题解析,参与比赛,赢取最高10万元奖金。
在现代分布式应用中,服务请求是由物理机或虚拟机组成的 server 池进行处理的。 通常,server 池规模巨大且服务容量各不相同,受网络、内存、CPU、下游服务等各种因素影响,一个 server 的服务容量始终处于动态变动和趋于稳定的状态,如何设计和实现这种系统的负载均衡算法是一个极具挑战的难题。
阿里巴巴中间件公众号对话框发送“挑战赛”,获取上一届优秀选手的解题思路,点击这里。
自适应负载均衡的需求背景
负载均衡有两个主要目标:
- 保持较短的请求响应时间和较小的请求阻塞概率;
- 负载均衡算法的 overhead 在可控级别,不占用过多的 CPU 、网络等资源。
自适应负载均衡是指无论系统处于空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,进行合理的流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。
这种算法对于现在的电商系统、数据中心、云计算等领域都很有必要,使用自适应负载均衡能够更合理的利用资源,提高性能。例如,在双十一零点,用户集中下单支付,整个电商系统的请求速率到达峰值。如果将这些请求流量只分配给少部分 server,这些机器接收到的请求速率会远超过处理速率,新来的任务来不及处理,产生请求任务堆积。
对用户而言,一旦产生任务堆积,请求会变慢甚至超时,体验严重下降,甚至导致服务不可用。而处理请求的机器也会由于堆积的任务越来越多而发生严重过载,直到被打垮。剩余的尚未宕机的其它机器会逐渐重复这个过程,直至整个应用不可用,发生系统故障。
为了避免这种情况发生,我们可能会想到一种常用的办法:在服务上线前提前进行压测,使用压测的容量作为限流值,当线上服务的请求速率大于限流值的时候,服务拒绝新到的服务,从而保障服务始终可用。但是这种方式也存在问题:压测时测试的容量进行限流通常会趋于保守,不能充分发挥异构系统的全部性能;也无法智能地应对由于网络、下游服务变化而导致的容量下降等问题,系统仍然存在宕机风险。
因此,我们需要具备自适应能力的负载均衡算法,来更好的进行流量分配调度以及稳定性保障,追求极致性能,挑战大促等场景下的流量洪峰。
结合中间件性能挑战赛的赛题
我们结合「第五届中间件性能挑战赛」中的初赛场景,来一起探讨一下设计和实现一个自适应的负载均衡的基本思路。
本次挑战赛的场景由施压程序(阿里云性能测试PTS)、服务调用方(Consumer)和三个规格不同的服务提供方(Provider) 组成。在评测过程中,每个程序都部署在不同的物理机上,以避免因 CPU、网络资源的竞争,导致评测程序抖动,影响最终评测成绩。
Becnhmarker 负责请求 Consumer, Consumer 收到请求后,从三台物理规格不同、服务响应时间和最大并发都不同的 Provider 中选择一个进行调用并返回结果。选择哪一个 Provider 进行调用的流程就是本次挑战赛需要实现的负载均衡算法。
为了简化环境部署和提升性能,本次挑战赛没有使用服务注册和发现机制。三个 Provider 对应的 URL 都已经被直接配置在了 Consumer 中,选手在开发和测试时可直接通过 Provider-small 等 hostname 访问相应的 Provider。
赛题分析
题目描述很简单,不考虑 Consumer 直接拒绝的情况下,场景可以简化为 3 选 1 的问题,但如何进行这个决策则是本次挑战赛考察的难点和重点。
官方题目组提供了Random算法作为默认实现:从 3 个 Provider 中随机取任意一个。对于单 dispatcher (在本次赛题中是 Consumer) 同构系统的场景,Random可以达到渐近负载均衡, 每个 Provider 接收到的总请求数接近。但是对于多 dispatcher 或异构系统而言,Random 算法由于缺少全局状态,无法保证全局随机,极端条件下,多个 dispatcher 可能将请求同时分配到一台 Provider 上,导致系统存在服务过载和宕机的风险;异构系统中,不同 Provider 服务容量实际是不同的,即使每个 Provider 请求速率相同也会产生空闲、稳定、过载等不同的服务状态,无法实现最优流量分配,更不能做到响应时间最小。显而易见,Random并不是符合赛题要求的自适应算法。
那么,如何实现自适应负载均衡呢?️接下来我们将利用题目给出的条件由浅入深的描述这个算法的设计过程。
自适应算法首先要解决如何对服务进行容量评估的问题。
本次比赛按照硬件规格不同,Provider 被分为 small、medium、和 large 三种,CPU 和内存对应的比例为 1:2:3 。在评测过程中,每个 Provider 的处理能力都会动态变化,主要体现在单次响应时间的变化和允许的最大的并发数上。来自 Consumer 的请求速率过快时, Provider 端新到的请求会排队等待处理,当排队线程数和工作线程数量之和达到最大线程数时,Provider 返回线程池用尽异常。在算法的实现和调优过程中,应该尽量避免产生线程池异常,减少排队。如何结合好程序和硬件的限制,区分出不同阶段的瓶颈,做出符合实际的容量评估是赛题的第一个难点。对于本次题目所采用的参数和变化过程,仅凭现有的理论和实践很难达到最优,所以需要选手充分理解题意和各参数配置,设计出更适合实际场景的算法。
第二个需要考虑的问题是如何应用容量评估结果,即如何维护代表 Provider 服务能力的状态,又如何在选择 Provider 阶段根据这些状态进行决策?
传统的单 Dispatcher 负载均衡模型由一个 Dispatcher 维护所有 Provider 的状态,在同构系统中,这种方式能够达到渐进最优负载均衡。但是它存在的问题也很明显:单 Dispatcher 性能存在天然瓶颈,可扩容性较差,当 Provider 数量成倍上升时,Dispatcher 需要维护的状态也在成倍上升,通信成本也在上升。本次挑战赛中为了降低难度,没有基于多 Dispatcher 模型构建题目,但多 Dispatcher 、多 Provider 才是 Dubbo 等微服务框架在实际生产环境中最常见的情况。因此,若能实现高性能且可扩展性良好的均衡算法,会是一个不错的加分项。
第三点是辅助接口的使用。为了不限制算法设计思路,赛题提供了多个可能用到的辅助接口,包括双向通信、Provider 限流等支持。但是这些接口都是非必选项,是否使用这些接口取决于算法实现的需要。
在评测环境中,任意一个 Provider 服务处理速率都小于评测程序的请求速率。三个 Provider 总的处理速率会在请求速率上下浮动。最终成绩由请求成功数和最大 TPS 组成,失败的请求不计入成绩。对于这个限制,可以有两种解读方式,一是为了保证服务不严重过载,可以适当拒绝请求。第二点是需要充分利用每个 Provider 的服务容量,保证性能最优的 Provider 请求数合理,适当的过载也是允许的。
以上仅作为一个主要的算法设计思路,优秀的负载均衡算法在工程上的实现也是很关键的一点,需要选取合适的数据结构,充分利用好内存和 CPU,压榨出比赛环境的每一点性能。当然,评测成绩并不代表一切,良好的代码结构、编码风格以及通用性,也在最终初赛成绩中占据很大比例。
赛题评测
评测环境由 1 台 4 核 8G 的施压机,1 台 4 核 8G 的网关机和 3 台 4 核 8G 的 Provider组成。Consumer 和 Provider 程序都会限制 CPU 和内存使用,每个评测任务都会独占五台机器。
- 准备跑分环境,创建并锁定工作区;
- 根据提交的 Git 地址,从代码仓库中拉取代码;
- 构建代码,生成最终执行的 fat JAR;
- 启动三个 Provider ,并验证服务可用性;
- 启动 Consumer ,并验证服务可用性;
- 对系统进行预热,持续 30 秒;
- 正式评测 1 分钟;
- 取正式评测的总成功请求数和最大 TPS 作为最终成绩,上报天池系统;
- 按顺序依次停止 Consumer、三个 Provider;
- 清理 Docker 实例及镜像;
- 收集日志并上传到 OSS;
- 解锁工作区,清理环境。
总结
本文结合第五届中间件性能挑战赛的赛题背景、题目场景、题目分析和评测环境与过程的角度,介绍了自适应负载均衡算法的基本设计思路,希望对即将参加比赛的同学们能有所帮助,也欢迎更多的技术同学报名参加我们的挑战赛,分享你在算法方面的思考和实践。
本文作者:
郭浩,花名项升,阿里巴巴中间件高级开发工程师,专注于 Queueing Theory,RPC 框架和高性能分布式系统构建。