[关闭]
@xuemingdeng 2017-02-23T13:50:53.000000Z 字数 3322 阅读 1039

Shopify的闪购限流解决方案

闪购,或者秒杀,对于现代互联网用户来说已经是一件司空见惯的事情。双十一、各大节庆日、限量供应的紧俏产品上线,大量用户在同一时间涌入网站抢购,突如其来的流量给网站带来了很大压力。那么各大电商网站是如何处理流量爆发的?

Shopify是加拿大著名的电子商务软件解决方案提供商,来自Shopify性能工程团队的Emil Stolarsky分享了Shopify的闪购限流解决方案。

第一波闪购和来自卡戴珊家族的闪购挑战

Shopify成立于2006年,在它成立的第二年,也就是2007年,Shopify经历了第一波闪购。Super Bowl在美国的收视率多年来一直名列前茅,在比赛结束之后,冠军球队的球迷们会涌入网站购买冠军球队的纪念T恤,Shopify因此第一次经历了闪购流量给网站带来的冲击。在那之后,应对像Super Bowl那样的闪购流量便成为家常便饭。

来自卡戴珊家族的Kylie Jenner也是Shopify的用户,她在Shopify上售卖自己设计的化妆品。因为她的粉丝数量众多,每次有新产品上线,粉丝们都会不约而同地冲入网站抢购。黑色星期五的热卖不过每年一次,但像Kylie这样的闪购几乎每周都在发生。在2016年2月的一次闪购中,疯狂的流量冲垮了她的店铺,同时也拖垮了整个数据库。因为数据库是共享的,直接导致其它店铺也跟着遭殃。这一事件引起了媒体的关注。Shopify需要找到更好的方案来避免同样的事故再次发生。

秘密武器:Nginx+Lua

在Shopify架构的最外层,是Nginx和OpenResty的Lua模块。他们将这个模块集成到Nginx的事件模型里,然后编写Lua脚本来处理请求消息流和响应消息流,有点类似Rack中间件。这样做的好处在于,寥寥几个工程师所做的修改就能够影响到整个站点,而且不需要对应用做大手术。

下面给出一个代码片段例子,通过Lua脚本在响应消息头里添加一个元数据。

  1. events {
  2. worker_connections 42;
  3. }
  4. http {
  5. server {
  6. listen 8080;
  7. header_filter_by_lua_block {
  8. ngx.header['X-Epoch'] = ngx.time()
  9. }
  10. location / {
  11. return 200 'Hello, World!';
  12. }
  13. }
  14. }

类似的方法可以适用于很多场景,包括将HTTP路由到多个数据中心、对请求消息流进行分片、防御DDoS攻击。

此处输入图片的描述

结算限流

Shopify团队通过使用Lua脚本在Nginx层解决负载问题,但他们仍然需要一个回压策略,因为再强大的服务器也无法满足无限制的流量增长。

回压的概念其实很简单,试想一个具有第二个出口的容器,为了不让倒入容器的液体溢出,可以通过第二个出口排出一部分液体。当一个应用无法处理更多的负载时,那么就可以通过一种友好的方式通知客户端暂缓请求的发送。速率限定就是一个很常见的回压用例。为了避免API被过度调用,可以通过速率限定对其进行限制。API的速率限定可以返回简单的错误码给客户端,但出于用户体验的考虑,对于网站的回压来说,需要使用一些精心设计的页面来代替简单的错误码。

回压策略产生了一定的效果,不过结算流程涉及了大量的写操作,仍然会拖垮部分店铺。最开始,他们为每个结算流程在MySQL数据库里创建一条记录,后续的每个步骤都会操作这条记录。当然,更为理想的方案是把整个结算流程的写操作合并到一起,变成一个最终的操作。不过时间紧迫,他们无法在短时间内实施这个方案,因为闪购活动一个接着一个,如果他们要实施这个方案,必然会引起服务中断,对闪购活动造成影响。所以在完全使用新方案之前,他们需要一个能够快速实施的临时过渡方案。

漏桶算法

Shopify在系统的最外层使用了漏桶算法(leaky bucket algorithm)对用户流量进行限流。系统根据自身的处理能力设置了一个流量上限,在某一段时间内,最多只有这么多的请求会进入到系统内部,超出的请求将被拒绝服务。对于那些被拒绝的请求,系统需要通知客户端何时再重试。

此处输入图片的描述

为了获得良好的用户体验,被拒绝的请求被重定向到一个排队页面。排队页面是由Shopify的平台用户提供的,Shopify在页面里注入了一段JavaScript脚本,这段脚本对“/checkout”端点发起轮询,如果轮询请求通过了漏桶,客户端会分配到一个经过安全签名的cookie,用于识别后续的session。如果客户端JavaScript被禁用,他们会使用meta refresh代替。

此处输入图片的描述

结算队列

Shopify使用漏桶算法成功地对流量进行了限流,但新的问题又接踵而至。有的顾客需要在排队页面等上40分钟,而整个闪购过程可能也只有短短的40分钟,甚至更短(在国内,紧俏的商品通常在几分钟内就被抢购一空,有的甚至是秒光),这对于顾客来说是一种很糟糕的体验。

问题是,真有那么多人在排队吗?竟然要排上40分钟那么久?后来Shopify意识到一个问题,他们让用户在排队页面等待,并通过向“/checkout”端点发起轮询再次发出结算请求,但轮询的时间间隔是随机的,所以有可能出现一直被拒绝的情况。所以这种排队是不平等的,如果有人结算成功就跟中了彩票一样幸运,而那些等了40分钟的人只能怪他们运气不好。

Shopify需要一个公平的排队机制。

时间戳

为了让排队机制更加公平,Shopify为每一个结算请求启用了时间戳。每个用户在发起第一个结算请求时会获得一个时间戳,如果请求没有通过漏桶,那么时间戳就会被保存起来。当用户再次发起轮询,之前保存的时间戳就会被用来做比对,时间戳排名靠前的会优先通过漏桶。这么一来,对于获得较早时间戳的用户,当他们的请求再次达到,可以确保不会落在时间戳靠后的请求后面。

这种方式虽然提高了公平性,但引入了数据存储,很容易成为新的故障点。而且如果系统需要跨多个数据中心,数据中心之间的数据复制会拖慢整个系统。所以他们需要找到一个不数据存储的解决方案。

新的方案是使用阈值。阈值是什么?可以打个形象的比喻:有个很受欢迎的餐厅,每次去这家餐厅吃饭的顾客人数都超出了它的服务能力,所以顾客需要排队。排队的顾客人手一个号码牌,每过一段时间,服务员就会出来喊道:“请第X号之前的顾客到里面用餐”。这样,餐厅就不需要每次比对顾客之间的号码牌,而是比较号码牌和服务员嘴里所说的那个号码。服务员每次所喊的号就是阈值,这个值会根据系统的处理能力动态变化,而顾客手里拿的号码牌就是那个时间戳,这个时间戳会被放在经过安全签名的cookie里,所以就没必要存储时间戳了。那么接下来的问题变成了该如何计算这个阈值。

PID控制器

为了计算出这个动态的阈值,也就是服务员口中的那个号码,Shopify在系统中使用了PID控制器。阈值需要根据实时的流量情况和系统处理能力进行动态调整,PID控制器是实现动态调整的关键组件。PID控制器是一个无限循环的自反馈组件,假设给定了一个预期的系统状态,PID控制器会计算当前状态与预期状态之间的差距(也就是系统错误值),对当前状态进行纠正,向预期状态靠拢。系统状态不断地发生变化,PID控制器根据反馈计算错误值,再次把系统带向预期状态,不断重复这样的过程。

此处输入图片的描述

关于PID控制器的工作原理,也有一个形象的比喻。想象一下房间的温控系统,假设预期温度为23度,当房间实际温度为22度时,控制器发现此时的温度与预期状态相差1度(也就是说错误值为1度),加热器就会被启动,通过提升温度来纠正错误值。过了一会儿,温度上升到24度,控制器发现此时的温度超过了预期状态(又出现了错误值),于是加热器被关闭,空调被打开,通过降温来纠正错误值。控制器不断地检测房间温度,让温度保持在预期的状态,这就是PID控制器的工作原理。

公平性比较

通过以下两张图片可以看出新的排队方案与之前的排队方案在公平性方面的差异。图中黄色代表平均排队时间,蓝色代表P95排队时间,紫色代表中值排队时间。

此处输入图片的描述

从图1可以看出,不公平的排队方案各条线之间的间隔较大,这也意味着用户的排队等待时间相差较大。

此处输入图片的描述

图2的各条线几乎是重合的,也就是说用户的排队等待时间几乎是相等的。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注