学会流量过滤和串行化,提升你的架构设计能力
Posted 技术管理者训练营
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学会流量过滤和串行化,提升你的架构设计能力相关的知识,希望对你有一定的参考价值。
如果我们的秒杀单机并发能力需要达到15万QPS 以上,在设计系统时你有没有想过:这 15 万请求是否都需要读写 Redis ?秒杀系统又是如何判断哪些请求应该读写 Redis?在设计系统时,这是我们需要考虑的因素,因为真实业务场景下,不可能让所有请求都读写 Redis。一来,整个集群每秒上千万请求都直接读写 Redis,这得需要多少 Redis 实例,得花多少钱呢;二来,每秒千万个请求中并不是每个请求都是合法的,如果这些请求都读写 Redis,很难保证 Redis 中的数据不乱套。
事实上,千万流量的秒杀系统中,至少有 80% 的流量是不需要读写 Redis 的。非法请求会在进入系统时被过滤掉,正常请求一部分使用多级缓存读写数据,剩下的才需要读写 Redis。那么,我们该如何处理流量,让它们安稳地请求到 Redis 呢?接下来给你分别介绍下。
如何设计流量拦截器?
由于秒杀活动的流量会远超平常,一般在流量入口,系统就要把那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。为了实现这个过滤功能,就需要我们设计流量拦截器。 什么是流量拦截器呢?通常,流量拦截器有多层,就像一个漏斗或者倒金字塔。在容量上,你可以联想到缓存金字塔,流量拦截器跟它正好相反。比如,位于上层的流量拦截器可能会负责过滤掉 40% 的流量,位于中间层的可能过滤掉 30% 的流量,而位于底层的则可能过滤掉 20% 的流量。那么,流量拦截器的最上层是什么样子的呢?
最上层流量入口是网关和 WAF(Web Application Firewall,Web 应用防火墙),它们会拦截大部分非法请求,比如一些恶意攻击的请求,一些用秒杀器疯狂刷接口的请求。在设计上,这一层通常采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法,在顶层入口处就拦截掉这些请求。这样获得的收益也是最大的,能为下游业务系统节省大量资源。
经过上层拦截器处理后,还是会有一些漏网之鱼,比如“黄牛”。于是就有了中间层拦截器,中间层拦截器主要是为了识别出不具备抢购资格的用户,并拦截他们的流量。以黄牛为例,早期的黄牛主要通过秒杀器来刷走商品,后来秒杀器被封禁后,他们改为采用多个账号同时参与秒杀活动。“黄牛”的这种行为无疑严重违反了秒杀活动的公平性,也损害了正常用户的利益,为此,我们就需要把这类流量拦截掉。
具体如何反黄牛呢?反黄牛的前提是需要先识别出谁是黄牛,这就需要一份黄牛名单了。那么,这份黄牛名单是如何产生的呢?通常它会由数据分析系统根据大量订单信息和用户信息生成,然后提供给秒杀接口服务使用。像多个账号每次都在一个 IP 下参与秒杀,每次抢到的商品都不是给自己账号用,或者通过自制秒杀工具抢到商品后快速支付,等等。虽然对于后端服务来说有些行为看着像正常用户,但是,在大数据分析下,还是能抓到一些蛛丝马迹。一般数据分析系统会定期生成黄牛名单,比如每天凌晨 3 点钟。然后秒杀接口服务会将黄牛名单更新到内存中。在秒杀活动进行时,秒杀接口服务会从请求中拿到账号信息后进行匹配。如果匹配到了,说明该账号是黄牛账号,需要拦截掉。
除了黄牛外,还有两部分不具备资格的流量需要拦截掉:
1)由未登录或者登录过期的用户产生的流量,当他们点击秒杀购买时,我们可以让这些用户跳转到登录页进行登录;
2)如果用户购买数量已经达到该场次商品数量限制,此时需要提醒用户已经参与过该场次,请勿重复参与。
那位于下层的拦截器负责做什么呢?我们知道,秒杀活动中库存数量远低于参与秒杀的用户数,于是如何快速判断哪些用户抢不到库存,就是个非常关键的问题,而这正是下层拦截器的核心工作。虽然前面两层拦截器已经拦截了大量请求,但下层拦截器面临的流量还是很大,单节点 QPS 至少上万。因此,下游拦截器判断库存的时候,对性能要求非常高。需要怎么做呢?通常是由秒杀服务将库存数据在本地内存中缓存一份,用于初步判断库存资格。在 Go 语言中,我们可以用 map 来缓存库存数据,利用锁来控制并发扣减库存。由于完全是在本地内存中操作,性能要比访问 Redis 好很多。要注意的是,本地内存缓存中的库存数据是比较粗略的,时间长了也容易出现误差,不能作为最终的扣减依据。所以通常需要有个定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。这个更新速度不能太快,也不能太慢。太快的话,可能导致内存中已扣减的库存还原成 Redis 中未扣减的库存;如果太慢,因超时关单归还的库存会无法及时同步到内存缓存中。我们可以根据流量大小设定一个合理的值,比如 100 毫秒同步一次。另外,内存缓存中的库存大小也需要注意按比例缩小。如果总共有 1000 个库存、50 个秒杀节点,平均分摊的话每个节点分到 20 个库存。实际上,每个节点需要略微高于平均值,以确保足够多的请求漏下去,将 Redis 中的库存扣减完,达到最大的活动效果。
由于内存中的库存数据不是十分准确,拿到库存资格的用户可能比实际库存要大,最终还是要通过从 Redis 中扣减库存来判断用户是否抢购成功。那么,如何保障 Redis 的并发压力不会超过它的承载能力呢?那就是下面要介绍的串行化。
如何将流量串行化?
所谓串行化,是指通过排队的方式将无序的并发流量整理成有序的串行流量。在 Redis 集群模式出现以前,大多数 Redis 都是采用一主多从的模式,写操作由主节点执行。由于一主多从模式下主节点只有一个节点,因此 Redis 的写操作并发能力远低于读操作并发能力。在千万级并发流量下,虽然前面我们通过流量拦截器将大部分流量过滤掉了,但剩下的流量也不小。比如虽然过滤了 990 万,也就是 99% 的流量,但还剩下 10 万流量。如果这些流量都去扣减库存,会对 Redis 主节点产生巨大压力。怎么办呢?这就需要流量串行化。具体要怎么做呢?
总的来说,秒杀中的串行化主要是通过队列和分布式事务来实现的,具体分三步。
第一步,秒杀服务在扣减内存缓存中的库存成功后,将流量转入到它的内存队列中,进行初步排序,为写 MQ 做准备。这里注意的是,要控制好内存队列缓冲区大小,太小可能会导致并发写入的时候大量请求被阻塞,可以将大小设置为消费端速度的两倍。比如消费端速度是 1000 QPS,则缓冲区可以设定为可以缓存 2000 个请求。
第二步,使用一个线程或者协程以固定速度从内存队列中消费流量,将流量写入到像 RabbitMQ 这种 MQ 中。这一步主要是为了减轻 MQ 的并发压力,需要根据 MQ 的承载能力计算好速度。比如 MQ 的并发承载能力为 5 万 QPS,秒杀有 50 个节点,则每个节点的速度应当低于 1000 QPS。保留 20% 余量的话,每个节点的速度可以设定为 800 QPS。
第三步,使用另一个线程或协程,以低于第二步中的固定速度从 MQ 中消费流量,然后利用 Redis 事务从 Redis 中扣减库存,避免超售。最终,根据扣减库存的结果,给用户返回对应的提示信息。
需要注意的是,秒杀服务有多个节点,不同节点需要用不同的队列,但单个节点的内部,必须用同一个队列。这么做是因为多个节点间是不能共享客户端连接的,只有在节点内部消费自己的流量,才能给用户返回处理结果。看到上面这三步,不知道你有没有联想到限流器。没错,第二步和第三步中的固定速度就是用限流器来实现。可以说,串行化的核心思路就是:使用队列将请求进行排队、限流,使用分布式锁对资源进行原子操作。
总之,单节点中实现漏斗模型比较容易,但在一个分布式系统,如秒杀系统中,需要考虑诸多因素。比如在计算队列消费速度参数的时候,需要将下游系统的承载能力平均分摊到每个秒杀节点上。由于秒杀节点数可能随时调整,秒杀服务需要做到动态计算队列消费速度。理想的方法是将这种元数据作为配置存放到 KV 存储,比如 ETCD 中,然后每个秒杀节点都实时监听配置变更,并重新计算各自的队列消费速度参数。这样可以避免因秒杀节点数增加而导致下游系统请求量增加被压垮。
以上是关于学会流量过滤和串行化,提升你的架构设计能力的主要内容,如果未能解决你的问题,请参考以下文章