高并发场景下如何保证系统稳定性

2022-11-22 15:40:31 浏览数 (2)

导语

微服务产品团队为了广大开发者朋友们可以更好的使用腾讯云微服务产品,将持续为大家提供微服务上云快速入门的指引性文档,内容通俗易懂易上手,本篇为本系列的第二篇,为开发者朋友们详解高并发场景下限流的解决方案,欢迎大家收看。

作者简介

刘远

腾讯云泛互联网首席解决方案架构师

本篇文章将从以下四个方面为大家详解高并发场景限流解决方案:

  1. 秒杀场景架构概述
  2. 限流实现原理及方案选型
  3. 限流配置实践
  4. 云书城沙盒环境演示

秒杀场景架构概述

场景特点

在电商行业里,商家经常会做商品促销的活动,来进行品牌推广或吸引更多客户访问,在这种大促的场景下,通常会有高并发流量进入系统,也就是我们俗称的秒杀场景。在这种场景下,一般会遇到四个典型的特征。

  • 瞬时请求量大,商品价格低廉,吸引大量用户在活动开始时进行抢购。
  • 热点数据,指定部分商品参与活动,大量用户浏览量相对集中。
  • 避免超卖,因商品让利较多,商家为控制成本,所以数量有限。
  • 不能影响其他业务,秒杀活动同时其他业务也需要正常进行。

在遇见以上特征带来的技术难题时,要如何保证系统正常运行呢?主要有以下几个设计要点:

  • 秒杀子系统与主站资源隔离;
  • 系统需要具备限流能力,能够消化掉秒杀开始瞬间的巨大流量;
  • 系统需要具备快速扩展能力;
  • 削峰填谷,避免写流量压垮数据库;
  • 热点商品提前缓存,通过缓存承载读流量;
  • 库存增减需要保证数据一致性。

架构目标

为保证活动的顺利开展,业务系统稳定,需要对承载高并发流量的架构进行合理改造。通常来讲,改造后的架构需要具备如下三个特点:

  • 高性能:能够承载秒杀时较高的读写流量,保证响应时长在可接受的范围内,并兼顾数据一致性。
  • 高可用:保证系统不宕机,即使发生故障,过载保护也能将故障控制在小范围内,不会影响核心业务运行。
  • 高扩展:系统具备水平/垂直扩展能力,避免单个服务成为性能瓶颈。

改造示例

下图是一个常见的电商平台架构,从上到下分别是流量链路途径的客户端、接入层、应用层以及数据层。在这样一个典型的架构上我们该如何改造,以实现高并发承载能力呢?

参考上图,首先会在③位置,接入层网关对南北流量的超额部分限流,避免后端系统过载,保证业务正常运行。

接下来,①、②、⑦这里进行读缓存的优化:①位置,客户端对部分变化不灵敏的数据进行本地缓存,减少后端读取压力;②位置,CDN 缓存图片、CSS、JS 等静态文件,就近加速访问,减少后端读取压力;⑦位置,Redis 缓存热点数据,分担数据库查询压力,这三部分都是为了实现读优化的性能改造。

写数据的优化,在⑤位置,常使用 MySQL 进行读写分离方式部署,多实例提升读写性能。如单实例遇到性能瓶颈时,也可同时利用水平分库分表的方式提升并发能力;⑥位置,使用消息队列进行异步解耦,以削峰填谷的方式控制请求处理速度。

最后,在④位置,应用层服务支持纵向或横向扩展,提升应用服务响应能力。微服务之间采用熔断降级的策略,实现容错处理,避免群体故障。

以上各实践均有助于解决高并发问题,但在实际设计中,架构师需要根据请求 QPS 量级,采用方案组合的形式逐步推进。具体落地进度,也要根据改造成本、资源成本、性能提升回报率等因素进行综合评估。如下图所示。

限流实现原理及方案选型

接下来我们会重点介绍阶段一和阶段二里的高并发限流能力。

什么是限流呢?限流是高并发系统中,对于服务提供方的一种常见的保护手段。通过控制 QPS 的方式,把后端服务无法承受的部分流量拒绝掉,只将能够稳定处理的流量放入进来,避免后端服务被瞬时的流量高峰冲垮,在南北向设置阈值,保障大后方的稳定性。

应用场景

  • 商品秒杀:保护网站不被高并发访问击垮;
  • 防恶意请求:防止恶意用户发送虚假流量,影响正常业务访问;防止注入、篡改或 DDos 攻击等;
  • 反爬虫:保护核心数据不被获取。

超额流量处理方式

  • 返回失败:HTTP 429 Too Many Requests;
  • 降级处理:自定义静态页面返回;
  • 请求排队:阻塞请求,一段时间后再继续处理,实现限速。

限流计数器

在限流应用的开发中,有多种代码逻辑实现,我们最常见的就是限流计数器。

固定窗口计数器(Fixed Window)

方法:通过单位时间设置,如秒、分钟、小时,采用离散计数的方法,统计这个时间段里的流量值,一旦请求大于阈值可承受范围,就会将这个请求拒绝掉。这种实现方案简单,而且内存优化,请求会在自己所属时间单位里计算,不会出现跨时间段的“阻塞现象”

问题:因时间段临界点问题,导致统计结果可能有偏差。以1s限定1000请求为例,在上一个统计时间的后0.5s进入了1000个请求,在下一个统计时间的前0.5s也进入了1000个请求,因为时间的连续性,实际1s内请求达到了2000,那限流是不符合预期的。

滑动窗口模式计数器(Sliding Window)

方法:对固定窗口的一种改进,原理类似 TCP 拥塞控制。将时间单位整合为多个区间,在区间内统计计数,统计区间逐步进行窗口滑动,解决临界点问题。

问题:内存占用较大,请求及时间戳需保留。

限流计数器设计缺陷

一般来讲,限流计数器适用于否决式限流,无法进行排队式限流,对流量“整形”,实现削峰填谷无能为力。

如果需要这样的功能,应该如何改进呢? 最直观的想法,就是使用队列,将超额的流量进行暂存,延迟进行处理。实现这种能力的算法模型,就是我们熟悉的漏桶算法。

漏桶算法

漏桶算法(Leaky Bucket)

如上图所示,网络流量和水流一样,不断的进入到系统,当我们系统的可承载能力很小的情况下,我们可以将超额的水在一个桶里暂存起来。当系统处理完前面的流量以后,后面的流量就会接着进行处理,这就起到了削峰填谷、流量限速的作用。下面为示意代码。

漏桶Golang示意代码

代码语言:javascript复制
requests := make(chan int, 5)for i := 1; i <= 5; i   {  requests <- i}close(requests)limiter := time.Tick(200 * time.Millisecond)for req := range requests {  <-limiter  fmt.Println("request", req, time.Now())}

能力:以固定的速率控制请求的访问速度;支持阻塞式限流;采用 FIFO 队列,实现简单。

问题:当短时间内有大量请求时,速率无法动态调整。即使服务器负载不高,新请求也得在队列中等待一段时间才能被响应,无法在固定时间内承诺响应,容易出现请求“饥饿”现象。

那这种问题又该如何解决呢?可以用到一个叫做令牌桶的算法。

令牌桶算法(Token Bucket)

令牌桶算法和漏桶算法的最大区别,在于这个桶里装的不再是请求,而是“通关”的令牌。每一个请求过来以后,都在队列里排队。当我们的监控系统发现大量请求到来,可以人为的增加通关令牌,快速的消耗掉这一大波的请求,使新进来的请求不会等待太长时间,从而造成饥饿现象。可以看一下下面的示意代码:

令牌桶Golang示意代码

代码语言:javascript复制
limiter := make(chan time.Time, 3)for i := 0; i < 3; i   {  limiter <- time.Now()}
go func() {  for t := range time.Tick(200 * time.Millisecond) {  limiter <- t  }}() // token depositor, dynamic rate
requests := make(chan int, 5)for i := 1; i <= 5; i   {  requests <- i}  close(requests)for req := range requests {  <-limiter  fmt.Println("request", req, time.Now())}

单机限流 vs 分布式限流

单机限流

概念:针对单个实例级别的限流,流量限额只针对当前被调实例生效。每一个实例都会有一个自己的限流值,当请求到达这个实例后,会进行计数,一旦超过现在可以接受的阈值后,就会直接拒绝请求。

问题:当我们做一个生产环境部署的时候,肯定不会只有一个网关,可能会有五个、十个,一个集群的网关。那么这个集群里面的每一个实例,它是没有全局感知的,每个实例都只能看见自己的限流值,无法达到共识,这种情况下很可能限流并不符合预期。

分布式限流

概念:针对服务下所有实例级别的限流,多个服务实例共享同一个全局流量限额。

方法:将所有服务的统计结果,存入集中式的中间件中,常用缓存实现如Redis,etcd,以实现集群实例共享流量配额;通过分布式锁、信号量或原子操作等控制方法,解决多实例并发读写问题。

衍生问题:获取配额会增加网络开销,处理能力会有所降低,如何解决?集中式限流中间件不可用时,流量如何应对?

分布式限流实现思路

我们先来看看,实现一个简单的分布式限流,步骤会有哪些:

  1. 发令牌的进程,和各个限流进程,通过统一中间件(如 Redis、Etcd 等)进行交互;
  2. 发令牌进程在中间件上设置限流进程个节点;每个节点里,按阈值(如分钟)设置该节点的“令牌”数量,如 key = /token/1或者/token/2,value =  ratelimit = 10;
  3. 限流进程将节点 value,作为令牌使用,获取以后做原子性减一操作;
  4. 一旦进程当前节点 value 不够,按照环形访问方式,使用下一个限流进程的令牌。

问题:刚才上面提到的两个衍生问题,在这样的实现下,如何解决呢?

方案:

  1. 各限流器对中间件,可以进行batch更新,通过牺牲部分准确率,换取访问压力减少,提高流控性能。
  2. 当中间件不可用时,当前实例可配置拒绝所有请求,或让流量正常通过。也可配置选用本地配额进行短路处理。

限流方案选型

Redis

使用“INCR“和“EXPIRE”进行代码实现,或使用redis-cell模块。

Nginx

官方限速模块,采用漏桶算法实现:limit_req_module: 限制 IP 在单位时间内的请求数;limit_conn_module: 限制同一时间连接数。

云原生网关(如Kong,APISIX)

以插件的方式提供,支持多种限流方案,支持分布式限流:Kong:固定窗口,滑动窗口,令牌桶;APISIX:固定窗口,漏桶。

服务治理中心(如北极星,Istio,Sentinel)

同时提供限流和熔断功能,且可对服务间流量进行细粒度治理,如就近访问等。

  • limit_req_module: 限制 IP 在单位时间内的请求数;
  • limit_conn_module: 限制同一时间连接数。

云原生网关(如Kong,APISIX)以插件的方式提供,支持多种限流方案,支持分布式限流:

  • Kong:固定窗口,滑动窗口,令牌桶;
  • APISIX:固定窗口,漏桶。

 服务治理中心(如北极星,Istio,Sentinel)同时提供限流和熔断功能,且可对服务间流量进行细粒度治理,如就近访问等。

限流配置实践

接下来我们看一下,在云原生网关上如何配置限流。

云原生网关限流

云原生优势

  • 减少自建网关的运维成本;
  • 降低服务器资源成本;
  • 100%兼容开源网关 Kong 的 API。

限流配置

  • 支持秒、分钟、小时、天、月、年等多时间维度单独或组合配置限流值;
  • 支持匀速排队;
  • 支持自定义返回的能力,设置返回状态码、返回内容和返回头;
  • 支持按Consumer、Credential、Ip、Service、Header、Path 等多个维度进行限流。

限流统计策略

  • Local:计数值保存在 Nginx 本地内存中,性能最高,不适合集群部署模式(单节点部署时推荐);
  • Cluster:计数值保存在 Kong 的数据库 PostgreSQL 中,性能较差,不适合高并发场景;
  • Redis:计数值保存于外部 Redis,适合集群部署场景,性能较高,需要额外的 Redis 组件(集群部署时推荐)。

服务治理中心限流

对应的,在服务治理中心—北极星上配置限流。

接入层服务流量治理

  • 支持服务/接口/标签的限流能力;
  • 支持快速失败及匀速排队两种处理方式;
  • 支持秒、分钟、小时、天等时间微服务间的限流能力。

服务间调用流量治理

  • 故障熔断,基于服务调用的失败率和错误数等信息对故障资源进行剔除。
  • 访问限流,支持服务/接口/标签多级限流能力,提前限制超过阈值的流量。

云书城沙盒环境演示

我们模拟了一个在线的云书城。它具有多个微服务模块,比如收藏功能、购买功能,用户管理功能,订单查询功能等。在秒杀场景下,系统增加了一个秒杀子系统,专门为大促活动时,商品秒杀使用,先来看下架构图。

从最北向进来的流量会首先经过云原生网关,到达商城主页。接下来流量进入业务网关层,它来做后端服务间 gRPC 的调用管理,最后是各微服务功能单元,通过业务逻辑进行分割。

接下来通过沙盒环境,演示云书城在大促期间,如何应对高并发流量的访问。

读写优化及扩缩容方案

横向扩缩容-TKE/EKS

演示视频中有使用到扩容的功能,这里我们简单讲解一下服务扩缩容。

熟悉 K8S 的同学都知道,采用 Scale 命令可以将服务的副本数提升,但是在限流的场景下,或者在大促时,这样手动操作肯定不现实。一个更好的方案是采用腾讯云上 TKE/EKS 的 HPC 功能,它具有定时扩缩容的能力,针对系统负载评估值,提前做好准备。配合 HPA 功能,针对 QPS 或系统负载进行动态的调整,将服务的承载能力,维持在一个合理的水位上。HPC 与 HPA 相配合,基本可以做到流量高峰时自动扩容,避免系统崩溃;流量低谷时自动缩容,节约成本。

产品优势

  • HPC 组件的作用:定时执行 pod 扩容或缩容的动作。
  • 选择 HPC 的原因:秒杀场景具有流量瞬间爆发式增长的特征,HPA 组件扩容需要1分钟左右,这段时间可能导致服务崩溃,HPC 组件可以根据秒杀开始时间设定提前扩容。HPA 组件可以作为补充,形成双重保证。
  • 节点池配置:无需事先购买节点,因资源不足而无法调度实例时,实现自动扩缩容,节约成本。

异步解耦-TDMQ Pulsar

在秒杀场景里,经常会对写请求和读请求进行优化。

写请求的优化,我们一般会想到,通过异步的方式来解耦数据层的访问,而不是直接将请求打到数据层上,因为数据层可能是系统里最薄弱的一个环节。我们将一些订单的处理或者用户购买信息的处理,放在消息队列里,这种设计逻辑和网关限流排队是一致的,目标都是以可控的方式,将系统外部的请求,维持在可承受范围内。

腾讯云有一款产品叫做 TDMQ Pulsar ,它以存算分离方式实现,对于快速扩容更有优势,产品保证了上方的计算层处于一个无状态的部署模式,对于自身的扩容速度是非常快的。另外 TDMQ Pulsar 数据层的设计机制,使得它和 Kafka 的最大区别,在于不限制 Topic 分区数,这样我们可以启动更多的 Consumer 来提升消费吞吐量。在秒杀场景中处理订单的消费,是会非常有帮助的。

产品优势

  • 采用 BookKeeper 协议实现数据强一致性;
  • 存算分离的架构带来灵活的横向扩展能力 ;
  • 高性能低延迟,单集群 QPS>10万;
  • 不同于 Kafka,TDMQ Pulsar 的消费者数量不受限于 Topic 的分区数,可启动更多的消费者提升处理能力;
  • 支持全局/局部顺序消息、定时消息、延时消息,满足各种业务需求。

热数据缓存-TDSQL Redis

上面说了写请求的优化,接下来再说一下读请求的优化。

读请求前面提到,可以在客户层进行缓存,也可以在 CDN层进行缓存,但更重要的是需要在数据库前也进行一次缓存,使得读请求不会直接到达系统最薄弱的环节——数据库,形成一个“冲突缓存带”。这里我们常将一些热点数据放在 Redis 里来供大促期间使用。

产品优势

  • 超高性能;标准版10万 QPS;集群版支持千万级 QPS;
  • 自动容灾切换;双机热备架构;主机故障后,访问秒级切换到备机,无需用户干预;
  • 在线扩容;控制台一键操作扩容;扩容过程中无需停服。

标准版架构

集群版架构

总结

除了本文提到的高并发秒杀场景外,在互联网服务的很多场景下,当系统希望实现高可用、高性能、高扩展的设计目标,都会使用到腾讯云云原生网关产品所提供的能力,比如灰度发布、全链路染色、多环境路由和多活容灾等架构。

云原生网关(Cloud-Native Gateway)是腾讯云基于开源网关Kong推出的一款高性能高可用的网关产品,100%完美兼容开源。同时提供TKE/EKS集群直通,Nacos/Consul/Polaris/Eureka 注册中心对接,实例弹性扩缩容等能力,并有特色能力插件增强,显著减少用户自建网关带来的开发及运维成本。另外,多可用区部署的模式,也保证了业务连续性,避免单可用区故障带来的服务中断。

无论在微服务架构下还是传统Web架构下,云原生网关都能以流量网关、安全网关和服务网关所需要的各种能力特性,为业务云上部署提供助力。

往期

推荐

《微服务上云快速入门指引》

《Apache Pulsar 在微信大流量实时推荐场景下的实践》

《好未来基于北极星的注册中心最佳实践》

《百万级 Topic,Apache Pulsar 在腾讯云的稳定性优化实践》

《预告|ArchSummit 全球架构师峰会杭州站即将盛大开幕》

《PolarisMesh北极星 V1.11.3 版本发布》

《Spring Cloud Tencent 1.7 版本最新发布》

《腾讯云微服务引擎 TSE 产品动态》

《千亿级、大规模:腾讯超大 Apache Pulsar 集群性能调优实践》

《云原生时代的Java应用优化实践》

《全面兼容Eureka:PolarisMesh(北极星)发布1.5.0版本》

《全面拥抱Go社区:PolarisMesh全功能对接gRPC-Go | PolarisMesh12月月报》

《SpringBoot应用优雅接入北极星PolarisMesh》

扫描下方二维码关注本公众号,

了解更多微服务、消息队列的相关信息!

解锁超多鹅厂周边!

戳原文,查看更多 微服务引擎 TSE 

的信息!

点个在看你最好看

0 人点赞