Update 消息队列.md

补充延迟消息、死信队列和 Kafka。
This commit is contained in:
CyC2018
2026-10-26 22:04:09 +08:00
parent 303ef42716
commit 6abd9bc0f8
5 changed files with 329 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

+133 -1
View File
@@ -14,6 +14,14 @@
* [接收端的可靠性](#接收端的可靠性)
* [四、顺序性](#四顺序性)
* [五、消息堆积](#五消息堆积)
* [六、延迟消息与死信队列](#六延迟消息与死信队列)
* [延迟消息](#延迟消息)
* [死信队列](#死信队列)
* [七、Kafka](#七kafka)
* [分区与消费者组](#分区与消费者组)
* [存储](#存储)
* [为什么吞吐量高](#为什么吞吐量高)
* [副本与可靠性](#副本与可靠性)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -104,8 +112,132 @@ Kafka、RocketMQ 等通过消费者组把两种模型结合起来:一条消息
解决方法是增加消费者数量来提高消费能力,但是消费者数量超过队列(分区)数量之后就不再有效,需要同时增加队列数量。
## 六、延迟消息与死信队列
### 延迟消息
延迟消息指的是发送后不立即投递,等到指定时间才能被消费的消息。典型的场景是订单 30 分钟未支付自动取消:下单时发送一条 30 分钟后投递的消息,到时消费者检查订单,仍未支付就取消。相比定时任务扫描全表,延迟消息只处理到期的订单。
各消息队列的支持情况:
- RocketMQ 4.x 只支持 18 个固定的延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,消息先存入内部的定时主题,到期后再投递到目标主题;
- RocketMQ 5.x 支持指定任意的投递时间,默认最长 24 小时,精度为秒级;
- RabbitMQ 没有原生的延迟消息,常用的做法是给消息设置过期时间(TTL),过期后转入死信交换机,由死信队列的消费者处理,或者安装延迟消息插件;
- Kafka 不支持延迟消息,需要自己实现,例如按延迟时长分成几个主题,消费者读到未到期的消息就暂停等待。
不使用消息队列时,也可以用 Redis 的有序集合实现:分值为到期时间,后台线程定期取出分值不大于当前时间的成员。多个实例同时轮询时,用 ZREM 的返回值判断由谁处理,返回 1 的实例才处理:
```text
> zadd delay:orders 1792997618 order:1024 1792999423 order:1025
(integer) 2
> zrangebyscore delay:orders 0 1792997623 limit 0 10
1) "order:1024"
> zrem delay:orders order:1024
(integer) 1
> zrem delay:orders order:1024
(integer) 0
```
JDK 自带的 DelayQueue 也能实现延迟,但数据只在单个进程的内存中,服务重启就会丢失,多个实例之间也无法共享。
### 死信队列
消息多次消费失败后,不再重试,而是转入死信队列,避免一条有问题的消息反复重试、阻塞其它消息,之后由人工或专门的程序排查处理。
- RocketMQ 并发消费时默认重试 16 次,仍然失败就进入名为 %DLQ% 加消费者组名的死信主题;
- RabbitMQ 中消息被消费者拒绝且不重新入队、消息的 TTL 过期、队列长度超过上限时,消息会被转发到死信交换机。
事务消息用于保证本地事务和发送消息同时成功或失败,见 [分布式事务中的事务消息](分布式.md#本地消息表)。
## 七、Kafka
### 分区与消费者组
Kafka 中的消息按主题(Topic)分类,每个主题分成多个分区(Partition)。分区是一个只能在末尾追加的日志,每条消息在分区内有一个递增的偏移量(Offset),消费者记录自己读到了哪个偏移量。
生产者发送消息时可以带一个 key,默认按 key 的哈希值对分区数取模选择分区,相同 key 的消息一定进入同一个分区,所以同一个订单的消息是有序的,不同分区之间则没有顺序。没有 key 时,生产者先把一批消息发往同一个分区,再换下一个分区,以便凑成批次。
<div align="center"> <img src="pics/kafka-topic.png" width="760px"/> </div><br>
消费者以消费者组为单位订阅主题:组内每个分区只分配给一个消费者,一个消费者可以负责多个分区。所以分区数决定了一个组最多有多少个消费者同时工作,多出来的消费者只能空闲。不同的组各自独立地消费全部消息,例如计费服务和物流服务各用一个组,都能收到每一条订单消息。
消费者加入、退出或者分区数变化时,组内会重新分配分区,称为重平衡(Rebalance)。重平衡期间被重新分配的分区暂停消费,还可能造成重复消费。一个常见的原因是处理一批消息的时间超过了 max.poll.interval.ms(默认 5 分钟),消费者被认为已经失效,它的分区被分给其他消费者。
### 存储
每个分区在磁盘上是一个目录,日志按大小切分成多个段(Segment),文件名是段中第一条消息的偏移量。本机把段的大小设为 1 MB(默认 1 GB),向一个只有一个分区的主题写入 3 万条 100 字节的消息后,目录中的文件和大小如下(省略了 .timeindex 等文件):
```text
00000000000000000000.log 1041728
00000000000000000000.index 504
00000000000000009472.log 1041728
00000000000000009472.index 504
00000000000000018944.log 1041728
00000000000000018944.index 504
00000000000000028416.log 174207
00000000000000028416.index 10485760
```
.index 是稀疏索引,每写入一段数据(index.interval.bytes,默认 4 KB)记录一条“偏移量 → 在 .log 文件中的位置”。查找偏移量 20000 的消息:
1. 按文件名二分查找,20000 落在 00000000000000018944 这个段中;
2. 在这个段的索引中二分查找,不大于 20000 的最近一条索引是 19979 → 97662;
3. 从 .log 文件的位置 97662 开始顺序读,下一个批次(偏移量 19980~20127)中就有 20000。
正在写入的段的索引文件预先分配了 10 MB,并用 mmap 映射到内存,段写满后才截断为实际大小。用 kafka-dump-log.sh 查看 .log 文件,可以看到消息是按批次存放的,这里每个批次有 148 条消息、16277 字节,接近生产者批次大小的默认值 16 KB。
旧数据按保留时间(默认 7 天)或总大小清理,每次删除一整个段,不需要修改文件内容;也可以配置为日志压缩,每个 key 只保留最新的一条消息。
### 为什么吞吐量高
顺序写和页缓存:消息只追加到日志的末尾,是顺序写,比随机写快得多。写入时只写到操作系统的页缓存,由操作系统决定何时刷盘,消费者读取刚写入不久的数据时也通常直接命中页缓存。可靠性不依靠每条消息都刷盘,而是依靠多个副本。
批量:生产者把发往同一个分区的消息攒成批次再发送,批次在网络传输、写入日志和副本复制时都作为一个整体处理,压缩也以批次为单位。在本机 3 个节点的集群上用 kafka-producer-perf-test.sh 发送 2 万条 100 字节的消息(acks=all),各运行 3 次:默认配置每秒约 5.7 万~6.8 万条,平均延迟 21~54 ms;设置 batch.size=0 关闭批量后,每秒只有约 6500~6800 条,发送的请求大量排队,平均延迟约 1.4 秒。
零拷贝:broker 把日志发给消费者时使用 sendfile 系统调用,数据不需要先读到用户空间再写回内核:
<div align="center"> <img src="pics/zero-copy.png" width="760px"/> </div><br>
分区并行:一个主题的多个分区分布在不同的 broker 上,读写由多台机器共同承担。
### 副本与可靠性
每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 Raft 协议实现。
本机创建 3 个分区、3 个副本的主题,然后用 kill -9 停掉 broker 3:
```text
停止前
Partition: 0 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
Partition: 1 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
Partition: 2 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
停止后
Partition: 0 Leader: 2 Replicas: 2,3,1 Isr: 2,1
Partition: 1 Leader: 1 Replicas: 3,1,2 Isr: 1,2
Partition: 2 Leader: 1 Replicas: 1,2,3 Isr: 1,2
```
分区 1 的 Leader 从 broker 3 切换到了 ISR 中的 broker 1,其它分区只是 ISR 中少了 3。broker 3 重启并追上数据后,又回到了 ISR 中。
生产者的 acks 参数决定写入多少个副本才算成功:
- acks=0:不等待确认,可能丢消息;
- acks=1:Leader 写入就返回,Leader 宕机而 Follower 还没同步时会丢消息;
- acks=all:ISR 中的副本都写入才返回,Kafka 3.0 起是默认值。
acks=all 要配合 min.insync.replicas 使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException,改用 acks=1 则仍然能写入。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
生产者没有收到确认就会重试,这可能让同一条消息写入两次。Kafka 3.0 起默认开启幂等生产者:broker 给每个生产者分配一个 ID,生产者给发往每个分区的消息编上序号,broker 发现序号重复就丢弃。前面查看 .log 文件时,每个批次都带有 producerId: 1001 和 baseSequence 字段。幂等只保证单个分区内不重复;要让写入多个分区的消息同时成功或失败,需要使用事务,消费者设置 isolation.level=read_committed 后只读取已提交事务中的消息。
消费者处理完消息后提交偏移量,下次从提交的位置继续读取。默认每 5 秒自动提交一次,已经处理但还没提交时崩溃,重启后就会重复消费。本机实验中,消费者读到 3 条扣款消息后被 kill -9,这时组的偏移量仍然为空;它在 session.timeout.ms(默认 45 秒)之后被移出消费者组,重新启动的消费者又收到了这 3 条消息。因此接收端必须做到幂等,方法见 [接收端的可靠性](#接收端的可靠性)。反过来,如果先提交偏移量再处理,处理失败的消息就丢失了。
## 参考资料
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105)
- [消息队列中点对点与发布订阅区别](https://web.archive.org/web/20250522024511/https://blog.csdn.net/lizhitao/article/details/47723105)
- [定时/延时消息 | RocketMQ](https://rocketmq.apache.org/zh/docs/featureBehavior/02delaymessage/)
- [Dead Letter Exchanges | RabbitMQ](https://www.rabbitmq.com/docs/dlx)
- [Apache Kafka Documentation: Design](https://kafka.apache.org/43/design/design/)
- [Apache Kafka Documentation: Configuration](https://kafka.apache.org/43/configuration/)
- [Apache Kafka Documentation: Upgrading](https://kafka.apache.org/43/getting-started/upgrade/)
+111
View File
@@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="370" viewBox="0 0 760 370">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">Kafka:按 key 选分区,组内每个分区只交给一个消费者</text>
<rect x="24" y="70" width="150" height="128" rx="5" fill="#FFFFFF" stroke="#BBBBBB" stroke-width="1.2"/>
<line x1="24" y1="84" x2="174" y2="84" stroke="#BBBBBB" stroke-width="1"/>
<circle cx="33" cy="77" r="2.2" fill="#BBBBBB"/>
<circle cx="41" cy="77" r="2.2" fill="#BBBBBB"/>
<circle cx="49" cy="77" r="2.2" fill="#BBBBBB"/>
<text x="33" y="100" class="sub mono">key value</text>
<text x="33" y="116" class="sub mono">1024 下单</text>
<text x="33" y="132" class="sub mono">1025 下单</text>
<text x="33" y="148" class="sub mono">1024 支付</text>
<text x="33" y="164" class="sub mono">1026 下单</text>
<text x="33" y="180" class="sub mono">1024 发货</text>
<text x="33" y="196" class="sub mono">1025 支付</text>
<text x="99" y="216" class="c sub b">生产者按顺序发送</text>
<text x="99" y="232" class="c sub mono">hash(key) % 3 选分区</text>
<text x="290" y="98" class="e sub b">分区 0</text>
<rect x="300" y="74" width="54" height="40" rx="3" fill="#C6E4FB" stroke="#7FB2D6" stroke-width="1"/>
<text x="327" y="98" class="c sub mono" style="fill:#444"></text>
<text x="327" y="91" class="c sub mono">1026</text>
<text x="327" y="107" class="c sub">下单</text>
<text x="327" y="70" class="c sub mono" style="font-size:10px">0</text>
<rect x="354" y="74" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="408" y="74" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="462" y="74" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="516" y="74" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<line x1="178" y1="130" x2="246" y2="94" class="arrow "/>
<text x="290" y="172" class="e sub b">分区 1</text>
<rect x="300" y="148" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="354" y="148" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="408" y="148" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="462" y="148" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<rect x="516" y="148" width="54" height="40" rx="3" fill="none" stroke="#D6D6D6" stroke-dasharray="3 2"/>
<line x1="178" y1="130" x2="246" y2="168" class="arrow "/>
<text x="290" y="246" class="e sub b">分区 2</text>
<rect x="300" y="222" width="54" height="40" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="327" y="246" class="c sub mono" style="fill:#444"></text>
<text x="327" y="239" class="c sub mono">1024</text>
<text x="327" y="255" class="c sub">下单</text>
<text x="327" y="218" class="c sub mono" style="font-size:10px">0</text>
<rect x="354" y="222" width="54" height="40" rx="3" fill="#C6E4FB" stroke="#7FB2D6" stroke-width="1"/>
<text x="381" y="246" class="c sub mono" style="fill:#444"></text>
<text x="381" y="239" class="c sub mono">1025</text>
<text x="381" y="255" class="c sub">下单</text>
<text x="381" y="218" class="c sub mono" style="font-size:10px">1</text>
<rect x="408" y="222" width="54" height="40" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="435" y="246" class="c sub mono" style="fill:#444"></text>
<text x="435" y="239" class="c sub mono">1024</text>
<text x="435" y="255" class="c sub">支付</text>
<text x="435" y="218" class="c sub mono" style="font-size:10px">2</text>
<rect x="462" y="222" width="54" height="40" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="489" y="246" class="c sub mono" style="fill:#444"></text>
<text x="489" y="239" class="c sub mono">1024</text>
<text x="489" y="255" class="c sub">发货</text>
<text x="489" y="218" class="c sub mono" style="font-size:10px">3</text>
<rect x="516" y="222" width="54" height="40" rx="3" fill="#C6E4FB" stroke="#7FB2D6" stroke-width="1"/>
<text x="543" y="246" class="c sub mono" style="fill:#444"></text>
<text x="543" y="239" class="c sub mono">1025</text>
<text x="543" y="255" class="c sub">支付</text>
<text x="543" y="218" class="c sub mono" style="font-size:10px">4</text>
<line x1="178" y1="130" x2="246" y2="242" class="arrow "/>
<text x="300" y="296" class="sub">订单 1024 的三条消息都在分区 2,顺序不变</text>
<text x="290" y="70" class="e sub">偏移量</text>
<rect x="590" y="56" width="150" height="268" rx="8" class="frame"/>
<text x="600" y="74" class="b">消费者组 billing</text>
<rect x="612" y="84" width="108" height="34" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="666" y="105.5" class="c sub b">consumer-1</text>
<line x1="574" y1="94" x2="610" y2="101" class="arrow "/>
<rect x="612" y="140" width="108" height="34" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="666" y="161.5" class="c sub b">consumer-2</text>
<line x1="574" y1="168" x2="610" y2="157" class="arrow "/>
<rect x="612" y="196" width="108" height="34" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="666" y="217.5" class="c sub b">consumer-3</text>
<line x1="574" y1="242" x2="610" y2="213" class="arrow "/>
<rect x="612" y="252" width="108" height="34" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="666" y="273.5" class="c sub b">consumer-4</text>
<text x="666" y="304" class="c sub note">没有分到分区</text>
<text x="24" y="334" class="sub">本机 3 个节点的 Kafka 4.4 实测:key 为 1024 和 1025 的消息都落在分区 2,1026 落在分区 0;</text>
<text x="24" y="352" class="sub">4 个消费者分 3 个分区,consumer-4 空闲。停掉 consumer-3 后触发重平衡,分区 2 改由 consumer-4 消费。</text>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

+85
View File
@@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="480" viewBox="0 0 760 480">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">零拷贝:sendfile 让数据不经过用户空间</text>
<text x="24" y="70" class="b">read + write:4 次拷贝,2 次系统调用带来 4 次用户态与内核态的切换</text>
<rect x="24" y="80" width="712" height="46" rx="4" fill="#F6F6F6" stroke="#E2E2E2"/>
<text x="36" y="107" class="sub b">用户空间</text>
<rect x="24" y="130" width="712" height="46" rx="4" fill="#FFFFFF" stroke="#E2E2E2"/>
<text x="36" y="157" class="sub b">内核空间</text>
<rect x="24" y="180" width="712" height="46" rx="4" fill="#F6F6F6" stroke="#E2E2E2"/>
<text x="36" y="207" class="sub b">硬件</text>
<rect x="120" y="188" width="104" height="30" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="172" y="207.5" class="c sub b">磁盘</text>
<rect x="250" y="138" width="104" height="30" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="302" y="157.5" class="c sub b">页缓存</text>
<rect x="380" y="88" width="104" height="30" rx="6" fill="#E6E3FA" stroke="#B8B0DE"/>
<text x="432" y="107.5" class="c sub b">应用的缓冲区</text>
<rect x="510" y="138" width="104" height="30" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="562" y="157.5" class="c sub b">Socket 缓冲区</text>
<rect x="630" y="188" width="104" height="30" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="682" y="207.5" class="c sub b">网卡</text>
<line x1="200" y1="188" x2="262" y2="168" class="arrow "/>
<text x="224" y="174" class="e sub">① DMA</text>
<line x1="330" y1="138" x2="392" y2="118" class="arrow "/>
<text x="354" y="124" class="e sub note">② CPU</text>
<line x1="472" y1="118" x2="530" y2="138" class="arrow "/>
<text x="508" y="124" class="sub note">③ CPU</text>
<line x1="600" y1="168" x2="660" y2="188" class="arrow "/>
<text x="636" y="174" class="sub">④ DMA</text>
<text x="300" y="108" class="c sub mono">read()</text>
<text x="600" y="108" class="c sub mono">write()</text>
<text x="24" y="258" class="b">sendfile:数据留在内核中,网卡支持 SG-DMA 时 CPU 不拷贝数据</text>
<rect x="24" y="268" width="712" height="46" rx="4" fill="#F6F6F6" stroke="#E2E2E2"/>
<text x="36" y="295" class="sub b">用户空间</text>
<rect x="24" y="318" width="712" height="46" rx="4" fill="#FFFFFF" stroke="#E2E2E2"/>
<text x="36" y="345" class="sub b">内核空间</text>
<rect x="24" y="368" width="712" height="46" rx="4" fill="#F6F6F6" stroke="#E2E2E2"/>
<text x="36" y="395" class="sub b">硬件</text>
<rect x="120" y="376" width="104" height="30" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="172" y="395.5" class="c sub b">磁盘</text>
<rect x="250" y="326" width="104" height="30" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="302" y="345.5" class="c sub b">页缓存</text>
<rect x="510" y="326" width="104" height="30" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="562" y="345.5" class="c sub b">Socket 缓冲区</text>
<rect x="630" y="376" width="104" height="30" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="682" y="395.5" class="c sub b">网卡</text>
<text x="380" y="296" class="c sub mono">应用只调用一次 sendfile()</text>
<line x1="200" y1="376" x2="262" y2="356" class="arrow "/>
<text x="224" y="362" class="e sub">① DMA</text>
<line x1="354" y1="341" x2="508" y2="341" class="arrow dash"/>
<text x="431" y="336" class="c sub">只追加地址和长度</text>
<path d="M330,356 C420,402 560,402 628,394" class="arrow green"/>
<text x="545" y="384" class="c sub ok">② DMA:网卡直接从页缓存读取</text>
<text x="24" y="450" class="sub">Kafka 的 broker 把日志文件发给消费者时使用 sendfile。启用 TLS 后数据要在用户空间加密,就不再使用零拷贝。</text>
<text x="24" y="468" class="sub">网卡不支持 SG-DMA 时,sendfile 仍要由 CPU 把页缓存中的数据拷贝到 Socket 缓冲区,共 3 次拷贝。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB