流量洪峰中如何设计弹性微服务架构
注:发言仅代表讲师个人观点
讲师介绍
go-zero框架作者
好未来资深专家、GitHub 2900+的星标
2001年南京大学毕业后先后就职于两家美企从事高性能计算和互联网后端研发工作
2007年开始合伙创业并任CTO,11年的社交App创业和并购经历
有近20年的开发和微服务架构经验,10多年的技术团队管理经验
《流量洪峰中如何设计弹性微服务架构》
当技术架构转型到微服务架构时,随着业务流量增加,如何保障服务的高可用,如何针对服务进行有效的治理?
我是在2013年开始做社交APP从原来单体改到微服务体系。在2014年到2015年的时候已经大规模上了go的微服务,整个场景切入到go。在今年8月7日,我把这么多年沉淀的go微服务体系整理开源出来。
本文给大家讲一下,当切入到go微服务体系上,在流量洪峰突然到来的时候怎样保证服务的稳定?今年在线教育在疫情之下的流量洪峰来的非常猛烈,我们怎么样去应对流量洪峰下系统的稳定性呢?我从以下几个方面进行分享:
数据拆分
缓存设计
微服务分层设计
微服务治理能力
概览
数据拆分
我们要想做好微服务的稳定,数据最重要。当我们服务稳定出现问题的时候,很多都是数据拆分不够清晰导致DB挂了,上层的服务再怎么梳理也没有办法保持稳定。而数据之上就是缓存,用大并发的系统来讲一般缓存涉及到的都是比较关键的地方,那么怎样在微服务上做分层,怎样做微服务的治理?当流量来了出现故障的时候,怎样去做可恢复弹性设计和可观测性等呢?
对于DB来讲,首先考虑是怎么样规划数据的边界。如果数据不能够清晰定义好,导致后续的过程比较苍白时我们怎么样做数据的拆分?有一个商城的例子,比如现在有用户、商品、订单、物流,都需要把数据库这一块切分清楚。每个服务只能直接访问自己的数据,在不同服务之间要通过不同的服务提供的RPC进行服务间的数据访问。而对于我们来说线上的并发量比较高,疫情期间达到将近百万QPS,在线上高并发的情况下把数据拆分开,当时并没有任何的join,但我还是谨慎的写了一个Less join, Less pain,免得大家说一个join都没有太绝对了。
缓存设计
数据拆分好以后,无论怎样DB都不可能扛住高并发的访问,而对于高并发的系统来讲缓存的设计是关键中的关键,所以在缓存设计时我们使用mysql/mongo clusters做缓存存储。对于我们常见的处理缓存来讲有三个避不开的点:
一是缓存穿透。有些伪请求假装用户频繁访问,这时缓存跟不上,这种请求就有可能把服务打挂,不停的刷不存在的用户是一个比较重要的点。我们对这种不存在数据的请求会做一分钟的缓存,一分钟里会出现不一致的问题,而在用户注册之后会把不存在的用户的缓存清掉,但是注意一定要有缓存这一步。
-
二是 缓存击穿。当一些热点Key过期时,前面的Server会一堆的请求同时打过来,这时如果不做管控,大量请求会同时打到DB上去导致DB挂掉。我们构建框架的时候对这种情况做了自动控制,在同一个进程里自动去做了保障进程内只会取一次,而在没有取到的过程中所有并发的请求都会共享一个请求的结果。高并发分布式的情况下只能全局并发一个请求,而不是进程内并发,但一般情况下不需要全局唯一。 三是缓存雪崩。在在线教育场景里面有很多请求高峰,比如说周一早上九点钟上课,突然会来一波,这种情况到下个周一可能又会再来一波,因为它的时间点是和用户行为一致。到下周一的时候,如果一堆缓存同时失效的话又会造成巨大的冲击。对于这种情况,第一次没办法避免,但是随着数据的累加需要消化,所以我们实现了一个5%的标准偏差。以7天为例,所有的缓存是分布在以这个中心点前后各八个小时,持续16个小时,里面是比较平滑的过期,保障所有的过期点会被打散,这样就能把这个高峰给削掉。
我们在做缓存的时候不只是缓存,还在缓存中做了一个比较详细的设计。像DB里如果用主键去拿,一次就可以拿到缓存记录,如果用索引去拿,需要先通过索引拿到主键再去拿。
对于缓存来讲,我们也做了这个设计,很多时候不是通过主键,而是通过唯一索引拿,通过索引再去拿到主键,再从缓存里拿到行记录。这样保证所有的行记录在缓存里只有一份,同时能保证修改或删除数据时是一致的,也不会出现多份的数据在缓存里面的情况。
在我们的内部不允许任何不过期的缓存。假如有9个集群做缓存,因为有些情况有几十个集群做缓存。现在有9个,再加1个到10个,在缓存迁移上只有十分之一缓存从原来9台迁移到新的上面去,通过这个方式让迁移变得更加平滑,不会重新来分布而是线上自动化迁移。
做缓存的都知道,缓存其实是很复杂的设计,即使把这些概念都讲清楚了也很难让大家在写业务时把缓存这件事写对了。我们只要提供一个类似MySQL的datasourse就可以全部生成出来,在框架里会自动把缓存的命中率,hit多少,miss多少,全部记在日志里,这样每个业务线就能经常看缓存的使用情况。
微服务分层设计
缓存的上面是RPC服务层,我们把每一个服务都存在前面的RPC层去提供外部的数据访问,行业通用的标准所有协议都是基于gRPC。我们用K8S和etcd做服务发现。在做负载均衡的时候,用p2c的ewma滑动平均方式。
Power of Two Choices方式是我们常见的做法。比如说现在有多个机房,因为线上的集群是多个,比如100个机器一个集群,我们正常把20个放到一个机房,一般要5个机房,就可以做到同城的多机房容灾。
对于这种情况以前的做法是需要在配置里面加zone,弄清楚这个机器的节点属于哪个zone,做一些复杂的配置去管理这个请求该去哪儿访问,通过这种方式就能比较好的进行规避。
比如说图里的A节点访问其它节点模拟了2ms的延迟,它可以比较好的把更多的请求放到本机房里,同时也确保本机房请求过多或延迟过高时会动态调度到其他的机房,这是一个动态的平衡。这里放了三个节点演示,通过数学的方式比较好的把多机房balance解决掉,而不需要用复杂的配置来完成这件事。
API层又做了外部的流控,默认管控流量的突发,做请求的鉴权,因为咱们业务需要稳定肯定要把恶意的请求过滤掉,让正常的请求打到后面去。前面做了参数的自动校验,让大家后面的服务写代码的时候尽可能少,代码越少BUG越少,这一层就做到了业务聚合。
下图是自适应熔断,常见的自适应熔断是比较硬性的方式,当来了十个请求,五个请求有问题,这个时候断开10秒试探一下,10秒以后OK了再打开。在容量器里每秒百万次的请求,十分之一的请求失败,连续失败十个的概率是非常高的,概率问题非常容易触发这种熔断。这样会把整个服务卡掉10秒导致效率很低。这时我们通过概率模型,根据请求成功的曲线来变化,当请求失败频率越大曲线越贴合它,这是一个跟随性的概率模型,我们把它全部内嵌在了框架里面。
刚才讲到可自动恢复,所有这些东西都可以自动恢复,在里面自动触发自动恢复。在熔断业界大家用的比较多,目前发现业务框架有自动恢复,其他的框架没有。我们都知道在K8S是80%的阈值去往旁边伸缩,而K8S有一个特点是每15s有个检测,4个周期检测到超限的时候才会扩。
对于一个高并发来讲,1分钟前面的服务全都打挂掉了,我们怎样去保护K8S能做自动伸缩呢?我们在服务里根据当前CPU资源使用率,滑动平均的方式去算当前CPU使用情况。当CPU占用不到80%的时候就没有办法去触发K8S做伸缩了。所以我们控制当使用率在90%的时候会把低优先级的请求按概率丢掉,当到了95%的时候会按概率丢弃高优先级的请求。
在我们的高并发场景里,最高只触发了将近94%的CPU使用,这样可以非常好的保护服务的可用性,也就是高优先级。比如说用户服务里面登录和注册是高优先级的,如果用户都不能注册和登录,后面所有的服务都没有用。但是对于用户服务里请求profile是低优先级请求,用户看不到这个信息,没关系再刷一下问题不大。洪峰流量时我们经常看到这样的情况,只要保证在90%多一点点周围动荡,让K8S能做水平伸缩对我们来讲就是比较有效的方式。
微服务治理能力
其他更多的治理能力,首先是超时,我们寻求服务的稳定就需要较好的控制超时问题。比如客户端设2秒的超时,服务端设3秒的超时,这个3秒就没有意义了。服务端是相互依赖的,从前往后第一个请求可能用了500ms,假如整个服务是1s,前面是500ms,后面再给它1s是没有意义的,在写服务时需要注意这是需要控制的。
其次是重试,很多业务人员一遇到失败就会无脑的重试三次。什么情况下触发重试?当服务有一些异常的时候才会触发重试。当无脑重试的时候,会加大后续依赖服务的压力。我们做重试时需要对每一个服务重试有规划。比如说拿20%容量出来,我的请求里面只有20%用来服务重试,当超过这个比例所有重试就得丢掉,需要保障正常的请求能优先处理。在重试的时候还要考虑时间的超时相关性,如果还有1ms再去重试就没有任何意义。
对于我们来讲,要做弹性设计和保护系统的高可用。我们需要从前往后看,Requests进来首先要做并发控制和限流。并发控制是为了防止那些恶意刷不能保障的,行业里攻击还是比较多,所以你要保护这些异常的请求不过来。过了这一层,到下一层做自适应降载,自适应降载保护本服务不会被打挂掉,当前服务接到请求,自己的服务被打挂掉了后面就拿不到请求了。所以自适应降载和弹性伸缩是结合起来做的。
这一层过了以后后面才是自适应熔断,熔断是解决后续依赖。负载均衡可以把它分布在不同的跨机房节点上去完成。
概览
除了底上一层和下面一层,一个客户端,一个DB。中间层能力也需要全部集成到框架里去,为什么在做高可用设计的时候要把这些全部集成在框架里面去呢。因为这么多的东西让业务人员去做是非常容易出错。
对我来讲,以最简单的呈现方式交给业务开发人员,对于他来讲不需要关注微服务最底层的技术只需要关心业务开发,并且我们提供了一个工具,把所有跟业务不相关的代码全部设置好,这样就规避了在写代码时一些常见的错误。本次分享基本主体就这些,更多内容请搜『go-zero微服务框架』。
精彩内容未完待续,我们下期见!
参考阅读