淘宝大秒系统设计详解

一些数据:前端

你们还记得2013年的小米秒杀吗?三款小米手机各11万台开卖,走的都是大秒系统,3分钟后成为双十一第一家也是最快破亿的旗舰店。通过日志统计,前端系统双11峰值有效请求约60w以上的QPS ,然后端cache的集群峰值近2000w/s、单机也近30w/s,但到真正的写时流量要小不少了,当时最高下单减库存tps是红米创造,达到1500/s。java

热点隔离:程序员

秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让1%的请求影响到另外的99%,隔离出来后也更方便对这1%的请求作针对性优化。针对秒杀咱们作了多个层次的隔离:redis

 

  • 业务隔离。把秒杀作成一种营销活动,卖家要参加秒杀这种营销活动须要单独报名,从技术上来讲,卖家报名后对咱们来讲就是已知热点,当真正开始时咱们能够提早作好预热。算法

  • 系统隔离。系统隔离更可能是运行时的隔离,能够经过分组部署的方式和另外99%分开。秒杀还申请了单独的域名,目的也是让请求落到不一样的集群中。数据库

  • 数据隔离。秒杀所调用的数据大部分都是热数据,好比会启用单独cache集群或MySQL数据库来放热点数据,目前也是不想0.01%的数据影响另外99.99%。apache

固然实现隔离颇有多办法,如能够按照用户来区分,给不一样用户分配不一样cookie,在接入层路由到不一样服务接口中;还有在接入层能够对URL的不一样Path来设置限流策略等。服务层经过调用不一样的服务接口;数据层能够给数据打上特殊的标来区分。目的都是把已经识别出来的热点和普通请求区分开来。后端

动静分离:数组

前面介绍在系统层面上的原则是要作隔离,接下去就是要把热点数据进行动静分离,这也是解决大流量系统的一个重要原则。如何给系统作动静分离的静态化改造我之前写过一篇《高访问量系统的静态化架构设计》详细介绍了淘宝商品系统的静态化设计思路,感兴趣的能够在《程序员》杂志上找一下。咱们的大秒系统是从商品详情系统发展而来,因此自己已经实现了动静分离,如图1。浏览器

除此以外还有以下特色:

 

  • 把整个页面Cache在用户浏览器

  • 若是强制刷新整个页面,也会请求到CDN

  • 实际有效请求只是“刷新抢宝”按钮

这样把90%的静态数据缓存在用户端或者CDN上,当真正秒杀时用户只须要点击特殊的按钮“刷新抢宝”便可,而不须要刷新整个页面,这样只向服务端请求不多的有效数据,而不须要重复请求大量静态数据。秒杀的动态数据和普通的详情页面的动态数据相比更少,性能也比普通的详情提高3倍以上。因此“刷新抢宝”这种设计思路很好地解决了不刷新页面就能请求到服务端最新的动态数据。

基于时间分片削峰

熟悉淘宝秒杀的都知道,初版的秒杀系统自己并无答题功能,后面才增长了秒杀答题,固然秒杀答题一个很重要的目的是为了防止秒杀器,2011年秒杀很是火的时候,秒杀器也比较猖獗,而没有达到全民参与和营销的目的,因此增长的答题来限制秒杀器。增长答题后,下单的时间基本控制在2s后,秒杀器的下单比例也降低到5%如下。新的答题页面如图2。

 

其实增长答题还有一个重要的功能,就是把峰值的下单请求给拉长了,从之前的1s以内延长到2~10s左右,请求峰值基于时间分片了,这个时间的分片对服务端处理并发很是重要,会减轻很大压力,另外因为请求的前后,靠后的请求天然也没有库存了,也根本到不了最后的下单步骤,因此真正的并发写就很是有限了。其实这种设计思路目前也很是广泛,如支付宝的“咻一咻”已及微信的摇一摇。

 

除了在前端经过答题在用户端进行流量削峰外,在服务端通常经过锁或者队列来控制瞬间请求。

数据分层校验

对大流量系统的数据作分层校验也是最重要的设计原则,所谓分层校验就是对大量的请求作成“漏斗”式设计,如图3所示:在不一样层次尽量把无效的请求过滤,“漏斗”的最末端才是有效的请求,要达到这个效果必须对数据作分层的校验,下面是一些原则:

 

  • 先作数据的动静分离

  • 将90%的数据缓存在客户端浏览器

  • 将动态请求的读数据Cache在Web端

  • 对读数据不作强一致性校验

  • 对写数据进行基于时间的合理分片

  • 对写请求作限流保护

  • 对写数据进行强一致性校验

秒杀系统正是按照这个原则设计的系统架构,如图4所示。

把大量静态不须要检验的数据放在离用户最近的地方;在前端读系统中检验一些基本信息,如用户是否具备秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束等;在写数据系统中再校验一些如是不是非法请求,营销等价物是否充足(淘金币等),写的数据一致性如检查库存是否还有等;最后在数据库层保证数据最终准确性,如库存不能减为负数。

实时热点发现:

其实秒杀系统本质是仍是一个数据读的热点问题,并且是最简单一种,由于在文提到经过业务隔离,咱们已能提早识别出这些热点数据,咱们能够提早作一些保护,提早识别的热点数据处理起来还相对简单,好比分析历史成交记录发现哪些商品比较热门,分析用户的购物车记录也能够发现那些商品可能会比较好卖,这些都是能够提早分析出来的热点。比较困难的是那种咱们提早发现不了忽然成为热点的商品成为热点,这种就要经过实时热点数据分析了,目前咱们设计能够在3s内发现交易链路上的实时热点数据,而后根据实时发现的热点数据每一个系统作实时保护。 具体实现以下:

 

  • 构建一个异步的能够收集交易链路上各个中间件产品如Tengine、Tair缓存、HSF等自己的统计的热点key(Tengine和Tair缓存等中间件产品自己已经有热点统计模块)。

  • 创建一个热点上报和能够按照需求订阅的热点服务的下发规范,主要目的是经过交易链路上各个系统(详情、购物车、交易、优惠、库存、物流)访问的时间差,把上游已经发现的热点可以透传给下游系统,提早作好保护。好比大促高峰期详情系统是最先知道的,在统计接入层上Tengine模块统计的热点URL。

  • 将上游的系统收集到热点数据发送到热点服务台上,而后下游系统如交易系统就会知道哪些商品被频繁调用,而后作热点保护。如图5所示。

重要的几个:其中关键部分包括:

 

  • 这个热点服务后台抓取热点数据日志最好是异步的,一方面便于作到通用性,另外一方面不影响业务系统和中间件产品的主流程。

  • 热点服务后台、现有各个中间件和应用在作的没有取代关系,每一个中间件和应用还须要保护本身,热点服务后台提供一个收集热点数据提供热点订阅服务的统一规范和工具,便于把各个系统热点数据透明出来。

  • 热点发现要作到实时(3s内)。

关键技术及优化点:

前面介绍了一些如何设计大流量读系统中用到的原则,可是当这些手段都用了,仍是有大流量涌入该如何处理呢?秒杀系统要解决几个关键问题。

 

Java处理大并发动态请求优化

 

其实Java和通用的Web服务器相比(Nginx或Apache)在处理大并发HTTP请求时要弱一点,因此通常咱们都会对大流量的Web系统作静态化改造,让大部分请求和数据直接在Nginx服务器或者Web代理服务器(Varnish、Squid等)上直接返回(能够减小数据的序列化与反序列化),不要将请求落到Java层上,让Java层只处理不多数据量的动态请求,固然针对这些请求也有一些优化手段能够使用:

 

  • 直接使用Servlet处理请求。避免使用传统的MVC框架也许能绕过一大堆复杂且用处不大的处理逻辑,节省个1ms时间,固然这个取决于你对MVC框架的依赖程度。

  • 直接输出流数据。使用resp.getOutputStream()而不是resp.getWriter()能够省掉一些不变字符数据编码,也能提高性能;还有数据输出时也推荐使用JSON而不是模板引擎(通常都是解释执行)输出页面。

 

同一商品大并发读问题

 

你会说这个问题很容易解决,无非放到Tair缓存里面就行,集中式Tair缓存为了保证命中率,通常都会采用一致性Hash,因此同一个key会落到一台机器上,虽然咱们的Tair缓存机器单台也能支撑30w/s的请求,可是像大秒这种级别的热点商品还远不够,那如何完全解决这种单点瓶颈?答案是采用应用层的Localcache,即在秒杀系统的单机上缓存商品相关的数据,如何cache数据?也分动态和静态:

 

  • 像商品中的标题和描述这些自己不变的会在秒杀开始以前全量推送到秒杀机器上并一直缓存直到秒杀结束。

  • 像库存这种动态数据会采用被动失效的方式缓存必定时间(通常是数秒),失效后再去Tair缓存拉取最新的数据。

 

你可能会有疑问,像库存这种频繁更新数据一旦数据不一致会不会致使超卖?其实这就要用到咱们前面介绍的读数据分层校验原则了,读的场景能够容许必定的脏数据,由于这里的误判只会致使少许一些本来已经没有库存的下单请求误认为还有库存而已,等到真正写数据时再保证最终的一致性。这样在数据的高可用性和一致性作平衡来解决这种高并发的数据读取问题。

 

同一数据大并发更新问题

 

解决大并发读问题采用Localcache和数据的分层校验的方式,可是不管如何像减库存这种大并发写仍是避免不了,这也是秒杀这个场景下最核心的技术难题。

 

同一数据在数据库里确定是一行存储(MySQL),因此会有大量的线程来竞争InnoDB行锁,当并发度越高时等待的线程也会越多,TPS会降低RT会上升,数据库的吞吐量会严重受到影响。说到这里会出现一个问题,就是单个热点商品会影响整个数据库的性能,就会出现咱们不肯意看到的0.01%商品影响99.99%的商品,因此一个思路也是要遵循前面介绍第一个原则进行隔离,把热点商品放到单独的热点库中。可是无疑也会带来维护的麻烦(要作热点数据的动态迁移以及单独的数据库等)。

 

分离热点商品到单独的数据库仍是没有解决并发锁的问题,要解决并发锁有两层办法。

 

  • 应用层作排队。按照商品维度设置队列顺序执行,这样能减小同一台机器对数据库同一行记录操做的并发度,同时也能控制单个商品占用数据库链接的数量,防止热点商品占用太多数据库链接。

  • 数据库层作排队。应用层只能作到单机排队,但应用机器数自己不少,这种排队方式控制并发仍然有限,因此若是能在数据库层作全局排队是最理想的,淘宝的数据库团队开发了针对这种MySQL的InnoDB层上的patch,能够作到数据库层上对单行记录作到并发排队,如图6所示。

你可能会问排队和锁竞争不要等待吗?有啥区别?若是熟悉MySQL会知道,InnoDB内部的死锁检测以及MySQL Server和InnoDB的切换会比较耗性能,淘宝的MySQL核心团队还作了不少其余方面的优化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch,配合在SQL里面加hint,在事务里不须要等待应用层提交COMMIT而在数据执行完最后一条SQL后直接根据TARGET_AFFECT_ROW结果提交或回滚,能够减小网络的等待时间(平均约0.7ms)。据我所知,目前阿里MySQL团队已将这些patch及提交给MySQL官方评审。

大促热点问题思考:

以秒杀这个典型系统为表明的热点问题根据多年经验我总结了些通用原则:隔离、动态分离、分层校验,必须从整个全链路来考虑和优化每一个环节,除了优化系统提高性能,作好限流和保护也是必备的功课。

 

除去前面介绍的这些热点问题外,淘系还有多种其余数据热点问题:

 

  • 数据访问热点,好比Detail中对某些热点商品的访问度很是高,即便是Tair缓存这种Cache自己也有瓶颈问题,一旦请求量达到单机极限也会存在热点保护问题。有时看起来好像很容易解决,好比说作好限流就行,但你想一想一旦某个热点触发了一台机器的限流阀值,那么这台机器Cache的数据都将无效,进而间接致使Cache被击穿,请求落地应用层数据库出现雪崩现象。这类问题须要与具体Cache产品结合才能有比较好的解决方案,这里提供一个通用的解决思路,就是在Cache的client端作本地Localcache,当发现热点数据时直接Cache在client里,而不要请求到Cache的Server。

  • 数据更新热点,更新问题除了前面介绍的热点隔离和排队处理以外,还有些场景,如对商品的lastmodifytime字段更新会很是频繁,在某些场景下这些多条SQL是能够合并的,必定时间内只执行最后一条SQL就好了,能够减小对数据库的update操做。另外热点商品的自动迁移,理论上也能够在数据路由层来完成,利用前面介绍的热点实时发现自动将热点从普通库里迁移出来放到单独的热点库中。

 

按照某种维度建的索引产生热点数据,好比实时搜索中按照商品维度关联评价数据,有些热点商品的评价很是多,致使搜索系统按照商品ID建评价数据的索引时内存已经放不下,交易维度关联订单信息也一样有这些问题。这类热点数据须要作数据散列,再增长一个维度,把数据从新组织。

1、题目

1, 这是一个秒杀系统,即大量用户抢有限的商品,先到先得

2, 用户并发访问流量很是大, 须要分布式的机器集群处理请求

3, 系统实现使用Java

2、模块设计

1, 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不一样的机器上。

2, 用户请求预处理模块:判断商品是否是还有剩余来决定是否是要处理该请求。

3, 用户请求处理模块:把经过预处理的请求封装成事务提交给数据库,并返回是否成功。

4, 数据库接口模块:该模块是数据库的惟一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。

第一部分就很少说了,配置HTTP服务器便可,这里主要谈谈后面的模块。

用户请求预处理模块

通过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,若是后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败便可,没必要再进一步发送事务了,示例代码能够以下所示:

package seckill;
import org.apache.http.HttpRequest;
/**
 * 预处理阶段,把没必要要的请求直接驳回,必要的请求添加到队列中进入下一阶段.
 */
public class PreProcessor {
	// 商品是否还有剩余
	private static boolean reminds = true;
	private static void forbidden() {
		// Do something.
	}
	public static boolean checkReminds() {
		if (reminds) {
			// 远程检测是否还有剩余,该RPC接口应由数据库服务器提供,没必要彻底严格检查.
			if (!RPC.checkReminds()) {
				reminds = false;
			}
		}
		return reminds;
	}
	/**
	 * 每个HTTP请求都要通过该预处理.
	 */
	public static void preProcess(HttpRequest request) {
		if (checkReminds()) {
			// 一个并发的队列
			RequestQueue.queue.add(request);
		} else {
			// 若是已经没有商品了,则直接驳回请求便可.
			forbidden();
		}
	}
}

并发队列的选择

Java的并发包提供了三个经常使用的并发队列实现,分别是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。

ArrayBlockingQueue是初始容量固定的阻塞队列,咱们能够用来做为数据库模块成功竞拍的队列,好比有10个商品,那么咱们就设定一个10大小的数组队列。

ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。

LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。

因为咱们的系统入队需求要远大于出队需求,通常不会出现队空的状况,因此咱们能够选择ConcurrentLinkedQueue来做为咱们的请求队列实现:

package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
    public static ConcurrentLinkedQueue<HttpRequest> queue =
            new ConcurrentLinkedQueue<HttpRequest>();
}

用户请求模块

package seckill;
import org.apache.http.HttpRequest;
public class Processor {
	/**
	 * 发送秒杀事务到数据库队列.
	 */
	public static void kill(BidInfo info) {
		DB.bids.add(info);
	}
	public static void process() {
		BidInfo info = new BidInfo(RequestQueue.queue.poll());
		if (info != null) {
			kill(info);
		}
	}
}
class BidInfo {
	BidInfo(HttpRequest request) {
		// Do something.
	}
}

数据库模块

数据库主要是使用一个 ArrayBlockingQueue 来暂存有可能成功的用户请求。

package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
 * DB应该是数据库的惟一接口.
 */
public class DB {
	public static int count = 10;
	public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
	public static boolean checkReminds() {
		// TODO
		return true;
	}
	// 单线程操做
	public static void bid() {
		BidInfo info = bids.poll();
		while (count-- > 0) {
			// insert into table Bids values(item_id, user_id, bid_date, other)
			// select count(id) from Bids where item_id = ?
			// 若是数据库商品数量大约总数,则标志秒杀已完成,设置标志位reminds = false.
			info = bids.poll();
		}
	}
}

3、结语

看起来大致这样应该就能够了,固然还有细节能够优化,好比数据库请求能够都作成异步的等等。

 

现现在,春节抢红包的活动已经逐渐变成你们过年的新风俗。亲朋好友的相互馈赠,微信、微博、支付宝等各大平台种类繁多的红包让你们收到手软。鸡年春节,链家也想给15万的全国员工包个大红包,因而咱们构建了一套旨在支撑10万每秒请求峰值的抢红包系统。经实践证实,春节期间咱们成功的为全部的小伙伴提供了高可靠的服务,红包总发放量近百万,抢红包的峰值流量达到3万/秒,最快的一轮抢红包活动3秒钟全部红包所有抢完,系统运行0故障。

红包系统,相似于电商平台的秒杀系统,本质上都是在一个很短的时间内面对巨大的请求流量,将有限的库存商品分发出去,并完成交易操做。好比12306抢票,库存的火车票是有限的,但瞬时的流量很是大,且都是在请求相同的资源,这里面数据库的并发读写冲突以及资源的锁请求冲突很是严重。就咱们实现这样一个红包系统自己来讲,面临着以下的一些挑战:

首先,到活动整点时刻,咱们有15万员工在固定时间点同时涌入系统抢某轮红包,瞬间的流量是很大的,而目前咱们整个链路上的系统和服务基础设施,都没有承受过如此高的吞吐量,要在短期内实现业务需求,在技术上的风险较大。

其次,公司是第一次开展这样的活动,咱们很难预知你们参与活动的状况,极端状况下可能会出现某轮红包没抢完,须要合并到下轮接着发放。这就要求系统有一个动态的红包发放策略和预算控制,其中涉及到的动态计算会是个较大的问题(这也是为系统高吞吐服务),实际的系统实现中咱们采用了一些预处理机制。

最后,这个系统是为了春节的庆祝活动而研发的定制系统,且只上线运行一次,这意味着咱们没法积累经验去对服务作持续的优化。而且相关的配套环境没有通过实际运行检验,缺乏参考指标,系统的薄弱环节发现的难度大。因此必需要追求设计至简,尽可能减小对环境的依赖(数据路径越长,出问题的环节越多),而且实现高可伸缩性,须要尽一切努力保证可靠性,即便有某环节失误,系统依然可以保障核心的用户体验正常。

系统设计

系统架构图如图所示。全部的静态资源提早部署在了第三方的CDN服务上,系统的核心功能主要划分到接入层和核心逻辑系统中,各自部署为集群模式而且独立。接入层主要是对用户身份鉴权和结果缓存,核心系统重点关注红包的分发,红色实线的模块是核心逻辑,为了保障其可靠性,咱们作了包括数据预处理、水平分库、多级缓存、精简RPC调用、过载保护等多项设计优化,而且在原生容器、MySQL等服务基础设施上针对特殊的业务场景作了优化,后面将为读者一一道来。

红包自己的信息经过预处理资源接口获取。运行中用户和红包的映射关系动态生成。底层使用内部开发的DB中间件在MySQL数据库集群上作红包发放结果持久化,以供异步支付红包金额到用户帐户使用。整个系统的绝大部分模块都有性能和保活监控。

优化方案

优化方案中最重要的目标是保障关键流程在应对大量请求时稳定运行,这须要很高的系统可用性。因此,业务流程和数据流程要尽可能精简,减小容易出错的环节。此外,缓存、DB、网络、容器环境,任何一个部分都要假设可能会短时出现故障,要有处理预案。针对以上的目标难点,咱们总结了以下的实践经验。

1.数据预处理

红包自己的属性信息(金额,状态,祝福语,发放策略),咱们结合活动预案要求,使用必定的算法提早生成好全部的信息,数据总的空间不是很大。为了最大化提高性能,这些红包数据,咱们事先存储在数据库中,而后在容器加载服务启动时,直接加载到本地缓存中看成只读数据。另外,咱们的员工信息,咱们也作了必定的裁剪,最基本的信息也和红包数据同样,预先生成,服务启动时加载。

此外,咱们的活动页面,有不少视频和图片资源,若是这么多的用户从咱们的网关实时访问,极可能咱们的带宽直接就被这些大流量的请求占满了,用户体验可想而知。最后这些静态资源,咱们都部署在了CDN上,经过数据预热的方式加速客户端的访问速度,网关的流量主要是来自于抢红包期间的小数据请求。

2.精简RPC调用

一般的服务请求流程,是在接入层访问用户中心进行用户鉴权,而后转发请求到后端服务,后端服务根据业务逻辑调用其余上游服务,而且查询数据库资源,再更新服务/数据库的数据。每一次RPC调用都会有额外的开销,因此,好比上一点所说的预加载,使得系统在运行期间每一个节点都有全量的查询数据可在本地访问,抢红包的核心流程就被简化为了生成红包和人的映射关系,以及发放红包的后续操做。再好比,咱们采用了异步拉的方式进行红包发放到帐,用户抢红包的请求再也不通过发放这一步,只记录关系,性能获得进一步提高。

实际上有些作法的可伸缩性是极强的。例如红包数据的预生成信息,在当时的场景下咱们是可以做为本地内存缓存加速访问的。当红包数据量很大的时候,在每一个服务节点上使用本地数据库,或者本地数据文件,甚至是本地Redis/MC缓存服务,都是能够保证空间足够的,而且还有额外的好处,越少的RPC,越少的服务抖动,只须要关注系统自己的健壮性便可,不须要考虑外部系统QoS。

3.抢红包的并发请求处理

春节整点时刻,同一个红包会被成千上万的人同时请求,如何控制并发请求,确保红包会且仅会被一个用户抢到?

作法一,使用加锁操做先占有锁资源,再占有红包。

能够使用分布式全局锁的方式(各类分布式锁组件或者数据库锁),申请lock该红包资源成功后再作后续操做。优势是,不会出现脏数据问题,某一个时刻只有一个应用线程持有lock,红包只会被至多一个用户抢到,数据一致性有保障。缺点是,全部请求同一时刻都在抢红包A,下一个时刻又都在抢红包B,而且只有一个抢成功,其余都失败,效率很低。

作法二,单独开发请求排队调度模块。

排队模块接收用户的抢红包请求,以FIFO模式保存下来,调度模块负责FIFO队列的动态调度,一旦有空闲资源,便从队列头部把用户的访问请求取出后交给真正提供服务的模块处理。优势是,具备中心节点的统一资源管理,对系统的可控性强,可深度定制。缺点是,全部请求流量都会有中心节点参与,效率必然会比分布式无中心系统低,而且,中心节点也很容易成为整个系统的性能瓶颈。

作法三,巧用Redis特性,使其成为分布式序号生成器。(咱们最终采用的作法)。

前文已经提到,红包系统所使用的红包数据都是预先生成好的,咱们使用数字ID来标识,这个ID是全局惟一的,全部围绕红包的操做都使用这个ID做为数据的关联项。在实际的请求流量过来时,咱们采用了“分组”处理流量的方式,以下图所示。

访问请求被LB分发到每一个分组,一个分组包含若干台应用容器、独立的数据库和Redis节点。Redis节点内存储的是这个分组能够分发的红包ID号段,利用Redis单进程的自减数值特性实现分布式红包ID生成器,服务经过此获取当前拆到的红包。落地数据都持久化在独立的数据库中,至关因而作了水平分库。某个分组内处理的请求,只会访问分组内部的Redis和数据库,和其余分组隔离开。

分组的方式使得整个系统实现了高内聚,低耦合的原则,能将数据流量分而治之,提高了系统的可伸缩性,当面临更大流量的需求时,经过线性扩容的方法,便可应对。而且当单个节点出现故障时,影响面可以控制在单个分组内部,系统也就具备了较好的隔离性。

4.系统容量评估,借助数据优化,过载保护

因为是首次开展活动,咱们缺少实际的运营数据,一切都是摸着石头过河。因此从项目伊始,咱们便强调对系统各个层次的预估,既包括了活动参与人数、每一个功能feature用户的高峰流量、后端请求的峰值、缓存系统请求峰值和数据库读写请求峰值等,还包括了整个业务流程和服务基础设施中潜在的薄弱环节。后者的难度更大由于很难量化。此前咱们连超大流量的全链路性能压测工具都较缺少,因此仍是有不少实践的困难的。

在这里心里真诚的感谢开源社区的力量,在咱们制定完系统的性能指标参考值后,借助如wrk等优秀的开源工具,咱们在有限的资源里实现了对整个系统的端到端全链路压测。实测中,咱们的核心接口在单个容器上能够达到20,000以上的QPS,整个服务集群在110,000以上的QPS压力下依然能稳定工做。

正是一次次的全链路压测参考指标,帮助咱们了解了性能的基准,并以此作了代码设计层面、容器层面、JVM层面、MySQL数据库层面、缓存集群层面的种种优化,极大的提高了系统的可用性。具体作法限于篇幅不在此赘述,有兴趣的读者欢迎交流。

此外,为了确保线上有超预估流量时系统稳定,咱们作了过载保护。超过性能上限阈值的流量,系统会快速返回特定的页面结果,将此部分流量清理掉,保障已经接受的有效流量能够正常处理。

5.完善监控

系统在线上运行过程当中,咱们很须要对其实时的运行状况获取信息,以便可以对出现的问题进行排查定位,及时采起措施。因此咱们必须有一套有效的监控系统,可以帮咱们观测到关键的指标。在实际的操做层面,咱们主要关注了以下指标:

服务接口的性能指标

借助系统的请求日志,观测服务接口的QPS,接口总的实时响应时间。同时经过HTTP的状态码观测服务的语义层面的可用性。

系统健康度

结合总的性能指标以及各个模块应用层的性能日志,包括模块接口返回耗时,和应用层日志的逻辑错误日志等,判断系统的健康度。

总体的网络情况

尽可能观测每一个点到点之间的网络状态,包括应用服务器的网卡流量、Redis节点、数据库节点的流量,以及入口带宽的占用状况。若是某条线路出现太高流量,即可及时采起扩容等措施缓解。

服务基础设施

应用服务器的CPU、Memory、磁盘IO情况,缓存节点和数据库的相应的数据,以及他们的链接数、链接时间、资源消耗检测数据,及时的去发现资源不足的预警信息。

对于关键的数据指标,在超过预估时制定的阈值时,还须要监控系统可以实时的经过手机和邮件实时通知的方式让相关人员知道。另外,咱们在系统中还作了若干逻辑开关,当某些资源出现问题而且自动降级和过载保护模块失去效果时,咱们能够根据情况直接人工介入,在服务不停机的前提早经过手动触发逻辑开关改变系统逻辑,达到快速响应故障,让服务尽快恢复稳定的目的。

6.服务降级

当服务器压力剧增的时候,若是某些依赖的服务设施或者基础组件超出了工做负荷能力,发生了故障,这时候极其须要根据当前的业务运行状况对系统服务进行有策略的降级运行措施,使得核心的业务流程可以顺利进行,而且减轻服务器资源的压力,最好在压力减少后还能自动恢复升级到原工做机制。

咱们在开发红包系统时,考虑到原有IDC机房的解决方案对于弹性扩容和流量带宽支持不太完美,选择了使用AWS的公有云做为服务基础环境。对于第三方的服务,缺乏实践经验的把握,因而从开发到运维过程当中,咱们都保持了一种防护式的思考方式,包括数据库、缓存节点故障,以及应用服务环境的崩溃、网络抖动,咱们都认为随时可能出问题,都须要对应的自动替换降级策略,严重时甚至可经过手动触发配置开关修改策略。固然,若是组件自身具备降级功能,能够给上层业务节约不少成本资源,要本身实现所有环节的降级能力的确是一件比较耗费资源的事情,这也是一个公司技术慢慢积累的过程。

结束语

以上是咱们整个系统研发运维的一些体会。此次春节红包活动,在资源有限的状况下成功抵抗超乎日常的流量峰值压力,对于技术而言是一次很大的挑战,也是一件快乐的事情,让咱们从中积累了不少实践经验。将来咱们将不断努力,但愿可以将部分转化成较为通用的技术,去更好的推进业务成功。真诚但愿本文的分享可以对你们的技术工做有所帮助。

电商的秒杀和抢购,对咱们来讲,都不是一个陌生的东西。然而,从技术的角度来讲,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定相当重要。此次咱们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为何咱们老是不容易抢到火车票的缘由?

1、大规模并发带来的挑战

在过去的工做中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程当中,整个Web系统遇到了不少的问题和挑战。若是Web系统不作针对性的优化,会垂手可得地陷入到异常状态。咱们如今一块儿来讨论下,优化的思路和方法哈。

1. 请求接口的合理设计

一个秒杀或者抢购页面,一般分为2个部分,一个是静态的HTML等内容,另外一个就是参与秒杀的Web后台请求接口。

一般静态HTML等内容,是经过CDN的部署,通常压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须可以支持高并发请求,同时,很是重要的一点,必须尽量“快”,在最短的时间里返回用户的请求结果。为了实现尽量快这一点,接口的后端存储使用内存级别的操做会更好一点。仍然直接面向MySQL之类的存储是不合适的,若是有这种复杂业务的需求,都建议采用异步写入。

固然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才能够从页面中看到用户是否秒杀成功。可是,这种属于“偷懒”行为,同时给用户的体验也很差,容易被用户认为是“暗箱操做”。

2. 高并发的挑战:必定要“快”

咱们一般衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标很是关键。举个例子,咱们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大链接数目)。

那么,咱们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?咱们的系统彷佛很强大,1秒钟能够处理完10万的请求,5w/s的秒杀彷佛是“纸老虎”哈。实际状况,固然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增长。

就Web服务器而言,Apache打开了越多的链接进程,CPU须要处理的上下文切换也越多,额外增长了CPU的消耗,而后就直接致使平均响应时间增长。所以上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。能够经过Apache自带的abench来测试一下,取一个合适的值。而后,咱们选择内存操做级别的存储的Redis,在高并发的状态下,存储的响应时间相当重要。网络带宽虽然也是一个因素,不过,这种请求数据包通常比较小,通常不多成为请求的瓶颈。负载均衡成为系统瓶颈的状况比较少,在这里不作讨论哈。

那么问题来了,假设咱们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际状况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

因而,咱们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

而后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒经过5部车,高速路口运做正常。忽然,这个路口1秒钟只能经过4部车,车流量仍然依旧,结果一定出现大塞车。(5条车道突然变成4条车道的感受)

同理,某一个秒内,20*500个可用链接进程都在满负荷工做中,却仍然有1万个新来请求,没有链接进程可用,系统陷入到异常状态也是预期以内。

其实在正常的非高并发的业务场景中,也有相似的状况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用链接数占满,其余正常的业务请求,无链接进程可用。

更可怕的问题是,是用户的行为特色,系统越是不可用,用户的点击越频繁,恶性循环最终致使“雪崩”(其中一台Web机器挂了,致使流量分散到其余正常工做的机器上,再致使正常的机器也挂,而后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

若是系统发生“雪崩”,贸然重启服务,是没法解决问题的。最多见的现象是,启动起来后,马上挂掉。这个时候,最好在入口层将流量拒绝,而后再将重启。若是是redis/memcache这种服务也挂了,重启的时候须要注意“预热”,而且极可能须要比较长的时间。

秒杀和抢购的场景,流量每每是超乎咱们系统的准备和想象的。这个时候,过载保护是必要的。若是检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,可是,这种作法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

2、做弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。很多用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽量多的请求到服务器。还有一部分高级用户,制做强大的自动请求脚本。这种作法的理由也很简单,就是在参与秒杀和抢购的请求中,本身的请求数目占比越多,成功的几率越高。

这些都是属于“做弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

1. 同一个帐号,一次性发出多个请求

部分用户经过浏览器的插件或者其余工具,在秒杀开始的时间里,以本身的帐号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有作数据安全处理的系统里,也可能形成另一种破坏,致使某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,若是没有则领取成功,最后写入到参与记录中。这是个很是简单的逻辑,可是,在高并发的场景下,存在深深的漏洞。多个并发请求经过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,而后,在某个请求成功写入参与记录的时间差内,其余的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

 

应对方案:

在程序入口处,一个帐号只容许接受1个请求,其余请求过滤。不只解决了同一个帐号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,能够经过Redis这种内存缓存服务,写入一个标志位(只容许1个请求写成功,结合watch的乐观锁的特性),成功写入的则能够继续参加。

或者,本身实现一个服务,将同一个帐号的请求放入一个队列中,处理完一个,再处理下一个。

2. 多个帐号,一次性发送多个请求

不少公司的帐号注册功能,在发展早期几乎是没有限制的,很容易就能够注册不少个帐号。所以,也致使了出现了一些特殊的工做室,经过编写自动注册脚本,积累了一大批“僵尸帐号”,数量庞大,几万甚至几十万的帐号不等,专门作各类刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,若是咱们使用几万个“僵尸号”去混进去转发,这样就能够大大提高咱们中奖的几率。

这种帐号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

应对方案:

这种场景,能够经过检测指定机器IP请求频率就能够解决,若是发现某个IP请求频率很高,能够给它弹出一个验证码或者直接禁止它的请求:

 

  1. 弹出验证码,最核心的追求,就是分辨出真实用户。所以,你们可能常常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让咱们根本没法看清。他们这样作的缘由,其实也是为了让验证码的图片不被轻易识别,由于强大的“自动脚本”能够经过图片识别里面的字符,而后让脚本自动填写验证码。实际上,有一些很是创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操做(例如百度贴吧的验证码)。
  2. 直接禁止IP,其实是有些粗暴的,由于有些真实用户的网络场景刚好是同一出口IP的,可能会有“误伤“。可是这一个作法简单高效,根据实际场景使用能够得到很好的效果。

 

3. 多个帐号,不一样IP发送不一样请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工做室”,发现你对单机IP请求频率有控制以后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。

有同窗会好奇,这些随机IP服务怎么来的。有一些是某些机构本身占据一批独立IP,而后作成一个随机代理IP的服务,有偿提供给这些“工做室”使用。还有一些更为黑暗一点的,就是经过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运做,只作一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。经过这种作法,黑客就拿到了大量的独立IP,而后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想作分辨很困难。再作进一步的限制很容易“误伤“真实用户,这个时候,一般只能经过设置业务门槛高来限制这种请求了,或者经过帐号行为的”数据挖掘“来提早清理掉它们。

僵尸帐号也仍是有一些共同特征的,例如帐号极可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特色,适当设置参与门槛,例如限制参与秒杀的帐号等级。经过这些业务手段,也是能够过滤掉一些僵尸号。

4. 火车票的抢购

看到这里,同窗们是否明白你为何抢不到火车票?若是你只是老老实实地去抢票,真的很难。经过多帐号的方式,火车票的黄牛将不少车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展现验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制做用被废除了,目前也没有很好的解决方案。

由于火车票是根据身份证明名制的,这里还有一个火车票的转让操做方式。大体的操做方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛帐号选择退票,而后黄牛买家成功经过本身的身份证购票成功。当一列车箱没有票了的时候,是没有不少人盯着看的,何况黄牛们的抢票工具也很强大,即便让咱们看见有退票,咱们也不必定能抢得过他们哈。

最终,黄牛顺利将火车票转移到买家的身份证下。

解决方案:

并无很好的解决方案,惟一能够动心思的也许是对帐号数据进行“数据挖掘”,这些黄牛帐号也是有一些共同特征的,例如常常抢票和退票,节假日异常活跃等等。将它们分析出来,再作进一步处理和甄别。

3、高并发下的数据安全

咱们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,若是每次运行结果和单线程运行的结果是同样的,结果和预期相同,就是线程安全的)。若是是MySQL数据库,能够使用它自带的锁机制很好的解决问题,可是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另一个问题,就是“超发”,若是在这方面控制不慎,会产生发送过多的状况。咱们也曾经据说过,某些电商搞抢购活动,买家成功拍下后,商家却不认可订单有效,拒绝发货。这里的问题,也许并不必定是商家奸诈,而是系统技术层面存在超发风险致使的。

1. 超发的缘由

假设某个抢购场景中,咱们一共只有100个商品,在最后一刻,咱们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,而后都经过了这一个余量判断,最终致使超发。(同文章前面说的场景)

在上面的这个图中,就致使了并发用户B也“抢购成功”,多让一我的得到了商品。这种场景,在高并发的状况下很是容易出现。

2. 悲观锁思路

解决线程安全的思路不少,能够从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,可是,别忘记,咱们的场景是“高并发”。也就是说,会不少这样的修改请求,每一个请求都须要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会不少,瞬间增大系统的平均响应时间,结果是可用链接数被耗尽,系统陷入异常。

3. FIFO队列思路

那好,那么咱们稍微修改一下上面的场景,咱们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,咱们就不会致使某些请求永远获取不到锁。看到这里,是否是有点强行将多线程变成单线程的感受哈。

而后,咱们如今解决了锁的问题,所有请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,由于请求不少,极可能一瞬间将队列内存“撑爆”,而后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,可是,系统处理完一个队列内请求的速度根本没法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候仍是会大幅降低,系统仍是陷入异常。

4. 乐观锁思路

这个时候,咱们就能够讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据全部请求都有资格去修改,但会得到一个该数据的版本号,只有版本号符合的才能更新成功,其余的返回抢购失败。这样的话,咱们就不须要考虑队列的问题,不过,它会增大CPU的计算开销。可是,综合来讲,这是一个比较好的解决方案。

有不少软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。经过这个实现,咱们保证了数据的安全。

4、小结

互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得愈来愈多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然咱们解决问题的具体技术方案可能千差万别,可是遇到的挑战倒是类似的,所以解决问题的思路也殊途同归。

相关文章
相关标签/搜索