first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@ -0,0 +1,49 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 秒杀系统架构设计都有哪些关键点?
你好我是许令波花名“君山”。说起来我的职业生涯算是比较简单2009年大学毕业后就进入了淘宝一直工作了七年多。这七年多的时间里我有幸看到了淘宝业务的快速增长并且以开发者的身份参与其中。
说实话,作为一名程序员,我的技术能力也在公司业务的快速增长过程中得到了历练,并积累了一些大流量高并发网站架构设计和优化的经验,尤其是针对“秒杀”这个场景。因为我确信,那个时候我们肯定是对系统做了足够多的极致优化,才能扛住当时洪峰般的流量请求。
记得早期的时候淘宝商品详情系统的PV还差不多是1亿的样子但是到2016年差不多已经升至50亿了。尤其是2012年到2014年那个时间段“秒杀”活动特别流行用户的参与热情一浪高过一浪系统要面对的流量也是成倍增长。
而每一次的秒杀活动对技术团队来说都是一次考验。现在想起来,那个时候我们整个团队,无所畏惧,逐步迭代创新,然后解决一个个难题的过程,也是极具挑战性和成就感的事情。
记得有一年,为了应对“双十一”,我们整个商品详情团队对系统做了很多优化,我们自认为已经是整个公司最牛的系统了,性能也已经是“业界之巅”。
但是那年“双十一”的晚上,我们的系统还是遇到了瓶颈。当时老大就跑过来盯着我们,问我们什么时候能够恢复,我们整个团队都承担着巨大的心理压力。
事后我们复盘宕机的原因发现当时的秒杀流量远远超过了我们的预想我们根本没想到大家的参与热情能有那么高。于是我们按照这个增长率去预估下一年的流量和服务器粗算下来我记得差不多要增加2000台服务器简直不可思议。
怎么可能真正增加这么多机器,所以这也就倒逼我们必须找出一些特殊的手段来优化系统。后面,经过一段时间的调研和分析,我们想到了把整个系统进行动静分离改造的解决方案。
秒杀系统也差不多那个时候才从商品详情系统独立出来成为一个独立产品的。因为我见证了秒杀系统的建设过程,所以也有颇多感慨。秒杀系统的迭代又是一个升级打怪的过程,我们也都是遇到问题解决问题,逐一优化。
那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高维度出发,从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
而从一个架构师的角度来看,要想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上从浏览器到服务端我们要遵循几个原则,就是要保证用户请求的数据尽量少、请求数尽量少、路径尽量短、依赖尽量少,并且不要有单点。这些关键点我会在后面的文章里重点讲解。
其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。
所谓“稳”,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
然后就是“准”就是秒杀10台iPhone那就只能成交10台多一台少一台都不行。一旦库存不对那平台就要承担损失所以“准”就是要求保证数据的一致性。
最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求,我们的专栏也将主要围绕这几个方面来展开,具体如下。
高性能。 秒杀涉及大量的并发读和并发写因此支持高并发访问这点非常关键。本专栏将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。
一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,我将用一篇文章来专门讲解如何设计秒杀减库存方案。
高可用。 虽然我介绍了很多极致的优化思路但现实中总难免出现一些我们考虑不到的情况所以要保证系统的高可用和正确性我们还要设计一个PlanB来兜底以便在最坏情况发生时仍然能够从容应对。专栏的最后我将带你思考可以从哪些环节来设计兜底方案。
最后,很幸运能在极客时间遇到你,希望这堂课能让你彻底理解大并发、高性能、高可用秒杀系统的设计之道,并能够在思考解决类似问题时有更准确的思考和判断。

View File

@ -0,0 +1,128 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 设计秒杀系统时应该注意的5个架构原则
说起秒杀我想你肯定不陌生这两年从双十一购物到春节抢红包再到12306抢火车票“秒杀”的场景处处可见。简单来说秒杀就是在同一个时刻有大量的请求争抢购买同一个商品并完成交易的过程用技术的行话来说就是大量的并发读和并发写。
不管是哪一门语言,并发都是程序员们最为头疼的部分。同样,对于一个软件而言也是这样,你可以很快增删改查做出一个秒杀系统,但是要让它支持高并发访问就没那么容易了。比如说,如何让系统面对百万级的请求流量不出故障?如何保证高并发情况下数据的一致性写?完全靠堆服务器来解决吗?这显然不是最好的解决方案。
在我看来,秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。今天,我们就来聊聊,如何在满足一个良好架构的分布式系统基础上,针对秒杀这种业务做到极致的性能改进。
架构原则“4要1不要”
如果你是一个架构师你首先要勾勒出一个轮廓想一想如何构建一个超大流量并发读写、高性能以及高可用的系统这其中有哪些要素需要考虑。我把这些要素总结为“4要1不要”。
数据要尽量少
所谓“数据要尽量少”,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。
为啥“数据要尽量少”呢因为首先这些数据在网络上传输需要时间其次不管是请求数据还是返回数据都需要服务器做处理而服务器在写网络时通常都要做压缩和字符编码这些都非常消耗CPU所以减少传输的数据量可以显著减少CPU的使用。例如我们可以简化秒杀页面的大小去掉不必要的页面装修效果等等。
其次“数据要尽量少”还要求系统依赖的数据能少就少包括系统完成某些业务逻辑需要读取和保存的数据这些数据一般是和后台服务以及数据库打交道的。调用其他服务会涉及数据的序列化和反序列化而这也是CPU的一大杀手同样也会增加延时。而且数据库本身也容易成为一个瓶颈所以和数据库打交道越少越好数据越简单、越小则越好。
请求数要尽量少
用户请求的页面返回后浏览器渲染这个页面还要包含其他的额外请求比如说这个页面依赖的CSS/JavaScript、图片以及Ajax请求等等都定义为“额外请求”这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗例如建立连接要做三次握手有的时候有页面依赖或者连接数限制一些请求例如JavaScript还需要串行加载等。另外如果不同请求的域名不一样的话还涉及这些域名的DNS解析可能会耗时更久。所以你要记住的是减少请求数可以显著减少以上这些因素导致的资源消耗。
例如减少请求数最常用的一个实践就是合并CSS和JavaScript文件把多个JavaScript文件合并成一个文件在URL中用逗号隔开https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js。这种方式在服务端仍然是单个文件各自存放只是服务端会有一个组件解析这个URL然后动态把这些文件合并起来一起返回。
路径要尽量短
所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
通常这些节点可以表示为一个系统或者一个新的Socket连接比如代理服务器只是创建一个新的Socket连接来转发请求。每经过一个节点一般都会产生一个新的Socket连接。
然而每增加一个连接都会增加新的不确定性。从概率统计上来说假如一次请求经过5个节点每个节点的可用性是99.9%的话那么整个请求的可用性是99.9%的5次方约等于99.5%。
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。
要缩短访问路径有一种办法就是多个相互强依赖的应用合并部署在一起把远程过程调用RPC变成JVM内部之间的方法调用。在《大型网站技术架构演进与性能优化》一书中我也有一章介绍了这种技术的详细实现。
依赖要尽量少
所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。
要减少依赖我们可以给系统进行分级比如0级系统、1级系统、2级系统、3级系统0级系统如果是最重要的系统那么0级系统强依赖的系统也同样是最重要的系统以此类推。
注意0级系统要尽量减少对1级系统的强依赖防止重要的系统被不重要的系统拖垮。例如支付系统是0级系统而优惠券是1级系统的话在极端情况下可以把优惠券给降级防止支付系统被优惠券这个1级系统给拖垮。
不要有单点
系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。
那如何避免单点呢?我认为关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。
如何那把服务的状态和机器解耦呢?这里也有很多实现方式。例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。
应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景一般要通过冗余多个备份的方式来解决单点问题。
前面介绍了这些设计上的一些原则,但是你有没有发现,我一直说的是“尽量”而不是“绝对”?
我想你肯定会问是不是请求最少就一定最好我的答案是“不一定”。我们曾经把有些CSS内联进页面里这样做可以减少依赖一个CSS的请求从而加快首页的渲染但是同样也增大了页面的大小又不符合“数据要尽量少”的原则这种情况下我们为了提升首屏的渲染速度只把首屏的HTML依赖的CSS内联进来其他CSS仍然放到文件中作为依赖加载尽量实现首屏的打开速度与整个页面加载性能的平衡。
所以说,架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。我希望你记住的是,这里所说的几点都只是一个个方向,你应该尽量往这些方向上去努力,但也要考虑平衡其他因素。
不同场景下的不同架构案例
前面我说了一些架构上的原则,那么针对“秒杀”这个场景,怎样才是一个好的架构呢?下面我以淘宝早期秒杀系统架构的演进为主线,来帮你梳理不同的请求体量下,我认为的最佳秒杀系统架构。
如果你想快速搭建一个简单的秒杀系统,只需要把你的商品购买页面增加一个“定时上架”功能,仅在秒杀开始时才让用户看到购买按钮,当商品的库存卖完了也就结束了。这就是当时第一个版本的秒杀系统实现方式。
但随着请求量的加大比如从1w/s到了10w/s的量级这个简单的架构很快就遇到了瓶颈因此需要做架构改造来提升系统性能。这些架构改造包括
把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化,例如这个独立出来的系统就减少了店铺装修的功能,减少了页面的复杂度;
在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;
将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
增加秒杀答题,防止有秒杀器抢单。
此时的系统架构变成了下图这个样子。最重要的就是秒杀详情成为了一个独立的新系统另外核心的一些数据放到了缓存Cache其他的关联系统也都以独立集群的方式进行部署。
图1 改造后的系统架构
然而这个架构仍然支持不了超过100w/s的请求量所以为了进一步提升秒杀系统的性能我们又对架构做进一步升级比如
对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
增加系统限流保护,防止最坏情况发生。
经过这些优化,系统架构变成了下图中的样子。在这里,我们对页面进行了进一步的静态化,秒杀过程中不需要刷新整个页面,而只需要向服务端请求很少的动态数据。而且,最关键的详情和交易系统都增加了本地缓存,来提前缓存秒杀商品的信息,热点数据库也做了独立部署,等等。
图2 进一步改造后的系统架构
从前面的几次升级来看,其实越到后面需要定制的地方越多,也就是越“不通用”。例如,把秒杀商品缓存在每台机器的内存中,这种方式显然不适合太多的商品同时进行秒杀的情况,因为单机的内存始终有限。所以要取得极致的性能,就要在其他地方(比如,通用性、易用性、成本等方面)有所牺牲。
总结
来让我们回顾下前面的内容我首先介绍了构建大并发、高性能、高可用系统中几种通用的优化思路并抽象总结为“4要1不要”原则也就是数据要尽量少、请求数要尽量少、路径要尽量短、依赖要尽量少以及不要有单点。当然这几点是你要努力的方向具体操作时还是要密切结合实际的场景和具体条件来进行。
然后,我给出了实际构建秒杀系统时,根据不同级别的流量,由简单到复杂打造的几种系统架构,希望能供你参考。当然,这里面我没有说具体的解决方案,比如缓存用什么、页面静态化用什么,因为这些对于架构来说并不重要,作为架构师,你应该时刻提醒自己主线是什么。
说了这么多,总体上我希望给你一个方向,就是想构建大并发、高性能、高可用的系统应该从哪几个方向上去努力,然后在不同性能要求的情况下系统架构应该从哪几个方面去做取舍。同时你也要明白,越追求极致性能,系统定制开发就会越多,同时系统的通用性也就会越差。
最后,欢迎你在评论区和我分享你在设计秒杀系统时的一些经验和思考,你的经验对我们这个专栏来说也很重要。

View File

@ -0,0 +1,184 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 如何才能做好动静分离?有哪些方案可选?
上一篇文章中,我介绍了秒杀系统在架构上要考虑的几个原则,我估计你很快就会问:“知易行难,这些原则应该怎么应用到系统中呢?”别急,从这篇文章开始,我就会逐一介绍秒杀系统的各个关键环节中涉及的关键技术。
今天我们就先来讨论第一个关键点:数据的动静分离。不知道你之前听过这个解决方案吗?不管你有没有听过,我都建议你先停下来思考动静分离的价值。如果你的系统还没有开始应用动静分离的方案,那你也可以想想为什么没有,是之前没有想到,还是说业务体量根本用不着?
不过我可以确信地说,如果你在一个业务飞速发展的公司里,并且你在深度参与公司内类秒杀类系统的架构或者开发工作,那么你迟早会想到动静分离的方案。为什么?很简单,秒杀的场景中,对于系统的要求其实就三个字:快、准、稳。
那怎么才能“快”起来呢?我觉得抽象起来讲,就只有两点,一点是提高单次请求的效率,一点是减少没必要的请求。今天我们聊到的“动静分离”其实就是瞄着这个大方向去的。
不知道你是否还记得,最早的秒杀系统其实是要刷新整体页面的,但后来秒杀的时候,你只要点击“刷新抢宝”按钮就够了,这种变化的本质就是动静分离,分离之后,客户端大幅度减少了请求的数据量。这不自然就“快”了吗?
何为动静数据
那到底什么才是动静分离呢所谓“动静分离”其实就是把用户请求的数据如HTML页面划分为“动态数据”和“静态数据”。
简单来说“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和URL、浏览者、时间、地域相关以及是否含有Cookie等私密数据。比如说
很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。
我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。
这里再强调一下我们所说的静态数据不能仅仅理解为传统意义上完全存在磁盘上的HTML页面它也可能是经过Java系统产生的页面但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”并不是说数据本身是否动静而是数据中是否含有和访问者相关的个性化数据。
还有一点要注意就是页面中“不包含”指的是“页面的HTML源码中不含有”这一点务必要清楚。
理解了静态数据和动态数据,我估计你很容易就能想明白“动静分离”这个方案的来龙去脉了。分离了动静数据,我们就可以对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。
那么,怎样对静态数据做缓存呢?我在这里总结了几个重点。
第一你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据因此我们可以把它们缓存起来。缓存到哪里呢常见的就三种用户浏览器里、CDN上或者在服务端的Cache中。你应该根据情况把它们尽量缓存到离用户最近的地方。
第二静态化改造就是要直接缓存HTTP连接。相较于普通的数据缓存而言你肯定还听过系统的静态化改造。静态化改造是直接缓存HTTP连接而不是仅仅缓存数据如下图所示Web代理服务器根据请求URL直接取出对应的HTTP响应头和响应体然后直接返回这个响应过程简单得连HTTP协议都不用重新组装甚至连HTTP请求头也不需要解析。
图1 静态化改造
第三让谁来缓存静态数据也很重要。不同语言写的Cache软件处理缓存数据的效率也各不相同。以Java为例因为Java系统本身也有其弱点比如不擅长处理大量连接请求每个连接消耗的内存较多Servlet容器解析HTTP协议较慢所以你可以不在Java层做缓存而是直接在Web服务器层上做这样你就可以屏蔽Java语言层面的一些弱点而相比起来Web服务器如Nginx、Apache、Varnish也更擅长处理大并发的静态文件请求。
如何做动静分离的改造
理解了动静态数据的“why”和“what”接下来我们就要看“how”了。我们如何把动态页面改造成适合缓存的静态页面呢其实也很简单就是去除前面所说的那几个影响因素把它们单独分离出来做动静分离。
下面我以典型的商品详情系统为例来详细介绍。这里你可以先打开京东或者淘宝的商品详情页看看这个页面里都有哪些动静数据。我们从以下5个方面来分离出动态内容。
URL唯一化。商品详情系统天然地就可以做到URL唯一化比如每个商品都由ID来标识那么http://item.xxx.com/item.htm?id=xxxx就可以作为唯一的URL标识。为啥要URL唯一呢前面说了我们是要缓存整个HTTP连接那么以什么作为Key呢就以URL作为缓存的Key例如以id=xxx这个格式进行区分。
分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
分离时间因素。服务端输出的时间也通过动态请求获取。
异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
去掉Cookie。服务端输出的页面包含的Cookie可以通过代码软件来删除如Web服务器Varnish可以通过unset req.http.cookie 命令去掉Cookie。注意这里说的去掉Cookie并不是用户端收到的页面就不含Cookie了而是说在缓存的静态数据中不含有Cookie。
分离出动态内容之后如何组织这些内容页就变得非常关键了。这里我要提醒你一点因为这其中很多动态内容都会被页面中的其他模块用到如判断该用户是否已登录、用户ID是否匹配等所以这个时候我们应该将这些信息JSON化用JSON格式组织这些数据以方便前端获取。
前面我们介绍里用缓存的方式来处理静态数据。而动态内容的处理通常有两种方案ESIEdge Side Includes方案和CSIClient Side Include方案。
ESI方案或者SSI即在Web代理服务器上做动态内容请求并将请求插入到静态页面中当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响但是用户体验较好。
CSI方案。即单独发起一个异步JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
动静分离的几种架构方案
前面我们通过改造把静态数据和动态数据做了分离,那么如何在系统架构上进一步对这些动态和静态数据重新组合,再完整地输出给用户呢?
这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度有3种方案可选
实体机单机部署;
统一Cache层
上CDN。
方案1实体机单机部署
这种方案是将虚拟机改为实体机以增大Cache的容量并且采用了一致性Hash分组的方式来提升命中率。这里将Cache分成若干组是希望能达到命中率和访问热点的平衡。Hash分组越少缓存的命中率肯定就会越高但短板是也会使单个商品集中在一个分组中容易导致Cache被击穿所以我们应该适当增加多个相同的分组来平衡访问热点和命中率的问题。
这里我给出了实体机单机部署方案的结构图,如下:
图2 Nginx+Cache+Java结构实体机单机部署
实体机单机部署有以下几个优点:
没有网络瓶颈,而且能使用大内存;
既能提升命中率又能减少Gzip压缩
减少Cache失效压力因为采用定时失效方式例如只缓存3秒钟过期即自动失效。
这个方案中虽然把通常只需要虚拟机或者容器运行的Java应用换成实体机优势很明显它会增加单机的内存容量但是一定程度上也造成了CPU的浪费因为单个的Java进程很难用完整个实体机的CPU。
另外就是一个实体机上部署了Java应用又作为Cache来使用这造成了运维上的高复杂度所以这是一个折中的方案。如果你的公司里没有更多的系统有类似需求那么这样做也比较合适如果你们有多个业务系统都有静态化改造的需求那还是建议把Cache层单独抽出来公用比较合理如下面的方案2所示。
方案2统一Cache层
所谓统一Cache层就是将单机的Cache统一分离出来形成一个单独的Cache集群。统一Cache层是个更理想的可推广方案该方案的结构图如下
图3 统一Cache
将Cache层单独拿出来统一管理可以减少运维成本同时也方便接入其他静态化系统。此外它还有一些优点。
单独一个Cache层可以减少多个应用接入时使用Cache的成本。这样接入的应用只要维护自己的Java系统就好不需要单独维护Cache而只关心如何使用即可。
统一Cache的方案更易于维护如后面加强监控、配置的自动化只需要一套解决方案就行统一起来维护升级也比较方便。
可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击。
这种方案虽然维护上更方便了,但是也带来了其他一些问题,比如缓存更加集中,导致:
Cache层内部交换网络成为瓶颈
缓存服务器的网卡也会是瓶颈;
机器少风险较大,挂掉一台就会影响很大一部分缓存数据。
要解决上面这些问题可以再对Cache做Hash分组即一组Cache缓存的内容相同这样能够避免热点数据过度集中导致新的瓶颈产生。
方案3上CDN
在将整个系统做动静分离后我们自然会想到更进一步的方案就是将Cache进一步前移到CDN上因为CDN离用户最近效果会更好。
但是要想这么做,有以下几个问题需要解决。
失效问题。前面我们也有提到过缓存时效的问题不知道你有没有理解我再来解释一下。谈到静态数据时我说过一个关键词叫“相对不变”它的言外之意是“可能会变化”。比如一篇文章现在不变但如果你发现个错别字是不是就会变化了如果你的缓存时效很长那用户端在很长一段时间内看到的都是错的。所以这个方案中也是我们需要保证CDN可以在秒级时间内让分布在全国各地的Cache同时失效这对CDN的失效系统要求很高。
命中率问题。Cache最重要的一个衡量指标就是“高命中率”不然Cache的存在就失去了意义。同样如果将数据全部放到全国的CDN上必然导致Cache分散而Cache分散又会导致访问请求命中同一个Cache的可能性降低那么命中率就成为一个问题。
发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且你还要考虑有问题时快速回滚和排查问题的简便性。
从前面的分析来看将商品详情系统放到全国的所有CDN节点上是不太现实的因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢答案是“可以”但是这样的节点需要满足几个条件
靠近访问量比较集中的地区;
离主站相对较远;
节点到主站间的网络比较好,而且稳定;
节点容量比较大不会占用其他CDN太多的资源。
最后,还有一点也很重要,那就是:节点不要太多。
基于上面几个因素选择CDN的二级Cache比较合适因为二级Cache数量偏少容量也更大让用户的请求先回源的CDN的二级Cache中如果没命中再回源站获取数据部署方式如下图所示
图4 CDN化部署方案
使用CDN的二级Cache作为缓存可以达到和当前服务端静态化Cache类似的命中率因为节点数不多Cache不是很分散访问量也比较集中这样也就解决了命中率问题同时能够给用户最好的访问体验是当前比较理想的一种CDN化方案。
除此之外CDN化部署方案还有以下几个特点
把整个页面缓存在用户浏览器中;
如果强制刷新整个页面也会请求CDN
实际有效请求,只是用户对“刷新抢宝”按钮的点击。
这样就把90%的静态数据缓存在了用户端或者CDN上当真正秒杀时用户只需要点击特殊的“刷新抢宝”按钮而不需要刷新整个页面。这样一来系统只是向服务端请求很少的有效数据而不需要重复请求大量的静态数据。
秒杀的动态数据和普通详情页面的动态数据相比更少性能也提升了3倍以上。所以“抢宝”这种设计思路让我们不用刷新页面就能够很好地请求到服务端最新的动态数据。
总结一下
今天我主要介绍了实现动静分离的几种思路并由易到难给出了几种架构方案以及它们各自的优缺点。可以看到不同的架构方案会引入不同的问题比如我们把缓存数据从CDN上移到用户的浏览器里针对秒杀这个场景是没问题的但针对一般的商品可否也这样做呢
你可能会问存储在浏览器或CDN上有多大区别我的回答是区别很大因为在CDN上我们可以做主动失效而在用户的浏览器里就更不可控如果用户不主动刷新的话你很难主动地把消息推送给用户的浏览器。
另外在什么地方把静态数据和动态数据合并并渲染出一个完整的页面也很关键假如在用户的浏览器里合并那么服务端可以减少渲染整个页面的CPU消耗。如果在服务端合并的话就要考虑缓存的数据是否进行Gzip压缩了如果缓存Gzip压缩后的静态数据可以减少缓存的数据量但是进行页面合并渲染时就要先解压然后再压缩完整的页面数据输出给用户如果缓存未压缩的静态数据这样不用解压静态数据但是会增加缓存容量。虽然这些都是细节问题但你在设计架构方案时都需要考虑清楚。
最后,欢迎在留言区分享讨论你对于数据动静分离的一些关键认知,我会第一时间给你反馈。

View File

@ -0,0 +1,109 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 二八原则:有针对性地处理好系统的“热点数据”
假设你的系统中存储有几十亿上百亿的商品,而每天有千万级的商品被上亿的用户访问,那么肯定有一部分被大量用户访问的热卖商品,这就是我们常说的“热点商品”。
这些热点商品中最极端的例子就是秒杀商品,它们在很短时间内被大量用户执行访问、添加购物车、下单等操作,这些操作我们就称为“热点操作”。那么问题来了:这些热点对系统有啥影响,我们非要关注这些热点吗?
为什么要关注热点
我们一定要关注热点,因为热点会对系统产生一系列的影响。
首先热点请求会大量占用服务器处理资源虽然这个热点可能只占请求总量的亿分之一然而却可能抢占90%的服务器资源,如果这个热点请求还是没有价值的无效请求,那么对系统资源来说完全是浪费。
其次,即使这些热点是有效的请求,我们也要识别出来做针对性的优化,从而用更低的代价来支撑这些热点请求。
既然热点对系统来说这么重要,那么热点到底包含哪些内容呢?
什么是“热点”
热点分为热点操作和热点数据。所谓“热点操作”例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操作。对系统来说这些操作可以抽象为“读请求”和“写请求”这两种热点请求的处理方式大相径庭读请求的优化空间要大一些而写请求的瓶颈一般都在存储层优化的思路就是根据CAP理论做平衡这个内容我在“减库存”一文再详细介绍。
而“热点数据”比较好理解,那就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。
所谓“静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
所谓“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。
由于热点操作是用户的行为,我们不好改变,但能做一些限制和保护,所以本文我主要针对热点数据来介绍如何进行优化。
发现热点数据
前面,我介绍了如何对单个秒杀商品的页面数据进行动静分离,以便针对性地对静态数据做优化处理,那么另外一个关键的问题来了:如何发现这些秒杀商品,或者更准确地说,如何发现热点商品呢?
你可能会说“参加秒杀的商品就是秒杀商品啊”,没错,关键是系统怎么知道哪些商品参加了秒杀活动呢?所以,你要有一个机制提前来区分普通商品和秒杀商品。
我们从发现静态热点和发现动态热点两个方面来看一下。
发现静态热点数据
如前面讲的,静态热点数据可以通过商业手段,例如强制让卖家通过报名参加的方式提前把热点商品筛选出来,实现方式是通过一个运营系统,把参加活动的商品数据进行打标,然后通过一个后台系统对这些热点商品进行预处理,如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题,即增加卖家的使用成本,而且实时性较差,也不太灵活。
不过除了提前报名筛选这种方式你还可以通过技术手段提前预测例如对买家每天访问的商品进行大数据计算然后统计出TOP N的商品我们可以认为这些TOP N的商品就是热点商品。
发现动态热点数据
我们可以通过卖家报名或者大数据预测这些手段来提前预测静态热点数据,但这其中有一个痛点,就是实时性较差,如果我们的系统能在秒级内自动发现热点商品那就完美了。
能够动态地实时发现热点不仅对秒杀商品,对其他热卖商品也同样有价值,所以我们需要想办法实现热点的动态发现功能。
这里我给出一个动态热点发现系统的具体实现。
构建一个异步的系统它可以收集交易链路上各个环节中的中间件产品的热点Key如Nginx、缓存、RPC服务框架等这些中间件一些中间件产品本身已经有热点统计模块
建立一个热点上报和可以按照需求订阅的热点服务的下发规范主要目的是通过交易链路上各个系统包括详情、购物车、交易、优惠、库存、物流等访问的时间差把上游已经发现的热点透传给下游系统提前做好保护。比如对于大促高峰期详情系统是最早知道的在统一接入层上Nginx模块统计的热点URL。
将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。
这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。
图1 一个动态热点发现系统
我们通过部署在每台机器上的Agent把日志汇总到聚合和分析集群中然后把符合一定规则的热点数据通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到Cache中或者直接推送到应用服务器的内存中还可以对这些数据进行拦截总之下游系统可以订阅这些数据然后根据自己的需求决定如何处理这些数据。
打造热点发现系统时,我根据以往经验总结了几点注意事项。
这个热点服务后台抓取热点数据日志最好采用异步方式,因为“异步”一方面便于保证通用性,另一方面又不影响业务系统和中间件产品的主流程。
热点服务发现和中间件自身的热点保护模块并存,每个中间件和应用还需要保护自己。热点服务台提供热点数据的收集和订阅服务,便于把各个系统的热点数据透明出来。
热点发现要做到接近实时3s内完成热点数据的发现因为只有做到接近实时动态发现才有意义才能实时地对下游系统提供保护。
处理热点数据
处理热点数据通常有几种思路:一是优化,二是限制,三是隔离。
先来说说优化。优化热点数据最有效的办法就是缓存热点数据如果热点数据做了动静分离那么可以长期缓存静态数据。但是缓存热点数据更多的是“临时”缓存即不管是静态数据还是动态数据都用一个队列短暂地缓存数秒钟由于队列长度有限可以采用LRU淘汰算法替换。
再来说说限制。限制更多的是一种保护机制限制的办法也有很多例如对被访问商品的ID做一致性Hash然后根据Hash做分桶每个分桶设置一个处理队列这样可以把热点商品限制在一个请求队列里防止因某些热点商品占用太多的服务器资源而使其他请求始终得不到服务器的处理资源。
最后介绍一下隔离。秒杀系统设计的第一个原则就是将这种热点数据隔离出来不要让1%的请求影响到另外的99%隔离出来后也更方便对这1%的请求做针对性的优化。
具体到“秒杀”业务,我们可以在以下几个层次实现隔离。
业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。
系统隔离。系统隔离更多的是运行时的隔离可以通过分组部署的方式和另外99%分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。
数据隔离。秒杀所调用的数据大部分都是热点数据比如会启用单独的Cache集群或者MySQL数据库来放热点数据目的也是不想0.01%的数据有机会影响99.99%数据。
当然了实现隔离有很多种办法。比如你可以按照用户来区分给不同的用户分配不同的Cookie在接入层路由到不同的服务接口中再比如你还可以在接入层针对URL中的不同Path来设置限流策略。服务层调用不同的服务接口以及数据层通过给数据打标来区分等等这些措施其目的都是把已经识别出来的热点请求和普通的请求区分开。
总结一下
本文与数据的动静分离不一样它从另外一个维度对数据进行了区分处理。你要明白区分的目的主要还是对读热点数据加以优化对照“4要1不要”原则它可以减少请求量也可以减少请求的路径。因为缓存的数据都是经过多个请求或者从多个系统中获取的数据经过计算后的结果。
热点的发现和隔离不仅对“秒杀”这个场景有意义,对其他的高性能分布式系统也非常有价值,尤其是热点的隔离非常重要。我介绍了业务层面的隔离和数据层面的隔离方式,最重要最简单的方式就是独立出来一个集群,单独处理热点数据。
但是能够独立出来一个集群的前提还是首先能够发现热点,为此我介绍了发现热点的几种方式,比如人工标识、大数据统计计算,以及实时热点发现方案,希望能够给你启发。
最后,欢迎你在留言区和我交流,你也可以说说在实际工作中,还有哪些发现和解决热点问题的不同思路或方案,非常期待。

View File

@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 流量削峰这事应该怎么做?
如果你看过秒杀系统的流量监控图的话,你会发现它是一条直线,就在秒杀开始那一秒是一条很直很直的线,这是因为秒杀请求在时间上高度集中于某一特定的时间点。这样一来,就会导致一个特别高的流量峰值,它对资源的消耗是瞬时的。
但是对秒杀这个场景来说最终能够抢到商品的人数是固定的也就是说100人和10000人发起请求的结果都是一样的并发度越高无效请求也越多。
但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,但是真正开始下单时,秒杀请求并不是越多越好。因此我们可以设计一些规则,让并发的请求更多地延缓,而且我们甚至可以过滤掉一些无效请求。
为什么要削峰
为什么要削峰呢?或者说峰值会带来哪些坏处?
我们知道服务器的处理资源是恒定的,你用或者不用它的处理能力都是一样的,所以出现峰值的话,很容易导致忙到处理不过来,闲的时候却又没有什么要处理。但是由于要保证服务质量,我们的很多处理资源只能按照忙的时候来预估,而这会导致资源的一个浪费。
这就好比因为存在早高峰和晚高峰的问题,所以有了错峰限行的解决方案。削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。针对秒杀这一场景,削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求,它遵从“请求数要尽量少”的原则。
今天,我就来介绍一下流量削峰的一些操作思路:排队、答题、分层过滤。这几种方式都是无损(即不会损失用户的发出请求)的实现方案,当然还有些有损的实现方案,包括我们后面要介绍的关于稳定性的一些办法,比如限流和机器负载保护等一些强制措施也能达到削峰保护的目的,当然这都是不得已的一些措施,因此就不归类到这里了。
排队
要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。在这里,消息队列就像“水库”一样, 拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。
用消息队列来缓冲瞬时流量的方案,如下图所示:
图1 用消息队列来缓冲瞬时流量
但是,如果流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列同样也会被压垮,这样虽然保护了下游的系统,但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时,即使是有水库恐怕也无济于事。
除了消息队列,类似的排队方式还有很多,例如:
利用线程池加锁等待也是一种常用的排队方式;
先进先出、先进后出等常用的内存排队算法的实现方式;
把请求序列化到文件中然后再顺序地读文件例如基于MySQL binlog的同步机制来恢复请求等方式。
可以看到,这些方式都有一个共同特征,就是把“一步的操作”变成“两步的操作”,其中增加的一步操作用来起到缓冲的作用。
说到这里你可能会说这样一来增加了访问请求的路径啊并不符合我们介绍的“4要1不要”原则。没错的确看起来不太合理但是如果不增加一个缓冲步骤那么在一些场景下系统很可能会直接崩溃所以最终还是需要你做出妥协和平衡。
答题
你是否还记得,最早期的秒杀只是纯粹地刷新页面和点击购买按钮,它是后来才增加了答题功能的。那么,为什么要增加答题功能呢?
这主要是为了增加购买的复杂度,从而达到两个目的。
第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊。2011年秒杀非常火的时候秒杀器也比较猖獗因而没有达到全民参与和营销的目的所以系统增加了答题来限制秒杀器。增加答题后下单的时间基本控制在2s后秒杀器的下单比例也大大下降。答题页面如下图所示。
图2 答题页面
第二个目的其实就是延缓请求起到对请求流量进行削峰的作用从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长从以前的1s之内延长到2s~10s。这样一来请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要会大大减轻压力。而且由于请求具有先后顺序靠后的请求到来时自然也就没有库存了因此根本到不了最后的下单步骤所以真正的并发写就非常有限了。这种设计思路目前用得非常普遍如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式。
这里,我重点说一下秒杀答题的设计思路。
图3 秒杀答题
如上图所示整个秒杀答题的逻辑主要分为3部分。
题库生成模块,这个部分主要就是生成一个个问题和答案,其实题目和答案本身并不需要很复杂,重要的是能够防止由机器来算出结果,即防止秒杀器来答题。
题库的推送模块,用于在秒杀答题前,把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的,目的也是防止答题作弊。
题目的图片生成模块用于把题目生成为图片格式并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题它要求只有人才能理解题目本身的含义。这里还要注意一点由于答题时网络比较拥挤我们应该把题目的图片提前推送到CDN上并且要进行预热不然的话当用户真正请求题目时图片可能加载比较慢从而影响答题的体验。
其实真正答题的逻辑比较简单很好理解当用户提交的答案和题目对应的答案做比较如果通过了就继续进行下一步的下单逻辑否则就失败。我们可以把问题和答案用下面这样的key来进行MD5加密
问题keyuserId+itemId+question_Id+time+PK
答案keyuserId+itemId+answer+PK
验证的逻辑如下图所示:
图4 答题的验证逻辑
注意这里面的验证逻辑除了验证问题的答案以外还包括用户本身身份的验证例如是否已经登录、用户的Cookie是否完整、用户是否重复频繁提交等。
除了做正确性验证我们还可以对提交答案的时间做些限制例如从开始答题到接受答案要超过1s因为小于1s是人为操作的可能性很小这样也能防止机器答题的情况。
分层过滤
前面介绍的排队和答题要么是少发请求,要么对发出来的请求进行缓冲,而针对秒杀场景还有一种方法,就是对请求进行分层过滤,从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示。
图5 分层过滤
假如请求分别经过CDN、前台读系统如商品详情系统、后台系统如交易系统和数据库这几层那么
大部分数据和流量在用户浏览器或者CDN上获取这一层可以拦截大部分数据的读取
经过第二层即前台系统时数据包括强一致性的数据尽量得走Cache过滤一些无效的请求
再到第三层后台系统,主要做数据的二次检验,对系统做好保护和限流,这样数据量和请求就进一步减少;
最后在数据层完成数据的强一致性校验。
这样就像漏斗一样,尽量把数据量和请求量一层一层地过滤和减少了。
分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。
分层校验的基本原则是:
将动态请求的读数据缓存Cache在Web端过滤掉无效的数据读
对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
对写请求做限流保护,将超出系统承载能力的请求过滤掉;
对写数据进行强一致性校验,只保留最后有效的数据。
分层校验的目的是:在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。
总结一下
今天我介绍了如何在网站面临大流量冲击时进行请求的削峰并主要介绍了削峰的3种处理方式一个是通过队列来缓冲请求即控制请求的发出一个是通过答题来延长请求发出的时间在请求发出后承接请求时进行控制最后再对不符合条件的请求进行过滤最后一种是对请求进行分层过滤。
其中,队列缓冲方式更加通用,它适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。
而答题更适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。
分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。
不过,在削峰的处理方式上除了采用技术手段,其实还可以采用业务手段来达到一定效果,例如在零点开启大促的时候由于流量太大导致支付系统阻塞,这个时候可以采用发放优惠券、发起抽奖活动等方式,将一部分流量分散到其他地方,这样也能起到缓冲流量的作用。
最后,欢迎你在留言区和我交流,你也可以说说在实际工作中,还有哪些对流量进行削峰的不同思路或方案,非常期待。

View File

@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 影响性能的因素有哪些?又该如何提高系统的性能?
不知不觉,我们已经讲到第五篇了,不知道听到这里,你对于秒杀系统的构建有没有形成一些框架性的认识,这里我再带你简单回忆下前面的主线。
前面的四篇文章里,我介绍的内容多少都和优化有关:第一篇介绍了一些指导原则;第二篇和第三篇从动静分离和热点数据两个维度,介绍了如何有针对性地对数据进行区分和优化处理;第四篇介绍了在保证实现基本业务功能的前提下,尽量减少和过滤一些无效请求的思路。
这几篇文章既是在讲根据指导原则实现的具体案例,也是在讲如何实现能够让整个系统更“快”。我想说的是,优化本身有很多手段,也是一个复杂的系统工程。今天,我就来结合秒杀这一场景,重点给你介绍下服务端的一些优化技巧。
影响性能的因素
你想要提升性能,首先肯定要知道哪些因素对于系统性能的影响最大,然后再针对这些具体的因素想办法做优化,是不是这个逻辑?
那么哪些因素对性能有影响呢在回答这个问题之前我们先定义一下“性能”服务设备不同对性能的定义也是不一样的例如CPU主要看主频、磁盘主要看IOPSInput/Output Operations Per Second即每秒进行读写操作的次数
而今天我们讨论的主要是系统服务端性能一般用QPSQuery Per Second每秒请求数来衡量还有一个影响和QPS也息息相关那就是响应时间Response TimeRT它可以理解为服务器处理响应的耗时。
正常情况下响应时间RT越短一秒钟处理的请求数QPS自然也就会越多这在单线程处理的情况下看起来是线性的关系即我们只要把每个请求的响应时间降到最低那么性能就会最高。
但是你可能想到响应时间总有一个极限不可能无限下降所以又出现了另外一个维度即通过多线程来处理请求。这样理论上就变成了“总QPS =1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。
接下来,我们一起看看这个两个因素到底会造成什么样的影响。
首先我们先来看看响应时间和QPS有啥关系。
对于大部分的Web系统而言响应时间一般都是由CPU执行时间和线程等待时间比如RPC、IO等待、Sleep、Wait等组成即服务器在处理一个请求时一部分是CPU本身在做运算还有一部分是在各种等待。
理解了服务器处理请求的逻辑估计你会说为什么我们不去减少这种等待时间。很遗憾根据我们实际的测试发现减少线程等待时间对提升性能的影响没有我们想象得那么大它并不是线性的提升关系这点在很多代理服务器Proxy上可以做验证。
如果代理服务器本身没有CPU消耗我们在每次给代理服务器代理的请求加个延时即增加响应时间但是这对代理服务器本身的吞吐量并没有多大的影响因为代理服务器本身的资源并没有被消耗可以通过增加代理服务器的处理线程数来弥补响应时间对代理服务器的QPS的影响。
其实真正对性能有影响的是CPU的执行时间。这也很好理解因为CPU的执行真正消耗了服务器的资源。经过实际的测试如果减少CPU一半的执行时间就可以增加一倍的QPS。
也就是说我们应该致力于减少CPU的执行时间。
其次我们再来看看线程数对QPS的影响。
单看“总QPS”的计算公式你会觉得线程数越多QPS也就会越高但这会一直正确吗显然不是线程数不是越多越好因为线程本身也消耗资源也受到其他因素的制约。例如线程越多系统的线程切换成本就会越高而且每个线程也都会耗费一定内存。
那么,设置什么样的线程数最合理呢?其实很多多线程的场景都有一个默认配置,即“线程数 = 2 * CPU核数 + 1”。除去这个配置还有一个根据最佳实践得出来的公式
线程数 = [(线程等待时间 + 线程CPU时间) / 线程CPU时间] × CPU数量
当然,最好的办法是通过性能测试来发现最佳的线程数。
换句话说要提升性能我们就要减少CPU的执行时间另外就是要设置一个合理的并发线程数通过这两方面来显著提升服务器的性能。
现在你知道了如何来快速提升性能那接下来你估计会问我应该怎么发现系统哪里最消耗CPU资源呢
如何发现瓶颈
就服务器而言会出现瓶颈的地方有很多例如CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外不同的系统对瓶颈的关注度也不一样例如对缓存系统而言制约它的是内存而对存储型系统来说I/O更容易是瓶颈。
这个专栏中我们定位的场景是秒杀它的瓶颈更多地发生在CPU上。
那么如何发现CPU的瓶颈呢其实有很多CPU诊断工具可以发现CPU的消耗最常用的就是JProfiler和Yourkit这两个工具它们可以列出整个请求中每个函数的CPU执行时间可以发现哪个函数消耗的CPU时间最多以便你有针对性地做优化。
当然还有一些办法也可以近似地统计CPU的耗时例如通过jstack定时地打印调用栈如果某些函数调用频繁或者耗时较多那么那些函数就会多次出现在系统调用栈里这样相当于采样的方式也能够发现耗时较多的函数。
虽说秒杀系统的瓶颈大部分在CPU但这并不表示其他方面就一定不出现瓶颈。例如如果海量请求涌过来你的页面又比较大那么网络就有可能出现瓶颈。
怎样简单地判断CPU是不是瓶颈呢一个办法就是看当QPS达到极限时你的服务器的CPU使用率是不是超过了95%如果没有超过那么表示CPU还有提升的空间要么是有锁限制要么是有过多的本地I/O等待发生。
现在你知道了优化哪些因素,又发现了瓶颈,那么接下来就要关注如何优化了。
如何优化系统
对Java系统来说可以优化的地方很多这里我重点说一下比较有效的几种手段供你参考它们是减少编码、减少序列化、Java极致优化、并发读优化。接下来我们分别来看一下。
减少编码
Java的编码运行比较慢这是Java的一大硬伤。在很多场景下只要涉及字符串的操作如输入输出操作、I/O操作都比较耗CPU资源不管它是磁盘I/O还是网络I/O因为都需要将字符转换成字节而这个转换必须编码。
每个字符的编码都需要查表,而这种查表的操作非常耗资源,所以减少字符到字节或者相反的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。
那么如何才能减少编码呢例如网页输出是可以直接进行流输出的即用resp.getOutputStream()函数写数据把一些静态的数据提前转化成字节等到真正往外写的时候再直接用OutputStream()函数写,就可以减少静态数据的编码转换。
我在《深入分析Java Web技术内幕》一书中介绍的“Velocity优化实践”一章的内容就是基于把静态的字符串提前编码成字节并缓存然后直接输出字节内容到页面从而大大减少编码的性能消耗的网页输出的性能比没有提前进行字符到字节转换时提升了30%左右。
减少序列化
序列化也是Java性能的一大天敌减少Java中的序列化操作也能大大提升性能。又因为序列化往往是和编码同时发生的所以减少序列化也就减少了编码。
序列化大部分是在RPC中发生的因此避免或者减少RPC就可以减少序列化当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案就是可以将多个关联性比较强的应用进行“合并部署”而减少不同应用之间的RPC也可以减少序列化的消耗。
所谓“合并部署”就是把两个原本在不同机器上的不同应用合并部署到一台机器上当然不仅仅是部署在一台机器上还要在同一个Tomcat容器中且不能走本机的Socket这样才能避免序列化的产生。
另外针对秒杀场景我们还可以做得更极致一些接下来我们来看第3点Java极致优化。
Java极致优化
Java和通用的Web服务器如Nginx或Apache服务器相比在处理大并发的HTTP请求时要弱一点所以一般我们都会对大流量的Web系统做静态化改造让大部分请求和数据直接在Nginx服务器或者Web代理服务器如Varnish、Squid等上直接返回这样可以减少数据的序列化与反序列化而Java层只需处理少量数据的动态请求。针对这些请求我们可以使用以下手段进行优化
直接使用Servlet处理请求。避免使用传统的MVC框架这样可以绕过一大堆复杂且用处不大的处理逻辑节省1ms时间具体取决于你对MVC框架的依赖程度
直接输出流数据。使用resp.getOutputStream()而不是resp.getWriter()函数可以省掉一些不变字符数据的编码从而提升性能数据输出时推荐使用JSON而不是模板引擎一般都是解释执行来输出页面。
并发读优化
也许有读者会觉得这个问题很容易解决无非就是放到Tair缓存里面。集中式缓存为了保证命中率一般都会采用一致性Hash所以同一个key会落到同一台机器上。虽然单台缓存机器也能支撑30w/s的请求但还是远不足以应对像“大秒”这种级别的热点商品。那么该如何彻底解决单点的瓶颈呢
答案是采用应用层的LocalCache即在秒杀系统的单机上缓存商品相关的数据。
那么又如何缓存Cache数据呢你需要划分成动态数据和静态数据分别进行处理
像商品中的“标题”和“描述”这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;
像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。
你可能还会有疑问:像库存这种频繁更新的数据,一旦数据不一致,会不会导致超卖?
这就要用到前面介绍的读数据的分层校验原则了,读的场景可以允许一定的脏数据,因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保证最终的一致性,通过在数据的高可用性和一致性之间的平衡,来解决高并发的数据读取问题。
总结一下
性能优化的过程首先要从发现短板开始,除了我今天介绍的一些优化措施外,你还可以在减少数据、数据分级(动静分离),以及减少中间环节、增加预处理等这些环节上做优化。
首先是“发现短板”比如考虑以下因素的一些限制光速光速C = 30万千米/秒光纤V = C/1.5=20 万千米/秒即数据传输是有物理距离的限制的、网速2017年11月知名测速网站Ookla发布报告全国平均上网带宽达到61.24 Mbps千兆带宽下10KB数据的极限QPS 为1.25万QPS=1000Mbps/8/10KB、网络结构交换机/网卡的限制、TCP/IP、虚拟机内存/CPU/IO等资源的限制和应用本身的一些瓶颈等。
其次是减少数据。事实上有两个地方特别影响性能一是服务端在处理数据时不可避免地存在字符到字节的相互转化二是HTTP请求时要做Gzip压缩还有网络传输的耗时这些都和数据大小密切相关。
再次,就是数据分级,也就是要保证首屏为先、重要信息为先,次要信息则异步加载,以这种方式提升用户获取数据的体验。
最后就是要减少中间环节,减少字符到字节的转换,增加预处理(提前做字符到字节的转换)去掉不需要的操作。
此外要做好优化你还需要做好应用基线比如性能基线何时性能突然下降、成本基线去年双11用了多少台机器、链路基线我们的系统发生了哪些变化你可以通过这些基线持续关注系统的性能做到在代码上提升编码质量在业务上改掉不合理的调用在架构和调用链路上不断的改进。
最后,欢迎你在留言区和我交流,你也可以说说在实际工作中,关于性能提升还有哪些更好的思路或者方案,我们一起沟通探讨。

View File

@ -0,0 +1,98 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 秒杀系统“减库存”设计的核心逻辑
如果要设计一套秒杀系统,那我想你的老板肯定会先对你说:千万不要超卖,这是大前提。
如果你第一次接触秒杀那你可能还不太理解库存100件就卖100件在数据库里减到0就好了啊这有什么麻烦的是的理论上是这样但是具体到业务场景中“减库存”就不是这么简单了。
例如,我们平常购物都是这样,看到喜欢的商品然后下单,但并不是每个下单请求你都最后付款了。你说系统是用户下单了就算这个商品卖出去了,还是等到用户真正付款了才算卖出了呢?这的确是个问题!
我们可以先根据减库存是发生在下单阶段还是付款阶段,把减库存做一下划分。
减库存有哪几种方式
在正常的电商平台购物场景中用户的实际购买过程一般分为两步下单和付款。你想买一台iPhone手机在商品页面点了“立即购买”按钮核对信息之后点击“提交订单”这一步称为下单操作。下单之后你只有真正完成付款操作才能算真正购买也就是俗话说的“落袋为安”。
那如果你是架构师,你会在哪个环节完成减库存的操作呢?总结来说,减库存操作一般有如下几个方式:
下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
预扣库存这种方式相对复杂一些买家下单后库存为其保留一定的时间如10分钟超过这个时间库存将会自动释放释放后其他买家就可以继续购买。在买家付款前系统会校验该订单的库存是否还有保留如果没有保留则再次尝试预扣如果库存不足也就是预扣失败则不允许继续付款如果预扣成功则完成付款并实际地减去库存。
以上这几种减库存的方式都会存在一些问题,下面我们一起来看下。
减库存可能存在的问题
由于购物过程中存在两步或者多步的操作,因此在不同的操作步骤中减库存,就会存在一些可能被恶意买家利用的漏洞,例如发生恶意下单的情况。
假如我们采用“下单减库存”的方式,即用户下单后就减去库存,正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单,让这款商品的库存减为零,那么这款商品就不能正常售卖了。要知道,这些恶意下单的人是不会真正付款的,这正是“下单减库存”方式的不足之处。
既然“下单减库存”可能导致恶意下单,从而影响卖家的商品销售,那么有没有办法解决呢?你可能会想,采用“付款减库存”的方式是不是就可以了?的确可以。但是,“付款减库存”又会导致另外一个问题:库存超卖。
假如有100件商品就可能出现300人下单成功的情况因为下单时不会减库存所以也就可能出现下单成功数远远超过真正库存数的情况这尤其会发生在做活动的热门商品上。这样一来就会导致很多买家下单成功但是付不了款买家的购物体验自然比较差。
可以看到,不管是“下单减库存”还是“付款减库存”,都会导致商品库存不能完全和实际售卖情况对应起来的情况,看来要把商品准确地卖出去还真是不容易啊!
那么,既然“下单减库存”和“付款减库存”都有缺点,我们能否把两者相结合,将两次操作进行前后关联起来,下单时先预扣,在规定时间内不付款再释放库存,即采用“预扣库存”这种方式呢?
这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢其实没有针对恶意下单这种情况虽然把有效的付款时间设置为10分钟但是恶意买家完全可以在10分钟后再次下单或者采用一次下单很多件的方式把库存减完。针对这种情况解决办法还是要结合安全和反作弊的措施来制止。
例如给经常下单不付款的买家进行识别打标可以在被打标的买家下单时不减库存、给某些类目设置最大购买件数例如参加活动的商品一人最多只能买3件以及对重复下单不付款的操作进行次数限制等。
针对“库存超卖”这种情况在10分钟时间内下单的数量仍然有可能超过库存数量遇到这种情况我们只能区别对待对普通的商品下单数量超过库存数量的情况可以通过补货来解决但是有些卖家完全不允许库存为负数的情况那只能在买家付款时提示库存不足。
大型秒杀中如何减库存?
目前来看,业务系统中最常见的就是预扣库存方案,像你在买机票、买电影票时,下单后一般都有个“有效付款时间”,超过这个时间订单自动释放,这都是典型的预扣库存方案。而具体到秒杀这个场景,应该采用哪种方案比较好呢?
由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。
“下单减库存”在数据一致性上主要就是保证大并发请求时库存数据不能为负数也就是要保证数据库中的库存字段值不能为负数一般我们有多种解决方案一种是在应用程序中通过事务来判断即保证减后库存不能为负数否则就回滚另一种办法是直接设置数据库的字段数据为无符号整数这样减后库存字段值小于零时会直接执行SQL语句来报错再有一种就是使用CASE WHEN判断语句例如这样的SQL语句
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
秒杀减库存的极致优化
在交易环节中“库存”是个关键数据也是个热点数据因为交易的各个环节中都可能涉及对库存的查询。但是我在前面介绍分层过滤时提到过秒杀中并不需要对库存有精确的一致性读把库存数据放到缓存Cache可以大大提升读性能。
解决大并发读问题可以采用LocalCache即在秒杀系统的单机上缓存商品相关的数据和对数据进行分层过滤的方式但是像减库存这种大并发写无论如何还是避免不了这也是秒杀场景下最为核心的一个技术难题。
因此,这里我想专门来说一下秒杀场景下减库存的极致优化思路,包括如何在缓存中减库存以及如何在数据库中减库存。
秒杀商品和普通商品的减库存还是有些差异的例如商品数量比较少交易时间段也比较短因此这里有一个大胆的假设即能否把秒杀商品减库存直接放到缓存系统中实现也就是直接在缓存中减库存或者在一个带有持久化功能的缓存系统如Redis中完成呢
如果你的秒杀商品的减库存逻辑非常单一比如没有复杂的SKU库存和总库存这种联动关系的话我觉得完全可以。但是如果有比较复杂的减库存逻辑或者需要使用事务你还是必须在数据库中完成减库存。
由于MySQL存储数据的特点同一数据在数据库里肯定是一行存储MySQL因此会有大量线程来竞争InnoDB行锁而并发度越高时等待线程会越多TPSTransaction Per Second即每秒处理的消息数会下降响应时间RT会上升数据库的吞吐量就会严重受影响。
这就可能引发一个问题,就是单个热点商品会影响整个数据库的性能, 导致0.01%的商品影响99.99%的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。
而分离热点商品到单独的数据库还是没有解决并发锁的问题,我们应该怎么办呢?要解决并发锁的问题,有两种办法:
应用层做排队。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。
数据库层做排队。应用层只能做到单机的排队但是应用机器数本身很多这种排队方式控制并发的能力仍然有限所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种MySQL的InnoDB层上的补丁程序patch可以在数据库层上对单行记录做到并发排队。
你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别?
如果熟悉MySQL的话你会知道InnoDB内部的死锁检测以及MySQL Server和InnoDB的切换会比较消耗性能淘宝的MySQL核心团队还做了很多其他方面的优化如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的补丁程序配合在SQL里面加提示hint在事务里不需要等待应用层提交COMMIT而在数据执行完最后一条SQL后直接根据TARGET_AFFECT_ROW的结果进行提交或回滚可以减少网络等待时间平均约0.7ms。据我所知目前阿里MySQL团队已经将包含这些补丁程序的MySQL开源。
另外数据更新问题除了前面介绍的热点隔离和排队处理之外还有些场景如对商品的lastmodifytime字段的更新会非常频繁在某些场景下这些多条SQL是可以合并的一定时间内只要执行最后一条SQL就行了以便减少对数据库的更新操作。
总结一下
今天,我围绕商品减库存的场景,介绍了减库存的三种实现方案,以及分别存在的问题和可能的缓解办法。最后,我又聚焦秒杀这个场景说了如何实现减库存,以及在这个场景下做到极致优化的一些思路。
当然减库存还有很多细节问题,例如预扣的库存超时后如何进行库存回补,再比如目前都是第三方支付,如何在付款时保证减库存和成功付款时的状态一致性,这些都是很大的挑战。
如果你也有实现减库存的经验或者问题,欢迎留言与我分享。

View File

@ -0,0 +1,97 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 准备Plan B如何设计兜底方案_
这是《如何设计一个秒杀系统》专栏的最后一篇文章,前面我们一起看了很多极致的优化思路,以及架构的优化方案。但是很遗憾,现实中总难免会发生一些这样或者那样的意外,而这些看似不经意的意外,却可能带来非常严重的后果。
我想对于很多秒杀系统而言,在诸如双十一这样的大流量的迅猛冲击下,都曾经或多或少发生过宕机的情况。当一个系统面临持续的大流量时,它其实很难单靠自身调整来恢复状态,你必须等待流量自然下降或者人为地把流量切走才行,这无疑会严重影响用户的购物体验。
同时,你也要知道,没有人能够提前预估所有情况,意外无法避免。那么,我们是不是就没办法了呢?当然不是,我们可以在系统达到不可用状态之前就做好流量限制,防止最坏情况的发生。用现在流行的话来说,任何一个系统,都需要“反脆弱”。
具体到秒杀这一场景下为了保证系统的高可用我们必须设计一个Plan B方案来兜底这样在最坏情况发生时我们仍然能够从容应对。今天我们就来看下兜底方案设计的一些具体思路。
高可用建设应该从哪里着手
说到系统的高可用建设,它其实是一个系统工程,需要考虑到系统建设的各个阶段,也就是说它其实贯穿了系统建设的整个生命周期,如下图所示:
图1 高可用系统建设
具体来说,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时。接下来,我们分别看一下。
架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。
编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。
测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。
运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。
为什么系统的高可用建设要放到整个生命周期中全面考虑?因为我们在每个环节中都可能犯错,而有些环节犯的错,你在后面是无法弥补的。例如在架构阶段,你没有消除单点问题,那么系统上线后,遇到突发流量把单点给挂了,你就只能干瞪眼,有时候想加机器都加不进去。所以高可用建设是一个系统工程,必须在每个环节都做好。
那么针对秒杀系统,我们重点介绍在遇到大流量时,应该从哪些方面来保障系统的稳定运行,所以更多的是看如何针对运行阶段进行处理,这就引出了接下来的内容:降级、限流和拒绝服务。
降级
所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
降级方案可以这样设计当秒杀流量达到5w/s时把成交记录的获取从展示20条降级到只展示5条。“从20改到5”这个操作由一个开关来实现也就是设置一个能够从开关系统动态获取的系统参数。
这里我给出开关系统的示意图。它分为两部分一部分是开关控制台它保存了开关的具体配置信息以及具体执行开关所对应的机器列表另一部分是执行下发开关数据的Agent主要任务就是保证开关被正确执行即使系统重启后也会生效。
图2 开关系统
执行降级无疑是在系统性能和用户体验之间选择了前者降级后肯定会影响一部分用户的体验例如在双11零点时如果优惠券系统扛不住可能会临时降级商品详情的优惠信息展示把有限的系统资源用在保障交易系统正确展示优惠信息上即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定是一个不得已而为之的举措。
限流
如果说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
这里我同样给出了限流系统的示意图。总体来说限流既可以是在客户端限流也可以是在服务端限流。此外限流的实现方式既要支持URL以及方法级别的限流也要支持基于QPS和线程的限流。
首先,我以内部的系统调用为例,来分别说下客户端限流和服务端限流的优缺点。
客户端限流,好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。
服务端限流,好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。
图3 限流系统
在限流的实现手段上来讲基于QPS和线程数的限流应用最多最大QPS很容易通过压测提前获取例如我们的系统最高支持1w QPS时可以设置8000来进行限流保护。线程数限流在客户端比较有效例如在远程调用时我们设置连接池的线程数超出这个并发线程请求就将线程进行排队或者直接超时丢弃。
限流无疑会影响用户的正常请求所以必然会导致一部分用户请求失败因此在系统处理这种异常时一定要设置超时时间防止因被限流的请求不能fast fail快速失败而拖垮系统。
拒绝服务
如果限流还不能解决问题,最后一招就是直接拒绝服务了。
当系统负载达到一定阈值时例如CPU使用率达到90%或者系统load值达到2*CPU核数时系统直接拒绝所有请求这种方式是最暴力但也最有效的系统保护方式。例如秒杀系统我们在如下几个环节设计过载保护
在最前端的Nginx上设置过载保护当机器负载达到某个值时直接拒绝HTTP请求并返回503错误码在Java层同样也可以设计过载保护。
拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
总结一下
网站的高可用建设是基础,可以说要深入到各个环节,更要长期规划并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设,每一个环节可能都有很多事情要做。
另外,要保证高可用建设的落实,你不仅要做系统建设,还要在组织上做好保障。高可用其实就是在说“稳定性”。稳定性是一个平时不重要,但真出了问题就会要命的事儿,所以很可能平时业务发展良好,稳定性建设就会给业务让路,相关的稳定性负责人员平时根本得不到重视,一旦遇到故障却又成了“背锅侠”。
而要防止出现这种情况就必须在组织上有所保障例如可以让业务负责人背上稳定性KPI考核指标然后在技术部门中建立稳定性建设小组小组成员由每个业务线的核心力量兼任他们的KPI由稳定性负责人来打分这样稳定性小组就可以把一些体系化的建设任务落实到具体的业务系统中了。
最后,欢迎你在留言区和我交流,你也可以说说在实际工作中,还有哪些关于系统稳定性建设方面有更好的思路或者方案,我们一起沟通探讨。

View File

@ -0,0 +1,80 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 答疑解惑:缓存失效的策略应该怎么定?
十一黄金周的时候,极客时间团队邀请到了前阿里巴巴高级技术专家许令波专门撰写了《如何设计一个秒杀系统》专栏,希望带你透彻理解秒杀系统的各个关键技术点,并借助“秒杀”这个互联网高并发场景中的典型代表,带你了解如何打造一个超大流量并发读写、高性能,以及高可用的系统架构。
专栏虽然只有短短7篇但却持续获得大量用户的支持和赞誉。留言区我们更是可以看到大量从学习角度或业务角度出发提出的各种问题。为此我们也特别邀请专栏作者许令波就一些关键或普遍的问题进一步“加餐”解答希望能够给你更好的帮助。
1. “06 | 秒杀系统‘减库存’设计的核心逻辑”一文中,很多用户比较关注应用层排队的问题,大家主要的疑问就是应用层用队列接受请求,然后结果怎么返回的问题。
其实我这里所说的排队,更多地是说在服务端的服务调用之间采用排队的策略。例如,秒杀需要调用商品服务、调用价格优惠服务或者是创建订单服务,由于调用这些服务出现性能瓶颈,或者由于热点请求过于集中导致远程调用的连接数都被热点请求占据,那么那些正常的商品请求(非秒杀商品)就得不到服务器的资源了,这样对整个网站来说是不公平的。
再比如说,正常整个网站上每秒只有几万个请求,这几万个请求可能是非常分散的,那么假如现在有一个秒杀商品,这个秒杀商品带来的瞬间请求一下子就打满了我们的服务器资源,这样就会导致那些正常的几万个请求得不到正常的服务,这个情况对系统来说是绝对不合理的,也是应该避免的。
所以我们设计了一些策略,把秒杀系统独立出来,部署单独的一些服务器,也隔离了一些热点的数据库,等等。但是实际上不能把整个秒杀系统涉及的所有系统都独立部署一套,不然这样代价太大。
既然不能所有系统都独立部署一套势必就会存在一部分系统不能区分秒杀请求和正常请求那么要如何防止前面所说的问题出现呢通常的解决方案就是在部分服务调用的地方对请求进行Hash分组来限制一部分热点请求过多地占用服务器资源分组的策略就可以根据商品ID来进行Hash热点商品的请求始终会进入一个分组中这样就解决了前面的问题。
我看问的问题很多是说对秒杀的请求进行排队如何把结果通知给用户我并不是说在用户HTTP请求时采用排队的策略也就是把用户的所有秒杀请求都放到一个队列进行排队然后在队列里按照进入队列的顺序进行选择先到先得虽然这看起来还是一个挺合理的设计但是实际上并没有必要这么做
为什么因为我们服务端接受请求本身就是按照请求顺序处理的而且这个处理在Web层是实时同步的处理的结果也会立马就返回给用户。但是我前面也说了整个请求的处理涉及很多服务调用也涉及很多其他的系统也会有部分的处理需要排队所以可能有部分先到的请求由于后面的一些排队的服务拖慢导致最终整个请求处理完成的时间反而比较后面的请求慢的情况。
这种情况理论上的确存在,你可能会说这样可能会不公平,但是这的确没有办法,这种所谓的“不公平”,并不是由于人为设置的因素导致的。
你可能会问(如果你一定要问),采用请求队列的方式能不能做?我会说“能”,但是有两点问题:
一是体验会比较差,因为是异步的方式,在页面中搞个倒计时,处理的时间会长一点;
二是如果是根据入队列的时间来判断谁获得秒杀商品,那也太没有意思了,没有运气成分不也就没有惊喜了?
至于大家在纠结异步请求如何返回结果的问题,其实有多种方案。
一是页面中采用轮询的方式定时主动去服务端查询结果,例如每秒请求一次服务端看看有没有处理结果(现在很多支付页面都采用了这种策略),这种方式的缺点是服务端的请求数会增加不少。
二是采用主动push的方式这种就要求服务端和客户端保持连接了服务端处理完请求主动push给客户端这种方式的缺点是服务端的连接数会比较多。
还有一个问题,就是如果异步的请求失败了,怎么办?对秒杀来说,我觉得如果失败了直接丢弃就好了,最坏的结果就是这个人没有抢到而已。但是你非要纠结的话,就要做异步消息的持久化以及重试机制了,要保证异步请求的最终正确处理一般都要借助消息系统,即消息的最终可达,例如阿里的消息中间件是能承诺只要客户端消息发送成功,那么消息系统一定会保证消息最终被送到目的地,即消息不会丢。因为客户端只要成功发送一条消息,下游消费方就一定会消费这条消息,所以也就不存在消息发送失败的问题了。
2. 在“02 | 如何才能做好动静分离有哪些方案可选”一文中有介绍静态化的方案中关于Hash分组的问题。
大家可能通常理解Hash分组像Cache这种可能一个key对应的数据只存在于一个实例中这样做其实是为了保证缓存命中率因为所有请求都被路由到一个缓存实例中除了第一次没有命中外后面的都会命中。
但是这样也存在一个问题就是如果热点商品过于集中Cache就会成为瓶颈这时单个实例也支撑不了。像秒杀这个场景中单个商品对Cache的访问会超过20w次一般单Cache实例都扛不住这么大的请求量。所以需要采用一个分组中有多个实例缓存相同的数据冗余的办法来支撑更大的访问量。
你可能会问一个商品数据存储在多个Cache实例中如何保证数据一致性呢关于失效问题大家问得也比较多后面再回答。这个专栏中提的Hash分组都是基于Nginx+Varnish实现的Nginx把请求的URL中的商品ID进行Hash并路由到一个upstream中这个upstream挂载一个Varnish分组如下图所示。这样一个相同的商品就可以随机访问一个分组的任意一台Varnish机器了。
另外一个问题关于Hash分组大家关注比较多的是命中率的问题就是Cache机器越多命中率会越低。
这个其实很好理解Cache实例越多那么这些Cache缓存数据需要访问的次数也就越多。例如我有3个Redis实例需要3个Redis实例都缓存商品A那么至少需要访问3次才行而且是这3次访问刚好落到不同的Redis实例中。那么从第4次访问开始才会被命中如果仅仅是一个Redis实例那么第二次访问时其实就能命中了。所以理论上Cache实例多会影响命中率。
你可能还会问如果访问量足够大那么只是影响前几次命中率而已是的如果Cache一直不失效的话是这样的但是在实际的生产环境中Cache失效是很频繁发生的事情。很多情况下还没等到所有Cache实例填满该商品就已经失效了。所以我们要根据商品的重复访问量来合理地设置Cache分组。
3. 在“02 | 如何才能做好动静分离有哪些方案可选”和“04 | 流量削峰这事应该怎么做”两篇文章中关于Cache失效的问题。
首先咱们要有个共识有Cache的地方就必然存在失效问题。为啥要失效因为要保证数据的一致性。所以要用到Cache必然会问如何保证Cache和DB的数据一致性如果Cache有分组的话还要保证一个分组中多个实例之间数据的一致性就像保证MySQL的主从一致一样。
其实,失效有主动失效和被动失效两种方式。
被动失效主要处理如模板变更和一些对时效性不太敏感数据的失效采用设置一定时间长度如只缓存3秒钟这种自动失效的方式。当然你也要开发一个后台管理界面以便能够在紧急情况下手工失效某些Cache。
主动失效一般有Cache失效中心监控数据库表变化发送失效请求、系统发布也需要清空Cache数据等几种场景。其中失效中心承担了主要的失效功能这个失效中心的逻辑图如下
失效中心会监控关键数据表的变更有个中间件来解析MySQL的binglog然后发现有Insert、Update、Delete等操作时会把变更前的数据以及要变更的数据转成一个消息发送给订阅方通过这种方式来发送失效请求给Cache从而清除Cache数据。如果Cache数据放在CDN上那么也可以采用类似的方式来设计级联的失效结构采用主动发请求给Cache软件失效的方式如下图所示
这种失效有失效中心将失效请求发送给每个CDN节点上的Console机然后Console机来发送失效请求给每台Cache机器。