This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
<audio id="audio" title="17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/eb/d4829a560385e6258804e9a5e28cfceb.mp3"></audio>
你好,我是唐扬。
在课程一开始,我就带你了解了高并发系统设计的三个目标:性能、可用性和可扩展性,而在提升系统性能方面我们一直关注的是系统的查询性能,也用了很多的篇幅去讲解数据库的分布式改造,各类缓存的原理和使用技巧。**究其原因在于我们遇到的大部分场景都是读多写少,尤其是在一个系统的初级阶段。**
比如一个社区的系统初期一定是只有少量的种子用户在生产内容而大部分的用户都在“围观”别人在说什么。此时整体的流量比较小而写流量可能只占整体流量的百分之一那么即使整体的QPS到了10000次/秒写请求也只是到了每秒100次如果要对写请求做性能优化它的性价比确实不太高。
但随着业务发展,你可能会遇到一些存在**高并发写请求的场景,其中秒杀抢购就是最典型的场景。**假设你的商城策划了一期秒杀活动活动在第五天的00:00开始仅限前200名那么秒杀即将开始时后台会显示用户正在疯狂地刷新APP或者浏览器来保证自己能够尽量早的看到商品。
这时,你面对的依旧是读请求过高,**那么应对的措施有哪些呢?**
因为用户查询的是少量的商品数据属于查询的热点数据你可以采用缓存策略将请求尽量挡在上层的缓存中能被静态化的数据比如商城里的图片和视频数据尽量做到静态化这样就可以命中CDN节点缓存减少Web服务器的查询量和带宽负担。Web服务器比如Nginx可以直接访问分布式缓存节点从而避免请求到达Tomcat等业务服务器。
当然你可以加上一些限流的策略比如对短时间之内来自某一个用户、某一个IP或者某一台设备的重复请求做丢弃处理。
通过这几种方式,请求就可以尽量挡在数据库之外了。
稍微缓解了读请求之后00:00分秒杀活动准时开始用户瞬间向电商系统请求生成订单扣减库存用户的这些写操作都是不经过缓存直达数据库的。1秒钟之内有1万个数据库连接同时达到系统的数据库濒临崩溃寻找能够应对如此高并发的写请求方案迫在眉睫。这时你想到了消息队列。
## 我所理解的消息队列
你应该已经了解消息队列到底是什么了,所以我不再讲解它的概念,只聊聊自己对消息队列的看法。我在历年的工作经历中,一直把消息队列看作暂时存储数据的一个容器,认为它是一个平衡低速系统和高速系统处理任务时间差的工具,**我给你举个形象的例子。**
比如古代的臣子经常去朝见皇上陈述一些国家大事,等着皇上拍板做决策。但是大臣很多,如果同时去找皇上,你说一句我说一句,皇上肯定会崩溃。后来变成臣子到了午门之后要原地等着皇上将他们一个一个地召见进大殿商议国事,这样就可以缓解皇上处理事情的压力了。你可以把午门看作一个暂时容纳臣子的容器,也就是我们所说的消息队列。
其实你在一些组件中都会看到消息队列的影子:
- 在Java线程池中我们就会使用一个队列来暂时存储提交的任务等待有空闲的线程处理这些任务
- 操作系统中,中断的下半部分也会使用工作队列来实现延后执行;
- 我们在实现一个RPC框架时也会将从网络上接收到的请求写到队列里再启动若干个工作线程来处理。
- ……
总之,队列是在系统设计时一种常见的组件。
那么我们如何用消息队列解决秒杀场景下的问题呢?接下来,我们结合具体的例子来看看消息队列在秒杀场景下起到的作用。
## 削去秒杀场景下的峰值写流量
刚才提到,在秒杀场景下短时间之内数据库的写流量会很高,那么依照我们以前的思路应该对数据做分库分表。如果已经做了分库分表,那么就需要扩展更多的数据库来应对更高的写流量。但是无论是分库分表还是扩充更多的数据库都会比较复杂,原因是你需要将数据库中的数据做迁移,这个时间就要按天甚至按周来计算了。
而在秒杀场景下高并发的写请求并不是持续的,也不是经常发生的,而只有在秒杀活动开始后的几秒或者十几秒时间内才会存在。为了应对这十几秒的瞬间写高峰花费几天甚至几周的时间来扩容数据库,再在秒杀之后花费几天的时间来做缩容,**这无疑是得不偿失的。**
**所以我们的思路是:**将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。
我们会在后台启动若干个队列处理程序消费消息队列中的消息,再执行校验库存、下单等逻辑。因为只有有限个队列处理线程在执行,所以落入后端数据库上的并发请求是有限的。而请求是可以在消息队列中被短暂地堆积,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了。
<img src="https://static001.geekbang.org/resource/image/de/ad/de0a7a65a0bf51e1463d40d666a034ad.jpg" alt="">
这就是消息队列在秒杀系统中最主要的作用:**削峰填谷,**也就是说它可以削平短暂的流量高峰,虽说堆积会造成请求被短暂延迟处理,但是只要我们时刻监控消息队列中的堆积长度,在堆积量超过一定量时,增加队列处理机数量来提升消息的处理能力就好了,而且秒杀的用户对于短暂延迟知晓秒杀的结果也是有一定容忍度的。
**这里需要注意一下,**我所说的是“短暂”延迟,如果长时间没有给用户公示秒杀结果,那么用户可能就会怀疑你的秒杀活动有猫腻了。所以在使用消息队列应对流量峰值时,需要对队列处理的时间、前端写入流量的大小、数据库处理能力做好评估,然后根据不同的量级来决定部署多少台队列处理程序。
比如你的秒杀商品有1000件处理一次购买请求的时间是500ms那么总共就需要500s的时间。这时你部署10个队列处理程序那么秒杀请求的处理时间就是50s也就是说用户需要等待50s才可以看到秒杀的结果这是可以接受的。这时会并发10个请求到达数据库并不会对数据库造成很大的压力。
## 通过异步处理简化秒杀请求中的业务流程
其实在大量的写请求“攻击”你的电商系统的时候,消息队列除了发挥主要的削峰填谷的作用之外,还可以实现**异步处理**来简化秒杀请求中的业务流程,提升系统的性能。
你想在刚才提到的秒杀场景下我们在处理购买请求时需要500ms。这时你分析了一下整个的购买流程发现**这里面会有主要的业务逻辑,也会有次要的业务逻辑:**比如说,主要的流程是生成订单、扣减库存;次要的流程可能是我们在下单购买成功之后会给用户发放优惠券,会增加用户的积分。
假如发放优惠券的耗时是50ms增加用户积分的耗时也是50ms那么如果我们将发放优惠券、增加积分的操作放在另外一个队列处理机中执行那么整个流程就缩短到了400ms性能提升了20%处理这1000件商品的时间就变成了400s。如果我们还是希望能在50s之内看到秒杀结果的话只需要部署8个队列程序就好了。
经过将一些业务流程异步处理之后,我们的秒杀系统部署结构也会有所改变:
<img src="https://static001.geekbang.org/resource/image/3b/aa/3b19c4b5e93eeb32fd9665e330e6efaa.jpg" alt="">
## 解耦实现秒杀系统模块之间松耦合
除了异步处理和削峰填谷以外,消息队列在秒杀系统中起到的另一个作用是解耦合。
比如数据团队对你说,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度、购买者人群的特点以及用户对于秒杀互动的满意程度等等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢?
**一个思路是:**使用HTTP或者RPC的方式来同步地调用也就是数据团队这边提供一个接口我们实时将秒杀的数据推送给它**但是这样调用会有两个问题:**
- 整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响到秒杀系统的可用性。
- 当数据系统需要新的字段,就要变更接口的参数,那么秒杀系统也要随着一起变更。
这时,我们可以考虑使用消息队列降低业务系统和数据系统的直接耦合度。
秒杀系统产生一条购买数据后,我们可以先把全部数据发送给消息队列,然后数据团队再订阅这个消息队列的话题,这样它们就可以接收到数据,然后再做过滤和处理了。
秒杀系统在这样解耦合之后,数据系统的故障就不会影响到秒杀系统了,同时当数据系统需要新的字段时,只需要解析消息队列中的消息,拿到需要的数据就好了。
<img src="https://static001.geekbang.org/resource/image/6e/f6/6e096e287f2c418f663ab201f435a5f6.jpg" alt="">
**异步处理、解耦合和削峰填谷**是消息队列在秒杀系统设计中起到的主要作用,其中异步处理可以简化业务流程中的步骤,提升系统性能;削峰填谷可以削去到达秒杀系统的峰值流量,让业务逻辑的处理更加缓和;解耦合可以将秒杀系统和数据系统解耦开,这样两个系统的任何变更都不会影响到另一个系统,
如果你的系统想要提升写入性能实现系统的低耦合,想要抵挡高并发的写流量,那么你就可以考虑使用消息队列来完成。
## 课程小结
本节课,我结合自己的实际经验,主要带你了解了消息队列在高并发系统设计中起到的作用以及一些注意事项,你需要了解的重点如下:
- 削峰填谷是消息队列最主要的作用,但是会造成请求处理的延迟。
- 异步处理是提升系统性能的神器,但是你需要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,我们需要考虑如何确保消息一定到达。
- 解耦合可以提升你的整体系统的鲁棒性。
当然,你要知道,在使用消息队列之后虽然可以解决现有的问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发?**这些问题都是我们需要考虑的。**我会利用接下来的两节课针对最主要的两个问题来讲讲解决思路:一个是如何处理消息的丢失和重复,另一个是如何减少消息的延迟。
引入了消息队列的同时也会引入了新的问题,需要新的方案来解决,这就是系统设计的挑战,也是系统设计独有的魅力,而我们也会在这些挑战中不断提升技术能力和系统设计能力。
## 一课一思
在今天的课程中,我提到了消息队列在高并发系统设计中起到的作用。那么你在开发过程中会在什么样的场景下使用消息队列呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,159 @@
<audio id="audio" title="18 | 消息投递:如何保证消息仅仅被消费一次?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/57/0116397b59bdc960230f827ce059b757.mp3"></audio>
你好,我是唐扬。
通过上一节课,我们在电商系统中增加了消息队列对峰值写流量做削峰填谷,对次要的业务逻辑做异步处理,对不同的系统模块做解耦合。因为业务逻辑从同步代码中移除了,所以我们也要有相应的队列处理程序来处理消息、执行业务逻辑,**这时你的系统架构变成了下面的样子:**
<img src="https://static001.geekbang.org/resource/image/c9/a6/c9f44acbc4025b2ff1f0a4b9fd0941a6.jpg" alt="">
这是一个简化版的架构图实际上随着业务逻辑越来越复杂会引入更多的外部系统和服务来解决业务上的问题。比如我们会引入Elasticsearch来解决商品和店铺搜索的问题也会引入审核系统对售卖的商品、用户的评论做自动的和人工的审核你会越来越多地使用消息队列与外部系统解耦合以及提升系统性能。
比如你的电商系统需要上一个新的红包功能:用户在购买一定数量的商品之后,由你的系统给用户发一个现金的红包鼓励用户消费。由于发放红包的过程不应该在购买商品的主流程之内,所以你考虑使用消息队列来异步处理。**这时你发现了一个问题:**如果消息在投递的过程中发生丢失,那么用户就会因为没有得到红包而投诉。相反,如果消息在投递的过程中出现了重复,你的系统就会因为发送两个红包而损失。
那么我们如何保证产生的消息一定会被消费到并且只被消费一次呢?这个问题虽然听起来很浅显、很好理解,但是实际上却藏着很多玄机,本节课我就带你深入探讨。
## 消息为什么会丢失
如果要保证消息只被消费一次,首先就要保证消息不会丢失。那么消息从被写入到消息队列到被消费者消费完成,这个链路上会有哪些地方存在丢失消息的可能呢?其实主要存在三个场景:
- 消息从生产者写入到消息队列的过程;
- 消息在消息队列中的存储场景;
- 消息被消费者消费的过程。
<img src="https://static001.geekbang.org/resource/image/4c/bc/4c43b9c64c6125ad107fd91e4fcc27bc.jpg" alt="">
接下来我就针对每一个场景详细地剖析一下,这样你可以针对不同的场景选择合适的减少消息丢失的解决方案。
#### 1.在消息生产的过程中丢失消息
在这个环节中主要有两种情况。
首先,消息的生产者一般是我们的业务服务器,消息队列是独立部署在单独的服务器上的。两者之间的网络虽然是内网但是也会存在抖动的可能,而一旦发生抖动,消息就有可能因为网络的错误而丢失。
**针对这种情况,我建议你采用的方案是消息重传。**也就是当你发现发送超时后就将消息重新发一次但也不能无限制地重传消息。一般来说如果不是消息队列发生故障或者是到消息队列的网络断开了重试23次就可以了。
不过这种方案可能会造成消息的重复,从而在消费的时候重复消费同样的消息。比方说消息生产时由于消息队列处理慢或者网络的抖动,导致虽然最终写入消息队列成功但在生产端却超时了,生产者重传这条消息就会形成重复的消息,针对上面的例子,直观显示在你面前的就会是你收到了两个现金红包。
那么消息发送到了消息队列之后是否就万无一失了呢?当然不是,**在消息队列中消息仍然有丢失的风险。**
#### 2.在消息队列中丢失消息
拿Kafka举例消息在Kafka中是存储在本地磁盘上的而为了减少消息存储时对磁盘的随机I/O我们一般会将消息先写入到操作系统的Page Cache中然后再找合适的时机刷新到磁盘上。
比如Kafka可以配置当达到某一时间间隔或者累积一定的消息数量的时候再刷盘**也就是所说的异步刷盘。**
来看一个形象的比喻假如你经营一个图书馆读者每还一本书你都要去把图书归位不仅工作量大而且效率低下但是如果你可以选择每隔3小时或者图书达到一定数量的时候再把图书归位这样可以把同一类型的书一起归位节省了查找图书位置的时间可以提高效率。
不过如果发生机器掉电或者机器异常重启Page Cache中还没有来得及刷盘的消息就会丢失了。**那么怎么解决呢?**
你可能会把刷盘的间隔设置很短或者设置累积一条消息就就刷盘,但这样频繁刷盘会对性能有比较大的影响,而且从经验来看,出现机器宕机或者掉电的几率也不高,**所以我不建议你这样做。**
<img src="https://static001.geekbang.org/resource/image/6c/43/6c667c8c21baf27468c314105e522243.jpg" alt="">
如果你的电商系统对消息丢失的容忍度很低,**你可以考虑以集群方式部署Kafka服务通过部署多个副本备份数据保证消息尽量不丢失。**
那么它是怎么实现的呢?
Kafka集群中有一个Leader负责消息的写入和消费可以有多个Follower负责数据的备份。Follower中有一个特殊的集合叫做ISRin-sync replicas当Leader故障时新选举出来的Leader会从ISR中选择默认Leader的数据会异步地复制给Follower这样在Leader发生掉电或者宕机时Kafka会从Follower中消费消息减少消息丢失的可能。
由于默认消息是异步地从Leader复制到Follower的所以一旦Leader宕机那些还没有来得及复制到Follower的消息还是会丢失。为了解决这个问题Kafka为生产者提供一个选项叫做“acks”当这个选项被设置为“all”时生产者发送的每一条消息除了发给Leader外还会发给所有的ISR并且必须得到Leader和所有ISR的确认后才被认为发送成功。这样只有Leader和所有的ISR都挂了消息才会丢失。
<img src="https://static001.geekbang.org/resource/image/64/3f/648951000b3c7e969f8d04e42da6ac3f.jpg" alt="">
从上面这张图来看当设置“acks=all”时需要同步执行1、3、4三个步骤对于消息生产的性能来说也是有比较大的影响的所以你在实际应用中需要仔细地权衡考量。**我给你的建议是:**
1.如果你需要确保消息一条都不能丢失那么建议不要开启消息队列的同步刷盘而是用集群的方式来解决可以配置当所有ISR Follower都接收到消息才返回成功。
2.如果对消息的丢失有一定的容忍度那么建议不部署集群即使以集群方式部署也建议配置只发送给一个Follower就可以返回成功了。
3.我们的业务系统一般对于消息的丢失有一定的容忍度,比如说以上面的红包系统为例,如果红包消息丢失了,我们只要后续给没有发送红包的用户补发红包就好了。
#### 3.在消费的过程中存在消息丢失的可能
我还是以Kafka为例来说明。一个消费者消费消息的进度是记录在消息队列集群中的而消费的过程分为三步接收消息、处理消息、更新消费进度。
这里面接收消息和处理消息的过程都可能会发生异常或者失败,比如消息接收时网络发生抖动,导致消息并没有被正确地接收到;处理消息时可能发生一些业务的异常导致处理流程未执行完成,这时如果更新消费进度,这条失败的消息就永远不会被处理了,也可以认为是丢失了。
**所以,在这里你需要注意的是,**一定要等到消息接收和处理完成后才能更新消费进度,但是这也会造成消息重复的问题,比方说某一条消息在处理之后消费者恰好宕机了,那么因为没有更新消费进度,所以当这个消费者重启之后还会重复地消费这条消息。
## 如何保证消息只被消费一次
从上面的分析中你能发现,为了避免消息丢失我们需要付出两方面的代价:一方面是性能的损耗,一方面可能造成消息重复消费。
性能的损耗我们还可以接受,因为一般业务系统只有在写请求时才会有发送消息队列的操作,而一般系统的写请求的量级并不高,但是消息一旦被重复消费就会造成业务逻辑处理的错误。那么我们要如何避免消息的重复呢?
想要完全地避免消息重复的发生是很难做到的,因为网络的抖动、机器的宕机和处理的异常都是比较难以避免的,在工业上并没有成熟的方法,因此我们会把要求放宽,只要保证即使消费到了重复的消息,从消费的最终结果来看和只消费一次是等同的就好了,也就是保证在消息的生产和消费的过程是“幂等”的。
#### 1.什么是幂等
幂等是一个数学上的概念,它的含义是多次执行同一个操作和执行一次操作,最终得到的结果是相同的,说起来可能有些抽象,我给你举个例子:
比如男生和女生吵架女生抓住一个点不放传递“你不在乎我了吗生产消息的信息。那么当多次抱怨“你不在乎我了吗”的时候多次生产相同消息她不知道的是男生的耳朵消息处理会自动把N多次的信息屏蔽就像只听到一次一样这就是幂等性。
如果我们消费一条消息的时候要给现有的库存数量减1那么如果消费两条相同的消息就会给库存数量减2这就不是幂等的。而如果消费一条消息后处理逻辑是将库存的数量设置为0或者是如果当前库存数量是10时则减1这样在消费多条消息时所得到的结果就是相同的**这就是幂等的。**
**说白了,你可以这么理解“幂等”:**一件事儿无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等性。
#### 2.在生产、消费过程中增加消息幂等性的保证
消息在生产和消费的过程中都可能会产生重复,所以你要做的是在生产过程和消费过程中增加消息幂等性的保证,这样就可以认为从“最终结果上来看”消息实际上是只被消费了一次的。
**在消息生产过程中,**在Kafka0.11版本和Pulsar中都支持“producer idempotency”的特性翻译过来就是生产过程的幂等性这种特性保证消息虽然可能在生产端产生重复但是最终在消息队列存储时只会存储一份。
它的做法是给每一个生产者一个唯一的ID并且为生产的每一条消息赋予一个唯一ID消息队列的服务端会存储&lt;生产者ID最后一条消息ID&gt;的映射。当某一个生产者产生新的消息时消息队列服务端会比对消息ID是否与存储的最后一条ID一致如果一致就认为是重复的消息服务端会自动丢弃。
<img src="https://static001.geekbang.org/resource/image/aa/bd/aab832cee23258972c41e03493b8e0bd.jpg" alt="">
**而在消费端,<strong>幂等性的保证会稍微复杂一些,你可以从**通用层和业务层</strong>两个层面来考虑。
在通用层面你可以在消息被生产的时候使用发号器给它生成一个全局唯一的消息ID消息被处理之后把这个ID存储在数据库中在处理下一条消息之前先从数据库里面查询这个全局ID是否被消费过如果被消费过就放弃消费。
你可以看到无论是生产端的幂等性保证方式还是消费端通用的幂等性保证方式它们的共同特点都是为每一个消息生成一个唯一的ID然后在使用这个消息的时候先比对这个ID是否已经存在如果存在则认为消息已经被使用过。所以这种方式是一种标准的实现幂等的方式**你在项目之中可以拿来直接使用,**它在逻辑上的伪代码就像下面这样:
```
boolean isIDExisted = selectByID(ID); // 判断ID是否存在
if(isIDExisted) {
return; //存在则直接返回
} else {
process(message); //不存在,则处理消息
saveID(ID); //存储ID
}
```
**不过这样会有一个问题:**如果消息在处理之后,还没有来得及写入数据库,消费者宕机了重启之后发现数据库中并没有这条消息,还是会重复执行两次消费逻辑,这时你就需要引入事务机制,保证消息处理和写入数据库必须同时成功或者同时失败,但是这样消息处理的成本就更高了,所以如果对于消息重复没有特别严格的要求,可以直接使用这种通用的方案,而不考虑引入事务。
**在业务层面怎么处理呢?**这里有很多种处理方式,其中有一种是增加乐观锁的方式。比如你的消息处理程序需要给一个人的账号加钱,那么你可以通过乐观锁的方式来解决。
**具体的操作方式是这样的:**你给每个人的账号数据中增加一个版本号的字段在生产消息时先查询这个账户的版本号并且将版本号连同消息一起发送给消息队列。消费端在拿到消息和版本号后在执行更新账户金额SQL的时候带上版本号类似于执行
```
update user set amount = amount + 20, version=version+1 where userId=1 and version=1;
```
你看我们在更新数据时给数据加了乐观锁这样在消费第一条消息时version值为1SQL可以执行成功并且同时把version值改为了2在执行第二条相同的消息时由于version值不再是1所以这条SQL不能执行成功也就保证了消息的幂等性。
## 课程小结
本节课,我主要带你了解了在消息队列中消息可能会发生丢失的场景和应对方法,以及在消息重复的场景下,你要如何保证尽量不影响消息最终的处理结果。我想强调的重点是:
<li>
消息的丢失可以通过生产端的重试、消息队列配置集群模式以及消费端合理处理消费进度三种方式来解决;
</li>
<li>
为了解决消息的丢失通常会造成性能上的问题以及消息的重复问题;
</li>
<li>
通过保证消息处理的幂等性可以解决消息的重复问题。
</li>
虽然我讲了很多应对消息丢失的方法,但并不是说消息丢失一定不能被接受,毕竟你可以看到在允许消息丢失的情况下,消息队列的性能更好,方案实现的复杂度也最低。比如像是日志处理的场景,日志存在的意义在于排查系统的问题,而系统出现问题的几率不高,偶发的丢失几条日志是可以接受的。
**所以方案设计看场景,这是一切设计的原则,**你不能把所有的消息队列都配置成防止消息丢失的方式,也不能要求所有的业务处理逻辑都要支持幂等性,这样会给开发和运维带来额外的负担。
## 一课一思
我提到了消息队列在生产和消费端需要保证消息处理的幂等性,那么你还了解哪些保证消息处理幂等性的方法呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="19 | 消息队列:如何降低消息队列系统中消息的延迟?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/ae/6678374cc2fc4cd726ffa3e4f156d6ae.mp3"></audio>
你好,我是唐扬。
学完前面两节课之后,相信你对在垂直电商项目中如何使用消息队列应对秒杀时的峰值流量已经有所了解。当然了,你也应该知道要如何做才能保证消息不会丢失,尽量避免消息重复带来的影响。**那么我想让你思考一下:**除了这些内容,你在使用消息队列时还需要关注哪些点呢?
**先来看一个场景:**在你的垂直电商项目中,你会在用户下单支付之后向消息队列里面发送一条消息,队列处理程序消费了消息后会增加用户的积分或者给用户发送优惠券。用户在下单之后,等待几分钟或者十几分钟拿到积分和优惠券是可以接受的,但是一旦消息队列出现大量堆积,用户消费完成后几小时还拿到优惠券,那就会有用户投诉了。
这时你要关注的就是消息队列中消息的延迟了,这其实是消费性能的问题,那么你要如何提升消费性能保证更短的消息延迟呢?**在我看来,**首先需要掌握如何来监控消息的延迟,因为有了数据之后你才可以知道目前的延迟数据是否满足要求,也可以评估优化之后的效果。然后你要掌握使用消息队列的正确姿势以及关注消息队列本身是如何保证消息尽快被存储和投递的。
接下来,我们先来看看第一点:如何监控消息延迟。
## 如何监控消息延迟
在我看来,监控消息的延迟有两种方式:
- 使用消息队列提供的工具,通过监控消息的堆积来完成;
- 通过生成监控消息的方式来监控消息的延迟情况。
接下来,我带你实际了解一下。
假设在开篇的场景之下电商系统中的消息队列已经堆积了大量的消息那么你要想监控消息的堆积情况首先需要从原理上了解在消息队列中消费者的消费进度是多少因为这样才方便计算当前的消费延迟是多少。比如生产者向队列中一共生产了1000条消息某一个消费者消费进度是900条那么这个消费者的消费延迟就是100条消息。
**在Kafka中消费者的消费进度在不同的版本上是不同的。**
在Kafka0.9之前的版本中消费进度是存储在ZooKeeper中的消费者在消费消息的时候先要从ZooKeeper中获取最新的消费进度再从这个进度的基础上消费后面的消息。
在Kafka0.9版本之后消费进度被迁入到Kakfa的一个专门的topic叫“__consumer_offsets”里面。所以如果你了解kafka的原理可以依据不同的版本从不同的位置获取到这个消费进度的信息。
当然作为一个成熟的组件Kafka也提供了一些工具来获取这个消费进度的信息帮助你实现自己的监控这个工具主要有两个
**首先Kafka提供了工具叫做“kafka-consumer-groups.sh”**它在Kafka安装包的bin目录下
为了帮助你理解我简单地搭建了一个Kafka节点并且写入和消费了一些信息然后我来使用命令看看消息累积情况具体的命令如下
```
./bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group test-consumer-group
```
结果如下:
<img src="https://static001.geekbang.org/resource/image/40/4b/404811b07db8edb4c1bb9f1cfc0bc94b.jpg" alt="">
- 图中的前两列是队列的基本信息,包括话题名和分区名;
- 第三列是当前消费者的消费进度;
- 第四列是当前生产消息的总数;
- 第五列就是消费消息的堆积数(也就是第四列与第三列的差值)。
通过这个命令你可以很方便地了解消费者的消费情况。
**第二个工具是JMX。**
Kafka通过JMX暴露了消息堆积的数据我在本地启动了一个console consumer然后使用jconsole连接consumer就可以看到consumer的堆积数据了就是下图中红框里的数据。这些数据你可以写代码来获取这样也可以方便地输出到监控系统中**我比较推荐这种方式。**
<img src="https://static001.geekbang.org/resource/image/33/2c/3384d3fcb52f98815fac667e5b543e2c.jpg" alt="">
除了使用消息队列提供的工具以外,你还可以通过生成监控消息的方式来监控消息的延迟。**具体怎么做呢?**
你先定义一种特殊的消息,然后启动一个监控程序将这个消息定时地循环写入到消息队列中,消息的内容可以是生成消息的时间戳并且也会作为队列的消费者消费数据。业务处理程序消费到这个消息时直接丢弃掉,而监控程序在消费到这个消息时就可以和这个消息的生成时间做比较,如果时间差达到某一个阈值就可以向我们报警。
<img src="https://static001.geekbang.org/resource/image/34/7f/34820c0b27e66af37fda116a1a98347f.jpg" alt="">
这两种方式都可以监控消息的消费延迟情况,**而从我的经验出发,我比较推荐两种方式结合来使用。**比如在实际项目中我会优先在监控程序中获取JMX中的队列堆积数据做到dashboard报表中同时也会启动探测进程确认消息的延迟情况是怎样的。
在我看来,消息的堆积是对于消息队列的基础监控,这是你无论如何都要做的。但是了解了消息的堆积情况并不能很直观地了解消息消费的延迟,你也只能利用经验来确定堆积的消息量到了多少才会影响到用户的体验;而第二种方式对于消费延迟的监控则更加直观,而且从时间的维度来做监控也比较容易确定报警阈值。
了解了消息延迟的监控方式之后,我们再来看看如何提升消息的写入和消费性能,这样才会让异步的消息得到尽快的处理。
## 减少消息延迟的正确姿势
想要减少消息的处理延迟,我们需要在**消费端和消息队列**两个层面来完成。
在消费端的目标是提升消费者的消息处理能力,你能做的是:
- 优化消费代码提升性能;
- 增加消费者的数量(这个方式比较简单)。
不过第二种方式会受限于消息队列的实现。如果消息队列使用的是Kafka就无法通过增加消费者数量的方式来提升消息处理能力。
因为在Kafka中一个Topic话题可以配置多个Partition分区数据会被平均或者按照生产者指定的方式写入到多个分区中那么在消费的时候Kafka约定一个分区只能被一个消费者消费为什么要这么设计呢在我看来如果有多个consumer消费者可以消费一个分区的数据那么在操作这个消费进度的时候就需要加锁可能会对性能有一定的影响。
所以说,话题的分区数量决定了消费的并行度,增加多余的消费者也是没有用处的,你可以通过增加分区来提高消费者的处理能力。
<img src="https://static001.geekbang.org/resource/image/cd/39/cdd960f49f982f8b96ab466d7e4b2739.jpg" alt="">
那么,如何在不增加分区的前提下提升消费能力呢?
虽然不能增加consumer但你可以在一个consumer中提升处理消息的并行度所以可以考虑使用多线程的方式来增加处理能力你可以预先创建一个或者多个线程池在接收到消息之后把消息丢到线程池中来异步地处理这样原本串行的消费消息的流程就变成了并行的消费可以提高消息消费的吞吐量在并行处理的前提下我们就可以在一次和消息队列的交互中多拉取几条数据然后分配给多个线程来处理。
<img src="https://static001.geekbang.org/resource/image/2c/79/2c0eefd526eed3a1fe4df89f068daf79.jpg" alt="">
另外,在消费队列中数据的时候还需要注意消费线程空转的问题。
**我是在测试自己写的一个消息中间件的时候发现的。**当时我发现运行消费客户端的进程会偶发地出现CPU跑满的情况于是打印了JVM线程堆栈找到了那个跑满CPU的线程。这个时候才发现原来是消息队列中有一段时间没有新的消息于是消费客户端拉取不到新的消息就会不间断地轮询拉取消息这个线程就把CPU跑满了。
所以你在写消费客户端的时候要考虑这种场景拉取不到消息可以等待一段时间再来拉取等待的时间不宜过长否则会增加消息的延迟。我一般建议固定的10ms~100ms也可以按照一定步长递增比如第一次拉取不到消息等待10ms第二次20ms最长可以到100ms直到拉取到消息再回到10ms。
说完了消费端的做法之后,**再来说说消息队列本身在读取性能优化方面做了哪些事情。**
我曾经也做过一个消息中间件,在最初设计中间件的时候我主要从两方面考虑读取性能问题:
- 消息的存储;
- 零拷贝技术。
**针对第一点,**我最初在设计的时候为了实现简单使用了普通的数据库来存储消息但是受限于数据库的性能瓶颈读取QPS只能到2000后面我重构了存储模块使用本地磁盘作为存储介质。Page Cache的存在就可以提升消息的读取速度即使要读取磁盘中的数据由于消息的读取是顺序的并且不需要跨网络读取数据所以读取消息的QPS提升了一个数量级。
**另外一个优化点是零拷贝技术,**说是零拷贝,其实我们不可能消灭数据的拷贝,只是尽量减少拷贝的次数。在读取消息队列的数据的时候,其实就是把磁盘中的数据通过网络发送给消费客户端,在实现上会有四次数据拷贝的步骤:
1.数据从磁盘拷贝到内核缓冲区;<br>
2.系统调用将内核缓存区的数据拷贝到用户缓冲区;<br>
3.用户缓冲区的数据被写入到Socket缓冲区中<br>
4.操作系统再将Socket缓冲区的数据拷贝到网卡的缓冲区中。
<img src="https://static001.geekbang.org/resource/image/52/8f/52c74ecac57e7a437442860029476d8f.jpg" alt="">
操作系统提供了Sendfile函数可以减少数据被拷贝的次数。使用了Sendfile之后在内核缓冲区的数据不会被拷贝到用户缓冲区而是直接被拷贝到Socket缓冲区节省了一次拷贝的过程提升了消息发送的性能。高级语言中对于Sendfile函数有封装比如说在Java里面的java.nio.channels.FileChannel类就提供了transferTo方法提供了Sendfile的功能。
<img src="https://static001.geekbang.org/resource/image/e3/ed/e38d36c7f077c6ce5b0b276efb8d4eed.jpg" alt="">
## 课程小结
本节课我带你了解了如何提升消息队列的性能来降低消息消费的延迟,这里我想让你明确的重点是:
- 我们可以使用消息队列提供的工具,或者通过发送监控消息的方式来监控消息的延迟情况;
- 横向扩展消费者是提升消费处理能力的重要方式;
- 选择高性能的数据存储方式配合零拷贝技术,可以提升消息的消费性能。
其实队列是一种常用的组件,只要涉及到队列,任务的堆积就是一个不可忽视的问题,**我遇到过的很多故障都是源于此。**
比如前一段时间处理的一个故障前期只是因为数据库性能衰减有少量的慢请求结果这些慢请求占满了Tomcat线程池导致整体服务的不可用。如果我们能对Tomcat线程池的任务堆积情况有实时的监控或者说对线程池有一些保护策略比方说线程全部使用之后丢弃请求也许就会避免故障的发生。在此我希望你在实际的工作中能够引以为戒只要有队列就要监控它的堆积情况把问题消灭在萌芽之中。
## 一课一思
在实际的项目中,你可能对于消息队列的使用已经很熟练了,那么结合今天的内容,你可以和我分享一下在研发过程中,你在降低消息延迟方面做过哪些事情呢?欢迎在留言区和我一起讨论,或者将你的实战经验分享给更多的人。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,3 @@
<audio id="audio" title="20 | 面试现场第二期:当问到项目经历时,面试官究竟想要了解什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0f/7b/0f7bdffc39525dc2519926e26d75447b.mp3"></audio>
<img src="https://static001.geekbang.org/resource/image/44/37/44156010052717821b4bf726a8c20d37.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/ad/18/ade92a3267329df2de7a2807c73bdc18.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/bc/57/bc23df7cb8cf956aecfdae41c4740457.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/ed/42/ed7bfcbb9ec098daacccfde3174cb342.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/69/47/6964a5e0ce04430ff3993b39426a8847.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/1a/09/1a115d21b519e783514b2ca27dffb909.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/74/29/7429da91a4e32e50c0623563cc968f29.jpg" alt="unpreview">

View File

@@ -0,0 +1,3 @@
<audio id="audio" title="期中测试 | 10道高并发系统设计题目自测" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ea/e8/ea68872251d3756bf4e87c0e00d4d0e8.mp3"></audio>
<img src="https://static001.geekbang.org/resource/image/0d/c9/0de41d53c767f04149c365014e53bbc9.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/88/da/88786b56bbd1f5d19bd924e5c3b982da.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/61/96/618f33da857e9180302feca801826496.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/f9/af/f9a1d75d3d1df97ee1ddfd6cc313a6af.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/09/d2/096ceab6ed7e91d6fd8b465a6829d4d2.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/50/a9/507bce509859f34f3b70a4e07844dea9.jpg" alt="unpreview">[<img src="https://static001.geekbang.org/resource/image/7f/13/7f419c3c4fbf9ea57f9e76926e6c6813.jpg" alt="unpreview">](https://time.geekbang.org/column/article/162811)

View File

@@ -0,0 +1,84 @@
你好我是Longslee很高兴与大家一起学习《高并发系统设计40问》。
我从事软件相关的职业已经有九年时间了,之前在一家税务行业类公司工作,目前在一家电信行业相关的公司从事开发和运维工作。
我并不是“极客时间”的老用户,因为接触“极客时间”只有短短几个月,一开始只抱着试试看的心态尝试着订阅了几门课程,后来便自然而然地将它当作工作之余获取信息的必需品。
要说跟这门课结缘还是在今年10月份那时我偶然打开“极客时间”看到了《高并发系统设计40问》的课程被开篇词的题目**“你为什么要学习高并发系统设计”**吸引了。开篇词中提到:
>
公司业务流量平稳,并不表示不会遇到一些高并发的需求场景;为了避免遇到问题时手忙脚乱,你有必要提前储备足够多的高并发知识,从而具备随时应对可能出现的高并发需求场景的能力……
这些信息着实戳中了我。
回想起来自己所处的行业是非常传统的IT行业几乎与“互联网”不着边所以我平时特别难接触一线的技术栈。然而虽然行业传统但并不妨碍日常工作中高并发的出现**比如偶尔出现的线上促销活动。**
单纯从我自己的角度出发,除了因为开篇词戳中之外,选择这个课程还在于自己想拓宽视野、激发潜能,另一方面当真的遇到“高并发”时,不至于望洋兴叹,脑海一片空白。
**在课程设计上,**每一节课的标题都是以问号结束,这种看似寻常的设计很容易让我在学习时联想到自己的实际工作,从而先问问自己:我们为什么要架构分层?如何避免消息重复?等自己有了一些答案后,再正式进入学习对概念性的知识查漏补缺。
我个人认为,这也算是这门课程的一个小的特色。唐扬老师抛出问题,并用自己的经验进行回答,让这篇文章有了一个很好的闭环。
目前来说,我所在的行业和项目为了应对日益复杂的业务场景和日渐频繁的促销活动,也在慢慢地转变,更多地引入互联网行业知识,产品也更加与时俱进。
作为这个行业的一员,在日常工作中我自然也遇到了一些难题,碰到了一些瓶颈,但是在寻找解决方式的时候往往局限在自己擅长的技术体系和历史的过往经验上。而在学习了这门课之后,我拓宽了眼界,会不自主地思考“是不是可以用今天学到的方式解决某些问题?”“当初选用的中间件和使用方式合不合理?”等等。
而且就像我提到的,自己所处的行业在不断改变,其实就目前的趋势来看,很早就存在的信息化产品和目前主流的互联网产品渐渐难以界定了。就比如高校的教务系统,听起来好像跟我们接触的各类网站大不一样,但是在开学的时候,又有多少选课系统能扛住同学们瞬间的巨大流量呢?
[《17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?》](https://time.geekbang.org/column/article/156904)讲的就是各厂处理可预见且短时间内大流量的“套路”而我认为这个“套路”也可以应用到大学的选课系统。因为教务系统在通常情况下都是很闲的如果整体升级来提高QPS性价比太低所以只要保证在选课时服务的稳定性就好了。这里可以引入消息队列来缓解数据库的压力再通过异步拆分提高核心业务的处理速度。
**其实还有好多节课都给我留下了深刻的印象,**比如第2讲、第10讲、第13讲等等。
单看[《02 | 架构分层:我们为什么一定要这么做?》](https://time.geekbang.org/column/article/138331)这个题目,我一开始会觉得“老生常谈”,软件分层在实际项目中运用得太多太多了,老师为什么单独拿出来一讲介绍呢?然而当我看到“如果业务逻辑很简单的话,可不可以从表示层直接到数据访问层,甚至直接读数据库呢?”这句话时,**联系到了自己的实际业务:**
>
我所参与的一个工程,确实因为业务逻辑基本等同数据库逻辑,所以从表示层直接与数据访问层交互了。但是如果数据库或者数据访问层发生改动,那将要修改表示层的多个地方,万一漏掉了需要调整的地方,连问题都不好查了,并且如果以后再无意地引入逻辑层,修改的层次也将变多。
对我而言,这篇文章能够有触动我的地方,引发我的思考,所以在接下来的项目中,我坚持选用分层架构。
而[《10 | 发号器如何保证分库分表后ID的全局唯一性》](https://time.geekbang.org/column/article/146454)**给我的项目提供了思路:**我的需求不是保证分库分表后主键的唯一性但由于需要给各个客户端分配唯一ID用客户端策略难免重复所以在读到
>
一种是嵌入到业务代码里,也就是分布在业务服务器中。这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些,但是就需要更多的机器 ID 位数来支持更多的业务服务器。另外由于业务服务器的数量很多,我们很难保证机器 ID 的唯一性,所以就需要引入 ZooKeeper等分布式一致性组件来保证每次机器重启时都能获得唯一的机器 ID……
我采取了类似发号器的概念并且摒弃了之前UUID似的算法。采用发号器分发的ID后在数据库排序性能有所提升业务含义也更强了。
除此之外,在学习[《13 | 缓存的使用姿势(一):如何选择缓存的读写策略?》](https://time.geekbang.org/column/article/150881)之前,我的项目中没有过多的考虑数据库与缓存的一致性。比如我在写入数据时,选择了先写数据库再写缓存,考虑到写数据库失败后事务回滚,缓存也不会被写入;如果缓存写入失败,再设计重试机制。
看起来好像蛮OK的样子但是因为没有考虑到在多线程更新的情况下确实会造成双方的不一致**所造成的后果是:有时候从前端查询到的结果与真实数据不符。**后来根据唐扬老师提到的Cache Aside旁路缓存策略我顿然醒悟然后将这一策略用于该工程中效果不错。这节课我从唐扬老师的亲身经历中学到了不少的经验直接用到了自己的项目中。
真的很感谢唐扬老师,也很开心能够遇到这门课程,在这里,想由衷地表达自己的感谢之情。
**那么我是怎么学习这门课程的呢?**在这里,我想分享几点:
- 知行合一
学完课程后,除了积极思考“能否用”“怎么用”“何时用”这些问题外,一定要趁热打铁,要么继续深入话题,翻阅其他资料,巩固下来;要么敲敲代码实现一遍,转化为自己的技能;如果时间充裕,甚至可以立马着手改进项目。
- 留言区=挖宝区
每节课结束,我都会在留言板留下疑问或者分享体验,我喜欢问问题其实是跟自己在大学时参加的一场宣讲会有关。当时,来招聘的负责人是一位美国留学回来的台湾工程师,他介绍完后问大家有没有疑问,并没有人回答。
后来他讲了一个经历,让我感慨良多。他说当他刚去美国大学的时候,教授讲完课就要答疑,一个白人学生提了一个在中国学生看来十分简单且幼稚的问题,以后的每节课这位白人同学都要提问,渐渐地,提的问题他都听不懂了!再后来,教授也不懂了。
所以,我会不断地发问不懂就问,把留言区当作挖宝区,看大家的留言进行思考。比如@李冲同学的几个跟帖,就解答了我对布隆过滤器的误解,并且还知道了另一种布谷鸟过滤器。
- 勤做笔记
有的时候我当时理解得比较透彻可过了两三天之后就有些模糊了所以后来我根据自己的理解写成思维导图形式随时随地都可以翻阅。另外在实现这些方案的代码后面也可以写下相应的注释Review的时候还可以温故知新。
**在最后,我也想分享一下自己为什么用专栏这种形式来学习。**善用搜索引擎的同学们都有体会,搜索出来的知识分布在各处,雷同的不少,有经验的介绍甚少,我没办法在有限的时间内,将搜索到的知识形成体系。
当然了,要想系统地学习可以借助书籍。**但是对我来说,**书籍类学习周期长,章节之间的关联性也不大,容易学了这里忘了那里。书籍多是讲一个专业点,对于跨专业的知识经常一笔带过,而专栏是有作者自己的理解在里边,前后之间有贯通,学习起来轻松愉悦。
**就拿一致性Hash这个知识点来说**我从网上看了不少关于一致性Hash的文章但没有看到应用更别谈应用中的缺陷有的描述甚至让我误认为节点变化后数据也会跟着迁移。唐扬老师的[《14 | 缓存的使用姿势(二):缓存如何做到高可用?》](https://time.geekbang.org/column/article/151949),倒是给了我网络上看不到的盲区,通过在留言区与老师交流后,颇有一种豁然开朗的收获感。
当然了,这些只是我个人的感受,见仁见智,**你或许有自己的学习方法,也或许大家的起点不同,**在这里,我只想把自己的真实感受分享出来,也十分感谢大家倾听我的故事。
总的来说,想要提升自己,并没有捷径,只有一步一步地踏实前行,从踩过的坑中,努力地爬出来。
对我来说唐扬老师的《高并发系统设计40问》犹如及时雨弥补了我高并发相关知识上的缺陷我相信认真学完课程之后自己的视野一定有所开拓职业生涯也会进入新的篇章。