ONE秒杀排队系统设计构想
传统秒杀系统疼痛
第一,秒杀的场面决定了秒杀是速度的较量。也就是说,俗话说“手快,手慢”。(阿尔伯特爱因斯坦)(美国)。
大家都争着在活动开始后,第一时间将商品抢到,完成下单。因此秒杀活动开始的一瞬间会有大量的流量涌入,几倍、甚至于十几倍的流量对系统的冲击不可谓不大。如果系统没有足够的capacity或应对措施,很可能就被瞬时高流量给压垮了。其次,突如其来的高流量,给系统各个模块都来了一连串的压力,系统可能会因此变慢,而且可能会彼此影响,影响可用性。比如:数据库更新同一个商品库存,需对同一行记录加锁,随着并发的压力逐渐增大,数据库更新的性能是逐渐下降的。从而引起提供库存service的应用服务性能下降,连锁的影响到下单service的性能,最终反馈到消费者的可能就是整个网站购物流程性能差、响应慢。而面对响应慢的系统,很多消费者可能采取反复刷新,多次尝试,这无疑又增大了对系统的压力。
上述种种给消费者带来的往往是体验上的痛苦。如:网站响应慢,点击抢购按钮没反应。好不容易可以操作了,却发现秒杀活动已经结束,消费者的参与感比较差。久而久之,可能就对此类活动失去了兴趣。
1号店秒杀系统设计理念
『 限流 』当秒杀活动开始后,只有少部分消费者能抢购到秒杀商品,意味着其实大部分用户的流量传达到后台服务后都是无效。如果能引导这大部分的流量,不让这大部分的流量传达到后台服务,其实对我们系统的压力就很小了。因此设计思路之一就是,仅让能成功抢购到商品的流量(可以有一定余量)进入我们的系统。
『削峰 』进入系统的有效流量虽然总量不一定是很大的,但却是在很短的时间内涌入的,因此会存在很高的瞬时流量峰值。总量相同的流量在1秒钟进入系统,和在10分钟均匀地进入系统,对系统的冲击是相差很大的。高峰值的流量往往能将系统压垮。因此另一个设计思路是,如何将进入系统的瞬时高流量拉平,使得系统可以在自己处理能力范围内,将所有抢购的请求处理完毕。
『 异步处理 』传统的系统对于请求是同步处理的,即收到请求后立即处理并把结果返回给用户。我们的系统有了削峰的设计后,请求不是被立刻处理的,因此就要求我们能将同步的服务改造成异步的。
『 可用性 』我们设计时始终把系统的可用性放在重要的位置,针对系统可能出现的各种状况,都尽最大程度地保证高可用。
『 用户体验 』系统设计一定要充分考虑用户体验。消费者点击抢购按钮后,无论是否能抢到商品,期望是能得到及时的反馈。系统上发生任何故障也要尽可能的保证用户体验的损害减到最小。
秒杀排队系统架构三个主要模块
『排队模块 』负责接收用户的抢购请求,将请求以先入先出的方式保存下来。每一个参加秒杀活动的商品保存一个队列,队列的大小可以根据参与秒杀的商品数量(或加点余量)自行定义。排队模块还负责提供一系列接口,如:给已进入队列的用户查询下单状态的接口,给调度模块拉取请求的接口,服务模块回写业务处理状态的接口等。
『 调度模块 』负责排队模块到服务模块的动态调度,不断检查服务模块,一旦处理能力有空闲,就从排队队列头上把用户访问请求调入服务模块。并负责向服务模块分发请求。这里调度模块扮演一个中介的角色,但不只是传递请求而已。它还担负着调节系统处理能力的重任。我们可以根据服务模块的实际处理能力,动态调节向排队系统拉取请求的速度。作用有点类似水坝的闸门,当下游干旱时就打开闸门多放些水,当下游洪涝时,就放下闸门少放些水。
『 服务模块 』是负责调用真正业务处理服务,并返回处理结果,并调用排队模块的接口回写业务处理结果。我们设计这个模块,是为了和后面真正的业务处理服务解耦。目前我们的系统不只支持秒杀抢购这种业务场景,后续有其他适用于排队系统的业务都可以接入,如:领取抵用券等等。同时我们也可以针对后面业务系统的处理能力,动态调节服务模块调用后面业务处理服务的速度。
容错处理
任何系统都不可能一帆风顺,但我们要能在出现错误时仍旧保证系统的高用性和良好的客户体验。举几个简单的例子,比如服务模块调用后面服务时,出现调用服务超时怎么办?对此我们设计了对于超时的请求,可以重试的机制。再比如,如果后面真正的业务处理系统宕机怎么办?如果是传统系统的话,可能就面临系统无法使用的尴尬。系统已异步,因此加入排队队列的用户请求,在业务处理系统恢复后都可以得到处理。只要我们在前端给用户以友好的交互、提示,系统还是能提供一定质量服务的。
用户交互
因为我们的系统设计成异步的,因此消费者不再是像以前一样同步地去等待反馈。消费者需要一个途径来获取抢购的状态和进度。我们的主体流程大体上分为几个阶段:
当等待人数大于500人,页面提示:排在您前面的人超过500位;
当等待人数小于等于500人,页面提示:您已挤进第***位;
当等待时间大于等于1分钟,页面提示:剩余时间约*分钟。每次以分钟倒计时。
当等待时间小于1分钟,页面提示:预计剩余*秒。
抢购成功,后续跳转到订单支付页面
【以下为2个PC端页面交互设计示例】
TWO分布式搜索引擎架构实践
应对大流量与高突发性的两个核心需求
『 可扩展 』如何抗住这样的流量,针对这个需求,1号店搜索团队构建了分布式搜索引擎,支持横向扩展;并且针对业务特点做了Routing优化,让搜索的效率更高。
『 快速响应 』流量越大,单位时间内的流量价值就越大,出现问题的损失也就越大,如何做到快速响应变得非常关键。针对这个需求,搜索系统支持自动部署和快速扩容以应对突发流量,索引数据从导入、处理到上线服务会经过层层验证,同时还有监控体系及时发现线上的问题。
分布式搜索引擎
1号店分布式搜索引擎是Lucene/Solr核心的,结合SOA框架Hedwig构建了一层分布式框架,支持搜索请求的分发和合并,并且构建了搜索管理后台,支持多索引管理、集群管理、全量索引切换和实时索引更新。
选择自己构建分布式方案,而不是采用开源的SolrCloud或ElasticSearch,主要是基于以下几点考虑:
ElasticSearch/SolrCloud都适合于把搜索引擎作为一个黑盒系统来使用,而1号店搜索业务的展现形式多样性很高,搜索条件有的会很复杂,有的需要通过自定义插件来实现,性能调优时也需要对引擎内部的执行细节进行监控。
将ElasticSearch/SolrCloud与公司内部的发布系统、监控系统和SOA体系结合起来,也是一项比较耗时的工作。
相对于整体使用,我们更倾向于把Lucene/Solr开源家族中的各个组件按需引入,一方面降低引入复杂工程的可维护性风险,另一方面逐渐深入理解这些组件,可以在必要时替换为定制化的组件。
分布式搜索是为了解决数据增长过程中索引变大和操作时间变长的问题,它将原来的单个索引文件划分成n个切片(shards),让每个shard都足够小,从而保证索引可以在多台服务器上部署,而且搜索操作可以在较短时间内返回。
如上图所示,分布式搜索中有两个主要组件:Shard Searcher和Broker,其中Shard Searcher与单机搜索引擎类似,基于Lucene/Solr完成基本的搜索任务。Broker负责把针对整个索引的搜索请求转化为针对单个Shard的搜索请求,它把Shard搜索请求分发给各个ShardSearcher,并且把各个Shard的结果进行合并,生成最终的结果。
分布式搜索中,一次搜索所需的资源与它要访问的Shard数和每个Shard要返回的结果数有非常强的关联关系,在Shard数特别多或结果数特别多时可能会碰到一些的内存、CPU资源使用的问题。针对结果数特别多的情况,可以按照业务场景优化,比如如果对排序无要求,就可以每次指定一个Shard进行搜索,搜完这个Shard再换下一个,这样就限制了每次搜索的Shard数,另一方面也可以考虑使用DeepPaging等技术,减少每次Shard搜索的成本。我们下一小节也会介绍1号店主站搜索是如何减少每次搜索Shard数的。
上图中的Broker和Shard Searcher仅仅是概念上的划分,实际部署时有几种选择:
每个节点上都有Broker和部分Shard的Shard Searcher。
Broker单独部署成一个集群,与Shard Searcher隔离。
Broker作为客户端的一部分,和搜索应用一起部署。
我们开始使用的是A方式,后来主站搜索转为C方式,主要是考虑到可以节省一次网络调用(以及请求和结果的序列化开销),另外Broker在客户端也可以更多地使用应用逻辑相关的数据进行更高效的Routing。
高效Routing
通过前面的讲述,我们不难看出,使用分布式搜索引擎,面临的核心问题就是如何选择高效的Sharding策略和Routing方案。为了设计Routing方案,我们需要深入理解业务场景。
1号店有很多的类目,每个类目的业务模式也不尽相同。以图书和快消品为例,图书是一种典型的长尾商品,它需要索引大量的SKU(Stock Keeping Unit,可以理解为一个独立的商品),但每个SKU的访问量和销量都不高;快消品是另外一个极端,总体SKU数量不高,但是访问量和效率都很高。这就造成了一个不平衡的局面,图书的SKU数目占比达到了50%以上,流量却小于10%,我们就首先排除了按Shard数目取模(id mod N)这种平衡划分的策略。
1号店搜索有两个主要入口,一个是搜索框的搜词,另外是类目导航。在这两个入口中,类目的点击肯定是访问到一个特定的一级类目下,搜词时用户其实也只会关注相关的几个类目。基于这种访问模式,我们就采用了按照类目来切分Shard的策略。基本操作为:
按照一级类目切分Shard。
如果该Shard过大,则按照二级类目继续切分。
经过前两步之后,如果切分后的Shard过小,则按照相关性进行Shard合并。
经过这样一番尝试,Sharding策略就确定下来,切分之后的Shard索引大小一般为200~500MB,Shard上单次搜索可以控制在10ms以下。
接下来说到Routing,我们还是分搜词和类目导航两种场景,对于类目导航,原理上非常简单,按照类目ID来查找Sharding策略,就可以确定需要访问的Shard,虽然现实中还需要考虑扩展类目等特殊场景,但是也不难做出一个简单的Routing策略。再加上类目数是有限的,Routing规则在Broker本地内存就可以缓存起来。
搜词场景就复杂很多,仅凭词本身很难判断它属于哪个Shard。我们首先按照词的热度分为两类,采取不同的Routing策略。对于热词,搜词流量同样符合80-20规则,20%的热词占比80%的搜索流量,对于热词,我们可以在建完索引之后,就跑一遍热词搜索,记录那些Shard有结果,离线构建出热词Routing表。切换索引时,Routing表也一起加载进去。对于非热词,则采用首次搜索去访问所有Shard,根据结果记录Routing表,这个词在下次搜索时,就有了缓存可用。
基本的Routing策略上线之后,通过监控每个Shard的访问量,我们又发现了新的问题,图书类目的访问量比它应有的流量要高出不少。仔细分析之后发现,由于图书类目的特殊性,很多词都可以在图书中找到结果,然而这些结果一般都不是用户想要的,实际上也不会排到前几页,并没有展示的机会。于是我们又增加了一种360-Routing策略,跟进搜索前五页的结果(每页72个商品,共360个商品)计算Routing,下次搜索时优先是用这份Routing规则。由于前五页的流量占比在80%以上,这就进一步减少了单次搜索需要访问的Shard数。
使用了以上这些Routing规则,1号店主站搜索每次搜索平均只需要访问1/3的索引数据,这就节约了2/3的资源,提高了搜索效率。
自动部署与快速扩容
按照类目进行Sharding和Routing的方式,在带来高效的同时,也带来了管理上的成本。按照类目切分,必然会导致各个Shard的大小不平均,而对应的Routing方案,必然会带来各个Shard的访问量不平均。这两个维度不平均就要求更加复杂的索引部署方案,主要的原则为:
首先根据流量比例和高可用的需求,确定每个Shard的副本数。
然后按照单个节点不能放置同一Shard的多个副本,节点上的承担的流量总和与节点的服务能力成正比。
每个节点上的索引总大小尽量也保持差异最小。
按照流量比例,副本数计算如下:
Shard | 副本数 | 备注 |
S0 | 4 | |
S1 | 2 | 高可用约束 |
S2 | 4 | |
S3 | 3 |
【部署之后的效果如下图所示】
Shard数增多之后,人工计算部署方案就显得较为复杂,于是我们就把部署方案生成做成了自动化,一个基本的Packing算法就可以完成这个工作。除了初始部署之外,自动部署工具还可以支持节点增加、减少和更换。
在11.11的场景下,这个自动化部署工具也可以支持快速扩容,基本过程为:
Index Server(部署工具界面)计算出扩容之后的部署方案,写入到ZooKeeper。这里的算法与初始部署有些不同,它需要保证现有服务器的Shard尽量不变。
每个Shard Searcher服务器都会监听ZooKeeper上的部署方案,方案改变之后,Shard Searcher会获取新的方案与本地方案对比,进行增减操作。
Shard Searcher从HDFS上获取索引数据,与最近的实时更新数据合并之后,启动索引提供服务。
有了自动扩容工具,11.11容量规划确定之后,新的服务器就可以很快部署上线,也减少了人工操作可能引入的失误等问题。大促期间如果需要紧急扩容,也可以在几分钟内提高整个系统的服务能力。
实时监控体系
在索引数据方面,从源头开始对索引数据准备的各个环节进行验证,包括数据的条数、处理过程中的异常、最后生成的索引在常见搜索中的执行结果差异等,层层预防,防止有问题的索引数据被用到线上。
在搜索服务方面,监控系统会定时执行常见的搜索,对比排序结果,结果差异较大时及时告警。同时大促期间会有一些商品很快卖完,这些商品继续显示在搜索结果中也没有价值,搜索监控也会及时发现这些商品,触发索引更新,把商品排到后面。
THREE从应用架构落地点谈高可用高并发高性能
一号店三高策略——高可用高并发高性能
1号店技术部从1个人做起到今天千人级别的规模,系统支持每天亿级的访问量、单Service支持每天亿级的请求、订单支持每分钟几万单级别、Service服务可用性达到99.9999%,架构上也经历了历次演进,今天我们就从应用架构历次演进的落地点谈起。
1号店应用架构的演进大致经历了以下历程:强依赖-> Service化->业务解耦->读写分离->异步->水平/垂直拆分->服务逻辑分组等。
当然这个过程从不是串行的,永远是并行的,并且这个过程永远是在1号店这辆系统大巴行进过程中进行的,因为我们不能停车也不能刹车,而且还必须不断提速。
应用架构的最大演进-SOA治理
早期的1号店系统,遵循简单的MVC架构,Controller层处理了所有的业务逻辑包括与DB的交互,在系统初期这种Simple的架构方便快捷,成本低业务响应快。但当业务开始变得复杂、人员规模爆发式增长,这种强耦合强依赖带来的弊端就成了巨大的瓶颈,代码耦合度高互相冲突、出错概率和事故概率明显提升,业务需求不能快速响应,SOA治理迫在眉睫,解耦和去依赖成为第一需求,于是Service化成为第一前提。
SOA治理-Service日志即保障
『 Tips 』
做Service规划首要考虑要素:
很多人想的是采用什么RPC框架、采用什么技术,怎么让性能更高;也有人首先想的是业务怎么拆分,怎么才能更合理。
我们首先想到的是如何做监控和问题定位。
高可用不是一步做到的,我们的Service可用性不是一步达到99.9999%的,在这过程中一定会有很多的问题出现,怎么提前发现这些问题、出现问题后如何快速定位,这才是最重要的。这只能依赖日志,这是监控和问题定位的基础。
下单接口业务性强,其对可用、并发、性能的要求作为技术人你懂的。我们就从这个接口开始下手,但我们没有先去分析业务,首先想的是如何定义日志系统,让以后的监控和问题定位更简单更快捷。事实证明这个决定是对的,直到现在1号店的大部分订单相关的监控、预警、问题排查定位等完全依赖这个日志。
日志系统的设计基于以下:一是进数据库、持久化有序化,二是分类化、层次化、错误code唯一。
进数据库、持久化有序化这个大家都理解,我曾经接手过的一个应用系统,一天下来Tomcat的log文件大小超过1G,会让你崩溃的。
分类化、层次化、特别是错误code唯一这个是关键,它是大海航行中的那盏灯塔,让你可以瞬间定位问题位置,它给监控预警带来的好处同样伟大,可以从各个维度去做监控预警。
1号店现在有了很好的SOA中间件 – Hedwig,可支持百亿级的访问请求,它有SOA级别的日志,也很完善。但应用级别的日志我们还是建议各应用系统自己做,它的业务性、个性化是公共架构永远代替不了的。
应用架构演进之落地
『业务垂直拆分 』
从业务角度规划Service,从产品、用户、订单三个维度上对Service进行了规划,构成1号店应用架构上的三架马车,确立了SOA治理的框架基础。
在此基础上,又陆续衍生出促销、积分、支付等众多Service业务,在三架马车中同样会细分至如文描、价格、库存、下单、订单查询等垂直服务。
Service化是前提,Service化完成后,后面可以大刀阔斧地干了,因为业务独立了、DB读写权限收回了,哈,好像整个天下都是我的了。但,得天下易治天下难,真正的大戏在后面。
『读写分流 』
读写分离的重要性不需多讲,除了最简单的读写分离,写可以从应用层面继续细分,读也可以从应用和DB层面再细分,如订单的读写分离:
『 水平拆分 』
早在2013年,1号店实现库存的水平拆分,2014年又一举完成订单水平拆库&去Oracle迁Mysql项目。
订单水平拆库&去Oracle迁Mysql两个重量级的大项目合并为一个项目并完美无缝上线,在经济上时间上节省的是成本,在技术上体现的1号店的整体技术实力和水平。
『 服务逻辑分组 』
物理分离好处明显,但其硬件成本、维护成本高的弊端也显而易见。这时候,我们的大杀器-Hedwig又上场了,有兴趣多来了解下我们SOA中间件-Hedwig,这可是获总裁奖的项目。
Hedwig提供了服务自动注册发现、软负载均衡、节点踢出与复活、服务动态逻辑分组、请求自动重试等众多SOA框架所需的强大功能,支持并行请求、灰度发布,其背后提供的调用链路及层次关系、日志分析、监控预警等更是为SOA治理提供了强大的后勤保障。
『 业务解耦|异步 』
上面提到的读写分离、水平拆分、逻辑分组等主要是从技术层面保障,也是技术人员最喜欢的话题。业务流程的梳理、优化、改造往往不被重视,但作为应用架构,我们最不能忽视的是业务。
我们的一个核心Service服务,性能从2年前的近200ms到现在的几十ms,从代码上、sql上的优化提升顶多占到10%,更多地优化都在业务流程的梳理改造上。
我们曾经花费两周多的时间将一个系统的整体性能优化提升了近10倍,最后总结下来,纯技术的优化(代码或sql质量导致的性能差)几乎没有。
优化主要在两方面,一是架构上,如使用缓存、单SKU的循环查询改成批量查询等,这个能优化的也并不太多,因为我们的架构整体还是比较合理的;最大的优化还是在业务梳理上,很多地方我们使用了重接口,接口里很多逻辑计算和DB查询,这些逻辑并不是所有的业务场景都需要的,但开发人员为了简单没有将业务场景细分,导致大量不合理的调用,既浪费了服务器资源又严重影响用户体验;同样,很多地方为了一个不重要的展示或逻辑也产生大量不合理的调用,反而影响了核心业务,这些都是最需要优化的,也是优化效果最明显的。
异步本身不是什么高深的技术,关键是哪些业务可以走异步,这更体现架构师的业务理解能力和综合能力。
如下单成功后给用户的消息通知、发票详细信息的生成等都可以异步,我们在这方面做了很多工作,包括和各业务方的大量沟通制定方案等,在不牺牲用户体验又保证业务流程完整的情况下,尽量走异步解耦,这对可用、性能都是极大的提升。
实例:一个下单接口定义135个错误编码
前面提到过日志和错误编码的定义,仅一个下单接口就定义了135个错误编码。接口上线后至今出现的错误编码在50-60个,也就是说有50-60处不合理或错误的地方,这个不合理或错误既有业务的又有程序的也有我们对编码定义的不合理。
目前日常每天下单出现3-5个错误编码,主要为业务性如特价商品已售完无库存等。在下单接口上线近2年后,一个之前从未出现过的错误编码跳出来了,是一个很难出现的业务场景,但通过编码马上定位问题,就不用去看代码。
实例:Service服务可用性99.9999%
实例:销售库存准确率99.9999%,超卖率为0
做过电商核心系统的人都明白库存控制的难度,库存不准甚至超卖的问题至今还有很多电商公司没有完全解决。
这个问题也曾经困扰我们,为此还专门写了一个库存刷子的程序来刷数据,现在这个程序已基本宣告废弃了,因为我们的库存准确率达到了6个9,超卖率为0。
怎么做到的?业务、业务、业务,重要的事说三遍。3个多月对所有库存应用场景逐一排查,最终梳理出16个有问题的库存场景,并逐一协调解决,让库存形成严格的闭环线路,保证了库存的准确性。在这过程中,对库存闭环方案和对业务的理解成为关键,纯靠技术,我们能做的并不多。
业务监控应用场景
业务监控首提订单监控,对订单我们从实际订单数据和Service接口调用量两个维度去做监控,保证了监控系统的稳定和准确(监控系统也会出错的),其中下单接口调用量使用的就是Service日志数据。
服务监控应用场景
『 依赖查询 』
『服务监控 』
本文内容来自周航、张立刚、刘霄晖三位作者,由InfoQ授权整理发布,文章仅供交流分享用,禁止商业用途转载。
ArchSummit全球架构师峰会北京站将于2015年12月18日~19日在北京国际会议中心召开,大会设置了《揭秘双十一背后的技术较量》专题来深入解读双十一背后的技术故事,欢迎关注,点击 阅读原文 进入官网了解。
1.《solr如何立即刷新》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《solr如何立即刷新》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/3216222.html