-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
519 lines (247 loc) · 650 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>谭新宇的博客</title>
<subtitle>Make Each Day Count</subtitle>
<link href="https://tanxinyu.work/atom.xml" rel="self"/>
<link href="https://tanxinyu.work/"/>
<updated>2025-01-28T06:05:02.379Z</updated>
<id>https://tanxinyu.work/</id>
<author>
<name>谭新宇</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>2024 年终总结:在清华 IoTDB 创业公司中构建起摩尔定律成长节奏</title>
<link href="https://tanxinyu.work/2024-annual-summary/"/>
<id>https://tanxinyu.work/2024-annual-summary/</id>
<published>2025-01-23T03:17:22.000Z</published>
<updated>2025-01-28T06:05:02.379Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>忙忙碌碌又是一年,2024 匆匆结束。回想这一年的成长和收获,除了个人能力的提升,在做人做事做选择等方面也有了更多的认识。可以说,自己并未虚度时光,过得十分充实。</p><p>临近除夕,总算抽出时间坚持自己之前的习惯来继续写年终总结。希望今年的总结不仅能继续鞭策自己寻找并实践摩尔定律的成长节奏,也能获得更多反馈来修正自己。</p><p>首先依然是自我介绍环节,我叫谭新宇,清华本硕,师从软件学院王建民/黄向东老师。目前在时序数据库 Apache IoTDB 的商业化公司天谋科技系统组担任内核开发工程师。我对分布式系统、性能优化等技术驱动的系统设计感兴趣,2024 年也一直致力于提升 Apache IoTDB 的集群易用性 & 鲁棒性、共识能力和写入性能等,并接手完成了若干具有挑战性的大项目。</p><p>接下来介绍一下我司:</p><p>天谋科技的物联网时序数据库 IoTDB 是一款低成本、高性能的时序数据库,技术原型发源于清华大学,自研完整的存储引擎、查询计算引擎、流处理引擎、智能分析引擎,并拓展集群管理、系统监控、可视化控制台等多项配套工具,可实现单平台采存算管用的横向一站式解决方案,与跨平台端边云协同的纵向一站式解决方案,可方便地满足用户在工业物联网场景多测点、多副本、多环境,达到灵活、高效的时序数据管理。</p><p>天谋科技由全球性开源项目、Apache Top-Level 项目 IoTDB 核心团队创立。公司围绕开源版持续进行产品性能打磨,提供更加全面的企业级服务与行业特色功能,并开发易用性工具,使得 IoTDB 的读写、压缩、处理速度、分布式高可用、部署运维等技术维度领先多家数据库厂商。目前,IoTDB 可达到单节点每秒千万级数据写入、10X 倍无损压缩、TB 数据毫秒级查询响应、两节点高可用、秒级扩容等性能表现,实现单设备万级点位、多设备亿级点位管理。</p><p>目前,IoTDB 能够为我国关键行业提供一个国产的、更加安全的、性能更加优异的选择。据不完全统计,IoTDB 已服务超 1000 家以上工业企业,在能源电力、钢铁冶炼、航空航天、石油石化、智慧工厂、车联网等行业均成功部署数十至数百套,并扩展至期货、基金等金融行业。目前已投入使用的企业包括华润电力、中核集团、国家电网、宝武钢铁、中冶赛迪、中航成飞、中国中车、长安汽车等。</p><p>值得一提的是,2024 年 IoTDB 的商业公司天谋科技营收同比增长近 300%,正式进入了指数增长的摩尔定律节奏。</p><h2 id="2024"><a href="#2024" class="headerlink" title="2024"></a>2024</h2><p>介绍完背景后,在这里回顾下 2024 年我们系统组的主要工作,可分为 TPCx-IoT 双版本登顶、共识遥遥领先、性能优化摩尔定律新时代、集群易用性 & 鲁棒性显著提升、海量项目支持和海量技术沉淀 6 个方面。</p><p>在 TPCx-IoT 双版本登顶方面,国际事务处理性能委员会(TPC)是全球最权威的数据库性能测评基准组织之一。TPCx-IoT(TPC Express Benchmark IoT)是业界首个能直接对比物联网场景下不同软件和硬件性能的基准,涵盖了性能和性价比两个维度。今年,TimechoDB 1.3.2.2 版本在开启和关闭 WAL 测试的两种配置下,分别在 TPCx-IoT 的性能和性价比两个维度均登顶。值得一提的是,原本性能第一的系统是开启 WAL 的,而性价比第一的系统则关闭了 WAL。做数据库的人都知道,是否开启 WAL 对写入性能和资源消耗有着巨大的影响。尽管如此,IoTDB 最终实现了即使开启 WAL,仍能在性能和性价比两个维度同时登顶。如果关闭 WAL,性能和性价比还能够进一步提升约 20%,这充分彰显了 IoTDB 在应对物联网高吞吐场景中的极致性能。如今回想今年与新豪一起完成的这一工作,个人感触颇深,这对我来说也是四年磨一剑的过程。还记得 2020 年下半年研究生刚入学时,我就第一次尝试用 TPCx-IoT 测试过 0.12 版本的 IoTDB 老分布式集群。当时的分布式架构存在较大问题,性能始终难以提升,那年我的外号也成了 “tpc”,因为我一个学期几乎都在做 TPCx-IoT 测试,遗憾的是最终没有得到理想结果。经过四年的彻底重构与迭代,IoTDB 自 1.0 版本推出了全新的分布式架构,并在分区、共识和写入性能等方面做了诸多创新与改进。这些进展不仅使 IoTDB 能够支撑更多的用户场景,也最终帮助我们在 TPCx-IoT 榜单上登顶,性能达到了老版本的近 6 倍,充分证明了技术创新始终是第一生产力。此外,随着商业化公司成立,本次登顶过程中,我们不再是单打独斗,获得了许多同事的全力支持,特别是在与 TPC 委员会沟通、撰写报告等方面。这里特别感谢鹏程、Chris、昊男和苏总等同事对我们组的支持与帮助。没有团队的默默付出与协作,我们也不可能在今年完成这一目标,让这把磨了四年的剑最终得以出鞘。</p><p>在共识遥遥领先方面,IoTDB 的 1.x 分布式版本参照 2020 OSDI 最佳论文 Facebook Delos 的思路抽象了一个支持不同共识算法的统一共识框架,允许用户在一致性、可用性、性能和存储成本等若干维度进行权衡。今年我们不仅将现有共识算法迭代的几乎稳定,更是创新的提出了一个新的性能遥遥领先的共识算法</p><ul><li>在强一致性算法 Ratis 方面,过去一年里我们组三位同学共为 Ratis 社区贡献了 35 个 commit,使得 Apache IoTDB 社区成为 Apache Ratis 社区仅次于原创团队的最大贡献社区。目前,Ratis 在 IoTDB 的通用场景下已经基本稳定,成员变更的稳定性也得到了显著提升。今年,宋哥成为了 Ratis 的 PMC,我成为了 Ratis 的 Committer,邓超也成为了 Ratis 的 Active Contributor。由于 IoTDB 每次发布正式版本不能依赖 Ratis 的快照版本,今年我们主动承担了多次 Ratis 社区的 Release Manager 角色,确保 IoTDB 每次发布正式版本时不依赖快照版本,从而符合 Apache 基金会的规范。今年,Ratis 共发布了 5 个版本,其中 4 次由我们组担任 Release Manager,Ratis-ThirdParty 发布了 3 个版本,其中 2 次由我们组担任 Release Manager。这标志着我们在 Ratis 维护方面已经积累了相当的能力和影响力。</li><li>在基于操作复制的最终一致性算法 IoTV1 方面,过去一年湘鹏,宇衡和我已经将其打磨至几乎彻底稳定,成员变更的稳定性也得到了显著提升。近半年以来,IoTV1 相关的工单几乎为零,这标志着 IoTDB 共识算法稳定性的显著提升。今年,我们在理论上也迈出了重要一步,针对多主异步复制共识算法在成员变更时的一致性机制和保证边界进行了详细分析和完善,最终效果是在上万次 Region 迁移中副本数据依然保持一致。甚至测试组的同学也发出了疑问:“IoTV1 已经这么稳定了,为什么还要开发新的共识算法?”。这进一步说明了 IoTV1 的稳定性已经得到了团队的广泛认可。</li><li>在基于状态复制的最终一致性算法 IoTV2 方面,过去一年我们结合物联网场景中写写冲突少的特点,与思成和俊植从 0 到 1 设计并实现了业界首个多节点多副本性能超越单节点单副本的共识算法。该算法不仅在鲁棒性上优于 IoTV1,解决了 WAL 堆积问题,还能在性能上超越现有常用的 Raft 算法 5-10 倍。在实现这一新共识算法的过程中,我们复用了流处理框架,推动了团队合力迭代,与苏总、哲涵、宇辰和振羽一起逐步提升了流处理框架的鲁棒性和稳定性。这一工作是我们在物联网场景下对共识算法的重要创新,也是我们在工程实践中发现的关键方向。我也首次完全自驱地联系伙伴(十篇 A 在投的张先生,以及我们组的新豪和俊植)共同撰写论文来沉淀我们组的工作成果,并最终得到了导师们的认可与支持。虽然没读博士,但也写了论文的两个章节过了一把博士的瘾。经过两个多月的努力,第一版论文已提交至系统方向的 A 类会议,现正等待评审。我非常期待 2025 年,IoTV2 能在学术与工程上同时与大家见面,这也是我们组今年最令人振奋、最反直觉的技术突破。</li></ul><p>在性能优化摩尔定律新时代方面,今年我跟旭鑫、昊男、雨峰、湘鹏、荣钊、钰铭、江天学长,田原学长和振宇师兄等团队成员一起进行了多项盲测写入性能优化工作并取得了显著进展。我们不仅在很多特定场景下实现了性能提升数十倍的效果,还在通用场景下实现了写入性能翻倍的成就,这是 IoTDB 写入性能提升最大的一年。通过分布式架构、存储引擎和系统优化的组合拳,我们成功让 IoTDB 在通用场景下的盲测写入性能进入了摩尔定律的成长节奏(每 18 个月性能翻一倍或资源利用率减少一半)。以 2023 年为基准,2024 年我们已经实现了这一目标,2025 和 2026 年现有的技术储备也已经为继续沿着摩尔定律节奏提升奠定好了基础。在具体优化方面,我们做了很多关键工作,仅列举已做的开源部分如下:</p><ul><li>行列接口自动转换:优化场景是用户使用行式接口写入列式数据,系统能够自动检测并转换为列式向量化执行,从而提升写入性能。</li><li>WAL 压缩:优化场景是通用 I/O 瓶颈场景,通过 WAL 压缩,显著节约实时磁盘 I/O 带宽。</li><li>WAL 批量化:优化场景是设备多测点少的行式批量写入接口,能够显著降低 CPU 利用率和写入延迟。</li><li>第一条写入调优:优化场景是空集群第一条写入耗时过长,通过优化显著提升了用户体验。</li><li>表模型写入性能优化:优化目标是使 IoTDB 即将发布的表模型写入性能与树模型相似,将树模型优化思路接入表模型,从而提升写入效率。</li><li>重启加速:优化场景是加速重启速度,使得重启时间不再与节点数据量挂钩,进而提升集群可用性。</li><li>默认 DataRegion 数与硬件资源绑定:优化场景是针对更强硬件(如 16 核以上机器),自动根据硬件资源配置合理的共识组个数,从而高效利用 CPU 资源。</li><li>Memtable State of the Art 数组:优化场景是解决 Memtable 读写删并发互相影响的性能问题,我们通过设计符合时序场景的 State of the Art 数组结构,显著提升了存储引擎 Memtable 的并发性能。</li></ul><p>在集群易用性 & 鲁棒性显著提升方面,我们也做了非常多的工作</p><ul><li>Region 迁移和 DataNode 缩容是 IoTDB 动态扩缩容能力的基石,针对在迭代过程中遇到的分布式状态维护难(多个节点的内存和磁盘均维护状态)、沟通成本高(牵扯模块多)和复杂场景多(考虑若干故障场景)的问题,宇衡、湘鹏、珍姐和我在考虑众多因素后进行了详细设计和开发。为了保证最终交付给用户的功能质量,我们进入了测试的 Bug 拉锯战,这个过程堪比系统组的诺曼底登陆,无数精力都投入在鲁棒性的打磨上。经过近一年的打磨,现阶段的 bug 已经基本收敛,功能也逐步从不可用到基本可用,我们已在若干实际用户场景中实践了该功能,2025 年我们已经不再畏惧上万次迁移和 TB 级别迁移的场景。</li><li>在易用性 & 鲁棒性方面,我也和宇衡、雨峰、文炜、湘鹏和荣钊一起进行了大量的完善,包括但不限于配置文件三合一、热更新加载参数缺失配置项恢复默认值、Set Configuration 集群更新配置语句、Region 重建/增删副本、Verify connection 检测网络连通性、CLI 激活 & 机器码缩短、Procedure 维护(10+ commit)、WAL 阻写默认阈值与磁盘大小绑定、CN 脑裂问题修复、节点启停流程双军问题工程完善、RTO/RPO 优化、多数据库负载均衡、多数据库创建保护机制、激活代码同步冲突处理等。这些工作显著提升了 IoTDB 的稳定性和用户黏性。</li></ul><p>在 IoT-Benchmark 方面,今年我们梳理了其项目结构和 README,使其逐步向更通用的时序基准测试工具演进。过去一年钰铭作为主力带领我们迭代了近 100+ commit,包括 50+ 稳定性修复和易用性提升、以及 10+ 性能优化。</p><p>在持续集成与迭代体系方面,今年在王老师的指引下,我们引入了对第三方库的 SBOM 管理,并开始使用 NVD 扫描并持续追踪开源项目中的 CVE 问题,从而逐步提升了对第三方依赖漏洞安全问题的重视。此外,我们还开始统计 IoTDB 的代码量,以评估代码复用效果和项目的复杂度。与此同时,我们也意识到,随着产品功能和复杂度的不断增加,测试用例的指数级增长与产品迭代效率之间存在一定的 trade-off。结合每天晚上和周末 CI 机器几乎都在空闲的现状——每周 168 个小时中,只有大约 1/3 的时间 CI 机器在工作,其余 2/3 的时间处于闲置状态——我与钰铭开始探索将 CI 拆分为不同级别的测试体系,包括 commit、daily 和 weekly 级别的测试。我们在 commit 级别保留最为关键的 CI 测试,在 daily 和 weekly 测试中充分利用闲置的机器资源,补充更多的测试用例。同时,我们也将引入智能化策略,自动识别并追踪有问题的 commit,从而在开发效率与质量保障之间找到更好的平衡点。</p><p>在海量项目支持这块,我则个人负责了若干探索性项目并参与了很多实际项目</p><ul><li>在某一关键领域的大客户项目中,我担任了技术架构师和部分项目经理的角色,与乔老师、祥志、洪胤和高飞学长从需求调研到业务建模,再到推动内核迭代优化,几乎将自己毕生所学都倾注其中。庆幸的是,今年我们的工作得到了双方的认可。我们的工作包括但不限于:特定领域数据时序数据库一体化建模与应用的可行性验证,较原生系统现有资料数据性能提升超过 10 倍,下一代负载更大 10 倍的资料数据从不可写转为可写;我们还实现了该领域首个针对多维查询的异构多副本高可用方案,性能提升了一个数量级;结合具体场景,我们对 IoTDB 的 LSM 引擎进行了架构优化,使 0 层 TsFile 文件大小提升了 300 倍,系统从不可用变为可用;此外,我们还针对大文本数据进行了关键技术演进,包括零拷贝和内存池优化等。最终,我们还预留了若干内核优化,期待在 2025 年继续打磨完善。这个极具性能挑战的业务场景,不仅让我在高压下不断突破自己,也让我更加深刻地体会到系统设计的乐趣。</li><li>除了预研项目外,我还通过项目工时表统计了自己今年 6-12 月共 7 个月参与的项目耗时,共计 257 小时,平均每个工作日 2 小时。具体支持的内容包括但不限于与竞品 PK 并取得胜利、撰写各种报告和文档、售前(包括纯英文售前)、oncall、写标书、评奖答辩和技术分享等。这些持续的项目和业务侧投入,使我能够始终接触产品一线,从一个更全局、具备发展眼光的视角去平衡不同工作的优先级,并理解一个 2B 创业产品需要关注的方方面面。在此,也特别感谢佳哥、红岩以及其他项目组的同事,帮助内核团队承担了大量的线上压力。</li></ul><p>在海量技术沉淀这块,则基本是我们出于技术 & 业务双驱动完成的很多探索</p><ul><li>在技术推送方面,今年我们组协调产研团队共发布了 19 篇技术推送,其中我们组独立完成了 10 篇,包括《分布式三部曲》系列、监控系列、与 HBase/InfluxDB 的对比系列、TPC 系列等。我们公众号的推送视角也逐渐从纯技术视角转向了用户视角,开始更加关注公众号目标用户的实际需求。现在,我也在跟随旋哥一起审核并整理一些 FAQ 问题,期望能够解决更多开源用户的问题。此外,我还在知乎上宣传了 IoTDB 在分布式架构下的细致考虑,收获了不少关注和反馈。</li><li>在学术成果方面,今年我们组产出了 1 篇软著、4 篇专利和 3 篇论文(2 篇在投),涵盖了我们组负责的负载均衡、共识算法、时序基准测试工具和 TPC 登顶等方面的工作。这些成果得益于去年王老师和东哥的要求,促使我们组不断沉淀并输出成果,在与同行交流的过程中激发了更多的创新点。</li><li>在 JVM GC 探究方面,今年俊植和我一起举办了 GC 讨论班,我们对 JDK8/11/17 的默认 GC 算法 PS 和 G1 的原理和所有可调优参数都进行了研究和分享,我们也整理了相关 Cook Book 便于更多的同事能够参照流程图进行 GC 调优。我们也完善了 IoTDB 启动相关的 GC 参数,使得默认的 GC 参数是我们实践得到的最优选择。此外关于默认 GC 算法到底应该选择 PS 还是 G1 的问题,团队内部有很多争论和质疑,由于默认总需要选择一个 GC 算法,而不可能有任何一个 GC 算法在所有维度(例如吞吐,最大暂停延迟,稳定性和内存占用等)都能够超过其它 GC 算法。为了避免大家在这里耗费太多精力(例如“我发现某个场景 PS 更好”,“我发现某个场景 G1 更好”,“我觉得应该 xxx”),我们组结合过去两年对 GC 算法的研究和所有的对比案例整理了一个文档,得出了在我们所接触的所有场景里默认使用 G1 对于盲测更优,如有特定需要可调整为 PS 的方案。通过这种方式,我们平息了大家对这块的时间投入,能够让大家抽出更多的精力去专注于其他更重要的事情。</li><li>在默认推荐 JDK 版本升级方面,23 年俊植和我曾尝试将 IoTDB 的默认推荐 JDK 版本从 JDK8 升级为 JDK17。然而,升级后冯老师发现写入、合并和导入导出等功能的耗时均有增加,经过近一个月的排查未果,我们暂时搁置了该问题。今年在调整 GC 算法默认参数时,我们意外发现,JDK17 性能下降的原因是 JDK15 之后默认关闭了偏向锁。我们在 JDK8 环境下关闭偏向锁也能复现类似的耗时增加现象。进一步排查后发现,IoTDB 内部的某些文件 IO 基础类过多使用了 synchronized,导致偏向锁取消时性能回退。通过优化这些基础类,我们解决了性能下降的问题,使 IoTDB 从 1.3.2 版本起默认推荐 JDK 17 部署。此举不仅让 IoTDB 的默认推荐 GC 算法从 PS 改为 G1,还为我们未来利用如 Vector API 等 JDK 高阶功能奠定了基础。</li><li>在 JVM 非堆内存上涨问题方面,23 年我们团队已将 JVM 内存划分为堆内内存、堆外内存和非堆内存。今年,在某用户环境中,我们发现配置好 IoTDB 的堆内和堆外内存后,整个进程占用内存依然不断上涨,最终被 OOM-Killer 杀掉,说明非堆内存出现泄漏。通过结合 NMT 工具和 Oracle 官网文档对非堆内存进行分析,俊植和我提出了 IoTDB 内存配置的安全部署公式,虽然解决了线上内存不断增长的问题,但由于用户不愿进一步在生产环境中帮助我们确认原因,问题的根因排查暂时被搁置。幸运的是,由于我们开源了我们沉淀的 JVM 内存管理文档,一家广州创业公司的程序员联系到了我们,他们的 Java 服务也复现了该问题。在进一步沟通后,我们定位到这个场景的问题是由于 JVM 的默认内存分配器 glibc 缓存机制引起的,经过更换为 jemalloc 后,内存 RSS 稳定不再出现泄漏。这一经历也让我们更加深刻地认识到沉淀和分享技术的重要性。</li><li>在访存瓶颈零拷贝优化方面,23 年我们发现,在一些大文本场景中,IoTDB 的 CPU、磁盘和网络均已经不再是瓶颈,反而是访存成为了瓶颈。今年,思屹和我系统整理了 IoTDB 的写入 RPC 请求从网卡到磁盘的端到端拷贝次数和访存次数,发现了 Thrift 框架中可优化的零拷贝部分。通过该优化,IoTDB 在 44KB 大文本场景下的写入吞吐提升了 35%。由于零拷贝技术需要完善控制对象生命周期,我们尽量平衡了性能收益与代码侵入性,首先优化了性能提升最大且生命周期控制最容易的客户端 Server 和共识 Server 部分。我们也为未来的扩展预留了接口,目前流处理组的宇辰已经开始尝试进一步引入客户端零拷贝来优化性能。</li><li>在 JVM 内存池优化方面,针对大文本场景中的 GC 问题,思屹和我仔细梳理了 GC 触发的根本原因,并分析了 Java 与 C++ 在内存管理上的差异。C++ 允许开发者手动管理对象生命周期,并显式申请和释放内存,而 Java 则通过后台的可达性分析机制异步回收内存。由于这一机制,在处理大对象(如 byte[]、long[] 数组等)时,Java 在频繁的大内存申请与释放过程中容易引发较多的 GC,消耗大量 CPU 资源并影响程序性能。这本质上也是 C++ 和 Java 在开发者心智负担与性能之间的 Trade-off。因此,我们设计并实现了一个支持变长 byte[] 的内存池,提供了手动和自动两种接口,允许调用者选择是否手动管理生命周期来池化 byte[]。手动接口需要牺牲一定的开发者体验,要求实现引用计数机制来显式归还,但能获得更好的性能;而自动接口则基于虚引用机制实现内存的自动归还,性能相对较差,但仍优于不做池化的方式。该内存池还引入了基于 EMA 算法的主动驱逐策略和基于 JMX 的 GC 感知被动驱逐策略,确保性能提升的同时,不会引入新的稳定性问题。根据我们的测试,在 4KB 至 1MB 的大文本场景下,写入性能提升了 6% 到 71%,GC 从很严重降低到几乎没有。未来,我们计划将所有 JVM 大对象数组接入该对象池,从而消除大多数 GC 开销。</li><li>在多 NUMA Node 机器性能优化方面,今年思屹和我在一台 192C 768G 内存的 4 NUMA Node 机器上进行了 IoTDB 性能提升的探索。我们首先尝试了单进程优化,发现 JDK 17 之后的 G1 垃圾回收算法已支持 NUMA 感知,但仅限于新生代,对于老年代的内存访问仍然存在较多的跨 NUMA Node 访问。对于 Java 来说,老年代内存可以通过第三方库如 Thread Affinity 进行核绑定,这要求访存和管存线程在同一个核上运行。然而,这种方式对内核侵入较大,因此我们推荐直接使用 JDK 17 以上版本自身的能力进行单进程优化。尽管单进程优化的空间有限,但我们发现多进程优化具有较大突破潜力。通过将每个 IoTDB 进程使用 numactl 命令绑定到一个 NUMA Node,我们能够以极小的代价显著提升性能。在该物理机上进行的 1 进程与 4 进程绑核的性能对比测试表明,读写性能最高可提升 1 倍,使用 Intel Vtune 工具观测到的跨 NUMA 带宽也显著降低。通过这一探索,IoTDB 在多 NUMA Node 机器上的高性能部署方案得到了进一步的优化和完善。</li><li>在外部技术输出方面,今年宇衡在持续追踪一个影响 IoTDB 运行的问题时,发现了一个 GraalVM 编译器的 bug,在将其提报到 Oracle 社区后,得到了认可并快速修复。此外我在解决一个线上 WAL 堆积问题时,发现根因是 Thrift AsyncServer 的 Epoll 存在 bug,通过深入研究代码,我明确了具体原因并推动 Thrift 社区最终解决了该问题。这些反馈也引发了我的深思:对于一个追求普适性的复杂产品,稳定性和兼容性往往需要最优先考虑,那么那些出于性能优化驱动的新技术(如异步 server、异步 I/O、direct I/O、虚拟线程等)便需要谨慎使用。即便使用了,也应提供开关。因为只要延迟够用,吞吐基本可以通过横向扩展来提升,不必过度依赖那些看起来非常新颖但可能不够稳定的技术。特别是在当前国产化的背景下,如果这些技术没有在各种硬件和操作系统环境中做足够的测试和完善,线上问题一旦发生,oncall 的体验将非常痛苦,且会对用户和产研团队带来更大的负担。</li><li>在更多的探索尝试方面,今年我们还有 6-7 个尚未合并到主分支的硬核技术尝试工作,具体细节暂不展开。期望这些工作能在 2025 年不断完善,为 IoTDB 2026 年通用场景盲测性能的摩尔定律节奏奠定基础。</li></ul><p>今年,我在 Apache IoTDB 社区提交并被合并了 84 个 PR(去年 119 个),Review 了 509 个 PR(去年 387 个)。相比去年,今年我的大部分精力都集中在贴近业务和技术管理上,也对个人和团队如何最大化产出有了更多思考和感悟。今年 8 月,我受邀成为 Ratis 社区的 Committer,并成功拥有了 1k Github Follower,这让我更加认可自己在开源领域的专注。回顾过去一年,我觉得我们成功将团队的许多工作通过各种方式沉淀下来,并影响了许多人。希望我们能始终在这段青春年华中保持对技术的热情,专注于我们的工作继续前行。在这里,我特别感谢我的女朋友🍊 始终支持我的工作并帮助我疏解情绪,让我感受到生活的美好与幸福。她还带我见识了许多新事物,让我对人生的很多方面有了新的认识和思考。</p><h2 id="一些感悟"><a href="#一些感悟" class="headerlink" title="一些感悟"></a>一些感悟</h2><p>介绍完充实的 2024,回顾 2023 年终总结,可以发现今年我们在去年四个维度的展望上都取得了不错的成绩</p><ul><li>做深:我们组输出了大量工程技术沉淀,也产出了 4 项专利、3 篇论文(2 篇在投)和 1 项软著。</li><li>做广:上半年与存储引擎合作,下半年与流处理引擎合作。</li><li>做好:我们显著提升了系统稳定性,降低了纯分布式模块的 oncall;通过若干内核功能优化,提升了易用性;同时,我们也通过分布式、存储引擎和系统优化组合拳打造了 IoTDB 通用场景盲测写入性能的摩尔定律节奏,并预留了三年的余量。</li><li>做响:我们成功完成了 TPCx-IoT 登顶工作,并获得了央视报道;此外,我们小组的技术输出总阅读量达 2w+,提升了团队的知名度和影响力。</li></ul><p>下面分享一下我今年的很多成长感悟,欢迎大家批评指正。</p><h3 id="稳定性打磨工作如何评估时间"><a href="#稳定性打磨工作如何评估时间" class="headerlink" title="稳定性打磨工作如何评估时间"></a>稳定性打磨工作如何评估时间</h3><p>对于一个稳定性打磨的功能,如何评估其完善时间?在打磨 Region 迁移的稳定性时,我今年思考了很久。如果考虑无数硬件环境(如 4C16G 和 192C768G)、测试负载(实时写入、读写混合),再加上注入各种异常(如节点重启、网络分区、断电等),以及多个模块功能的组合(如多级存储、存储引擎、共识层、流处理引擎等),可以看出它们的组合是指数级扩展的。即使研发进行了完善的设计与实现,但提测后仅测试完善的开销就几乎永无止境。但如果一开始就定下工作周期为半年或一年,也难以做出可靠的过程管理来向上汇报。</p><p>在这种困境下,我们必须意识到场景是无限的,在精力有限的情况下,我们的目标是用最小的研发和测试代价解决尽可能多的 bug。最初,我们按照研发视角将功能的稳定性迭代分为 V1、V2、V3,期望逐步打磨到稳定状态。然而在实际测试中,我们发现测试视角与研发视角并不同频,导致测出的 bug 比较分散,尽管测试与研发一同打磨很久,仍难以向上汇报阶段性成果,因为每个模块都有不少 bug 被修复。这使得这个工作看起来像是无底洞,且容易受到质疑。其实,问题的根本在于缺乏多方共识的客观评价标准。</p><p>回顾整体流程,我认为可以在以下两个方面做得更好</p><ul><li>避免测试开销的指数级扩展:对功能中的核心模块,尽量做好抽象并补充完备的测试。在功能从 UT、IT、研发自测、测试自测、用户 POC 到用户线上等各个环节中,越早发现问题,整体时间成本就越低。更有趣的是,这种流程优化能潜移默化提升整个团队的效率。同样工作 8 个小时,高效与低效的差距,对于软件开发团队的影响会非常的大,这也是我未来需要持续反思与提升的地方。</li><li>多方对齐:对于稳定性迭代工作,需要研发、测试和产品在功能、性能和测试场景等多个维度上提前对齐优先级,并将工作分配到 V1、V2、V3 版本中。对于每个小工作项,能够精确预估时间;对于大工作项,提供概要预估时间即可。这样即便出现延期,团队也能清楚了解目前功能的进展,哪些场景可以交付给用户,哪些还需改进。这会大大提高工作的透明度和效率。</li></ul><h3 id="软件工程没有银弹"><a href="#软件工程没有银弹" class="headerlink" title="软件工程没有银弹"></a>软件工程没有银弹</h3><p>今年有件让我深受感触的事,那就是发现大家对 IoTV2 共识算法的价值产生了质疑。从纯研发的视角来看,IoTV2 显然在创新性和性能上都显著超越了 IoTV1,是我们组过去几年最具创新性的工作之一,其他共识算法也都花了好几年才稳定,IoTV2 毕竟才诞生一年。但如果想在开发团队中获得更广泛的认可,就需要考虑大家关注的不同方面,包括稳定性、创新性、问题收敛程度、潜在收益与投入的平衡等。可以看出,这些维度之间往往存在矛盾,而且很难得出绝对客观的结论,很多东西也完全看未来的事在人为,因此很难在所有人中达成共识。这让我意识到,当一个软件项目和团队发展到一定阶段后,是否落实创新工作,往往会面临保守和激进的分歧,二者需要不断博弈与制衡,才能走向一个可行的方向。完全激进或者完全保守都会带来不可预知的风险。</p><p>回到 IoTV2,我们能够平息质疑的一个重要原因是我们做了共识层的抽象,能在一套接口下支持不同的共识算法,从而使得各个共识算法可以单独迭代。如果没有这个统一的接口,不管 IoTV2 作为业界第一个多副本超越单副本的共识算法有多么创新,仍然会面临无数质疑,甚至可能导致无法迭代。而对于竞品来说,除非照抄 IoTDB 整体共识层的设计,否则也很难平息内部质疑,全力推进这项工作。这为我们未来的扩展性设计提供了指导——良好的接口抽象能够使得系统的关键迭代从“不可能”变为“可能”。当然,软件工程没有银弹,即使我们通过共识层的抽象让 IoTV2 的迭代得以顺利进行,但代价就是翻倍的测试和打磨开销。总体而言,抽象得越好,复杂度封装得越到位,测试和打磨的开销也就越低。</p><h3 id="个人的管理成本-ROI"><a href="#个人的管理成本-ROI" class="headerlink" title="个人的管理成本 ROI"></a>个人的管理成本 ROI</h3><p>在今年参与更多技术管理工作后,我渐渐关注到个人管理成本 ROI 这一概念,其本质是消耗尽可能低的 +1 管理成本(包括时间和资源等)完成更复杂的工作,并在有风险时及时汇报并提供辅助决策的数据。</p><p>总体而言,不同的人有不同的管理风格,同一个人对于不同的事情也会采取不同的管理方式。有时像《大明王朝 1566》中的嘉靖一样,只关注结果,不拘过程;有时又像《大决战》中的 101 一样,会关注每一个细节。其实,不论是哪种管理风格,最终目标都是完成工作,并没有绝对的好坏之分。</p><p>对于我们个人而言,我们控制不了别人,唯一能够不断改善的就是提升自己的管理成本 ROI。通过这种方式,我发现能够使得自己与他人的合作变得更加高效和愉快。类似的例子包括但不限于</p><ul><li>完成一个 PR 后,补充详细描述和充足的测试用例,并至少自己 Review 一遍,再让 +1 Review,这样 +1 只需花费极小代价即可合并 PR。</li><li>对于自己负责的工作方向,如果涉及到非常多的琐碎事项,主动周期性汇总关键点并屏蔽细节与 +1 沟通,让其在最短时间内了解现状并能进一步向上汇报,不要让他消耗很多时间去整理细节。</li><li>在几乎不消耗 +1 时间的情况下,完成复杂架构设计并获得原本 +1 需要沟通的人员认可,让 +1 仅需做最终决策。</li><li>在发现项目潜在风险时,及时整理现状和信息与 +1 沟通,评估是否需要更多资源,确保项目整体风险可控。</li></ul><p>这些经验很多是在与我们组俊植一起进步的过程中学到的。希望自己能像俊植一样,不断提升自己的管理成本 ROI,进而锻炼出更好的职业素养。</p><h3 id="十倍程序员如何进一步提升"><a href="#十倍程序员如何进一步提升" class="headerlink" title="十倍程序员如何进一步提升"></a>十倍程序员如何进一步提升</h3><p>今年年中,我读了润基哥哥的十倍程序员文章,受益良多。对于十倍程序员的成长,本质上有两个方面:</p><ul><li>基于延迟的纵向扩展:这既包括以前做不到的事现在能做了,即延迟从无穷大变成了可量化的数字,也包括以前能做的事现在能更高效完成,即延迟更低。</li><li>基于吞吐的横向扩展:这包含了能够带领团队在单位时间内并行产出更多成果,这中间需要避免单点瓶颈和负载不均衡现象,才能发挥出团队最大的力量。</li></ul><p>从纯个人能力上来看,可以按照上述思路进行纵向和横向扩展,但今年我也意识到人力始终有限,战略上选择一个正确的方向才是事半功倍的关键。所以今年除了个人业务能力的提升,我还积累了很多做人做事的经验。对于很多事情的可行性,我不用再依赖他人的意见,而是能够自己进行主观判断。希望自己能在这个方向上继续沉淀,用靠谱的战略指引自己不断成长。</p><h3 id="决定不做什么往往比决定做什么更难"><a href="#决定不做什么往往比决定做什么更难" class="headerlink" title="决定不做什么往往比决定做什么更难"></a>决定不做什么往往比决定做什么更难</h3><p>其实这个感悟与战略类似,说一个应该做的事情只需要十秒钟,然而将这个事情具体落地可能需要十周甚至十个月的时间。人力始终有限,尤其对于一个软件团队来说,面对无数的输入和决策指引,客观上这些工作不可能面面俱到,必须进行战略性取舍。</p><p>决定做什么往往没有太多压力,因为人性中总有一种“即使失败了,没有功劳也有苦劳”的自我安慰。但如果要决定不做什么,则必须对自己的业务和竞争力有深刻理解,出于提高人效的角度思考且愿意承担政治责任,才能最终说服别人。这种决策十分困难且珍贵,但也正是许多高效团队能够成功的关键。</p><p>今年,我有幸在博士生组会中跟随王老师龙老师带领的实验室团队学习时序 AI 大模型的落地思路。虽然王老师龙老师一直强调我们现在已经做好了“存数”,接下来要把“用数”做好,但他们也明确通过案例分析告诉我们,哪些 AI 项目是靠谱的能够最终产生实际价值,哪些 AI 项目是不靠谱的做了也只是白白耗费精力不创造实际价值。这种战略定力和担当,让我深受触动。</p><h3 id="人性的惯性是根据情绪和结果来评价过程"><a href="#人性的惯性是根据情绪和结果来评价过程" class="headerlink" title="人性的惯性是根据情绪和结果来评价过程"></a>人性的惯性是根据情绪和结果来评价过程</h3><p>尽管我一直对历史很感兴趣,很多大道理早已听过,但今年在工作中,我从切身实践中获得了一个深刻的感悟:任何事、任何人都会有正负面的影响和评价,不存在一个完人,也不存在一个能够得到所有人认可的方案。</p><p>人性大多数时候是非常真实的,大家常常根据结果来判断过程。如果事情没有做成,就会有人列举一堆负面评价来解释为什么失败;如果事情做成了,又会有人说一堆正面评价来证明“早就看他行”。但实际上,成与不成,除了人的因素,还很大程度上取决于天时地利,而这些天时地利,作为非人为因素,反而会深刻影响最终大家的评价。此外,尽管我们都在强调要客观理智,但根据我的观察,大多数人,包括我自己,也曾在情绪驱动下做出一些非预期的行为,并且不断强调自己并没有被情绪左右。</p><p>意识到这些后,我明白了很多事情,要么不做,要做就尽己所能做到最好,不必过多顾虑他人的评价。只有这样,才能避免不必要的内耗和沟通成本,将精力集中在更重要的事情上,这样反而更有可能将事情做成。</p><h3 id="刚柔并济才能实现可持续发展"><a href="#刚柔并济才能实现可持续发展" class="headerlink" title="刚柔并济才能实现可持续发展"></a>刚柔并济才能实现可持续发展</h3><p>在今年的工作中,我逐渐有了一些中庸之道的感悟。每个人都有不同的特点,要凝聚一个多样化的团队,需要更多的包容性和开放性。过于从自己的视角偏重某一维度,反而可能导致适得其反,产生“过刚易折”的效果。在做人做事时,面对大的目标和原则性问题时,我们要坚持“刚”;而对于那些不影响最终目标的小细节,则可以选择“柔”。此外,在与许多跨行业朋友的沟通中,我逐渐意识到,尽管我们都在努力工作,但很多事情还是需要天时地利。有时候,顺势而为、蛰伏等待也许是成功的关键。</p><p>因此,保持良好的工作心态,营造融洽的工作氛围,在自驱保证自我成长的基础上,不必过于纠结于远方的目标,而应专注当下刚柔并济,才能实现可持续发展,并与团队一起走得更远。</p><h3 id="如何平衡个人输出和团队输出"><a href="#如何平衡个人输出和团队输出" class="headerlink" title="如何平衡个人输出和团队输出"></a>如何平衡个人输出和团队输出</h3><p>今年对我个人的时间管理和抗压能力来说是极具挑战的一年。上半年临危受命接管存储组,scope 显著扩大,团队人数相比去年接近翻倍。由于一些原因,无法进一步进行分级管理,这对我个人精力提出了极大挑战。基本上,我每天都在不断地线程切换,盯着十几二十个事项。尽管我已经转变为“没有深入参与时间,只略微沟通过程便要结果”的最低投入策略,但由于我依然是单点瓶颈,很多进展缓慢的事情和无人处理的 bug 需要我来当“救火队员”。我的时间依然远远不够用,一旦我在个人处理的某个问题上阻塞了一两个小时,那基本上会造成四五个问题的连锁阻塞。高压状态下,这种情况对我个人的心态和情绪也产生了一定的影响。幸好,下半年江天学长挺身而出,接过了存储组的压力,帮助我们组的人数恢复到了一个相对合理的规模,让我有更多精力去探索和深度参与我们组的很多工作。</p><p>现在回想这段经历,我意识到一个人的合理管理半径不应超过 10 个人。在这个范围内,能够在个人输出和团队输出之间取得一个良好的平衡。此外,只有与团队一起成长、大家自驱地去做事,才能在不线性增加时间和精力投入的情况下,扩展管理半径。</p><h3 id="与指数增长团队一起指数增长"><a href="#与指数增长团队一起指数增长" class="headerlink" title="与指数增长团队一起指数增长"></a>与指数增长团队一起指数增长</h3><p>即使是同一个人,在不同的年纪,对于金钱、工作氛围、健康和工作生活平衡等方面的追求都会有所不同。但一直以来,驱动我前进并屏蔽这些外部欲望的动力,始终是如何在单位时间内获得更多的成长。随着时间的推移,我逐渐意识到,能够承担越来越大的责任,并创造更多的价值,才是个人成长的核心所在。</p><p>固然我们可以在任何地方按照这个思路去追求自我成长,但只有在一个增量团队中,团队和个人的双指数增长才更容易实现。希望大家可以找到与自己 match 的指数增长团队。</p><h2 id="来年展望"><a href="#来年展望" class="headerlink" title="来年展望"></a>来年展望</h2><p>通过这一年,我们为 IoTDB 在技术上构建了摩尔定律的成长节奏。幸运的是,这些技术积累也立即在影响力和营收上得到了体现。希望在新的一年,无论是我个人还是团队,都能继续保持这种摩尔定律般的成长节奏,推动更多的技术创新和业务突破。</p><p>最后,在除夕之际,预祝大家新年万事如意,心想事成!愿每个人在新的一年中都能够事业有成,收获满满!</p>]]></content>
<summary type="html"><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>忙忙碌碌又是一年,2024 匆匆结束。回想这一年的成长和收获,除了个人能力的提升,在做人做事做选择等方面也有了更多的认识。可以说,自己并未虚</summary>
<category term="IoTDB" scheme="https://tanxinyu.work/tags/IoTDB/"/>
<category term="年终总结" scheme="https://tanxinyu.work/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>2023 年终总结:从清华 Apache IoTDB 组到创业公司天谋科技</title>
<link href="https://tanxinyu.work/2023-annual-summary/"/>
<id>https://tanxinyu.work/2023-annual-summary/</id>
<published>2024-02-07T07:24:52.000Z</published>
<updated>2024-02-11T05:47:14.004Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>兜兜转转又是一年,不知不觉 2023 已经结束。回想自己过去一年的成长与感悟,依然觉得是收获满满。今年工作之后闲余时间相比学生时代少了许多,到了除夕才有时间来写今年的年终总结。好在自己还是下定决心将这个习惯坚持下去,希望这些年终总结不仅能够在未来的时光里鞭策自己,也能够获得更多大家的反馈来修正自己。</p><p>首先依然是自我介绍环节,我叫谭新宇,清华本硕,师从软件学院王建民/黄向东老师。目前在时序数据库 Apache IoTDB 的商业化公司天谋科技担任内核开发工程师。我对分布式系统、可观测性和性能优化都比较感兴趣,2023 年也一直致力于提升 Apache IoTDB 的分布式能力、可观测性和写入性能。</p><p>接下来介绍一下我司:</p><p>天谋科技的物联网时序数据库 IoTDB 是一款低成本、高性能的时序数据库,技术原型发源于清华大学,自研完整的存储引擎、查询计算引擎、流处理引擎、智能分析引擎,并拓展集群管理、系统监控、可视化控制台等多项配套工具,可实现单平台采存算管用的横向一站式解决方案,与跨平台端边云协同的纵向一站式解决方案,可方便地满足用户在工业物联网场景多测点、多副本、多环境,达到灵活、高效的时序数据管理。</p><p>天谋科技由全球性开源项目、Apache Top-Level 项目 IoTDB 核心团队创立。公司围绕开源版持续进行产品性能打磨,提供更加全面的企业级服务与行业特色功能,并开发易用性工具,使得 IoTDB 的读写、压缩、处理速度、分布式高可用、部署运维等技术维度领先多家数据库厂商。目前,IoTDB 可达到单节点每秒千万级数据写入、10X 倍无损压缩、TB 数据毫秒级查询响应、两节点高可用、秒级扩容等性能表现,实现单设备万级点位、多设备亿级点位管理。</p><p>目前,IoTDB 能够为我国关键行业提供一个国产的、更加安全的、性能更加优异的选择。据不完全统计,IoTDB 已服务超 1000 家以上工业企业,在能源电力、钢铁冶炼、航空航天、石油石化、智慧工厂、车联网等行业均成功部署数十至数百套,并扩展至期货、基金等金融行业。目前已投入使用的企业包括华润电力、中核集团、国家电网、宝武钢铁、中冶赛迪、中航成飞、中国中车、长安汽车等。</p><h1 id="2023"><a href="#2023" class="headerlink" title="2023"></a>2023</h1><p>介绍完背景后,在这里回顾下 2023 年我们系统组的主要工作,可分为高扩展性、高可用性、可观测性、性能优化、技术支持和技术沉淀 6 个方面。</p><p>在高扩展性方面,我们主要做了以下工作:</p><ul><li>计算负载均衡: Share Nothing 架构面临的主要挑战之一是扩展性问题,扩缩容过程中需要迁移大量数据,这不可避免地消耗系统资源,进而影响现有的读写性能。为了解决这个问题,Snowflake 带头在业界推广了存算分离的架构设计,近年来的 Serverless 架构则进一步追求了更极致的弹性。尽管存算分离架构能够避免在扩缩容时迁移大量数据的问题,但它仍面临着冷启动问题。也就是说,当一个计算节点宕机后,从对象存储服务恢复宕机节点数据的过程可能会比较耗时,这对于对 SLA 要求极高的应用场景构成了挑战。那要如何解决这一问题呢?在 VLDB 2019 的论文中,ADB 介绍了其架构解决方案,其中一个值得注意的点是,它对本应无状态的 ReadNode 实施了热备份。虽然论文没有解释为何采取这种做法,但很明显,这种方案可以通过增加机器资源消耗来确保 SLA 指标,从而进一步说明了面对不同业务场景和问题时,不同架构可以找到更加适合的 trade-off。针对 Share Nothing 架构的扩展性问题,乔老师今年引导我们探讨了在时序场景中是否可能避免扩缩容时的数据迁移。我们发现,相比传统的 TP/AP 场景,时序场景有几个不同之处:首先,读写负载相对更加稳定可预测;其次,大部分情况下数据的时间戳会呈现正态分布,并随着时间不断递增。这为我们提供了结合场景进行优化的可能性。我们通过将数据划分为不同的时间分区,并在新的时间分区到来时进行实时负载均衡分配,从而实现了无需迁移数据即可达到计算资源均衡的效果,甚至在运行 TTL 时间后,还能进一步实现存储和计算资源的双均衡。回顾我们的设计,通过牺牲新节点立即提供服务的能力,我们避免了扩容时的数据迁移,这在大多数负载可预测的时序场景下取得了良好的效果。当然,对于一些特殊场景,我们也提供了手动 Region 迁移的指令,以便运维人员根据业务需求,在存储和计算资源的平衡时间上进行手动调整。</li><li>分片分配算法:在今年上半年针对某用户的 12 节点 2 副本场景进行高可用性测试时,我们遇到了一个问题:当我们故意使一个节点宕机后,发现另一个节点出现了 OOM 现象。深入分析后,我们明白了问题所在:由于整个集群仅有 6 个副本集合,一个节点的宕机导致约 1/6 的 Region Leader 被迫迁移到了同一节点上,这导致了该节点过载,进而出现 OOM。其实这一问题是一个典型的分片分配问题。我们调研学习了来自 Stanford 的 ATC 2013 Best Paper Copysets 论文以及该作者两年后在扩缩容场景对 Copysets 算法的补充,并决定将该算法应用到 IoTDB 中。通过这一改动,客户场景中的节点散度从 1 增加到了 5.11,这意味着当单个节点宕机时,多个节点能够分摊待迁移 Leader 的压力,有效避免了 OOM 现象的发生。此外,集成 Copysets 算法还带来了其论文提到的对于数据丢失概率和副本恢复速度的提升。回顾这项工作,最让人印象深刻的是陈老师的深厚算法功底。在我们努力理解论文理论证明的过程中,陈老师补充了论文中遗漏的公式证明。当陈老师引入泊松过程的概念时,我们尚能跟上步伐;然而当陈老师引入指数型随机变量和连续马尔可夫链的概念时,我们只能赞叹:天不生陈老师,飞书 Latex 公式功能万古如长夜了。</li><li>企业版激活:对于企业版软件实现可信授权,我们面临多项挑战:如何在不依赖网络的情况下部署,同时通过硬件绑定来防止许可证的滥用?如何设计一个系统,让激活次数不再受节点数量的限制,以提高整个集群的激活效率?我们还需要引入一系列的使用限制,包括许可证的有效期、节点数、CPU 核心数、序列号和设备数等等。此外,还需考虑如何防止各种潜在的破解尝试,比如回调系统时间、复制文件目录、在使用相同机器码的云平台上部署等,同时保证这些安全措施不会影响到商业用户的使用体验,例如支持非 root 用户激活、提供一键激活功能等。面对这些问题,我们逐一制定了解决方案并加以设计实现。在这个过程中,我和宇衡对各种 Corner Case 进行了深入的分析和讨论,这段经历让我受益匪浅。</li></ul><p>在高可用性方面,我们主要做了以下工作:</p><ul><li>IoTConsensus:在过去的一年里,我们针对基于异步复制思路的 IoTConsensus 共识算法,在性能、稳定性、鲁棒性和可观测性方面做出了显著提升。如今,在线上的大部分场景中,该共识算法已经被优化至接近实时同步的效果,基本上不会再出现因为同步速度跟不上写入速度而导致的 WAL 堆积现象。接着我们开始思考一个命题:在异步复制系统中,不考虑节点宕机等异常情况,是否能在任何写入负载下都保持同步速度与写入速度同步?通过对 MySQL binlog 异步复制等类似场景的观察,湘鹏和我通过排队论的论证和性能实测发现,这个假设是错误的。这一发现促使我们开始进一步探索和设计基于操作变更到状态变更的共识算法。尽管理论上 Leader 侧的 WAL 堆积问题似乎无解,但在实际工程应用中,我们找到了解决办法。我们不仅在多个方面迭代优化以减少 WAL 堆积的可能性,还特别总结了导致 WAL 堆积的八大潜在原因及其解决策略。目前,我们团队已有许多成员能够独立地诊断并解决这一问题,有效地消除了这一单点瓶颈。</li><li>RatisConsensus:今年,我们对 Apache Ratis 社区做出了显著贡献,包括引入了基于 Read-Index 和 Lease Read 的线性一致性读功能,以及若干状态机易用的 API。我们还提高了 Snapshot 传输的稳定性,并提交了超过 30 个 patch,涵盖了各种 bug 修复。除此之外,宋哥不仅多次担任 Ratis 社区的 Release Manager,近期还荣幸被邀请成为 Apache Ratis 社区的 PMC 成员。宋哥作为目前 Ratis 社区 Top3 活跃的开发者,已经时常被我们开玩笑称为 Ratis 社区 Vice PMC Chair 了。</li><li>共识层:去年,IoTDB 共识层参考了 OSDI 2020 Best Paper Delos 的思路进行了设计和实现,支持了多种具有不同一致性和性能特性的共识算法。今年,我们在性能与一致性级别这两个维度上对其支持的不同共识算法进行了深入的对比分析,为 IoTDB 的实施及用户在选择共识算法时提供了重要参考。我们还广泛调研了多种数据库的共识算法实现,通过文档阅读、代码走读和性能实测等多种方法,从共识算法的功能和性能开销等多个角度进行了细致地对比,并取其精华,去其糟粕。此外,今年我们在一些内存紧张的特殊场景下,发现 IoTConsensus 可能会出现副本不一致的问题。经过排查,我们认识到问题并非出在共识算法本身,而是由于状态机执行的不确定性导致的。虽然理论上根据 RSM 模型,所有副本应当达到一致状态,但在实际工程实践中,许多问题都可能使得 RSM 模型不完全适用,比如 Leader 的磁盘写满而 Follower 的磁盘未写满,可能引发执行的不确定性。针对这一问题,我们咨询了曾在 OB 工作的剑神,并在知乎上发问探寻大佬们的解决思路。收到的许多反馈都倾向于“Fail Fast”的处理原则,这可能是因为对许多 TP 系统而言,一致性比可用性更为重要。然而,对于时序场景,可用性往往比短暂的不一致性更加重要。因此,我们认为在遇到此类问题时直接退出进程并不是一个合适的解决方案。为此,我们通过在共识层捕获此类异常并采取有限重试的策略,以避免让业务感知到这种现象,从而保证了系统的高可用性和一致性。</li></ul><p>在可观测性方面,我们主要做了以下工作:</p><ul><li>监控面板:今年,我们借鉴了火焰图作者在《性能之巅》中的思路,从用户视角和资源视角出发,构建并完善了四个监控面板,共计近四百个 panel。这些面板的建设旨在提供全面的性能监控和分析能力,帮助我们更有效地诊断和解决性能问题。首先,我们设立了 Performance Overview 面板,该面板汇总了集群信息,不仅能帮助我们判断性能瓶颈是否存在于 IoTDB 中,还能进一步拆解并统计不同类型请求的延迟分布,从而精确定位到 IoTDB 内部读写流程的具体瓶颈环节。其次是 System 面板,它聚焦于系统资源,包括网络、磁盘、CPU、线程池利用率、JVM 内存和 GC 等多个维度的监控数据。这个面板为系统资源瓶颈的分析提供了丰富的数据支持,使我们能够从资源层面进行深入分析。接下来,我们还有包含集群节点状态、分区信息等的 ConfigNode 面板,以及涵盖存储、查询、元数据、共识和流计算等引擎监控的 DataNode 面板。这两个面板从不同角度提供了 IoTDB 集群的详尽状态和性能信息,为我们提供了全面的监控视图。在这个过程中,我们团队中也涌现出了包括吾皇,彦桑在内的多位 Grafana 艺术家。他们运用 Grafana 的高级功能,创造了许多既美观又实用的监控面板,所有这些都是各位艺术家精心设计的作品。</li><li>监控模块:在过去一年中,随着 IoTDB 各模块可观测性的显著提升,监控指标数量从 100 多个增加到了 900 多个。尽管监控指标数量增加了近 10 倍,但监控模块在火焰图中的 CPU 开销却从 11.34% 下降到了 5.81%,实现了显著的开销节省。这一成就主要归功于俊植、洪胤和我对监控模块的持续迭代和优化。我们不仅对 IoTDB 自身的监控框架进行了大量优化,还结合了 Micrometer 和 Dropwizard 这两个 Metric 库,通过白盒调参或自研选择了对写入操作最友好的实现方式,并针对不同监控指标类型进行了精细化管理。此外,今年雨峰、洪胤和我还持续完善了线上 IoTDB 的巡检文档、告警文档以及面板快照的导出方法等,进一步提升了运维工作的效率和便捷性。通过整个团队一年的共同努力,我们的监控模块不仅大幅提高了问题排查和性能调优的效率,而且已经成为运维 IoTDB 不可或缺的工具。现在我也可以非常自豪地说,IoTDB 现在的可观测性水平已经接近 2022 年暑假我在 PingCAP 实习时体验到的 TiDB 的可观测性水平,在时序数据库中也处于领先地位,这对于我个人和我们组来说是一个巨大的成就。</li><li>日志精简:今年,我们注意到 IoTDB 线上环境中日志打印量较大,这在一定程度上影响了问题排查的效率。随着监控面板的日益完善,许多原本需要通过日志记录的性能统计信息已经能够通过监控模块以更高的信息密度进行记录,这使得部分日志变得不再必要。因此,吾皇和我针对 36 个用户和测试场景进行了深入的日志挖掘分析,筛选出了 62 条出现频率较高的日志记录。经过与各模块负责人的逐一讨论,我们对其中 23 条日志进行了降级(例如从 info 降至 debug)或直接删除的优化处理。此外,团队内部就如何打印性能调优、系统关键行为、SQL 执行错误等异常情况的日志达成了共识。通过这次日志精简工作,在不同场景下我们总共减少了约 37% 到 74% 的日志打印量,取得了明显的效果。其实这项工作可大做可小做,但我们还是非常认真地编写了日志分析脚本进行分析,并进行了量化的数据统计和效果预估。完成这项工作后,有一次我和在北大读博做可观测性研究的张先生闲聊,居然发现我们的工作思路与他们领域内腾讯和中山大学在 2023 ICSE 上发表的顶会论文 LogReducer 非常相似。这种巧合让我感到非常有成就感。我们的工作不仅提升了 IoTDB 的运维效率,还与学术前沿领域的研究工作不谋而合,证明了我们的方向和方法是具有前瞻性和实际应用价值的。</li></ul><p>在性能优化方案,我们主要做了以下工作:</p><ul><li>某知名测试场景性能调优及打磨:今年后半年,我和刚上博一对 IoTDB 几乎 0 基础的谷博共同投入到了某知名测试场景的瓶颈分析、性能调优和内核迭代中。在短短三个月的时间里,谷博迅速成长为一个具备系统思维和深度 IoTDB 调优能力的专家。我们的努力最终获得了显著成果,不仅在该测试场景中取得了第一名的成绩,还通过了第三方的评测。这一成就不仅证明了 IoTDB 1.x 架构的出色性能,也让我们对于 2024 年能够实现更进一步的成绩充满期待。</li><li>写入性能优化预研:IoTDB 之前主要集中在列式写入接口的性能迭代,而对行式写入接口的关注不足。鉴于今年许多用户由于各种原因必须使用行式接口,我们迫切需要对行式写入接口进行深入的瓶颈分析和性能优化。借助于我们目前的可观测性能力,以及对各种性能分析工具(如 JProfile、Arthas)的熟练使用,旭鑫和我对可能的性能提升方案进行了大量的 demo 级别预研。针对典型场景,我们已经找到了 5 个主要的优化点,预计完成这些优化后性能将提升一倍以上。当然,性能优化是一项需要持续投入的工作。当把目前发现的主要优化点做进去后,我们也会基于新的 codebase,继续探索新的瓶颈和优化方案。在这个过程中,我们意识到最重要的是积累理论建模能力和系统思维。如何针对任何系统分析当前的瓶颈并提出有效的优化方案,成为了我们在这项工作中积累的最宝贵财富。</li></ul><p>在技术支持方面,我们主要做了以下工作:</p><ul><li>IoT-Benchmark 基准测试工具的发展:IoT-Benchmark 在过去一年中实现了显著的功能提升,特别是在写入能力(跨设备写入)、查询能力(align by device/desc/limit 查询)和元数据建模能力(支持不同 TagKey 层级设置 TagValue 个数)方面。通过持续的迭代更新(50+ commits),我们不仅增强了工具的功能和稳定性,还吸引了其他时序数据库社区的贡献者,如 CnosDB 的开发者就在最近为我们贡献了 CnosDB Client Driver 的代码。我们期待 IoT-Benchmark 能够成为时序数据库领域内公认的基准测试工具,为不同的时序数据库提供一个公平竞技的平台。</li><li>POC:今年我们组参与了 10+ POC 项目,覆盖了海、陆、空、天等多个领域,并成功部署上线了 95 节点的 IoTDB 集群,实现了 62.6 GB/s 的最大吞吐量和 0.8 以上的集群线性比。参与这些带有挑战性的项目并最终成功落地还是非常让人有成就感的。</li><li>DBA 宝典:在乔老师的带领下,我们逐步构建了面向 IoTDB 的 DBA 宝典。通过梳理异常排查方案和问题导图,我们为 33 个常见问题提供了原因分析和解决策略。DBA 宝典的存在大大降低了实施团队处理异常的难度,有效减轻了产研团队的 Oncall 负担。</li><li>Oncall:今年,我个人承担了组内 80% 以上的 Oncall 工作,这不仅是一次对个人能力的极大考验,也是一次成长和学习的机会。通过不断地思考和解决问题,我对 IoTDB 的各个模块有了更深入的了解,并明确了可观测性建设的推进思路。值得一提的是,尽管项目数量还在增加,我的 Oncall 效率已经显著提升,感受到的压力也在逐渐减轻,这与 DBA 宝典的不断完善和实施团队技术支持团队的建立息息相关。</li></ul><p>在技术沉淀方面,我们主要做了以下工作:</p><ul><li>技术工具:今年我们梳理了常用的 JDK 和 Linux 命令,也用熟了问题排查工具 JProfile 和 Arthas。回想之前看一个 Runtime 的值还需要使用 UDF 去 hack,现在我们直接用 Arthas 就可以了,技术工具的进步极大地提升了我们的生产力。在性能调优方面,除了常见的 JProfile 线程耗时分析和 Arthas 火焰图,权博带领我们探索了 Intel vTune 工具,用于观测高性能机器上的跨 NUMA 访问比例和 CPU 前后端执行效率等。随着 IoTDB 性能优化进入深水区,需要不断将硬件性能进一步压榨,学会使用这些原本 HPC 才可能需要的工具也就非常重要了。</li><li>论文讨论班:今年我们组组织了 6 次工程讨论班和 6 次论文讨论班,对 6 个方向的共 15 篇论文进行了分享介绍,其中一些论文已经提供了写入性能的优化思路并 demo 实测有效。这中间最让我印象深刻的还是旭鑫的存算分离讨论班,我们对若干友商的云服务版本进行了计价统计,发现某些号称云原生时序数据库的系统定价显著高于其他时序数据库,我猜测是因为系统架构用了 EBS 而非对象服务吧,那么高成本就只能让用户买单了。</li><li>JVM:今年我们对 JVM 有了一些深入的探索和技术沉淀。俊植和我细致调研了 Java 的内存分类和观测手段,通过使用 NMT 等工具,我们发现堆外内存分类居然有 19 种之多,这是我在外面的八股中从没看到的结论。在 GC 方面,俊植和我不仅完善了 GC 的可观测性指标,例如不同 GC cause 的次数和耗时以及 GC 占据 Runtime 的比例等等。我们还针对 JDK 8/11/17 的默认 GC 算法 PS 和 G1,分析学习其原理并列举其所有可调参数,搜索优质 GC 调优博客并积累 GC 调优经验。目前我们已经基本具备了对 GC 深度调优的能力,在 GC 严重场景通过调优甚至能带来 60%+ 吞吐的提升,今年我们也会不断细化沉淀这里的方法论并择机分享。在向量化 API 方面,今年旭鑫实测了 JDK21 的 Vector API,在部分场景下能够带来最大 13.5 倍的性能提升,这也是 IoTDB 未来进行性能演进的技术储备之一。</li><li>IoTDB 磁盘文件地图:今年我们参照 Oracle/IBM 等数据库绘制了 IoTDB 的磁盘文件地图。通过该地图,我们不仅发现了一些可以潜在优化的点,还理顺了不同模块落盘文件的逻辑关系。</li><li>压缩算法性能测试:今年我们针对若干用户场景的真实数据进行了压缩算法的对比测试,发现大多数场景下 LZ4 相比 Snappy 有更好的压缩效果,这也促使了 IoTDB 默认压缩算法的更改。</li><li>难点预研:今年我们组还针对多个复杂问题,如共识组数与集群性能、线程模型、集群滚动升级方案和大 Text 值类型访存瓶颈优化方案等进行了深入的调研和测试,虽然部分工作尚未得出最终结论,但已经为未来的深入研究奠定了基础。</li></ul><p>今年我在 Apache IoTDB 社区提交并被合并了 119 个 PR, Review 了 387 个 PR。从 PR 数量上来说相比去年和前年有了显著提升,可能是由于更加专注于工作,并且 scope 也在不断扩大吧。此外我也于今年 9 月受邀成为了 Apache IoTDB 社区的 PMC 成员,感谢社区对我的认可。</p><p>因时间所限,我今年在知乎等社交平台的活跃度有所下降。但回顾这一年,我觉得我们团队完成了许多既有趣又深入的工作,并且几乎都有相应的文档沉淀下来。这些宝贵的积累完全可以与业界分享以交流学习。我期待在 2024 年,我们团队能够更频繁地分享我们的技术沉淀,并吸引更多对技术有兴趣的同学加入 IoTDB 社区或我们的实验室进行交流!</p><h1 id="一些感悟"><a href="#一些感悟" class="headerlink" title="一些感悟"></a>一些感悟</h1><h2 id="性能优化:体系结构和操作系统是基本功"><a href="#性能优化:体系结构和操作系统是基本功" class="headerlink" title="性能优化:体系结构和操作系统是基本功"></a>性能优化:体系结构和操作系统是基本功</h2><p>在深入研究和优化数据库系统在各种硬件环境及业务负载下的性能过程中,我越发认识到掌握体系结构和操作系统知识是进行性能优化的基础。今年,我在这两方面补充了许多知识,并阅读了《性能之巅》的部分章节。然而,令人感到有些沮丧的是,随着知识的增加,我反而越来越感觉到自己的无知。但我仍然希望,在 2024 年能够跨越这段充满挑战的绝望之谷,登上开悟之坡。</p><p>对于有意向学习 CMU 15-418 课程的朋友,我非常期待能够一同学习和进步!如果有经验丰富的大佬愿意指导,我将不胜感激!</p><h2 id="GC-算法:追求吞吐还是延迟?"><a href="#GC-算法:追求吞吐还是延迟?" class="headerlink" title="GC 算法:追求吞吐还是延迟?"></a>GC 算法:追求吞吐还是延迟?</h2><p>今年,我们组深入研究了 JDK 的垃圾回收(GC)算法,包括但不限于 Parallel Scavenge(PS)、Concurrent Mark Sweep(CMS)、Garbage-First(G1)和 Z Garbage Collector(ZGC)。我们还对 IoTDB 在相同业务负载下采用不同 GC 算法的吞吐量和延迟性能进行了比较测试,结果表明在不同的负载条件下,各 GC 算法的性能表现排序也有所不同。</p><p>在 GC 算法的选择上,我们面临着内存占用(footprint)、吞吐量(throughput)和延迟(latency)三者之间的取舍,类似于 CAP 定理,这三者不可能同时被完全满足,最多只能满足其中的两项。通常情况下,高吞吐量的 GC 算法会伴随较长的单次 STW 时间;而 STW 时间较短的 GC 算法往往会频繁触发 GC,占用更多的线程资源,导致吞吐量下降。例如,PS GC 虽然只有一次 STW,但可能耗时较长;G1 的 Mixed GC 在三次 STW 中的 Copying 阶段可能造成几百毫秒的延迟;而 ZGC 的三次 STW 时间都与 GC Roots 数量有关,因此 STW 延迟可以控制在毫秒级别。</p><p>JDK GC 算法的发展趋势似乎是在尽量减少 GC 对业务延迟的影响,但这种优化的代价是消耗更多的 CPU 资源(JDK 21 引入的分代 ZGC 有望大幅降低 ZGC 的 CPU 开销)。在 CPU 资源本身成为瓶颈的场景下,使用 ZGC 和 G1 等 GC 算法的吞吐量可能会低于 PS。GC 算法目前的演进具有两面性,例如 Go 语言就由于其默认 GC 与 Java 相比 STW 时间较短而被赞扬,但其 CPU 资源消耗大也会被批评,我们需要根据不同的目标选择合适的 GC 算法。</p><p>然而,GC 算法朝低延迟方向的不断演进仍具有重要意义,因为吞吐问题可以通过增加机器进行横向扩展来解决,而延迟问题则只能依赖于 GC 算法的改进。因此,在调优时应该有针对性,分别针对吞吐和延迟进行优化,而不是同时追求两者。如果追求吞吐量,可以优先考虑使用 PS;如果追求低延迟,可以考虑使用 G1/ZGC,并为之准备额外的机器资源以支付低延迟的代价。</p><h2 id="全局成本:C-C-相比-Java-性能更好?"><a href="#全局成本:C-C-相比-Java-性能更好?" class="headerlink" title="全局成本:C/C++ 相比 Java 性能更好?"></a>全局成本:C/C++ 相比 Java 性能更好?</h2><p>今年,我参与了许多问题修复和优先级排序的工作,同时深入思考了编程语言对软件开发总成本的影响。</p><p>在 PingCAP 实习期间的一次闲聊中,有些同事提出 TiDB 应该用 Rust 或 C++重写,理由是用 Go 语言编写的性能较差。然而,我的 mentor 徐总认为,采用 Go 语言后显著减少了大家的 OnCall 次数,从而节约了大量研发成本。</p><p>从纯技术的角度看,C/C++ 在极限优化下确实能比 Java 更好地发挥硬件特性。但工程开发,尤其是内核开发,不仅仅是技术问题,它更多涉及到软件工程的广泛议题。现实中,我们经常面临着无休止的问题修复和需求实现,性能优化往往未能充分利用硬件能力。我认为,尽管开发团队采用的编程语言可能影响理论上的性能上限,但在大多数工程实践中,项目成功的关键并不仅仅在于将性能优化到极致。更重要的是,在有限资源下如何优先追求满足用户需求的产品特性、如何持续保证产品的稳定性和可维护性、如何提升系统的横向扩展能力、以及如何在现有代码基础上持续进行性能优化。我相信,这些因素比起编程语言的选择所带来的潜在收益要重要得多。</p><p>因此,除了少数极特别的场景(例如追求超低延迟 or 边缘端等),选择一个团队熟悉且学习成本较低的编程语言就足够了。</p><h2 id="工程难题:不是所有技术问题都能够立即找到解决方案"><a href="#工程难题:不是所有技术问题都能够立即找到解决方案" class="headerlink" title="工程难题:不是所有技术问题都能够立即找到解决方案"></a>工程难题:不是所有技术问题都能够立即找到解决方案</h2><p>今年,我们面对并快速解决了许多棘手的问题,但同时也遇到了一些难以快速找到原因的疑难杂症。这些问题涵盖了多个方面,例如 DataNode 进程在 OOM 后仍能响应心跳但无法处理新的读写请求(这是因为 JVM 在 OOM 后随机终止了一些线程,导致监听线程被终止无法响应新连接而心跳服务线程仍在运行),以及 Ratis consensusGroupID 编码错误导致的 GroupNotFound 错误(使用 Arthas 监控后问题消失,我们怀疑这是 JVM JIT 的 bug)等。</p><p>解决这些问题的过程加深了我们对于设计新功能时对各种异常场景的考虑,有效避免了许多未来可能发生的 Oncall 问题。</p><p>在面对问题和解决问题的过程中,我深刻体会到人的认知可以分为四个象限:已知的已知、已知的未知、未知的已知以及未知的未知。其中,最难以应对的是“未知的未知”。我一直在思考工程经验这四个字究竟意味着什么?现在我认为,工程经验的积累不仅意味着将更多的“已知的未知”转化为“已知的已知”,还需要将更多的“未知的未知”变成“已知的未知”,这样才能具有可持续性。</p><h2 id="流程体系:软件开发团队的重中之重"><a href="#流程体系:软件开发团队的重中之重" class="headerlink" title="流程体系:软件开发团队的重中之重"></a>流程体系:软件开发团队的重中之重</h2><p>今年,我深刻体会到了流程体系在构建一个可持续发展的软件开发团队中的重要性。我认识到只有拥有一流的团队,才能够开发出一流的软件。</p><p>在王老师软件工程理念的统筹指导和 Apache 基金会的支持下,我认为我们的产品流程体系已经相对健全,包括但不限于以下几个方面:</p><ul><li>CI/CD:对不稳定的 UT 和 IT 进行持续的修复,确保代码质量和功能稳定性。</li><li>代码质量静态检测:利用 Sonar 等工具持续提升代码质量,确保软件的健壮性。</li><li>Commit 级别的监控:针对不同的用户和测试场景,实现性能和资源使用量的监测,防止出现非预期的产品回退。</li><li>定期封版和发版:对每一项 Release Note 进行逐项测试验证,通过多轮的 RC 版本,不断收敛测试范围,确保成功发布。</li><li>定期的功能和技术评审会议:各模块的核心开发者共同参与,评估产品的功能和技术实现。</li><li>发版问题同步会:确保团队成员对 RC 验证中发现的问题能够快速响应。</li><li>P0 项目支持任务同步会:对重要项目的支持任务进行同步和讨论。</li><li>多层级技术支持团队(L0/L1/L2):根据问题的复杂度,提供分层次的技术支持。</li><li>敏捷开发的支持工具:使用多维表格等工具,支持敏捷开发流程。</li><li>论文讨论班:持续学习和探索行业内的最新研究成果。</li><li>竞品功能和技术分析:分析竞争对手的产品,从而不断优化自身产品。</li><li>安全漏洞感知和修复:及时发现和修补安全漏洞,保证产品的安全性。</li></ul><p>通过在这样的团队中工作,我对如何打造一个可持续的软件工程体系有了更深地理解。</p><h2 id="工作管理:一键生成总结是好是坏?"><a href="#工作管理:一键生成总结是好是坏?" class="headerlink" title="工作管理:一键生成总结是好是坏?"></a>工作管理:一键生成总结是好是坏?</h2><p>随着我们组负责的模块和同学数量的增加,我逐渐发现,仅仅通过飞书文档记录工作内容的做法,虽然实现了工作的“记录”,却缺乏了有效的“管理”。例如,我们组面临的任务琐碎而多样,大家都经常会忘记一些计划中的任务;同时,我们的业务需求变化迅速,虽然大家都在同时推进多项任务,但仍然跟不上需求的变化速度。这就要求我们能够及时调整任务的优先级,以便灵活应对并优先完成 ROI 最高的任务。此外,我们以前的月度总结并没有持续进行,我分析的原因是任务汇总本身就是一种成本,导致月度总结难以持续,从而失去了很多总结沟通的机会。</p><p>为了解决这些问题,我开始学习并使用飞书的多维表格来管理团队的任务。通过多维表格,我们不仅可以清晰地看到每位成员当前的工作任务,还可以在团队会议上根据业务需要灵活调整任务优先级,甚至能够一键生成甘特图来明确不同优先级任务的时间线。在进行每周和每月总结时,我们也能够通过筛选日期快速生成任务汇总。</p><p>一开始,多维表格似乎完美地解决了我们之前的问题。然而,随着时间的推移,我发现这种方式也存在缺陷。由于总结能够一键生成,我不再每周花费一小时来统计和规划我们的周报和下周计划,甚至我们的月度总结也鲜少举办。这反而导致我们的日常开发缺乏规划,显得有些随波逐流。在东哥的点拨下,我重新开始在飞书文档中记录周报,并且连续三个月组织了月度总结会。通过定时的每周和每月汇总与沟通,团队的工作变得更加有序和明确。现在如果让我去说上半年做了什么主要工作,我可能还需要看多维表格筛选半天,但如果问我后 3 个月做了什么,我只需要看每月的月度总结就可以了。</p><p>现在,我们通过多维表格来管理任务的优先级,同时利用飞书文档来汇总周报和月报。通过对我们组流程管理的持续迭代和优化,我意识到有时候追求速度反而会拖慢进度,而适当地放慢脚步思考反而能够使我们更加高效。</p><h2 id="团队协作:分布式系统的高扩展性和高可用性"><a href="#团队协作:分布式系统的高扩展性和高可用性" class="headerlink" title="团队协作:分布式系统的高扩展性和高可用性"></a>团队协作:分布式系统的高扩展性和高可用性</h2><p>在技术方面,我最开始深入了解的就是分布式系统,我一直在学习如何实现系统的高扩展性和高可用性。随着时间的推移,我发现这些分布式系统的理念同样适用于团队协作中。</p><p>为了实现高扩展性,关键在于让所有团队成员并行工作,而不是仅依赖于“主节点”或关键个体,这要求每个成员都能独自完成任务并持续提高自己的工作效率,这样才能提升整个团队的整体性能。同时,团队还需要能够支持成员的动态调整,如新成员的加入和旧成员的离开,确保团队结构的灵活性和适应性。</p><p>为了满足高可用性,就需要在关键任务或数据上实施冗余策略,以防止暂时的不可用状态对团队工作造成影响。这可能意味着需要在某些区域投入额外的资源,确保信息、知识或工作负载能够在多个成员之间共享,保持一致性。</p><p>这一年来,我们团队负责的模块不断增加,但每个模块都至少有 3 位以上的成员熟悉,上半年我的感受是每天从早忙到晚,连半天假都请不了。但到后半年我感觉偶尔请一两天假也不会对外产生可感知的影响了,这代表了我们组的高可用性出现了显著提升。针对我们组负责的模块,我们维护了详尽的功能和技术设计文档,以及改进措施的追踪记录,这不仅加速了新成员的融入,也保持了团队知识的一致性。此外,我们通过引入自动化工具,如飞书激活解密机器人、各类测试脚本、木马清理脚本等,有效提升了团队的工作效率,体现了我们组在高扩展性方面的进步。</p><p>希望 24 年我们组能在高扩展性和高可用性方面继续取得显著进步,为实现更加高效和稳定的团队协作模式而不断努力。</p><h2 id="时间管理:可观测性"><a href="#时间管理:可观测性" class="headerlink" title="时间管理:可观测性"></a>时间管理:可观测性</h2><p>今年我们组的主要工作之一便是打造 IoTDB 的可观测性,目前已经显著提升了问题排查和性能调优的效率,成为线上运维 IoTDB 的必备工具。回到时间管理上,我发现可观测性的很多理念也同样适用。</p><p>随着组内同学越来越多,scope 越来越大,沟通协调的成本已经不容忽视,我自己的时间越来越不够用,逐渐成为了单点瓶颈。在向东哥请教后,我开始按照半小时为单位记录自己每天的工作内容,并定期反思每半小时的工作是否满足了高效率。</p><p>通过整理自己工作日一天 24 小时的时间分配,我发现自己实际可用于工作的时间并不超过 11 小时,因为每天基本要包括睡眠 8 小时、起床和就寝的准备及洗漱时间 1 小时、通勤 1 小时、餐饮和午休 2 小时以及运动 1 小时(有时会被娱乐消遣取代)。11 月份的数据显示,我的平均工作时间约为 10 小时(没有摸鱼时间),已经接近饱和每天都十分充实。这促使我思考如何提升自己和团队单位时间的工作效率,比如在协调任务时明确目标和截止日期,实行更细致的分工以解决我作为单点瓶颈的问题等。通过这些措施,到了 12 月份,我的平均每日工作时间减少到了 9.5 小时,而感觉团队的整体产出反而有所提升。不过,到了 1 月份,由于一些新的工作安排尚未完全理顺,我的平均工作时间又回升到了 10 小时,这需要我持续进行优化。</p><p>总的来说,定期统计和评估自己的时间分配及其 ROI,我觉得对于提高工作效率具有重大意义。</p><h2 id="心态变化:职业发展和生活的关系"><a href="#心态变化:职业发展和生活的关系" class="headerlink" title="心态变化:职业发展和生活的关系"></a>心态变化:职业发展和生活的关系</h2><p>在经历了半年学生生活和半年职场生活后,我对职业发展与生活的关系有了新的认识和感悟。之前我是那种职业动机极强以至于生活显得相对单调的人。对我来说,除了那些能带给我快乐的少数娱乐活动外,生活中的许多琐碎事务如做饭洗碗,都被视为时间的浪费,不如将这些时间用于创造更多的价值。在地铁和高铁上不学习,我也会感到是对时间的浪费。我认为既然职业发展对我而言十分重要且能从中获得快乐,那么我应该将所有可用的时间都投入其中。</p><p>然而今年我的心态发生了显著的变化。我逐渐意识到,即使职业发展很重要,即使我能从中获得快乐,它也只是生活的一部分。我开始挤出更多的时间来陪伴家人,也开始与各行各业的老朋友新朋友进行交流。我不再认为生活中的全部琐事是对时间的浪费。我更加注重如何在有限的工作时间内提升效率完成超出预期的工作,而不是简单地用更多的时间去完成这些工作。</p><p>这种心态的转变对个人来说不一定是坏事。如果我的心态没有这些变化,可能会投入全部可用的时间于职业发展中,但这样的状态不确定能够持续多久。如果我的心态发生了变化,那我可能会更加注重工作效率和生活体验感,也许能达到职业发展和生活的双赢。</p><p>总的来说,每个人在不同的年龄阶段对这种平衡的感悟都会有所不同。我目前的想法是,顺应我们不断成熟的心态,选择让我们感到最舒适的状态,这不仅能让我们的心理状态更加健康,也能更好地平衡职业发展和生活的关系。</p><h2 id="任务分配:兴趣驱动,效率优先"><a href="#任务分配:兴趣驱动,效率优先" class="headerlink" title="任务分配:兴趣驱动,效率优先"></a>任务分配:兴趣驱动,效率优先</h2><p>马克思指出社会分工是生产力发展的结果和需要,这种分工具有历史的必然性。对于创业公司而言,追求指数型增长是生存和发展的关键,因为即使是线性增长,在激烈的市场竞争中也可能面临被淘汰的风险。如何实现这种增长,是一个复杂且多维的问题,我在这里只从任务分配的角度分享一些个人理解。</p><p>在创业团队中,自上而下的任务繁多,而自下而上每个成员的兴趣和专长也各不相同。如何最大化团队的价值?关键在于沟通和了解每个成员的兴趣点和擅长点,尽可能让他们大部分时间都在做自己感兴趣和擅长的工作。虽然总有一些额外的任务需要团队共同承担,但是优先保证成员大部分时间能够从事自己感兴趣且擅长的工作是非常重要的。只有这样,每个人才会带着兴趣和专长去挖掘提升效率的可能,从而可能产生指数级的复利效应,并最终影响整个团队的产出。在现有的权力结构体系下,无论是企业还是更广泛的社会,我觉得自上而下的人员任命也基本遵循这一原则。</p><p>基于这样的理解,我在分配我们组的任务时,尽可能根据我对团队成员的了解,分配给每个人感兴趣和擅长的任务,并与大家一起探索提升效率和价值的途径。这一年里,我一直在寻求任务分配的全局最优解,并坚信找到合适的人做他们感兴趣的工作,能够产生的复利远远超过随机或平均分配工作所能带来的效益。</p><h2 id="个人发展:更广还是更深?"><a href="#个人发展:更广还是更深?" class="headerlink" title="个人发展:更广还是更深?"></a>个人发展:更广还是更深?</h2><p>在创业团队的初期阶段,各方面的需求和缺口(技术,市场,运行,销售等等)很多。从公司的角度看,这就非常需要大家能够主动承担额外的职责。从个人的角度看,我们不论是承担更多的职责还是在自己所做的工作上做得更突出,都是对公司的贡献,也都能收获成长。然而人的精力总是有限的,一个人不可能完美地做完所有事情,总是要把有限的精力投入到有限的事情上。面对这样的环境,每个人都面临着如何在工作的广度和深度之间做出选择的问题。</p><p>对于这个问题,我今年有了一番思考和探索。个人觉得对于职场新人来说,寻找一个自己擅长且能从中获得乐趣和成就感的领域至关重要,并且需要与领导进行积极的沟通,以获得相应的支持和资源。每个人的选择可能不同,领导的任务就是在团队成员之间找到一个平衡点,不仅能够完成所有任务,还要尽量让每个人能在其擅长的领域内发挥最大的复利效应。</p><p>就我个人而言,我目前更倾向于追求工作的深度,希望能够深入学习并掌握我目前尚不擅长但团队需要的技术知识。通过专注于深度,我希望能够在专业领域内取得更大的进步,并为团队带来更具影响力的贡献。当然,这也并不意味着就完全抛弃广度,随着时间不断推移,我在广度上投入的精力也会越来越多。</p><h2 id="协作理念:以人为本,真诚坦率"><a href="#协作理念:以人为本,真诚坦率" class="headerlink" title="协作理念:以人为本,真诚坦率"></a>协作理念:以人为本,真诚坦率</h2><p>今年,通过阅读《跳出盒子——领导与自欺的管理寓言》和李玉琢老师的《办中国最出色企业:我的职业经理人生涯》,我对管理有了初步的理解和感悟。这两本书分别代表了不同的管理理念,一种强调以人为本,另一种则是以结果为导向的雷厉风行。对于我目前的心态而言,我认同后者的评价体系,但从个人性格上我自己的风格更像前者。</p><p>在日常的产品迭代和团队管理中,我始终认为把人放在第一位是非常重要的。通过团结所有可以团结的力量,关注每个成员的工作态度、能力、心理状态以及需求和期望,找到大家适合的方向,往往能比反复推动大家完成不情愿的工作更加高效。</p><p>当然,在工作过程中难免会遇到与某些人的争执和冲突。面对这些情况,我常采取的做法是换位思考。我会设身处地地想,如果我是对方,我是否也会做出同样的选择?如果答案是肯定的,那么这往往是角色之间的冲突,而非个人情感的问题,我就不会在情感层面上过多消耗精力。如果答案是否定的,我则会进一步探索解决分歧的方法。我是一个性格相对温和的人,我通常不倾向于与人争执,而是尽可能地通过和平的方式解决问题。今年,我几乎都是这样处理冲突的。</p><p>然而,我也逐渐意识到,过分的忍让并不会赢得他人的尊重和理解,反而会被得过且过。有些原则和理念是需要坚持的底线,绝不能妥协。希望在未来的一年里,我能够在保持真诚坦率的同时,也能够坚持自己的原则和底线。</p><h2 id="人生成就:小赢靠智,大赢靠势"><a href="#人生成就:小赢靠智,大赢靠势" class="headerlink" title="人生成就:小赢靠智,大赢靠势"></a>人生成就:小赢靠智,大赢靠势</h2><p>今年在工作之余也读了《新程序员》杂志,深入了解了很多大佬的成长经历,也获得了不少启发。一个很深刻的感悟还是江同志的一句话:一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的进程。</p><p>自从 ChatGPT 爆火以来,周围已经涌现出许多彻底成功的案例,这些故事不仅激励着我,也让我对未来充满了好奇和期待。尽管对于自己未来的方向,我目前还没有一个清晰的规划,甚至只能对未来 1 到 2 年内的工作做出一些预测,3 年后会做什么我还没有确切的答案。</p><p>但在这样的不确定性中,我坚信的一点是,只要相信自己当前的工作富有意义和前景,并且能够在其中找到快乐,那么就值得坚持下去,全力以赴。关于未来命运将我们带往何方,或许可以交给时间和命运去安排。在这个快速变化的时代,保持学习和成长的心态,积极面对每一次机遇和挑战,可能就是我们能做的最好的准备了。</p><h1 id="来年展望"><a href="#来年展望" class="headerlink" title="来年展望"></a>来年展望</h1><p>经过一天多的思考,我终于完成了今年的年终总结。回顾这一年,我在技术和管理方面取得了一些进步,但同时也深刻意识到,在让企业成功的方方面面,我还有太多不了解不擅长需要学习的地方。</p><p>展望新的一年,我为自己和我们组设定了以下几点期望:</p><ul><li>做深:希望能够系统地学习体系结构、操作系统以及《性能之巅》中的相关知识,并将这些知识应用到实践中,不断提升 IoTDB 的技术水平和性能表现。</li><li>做广:除了在分布式和可观测性方面的投入之外,希望能深入学习时序数据存储引擎和流处理引擎的知识,向优秀的同事和业界前辈学习。</li><li>做好:持续努力提高 IoTDB 的稳定性、鲁棒性和易用性,确保它成为用户信赖的时序数据库。</li><li>做响:寻找机会将我们团队的工作成果和经验分享给外部,与更多的同行进行技术交流,不断增强 IoTDB 的知名度和技术影响力。</li></ul><p>最后,感谢您的阅读。欢迎各位读者批评指正。</p><p>在新的一年里,祝愿大家身体健康、家庭幸福、梦想成真。希望我们都能在新的一年中取得更大的进步!</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>兜兜转转又是一年,不知不觉 2023 已经结束。回想自己过去一年的成长与感悟,依然觉得是收获满满。今年工作之后闲余时间相比学生时代少了许多,</summary>
<category term="IoTDB" scheme="https://tanxinyu.work/tags/IoTDB/"/>
<category term="年终总结" scheme="https://tanxinyu.work/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>2023 IoTDB 用户大会分享:如何用 IoTDB 监控工具进行深度系统调优</title>
<link href="https://tanxinyu.work/2023-iotdb-submit/"/>
<id>https://tanxinyu.work/2023-iotdb-submit/</id>
<published>2023-12-07T10:03:00.000Z</published>
<updated>2023-12-07T14:29:43.476Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2023 年 12 月 3 日,IoTDB 一年一度的 <a href="https://mp.weixin.qq.com/s/d5GR8OPoReMN_lBBnOFuPg">用户大会</a> 成功举办。</p><p>在本次大会中,我有幸作为讲师之一做了《优其效:如何用 IoTDB 监控工具进行深度系统调优》的分享,系统介绍了 IoTDB 这一年来在可观测性方面的进展,并展示了它如何显著提升我们的性能调优和问题排查效率。</p><p>本博客将通过文字和图片的方式展示我的分享内容。</p><p>我们在可观测性方面做的工作后续也会有更多的博客输出出来,敬请期待!</p><h2 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h2><p><img src="/2023-iotdb-submit/1.png" alt></p><p>大家好,我是来自天谋科技的谭新宇,接下来我为大家分享的主题是”如何用 IoTDB 监控工具进行深度系统调优”。</p><p><img src="/2023-iotdb-submit/2.png" alt></p><p>本次分享分为 5 个方面,首先我们将介绍数据库系统的用户服务和架构演进挑战,这些挑战的本质都是如何去提升效率;接着我们会对 IoTDB 可观测性的发展进行概览,主要包括 Logging,Metrics 和 Tracing 三个方面;然后我们会介绍一下 IoTDB 的监控模块,其构建主要参考了火焰图作者著作《性能之巅》的思路,即从负载视角和资源视角两个维度对系统进行观测;最后我们会概述一下 IoTDB 的 4 个监控面板并着重做一些性能调优和问题排查的典型案例分享。</p><p><img src="/2023-iotdb-submit/3.png" alt></p><p>首先先来分析一下数据库系统的用户服务和架构演进有着怎样的挑战。</p><p><img src="/2023-iotdb-submit/4.png" alt></p><p>对于用户服务,主要存在以下三个挑战:</p><p>第一是如何快速找到业务场景的瓶颈点?系统的性能存在木板效应,会受限于系统最慢的模块,比如某节点的 CPU 和磁盘还没有打满,但网卡已经被打满了,此时增加写入负载也不会获得任何性能上的提升。</p><p>第二是如何对业务场景进行针对性调优?不同硬件环境和业务负载的排列组合会使得很多默认参数并不是当下最优的值,针对这个问题,一种理想流派是像 ottertune 一样使用机器学习的方式去找到最优的参数组合,另一种更为实际的流派则是能够对系统进行白盒调优。</p><p>第三是如何形成可扩展的调优体系?对于性能调优这个工作,其实非常容易形成马太效应,即越会调优的人越容易被分配更多性能调优的工作,虽然他会越来越能调优,但这也容易形成单点瓶颈,导致调优工作横向扩展不起来,这样其实是不利于整个产研团队和实施团队的共同成长的。因此需要针对调优这项工作形成可复制的调优方法论,大家共享互补调优的知识,一起成长。</p><p><img src="/2023-iotdb-submit/5.png" alt></p><p>对于架构演进,也主要存在以下三个挑战:</p><p>第一是如何确定典型业务场景?性能优化需要结合场景谈论才有意义,而一个系统往往也会有很多用户场景,这就需要从中抽象出来通用普适的典型场景并总结他们的典型特征。比如硬件环境到底是 4 核 8G 还是 64 核 128G ,业务需求到底是追求低延迟还是高吞吐等等。</p><p>第二是如何进一步演进典型业务场景下的性能?任何系统在特定业务场景下都存在进一步性能演进的可能,我们需要在寻找瓶颈的过程中区分出来哪些是工程问题(比如 GC 参数调优,代码写的冗余),哪些是学术问题(比如针对 IoTDB 特定的时序场景,有些数据库原理的 trade-off 发生了变化,这个时候就可以结合场景做一些更针对的设计,IoTDB 近几年在 Fast 和 ICDE 等顶会上发表的论文都是沿着这个思路去设计的),区分出这两个问题之后就可以利用不同的思路去并行协作优化性能了。</p><p>第三是如何确保性能优化的 ROI 最大?对于一个系统怎么优化,收集一圈能够得到一大堆思路,到底哪些效果会好,哪些效果会差?我们需要能够精确评估一个优化的正向作用和负面影响,并能够量化排列优先级,这样才可能将有限的资源持续投入到 ROI 最大的性能优化上,坚持做最正确的选择。</p><p><img src="/2023-iotdb-submit/6.png" alt></p><p>分析完了挑战,其实我们也都清楚了可观测性对于解决这些挑战的重要性。那么接下来我们介绍一下 IoTDB 的可观测性发展概览。</p><p><img src="/2023-iotdb-submit/7.png" alt></p><p>随着分布式架构成为主流,可观测性这一名词逐渐被大家频繁提及。学术界一般会将可观测性分为三个更具体的方向进行研究,分别是 Logging,Metrics 和 Tracing。</p><p>Logging 的职责是记录离散事件,从而使得事后可以通过这些记录来分析程序的行为。</p><p>Metrics 的职责是将不同类型的消息分别进行统计聚合处理,从而能够对系统进行持续的监控和预警。</p><p>Tracing 的职责是记录完整的调用轨迹,这就包含了服务间的网络传输信息与各个服务内部的调用堆栈信息。</p><p><img src="/2023-iotdb-submit/8.png" alt></p><p>IoTDB 自诞生时就使用了 Logback 框架来管理日志,随着版本的不断迭代,目前已经将不同级别和模块的日志拆分成了不同的文件便于检索。</p><p>虽然这些日志很重要,但它所有的信息都是离散的。如果要对某一类的信息进行一些汇总聚合统计,比如统计一段时间的平均刷盘点数,就需要首先 cat 文件,接着再 grep 过滤出同一类型的日志,然后还要写脚本来计算次数,平均值之类的,这就非常繁琐。</p><p>很自然的这就需要引入 Metrics 了。</p><p><img src="/2023-iotdb-submit/9.png" alt></p><p>IoTDB 在 0.12 版本就开始设计开发 Metrics,但从 1.0 版本之后才开始投入大量精力打磨 Metrics,到了 1.3 版本 Metrics 已经基本打磨的差不多了。</p><p>我们用了 Micrometer 和 DropWizard 的算法库来作为监控指标的类型支撑,具体的存储可以导出到 Prometheus 或者 IoTDB 中,可视化目前主要是在用 Grafana 工具。</p><p>右边贴了一张我们监控面板的图,还是非常漂亮的,后面会进一步介绍。</p><p>有了 Metric 之后,我们可以统计同一类请求的聚合信息,例如平均值,P99 值等等。这其实已经能够解决 90% 以上的问题,但对于剩下 10% 的问题,比如海量小查询和一个大查询并发执行时,大查询的执行耗时会被吞并,从而无法体现在 Metrics 中。此时我们就需要具备单独观测一条请求完整调用链路耗时的能力。</p><p><img src="/2023-iotdb-submit/10.png" alt></p><p>为了满足这种需求,今年我们也启动了对 Tracing 工作的研究,我们用 OpenTelemetry,ElasticSearch 和 Grafana 搭建了 Tracing 系统。</p><p>比如右图对于 show region 请求的调用链路,我们可以在 Grafana 中展示这个请求跨进程通信时不同进程内部调用栈的详细耗时信息,这对于慢查询等场景的性能排查效率会有显著提升。</p><p><img src="/2023-iotdb-submit/11.png" alt></p><p>总体而言,IoTDB 的可观测性能力在今年发生了质变。我们有信心也非常欢迎我们的用户朋友前来体验。</p><p>接下来我会着重介绍一下 IoTDB 的监控模块:</p><p><img src="/2023-iotdb-submit/12.png" alt></p><p>对于监控模块而言,它的灵魂就是他拥有哪些监控指标。</p><p>这里我们参照火焰图作者著作《性能之巅》的思路。从负载分析和资源分析这两个相反的角度去互补推进监控指标体系的建设。</p><p><img src="/2023-iotdb-submit/13.png" alt></p><p>对于自顶向下的负载视角:</p><p>我们对客户端写入 IoTDB 的流程进行了拆解。对于每个 IoTDB 的连接,当它将请求交给到 IoTDB 执行时,该连接被视为忙碌状态;当它在客户端攒批等待或者向服务端传输时,该连接被视为闲置状态。通过这种区分,我们能够对瓶颈是否在 IoTDB 内部有一个评估。比如每次连接繁忙都是 10ms,之后却要闲置 5 分钟,那基本瓶颈就不在 IoTDB 端了。</p><p>如果发现连接繁忙的时间要更大,要如何进一步去寻找瓶颈呢。我们将 IoTDB 的写入请求延迟进行了拆解,将写入流程分成了若干阶段,并对一般情况下更为耗时的阶段进行了更细粒度的拆分,从而能够确保发现瓶颈出现在哪个模块。比如调度执行阶段一直存在远程转发,那就需要去排查客户端的分区缓存是否失效。</p><p>总之,通过这种自顶向下的分析,我们能够找到系统当前的瓶颈是在哪些模块。</p><p><img src="/2023-iotdb-submit/14.png" alt></p><p>对于自底向上的资源视角:</p><p>我们主要从 4 个维度进行了考虑:</p><p>在磁盘方面,我们希望我们要比 Linux 的常用磁盘监控命令 iostat 更为丰富,比如除了磁盘利用率,吞吐 iops 之外,我们还想统计进程级别的磁盘读写情况和 page cache 的使用情况。</p><p>在网络方面,我们希望我们要比 Linux 的常用网络监控命令 sar 更为丰富,比如除了网络吞吐和收发包的速度之外,我们还想统计进程级别的连接个数等等。</p><p>在 CPU 方面,我们不仅要统计操作系统和进程的 CPU 利用率,还想统计 IoTDB 进程内部不同模块不同线程池的 CPU 利用率,也还想统计进程内部线程池的关键参数。</p><p>在 JVM 方面,我们不仅要对堆内堆外的内存大小做观测,对不同状态的线程个数做观测,还想对 GC 做更细致的观测。</p><p>总之,通过这种自底向上的分析,我们能为很多模块的瓶颈原因提供思路</p><p><img src="/2023-iotdb-submit/15.png" alt></p><p>那到了 1.3.0 版本,我们前面提到的监控指标都已经实现了,那么监控模块对于性能的影响到底大不大呢,线上敢不敢打开呢?</p><p>其实这块我们也在持续的做性能优化,尽管 IoTDB 监控指标的个数已经从 1.0.0 版本的 134 涨到了 1.3.0 版本的 905,增加了接近 8 倍,但监控模块 CPU 的开销反而从 11.34% 降低到了 5.81%,减少了近 50% 。其对于读写性能的影响也从 7% 以内降低到了 3% 以内。</p><p>因此,大家可以放心的开启监控模块,它对于系统运维的收益绝对远远大于这一点点性能损耗。</p><p><img src="/2023-iotdb-submit/16.png" alt></p><p>基于这些监控指标,接下来我们简单介绍一下 IoTDB 的监控面板:</p><p><img src="/2023-iotdb-submit/17.png" alt></p><p>主要分为四个监控面板:</p><p>分别 Performance Overview,System,ConfigNode 和 DataNode 面板。</p><p>下面将给出这些面板的示例:</p><p><img src="/2023-iotdb-submit/18.png" alt></p><p>对于 Performance Overview 面板:</p><p>它汇总统计了集群的基本信息,例如集群大小,总时间序列个数,总写入吞吐等等。</p><p>它还以延迟拆解的方式展示了客户端写入不同阶段的耗时统计,辅助定位瓶颈存在于哪个模块。</p><p><img src="/2023-iotdb-submit/19.png" alt></p><p>任何一个子面板我们都写了详细的注释,比如左图这个面板就展示了不同接口的耗时统计。</p><p>同时我们也可以在一个面板中同时查看多个节点的监控数据,便于定位相同时间不同节点的状态。</p><p><img src="/2023-iotdb-submit/20.png" alt></p><p>对于 System 面板,它提供了 CPU, JVM, 磁盘和网络维度的监控数据,在用于定位系统资源是否为瓶颈时非常管用。</p><p><img src="/2023-iotdb-submit/21.png" alt></p><p>对于 ConfigNode 面板,它也汇总统计了集群的基本信息,还提供了元数据及数据分区 Leader 分布等维度的监控。在用户定位集群扩展性能力时非常有用,比如是否所有的节点上都分配了读写流量,是否有节点宕机等等。</p><p><img src="/2023-iotdb-submit/22.png" alt></p><p>对于 DataNode 面板,它汇总了单节点引擎内部的细致监控,如存储,查询,元数据,共识和流处理引擎等等。在判断模块内部瓶颈原因时非常有用。</p><p><img src="/2023-iotdb-submit/23.png" alt></p><p>现在 IoTDB 有接近上千的监控指标,这些指标很难在今天短短的分享中介绍完,那接下来我就分享 5 个典型案例来展示一下 IoTDB 监控模块的能力:</p><p><img src="/2023-iotdb-submit/24.png" alt></p><p>第一个案例是在某高吞吐量场景下如何去确认瓶颈所在。</p><p>当业务链路较为复杂时,如果整体的性能不达标,用户其实是不太好去确认到底瓶颈是在业务上还是在 IoTDB 中。</p><p>那现在 IoTDB 的监控模块则是可以帮忙定位瓶颈是否在数据库中。</p><p><img src="/2023-iotdb-submit/25.png" alt></p><p>比如对于一个 Flink 实时消费 Kafka 数据来写入 IoTDB 的用户场景,业务链路上有 128 节点的 Kafka 集群,96 节点的 Flink 和 IoTDB 集群。</p><p>由于集群规模较大,部署测试调优运维的成本都较高。当时跑通整个链路后业务给我们的反馈就是 IoTDB 写入性能不够,IoTDB 集群总写入吞吐仅为 15GB/s,扩展性很差等等。</p><p><img src="/2023-iotdb-submit/26.png" alt></p><p>那当我们进行瓶颈排查之后发现锅并不在 IoTDB 而是在业务上层。</p><p>比如我们发现每个连接平均控制 4s 才会繁忙执行请求 20ms,每个节点平均每秒才接受 3 个请求且系统资源利用率都非常低。</p><p>因此我们推动了业务侧进行排查,他们发现即使把 Flink 的 Sink 侧置为空写整体吞吐也才 20GB/s,最终他们找到了问题所在并对 Flink 侧进行了优化。</p><p><img src="/2023-iotdb-submit/27.png" alt></p><p>在业务调整进行复测,我们发现 IoTDB 集群的整体吞吐可以达到 62.6GB/s,相比之前提升了 4 倍以上的性能,集群的线性比最高也达到了 0.89。</p><p>如果没有监控模块指导我们去推动业务侧改造,我们还一门心思的在数据库内部找瓶颈,那最终的结果一定是事倍功半。</p><p><img src="/2023-iotdb-submit/28.png" alt></p><p>第二个案例是某车联网场景的写入性能尖刺调优。</p><p>由于 IoTDB 是 Java 写的,很多用户也会询问我们 GC 对 IoTDB 性能的影响。由于我们在内存中也做了不少的池化来自己管理内存,所以大部分场景下用户其实感知不到 GC 对性能的影响。只有极少数个别场景才会观测到,比如这个案例就是 GC 导致了写入性能尖刺。</p><p>那现在 IoTDB 的监控模块内嵌了 GC 调优分析器,其实是具备对 GC 深度观测和调优的能力的,接下来让我们一探究竟。</p><p><img src="/2023-iotdb-submit/29.png" alt></p><p>该场景的整体架构是一个 3c 12D 的 IoTDB 集群,也是 Flink 实时消费 Kafka 的数据写入 IoTDB 集群。</p><p><img src="/2023-iotdb-submit/30.png" alt></p><p>在写入压测过程中,我们发现 IoTDB 的写入吞吐能力基本符合预期,但是存在尖刺,有时吞吐会接近 0。进一步排查原因我们发现这是由于 JVM 每 20 分钟会触发一次 Full GC,每次 Full GC 都耗时 1 min 以上,那这样的 GC 其实是非常不健康的。</p><p><img src="/2023-iotdb-submit/31.png" alt></p><p>那对于 GC 应该如何调优呢?常见的流程是启动 JVM 时打开 GC 日志,测试一段时间后上传 GC 日志到特定的网站进行分析,其会将不同原因导致的 GC 进行耗时和次数的汇总,然后我们可以基于这些聚合后的高密度 GC 信息再分析应该如何调整 GC 算法参数。</p><p><img src="/2023-iotdb-submit/32.png" alt></p><p>在建设好可观测性之后,现在的 IoTDB 如何去做 GC 调优分析呢?</p><p>我们首先提供了 GC 耗时比例的新手指标,它表示了 GC STW 耗时占整个 JVM RunTime 耗时的比例,如果这个比例小于 5-10%,则说明 GC 对系统整体的吞吐影响不大,如果在延迟上没有额外要求,那一般就不需要再调优了。</p><p>如果这个比例大于 10-15%,则一般说明可以对 GC 进行进一步调优。我们这时提供了若干专家指标,比如我们对不同 GC 原因导致的耗时和次数进行了统计,还对于种种 GC 前后的内存申请,内存大小都做了统计,这都能作为我们进行调优 GC 的数据支撑。</p><p><img src="/2023-iotdb-submit/33.png" alt></p><p>在该用户环境下,我们的调优思路首先是将 GC 算法从 PS 换成了对大内存更为友好的 G1,接着又结合负载和 IoTDB 的特点进行了 GC 参数的调优,其核心思路也是延缓并发标记阈值,提升 MixedGC 吞吐,控制单次 GC 耗时软上限等。</p><p>那最终调优的结果呢是写入吞吐稳定,不再又 Full GC,同时写入性能也提升了接近 50%,还是比较可观的。</p><p>这个案例主要是说明一下 IoTDB 对 GC 的观测能力和调优能力。</p><p><img src="/2023-iotdb-submit/34.png" alt></p><p>第三个案例是某测试场景下的硬件瓶颈原因探究。</p><p>在一些 POC 阶段,当系统出现瓶颈时,如果将系统视为黑盒,其实是不知道如何升级硬件的收益最高的。</p><p>结合 IoTDB 的监控模块,我们可以量化算出升级硬件带来的潜在性能收益,用以选择收益最高的硬件升级方案。</p><p><img src="/2023-iotdb-submit/35.png" alt></p><p>比如在该测试场景下,我们用 2 个客户端机器去压测高配机器的单节点集群,发现系统地性能仅为 1.2GB/s,不符合我们对如此高配机器的想象。</p><p><img src="/2023-iotdb-submit/36.png" alt></p><p>那接着我们对系统资源进行了分析,发现 CPU 和网络都没有达到瓶颈,但磁盘的繁忙程度达到了 100% 成为了瓶颈,从而限制了整体吞吐,此时就需要升级磁盘才能进一步提升性能了。</p><p><img src="/2023-iotdb-submit/37.png" alt></p><p>这就是一个典型的木桶效应,在理想状况下,所有资源应该同时达到瓶颈,这样硬件资源才没有浪费;然而在实际情况中,往往会有个别资源先到瓶颈,从而限制整体性能。</p><p><img src="/2023-iotdb-submit/38.png" alt></p><p>在升级磁盘之后,我们发现磁盘和网络不再是瓶颈,写入吞吐也提升到了原来的 2.5 倍,此时 CPU 又成为了新的瓶颈。</p><p>通过该案例,可以说明 IoTDB 的系统资源监控可以帮助我们快速找到硬件瓶颈,从而用最小的成本达到最大的收益。</p><p><img src="/2023-iotdb-submit/39.png" alt></p><p>案例 4 是某周测场景的写入性能波动变大问题排查。</p><p>对于服务器的 CPU 利用率出现波动这类问题,其实是比较难排查的,因为他不一定持续,等到我们去排查的时候可能已经不波动了。</p><p>对于这类问题,IoTDB 监控模块内置了操作系统,进程,线程池和模块 CPU 利用率监控,我们可以首先确认该波动是不是 IoTDB 引起的,如果是则可以一更进一步给出调优建议。</p><p><img src="/2023-iotdb-submit/40.png" alt></p><p>比如该问题就是我们在日常的周测场景中发现 1.2.0 rc5 版本的写入吞吐相比之前的一个版本波动更大,这其实属于很细致的观察了,不一定对业务有什么影响。</p><p>但我们没有放弃这一次机会进行了原因探究。</p><p><img src="/2023-iotdb-submit/41.png" alt></p><p>首先我们排查了操作系统及进程的 CPU 监控:发现新版本的 CPU 利用率波动更大,它应该是造成写入性能波动更大的原因。</p><p><img src="/2023-iotdb-submit/42.png" alt></p><p>那更进一步我们直接用了我们的大杀器-线程池 CPU 利用率监控,发现新版本中后台执行的合并线程池利用率在 0-18% 进行大幅波动,而老版本则稳定在 8% 左右。</p><p><img src="/2023-iotdb-submit/43.png" alt></p><p>更进一步我们去排查了存储引擎 TsFile 层级监控:发现新版本的存储引擎合并速度更快,文件状态也更健康。</p><p><img src="/2023-iotdb-submit/44.png" alt></p><p>因此我们就检查那段时间合入的代码,发现新版本修复了之前合并模块 IO 大小预估偏大的问题,可能导致之前受 IO 限速不能执行的合并任务受现在能够被执行。</p><p>至此我们已经明确了该现象的根因,考虑到改进后文件合并的更为健康,因此我们也没有进一步修改默认的限速参数。</p><p>但对于写入性能波动有要求的场景,我们也可以进一步降低合并模块的限速,从而达到与之前版本近似的效果。</p><p>该案例主要是说明 IoTDB 对 CPU 利用率的观测和掌控能力。</p><p><img src="/2023-iotdb-submit/45.png" alt></p><p>最后一个案例则是某钢厂场景的写入性能周期性下降 5% 排查。</p><p>随着我们可观测性做的不断深入,很多用户也开始对我们的监控面板越来越感兴趣,每天就上来翻一翻。</p><p>那在翻的过程中如果发现监控面板中有一些不影响业务的异样,是否有必要继续深挖?我们欢迎并鼓励这样的行为。</p><p>该案例就是由于用户的深挖反而促进了我们内核迭代的进一步演进,从而达到了双赢的效果。</p><p><img src="/2023-iotdb-submit/46.png" alt></p><p>该场景的架构是一个 3c3d 的集群,客户端会定期攒批写入 IoTDB,也会定期查询单设备过去 1 天的全量数据。</p><p>在用户日常巡检监控面板时,他发现了一个有趣的现象,即每 7 天会出现一次持续一天的 5% 性能下降。</p><p>这个问题其实可大可小,如果没有我们的监控面板,业务都不会感知到这件事情的存在,但该用户跟我们进行了反馈,于是我们也进行排查。</p><p><img src="/2023-iotdb-submit/47.png" alt></p><p>我们首先排查了系统及进程 CPU 监控:发现写入性能下降 5% 之后 CPU 占用率增加 5%,那依然还是怀疑 CPU 利用率的升高应该是写入性能下降 5% 的根因。</p><p><img src="/2023-iotdb-submit/48.png" alt></p><p>然后我们排查了写入延迟拆解监控,发现写入性能刚下降时存在跨节点转发,这代表客户端缓存失效,同时也观测到写入性能下降时调度执行阶段 P99 耗时增加。这基本可以辅助确定写入性能下降时 IoTDB 切换了时间分区,导致数据需要被写到新的节点。</p><p><img src="/2023-iotdb-submit/49.png" alt></p><p>接着我们对查询进行了分析,尽管查询的逻辑数据量始终是最近一天的数据,但跨分片查询时,由于涉及到更多的 operator 算子和跨节点序列化反序列化开销,这也会对 CPU 造成更大的消耗。</p><p>至此该问题的原因便找到了,它也催生了 IoTDB 内核的两个后续优化,第一个是尽量使得同一设备的数据保留在一个分片中,这样即可以避免该现象出现,第二个则是线程池 CPU 利用率监控,有了它我们则可以更直观的观察到增加的 5% CPU 都是在查询线程池导致的,排查效率就更高了。</p><p>该案例主要说明了 IoTDB 对这种很细微的业务感知不到的波动也具有诊断和迭代能力。</p><p><img src="/2023-iotdb-submit/50.png" alt></p><p>最后对以上 5 个案例做一个总结:</p><p>IoTDB 的可观测性目标是高效定位遇到的一切性能问题,虽然还有很长的路要走,但大家已经能够看到我们这一年的质变。</p><p>据我们的经验而言,针对硬件环境和业务负载调优一般可以获得 50% - 1000% 的性能提升。</p><p>所以我们非常欢迎大家来试用我们的商业版 IoTDB,也非常欢迎大家在使用监控模块过程中对想不通的性能问题和我们沟通。</p><p>我们期待与用户一起获得业务和技术上的成长。</p><p><img src="/2023-iotdb-submit/51.png" alt></p><p>我的分享就这么多,谢谢大家!</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2023 年 12 月 3 日,IoTDB 一年一度的 <a href="https://mp.weixin.qq.com/s/d5GR8O</summary>
<category term="分享" scheme="https://tanxinyu.work/tags/%E5%88%86%E4%BA%AB/"/>
<category term="性能调优" scheme="https://tanxinyu.work/tags/%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98/"/>
<category term="可观测性" scheme="https://tanxinyu.work/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
</entry>
<entry>
<title>2022 年终总结:记清华硕士的秋招之年</title>
<link href="https://tanxinyu.work/2022-annual-summary/"/>
<id>https://tanxinyu.work/2022-annual-summary/</id>
<published>2023-01-19T04:00:33.000Z</published>
<updated>2023-01-22T16:17:53.369Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>忙忙碌碌又是一年,终于到了 2022 年底。去年第一次写年终总结受到了不少的关注,我在这一年里也时常会重读自己的 2021 年终总结来鞭策自己。今年由于家里的特殊原因需要在医院过年,时间比较仓促,勉强抽出了一天的时间来简短写写年终总结。一方面是给 2022 年的自己一个交代,另一方面也是给 2023 年的自己一个警醒。希望我的经历和感悟能给大家一些启发。</p><p>首先依然是自我介绍环节,我叫谭新宇,清华本硕,现在清华大学软件学院 Apache IoTDB 组就读研三,师从王建民/黄向东老师,我对共识算法,分布式存储系统,时序数据库和分布式事务都比较感兴趣。</p><p>接着简单介绍一下我们组的工作:Apache IoTDB(物联网数据库)是一体化收集、存储、管理与分析物联网时序数据的软件系统。Apache IoTDB 采用轻量式架构,具有高性能和丰富的功能,并与 Apache Hadoop、Spark 和 Flink 等进行了深度集成,可以满足工业物联网领域的海量数据存储、高速数据读取和复杂数据分析需求。</p><h2 id="2022"><a href="#2022" class="headerlink" title="2022"></a>2022</h2><p>介绍完背景之后,在这里回顾下 2022 年的经历。</p><p>2 月,Apache IoTDB 社区的新分布式架构设计正在整个社区的努力下如火如荼的进行着,社区针对分布式时序数据库架构的方方面面都进行了广泛的调研和讨论,我也很幸运地参加了若干模块的调研设计,收获良多。对于分区方式,我们结合一致性哈希和查找表的优缺点,选择了一个更符合时序场景 trade-off 的分区方式,既不引入较大的存储成本,又具备一定的负载均衡灵活性,而且还留有足够的扩展性。对于扩展能力,我们将时序元数据与时序数据等同来看都做了多分片,使得集群拥有很强的横向扩展能力。对于查询引擎,我们调研了 Trino/Impala/Doris 等系统的 MPP 框架,又基于我们对时序场景的理解设计出了针对时序场景特殊优化的 MPP 框架和 Pipeline 执行引擎。对于共识算法,我们观察到不同的业务对于共识算法一致性和性能之间的 trade-off 有不一样的倾向,开始思考并着手设计一个能够支持不同共识算法的通用共识框架。</p><p>2 月下旬,在为 Talent Plan 社区提交若干代码修复并做了 3 次有关共识算法和分布式事务的公开分享之后,我荣幸的被 Talent Plan 社区接纳为了 mentor,期待以后能够继续和志同道合的小伙伴在 Talent Plan 社区沟通交流。</p><p>3 月,需要毕设开题的我结合 Apache IoTDB 新分布式架构对通用共识框架的需求,开始调研学术界和工业界在通用共识框架领域的相关工作。在调研中,我发现了 Facebook 的 Delos 框架不仅支持在一套接口下实现不同的共识算法,还能够支持生产环境动态变更共识算法,他们的工作也得到了学术界的认可,成为了 2020 OSDI 的 Best Paper。沿着他们的思路,在摒弃了当前 ROI 收益较低的生产环境动态变更共识算法后,我便开始着手设计通用共识框架的接口,该框架不仅需要先支持强一致性的 Raft 算法和弱一致性的异步复制共识算法,还需要为未来更好更丰富的共识算法接入留下扩展性(例如 2021 FAST 的 Best Paper 就是基于异步复制的思路在提升写性能的同时又在读时添加了一些约束,从而提供了跨客户端单调读一致性,基于 ZK 实现后相比 ZK 性能能够提升 1.8-3.3 倍)。由于这个思路在学术界和工业界都相对新颖,我的开题便较为顺利的被通过了。</p><p>3-4 月,由于毕设开题读了一大堆论文状态比较好,我开始在知乎上回答自己感兴趣领域的很多问题,并幸运的在最开始就得到了很多赞,这些对我的认可又成为了我进一步在知乎上学习和活跃的动力,从而形成了一个正反馈效应。正如去年年终总结的感悟中所提到的,这种正向反馈对于我自己的激励作用是非常大的。现在回过头来看,那段时间尽管我也有一些输出得到了很多赞,但收获最大的还是在此过程中阅读了知乎上分布式系统领域非常多的优秀回答,学到了很多技术知识,了解了很多思考维度。</p><p>4 月,忙完毕设开题的我开始面试暑期实习,尽管八股和项目由于日常的积累都没有什么问题,但由于实在提不起兴趣和动力刷题,所以在面试过程中还是多少有些磕磕绊绊。幸运的是最后投的几家公司除了微软之外总体都面的不错,最后思考再三后还是选择去 PingCAP 实习。一方面是 PingCAP 的三轮面试官(赵磊老师,徐锐老师,金鹏老师)给我的面试体验都非常好,另一方面则是我一直对 PingCAP 的很多体系(例如工程服务体系,架构演进体系,开源体系等等)非常敬佩和好奇,想要借此机会去学习感悟。</p><p>5-6 月,我专心投入到了 Apache IoTDB 新分布式框架的实现中。最主要的工作便是通用共识框架,这期间一方面抽象了其对上对下的通用接口,另一方面则是为其支持了若干共识算法。对于强一致性共识算法,我调研了 Java 实现的若干 Raft 库的成熟度,并最终得出只有 SofaJRaft 和 Apache Ratis 可以使用的结论。基于此结论,我和子阳探索了 Apache Ratis 的实现并将其集成到了我们的共识框架中,这使得 Apache IoTDB 的新分布式架构具备了强一致性的能力(这半年以来,子阳在 Apache Ratis 的稳定性和性能优化上投入了大量的精力,令人开心的是目前他已经得到了 Apache Ratis 社区的认可,成为了 Committer,可以说是双赢了)。对于弱一致性共识算法,我们结合时序场景写写冲突极少的业务特点,设计并实现了基于异步复制思路的弱一致性共识算法 IoTConsensus。我和乔老师,恺丰,海铭,金瑞,洪胤,厚亮和珍姐都参与了 IoTConsensus 的设计实现与迭代测试。IoTConsensus 做了非常多的工程优化,包括但不限于 Batching,Pipeline,Thrift AsyncClient/AsyncServer 等等,所以流程非常复杂。虽然实现和 debug 的过程非常艰苦,但我们在异步编程,内存控制,可观测性,debug 技巧等方面都有了显著的进步,可以说是既痛苦又有收获吧。此外,为了兼容统一的共识框架,避免单副本时共识框架的额外开销,我们还专门针对单副本的场景(只需要 scale out 而不需要 high availability 的场景)设计了极为轻量的 SimpleConsensus,避免出现 RaftLog 和存储引擎 WAL 双写的现象出现。在未来,我们还计划为 Apache IoTDB 的共识框架加入更多的共识算法实现,例如 SofaJRaft 和我们组今年中的 ICDE NB-Raft 等等。我非常期待 Apache IoTDB 共识框架的文档,功能,性能,稳定性和正确性等等都能够迅速成熟,甚至可以成为大家未来使用通用共识框架的范式,这样如果业务上对共识算法一致性和性能的 trade-off 有不同的需求,便可以直接使用我们已经封装好的共识框架而不用再去造轮子了。这里也非常欢迎对共识算法感兴趣的同学一起参与进来~</p><p>7-10 月,我在 PingCAP 进行了全职实习。在 PingCAP 的暑期实习是我个人体验最好的一次实习。在技术上,我不仅可以去学习公司内部海量的技术积累(Rust,分布式数据库,TiKV,调优培训等)和流程规范(如何建立可扩展的工程服务体系和可持续的架构演进体系 -> 可观测性 + 分级 Support && 业务场景持续打磨),也可以系统学习 TiKV 事务引擎演进的历史(乐观事务->悲观事务->大事务->Async Commit/1PC-> 悲观事务内存悲观锁等)来培养自己的产品思维,最后还可以基于这些成长去做一些深入的探索并取得了不错的成果。在生活上,同事们会非常耐心友善地回答我的种种问题,工程经验十分丰富的 mentor 徐锐老师也花了非常多的时间和我 one-one 沟通我的种种疑问来帮助我快速成长(技术问题。如何平衡技术驱动和业务驱动?如何评估架构演进和工程服务的 ROI 等等),金鹏老师和 HRBP 也会和我定期沟通最近的工作进展。PingCAP 是国内开源数据库和开源社区的佼佼者,非常推荐大家有机会前去实习,一定会不虚此行~</p><p>8-10 月,我海投了很多感兴趣的数据库团队,并以没有任何职业背景的身份参加了 41 场秋招面试。虽然非常忙碌,但这些面试尤其是终面加面环节对我技术的成长和产品思维的提升有很大的帮助。在面试过程中我也有幸认识了很多大佬,并幸运地拿到了不少 offer,在这里表达对相关面试官和 HR 的真诚感谢。</p><p>10 月中旬,我被评选为了 2022 软件学院科研科创年度人物,这令我诚惶诚恐。在我看来,这个奖项更适合论文发到手软的大佬。询问之后发现现在的评审规则中也包含了开源贡献,所以现在做系统做工程的同学也可以得到学院的认可。在这里也真诚感谢学院对我的认可。</p><p>10 月下旬,我和祥威洪胤子阳参加了 2022 TiDB Hackathon,并最终拿到了产品组的最佳校园奖。在这次 Hackathon 中,从技术上我们为 TiKV 做了 Parallel Apply 的优化 Demo;从产品上我们在高并发批量写入热点场景会有不错的收益。我们模拟了银行清算结算等跑批业务的极致情况,在 60 并发下,不同 BatchSize 的批量写入性能提升 89.4%~119.0%。TiKV CPU 利用率从 700% 左右提升至 1500% 左右。我们也尝试了通用的批量导入热点场景,对于 TPCC prepare,在 1024 BatchSize 下,不同并发批量写入性能提升 29.8%~36.0%,TiKV CPU 利用率也从 750% 左右提升至 1000% 左右。通过这次 Hackathon,结合 OB 4.0 版本自适应日志流的设计,我们对 Raft 和 Multi-Paxos 的异同有了更深刻的理解。总体而看,一个系统的架构设计就是要在关键模块的各种 trade-off 中做出纠结的选择,并且一旦做出了某个选择,就需要基于这个选择更进一步做非常多的产品化工作和优化打磨来提升这些技术对于用户的实际价值。例如,TiDB 选了 TSO 的时间戳获取方案并不代表就不能服务跨数据中心场景了,其也做了 Local/Global TSO 的产品化工作来满足部分用户场景的跨数据中心需求。CRDB 用了 HLC 之后并不仅仅体现在获取事务时间戳更快,其至少还基于 HLC 在 Strong/Stale Follower Read 这一块做了许多工作来减少跨域流量从而降低成本。对于 OB,其选择了 Multi-Paxos 而不是 Raft,并且更进一步在 4.0 架构中提出了单机分布式一体化架构来解决其他数据库很难彻底解决的写热点缓解,大事务支持和 1PC 比例增大等难点。当然,这些技术和产品化的工作短期内很难形成事实标准,也都能够在各自的用户场景产生价值,至于孰优孰劣就很难客观判断了。</p><p>11 月,我手上的秋招 offer 陆陆续续开奖了,这期间涉及到了非常复杂的心理变化。有期待,有失望,有惊喜,有反思,有迷茫,有透彻。对于最终选择去哪里,我纠结了许久,请教了很多朋友,前辈,同学,师兄,父母和导师的意见。其实选择哪条路都不会太差,主要还是需要剖析自己更喜欢怎样的工作方式,并且说服自己一旦做出选择后就不再患得患失。此外王老师东哥乔老师都对我的就业方向和未来发展路径给予了中肯的建议,最终我选择了 Apache IoTDB 的商业化公司天谋科技继续做 IoTDB。我非常感谢研究生期间 IoTDB 这个大平台对我的帮助,也希望自己未来能够继续在这个大平台发光发热。</p><p>12 月,Apache IoTDB 的 1.0 版本发布了,这次新版本凝聚了整个社区的智慧和力量,标志着 Apache IoTDB 从此彻底拥有分布式,为后续产品快速发展奠定了良好基础。这次分布式 1.0 架构的发布也是促使我决定留下的重要原因之一,因为我觉得这个架构有太多可以做的工作可以去做,一些积累的技术宅已经还清,我相信未来 Apache IoTDB 一定能够在工业物联网的时序场景大放异彩。想通以后,我也和洪胤子阳湘鹏组成了系统优化小组,不断扩大我们的 scope 去和 IoTDB 一块成长了~</p><p>今年我在 Apache IoTDB 社区 Review 了 187 个 PR,提交并被合并了 48 个 PR。今年的工作有很多新颖的东西,希望明年能够继续保持下去。</p><p>今年我在知乎上写了几十篇博客和回答,粉丝数和点赞数相比去年同期有了接近 10 倍的增长。Github 上的 Follower 数和个人仓库 Star 数也有了 6 倍多的增长。希望自己明年还能继续坚持输出有价值有意思的知识。</p><p>今年我看了权谋剧的巅峰之作《大明王朝 1566》,尽管剧中演绎的是封建社会,但其中一些社会运行的本质规律放到今天也依然适用,我自认为受益良多,非常推荐大家去看。此外我也读了 IT 人的必读书籍《浪潮之巅》,了解了信息革命以来很多著名公司的兴衰更迭及背后的哲学原因。天下大势,浩浩汤汤,顺之者昌,逆之者亡。不论是公司,组织还是个人,自己的努力不可或缺,但只有抓准大势站上浪潮之巅,才有可能干一番大事。</p><h2 id="一些感悟"><a href="#一些感悟" class="headerlink" title="一些感悟"></a>一些感悟</h2><p>介绍完了 2022 年的经历,在这里谈谈自己这一年的新感悟,这些感悟不一定适用于每个人,但都是我个人在今年得到成长的诀窍。</p><h3 id="没有不写-bug-的人,但要成为追求不写-bug-的人"><a href="#没有不写-bug-的人,但要成为追求不写-bug-的人" class="headerlink" title="没有不写 bug 的人,但要成为追求不写 bug 的人"></a>没有不写 bug 的人,但要成为追求不写 bug 的人</h3><p>这一年在日常的开发过程中修复了无数的 bug,一些是陈年老 bug,一些则是自己在开发过程中引入的新 bug。这期间我一直在思考为什么会有这么多的 bug?什么时候能够修复完所有的 bug?现在我有了更明确的感悟:</p><p>对于一个 bug,从设计->编码->UT->IT->压力测试->混沌测试->发版测试->用户 POC 环境->用户生产环境这套全链路的流程中,越晚被发现,则修复的成本就越高。世界上没有不写 bug 的人,也不存在没有 bug 的系统,我们需要尊重这一客观规律。出现 bug 不可怕,但不对其进行反思改进就非常可怕了。虽然没有不写 bug 的人,但要成为追求不写 bug 的人,因为只有具备了这个追求,才会在日常的开发过程中注重 bug 产生原因的积累,并通过种种技术手段来规避 bug 的产生。正是因为对 bug 的反思,学术界和工业界才产生了很多对系统稳定性至关重要的工作(形式化验证,确定性模拟器,混沌测试,持续集成等等)。一个工程师的成长必然伴随着对种种异常情况的考虑,知识边界被拓宽后自然写出 bug 的概率就会越来越低。</p><p>新的系统,新的模块,新的引擎,新的算法需要一段时间的稳定性打磨,没有用户去用就很难成熟,但如果用户去用了,团队也一定要利用好这次机会,让产品的稳定性和工程师的能力一起成长,进入一个正反馈循环才有可能让产品越来越好。</p><h3 id="性能优化与代码可维护性需要有一个平衡"><a href="#性能优化与代码可维护性需要有一个平衡" class="headerlink" title="性能优化与代码可维护性需要有一个平衡"></a>性能优化与代码可维护性需要有一个平衡</h3><p>一般而言,复杂的性能优化往往会导致代码维护成本的上升。例如事件驱动的并发编程模型具有更高的自主性,性能上限相比完全被 Runtime 接管的协程可能会高一点,但带来的回调地狱问题又可能会大幅增大代码复杂度,导致代码维护成本大幅上升。从全局的 ROI 评估来看,这样的工作不一定收益很高。</p><p>今年我在开发 IoTConsensus 过程中就有点过于追求性能优化,加了一堆异步逻辑和工程优化,虽然优化完后性能还不错,但这种代码上的复杂度也使得大家在 debug 时非常痛苦,新人也比较难一下子看懂主要逻辑,只能靠进一步完善文档细节并做代码走读才有可能让新人逐渐上手。</p><p>在未来的工作中,我也会吸取这次的教训更关注性能优化与代码可维护性之间的平衡。</p><h3 id="有太多能力可以提升,不要只看到技术"><a href="#有太多能力可以提升,不要只看到技术" class="headerlink" title="有太多能力可以提升,不要只看到技术"></a>有太多能力可以提升,不要只看到技术</h3><p>今年在实习和秋招面试过程中有幸和非常多的技术大佬有过交流,在他们身上技术能力只是一个值得学习的维度,其他维度的能力还包括管理,演讲,分享,社交,营销,商业感知,为人处世等等。</p><p>要想进一步实现职场的抱负,综合能力是十分重要的。总之,有太多领域的太多能力可以去提升,技术只是其中比较重要的方面之一,最好不要仅仅把自己拘泥在技术上。</p><p>不断扩展自己的 scope,锻炼培养自己的综合能力,便能在单位时间内获得更大的成长拥有更丰富的阅历,从而可能在机遇出现时顶住压力抓住机会。</p><h3 id="技术是满足业务的手段,一定要带有产品思维"><a href="#技术是满足业务的手段,一定要带有产品思维" class="headerlink" title="技术是满足业务的手段,一定要带有产品思维"></a>技术是满足业务的手段,一定要带有产品思维</h3><p>技术最终是要为业务服务的。没有业务侧的需求,技术最终也难以发挥实际价值走向成熟。</p><p>在做日常的功能开发和性能优化时,最好能够带着产品思维去思考问题:自己所做的工作在整个产品中处于怎样的一个位置?做了这个工作之后整个产品会有什么样的不同?一旦能够培养出来这种端到端的产品思维,自己也会更明确工作目标并更有动力去做事成长。</p><h3 id="针对不同硬件环境和业务负载的排列组合,有太多有意思的工作可以去做"><a href="#针对不同硬件环境和业务负载的排列组合,有太多有意思的工作可以去做" class="headerlink" title="针对不同硬件环境和业务负载的排列组合,有太多有意思的工作可以去做"></a>针对不同硬件环境和业务负载的排列组合,有太多有意思的工作可以去做</h3><p>刚入门数据库的时候一直比较好奇,像 Oracle 这样的数据库厂商是如何每年都有那么多新的工作可以做?为什么我就想不到做那些呢?</p><p>通过在 Apache IoTDB 实验室的成长和在 PingCAP 的实习,我理解了业务驱动对于产品的意义。只要有用户和业务的支持,就会有源源不断的需求出来。对于数据库系统而言,在不同的硬件环境和业务负载下,有太多的功能和性能优化可以去做,几乎不存在技术上没有事情可做的情况。当然,任何组织任何个人所能调度的资源都是有限的,如何评估当前所有工作的 ROI,如何在有限的资源上持续的做紧急重要且正确的事,也是产品成败存亡的关键。</p><h3 id="好的系统需要在天花板高的架构设计上进行日复一日的持续打磨"><a href="#好的系统需要在天花板高的架构设计上进行日复一日的持续打磨" class="headerlink" title="好的系统需要在天花板高的架构设计上进行日复一日的持续打磨"></a>好的系统需要在天花板高的架构设计上进行日复一日的持续打磨</h3><p>OceanBase 的杨传辉老师提过一个观点:每个系统设计时都需要考虑架构、稳定性和性能,这三者之间的关系是什么?一个经典的规律是“把稳定的系统做高效,远比把高效的系统做稳定更容易”。最难的是从 0 到 1 把系统做稳定。有了稳定的系统,接下来逐步优化性能往往会比较顺利,直到遇到系统架构的性能天花板。因此,系统架构设计之前,首先要考虑清楚系统的目标和性能天花板,接着基于正确的架构把系统做稳定,最后优化性能。</p><p>在 PingCAP 的暑期实习期间我很明显的一个感受就是,TiDB 的可观测性做得非常好,同事们都比较明确特定业务场景下当前 TiDB 的短板在哪里,也明白自己工作的端到端价值,从而能够持续打磨 TiDB 在特定业务场景下的性能。大家的工作并行汇聚起来便能够促使 TiDB 每个版本都能够在很多业务场景下有不错的进步。</p><p>在现在的我看来,Oracle 最有价值的东西其实是在当前的市场规模下能够接触到最多最复杂的业务场景,从而能够持续的对系统进行打磨,这是其他任何单机数据库都没法做到的,自然也就无法在单机数据库领域打败 Oracle 了。</p><p>在系统初期,调研设计出一个性能天花板高的系统架构非常重要。在系统搭建完成基本稳定之后,在业务场景下进行持续的打磨就更为重要了。比如 Apache IoTDB 1.0 的分布式架构性能天花板我自认为就很高,目前性能也基本打磨到了和 0.13 版本一致的地步,下一步就是在特定业务场景下进行持续打磨做稳定性和性能的进一步提升了。</p><h3 id="提出问题比解决问题更为重要"><a href="#提出问题比解决问题更为重要" class="headerlink" title="提出问题比解决问题更为重要"></a>提出问题比解决问题更为重要</h3><p>好奇心是人类进化创新的驱动力。发现当前存在的问题,才有可能去进一步改进解决。爱因斯坦早在《物理学的进化》中就说过:”提出一个问题比解决一个问题更为重要。 因为解决一个问题也许是一个数学上或实验上的技巧,而提出新的问题,新的可能性,从新的方向看旧问题,则需要创造性的想象力,而且标志着科学的真正进步”。</p><p>对于数据库系统,在特定硬件环境和业务负载下,性能瓶颈到底在何处?若想优化当前最大的性能瓶颈,它到底是一个工程问题还是学术问题?如果是工程问题,需要投入多少资源去完成?会有多少收益?如果是学术问题,它的 trade-off 到底是什么?有什么更符合当前业务场景的可能性吗?一旦有了这样思考问题的方式,就会发现技术上其实存在很多可能。</p><h3 id="个人的努力不可或缺,但平台和机遇往往更为重要"><a href="#个人的努力不可或缺,但平台和机遇往往更为重要" class="headerlink" title="个人的努力不可或缺,但平台和机遇往往更为重要"></a>个人的努力不可或缺,但平台和机遇往往更为重要</h3><p>在看了《大明王朝 1566》 和《浪潮之巅》后,越发认识到时势造英雄的客观规律。个人的努力能够决定个人的发展下限,但平台和机遇才能够决定个人的发展上限。很多时候,玄而又玄的天下大势和因缘际会反而可能会在若干年后造就一番佳话。</p><p>就目前而言,认识到平台的重要性,学会顺势而为,在职责范围内乐于沟通踏实做事,可能是个人职业生涯短期的最优解了吧。</p><h3 id="没有绝对的成功,以个人最舒服的姿态和最擅长的方式去活一生就已足够"><a href="#没有绝对的成功,以个人最舒服的姿态和最擅长的方式去活一生就已足够" class="headerlink" title="没有绝对的成功,以个人最舒服的姿态和最擅长的方式去活一生就已足够"></a>没有绝对的成功,以个人最舒服的姿态和最擅长的方式去活一生就已足够</h3><p>对于个人而言,绝对的成功很难定义,技术和商业也仅仅是很小的一部分。在认识到时势造英雄的客观规律后,我愈发觉得精神上的富足对于一个人一生的幸福最为重要。有一句话叫做”但行好事,莫问前程”,没有必要去和其他人比来比去,任何人都是比上不足比下有余,能够以个人最舒服的姿态和最擅长的方式去活一生就已足够,剩下的交给命运即可。</p><h3 id="多站在别人的角度思考问题"><a href="#多站在别人的角度思考问题" class="headerlink" title="多站在别人的角度思考问题"></a>多站在别人的角度思考问题</h3><p>在日常工作中,很多时候会涉及到任务的分工与合作,这就需要频繁地沟通。</p><p>每个人都有完全不同的成长环境,不同的成长环境造就了不同的三观以及思维方式,进而产生出不同的职业动机和职业规划,最终使得对待工作的态度和动力也会有所差别。这些差异无关对错,但有时候过于从自己角度思考问题并进行沟通反而会适得其反。</p><p>在与他人沟通时,如果能够站在别人的角度去思考问题,懂得换位思考,则很多矛盾在沟通过程中就可以被缓解甚至解决。当然,有些天然对立的矛盾是没办法彻底解决的,只能选择一个平衡点进行缓解。</p><h2 id="来年展望"><a href="#来年展望" class="headerlink" title="来年展望"></a>来年展望</h2><p>今年的年终总结写的比较仓促,但也算是勉强写完了。</p><p>新的一年就主要聊聊与 Apache IoTDB 有关的技术展望吧:</p><ul><li>共识框架:功能,性能,稳定性,正确性和文档稳定推进,希望能够吸引感兴趣的同学加入进来~</li><li>可观测性:深受 TiDB Performance Overview 面板的影响,下一步计划大幅加强 Apache IoTDB 的可观测性,并基于此来建立可扩展的工程服务体系和可持续的架构演进体系。</li><li>Java 全异步执行框架:暑期在学 Rust 时就已经被其 async/await 的无栈协程设计所折服。对于 Java 来说,自带的有栈协程框架 Loom 刚刚 GA,离正式生产环境用到还很久远。事件驱动的回调机制尽管性能很好,但回掉地狱的出现又会极大的影响代码可读性和可维护性。这一块在调研很多 Java 实现的大数据栈后并没有查到相关的资料。幸运的是,通过对 CompletableFuture 和 Thrift AsyncServer/AsyncClient 接口和源码的探索,对于如何基于 Thrift 的 NIO 机制来提升性能又利用 CompletableFuture 的编排能力提升代码的可读性可维护性,我已经有了一些想法和探索,期待明年能够结合这两者的优点进行一些 Java 数据库下 SOTA 线程模型的探索论证和实现测试,并形成系统性的博客向社区输出。</li></ul><p>最后,感谢您的阅读,希望这篇总结能够对您有所帮助。</p><p>在除夕这天,预祝大家新年身体健康,阖家幸福,心想事成~</p>]]></content>
<summary type="html"><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>忙忙碌碌又是一年,终于到了 2022 年底。去年第一次写年终总结受到了不少的关注,我在这一年里也时常会重读自己的 2021 年终总结来鞭策自</summary>
<category term="IoTDB" scheme="https://tanxinyu.work/tags/IoTDB/"/>
<category term="年终总结" scheme="https://tanxinyu.work/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>2022 互联网求职经验分享</title>
<link href="https://tanxinyu.work/2022-internet-job-hunting-experience-sharing/"/>
<id>https://tanxinyu.work/2022-internet-job-hunting-experience-sharing/</id>
<published>2022-12-15T02:21:29.000Z</published>
<updated>2023-01-18T15:01:12.209Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在前前后后忙活了接近四个月后,我的秋招终于结束了。</p><p>这篇 <a href="https://www.zhihu.com/question/351956937/answer/2798391706">回答</a> 分享了我几乎所有技术面的面经,本博客将结合本人在今年秋招中的经验和在院系就业分享会中的分享内容介绍一下实习和秋招各个环节的注意事项,希望能够帮助到对分布式数据库内核研发岗感兴趣的同学。</p><p>注:本文内容仅代表个人看法。</p><h1 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h1><p><img src="/2022-internet-job-hunting-experience-sharing/1.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/2.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/3.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/4.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/5.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/6.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/7.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/8.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/9.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/10.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/11.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/12.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/13.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/14.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/15.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/16.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/17.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/18.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/19.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/20.png" alt></p><p><img src="/2022-internet-job-hunting-experience-sharing/21.png" alt></p>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在前前后后忙活了接近四个月后,我的秋招终于结束了。</p>
<p>这篇 <a href="https://www.zhihu.com/que</summary>
<category term="数据库" scheme="https://tanxinyu.work/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="分享" scheme="https://tanxinyu.work/tags/%E5%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>2022 TiDB Hackathon 产品组最佳校园奖总结</title>
<link href="https://tanxinyu.work/2022-tidb-hackathon/"/>
<id>https://tanxinyu.work/2022-tidb-hackathon/</id>
<published>2022-12-08T12:28:07.000Z</published>
<updated>2022-12-15T02:24:52.089Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2022 年 10 月,<a href="https://tidb.net/events/hackathon2022">2022 TiDB Hackathon Possibility at Scale</a> 成功举办。</p><p>作为暑期实习在贵司事务组的 intern,在对 TiKV 的 codebase 有一定了解后,我兴致勃勃地拉了实验室的同学报名参加了此次 Hackathon,并且最终拿到了产品组的最佳校园奖,虽然没有拿到更大的奖项,但已经玩得十分开心了。</p><p>非常感谢 Hackathon 期间队友,mentor,组织者和评委对我们组的帮助和认可。</p><p>本文将简单介绍我们组的工作和思考以做回顾。</p><p>以下是我们项目的一些相关资料,欢迎点击了解:</p><ul><li><a href="https://github.com/orgs/FWH-TiKV/repositories">代码</a></li><li><a href="https://github.com/FWH-TiKV/RFC">RFC</a></li><li><a href="https://www.bilibili.com/video/BV1cP4y1S7Xo/">答辩视频</a></li></ul><p>注:本文仅代表个人看法。</p><h2 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h2><p>本小节将沿着答辩 PPT 的思路介绍我们的工作。</p><p>我们的名字是热点清零队。我们的项目是无畏写热点,我们希望能够解决 TiKV 写热点问题的最后一公里。</p><p><img src="/2022-tidb-hackathon/1.png" alt></p><p>我们的团队成员均来自清华大学软件学院。今年 hackathon 的主题是探索 scale 的可能性,我们的主要工作是解决热点 Region 在单机多核上的扩展性问题。在极致场景下我们提升了 TiDB 接近 1.2 倍的吞吐。</p><p><img src="/2022-tidb-hackathon/2.png" alt></p><p>从用户视角来看,写热点问题会给他们带来多少困扰呢?比如某用户就提出,批量写场景下 TiKV 的 CPU/IO 都没用满,但写入依然很慢,这些现象给用户带来了不便和困扰。我们统计了 AskTug 论坛上写满 tag 帖子的原因分布,大致如右上图所示,可以看到写热点问题占比第一。</p><p><img src="/2022-tidb-hackathon/3.png" alt></p><p>那现在的 TiDB 是如何解决热点问题的呢?其整体思路都是通过划分 region 来均匀承担负载,从而能够扩展。如右图所示,理想情况下随着 region 的分裂,热点会被稀释,进而线性扩展起来。然而,在当前上层数据分片的语义下,对于聚簇索引,唯一自增索引等场景,写热点问题难以避免。</p><p><img src="/2022-tidb-hackathon/4.png" alt></p><p>当分裂 region 不能均分负载时,实际情况会如右上图所示,尽管分裂出了多的 region,但写热点始终集中在个别 region 上。在这种场景下,往往会遇到单节点存算资源并没有被充分利用的情况。其实不论上层怎么去分裂 region,单 region 热点始终是写热点问题的最后一公里,无法回避。</p><p><img src="/2022-tidb-hackathon/5.png" alt></p><p>那让我们看看在 TiKV 中单 region 的执行路径。总体来说是需要在 StorePool 中进行 commit,在 ApplyPool 中进行 apply。然而在现有的实现中,不论是 StorePool 的持久化还是 ApplyPool 的写 memtable,并行度都是 1,因而不具备在单机上的扩展性。</p><p><img src="/2022-tidb-hackathon/6.png" alt></p><p>那如果我们想要在单机上扩展热点 region,那可以从存储资源和计算资源两个方面来入手。对于存储资源,可以利用 IO 并行来提高吞吐,比如将单 region 不同批的日志用异步 IO 并行化处理,去年的 TPC 项目已经做过尝试,取得了不错的效果。对于计算资源,理论上也可以采用多核并行来提高吞吐,这也是我们的尝试,即去做 Parallel Apply</p><p><img src="/2022-tidb-hackathon/7.png" alt></p><p>Parallel Apply 的整体思路是将无依赖的日志并行处理,从而提升整体吞吐。2018 年 VLDB 和 2021 年 JOS 已经对 Parallel Apply 的可行性和正确性进行了证明。</p><p><img src="/2022-tidb-hackathon/8.png" alt></p><p>那在 TiKV 上落地 Parallel Apply 会有哪些难点呢?我们主要遇到了 4 个问题,比如如何保证依赖顺序的正确性,数据正确性,语义正确性和 index 正确性等等,以下分别进行介绍</p><p><img src="/2022-tidb-hackathon/9.png" alt></p><p>我们的整体思路是在 ApplyPool 以外额外引入一个 ParallelApplyPool,并在 StorePool 中判断路由,进而使得单 region 的日志存在并行的可能性。同时在实现过程中,为了避免锁导致的线程切换,我们的共享状态均使用了原子变量。</p><p><img src="/2022-tidb-hackathon/10.png" alt></p><p>对于刚刚提到的难点,我们简单介绍一下解决方案。</p><p>对于如何保证具有依赖的日志执行顺序正确?在 Leader 侧和在 Follower 侧需要有不同的处理方案。</p><ul><li>在 Leader 侧,得益于上层事务语义的约束,我们不需要引入依赖检测结果便可以无约束的并行 apply 普通日志,因为他们的 key 范围必定不会重叠。</li><li>在 Follower 侧,最简单可以串行执行来保证正确性,更进一步也可以考虑基于拓扑排序的依赖检测机制来并行 apply 日志从而满足依赖关系。</li></ul><p>对于如何保证 leader 切换或重启时数据依然正确?我们则很简单的使用了 Raft 的 term 变量,当且仅当日志的 term 为当前 term 时才考虑路由到 ParallelApplyPool 并行处理。这样的机制保证了 Leader 切换或者节点重启时系统不会将本不能并行处理的日志并行化处理,从而导致数据不一致。</p><p><img src="/2022-tidb-hackathon/11.png" alt></p><p>对于如何保证 admin 等特殊日志的执行依然符合串行语义?即在 admin 日志执行之前其前面的所有日志均需要已经执行,在 admin 之后的日志执行前必须保证该 admin 日志已经执行。这个具体实现非常复杂,简单来说,我们在 Parallel Apply Pool 和 ApplyPool 中用原子变量共享了一些状态,对于单 region,StorePool 会将无冲突的普通日志在 Parallel ApplyPool 中并行执行,当出现 admin 日志时,当 StorePool 未感知到其执行完时,所有的日志都会被路由到 ApplyPool 中串行执行,当 admin 日志的 apply 结果返回 StorePool 后,之后的普通日志可以被继续路由到 Parallel Apply Pool 中并行执行。在 ApplyPool 中,我们还用原子变量来保证了只有 Parallel Apply Pool 中有关该 region 的所有普通日志都已执行完才去执行 Admin 日志。这些工作使得 admin 等特殊日志的执行依然符合了串行语义。在实际测试中,大部分普通日志都能够在 Parallel Apply Pool 中并行处理。,这也是我们在写热点场景性能提升的根源。</p><p><img src="/2022-tidb-hackathon/12.png" alt></p><p>对于如何保证 applyIndex 的更新,我们发现不需要在磁盘上实时更新 applyState,这主要与底层 KV 引擎的幂等语义有关。我们在内存中维护了可能存在空洞的 applyIndex,当其连续时才推进 StorePool 中的 applyIndex,这也与现有的代码实现了兼容。</p><p><img src="/2022-tidb-hackathon/13.png" alt></p><p>我们模拟了银行清算结算等跑批业务的极致情况,在 60 并发下,不同 BatchSize 的批量写入性能提升 <strong>89.4%~119.0%</strong>。TiKV CPU 利用率从 700% 左右提升至 1500% 左右。</p><p><img src="/2022-tidb-hackathon/14.png" alt></p><p>我们也尝试了通用的批量导入热点场景,对于 TPCC prepare,在 1024 BatchSize 下,不同并发批量写入性能提升 <strong>29.8%~36.0%</strong>,TiKV CPU 利用率也从 750% 左右提升至 1000% 左右</p><p><img src="/2022-tidb-hackathon/15.png" alt></p><p>在测试过程中,我们也更深刻地体会到了木桶效应。在 apply 是瓶颈的热点场景下,我们能取得很好的效果。但是对于 apply 不是瓶颈的场景,尽管这个阶段可能会加速,但根据阿姆达尔定律,最终的整体收益也不明显。</p><p><img src="/2022-tidb-hackathon/16.png" alt></p><p>展望未来,我们认为我们的工作拼好了 TiKV 解决写热点问题的最后一块拼图,给出了解决写热点问题的终极形态。在多节点上,我们可以用 Split Region 的方式来在多节点上 scale。在单机上,我们可以用 Parallel Redo log 甚至是 Multi-Paxos 来更好地利用磁盘资源,也可以用 Parallel Apply 来更好的利用 CPU 资源,最终也能够在单机上彻底 scale。</p><p><img src="/2022-tidb-hackathon/17.png" alt></p><p>同时在引擎演进方面,我们解决了不相关事务的日志回放顺序依赖问题,为 TiKV 更好的 CPU Scheduling 和极致性能做了铺垫。比如未来可以结合乱序确认乱序 commit 和 TPC 策略来对资源进行更精细的控制。</p><p><img src="/2022-tidb-hackathon/18.png" alt></p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>本小节将更随心的介绍一些我们的思考。</p><p>在这次 Hackathon 中,从技术上我们为 TiKV 做了 Parallel Apply 的优化;从产品上我们在高并发批量写入热点场景会有不错的收益。</p><p>上小节已经介绍了基于 region split 的方式来缓解热点问题的局限性,那么如何解决这种局限性呢?这就要回到一个圣战问题了:Raft 和 Multi-Paxos 有什么区别?</p><p>如同我在 <a href="https://github.com/FWH-TiKV/RFC">RFC</a> 中介绍的一样,相比 Multi-Paxos,Raft 不能乱序 ack,不能乱序 commit,不能乱序 apply,因而有同学认为 Raft 不如 Multi-Paxos。</p><p>dragonboat 的作者对此观点进行了 <a href="https://www.zhihu.com/question/278984902/answer/404439990">反驳</a>,其主要有两个观点:</p><ul><li>乱序并行 apply 不如拆分出更多的 raft 组来并行。</li><li>对于一个通用的共识库,不能乱序 apply 是受限于 RSM 模型本身的限制,并不是 Raft 本身的问题。对于特定场景,乱序 apply 可以达到一定效果,但并不是一个通用性的优化。</li></ul><p>对于第一个观点,从 TiKV 的角度来看,目前默认的 region 大小为 128M,尽管理论上 raft 组数拆的越多,单 raft 组内的串行化 apply 对性能的影响就越小。然而,raft 组过多带来的其他问题也会接踵而来,比如在 TiKV 实际测试中观察到过多的 region 和过大的 LSM Tree 都会导致性能的回退,因而未来 TiDB 的 Dynamic Region 工作计划一方面调大 region 大小到 512MB 至 10GB 从而减少 region 个数,另一方面则是拆分 RocksDB 实例。由此可见,影响 region 大小的因素并不只有 raft 串行化的问题。在未来,一方面 TiKV 的 region 会比现在更大,因此单 raft 组内的串行化问题会更加明显从而可能成为瓶颈;另一方面拆分 RocksDB 实例后 split 也不会再像现在这么轻量,因而实时动态的 split 负载均衡策略相比现在也会趋于保守。总体来看,在 TiKV 内实现乱序 apply 在未来是一个非常有可能的性能优化方向。</p><p>对于第二个观点,的确从通用的共识库的角度出发,乱序 apply 并无太大意义。但从 TiKV 中内嵌的共识算法来看,由于共识层之上的事务层已经定了一次序,因而共识层的重复定序在有些 case 下是没有意义,此时的乱序并行 apply 更可能提升热点场景的性能。</p><p>事实上,如果不支持乱序 apply,那共识算法的乱序 ack 和乱序 commit 可能没太大意义,因为整个共识组的瓶颈受限于最慢的模块。如果只能顺序 apply,那即使乱序 commit 了一批日志,如果这些日志之前存在空洞,那么这批日志也只能在内存中等待而不能被 apply。然而如果支持了乱序 apply,那结合乱序 ack 和乱序 commit 就更可能提高共识组的吞吐上限。比如一旦支持乱序 commit,那可以使用多个 IO depth 来持久化不同批的日志,这样每次 IO 的大小减少了,也可能能够减少每次 IO 的平均时间。此外支持乱序 commit 后也可以将前面存在空洞但确保与空洞日志无依赖关系的一批乱序 commit 日志提前 apply 处理,进而抬高 apply 的瓶颈天花板。</p><p>总体来看,Parallel Apply 能够解决 region 写热点在多核上的扩展性问题,Multi-Paxos 能够解决 region 写热点在现代硬盘上的扩展性问题。因此,Multi-Paxos + Parallel Apply 理论上能够解决写入热点在现代硬件上的扩展性问题。预计能够在写热点场景更充分的利用硬件资源,从而提升性能。</p><p>在写 Hackathon RFC 的时候我更多的关注了对于写热点场景下 Raft 与 Multi-Paxos 的区别,然而对于其他场景,我意外的发现 Raft 和 Multi-Paxos 也能体现不同的价值。</p><p>在 OB 社区 4.0 版本的 <a href="https://mp.weixin.qq.com/s/zKZrl26TIfkCK5EECLURzg">介绍</a> 中,专门提到了一个很大的变动:自适应日志流,即将分片与共识解耦。在不考虑 leader 打散情况下,每个租户在一个进程中可以有多个分片但只会有一个共识组。</p><p><img src="/2022-tidb-hackathon/19.png" alt></p><p>基于传统 Raft 的方案是很难去做出这样的架构设计的,但 Multi-Paxos 可以做到。</p><p><img src="/2022-tidb-hackathon/20.png" alt></p><p>在收益方面,除了完全控制共识组个数和增大 1PC 比例以外,OB 4.0 更是基于自适应日志流架构重构了事务引擎,从而对大事务有了更好的支持,并基于这些工作产品化包装出了单机分布式一体化架构。这个架构的缺点就是灵活扩缩容和实时负载均衡会更难做,但如果考虑云上的部署形态,这些新的问题又可以通过其他方案来缓解。</p><p><img src="/2022-tidb-hackathon/21.png" alt></p><p>总体而看,一个系统的架构设计就是要在关键模块的各种 trade-off 中做出纠结的选择,并且一旦做出了某个选择,就需要基于这个选择更进一步做非常多的产品化工作和优化打磨来提升这些技术对于用户的实际价值。例如,TiDB 选了 TSO 的时间戳获取方案并不代表就不能服务跨数据中心场景了,其也做了 Local/Global TSO 的产品化工作来满足部分用户场景的跨数据中心需求。CRDB 用了 HLC 之后并不仅仅体现在获取事务时间戳更快,其至少还基于 HLC 在 Strong/Stale Follower Read 这一块做了许多工作来减少跨域流量从而降低成本。对于 OB,其选择了 Multi-Paxos 而不是 Raft,并且更进一步在 4.0 架构中提出了单机分布式一体化架构来解决其他数据库很难彻底解决的写热点缓解,大事务支持和 1PC 比例增大等难点。当然,这些技术和产品化的工作短期内很难形成事实标准,也都能够在各自的用户场景产生价值,至于孰优孰劣就很难客观判断了。</p><p><img src="/2022-tidb-hackathon/22.png" alt></p><p>仅就技术而言,如同我在之前有关 <a href="https://zhuanlan.zhihu.com/p/581125699">共识算法综述博客</a> 开头中介绍的一样,对于 Raft 和 Multi Paxos 孰优孰劣这一圣战问题,我的主观看法是对于普通 KV,很可能区别不大。对于结合共识和事务模块的 NewSQL 数据库,Multi-Paxos 能够在整体上为一些难点问题提供一点不一样的思路(例如增大 1PC 比例,缓解写热点问题,大事务支持等等),可能有更高的性能天花板。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简要总结了 2022 TiDB Hackathon 产品组最佳校园奖热点清零队的工作,并简要分享了本人对 Raft 和 Multi-Paxos 异同的看法,希望能够引起更多的讨论。</p><p>感谢您的阅读~</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2022 年 10 月,<a href="https://tidb.net/events/hackathon2022">2022 TiDB </summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="共识算法" scheme="https://tanxinyu.work/tags/%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/"/>
<category term="分布式存储" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AD%98%E5%82%A8/"/>
</entry>
<entry>
<title>Have we reached consensus on consensus?</title>
<link href="https://tanxinyu.work/have-we-reached-consensus-on-consensus/"/>
<id>https://tanxinyu.work/have-we-reached-consensus-on-consensus/</id>
<published>2022-11-07T03:02:32.000Z</published>
<updated>2022-12-15T02:25:02.523Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>分享一下前不久在 PingCAP 实习时做的有关共识算法及其应用的介绍(已经删除了部分不便公开的内容)。</p><p>这次分享主要针对 Raft 和 Paxos 做了相对详细的介绍,并从学术界和工业界的不同视角对比了他们的异同,最后也基于 TiKV 当前架构现状和其他系统的架构设计从一个 intern 的视角拍脑袋介绍了几个可能的演进方向。</p><p>回到 Raft 和 Multi Paxos 孰优孰劣这一圣战问题,可以明确的是对于普通 KV,很可能区别不大。对于结合共识和事务模块的 NewSQL 数据库,Multi-Paxos 能够在整体上为一些难点问题提供一点不一样的思路(例如增大 1PC 比例,缓解写热点问题等等),可能有更高的性能天花板。</p><p>注:本文内容仅代表个人看法。</p><p>注:以下仅为图片,可以在 <a href="https://vevotse3pn.feishu.cn/file/boxcnBKfW8q9E61Bfi314R0hOfe">此处</a> 在线浏览 PPT 原件。</p><h1 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h1><p><img src="/have-we-reached-consensus-on-consensus/1.png" alt><br><img src="/have-we-reached-consensus-on-consensus/2.png" alt><br><img src="/have-we-reached-consensus-on-consensus/3.png" alt><br><img src="/have-we-reached-consensus-on-consensus/4.png" alt><br><img src="/have-we-reached-consensus-on-consensus/5.png" alt><br><img src="/have-we-reached-consensus-on-consensus/6.png" alt><br><img src="/have-we-reached-consensus-on-consensus/7.png" alt><br><img src="/have-we-reached-consensus-on-consensus/8.png" alt><br><img src="/have-we-reached-consensus-on-consensus/9.png" alt><br><img src="/have-we-reached-consensus-on-consensus/10.png" alt><br><img src="/have-we-reached-consensus-on-consensus/11.png" alt><br><img src="/have-we-reached-consensus-on-consensus/12.png" alt><br><img src="/have-we-reached-consensus-on-consensus/13.png" alt><br><img src="/have-we-reached-consensus-on-consensus/14.png" alt><br><img src="/have-we-reached-consensus-on-consensus/15.png" alt><br><img src="/have-we-reached-consensus-on-consensus/16.png" alt><br><img src="/have-we-reached-consensus-on-consensus/17.png" alt><br><img src="/have-we-reached-consensus-on-consensus/18.png" alt><br><img src="/have-we-reached-consensus-on-consensus/19.png" alt><br><img src="/have-we-reached-consensus-on-consensus/20.png" alt><br><img src="/have-we-reached-consensus-on-consensus/21.png" alt><br><img src="/have-we-reached-consensus-on-consensus/22.png" alt><br><img src="/have-we-reached-consensus-on-consensus/23.png" alt><br><img src="/have-we-reached-consensus-on-consensus/24.png" alt><br><img src="/have-we-reached-consensus-on-consensus/25.png" alt><br><img src="/have-we-reached-consensus-on-consensus/26.png" alt><br><img src="/have-we-reached-consensus-on-consensus/27.png" alt><br><img src="/have-we-reached-consensus-on-consensus/28.png" alt><br><img src="/have-we-reached-consensus-on-consensus/29.png" alt><br><img src="/have-we-reached-consensus-on-consensus/30.png" alt><br><img src="/have-we-reached-consensus-on-consensus/31.png" alt><br><img src="/have-we-reached-consensus-on-consensus/32.png" alt><br><img src="/have-we-reached-consensus-on-consensus/33.png" alt><br><img src="/have-we-reached-consensus-on-consensus/34.png" alt><br><img src="/have-we-reached-consensus-on-consensus/35.png" alt><br><img src="/have-we-reached-consensus-on-consensus/36.png" alt><br><img src="/have-we-reached-consensus-on-consensus/37.png" alt><br><img src="/have-we-reached-consensus-on-consensus/38.png" alt><br><img src="/have-we-reached-consensus-on-consensus/39.png" alt><br><img src="/have-we-reached-consensus-on-consensus/40.png" alt><br><img src="/have-we-reached-consensus-on-consensus/41.png" alt><br><img src="/have-we-reached-consensus-on-consensus/42.png" alt><br><img src="/have-we-reached-consensus-on-consensus/43.png" alt><br><img src="/have-we-reached-consensus-on-consensus/44.png" alt><br><img src="/have-we-reached-consensus-on-consensus/45.png" alt><br><img src="/have-we-reached-consensus-on-consensus/46.png" alt><br><img src="/have-we-reached-consensus-on-consensus/47.png" alt><br><img src="/have-we-reached-consensus-on-consensus/48.png" alt><br><img src="/have-we-reached-consensus-on-consensus/49.png" alt><br><img src="/have-we-reached-consensus-on-consensus/50.png" alt><br><img src="/have-we-reached-consensus-on-consensus/51.png" alt><br><img src="/have-we-reached-consensus-on-consensus/52.png" alt><br><img src="/have-we-reached-consensus-on-consensus/53.png" alt><br><img src="/have-we-reached-consensus-on-consensus/54.png" alt><br><img src="/have-we-reached-consensus-on-consensus/55.png" alt><br><img src="/have-we-reached-consensus-on-consensus/56.png" alt><br><img src="/have-we-reached-consensus-on-consensus/57.png" alt><br><img src="/have-we-reached-consensus-on-consensus/58.png" alt><br><img src="/have-we-reached-consensus-on-consensus/59.png" alt><br><img src="/have-we-reached-consensus-on-consensus/60.png" alt><br><img src="/have-we-reached-consensus-on-consensus/61.png" alt><br><img src="/have-we-reached-consensus-on-consensus/62.png" alt><br><img src="/have-we-reached-consensus-on-consensus/63.png" alt><br><img src="/have-we-reached-consensus-on-consensus/64.png" alt><br><img src="/have-we-reached-consensus-on-consensus/65.png" alt><br><img src="/have-we-reached-consensus-on-consensus/66.png" alt><br><img src="/have-we-reached-consensus-on-consensus/67.png" alt></p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><h2 id="论文"><a href="#论文" class="headerlink" title="论文"></a>论文</h2><ul><li><a href="https://lamport.azurewebsites.net/pubs/paxos-simple.pdf">Paxos Made Simple</a></li><li><a href="https://www.cs.utexas.edu/users/lorenzo/corsi/cs380d/papers/paper2-1.pdf">Paxos Made Live - An Engineering Perspective</a></li><li><a href="https://dada.cs.washington.edu/research/tr/2009/09/UW-CSE-09-09-02.PDF">Multi-Paxos: An Implementation and Evaluation</a></li><li><a href="https://people.csail.mit.edu/malte/pub/papers/2015-osr-raft.pdf">Raft Refloated: Do We Have Consensus?</a></li><li><a href="https://www.vldb.org/pvldb/vol11/p1849-cao.pdf">PolarFS: An Ultra-low Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database</a></li><li><a href="https://dl.acm.org/doi/abs/10.14778/3137765.3137778">PaxosStore: High-availability Storage Made Practical in WeChat</a></li><li><a href="https://dl.acm.org/doi/abs/10.14778/1938545.1938549">Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore</a></li><li><a href="https://dl.acm.org/doi/abs/10.1145/3380787.3393681">Paxos vs Raft: have we reached consensus on distributed consensus?</a></li><li><a href="https://dl.acm.org/doi/pdf/10.1145/3293611.3331595">On the Parallels between Paxos and Raft, and how to Port Optimizations</a></li><li><a href="https://www.usenix.org/system/files/fast21-chen-hao.pdf">SpanDB: A Fast, Cost-Effective LSM-tree Based KV Store on Hybrid Storage</a></li></ul><h2 id="博客"><a href="#博客" class="headerlink" title="博客"></a>博客</h2><ul><li><a href="http://icyfenix.cn/distribution/consensus/raft.html">凤凰架构 Multi-Paxos</a></li><li><a href="https://mp.weixin.qq.com/s/4D5_x-Ftg8Qzl6hPdoVTxQ">将 paxos 和 raft 统一为一个协议:abstract-paxos</a></li><li><a href="https://mp.weixin.qq.com/s/3OZgqEWQcEf6V9v6eoSUUA">可靠分布式系统-paxos 的直观解释</a></li><li><a href="https://blog.csdn.net/sinat_22338935/article/details/114320664">Page Cache 引起的业务问题处理</a></li><li><a href="https://segmentfault.com/a/1190000038967218">深入了解 MySQL 主从复制的原理</a></li><li><a href="https://github.com/oceanbase/oceanbase">OceanBase 文档</a></li><li><a href="https://mp.weixin.qq.com/s/zKZrl26TIfkCK5EECLURzg">OceanBase 社区版 4.0 发版:一个全新的里程碑</a></li><li><a href="https://www.bilibili.com/video/BV14d4y1D7wz/?spm_id_from=333.337.search-card.all.click&vd_source=c856dfca0df73fe9dce87bfc54acabc5">【PingCAP Infra Meetup】No.151 TiDB 事务模型演进</a></li><li><a href="https://cloud.tencent.com/developer/article/1352070">Paxos 和 Raft 的前世今生</a></li><li><a href="https://www.infoq.cn/article/wechat-paxosstore-paxos-algorithm-protocol/">微信 PaxosStore:深入浅出 Paxos 算法协议</a></li><li><a href="https://zhuanlan.zhihu.com/p/45720365">共识算法 之 Basic Paxos</a></li><li><a href="https://www.zhihu.com/question/57321934/answer/152640954">Paxos(Multi-Paxos) 在工程实现中需要注意哪些问题?</a></li><li><a href="https://ongardie.net/static/raft/userstudy/paxos.pptx">Implementing Replicated Logs with Paxos</a></li><li><a href="https://github.com/TPC-TiKV/rfc">2021 TiDB Hackathon TPC-TiKV RFC</a></li><li><a href="https://github.com/FWH-TiKV/RFC">2022 TiDB Hackathon FWH-TiKV RFC</a></li></ul>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>分享一下前不久在 PingCAP 实习时做的有关共识算法及其应用的介绍(已经删除了部分不便公开的内容)。</p>
<p>这次分享主要针对 R</summary>
<category term="分享" scheme="https://tanxinyu.work/tags/%E5%88%86%E4%BA%AB/"/>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="共识算法" scheme="https://tanxinyu.work/tags/%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>TiKV 源码阅读三部曲(三)写流程</title>
<link href="https://tanxinyu.work/tikv-source-code-reading-write/"/>
<id>https://tanxinyu.work/tikv-source-code-reading-write/</id>
<published>2022-10-05T10:37:12.000Z</published>
<updated>2022-10-08T03:20:35.422Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value 数据库,目前已经是 <a href="https://www.cncf.io/projects/">CNCF 基金会</a> 的顶级项目。</p><p>作为一个新同学,需要一定的前期准备才能够有能力参与 TiKV 社区的代码开发,包括但不限于学习 Rust 语言,理解 TiKV 的原理和在前两者的基础上了解熟悉 TiKV 的源码。</p><p><a href="https://pingcap.com/zh/blog/?tag=TiKV%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90">TiKV 官方源码解析文档</a> 详细地介绍了 TiKV 3.x 版本重要模块的设计要点,主要流程和相应代码片段,是学习 TiKV 源码必读的学习资料。当前 TiKV 已经迭代到了 6.x 版本,不仅引入了很多新的功能和优化,而且对源码也进行了多次重构,因而一些官方源码解析文档中的代码片段已经不复存在,这使得读者在阅读源码解析文档时无法对照最新源码加深理解;此外尽管 TiKV 官方源码解析文档系统地介绍了若干重要模块的工作,但并没有将读写流程全链路串起来去介绍经过的模块和对应的代码片段,实际上尽快地熟悉读写流程全链路会更利于新同学从全局角度理解代码。</p><p>基于以上存在的问题,笔者将基于 6.1 版本的源码撰写三篇博客,分别介绍以下三个方面:</p><ul><li><a href="https://tanxinyu.work/tikv-source-code-reading-module/">TiKV 源码阅读三部曲(一)重要模块</a>:TiKV 的基本概念,TiKV 读写路径上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的方案</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-read/">TiKV 源码阅读三部曲(二)读流程</a>:TiKV 中一条读请求的全链路流程</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-write/">TiKV 源码阅读三部曲(三)写流程</a>:TiKV 中一条写请求的全链路流程</li></ul><p>希望此三篇博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>本文为第三篇博客,将主要介绍 TiKV 中一条写请求的全链路流程。</p><h2 id="写流程"><a href="#写流程" class="headerlink" title="写流程"></a>写流程</h2><p>以下四篇博客由上到下分别介绍了 TiKV 3.x 版本 KVService,Storage 和 RaftStore 模块对于分布式事务请求的执行流程。</p><ul><li><a href="https://cn.pingcap.com/blog/tikv-source-code-reading-9">TiKV 源码解析系列文章(九)Service 层处理流程解析</a></li><li><a href="https://cn.pingcap.com/blog/tikv-source-code-reading-11">TiKV 源码解析系列文章(十一)Storage - 事务控制层</a></li><li><a href="https://cn.pingcap.com/blog/tikv-source-code-reading-12">TiKV 源码解析系列文章(十二)分布式事务</a></li><li><a href="https://cn.pingcap.com/blog/tikv-source-code-reading-18">TiKV 源码解析系列文章(十八)Raft Propose 的 Commit 和 Apply 情景分析</a></li></ul><p>本小节将在 TiKV 6.1 版本的基础上,以一条 PreWrite 请求为例,介绍当前版本的写请求全链路执行流程。</p><h3 id="KVService"><a href="#KVService" class="headerlink" title="KVService"></a>KVService</h3><p>在 KVService 层,通过 handle_request 和 txn_command_future 宏,PreWrite 接口的请求会直接被路由到 <code>Storage::sched_txn_command</code> 函数中。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-keyword">impl</span><T: RaftStoreRouter<E::Local> + <span class="hljs-symbol">'static</span>, E: Engine, L: LockManager, F: KvFormat> Tikv<br> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Service</span><T, E, L, F><br>{<br> handle_request!(<br> kv_prewrite,<br> future_prewrite,<br> PrewriteRequest,<br> PrewriteResponse,<br> has_time_detail<br> );<br>} <br><br>txn_command_future!(future_prewrite, PrewriteRequest, PrewriteResponse, (v, resp, tracker) {{<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Ok</span>(v) = &v {<br> resp.<span class="hljs-title function_ invoke__">set_min_commit_ts</span>(v.min_commit_ts.<span class="hljs-title function_ invoke__">into_inner</span>());<br> resp.<span class="hljs-title function_ invoke__">set_one_pc_commit_ts</span>(v.one_pc_commit_ts.<span class="hljs-title function_ invoke__">into_inner</span>());<br> GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">with_tracker</span>(tracker, |tracker| {<br> tracker.<span class="hljs-title function_ invoke__">write_scan_detail</span>(resp.<span class="hljs-title function_ invoke__">mut_exec_details_v2</span>().<span class="hljs-title function_ invoke__">mut_scan_detail_v2</span>());<br> tracker.<span class="hljs-title function_ invoke__">write_write_detail</span>(resp.<span class="hljs-title function_ invoke__">mut_exec_details_v2</span>().<span class="hljs-title function_ invoke__">mut_write_detail</span>());<br> });<br> }<br> resp.<span class="hljs-title function_ invoke__">set_errors</span>(<span class="hljs-title function_ invoke__">extract_key_errors</span>(v.<span class="hljs-title function_ invoke__">map</span>(|v| v.locks)).<span class="hljs-title function_ invoke__">into</span>());<br>}});<br></code></pre></td></tr></table></figure><h3 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h3><p>在 Storage 模块,其会将请求路由到 <code>Scheduler::run_cmd</code> 函数中,并进一步路由到 <code>Scheduler::schedule_command</code> 函数中。在 <code>schedule_command</code> 函数中,当前 command 连同 callback 等上下文会被保存到 task_slots 中,如果当前线程申请到了所有 latch 则会调用 execute 函数继续执行该 task,否则如前文所述,当前任务便会被阻塞在某些 latch 上等待其他线程去唤醒进而执行,当前线程会直接返回并执行其他的工作。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">// The entry point of the storage scheduler. Not only transaction commands need</span><br><span class="hljs-comment">// to access keys serially.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">sched_txn_command</span><T: StorageCallbackType>(<br> &<span class="hljs-keyword">self</span>,<br> cmd: TypedCommand<T>,<br> callback: Callback<T>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><()> {<br><br> ...<br> <br> <span class="hljs-keyword">self</span>.sched.<span class="hljs-title function_ invoke__">run_cmd</span>(cmd, T::<span class="hljs-title function_ invoke__">callback</span>(callback));<br><br> <span class="hljs-title function_ invoke__">Ok</span>(())<br>}<br><br><span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">in</span> crate::storage) <span class="hljs-keyword">fn</span> <span class="hljs-title function_">run_cmd</span>(&<span class="hljs-keyword">self</span>, cmd: Command, callback: StorageCallback) {<br> <span class="hljs-comment">// write flow control</span><br> <span class="hljs-keyword">if</span> cmd.<span class="hljs-title function_ invoke__">need_flow_control</span>() && <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">too_busy</span>(cmd.<span class="hljs-title function_ invoke__">ctx</span>().region_id) {<br> SCHED_TOO_BUSY_COUNTER_VEC.<span class="hljs-title function_ invoke__">get</span>(cmd.<span class="hljs-title function_ invoke__">tag</span>()).<span class="hljs-title function_ invoke__">inc</span>();<br> callback.<span class="hljs-title function_ invoke__">execute</span>(ProcessResult::Failed {<br> err: StorageError::<span class="hljs-title function_ invoke__">from</span>(StorageErrorInner::SchedTooBusy),<br> });<br> <span class="hljs-keyword">return</span>;<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">schedule_command</span>(cmd, callback);<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">schedule_command</span>(&<span class="hljs-keyword">self</span>, cmd: Command, callback: StorageCallback) {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cid</span> = <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">gen_id</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tracker</span> = <span class="hljs-title function_ invoke__">get_tls_tracker_token</span>();<br> debug!(<span class="hljs-string">"received new command"</span>; <span class="hljs-string">"cid"</span> => cid, <span class="hljs-string">"cmd"</span> => ?cmd, <span class="hljs-string">"tracker"</span> => ?tracker);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tag</span> = cmd.<span class="hljs-title function_ invoke__">tag</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">priority_tag</span> = <span class="hljs-title function_ invoke__">get_priority_tag</span>(cmd.<span class="hljs-title function_ invoke__">priority</span>());<br> SCHED_STAGE_COUNTER_VEC.<span class="hljs-title function_ invoke__">get</span>(tag).new.<span class="hljs-title function_ invoke__">inc</span>();<br> SCHED_COMMANDS_PRI_COUNTER_VEC_STATIC<br> .<span class="hljs-title function_ invoke__">get</span>(priority_tag)<br> .<span class="hljs-title function_ invoke__">inc</span>();<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">task_slot</span> = <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">get_task_slot</span>(cid);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tctx</span> = task_slot.<span class="hljs-title function_ invoke__">entry</span>(cid).<span class="hljs-title function_ invoke__">or_insert_with</span>(|| {<br> <span class="hljs-keyword">self</span>.inner<br> .<span class="hljs-title function_ invoke__">new_task_context</span>(Task::<span class="hljs-title function_ invoke__">new</span>(cid, tracker, cmd), callback)<br> });<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.inner.latches.<span class="hljs-title function_ invoke__">acquire</span>(&<span class="hljs-keyword">mut</span> tctx.lock, cid) {<br> fail_point!(<span class="hljs-string">"txn_scheduler_acquire_success"</span>);<br> tctx.<span class="hljs-title function_ invoke__">on_schedule</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = tctx.task.<span class="hljs-title function_ invoke__">take</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-title function_ invoke__">drop</span>(task_slot);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">execute</span>(task);<br> <span class="hljs-keyword">return</span>;<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = tctx.task.<span class="hljs-title function_ invoke__">as_ref</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">deadline</span> = task.cmd.<span class="hljs-title function_ invoke__">deadline</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cmd_ctx</span> = task.cmd.<span class="hljs-title function_ invoke__">ctx</span>().<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">fail_fast_or_check_deadline</span>(cid, tag, cmd_ctx, deadline);<br> fail_point!(<span class="hljs-string">"txn_scheduler_acquire_fail"</span>);<br>}<br></code></pre></td></tr></table></figure><p>在 execute 函数中,当前线程会生成一个异步任务 spawn 到另一个 worker 线程池中去,该任务主要包含以下两个步骤:</p><ul><li>使用 <code>Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await</code> 获取 snapshot。此步骤与上文读流程中获取 snapshot 的步骤相同,可能通过 ReadLocal 也可能通过 ReadIndex 来获取引擎的 snapshot,此小节不在赘述</li><li>使用 <code>sched.process(snapshot, task).await</code> 基于获取到的 snapshot 和对应 task 去调用 <code>scheduler::process</code> 函数,进而被路由到 <code>scheduler::process_write</code> 函数中</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Executes the task in the sched pool.</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">execute</span>(&<span class="hljs-keyword">self</span>, <span class="hljs-keyword">mut</span> task: Task) {<br> <span class="hljs-title function_ invoke__">set_tls_tracker_token</span>(task.tracker);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">sched</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">get_sched_pool</span>(task.cmd.<span class="hljs-title function_ invoke__">priority</span>())<br> .pool<br> .<span class="hljs-title function_ invoke__">spawn</span>(<span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> <br> ...<br><br> <span class="hljs-comment">// The program is currently in scheduler worker threads.</span><br> <span class="hljs-comment">// Safety: `self.inner.worker_pool` should ensure that a TLS engine exists.</span><br> <span class="hljs-keyword">match</span> <span class="hljs-keyword">unsafe</span> { <span class="hljs-title function_ invoke__">with_tls_engine</span>(|engine: &E| kv::<span class="hljs-title function_ invoke__">snapshot</span>(engine, snap_ctx)) }.<span class="hljs-keyword">await</span><br> {<br> <span class="hljs-title function_ invoke__">Ok</span>(snapshot) => {<br> <br> ...<br><br> sched.<span class="hljs-title function_ invoke__">process</span>(snapshot, task).<span class="hljs-keyword">await</span>;<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(err) => {<br> ...<br> }<br> }<br> })<br> .<span class="hljs-title function_ invoke__">unwrap</span>();<br>}<br><br> <span class="hljs-comment">/// Process the task in the current thread.</span><br><span class="hljs-keyword">async</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">process</span>(<span class="hljs-keyword">self</span>, snapshot: E::Snap, task: Task) {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">check_task_deadline_exceeded</span>(&task) {<br> <span class="hljs-keyword">return</span>;<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">resource_tag</span> = <span class="hljs-keyword">self</span>.inner.resource_tag_factory.<span class="hljs-title function_ invoke__">new_tag</span>(task.cmd.<span class="hljs-title function_ invoke__">ctx</span>());<br> <span class="hljs-keyword">async</span> {<br> <br> ...<br><br> <span class="hljs-keyword">if</span> task.cmd.<span class="hljs-title function_ invoke__">readonly</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">process_read</span>(snapshot, task, &<span class="hljs-keyword">mut</span> statistics);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, task, &<span class="hljs-keyword">mut</span> statistics).<span class="hljs-keyword">await</span>;<br> };<br> <br> ...<br> }<br> .<span class="hljs-title function_ invoke__">in_resource_metering_tag</span>(resource_tag)<br> .<span class="hljs-keyword">await</span>;<br>}<br></code></pre></td></tr></table></figure><p><code>scheduler::process_write</code> 函数是事务处理的关键函数,目前已经有近四百行,里面夹杂了很多新特性和新优化的复杂逻辑,其中最重要的逻辑有两个:</p><ul><li>使用 <code>task.cmd.process_write(snapshot, context).map_err(StorageError::from)</code> 根据 snapshot 和 task 执行事务对应的语义:可以从 <code>Command::process_write</code> 函数看到不同的请求都有不同的实现,每种请求都可能根据 snapshot 去底层获取一些数据并尝试写入一些数据。有关 PreWrite 和其他请求的具体操作可以参照 <a href="https://pingcap.com/zh/blog/tikv-source-code-reading-12">TiKV 源码解析系列文章(十二)分布式事务</a>,此处不再赘述。需要注意的是,此时的写入仅仅缓存在了 WriteData 中,并没有对底层引擎进行实际修改。</li><li>使用 <code>engine.async_write_ext(&ctx, to_be_write, engine_cb, proposed_cb, committed_cb)</code> 将缓存的 WriteData 实际写入到 engine 层,对于 RaftKV 来说则是表示一次 propose,想要对这一批 WriteData commit 且 apply</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">async</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">process_write</span>(<span class="hljs-keyword">self</span>, snapshot: E::Snap, task: Task, statistics: &<span class="hljs-keyword">mut</span> Statistics) {<br> <br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">write_result</span> = {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">_guard</span> = sample.<span class="hljs-title function_ invoke__">observe_cpu</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">context</span> = WriteContext {<br> lock_mgr: &<span class="hljs-keyword">self</span>.inner.lock_mgr,<br> concurrency_manager: <span class="hljs-keyword">self</span>.inner.concurrency_manager.<span class="hljs-title function_ invoke__">clone</span>(),<br> extra_op: task.extra_op,<br> statistics,<br> async_apply_prewrite: <span class="hljs-keyword">self</span>.inner.enable_async_apply_prewrite,<br> };<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin_instant</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">unsafe</span> {<br> with_perf_context::<E, _, _>(tag, || {<br> task.cmd<br> .<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context)<br> .<span class="hljs-title function_ invoke__">map_err</span>(StorageError::from)<br> })<br> };<br> SCHED_PROCESSING_READ_HISTOGRAM_STATIC<br> .<span class="hljs-title function_ invoke__">get</span>(tag)<br> .<span class="hljs-title function_ invoke__">observe</span>(begin_instant.<span class="hljs-title function_ invoke__">saturating_elapsed_secs</span>());<br> res<br> };<br><br> ...<br><br> <span class="hljs-comment">// Safety: `self.sched_pool` ensures a TLS engine exists.</span><br> <span class="hljs-keyword">unsafe</span> {<br> <span class="hljs-title function_ invoke__">with_tls_engine</span>(|engine: &E| {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Err</span>(e) =<br> engine.<span class="hljs-title function_ invoke__">async_write_ext</span>(&ctx, to_be_write, engine_cb, proposed_cb, committed_cb)<br> {<br> SCHED_STAGE_COUNTER_VEC.<span class="hljs-title function_ invoke__">get</span>(tag).async_write_err.<span class="hljs-title function_ invoke__">inc</span>();<br><br> info!(<span class="hljs-string">"engine async_write failed"</span>; <span class="hljs-string">"cid"</span> => cid, <span class="hljs-string">"err"</span> => ?e);<br> scheduler.<span class="hljs-title function_ invoke__">finish_with_err</span>(cid, e);<br> }<br> })<br> }<br>}<br><br><span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">crate</span>) <span class="hljs-keyword">fn</span> <span class="hljs-title function_">process_write</span><S: Snapshot, L: LockManager>(<br> <span class="hljs-keyword">self</span>,<br> snapshot: S,<br> context: WriteContext<<span class="hljs-symbol">'_</span>, L>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><WriteResult> {<br> <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span> {<br> Command::<span class="hljs-title function_ invoke__">Prewrite</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">PrewritePessimistic</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">AcquirePessimisticLock</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">Commit</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">Cleanup</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">Rollback</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">PessimisticRollback</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">ResolveLock</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">ResolveLockLite</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">TxnHeartBeat</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">CheckTxnStatus</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">CheckSecondaryLocks</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">Pause</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">RawCompareAndSwap</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> Command::<span class="hljs-title function_ invoke__">RawAtomicStore</span>(t) => t.<span class="hljs-title function_ invoke__">process_write</span>(snapshot, context),<br> _ => <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"unsupported write command"</span>),<br> }<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">async_write_ext</span>(<br> &<span class="hljs-keyword">self</span>,<br> ctx: &Context,<br> batch: WriteData,<br> write_cb: Callback<()>,<br> proposed_cb: <span class="hljs-type">Option</span><ExtCallback>,<br> committed_cb: <span class="hljs-type">Option</span><ExtCallback>,<br>) <span class="hljs-punctuation">-></span> kv::<span class="hljs-type">Result</span><()> {<br> fail_point!(<span class="hljs-string">"raftkv_async_write"</span>);<br> <span class="hljs-keyword">if</span> batch.modifies.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">Err</span>(KvError::<span class="hljs-title function_ invoke__">from</span>(KvErrorInner::EmptyRequest));<br> }<br><br> ASYNC_REQUESTS_COUNTER_VEC.write.all.<span class="hljs-title function_ invoke__">inc</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin_instant</span> = Instant::<span class="hljs-title function_ invoke__">now_coarse</span>();<br><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">exec_write_requests</span>(<br> ctx,<br> batch,<br> <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-keyword">move</span> |res| <span class="hljs-keyword">match</span> res {<br><br> ...<br><br> }),<br> proposed_cb,<br> committed_cb,<br> )<br> .<span class="hljs-title function_ invoke__">map_err</span>(|e| {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">status_kind</span> = <span class="hljs-title function_ invoke__">get_status_kind_from_error</span>(&e);<br> ASYNC_REQUESTS_COUNTER_VEC.write.<span class="hljs-title function_ invoke__">get</span>(status_kind).<span class="hljs-title function_ invoke__">inc</span>();<br> e.<span class="hljs-title function_ invoke__">into</span>()<br> })<br>}<br></code></pre></td></tr></table></figure><p>进入 <code>raftkv::async_write_ext</code> 函数后,其进而通过 <code>raftkv::exec_write_requests -> RaftStoreRouter::send_command</code> 的调用栈将 task 连带 callback 发送给 RaftBatchSystem 交由 RaftStore 模块处理。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">exec_write_requests</span>(<br> &<span class="hljs-keyword">self</span>,<br> ctx: &Context,<br> batch: WriteData,<br> write_cb: Callback<CmdRes<E::Snapshot>>,<br> proposed_cb: <span class="hljs-type">Option</span><ExtCallback>,<br> committed_cb: <span class="hljs-type">Option</span><ExtCallback>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><()> {<br> <br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cb</span> = StoreCallback::<span class="hljs-title function_ invoke__">write_ext</span>(<br> <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-keyword">move</span> |resp| {<br> <span class="hljs-title function_ invoke__">write_cb</span>(<span class="hljs-title function_ invoke__">on_write_result</span>(resp).<span class="hljs-title function_ invoke__">map_err</span>(Error::into));<br> }),<br> proposed_cb,<br> committed_cb,<br> );<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">extra_opts</span> = RaftCmdExtraOpts {<br> deadline: batch.deadline,<br> disk_full_opt: batch.disk_full_opt,<br> };<br> <span class="hljs-keyword">self</span>.router.<span class="hljs-title function_ invoke__">send_command</span>(cmd, cb, extra_opts)?;<br><br> <span class="hljs-title function_ invoke__">Ok</span>(())<br>}<br><br> <span class="hljs-comment">/// Sends RaftCmdRequest to local store.</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">send_command</span>(<br> &<span class="hljs-keyword">self</span>,<br> req: RaftCmdRequest,<br> cb: Callback<EK::Snapshot>,<br> extra_opts: RaftCmdExtraOpts,<br>) <span class="hljs-punctuation">-></span> RaftStoreResult<()> {<br> send_command_impl::<EK, _>(<span class="hljs-keyword">self</span>, req, cb, extra_opts)<br>}<br></code></pre></td></tr></table></figure><h3 id="RaftStore"><a href="#RaftStore" class="headerlink" title="RaftStore"></a>RaftStore</h3><p>直接定位到 <code>RaftPoller</code> 的 <code>handle_normal</code> 函数。</p><p>与处理 ReadIndex 请求相似, <code>RaftPoller</code> 会首先尝试获取 <code>messages_per_tick</code> 次路由到该状态机的消息,接着调用 <code>PeerFsmDelegate::handle_msgs</code> 函数进行处理,</p><p>这里依然只列出了我们需要关注的几种消息类型:</p><ul><li>RaftMessage: 其他 Peer 发送过来 Raft 消息,包括心跳、日志、投票消息等。</li><li>RaftCommand: 上层提出的 proposal,其中包含了需要通过 Raft 同步的操作,以及操作成功之后需要调用的 callback 函数。PreWrite 包装出的 RaftCommand 便是最正常的 proposal。</li><li>ApplyRes: ApplyFsm 在将日志应用到状态机之后发送给 PeerFsm 的消息,用于在进行操作之后更新某些内存状态。</li></ul><p>对于 PreWrite 请求,其会进入 <code>PeerMsg::RaftCommand(cmd)</code> 分支,进而以 <code>PeerFsmDelegate::propose_raft_command -> PeerFsmDelegate::propose_raft_command_internal -> Peer::propose -> Peer::propose_normal</code> 的调用链最终被 propose 到 raft-rs 的 RawNode 接口中,同时其 callback 会连带该请求的 logIndex 被 push 到该 Peer 的 <code>proposals</code> 中去。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span><EK: KvEngine, ER: RaftEngine, T: Transport> PollHandler<PeerFsm<EK, ER>, StoreFsm<EK>><br> <span class="hljs-keyword">for</span> <span class="hljs-title class_">RaftPoller</span><EK, ER, T><br>{<br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_normal</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> peer: &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = PeerFsm<EK, ER>>,<br> ) <span class="hljs-punctuation">-></span> HandleResult {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">handle_result</span> = HandleResult::KeepProcessing;<br><br> ...<br><br> <span class="hljs-keyword">while</span> <span class="hljs-keyword">self</span>.peer_msg_buf.<span class="hljs-title function_ invoke__">len</span>() < <span class="hljs-keyword">self</span>.messages_per_tick {<br> <span class="hljs-keyword">match</span> peer.receiver.<span class="hljs-title function_ invoke__">try_recv</span>() {<br> <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> we may need a way to optimize the message copy.</span><br> <span class="hljs-title function_ invoke__">Ok</span>(msg) => {<br> ...<br> <span class="hljs-keyword">self</span>.peer_msg_buf.<span class="hljs-title function_ invoke__">push</span>(msg);<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Empty) => {<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Disconnected) => {<br> peer.<span class="hljs-title function_ invoke__">stop</span>();<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">delegate</span> = PeerFsmDelegate::<span class="hljs-title function_ invoke__">new</span>(peer, &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.poll_ctx);<br> delegate.<span class="hljs-title function_ invoke__">handle_msgs</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.peer_msg_buf);<br> <span class="hljs-comment">// No readiness is generated and using sync write, skipping calling ready and</span><br> <span class="hljs-comment">// release early.</span><br> <span class="hljs-keyword">if</span> !delegate.<span class="hljs-title function_ invoke__">collect_ready</span>() && <span class="hljs-keyword">self</span>.poll_ctx.sync_write_worker.<span class="hljs-title function_ invoke__">is_some</span>() {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">HandleResult</span>::StopAt { skip_end, .. } = &<span class="hljs-keyword">mut</span> handle_result {<br> *skip_end = <span class="hljs-literal">true</span>;<br> }<br> }<br><br> handle_result<br> }<br>}<br><br><span class="hljs-keyword">impl</span><<span class="hljs-symbol">'a</span>, EK, ER, T: Transport> PeerFsmDelegate<<span class="hljs-symbol">'a</span>, EK, ER, T><br><span class="hljs-keyword">where</span><br> EK: KvEngine,<br> ER: RaftEngine,<br>{<br> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_msgs</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, msgs: &<span class="hljs-keyword">mut</span> <span class="hljs-type">Vec</span><PeerMsg<EK>>) {<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">m</span> <span class="hljs-keyword">in</span> msgs.<span class="hljs-title function_ invoke__">drain</span>(..) {<br> <span class="hljs-keyword">match</span> m {<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftMessage</span>(msg) => {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Err</span>(e) = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_raft_message</span>(msg) {<br> error!(%e;<br> <span class="hljs-string">"handle raft message err"</span>;<br> <span class="hljs-string">"region_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">region_id</span>(),<br> <span class="hljs-string">"peer_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">peer_id</span>(),<br> );<br> }<br> }<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftCommand</span>(cmd) => {<br> ...<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_raft_command</span>(<br> cmd.request,<br> cmd.callback,<br> cmd.extra_opts.disk_full_opt,<br> );<br> }<br> }<br> PeerMsg::ApplyRes { res } => {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_apply_res</span>(res);<br> }<br> ...<br> }<br> }<br>}<br><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">propose</span><T: Transport>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br> <span class="hljs-keyword">mut</span> cb: Callback<EK::Snapshot>,<br> req: RaftCmdRequest,<br> <span class="hljs-keyword">mut</span> err_resp: RaftCmdResponse,<br> <span class="hljs-keyword">mut</span> disk_full_opt: DiskFullOpt,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">policy</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">inspect</span>(&req);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">match</span> policy {<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadLocal) | <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::StaleRead) => {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">read_local</span>(ctx, req, cb);<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex) => <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">read_index</span>(ctx, req, err_resp, cb),<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeTransferLeader) => {<br> <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_transfer_leader</span>(ctx, req, cb);<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeNormal) => {<br> <span class="hljs-comment">// For admin cmds, only region split/merge comes here.</span><br> <span class="hljs-keyword">if</span> req.<span class="hljs-title function_ invoke__">has_admin_request</span>() {<br> disk_full_opt = DiskFullOpt::AllowedOnAlmostFull;<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">check_normal_proposal_with_disk_full_opt</span>(ctx, disk_full_opt)<br> .<span class="hljs-title function_ invoke__">and_then</span>(|_| <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_normal</span>(ctx, req))<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeConfChange) => <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_conf_change</span>(ctx, &req),<br> <span class="hljs-title function_ invoke__">Err</span>(e) => <span class="hljs-title function_ invoke__">Err</span>(e),<br> };<br> fail_point!(<span class="hljs-string">"after_propose"</span>);<br><br> <span class="hljs-keyword">match</span> res {<br> <span class="hljs-title function_ invoke__">Err</span>(e) => {<br> cmd_resp::<span class="hljs-title function_ invoke__">bind_error</span>(&<span class="hljs-keyword">mut</span> err_resp, e);<br> cb.<span class="hljs-title function_ invoke__">invoke_with_response</span>(err_resp);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">post_propose_fail</span>(req_admin_cmd_type);<br> <span class="hljs-literal">false</span><br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(Either::<span class="hljs-title function_ invoke__">Right</span>(idx)) => {<br> <span class="hljs-keyword">if</span> !cb.<span class="hljs-title function_ invoke__">is_none</span>() {<br> <span class="hljs-keyword">self</span>.cmd_epoch_checker.<span class="hljs-title function_ invoke__">attach_to_conflict_cmd</span>(idx, cb);<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">post_propose_fail</span>(req_admin_cmd_type);<br> <span class="hljs-literal">false</span><br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(Either::<span class="hljs-title function_ invoke__">Left</span>(idx)) => {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">has_applied_to_current_term</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">has_applied_to_current_term</span>();<br> <span class="hljs-keyword">if</span> has_applied_to_current_term {<br> <span class="hljs-comment">// After this peer has applied to current term and passed above checking</span><br> <span class="hljs-comment">// including `cmd_epoch_checker`, we can safely guarantee</span><br> <span class="hljs-comment">// that this proposal will be committed if there is no abnormal leader transfer</span><br> <span class="hljs-comment">// in the near future. Thus proposed callback can be called.</span><br> cb.<span class="hljs-title function_ invoke__">invoke_proposed</span>();<br> }<br> <span class="hljs-keyword">if</span> is_urgent {<br> <span class="hljs-keyword">self</span>.last_urgent_proposal_idx = idx;<br> <span class="hljs-comment">// Eager flush to make urgent proposal be applied on all nodes as soon as</span><br> <span class="hljs-comment">// possible.</span><br> <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">skip_bcast_commit</span>(<span class="hljs-literal">false</span>);<br> }<br> <span class="hljs-keyword">self</span>.should_wake_up = <span class="hljs-literal">true</span>;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = Proposal {<br> is_conf_change: req_admin_cmd_type == <span class="hljs-title function_ invoke__">Some</span>(AdminCmdType::ChangePeer)<br> || req_admin_cmd_type == <span class="hljs-title function_ invoke__">Some</span>(AdminCmdType::ChangePeerV2),<br> index: idx,<br> term: <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">term</span>(),<br> cb,<br> propose_time: <span class="hljs-literal">None</span>,<br> must_pass_epoch_check: has_applied_to_current_term,<br> };<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(cmd_type) = req_admin_cmd_type {<br> <span class="hljs-keyword">self</span>.cmd_epoch_checker<br> .<span class="hljs-title function_ invoke__">post_propose</span>(cmd_type, idx, <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">term</span>());<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">post_propose</span>(ctx, p);<br> <span class="hljs-literal">true</span><br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>在调用完 <code>PeerFsmDelegate::handle_msgs</code> 处理完消息后,会再调用 <code>PeerFsmDelegate::collect_ready()</code> 函数,进而进入 <code>Peer::handle_raft_ready_append</code> 函数。在该函数中会收集 normal 状态机的一次 ready,接着对需要持久化的未提交日志进行持久化(延后攒批),需要发送的消息进行异步发送,需要应用的已提交日志发送给 ApplyBatchSystem。</p><p>在三副本情况下,该 PreWrite 请求会存在于本次 ready 需要持久化的日志和需要发往其他两个 peer 的 message 中,对于 message,一旦收到就会 spawn 给 Transport 让其异步发送,对于持久化,在不开启 async-io 的情况下,数据会被暂存到内存中在当前 loop 结尾的 end 函数中实际写入到底层引擎中去。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Collect ready if any.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// Returns false is no readiness is generated.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">collect_ready</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">self</span>.fsm.peer.<span class="hljs-title function_ invoke__">handle_raft_ready_append</span>(<span class="hljs-keyword">self</span>.ctx);<br><br> ...<br><br>}<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_raft_ready_append</span><T: Transport>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Option</span><ReadyResult> {<br><br> ...<br><br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">has_ready</span>() {<br> fail_point!(<span class="hljs-string">"before_no_ready_gen_snap_task"</span>, |_| <span class="hljs-literal">None</span>);<br> <span class="hljs-comment">// Generating snapshot task won't set ready for raft group.</span><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(gen_task) = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">mut_store</span>().<span class="hljs-title function_ invoke__">take_gen_snap_task</span>() {<br> <span class="hljs-keyword">self</span>.pending_request_snapshot_count<br> .<span class="hljs-title function_ invoke__">fetch_add</span>(<span class="hljs-number">1</span>, Ordering::SeqCst);<br> ctx.apply_router<br> .<span class="hljs-title function_ invoke__">schedule_task</span>(<span class="hljs-keyword">self</span>.region_id, ApplyTask::<span class="hljs-title function_ invoke__">Snapshot</span>(gen_task));<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>;<br> }<br><br> ...<br> <br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">ready</span> = <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">ready</span>();<br><br> ...<br><br> <span class="hljs-keyword">if</span> !ready.<span class="hljs-title function_ invoke__">must_sync</span>() {<br> <span class="hljs-comment">// If this ready need not to sync, the term, vote must not be changed,</span><br> <span class="hljs-comment">// entries and snapshot must be empty.</span><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(hs) = ready.<span class="hljs-title function_ invoke__">hs</span>() {<br> <span class="hljs-built_in">assert_eq!</span>(hs.<span class="hljs-title function_ invoke__">get_term</span>(), <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">get_store</span>().<span class="hljs-title function_ invoke__">hard_state</span>().<span class="hljs-title function_ invoke__">get_term</span>());<br> <span class="hljs-built_in">assert_eq!</span>(hs.<span class="hljs-title function_ invoke__">get_vote</span>(), <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">get_store</span>().<span class="hljs-title function_ invoke__">hard_state</span>().<span class="hljs-title function_ invoke__">get_vote</span>());<br> }<br> <span class="hljs-built_in">assert!</span>(ready.<span class="hljs-title function_ invoke__">entries</span>().<span class="hljs-title function_ invoke__">is_empty</span>());<br> <span class="hljs-built_in">assert!</span>(ready.<span class="hljs-title function_ invoke__">snapshot</span>().<span class="hljs-title function_ invoke__">is_empty</span>());<br> }<br><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_role_changed</span>(ctx, &ready);<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(hs) = ready.<span class="hljs-title function_ invoke__">hs</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">pre_commit_index</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">get_store</span>().<span class="hljs-title function_ invoke__">commit_index</span>();<br> <span class="hljs-built_in">assert!</span>(hs.<span class="hljs-title function_ invoke__">get_commit</span>() >= pre_commit_index);<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">is_leader</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_leader_commit_idx_changed</span>(pre_commit_index, hs.<span class="hljs-title function_ invoke__">get_commit</span>());<br> }<br> }<br><br> <span class="hljs-keyword">if</span> !ready.<span class="hljs-title function_ invoke__">messages</span>().<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-built_in">assert!</span>(<span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">is_leader</span>());<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">raft_msgs</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">build_raft_messages</span>(ctx, ready.<span class="hljs-title function_ invoke__">take_messages</span>());<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">send_raft_messages</span>(ctx, raft_msgs);<br> }<br><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">apply_reads</span>(ctx, &ready);<br><br> <span class="hljs-keyword">if</span> !ready.<span class="hljs-title function_ invoke__">committed_entries</span>().<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">handle_raft_committed_entries</span>(ctx, ready.<span class="hljs-title function_ invoke__">take_committed_entries</span>());<br> }<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">ready_number</span> = ready.<span class="hljs-title function_ invoke__">number</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">persisted_msgs</span> = ready.<span class="hljs-title function_ invoke__">take_persisted_messages</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">has_write_ready</span> = <span class="hljs-literal">false</span>;<br> <span class="hljs-keyword">match</span> &res {<br> HandleReadyResult::SendIoTask | HandleReadyResult::Snapshot { .. } => {<br> <span class="hljs-keyword">if</span> !persisted_msgs.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> task.messages = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">build_raft_messages</span>(ctx, persisted_msgs);<br> }<br><br> <span class="hljs-keyword">if</span> !trackers.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> task.trackers = trackers;<br> }<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(write_worker) = &<span class="hljs-keyword">mut</span> ctx.sync_write_worker {<br> write_worker.<span class="hljs-title function_ invoke__">handle_write_task</span>(task);<br><br> <span class="hljs-built_in">assert_eq!</span>(<span class="hljs-keyword">self</span>.unpersisted_ready, <span class="hljs-literal">None</span>);<br> <span class="hljs-keyword">self</span>.unpersisted_ready = <span class="hljs-title function_ invoke__">Some</span>(ready);<br> has_write_ready = <span class="hljs-literal">true</span>;<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">self</span>.write_router.<span class="hljs-title function_ invoke__">send_write_msg</span>(<br> ctx,<br> <span class="hljs-keyword">self</span>.unpersisted_readies.<span class="hljs-title function_ invoke__">back</span>().<span class="hljs-title function_ invoke__">map</span>(|r| r.number),<br> WriteMsg::<span class="hljs-title function_ invoke__">WriteTask</span>(task),<br> );<br><br> <span class="hljs-keyword">self</span>.unpersisted_readies.<span class="hljs-title function_ invoke__">push_back</span>(UnpersistedReady {<br> number: ready_number,<br> max_empty_number: ready_number,<br> raft_msgs: <span class="hljs-built_in">vec!</span>[],<br> });<br><br> <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">advance_append_async</span>(ready);<br> }<br> }<br> HandleReadyResult::NoIoTask => {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(last) = <span class="hljs-keyword">self</span>.unpersisted_readies.<span class="hljs-title function_ invoke__">back_mut</span>() {<br> <span class="hljs-comment">// Attach to the last unpersisted ready so that it can be considered to be</span><br> <span class="hljs-comment">// persisted with the last ready at the same time.</span><br> <span class="hljs-keyword">if</span> ready_number <= last.max_empty_number {<br> <span class="hljs-built_in">panic!</span>(<br> <span class="hljs-string">"{} ready number is not monotonically increaing, {} <= {}"</span>,<br> <span class="hljs-keyword">self</span>.tag, ready_number, last.max_empty_number<br> );<br> }<br> last.max_empty_number = ready_number;<br><br> <span class="hljs-keyword">if</span> !persisted_msgs.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">self</span>.unpersisted_message_count += persisted_msgs.<span class="hljs-title function_ invoke__">capacity</span>();<br> last.raft_msgs.<span class="hljs-title function_ invoke__">push</span>(persisted_msgs);<br> }<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-comment">// If this ready don't need to be persisted and there is no previous unpersisted</span><br> <span class="hljs-comment">// ready, we can safely consider it is persisted so the persisted msgs can be</span><br> <span class="hljs-comment">// sent immediately.</span><br> <span class="hljs-keyword">self</span>.persisted_number = ready_number;<br><br> <span class="hljs-keyword">if</span> !persisted_msgs.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> fail_point!(<span class="hljs-string">"raft_before_follower_send"</span>);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">msgs</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">build_raft_messages</span>(ctx, persisted_msgs);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">send_raft_messages</span>(ctx, msgs);<br> }<br><br> <span class="hljs-comment">// The commit index and messages of light ready should be empty because no data</span><br> <span class="hljs-comment">// needs to be persisted.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">light_rd</span> = <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">advance_append</span>(ready);<br><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">add_light_ready_metric</span>(&light_rd, &<span class="hljs-keyword">mut</span> ctx.raft_metrics);<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(idx) = light_rd.<span class="hljs-title function_ invoke__">commit_index</span>() {<br> <span class="hljs-built_in">panic!</span>(<br> <span class="hljs-string">"{} advance ready that has no io task but commit index is changed to {}"</span>,<br> <span class="hljs-keyword">self</span>.tag, idx<br> );<br> }<br> <span class="hljs-keyword">if</span> !light_rd.<span class="hljs-title function_ invoke__">messages</span>().<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-built_in">panic!</span>(<br> <span class="hljs-string">"{} advance ready that has no io task but message is not empty {:?}"</span>,<br> <span class="hljs-keyword">self</span>.tag,<br> light_rd.<span class="hljs-title function_ invoke__">messages</span>()<br> );<br> }<br> <span class="hljs-comment">// The committed entries may not be empty when the size is too large to</span><br> <span class="hljs-comment">// be fetched in the previous ready.</span><br> <span class="hljs-keyword">if</span> !light_rd.<span class="hljs-title function_ invoke__">committed_entries</span>().<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">handle_raft_committed_entries</span>(ctx, light_rd.<span class="hljs-title function_ invoke__">take_committed_entries</span>());<br> }<br> }<br> }<br> }<br><br> ...<br>} <br></code></pre></td></tr></table></figure><p>等到任何一个 follower 返回确认后,该 response 会被路由到 RaftBatchSystem,PollHandler 在接下来的一次 loop 中对其进行处理,该请求会被路由到 <code>PeerFsmDelegate::handle_msgs</code> 函数的 <code>PeerMsg::RaftMessage(msg)</code> 分支中,进而调用 step 函数交给 raft-rs 状态机进行处理。</p><p>由于此时已经满足了 quorum 的写入,raft-rs 会将该 PreWrite 请求对应的 raftlog 进行提交并在下一次被获取 ready 时返回,在本轮 loop 的 <code>PeerFsmDelegate::collect_ready()</code> 函数及 <code>Peer::handle_raft_ready_append</code> 函数中,会调用 <code>self.handle_raft_committed_entries(ctx, ready.take_committed_entries())</code> 函数。在该函数中,其会根据已提交日志从 Peer 的 <code>proposals</code> 中获取到对应的 callback,连带这一批所有的已提交日志构建一个 Apply Task 通过 apply_router 发送给 ApplyBatchSystem。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span><<span class="hljs-symbol">'a</span>, EK, ER, T: Transport> PeerFsmDelegate<<span class="hljs-symbol">'a</span>, EK, ER, T><br><span class="hljs-keyword">where</span><br> EK: KvEngine,<br> ER: RaftEngine,<br>{<br> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_msgs</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, msgs: &<span class="hljs-keyword">mut</span> <span class="hljs-type">Vec</span><PeerMsg<EK>>) {<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">m</span> <span class="hljs-keyword">in</span> msgs.<span class="hljs-title function_ invoke__">drain</span>(..) {<br> <span class="hljs-keyword">match</span> m {<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftMessage</span>(msg) => {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Err</span>(e) = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_raft_message</span>(msg) {<br> error!(%e;<br> <span class="hljs-string">"handle raft message err"</span>;<br> <span class="hljs-string">"region_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">region_id</span>(),<br> <span class="hljs-string">"peer_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">peer_id</span>(),<br> );<br> }<br> }<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftCommand</span>(cmd) => {<br> ...<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_raft_command</span>(<br> cmd.request,<br> cmd.callback,<br> cmd.extra_opts.disk_full_opt,<br> );<br> }<br> }<br> PeerMsg::ApplyRes { res } => {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_apply_res</span>(res);<br> }<br> ...<br> }<br> }<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_raft_committed_entries</span><T>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br> committed_entries: <span class="hljs-type">Vec</span><Entry>,<br>) {<br> <span class="hljs-keyword">if</span> committed_entries.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">return</span>;<br> }<br><br> ...<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(last_entry) = committed_entries.<span class="hljs-title function_ invoke__">last</span>() {<br> <span class="hljs-keyword">self</span>.last_applying_idx = last_entry.<span class="hljs-title function_ invoke__">get_index</span>();<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.last_applying_idx >= <span class="hljs-keyword">self</span>.last_urgent_proposal_idx {<br> <span class="hljs-comment">// Urgent requests are flushed, make it lazy again.</span><br> <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">skip_bcast_commit</span>(<span class="hljs-literal">true</span>);<br> <span class="hljs-keyword">self</span>.last_urgent_proposal_idx = <span class="hljs-type">u64</span>::MAX;<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cbs</span> = <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.proposals.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">current_term</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">term</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cbs</span> = committed_entries<br> .<span class="hljs-title function_ invoke__">iter</span>()<br> .<span class="hljs-title function_ invoke__">filter_map</span>(|e| {<br> <span class="hljs-keyword">self</span>.proposals<br> .<span class="hljs-title function_ invoke__">find_proposal</span>(e.<span class="hljs-title function_ invoke__">get_term</span>(), e.<span class="hljs-title function_ invoke__">get_index</span>(), current_term)<br> })<br> .<span class="hljs-title function_ invoke__">map</span>(|<span class="hljs-keyword">mut</span> p| {<br> <span class="hljs-keyword">if</span> p.must_pass_epoch_check {<br> <span class="hljs-comment">// In this case the apply can be guaranteed to be successful. Invoke the</span><br> <span class="hljs-comment">// on_committed callback if necessary.</span><br> p.cb.<span class="hljs-title function_ invoke__">invoke_committed</span>();<br> }<br> p<br> })<br> .<span class="hljs-title function_ invoke__">collect</span>();<br> <span class="hljs-keyword">self</span>.proposals.<span class="hljs-title function_ invoke__">gc</span>();<br> cbs<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-built_in">vec!</span>[]<br> };<br> <span class="hljs-comment">// Note that the `commit_index` and `commit_term` here may be used to</span><br> <span class="hljs-comment">// forward the commit index. So it must be less than or equal to persist</span><br> <span class="hljs-comment">// index.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">commit_index</span> = cmp::<span class="hljs-title function_ invoke__">min</span>(<br> <span class="hljs-keyword">self</span>.raft_group.raft.raft_log.committed,<br> <span class="hljs-keyword">self</span>.raft_group.raft.raft_log.persisted,<br> );<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">commit_term</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">get_store</span>().<span class="hljs-title function_ invoke__">term</span>(commit_index).<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">apply</span> = Apply::<span class="hljs-title function_ invoke__">new</span>(<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">peer_id</span>(),<br> <span class="hljs-keyword">self</span>.region_id,<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">term</span>(),<br> commit_index,<br> commit_term,<br> committed_entries,<br> cbs,<br> <span class="hljs-keyword">self</span>.region_buckets.<span class="hljs-title function_ invoke__">as_ref</span>().<span class="hljs-title function_ invoke__">map</span>(|b| b.meta.<span class="hljs-title function_ invoke__">clone</span>()),<br> );<br> apply.<span class="hljs-title function_ invoke__">on_schedule</span>(&ctx.raft_metrics);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">mut_store</span>()<br> .<span class="hljs-title function_ invoke__">trace_cached_entries</span>(apply.entries[<span class="hljs-number">0</span>].<span class="hljs-title function_ invoke__">clone</span>());<br> <span class="hljs-keyword">if</span> <span class="hljs-title function_ invoke__">needs_evict_entry_cache</span>(ctx.cfg.evict_cache_on_memory_ratio) {<br> <span class="hljs-comment">// Compact all cached entries instead of half evict.</span><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">mut_store</span>().<span class="hljs-title function_ invoke__">evict_entry_cache</span>(<span class="hljs-literal">false</span>);<br> }<br> ctx.apply_router<br> .<span class="hljs-title function_ invoke__">schedule_task</span>(<span class="hljs-keyword">self</span>.region_id, ApplyTask::<span class="hljs-title function_ invoke__">apply</span>(apply));<br> }<br> fail_point!(<span class="hljs-string">"after_send_to_apply_1003"</span>, <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">peer_id</span>() == <span class="hljs-number">1003</span>, |_| {});<br>}<br></code></pre></td></tr></table></figure><p>此时直接定位到 <code>ApplyPoller</code> 的 <code>handle_normal</code> 函数,可以看到,<code>ApplyPoller</code> 也会首先尝试获取 <code>messages_per_tick</code> 次路由到该状态机的消息,接着调用 <code>ApplyFSM::handle_tasks</code> 函数进行处理。然后其会经历 <code>ApplyFSM::handle_apply -> ApplyDelegate::handle_raft_committed_entries</code> 的调用链来到 <code>ApplyDelegate::handle_raft_entry_normal</code> 函数中,在该函数中,会尝试将调用 <code>ApplyDelegate::process_raft_cmd</code> 函数来将本次写入缓存到 <code>kv_write_batch</code> 中,值得一提的是,在写入缓存之前会首先判断是否能够进行一次提交,如果可以则需要在写入缓存之前将这一批日志提交到底层引擎。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_normal</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, normal: &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = ApplyFsm<EK>>) <span class="hljs-punctuation">-></span> HandleResult {<br><br> ...<br> <br> <span class="hljs-keyword">while</span> <span class="hljs-keyword">self</span>.msg_buf.<span class="hljs-title function_ invoke__">len</span>() < <span class="hljs-keyword">self</span>.messages_per_tick {<br> <span class="hljs-keyword">match</span> normal.receiver.<span class="hljs-title function_ invoke__">try_recv</span>() {<br> <span class="hljs-title function_ invoke__">Ok</span>(msg) => <span class="hljs-keyword">self</span>.msg_buf.<span class="hljs-title function_ invoke__">push</span>(msg),<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Empty) => {<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Disconnected) => {<br> normal.delegate.stopped = <span class="hljs-literal">true</span>;<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br> }<br><br> normal.<span class="hljs-title function_ invoke__">handle_tasks</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.apply_ctx, &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.msg_buf);<br><br> <span class="hljs-keyword">if</span> normal.delegate.wait_merge_state.<span class="hljs-title function_ invoke__">is_some</span>() {<br> <span class="hljs-comment">// Check it again immediately as catching up logs can be very fast.</span><br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> normal.delegate.yield_state.<span class="hljs-title function_ invoke__">is_some</span>() {<br> <span class="hljs-comment">// Let it continue to run next time.</span><br> handle_result = HandleResult::KeepProcessing;<br> }<br> handle_result<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_raft_entry_normal</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> apply_ctx: &<span class="hljs-keyword">mut</span> ApplyContext<EK>,<br> entry: &Entry,<br>) <span class="hljs-punctuation">-></span> ApplyResult<EK::Snapshot> {<br> fail_point!(<br> <span class="hljs-string">"yield_apply_first_region"</span>,<br> <span class="hljs-keyword">self</span>.region.<span class="hljs-title function_ invoke__">get_start_key</span>().<span class="hljs-title function_ invoke__">is_empty</span>() && !<span class="hljs-keyword">self</span>.region.<span class="hljs-title function_ invoke__">get_end_key</span>().<span class="hljs-title function_ invoke__">is_empty</span>(),<br> |_| ApplyResult::Yield<br> );<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">index</span> = entry.<span class="hljs-title function_ invoke__">get_index</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">term</span> = entry.<span class="hljs-title function_ invoke__">get_term</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">data</span> = entry.<span class="hljs-title function_ invoke__">get_data</span>();<br><br> <span class="hljs-keyword">if</span> !data.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cmd</span> = util::<span class="hljs-title function_ invoke__">parse_data_at</span>(data, index, &<span class="hljs-keyword">self</span>.tag);<br><br> <span class="hljs-keyword">if</span> apply_ctx.yield_high_latency_operation && <span class="hljs-title function_ invoke__">has_high_latency_operation</span>(&cmd) {<br> <span class="hljs-keyword">self</span>.priority = Priority::Low;<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">has_unflushed_data</span> =<br> <span class="hljs-keyword">self</span>.last_flush_applied_index != <span class="hljs-keyword">self</span>.apply_state.<span class="hljs-title function_ invoke__">get_applied_index</span>();<br> <span class="hljs-keyword">if</span> has_unflushed_data && <span class="hljs-title function_ invoke__">should_write_to_engine</span>(&cmd)<br> || apply_ctx.<span class="hljs-title function_ invoke__">kv_wb</span>().<span class="hljs-title function_ invoke__">should_write_to_engine</span>()<br> {<br> apply_ctx.<span class="hljs-title function_ invoke__">commit</span>(<span class="hljs-keyword">self</span>);<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(start) = <span class="hljs-keyword">self</span>.handle_start.<span class="hljs-title function_ invoke__">as_ref</span>() {<br> <span class="hljs-keyword">if</span> start.<span class="hljs-title function_ invoke__">saturating_elapsed</span>() >= apply_ctx.yield_duration {<br> <span class="hljs-keyword">return</span> ApplyResult::Yield;<br> }<br> }<br> has_unflushed_data = <span class="hljs-literal">false</span>;<br> }<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.priority != apply_ctx.priority {<br> <span class="hljs-keyword">if</span> has_unflushed_data {<br> apply_ctx.<span class="hljs-title function_ invoke__">commit</span>(<span class="hljs-keyword">self</span>);<br> }<br> <span class="hljs-keyword">return</span> ApplyResult::Yield;<br> }<br><br> <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">process_raft_cmd</span>(apply_ctx, index, term, cmd);<br> }<br><br> ...<br>}<br></code></pre></td></tr></table></figure><p>那么为什么不像 RaftBatchSystem 一样在 end 函数中统一进行攒批提交呢?原因是此时只要攒够一定的大小不对底层引擎造成过大的负载就可以快速提交并返回客户端了,等到最后再去处理只会增加写入延时而没有太大的收益。</p><p>让我们阅读一下提交 batch 的逻辑,其会经由 <code>ApplyContext::commit -> ApplyContext::commit_opt</code> 的调用链来到 <code>ApplyContext::write_to_db</code> 函数,在该函数中,会调用 <code>self.kv_wb_mut().write_opt(&write_opts)</code> 函数将该 <code>WriteBatch</code> 提交到底层引擎,接着在最后调用 <code>cb.invoke_with_response(resp)</code> 来执行 callback 尽快返回客户端。 </p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Commits all changes have done for delegate. `persistent` indicates</span><br><span class="hljs-comment">/// whether write the changes into rocksdb.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// This call is valid only when it's between a `prepare_for` and</span><br><span class="hljs-comment">/// `finish_for`.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">commit</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, delegate: &<span class="hljs-keyword">mut</span> ApplyDelegate<EK>) {<br> <span class="hljs-keyword">if</span> delegate.last_flush_applied_index < delegate.apply_state.<span class="hljs-title function_ invoke__">get_applied_index</span>() {<br> delegate.<span class="hljs-title function_ invoke__">write_apply_state</span>(<span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb_mut</span>());<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">commit_opt</span>(delegate, <span class="hljs-literal">true</span>);<br>}<br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">commit_opt</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, delegate: &<span class="hljs-keyword">mut</span> ApplyDelegate<EK>, persistent: <span class="hljs-type">bool</span>) {<br> delegate.<span class="hljs-title function_ invoke__">update_metrics</span>(<span class="hljs-keyword">self</span>);<br> <span class="hljs-keyword">if</span> persistent {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">write_to_db</span>();<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">prepare_for</span>(delegate);<br> delegate.last_flush_applied_index = delegate.apply_state.<span class="hljs-title function_ invoke__">get_applied_index</span>()<br> }<br> <span class="hljs-keyword">self</span>.kv_wb_last_bytes = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb</span>().<span class="hljs-title function_ invoke__">data_size</span>() <span class="hljs-keyword">as</span> <span class="hljs-type">u64</span>;<br> <span class="hljs-keyword">self</span>.kv_wb_last_keys = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb</span>().<span class="hljs-title function_ invoke__">count</span>() <span class="hljs-keyword">as</span> <span class="hljs-type">u64</span>;<br>}<br><br><span class="hljs-comment">/// Writes all the changes into RocksDB.</span><br><span class="hljs-comment">/// If it returns true, all pending writes are persisted in engines.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">write_to_db</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">need_sync</span> = <span class="hljs-keyword">self</span>.sync_log_hint;<br> <span class="hljs-comment">// There may be put and delete requests after ingest request in the same fsm.</span><br> <span class="hljs-comment">// To guarantee the correct order, we must ingest the pending_sst first, and</span><br> <span class="hljs-comment">// then persist the kv write batch to engine.</span><br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.pending_ssts.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tag</span> = <span class="hljs-keyword">self</span>.tag.<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">self</span>.importer<br> .<span class="hljs-title function_ invoke__">ingest</span>(&<span class="hljs-keyword">self</span>.pending_ssts, &<span class="hljs-keyword">self</span>.engine)<br> .<span class="hljs-title function_ invoke__">unwrap_or_else</span>(|e| {<br> <span class="hljs-built_in">panic!</span>(<br> <span class="hljs-string">"{} failed to ingest ssts {:?}: {:?}"</span>,<br> tag, <span class="hljs-keyword">self</span>.pending_ssts, e<br> );<br> });<br> <span class="hljs-keyword">self</span>.pending_ssts = <span class="hljs-built_in">vec!</span>[];<br> }<br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb_mut</span>().<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">self</span>.perf_context.<span class="hljs-title function_ invoke__">start_observe</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">write_opts</span> = engine_traits::WriteOptions::<span class="hljs-title function_ invoke__">new</span>();<br> write_opts.<span class="hljs-title function_ invoke__">set_sync</span>(need_sync);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb_mut</span>().<span class="hljs-title function_ invoke__">write_opt</span>(&write_opts).<span class="hljs-title function_ invoke__">unwrap_or_else</span>(|e| {<br> <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"failed to write to engine: {:?}"</span>, e);<br> });<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">trackers</span>: <span class="hljs-type">Vec</span><_> = <span class="hljs-keyword">self</span><br> .applied_batch<br> .cb_batch<br> .<span class="hljs-title function_ invoke__">iter</span>()<br> .<span class="hljs-title function_ invoke__">flat_map</span>(|(cb, _)| cb.<span class="hljs-title function_ invoke__">write_trackers</span>())<br> .<span class="hljs-title function_ invoke__">flat_map</span>(|trackers| trackers.<span class="hljs-title function_ invoke__">iter</span>().<span class="hljs-title function_ invoke__">map</span>(|t| t.<span class="hljs-title function_ invoke__">as_tracker_token</span>()))<br> .<span class="hljs-title function_ invoke__">flatten</span>()<br> .<span class="hljs-title function_ invoke__">collect</span>();<br> <span class="hljs-keyword">self</span>.perf_context.<span class="hljs-title function_ invoke__">report_metrics</span>(&trackers);<br> <span class="hljs-keyword">self</span>.sync_log_hint = <span class="hljs-literal">false</span>;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">data_size</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb</span>().<span class="hljs-title function_ invoke__">data_size</span>();<br> <span class="hljs-keyword">if</span> data_size > APPLY_WB_SHRINK_SIZE {<br> <span class="hljs-comment">// Control the memory usage for the WriteBatch.</span><br> <span class="hljs-keyword">self</span>.kv_wb = <span class="hljs-keyword">self</span>.engine.<span class="hljs-title function_ invoke__">write_batch_with_cap</span>(DEFAULT_APPLY_WB_SIZE);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-comment">// Clear data, reuse the WriteBatch, this can reduce memory allocations and</span><br> <span class="hljs-comment">// deallocations.</span><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">kv_wb_mut</span>().<span class="hljs-title function_ invoke__">clear</span>();<br> }<br> <span class="hljs-keyword">self</span>.kv_wb_last_bytes = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">self</span>.kv_wb_last_keys = <span class="hljs-number">0</span>;<br> }<br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.delete_ssts.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tag</span> = <span class="hljs-keyword">self</span>.tag.<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">sst</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">self</span>.delete_ssts.<span class="hljs-title function_ invoke__">drain</span>(..) {<br> <span class="hljs-keyword">self</span>.importer.<span class="hljs-title function_ invoke__">delete</span>(&sst.meta).<span class="hljs-title function_ invoke__">unwrap_or_else</span>(|e| {<br> <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"{} cleanup ingested file {:?}: {:?}"</span>, tag, sst, e);<br> });<br> }<br> }<br> <span class="hljs-comment">// Take the applied commands and their callback</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">ApplyCallbackBatch</span> {<br> cmd_batch,<br> batch_max_level,<br> <span class="hljs-keyword">mut</span> cb_batch,<br> } = mem::<span class="hljs-title function_ invoke__">replace</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.applied_batch, ApplyCallbackBatch::<span class="hljs-title function_ invoke__">new</span>());<br> <span class="hljs-comment">// Call it before invoking callback for preventing Commit is executed before</span><br> <span class="hljs-comment">// Prewrite is observed.</span><br> <span class="hljs-keyword">self</span>.host<br> .<span class="hljs-title function_ invoke__">on_flush_applied_cmd_batch</span>(batch_max_level, cmd_batch, &<span class="hljs-keyword">self</span>.engine);<br> <span class="hljs-comment">// Invoke callbacks</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">now</span> = std::time::Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">for</span> (cb, resp) <span class="hljs-keyword">in</span> cb_batch.<span class="hljs-title function_ invoke__">drain</span>(..) {<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">tracker</span> <span class="hljs-keyword">in</span> cb.<span class="hljs-title function_ invoke__">write_trackers</span>().<span class="hljs-title function_ invoke__">iter</span>().<span class="hljs-title function_ invoke__">flat_map</span>(|v| *v) {<br> tracker.<span class="hljs-title function_ invoke__">observe</span>(now, &<span class="hljs-keyword">self</span>.apply_time, |t| &<span class="hljs-keyword">mut</span> t.metrics.apply_time_nanos);<br> }<br> cb.<span class="hljs-title function_ invoke__">invoke_with_response</span>(resp);<br> }<br> <span class="hljs-keyword">self</span>.apply_time.<span class="hljs-title function_ invoke__">flush</span>();<br> <span class="hljs-keyword">self</span>.apply_wait.<span class="hljs-title function_ invoke__">flush</span>();<br> need_sync<br>}<br></code></pre></td></tr></table></figure><p>在 <code>ApplyPoller</code> 一轮 loop 结尾的 end 函数中,其会调用 <code>ApplyContext::flush</code> 函数,进而通过 <code>self.notifier.notify(apply_res)</code> 将 ApplyRes 重新发送到 RaftBatchSystem 中去,进而更新某些内存结构,此处不再赘述。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">end</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, fsms: &<span class="hljs-keyword">mut</span> [<span class="hljs-type">Option</span><<span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = ApplyFsm<EK>>>]) {<br> <span class="hljs-keyword">self</span>.apply_ctx.<span class="hljs-title function_ invoke__">flush</span>();<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">fsm</span> <span class="hljs-keyword">in</span> fsms.<span class="hljs-title function_ invoke__">iter_mut</span>().<span class="hljs-title function_ invoke__">flatten</span>() {<br> fsm.delegate.last_flush_applied_index = fsm.delegate.apply_state.<span class="hljs-title function_ invoke__">get_applied_index</span>();<br> fsm.delegate.<span class="hljs-title function_ invoke__">update_memory_trace</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.trace_event);<br> }<br> MEMTRACE_APPLYS.<span class="hljs-title function_ invoke__">trace</span>(mem::<span class="hljs-title function_ invoke__">take</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.trace_event));<br>}<br><br> <span class="hljs-comment">/// Flush all pending writes to engines.</span><br><span class="hljs-comment">/// If it returns true, all pending writes are persisted in engines.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">flush</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br> <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> this check is too hacky, need to be more verbose and less buggy.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">t</span> = <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.timer.<span class="hljs-title function_ invoke__">take</span>() {<br> <span class="hljs-title function_ invoke__">Some</span>(t) => t,<br> <span class="hljs-literal">None</span> => <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>,<br> };<br><br> <span class="hljs-comment">// Write to engine</span><br> <span class="hljs-comment">// raftstore.sync-log = true means we need prevent data loss when power failure.</span><br> <span class="hljs-comment">// take raft log gc for example, we write kv WAL first, then write raft WAL,</span><br> <span class="hljs-comment">// if power failure happen, raft WAL may synced to disk, but kv WAL may not.</span><br> <span class="hljs-comment">// so we use sync-log flag here.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">is_synced</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">write_to_db</span>();<br><br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.apply_res.<span class="hljs-title function_ invoke__">is_empty</span>() {<br> fail_point!(<span class="hljs-string">"before_nofity_apply_res"</span>);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">apply_res</span> = mem::<span class="hljs-title function_ invoke__">take</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.apply_res);<br> <span class="hljs-keyword">self</span>.notifier.<span class="hljs-title function_ invoke__">notify</span>(apply_res);<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">elapsed</span> = t.<span class="hljs-title function_ invoke__">saturating_elapsed</span>();<br> STORE_APPLY_LOG_HISTOGRAM.<span class="hljs-title function_ invoke__">observe</span>(<span class="hljs-title function_ invoke__">duration_to_sec</span>(elapsed) <span class="hljs-keyword">as</span> <span class="hljs-type">f64</span>);<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">mut</span> <span class="hljs-keyword">in</span>spector <span class="hljs-keyword">in</span> std::mem::<span class="hljs-title function_ invoke__">take</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.pending_latency_inspect) {<br> inspector.<span class="hljs-title function_ invoke__">record_apply_process</span>(elapsed);<br> inspector.<span class="hljs-title function_ invoke__">finish</span>();<br> }<br><br> slow_log!(<br> elapsed,<br> <span class="hljs-string">"{} handle ready {} committed entries"</span>,<br> <span class="hljs-keyword">self</span>.tag,<br> <span class="hljs-keyword">self</span>.committed_count<br> );<br> <span class="hljs-keyword">self</span>.committed_count = <span class="hljs-number">0</span>;<br> is_synced<br>}<br></code></pre></td></tr></table></figure><p>通过本小节,希望您能够了解 PreWrite 请求的完整流程,并进而具备分析其他写请求全链路的能力。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇博客介绍了 TiKV 中一条写请求的全链路流程。</p><p>希望本博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>感谢您的阅读~</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value </summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="源码阅读" scheme="https://tanxinyu.work/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="TiKV" scheme="https://tanxinyu.work/tags/TiKV/"/>
</entry>
<entry>
<title>TiKV 源码阅读三部曲(二)读流程</title>
<link href="https://tanxinyu.work/tikv-source-code-reading-read/"/>
<id>https://tanxinyu.work/tikv-source-code-reading-read/</id>
<published>2022-10-05T09:37:12.000Z</published>
<updated>2022-10-08T03:20:31.162Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value 数据库,目前已经是 <a href="https://www.cncf.io/projects/">CNCF 基金会</a> 的顶级项目。</p><p>作为一个新同学,需要一定的前期准备才能够有能力参与 TiKV 社区的代码开发,包括但不限于学习 Rust 语言,理解 TiKV 的原理和在前两者的基础上了解熟悉 TiKV 的源码。</p><p><a href="https://pingcap.com/zh/blog/?tag=TiKV%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90">TiKV 官方源码解析文档</a> 详细地介绍了 TiKV 3.x 版本重要模块的设计要点,主要流程和相应代码片段,是学习 TiKV 源码必读的学习资料。当前 TiKV 已经迭代到了 6.x 版本,不仅引入了很多新的功能和优化,而且对源码也进行了多次重构,因而一些官方源码解析文档中的代码片段已经不复存在,这使得读者在阅读源码解析文档时无法对照最新源码加深理解;此外尽管 TiKV 官方源码解析文档系统地介绍了若干重要模块的工作,但并没有将读写流程全链路串起来去介绍经过的模块和对应的代码片段,实际上尽快地熟悉读写流程全链路会更利于新同学从全局角度理解代码。</p><p>基于以上存在的问题,笔者将基于 6.1 版本的源码撰写三篇博客,分别介绍以下三个方面:</p><ul><li><a href="https://tanxinyu.work/tikv-source-code-reading-module/">TiKV 源码阅读三部曲(一)重要模块</a>:TiKV 的基本概念,TiKV 读写路径上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的方案</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-read/">TiKV 源码阅读三部曲(二)读流程</a>:TiKV 中一条读请求的全链路流程</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-write/">TiKV 源码阅读三部曲(三)写流程</a>:TiKV 中一条写请求的全链路流程</li></ul><p>希望此三篇博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>本文为第二篇博客,将主要介绍 TiKV 中一条读请求的全链路流程。</p><h2 id="读流程"><a href="#读流程" class="headerlink" title="读流程"></a>读流程</h2><p><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-19">TiKV 源码解析系列文章(十九)read index 和 local read 情景分析</a> 介绍了 TiKV 3.x 版本的 ReadIndex/LeaseRead 实现方案。</p><p>本小节将在 TiKV 6.1 版本的源码基础上,以一条读请求为例,介绍当前版本读请求的全链路执行流程。</p><p>前文已经提到,可以从 <a href="https://github.com/pingcap/kvproto/blob/master/proto/tikvpb.proto#L20">kvproto</a> 对应的 <code>service Tikv</code> 中了解当前 TiKV 支持的 RPC 接口。</p><p>经过简单整理,常用的读接口如下:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-comment">// Key/value store API for TiKV.</span><br>service Tikv { <br><br> rpc <span class="hljs-title function_ invoke__">KvGet</span>(kvrpcpb.GetRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.GetResponse) {}<br> rpc <span class="hljs-title function_ invoke__">KvScan</span>(kvrpcpb.ScanRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.ScanResponse) {}<br> rpc <span class="hljs-title function_ invoke__">KvBatchGet</span>(kvrpcpb.BatchGetRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.BatchGetResponse) {}<br><br> rpc <span class="hljs-title function_ invoke__">RawGet</span>(kvrpcpb.RawGetRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.RawGetResponse) {}<br> rpc <span class="hljs-title function_ invoke__">RawBatchGet</span>(kvrpcpb.RawBatchGetRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.RawBatchGetResponse) {}<br> rpc <span class="hljs-title function_ invoke__">RawScan</span>(kvrpcpb.RawScanRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.RawScanResponse) {}<br> rpc <span class="hljs-title function_ invoke__">RawBatchScan</span>(kvrpcpb.RawBatchScanRequest) <span class="hljs-title function_ invoke__">returns</span> (kvrpcpb.RawBatchScanResponse) {}<br><br> ...<br>}<br></code></pre></td></tr></table></figure><p>以下将以最常用的 KvGet 接口为例介绍读流程,其他的读接口所经过的模块大致相似,之后也可以用断点调试的方案去自行阅读。</p><h3 id="KVService"><a href="#KVService" class="headerlink" title="KVService"></a>KVService</h3><p>在 KVService 中, handle_request 宏将业务逻辑封装到了 future_get 函数中。在 future_get 函数中,主要使用了 <code>storage.get(req.take_context(), Key::from_raw(req.get_key()), req.get_version().into())</code> 函数将请求路由到 Storage 模块去执行。</p><p>为了可观测性,当前 TiKV 在读写关键路径上加了很多全局和 request 级别的 metric,这一定程度上影响了刚开始阅读代码的体验。其实刚开始熟悉代码时只需要关注核心逻辑即可,metric 相关的代码可以先不用细究。 </p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-keyword">impl</span><T: RaftStoreRouter<E::Local> + <span class="hljs-symbol">'static</span>, E: Engine, L: LockManager, F: KvFormat> Tikv<br> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Service</span><T, E, L, F><br>{<br> handle_request!(kv_get, future_get, GetRequest, GetResponse, has_time_detail);<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">future_get</span><E: Engine, L: LockManager, F: KvFormat>(<br> storage: &Storage<E, L, F>,<br> <span class="hljs-keyword">mut</span> req: GetRequest,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">Future</span><Output = ServerResult<GetResponse>> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">v</span> = storage.<span class="hljs-title function_ invoke__">get</span>(<br> req.<span class="hljs-title function_ invoke__">take_context</span>(),<br> Key::<span class="hljs-title function_ invoke__">from_raw</span>(req.<span class="hljs-title function_ invoke__">get_key</span>()),<br> req.<span class="hljs-title function_ invoke__">get_version</span>().<span class="hljs-title function_ invoke__">into</span>(),<br> );<br><br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">v</span> = v.<span class="hljs-keyword">await</span>;<br> <br> ...<br> <br> <span class="hljs-title function_ invoke__">Ok</span>(resp)<br> }<br>}<br></code></pre></td></tr></table></figure><h3 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h3><p>在 Storage 模块的 get 函数中,所有的 task 都会被 spawn 到 readPool 中执行,具体执行的任务主要包含以下两个工作:</p><ul><li>使用 <code>Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await?</code> 获取 snapshot</li><li>使用 <code>snap_store.get(&key, &mut statistics)</code> 基于获取到的 snapshot 获取符合对应事务语义的数据</li></ul><p>第二个工作比较简单,本小节不再赘述,以下主要介绍第一个工作的具体代码流程。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-comment">/// Get value of the given key from a snapshot.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// Only writes that are committed before `start_ts` are visible.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>(<br> &<span class="hljs-keyword">self</span>,<br> <span class="hljs-keyword">mut</span> ctx: Context,<br> key: Key,<br> start_ts: TimeStamp,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">Future</span><Output = <span class="hljs-type">Result</span><(<span class="hljs-type">Option</span><Value>, KvGetStatistics)>> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">self</span>.read_pool.<span class="hljs-title function_ invoke__">spawn_handle</span>(<br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">snap_ctx</span> = <span class="hljs-title function_ invoke__">prepare_snap_ctx</span>(<br> &ctx,<br> iter::<span class="hljs-title function_ invoke__">once</span>(&key),<br> start_ts,<br> &bypass_locks,<br> &concurrency_manager,<br> CMD,<br> )?;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">snapshot</span> =<br> <span class="hljs-keyword">Self</span>::<span class="hljs-title function_ invoke__">with_tls_engine</span>(|engine| <span class="hljs-keyword">Self</span>::<span class="hljs-title function_ invoke__">snapshot</span>(engine, snap_ctx)).<span class="hljs-keyword">await</span>?;<br><br> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin_instant</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">stage_snap_recv_ts</span> = begin_instant;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">buckets</span> = snapshot.<span class="hljs-title function_ invoke__">ext</span>().<span class="hljs-title function_ invoke__">get_buckets</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">statistics</span> = Statistics::<span class="hljs-title function_ invoke__">default</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">result</span> = <span class="hljs-keyword">Self</span>::<span class="hljs-title function_ invoke__">with_perf_context</span>(CMD, || {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">_guard</span> = sample.<span class="hljs-title function_ invoke__">observe_cpu</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">snap_store</span> = SnapshotStore::<span class="hljs-title function_ invoke__">new</span>(<br> snapshot,<br> start_ts,<br> ctx.<span class="hljs-title function_ invoke__">get_isolation_level</span>(),<br> !ctx.<span class="hljs-title function_ invoke__">get_not_fill_cache</span>(),<br> bypass_locks,<br> access_locks,<br> <span class="hljs-literal">false</span>,<br> );<br> snap_store<br> .<span class="hljs-title function_ invoke__">get</span>(&key, &<span class="hljs-keyword">mut</span> statistics)<br> <span class="hljs-comment">// map storage::txn::Error -> storage::Error</span><br> .<span class="hljs-title function_ invoke__">map_err</span>(Error::from)<br> .<span class="hljs-title function_ invoke__">map</span>(|r| {<br> KV_COMMAND_KEYREAD_HISTOGRAM_STATIC.<span class="hljs-title function_ invoke__">get</span>(CMD).<span class="hljs-title function_ invoke__">observe</span>(<span class="hljs-number">1_f64</span>);<br> r<br> })<br> });<br> <br> ...<br> <br> <span class="hljs-title function_ invoke__">Ok</span>((<br> result?,<br> KvGetStatistics {<br> stats: statistics,<br> latency_stats,<br> },<br> ))<br> }<br> }<br> .<span class="hljs-title function_ invoke__">in_resource_metering_tag</span>(resource_tag),<br> priority,<br> <span class="hljs-title function_ invoke__">thread_rng</span>().<span class="hljs-title function_ invoke__">next_u64</span>(),<br> );<br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> res.<span class="hljs-title function_ invoke__">map_err</span>(|_| Error::<span class="hljs-title function_ invoke__">from</span>(ErrorInner::SchedTooBusy))<br> .<span class="hljs-keyword">await</span>?<br> }<br>}<br></code></pre></td></tr></table></figure><p>对于 <code>Self::snapshot(engine, snap_ctx)</code> 函数,其会经由 <code>storage::snapshot -> kv::snapshot -> raftkv::async_snapshot -> raftkv::exec_snapshot</code> 的调用链来到 <code>ServerRaftStoreRouter::read</code> 函数中。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Get a snapshot of `engine`.</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">snapshot</span>(<br> engine: &E,<br> ctx: SnapContext<<span class="hljs-symbol">'_</span>>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">std</span>::future::Future<Output = <span class="hljs-type">Result</span><E::Snap>> {<br> kv::<span class="hljs-title function_ invoke__">snapshot</span>(engine, ctx)<br> .<span class="hljs-title function_ invoke__">map_err</span>(txn::Error::from)<br> .<span class="hljs-title function_ invoke__">map_err</span>(Error::from)<br>}<br><br><span class="hljs-comment">/// Get a snapshot of `engine`.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">snapshot</span><E: Engine>(<br> engine: &E,<br> ctx: SnapContext<<span class="hljs-symbol">'_</span>>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">std</span>::future::Future<Output = <span class="hljs-type">Result</span><E::Snap>> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">let</span> (callback, future) =<br> tikv_util::future::<span class="hljs-title function_ invoke__">paired_must_called_future_callback</span>(drop_snapshot_callback::<E>);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">val</span> = engine.<span class="hljs-title function_ invoke__">async_snapshot</span>(ctx, callback);<br> <span class="hljs-comment">// make engine not cross yield point</span><br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> val?; <span class="hljs-comment">// propagate error</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">result</span> = future<br> .<span class="hljs-title function_ invoke__">map_err</span>(|cancel| Error::<span class="hljs-title function_ invoke__">from</span>(ErrorInner::<span class="hljs-title function_ invoke__">Other</span>(box_err!(cancel))))<br> .<span class="hljs-keyword">await</span>?;<br> <span class="hljs-title function_ invoke__">with_tls_tracker</span>(|tracker| {<br> tracker.metrics.get_snapshot_nanos += begin.<span class="hljs-title function_ invoke__">elapsed</span>().<span class="hljs-title function_ invoke__">as_nanos</span>() <span class="hljs-keyword">as</span> <span class="hljs-type">u64</span>;<br> });<br> fail_point!(<span class="hljs-string">"after-snapshot"</span>);<br> result<br> }<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">async_snapshot</span>(&<span class="hljs-keyword">self</span>, <span class="hljs-keyword">mut</span> ctx: SnapContext<<span class="hljs-symbol">'_</span>>, cb: Callback<<span class="hljs-keyword">Self</span>::Snap>) <span class="hljs-punctuation">-></span> kv::<span class="hljs-type">Result</span><()> {<br> <br> ...<br><br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">exec_snapshot</span>(<br> ctx,<br> req,<br> <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-keyword">move</span> |res| <span class="hljs-keyword">match</span> res {<br> ...<br> }),<br> )<br> .<span class="hljs-title function_ invoke__">map_err</span>(|e| {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">status_kind</span> = <span class="hljs-title function_ invoke__">get_status_kind_from_error</span>(&e);<br> ASYNC_REQUESTS_COUNTER_VEC.snapshot.<span class="hljs-title function_ invoke__">get</span>(status_kind).<span class="hljs-title function_ invoke__">inc</span>();<br> e.<span class="hljs-title function_ invoke__">into</span>()<br> })<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">exec_snapshot</span>(<br> &<span class="hljs-keyword">self</span>,<br> ctx: SnapContext<<span class="hljs-symbol">'_</span>>,<br> req: Request,<br> cb: Callback<CmdRes<E::Snapshot>>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><()> {<br><br> ...<br> <br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">cmd</span> = RaftCmdRequest::<span class="hljs-title function_ invoke__">default</span>();<br> cmd.<span class="hljs-title function_ invoke__">set_header</span>(header);<br> cmd.<span class="hljs-title function_ invoke__">set_requests</span>(<span class="hljs-built_in">vec!</span>[req].<span class="hljs-title function_ invoke__">into</span>());<br> <span class="hljs-keyword">self</span>.router<br> .<span class="hljs-title function_ invoke__">read</span>(<br> ctx.read_id,<br> cmd,<br> StoreCallback::<span class="hljs-title function_ invoke__">read</span>(<span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-keyword">move</span> |resp| {<br> <span class="hljs-title function_ invoke__">cb</span>(<span class="hljs-title function_ invoke__">on_read_result</span>(resp).<span class="hljs-title function_ invoke__">map_err</span>(Error::into));<br> })),<br> )<br> .<span class="hljs-title function_ invoke__">map_err</span>(<span class="hljs-built_in">From</span>::from)<br>}<br><br><span class="hljs-keyword">impl</span><EK: KvEngine, ER: RaftEngine> LocalReadRouter<EK> <span class="hljs-keyword">for</span> <span class="hljs-title class_">ServerRaftStoreRouter</span><EK, ER> {<br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">read</span>(<br> &<span class="hljs-keyword">self</span>,<br> read_id: <span class="hljs-type">Option</span><ThreadReadId>,<br> req: RaftCmdRequest,<br> cb: Callback<EK::Snapshot>,<br> ) <span class="hljs-punctuation">-></span> RaftStoreResult<()> {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">local_reader</span> = <span class="hljs-keyword">self</span>.local_reader.<span class="hljs-title function_ invoke__">borrow_mut</span>();<br> local_reader.<span class="hljs-title function_ invoke__">read</span>(read_id, req, cb);<br> <span class="hljs-title function_ invoke__">Ok</span>(())<br> }<br>}<br></code></pre></td></tr></table></figure><p>在 <code>ServerRaftStoreRouter::read</code> 函数中,其会调用 <code>local_reader</code> 的 <code>read</code> 函数,并进而路由到 <code>LocalReader::propose_raft_command</code> 函数。在该函数中,会使用 <code>LocalReader::pre_propose_raft_command</code> 函数来判断是否能够 ReadLocal,如果可以则直接获取本地引擎的 snapshot 并执行 callback 返回即可,否则便调用 <code>redirect</code> 函数连带 callback 路由到 RaftBatchSystem 的对应 normal 状态机中去执行 ReadIndex 读,之后本线程不再处理该任务。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-meta">#[inline]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">read</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> read_id: <span class="hljs-type">Option</span><ThreadReadId>,<br> req: RaftCmdRequest,<br> cb: Callback<E::Snapshot>,<br>) {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_raft_command</span>(read_id, req, cb);<br> <span class="hljs-title function_ invoke__">maybe_tls_local_read_metrics_flush</span>();<br>}<br><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">propose_raft_command</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> <span class="hljs-keyword">mut</span> read_id: <span class="hljs-type">Option</span><ThreadReadId>,<br> req: RaftCmdRequest,<br> cb: Callback<E::Snapshot>,<br>) {<br> <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">pre_propose_raft_command</span>(&req) {<br> <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-title function_ invoke__">Some</span>((<span class="hljs-keyword">mut</span> delegate, policy))) => {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">delegate_ext</span>: LocalReadContext<<span class="hljs-symbol">'_</span>, E>;<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">response</span> = <span class="hljs-keyword">match</span> policy {<br> <span class="hljs-comment">// Leader can read local if and only if it is in lease.</span><br> RequestPolicy::ReadLocal => {<br> <br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">region</span> = Arc::<span class="hljs-title function_ invoke__">clone</span>(&delegate.region);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">response</span> =<br> delegate.<span class="hljs-title function_ invoke__">execute</span>(&req, &region, <span class="hljs-literal">None</span>, read_id, <span class="hljs-title function_ invoke__">Some</span>(delegate_ext));<br> <span class="hljs-comment">// Try renew lease in advance</span><br> delegate.<span class="hljs-title function_ invoke__">maybe_renew_lease_advance</span>(&<span class="hljs-keyword">self</span>.router, snapshot_ts);<br> response<br> }<br> <span class="hljs-comment">// Replica can serve stale read if and only if its `safe_ts` >= `read_ts`</span><br> RequestPolicy::StaleRead => {<br> <br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">region</span> = Arc::<span class="hljs-title function_ invoke__">clone</span>(&delegate.region);<br> <span class="hljs-comment">// Getting the snapshot</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">response</span> =<br> delegate.<span class="hljs-title function_ invoke__">execute</span>(&req, &region, <span class="hljs-literal">None</span>, read_id, <span class="hljs-title function_ invoke__">Some</span>(delegate_ext));<br><br> ...<br> <br> }<br> _ => <span class="hljs-built_in">unreachable!</span>(),<br> };<br> <br> ...<br> <br> cb.<span class="hljs-title function_ invoke__">invoke_read</span>(response);<br> }<br> <span class="hljs-comment">// Forward to raftstore.</span><br> <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-literal">None</span>) => <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">redirect</span>(RaftCommand::<span class="hljs-title function_ invoke__">new</span>(req, cb)),<br> <span class="hljs-title function_ invoke__">Err</span>(e) => {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">response</span> = cmd_resp::<span class="hljs-title function_ invoke__">new_error</span>(e);<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(delegate) = <span class="hljs-keyword">self</span>.delegates.<span class="hljs-title function_ invoke__">get</span>(&req.<span class="hljs-title function_ invoke__">get_header</span>().<span class="hljs-title function_ invoke__">get_region_id</span>()) {<br> cmd_resp::<span class="hljs-title function_ invoke__">bind_term</span>(&<span class="hljs-keyword">mut</span> response, delegate.term);<br> }<br> cb.<span class="hljs-title function_ invoke__">invoke_read</span>(ReadResponse {<br> response,<br> snapshot: <span class="hljs-literal">None</span>,<br> txn_extra_op: TxnExtraOp::Noop,<br> });<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>需要注意的是,在此处能否 ReadLocal 的判断是可以并行的,也就是乐观情况下并行的读请求可以并行获取底层引擎的 snapshot,不需要经过 RaftBatchSystem 。</p><p>那么到底什么时候可以直接读取 snapshot 而不需要经过 RaftStore 走一轮 ReadIndex 来处理呢?原理就是 Lease 机制,可以先简单阅读一下 <a href="https://pingcap.com/zh/blog/lease-read">TiKV Lease Read 的功能介绍</a>。</p><p>接着让我们回到 <code>LocalReader::pre_propose_raft_command</code> 函数,其会进行一系列的检查(此处已略去),如果皆通过则会进一步调用 <code>inspector.inspect(req)</code> 函数,在其内部,其会进行一系列的判断并返回是否可以 ReadLocal。</p><ul><li><code>req.get_header().get_read_quorum()</code>:如果该请求明确要求需要用 read index 方式处理,所以返回 ReadIndex。</li><li><code>self.has_applied_to_current_term()</code>:如果该 leader 尚未 apply 到它自己的 term,则使用 ReadIndex 处理,这是 Raft 有关线性一致性读的一个 corner case。</li><li><code>self.inspect_lease()</code>:如果该 leader 的 lease 已经过期或者不确定,说明可能出现了一些问题,比如网络不稳定,心跳没成功等,此时使用 ReadIndex 处理,否则便可以使用 ReadLocal 处理。</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">pre_propose_raft_command</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> req: &RaftCmdRequest,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><<span class="hljs-type">Option</span><(D, RequestPolicy)>> {<br> <br> ...<br><br> <span class="hljs-keyword">match</span> inspector.<span class="hljs-title function_ invoke__">inspect</span>(req) {<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadLocal) => <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-title function_ invoke__">Some</span>((delegate, RequestPolicy::ReadLocal))),<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::StaleRead) => <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-title function_ invoke__">Some</span>((delegate, RequestPolicy::StaleRead))),<br> <span class="hljs-comment">// It can not handle other policies.</span><br> <span class="hljs-title function_ invoke__">Ok</span>(_) => <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-literal">None</span>),<br> <span class="hljs-title function_ invoke__">Err</span>(e) => <span class="hljs-title function_ invoke__">Err</span>(e),<br> }<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">inspect</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, req: &RaftCmdRequest) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><RequestPolicy> {<br><br> ...<br><br> fail_point!(<span class="hljs-string">"perform_read_index"</span>, |_| <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex));<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">flags</span> = WriteBatchFlags::<span class="hljs-title function_ invoke__">from_bits_check</span>(req.<span class="hljs-title function_ invoke__">get_header</span>().<span class="hljs-title function_ invoke__">get_flags</span>());<br> <span class="hljs-keyword">if</span> flags.<span class="hljs-title function_ invoke__">contains</span>(WriteBatchFlags::STALE_READ) {<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::StaleRead);<br> }<br><br> <span class="hljs-keyword">if</span> req.<span class="hljs-title function_ invoke__">get_header</span>().<span class="hljs-title function_ invoke__">get_read_quorum</span>() {<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex);<br> }<br><br> <span class="hljs-comment">// If applied index's term is differ from current raft's term, leader transfer</span><br> <span class="hljs-comment">// must happened, if read locally, we may read old value.</span><br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">has_applied_to_current_term</span>() {<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex);<br> }<br><br> <span class="hljs-comment">// Local read should be performed, if and only if leader is in lease.</span><br> <span class="hljs-comment">// None for now.</span><br> <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">inspect_lease</span>() {<br> LeaseState::Valid => <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadLocal),<br> LeaseState::Expired | LeaseState::Suspect => {<br> <span class="hljs-comment">// Perform a consistent read to Raft quorum and try to renew the leader lease.</span><br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex)<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>乐观情况下的 ReadLocal 流程我们已经了解,接下来让我们看看 ReadIndex 在 RaftStore 中的执行路径。</p><h3 id="RaftStore"><a href="#RaftStore" class="headerlink" title="RaftStore"></a>RaftStore</h3><p>前文已经介绍过 RaftBatchSystem 的大体框架,我们已知会有多个 PollHandler 线程调用 poll 函数进入长期循环来事件驱动并动态均衡地管理所有 normal 状态机。</p><p>当 ReadIndex 请求被路由到 RaftBatchSystem 中的对应 normal 状态机后,某个 PollHandler 会在接下来的一次 loop 中处理该状态机的消息。</p><p>直接定位到 <code>RaftPoller</code> 的 <code>handle_normal</code> 函数。可以看到,其会首先尝试获取 <code>messages_per_tick</code> 次路由到该状态机的消息,接着调用 <code>PeerFsmDelegate::handle_msgs</code> 函数进行处理,</p><p>这里只列出了我们需要关注的几种消息类型:</p><ul><li>RaftMessage: 其他 Peer 发送过来 Raft 消息,包括心跳、日志、投票消息等。</li><li>RaftCommand: 上层提出的 proposal,其中包含了需要通过 Raft 同步的操作,以及操作成功之后需要调用的 callback 函数。ReadIndex 请求便是一种特殊的 proposal。</li><li>ApplyRes: ApplyFsm 在将日志应用到状态机之后发送给 PeerFsm 的消息,用于在进行操作之后更新某些内存状态。</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span><EK: KvEngine, ER: RaftEngine, T: Transport> PollHandler<PeerFsm<EK, ER>, StoreFsm<EK>><br> <span class="hljs-keyword">for</span> <span class="hljs-title class_">RaftPoller</span><EK, ER, T><br>{<br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_normal</span>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> peer: &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = PeerFsm<EK, ER>>,<br> ) <span class="hljs-punctuation">-></span> HandleResult {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">handle_result</span> = HandleResult::KeepProcessing;<br><br> ...<br><br> <span class="hljs-keyword">while</span> <span class="hljs-keyword">self</span>.peer_msg_buf.<span class="hljs-title function_ invoke__">len</span>() < <span class="hljs-keyword">self</span>.messages_per_tick {<br> <span class="hljs-keyword">match</span> peer.receiver.<span class="hljs-title function_ invoke__">try_recv</span>() {<br> <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> we may need a way to optimize the message copy.</span><br> <span class="hljs-title function_ invoke__">Ok</span>(msg) => {<br> ...<br> <span class="hljs-keyword">self</span>.peer_msg_buf.<span class="hljs-title function_ invoke__">push</span>(msg);<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Empty) => {<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(TryRecvError::Disconnected) => {<br> peer.<span class="hljs-title function_ invoke__">stop</span>();<br> handle_result = HandleResult::<span class="hljs-title function_ invoke__">stop_at</span>(<span class="hljs-number">0</span>, <span class="hljs-literal">false</span>);<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">delegate</span> = PeerFsmDelegate::<span class="hljs-title function_ invoke__">new</span>(peer, &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.poll_ctx);<br> delegate.<span class="hljs-title function_ invoke__">handle_msgs</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.peer_msg_buf);<br> <span class="hljs-comment">// No readiness is generated and using sync write, skipping calling ready and</span><br> <span class="hljs-comment">// release early.</span><br> <span class="hljs-keyword">if</span> !delegate.<span class="hljs-title function_ invoke__">collect_ready</span>() && <span class="hljs-keyword">self</span>.poll_ctx.sync_write_worker.<span class="hljs-title function_ invoke__">is_some</span>() {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">HandleResult</span>::StopAt { skip_end, .. } = &<span class="hljs-keyword">mut</span> handle_result {<br> *skip_end = <span class="hljs-literal">true</span>;<br> }<br> }<br><br> handle_result<br> }<br>}<br><br><span class="hljs-keyword">impl</span><<span class="hljs-symbol">'a</span>, EK, ER, T: Transport> PeerFsmDelegate<<span class="hljs-symbol">'a</span>, EK, ER, T><br><span class="hljs-keyword">where</span><br> EK: KvEngine,<br> ER: RaftEngine,<br>{<br> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_msgs</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, msgs: &<span class="hljs-keyword">mut</span> <span class="hljs-type">Vec</span><PeerMsg<EK>>) {<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">m</span> <span class="hljs-keyword">in</span> msgs.<span class="hljs-title function_ invoke__">drain</span>(..) {<br> <span class="hljs-keyword">match</span> m {<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftMessage</span>(msg) => {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Err</span>(e) = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_raft_message</span>(msg) {<br> error!(%e;<br> <span class="hljs-string">"handle raft message err"</span>;<br> <span class="hljs-string">"region_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">region_id</span>(),<br> <span class="hljs-string">"peer_id"</span> => <span class="hljs-keyword">self</span>.fsm.<span class="hljs-title function_ invoke__">peer_id</span>(),<br> );<br> }<br> }<br> PeerMsg::<span class="hljs-title function_ invoke__">RaftCommand</span>(cmd) => {<br> ...<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_raft_command</span>(<br> cmd.request,<br> cmd.callback,<br> cmd.extra_opts.disk_full_opt,<br> );<br> }<br> }<br> PeerMsg::ApplyRes { res } => {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">on_apply_res</span>(res);<br> }<br> ...<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>对于 ReadIndex 请求,其会进入 <code>PeerMsg::RaftCommand(cmd)</code> 分支,进而以 <code>PeerFsmDelegate::propose_raft_command -> PeerFsmDelegate::propose_raft_command_internal</code> 的调用链走到 <code>store::propose</code> 函数中,在该函数中,会再进行一次 <code>self.inspect()</code>,如果此时 Leader 的 lease 已经稳定,则会调用 <code>read_local</code> 函数直接获取引擎的 snapshot 并执行 callback 返回,否则调用 <code>read_index</code> 函数执行 ReadIndex 流程。</p><p>在 read_index 函数中,ReadIndex 请求连带 callback 会被构建成一个 ReadIndexRequest 被 push 到 pending_reads 即一个 ReadIndexQueue 中,之后当前线程即可结束本轮流程,之后的事件会进而触发该 ReadIndexRequest 的执行。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">propose</span><T: Transport>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br> <span class="hljs-keyword">mut</span> cb: Callback<EK::Snapshot>,<br> req: RaftCmdRequest,<br> <span class="hljs-keyword">mut</span> err_resp: RaftCmdResponse,<br> <span class="hljs-keyword">mut</span> disk_full_opt: DiskFullOpt,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">policy</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">inspect</span>(&req);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">match</span> policy {<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadLocal) | <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::StaleRead) => {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">read_local</span>(ctx, req, cb);<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ReadIndex) => <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">read_index</span>(ctx, req, err_resp, cb),<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeTransferLeader) => {<br> <span class="hljs-keyword">return</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_transfer_leader</span>(ctx, req, cb);<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeNormal) => {<br> <span class="hljs-comment">// For admin cmds, only region split/merge comes here.</span><br> <span class="hljs-keyword">if</span> req.<span class="hljs-title function_ invoke__">has_admin_request</span>() {<br> disk_full_opt = DiskFullOpt::AllowedOnAlmostFull;<br> }<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">check_normal_proposal_with_disk_full_opt</span>(ctx, disk_full_opt)<br> .<span class="hljs-title function_ invoke__">and_then</span>(|_| <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_normal</span>(ctx, req))<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(RequestPolicy::ProposeConfChange) => <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">propose_conf_change</span>(ctx, &req),<br> <span class="hljs-title function_ invoke__">Err</span>(e) => <span class="hljs-title function_ invoke__">Err</span>(e),<br> };<br> fail_point!(<span class="hljs-string">"after_propose"</span>);<br><br> ...<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">read_index</span><T: Transport>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> poll_ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br> <span class="hljs-keyword">mut</span> req: RaftCmdRequest,<br> <span class="hljs-keyword">mut</span> err_resp: RaftCmdResponse,<br> cb: Callback<EK::Snapshot>,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">read</span> = ReadIndexRequest::<span class="hljs-title function_ invoke__">with_command</span>(id, req, cb, now);<br> read.addition_request = request.<span class="hljs-title function_ invoke__">map</span>(<span class="hljs-type">Box</span>::new);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">push_pending_read</span>(read, <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">is_leader</span>());<br> <span class="hljs-keyword">self</span>.should_wake_up = <span class="hljs-literal">true</span>;<br><br> ...<br><br> <span class="hljs-literal">true</span><br>}<br></code></pre></td></tr></table></figure><p>那么什么条件满足后该 ReadIndexRequest 会被 pop 出队列并执行呢?</p><p>前面已经提到 ApplyBatchSystem 在应用一批日志之后首先会调用对应的 callback 尽快回复客户端,之后会发送一条 ApplyRes 的消息到 RaftBatchSystem,该消息和以上的 ReadIndex 请求一样被 PollHandler 在一次 loop 中被处理,并最终进入 <code>PeerFsmDelegate::handle_msgs</code> 函数的 <code>PeerMsg::ApplyRes { res }</code> 分支,接着其会调用 <code>PeerFsmDelegate::on_apply_res</code> 函数并进入 <code>store::peer::post_apply</code> 函数,在该函数中,ApplyRes 中携带的信息会被用来更新一些内存状态例如 <code>raft_group</code> 和 <code>cmd_epoch_checker</code>,当然,这些信息也会通过 <code>store::peer::post_pending_read_index_on_replica</code> 和 <code>self.pending_reads.pop_front()</code> 来释放某些满足条件的 ReadIndexRequest,对于每个 ReadIndexRequest ,此时可以通过 <code>store::peer::response_read</code> 函数来获取底层引擎的 Snapshot 并执行 callback 返回。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">on_apply_res</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, res: ApplyTaskRes<EK::Snapshot>) {<br> fail_point!(<span class="hljs-string">"on_apply_res"</span>, |_| {});<br> <span class="hljs-keyword">match</span> res {<br> ApplyTaskRes::<span class="hljs-title function_ invoke__">Apply</span>(<span class="hljs-keyword">mut</span> res) => {<br> <br> ...<br><br> <span class="hljs-keyword">self</span>.fsm.has_ready |= <span class="hljs-keyword">self</span>.fsm.peer.<span class="hljs-title function_ invoke__">post_apply</span>(<br> <span class="hljs-keyword">self</span>.ctx,<br> res.apply_state,<br> res.applied_term,<br> &res.metrics,<br> );<br> <br> ...<br> }<br> ApplyTaskRes::Destroy {<br> region_id,<br> peer_id,<br> merge_from_snapshot,<br> } => {<br> ...<br> }<br> }<br>}<br><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">post_apply</span><T>(<br> &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br> ctx: &<span class="hljs-keyword">mut</span> PollContext<EK, ER, T>,<br> apply_state: RaftApplyState,<br> applied_term: <span class="hljs-type">u64</span>,<br> apply_metrics: &ApplyMetrics,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">has_ready</span> = <span class="hljs-literal">false</span>;<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">is_handling_snapshot</span>() {<br> <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"{} should not applying snapshot."</span>, <span class="hljs-keyword">self</span>.tag);<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">applied_index</span> = apply_state.<span class="hljs-title function_ invoke__">get_applied_index</span>();<br> <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">advance_apply_to</span>(applied_index);<br><br> <span class="hljs-keyword">self</span>.cmd_epoch_checker.<span class="hljs-title function_ invoke__">advance_apply</span>(<br> applied_index,<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">term</span>(),<br> <span class="hljs-keyword">self</span>.raft_group.<span class="hljs-title function_ invoke__">store</span>().<span class="hljs-title function_ invoke__">region</span>(),<br> );<br><br> ...<br><br> <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">is_leader</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">post_pending_read_index_on_replica</span>(ctx)<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">ready_to_handle_read</span>() {<br> <span class="hljs-keyword">while</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(<span class="hljs-keyword">mut</span> read) = <span class="hljs-keyword">self</span>.pending_reads.<span class="hljs-title function_ invoke__">pop_front</span>() {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">response_read</span>(&<span class="hljs-keyword">mut</span> read, ctx, <span class="hljs-literal">false</span>);<br> }<br> }<br> <span class="hljs-keyword">self</span>.pending_reads.<span class="hljs-title function_ invoke__">gc</span>();<br><br> ...<br><br> has_ready<br>}<br></code></pre></td></tr></table></figure><p>综上,ReadIndexRequest 入队和出队的时机已经被介绍,那么 ReadIndex 的整体流程也基本介绍完整了。</p><p>通过本小节,希望您能够了解 KVGet 读请求的完整流程,并进而具备分析其他读请求全链路的能力。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇博客介绍了 TiKV 中一条读请求的全链路流程。</p><p>希望本博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>感谢您的阅读~</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value </summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="源码阅读" scheme="https://tanxinyu.work/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="TiKV" scheme="https://tanxinyu.work/tags/TiKV/"/>
</entry>
<entry>
<title>TiKV 源码阅读三部曲(一)重要模块</title>
<link href="https://tanxinyu.work/tikv-source-code-reading-module/"/>
<id>https://tanxinyu.work/tikv-source-code-reading-module/</id>
<published>2022-10-05T08:37:12.000Z</published>
<updated>2022-10-08T06:59:08.845Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value 数据库,目前已经是 <a href="https://www.cncf.io/projects/">CNCF 基金会</a> 的顶级项目。</p><p>作为一个新同学,需要一定的前期准备才能够有能力参与 TiKV 社区的代码开发,包括但不限于学习 Rust 语言,理解 TiKV 的原理和在前两者的基础上了解熟悉 TiKV 的源码。</p><p><a href="https://pingcap.com/zh/blog/?tag=TiKV%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90">TiKV 官方源码解析文档</a> 详细地介绍了 TiKV 3.x 版本重要模块的设计要点,主要流程和相应代码片段,是学习 TiKV 源码必读的学习资料。当前 TiKV 已经迭代到了 6.x 版本,不仅引入了很多新的功能和优化,而且对源码也进行了多次重构,因而一些官方源码解析文档中的代码片段已经不复存在,这使得读者在阅读源码解析文档时无法对照最新源码加深理解;此外尽管 TiKV 官方源码解析文档系统地介绍了若干重要模块的工作,但并没有将读写流程全链路串起来去介绍经过的模块和对应的代码片段,实际上尽快地熟悉读写流程全链路会更利于新同学从全局角度理解代码。</p><p>基于以上存在的问题,笔者将基于 6.1 版本的源码撰写三篇博客,分别介绍以下三个方面:</p><ul><li><a href="https://tanxinyu.work/tikv-source-code-reading-module/">TiKV 源码阅读三部曲(一)重要模块</a>:TiKV 的基本概念,TiKV 读写路径上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的方案</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-read/">TiKV 源码阅读三部曲(二)读流程</a>:TiKV 中一条读请求的全链路流程</li><li><a href="https://tanxinyu.work/tikv-source-code-reading-write/">TiKV 源码阅读三部曲(三)写流程</a>:TiKV 中一条写请求的全链路流程</li></ul><p>希望此三篇博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>本文为第一篇博客,将主要介绍 TiKV 的基本概念,TiKV 读写路径上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的方案。</p><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p>TiKV 的架构简介可以查看 <a href="https://docs.pingcap.com/zh/tidb/dev/tikv-overview">官方文档</a>。总体来看,TiKV 是一个通过 Multi-Raft 实现的分布式 KV 数据库。</p><p>TiKV 的每个进程拥有一个 store,store 中拥有若干 region。每个 region 是一个 raft 组,会存在于副本数个 store 上管理一段 KV 区间的数据。</p><p><img src="https://download.pingcap.com/images/docs-cn/tikv-arch.png" alt></p><h2 id="重要模块"><a href="#重要模块" class="headerlink" title="重要模块"></a>重要模块</h2><h3 id="KVService"><a href="#KVService" class="headerlink" title="KVService"></a>KVService</h3><p>TiKV 的 Service 层代码位于 src/server 文件夹下,其职责包括提供 RPC 服务、将 store id 解析成地址、TiKV 之间的相互通信等。有关 Service 层的概念解析可以查看阅读 <a href="https://cn.pingcap.com/blog/tikv-source-code-reading-9">TiKV 源码解析系列文章(九)Service 层处理流程解析</a>。</p><p><img src="https://img1.www.pingcap.com/prod/1_1c0d99f78e.png" alt></p><p>TiKV 包含多个 gRPC service。其中最重要的一个是 KVService,位于 src/server/service/kv.rs 文件中。</p><p>KVService 定义了 TiKV 的 kv_get,kv_scan,kv_prewrite,kv_commit 等事务操作 API,用于执行 TiDB 下推下来的复杂查询和计算的 coprocessor API,以及 raw_get,raw_put 等 Raw KV API。batch_commands 接口则是用于将上述的接口 batch 起来,以优化高吞吐量的场景。另外,TiKV 的 Raft group 各成员之间通信用到的 raft 和 batch_raft 接口也是在这里提供的。</p><p>本小节将简单介绍 KVService 及其启动流程,并顺带介绍 TiKV 若干重要结构的初始化流程。</p><p>cmd/tikv-server/main.rs 是 TiKV 进程启动的入口,其主要做了以下两个工作:</p><ul><li>解析配置参数</li><li>使用 <code>server::server::run_tikv(config)</code> 启动 tikv 进程</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">build_timestamp</span> = <span class="hljs-built_in">option_env!</span>(<span class="hljs-string">"TIKV_BUILD_TIME"</span>);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">version_info</span> = tikv::<span class="hljs-title function_ invoke__">tikv_version_info</span>(build_timestamp);<br> <br> <span class="hljs-comment">// config parsing</span><br> <span class="hljs-comment">// ...</span><br> <span class="hljs-comment">// config parsing</span><br> <br> server::server::<span class="hljs-title function_ invoke__">run_tikv</span>(config);<br>}<br></code></pre></td></tr></table></figure><p>对于 components/server/src/server.rs 的 run-tikv 函数,其会调用 run_impl 函数并根据配置参数来启动对应的 KV 引擎。</p><p>在 run_impl 函数中,首先会调用 <code>TikvServer::<CER>::init::<F>(config)</code> 函数做若干重要结构的初始化,包含但不限于 batch_system, concurrency_manager, background_worker, quota_limiter 等等,接着在 <code>tikv.init_servers::<F>()</code> 里将 RPC handler 与 KVService 绑定起来,最后在 <code>tikv.run_server(server_config)</code> 中便会使用 <a href="https://pingcap.com/zh/blog/tikv-source-code-reading-7">TiKV 源码解析系列文章(七)gRPC Server 的初始化和启动流程</a> 中介绍的 grpc server 绑定对应的端口并开始监听连接了。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><code class="hljs Rust"><span class="hljs-comment">/// Run a TiKV server. Returns when the server is shutdown by the user, in which</span><br><span class="hljs-comment">/// case the server will be properly stopped.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">run_tikv</span>(config: TikvConfig) {<br><br> ...<br><br> <span class="hljs-comment">// Do some prepare works before start.</span><br> <span class="hljs-title function_ invoke__">pre_start</span>();<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">_m</span> = Monitor::<span class="hljs-title function_ invoke__">default</span>();<br><br> dispatch_api_version!(config.storage.<span class="hljs-title function_ invoke__">api_version</span>(), {<br> <span class="hljs-keyword">if</span> !config.raft_engine.enable {<br> run_impl::<RocksEngine, API>(config)<br> } <span class="hljs-keyword">else</span> {<br> run_impl::<RaftLogEngine, API>(config)<br> }<br> })<br>}<br><br><span class="hljs-meta">#[inline]</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">run_impl</span><CER: ConfiguredRaftEngine, F: KvFormat>(config: TikvConfig) {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">tikv</span> = TikvServer::<CER>::init::<F>(config);<br><br> ...<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">server_config</span> = tikv.init_servers::<F>();<br> <br> ...<br> <br> tikv.<span class="hljs-title function_ invoke__">run_server</span>(server_config);<br><br> signal_handler::<span class="hljs-title function_ invoke__">wait_for_signal</span>(<span class="hljs-title function_ invoke__">Some</span>(tikv.engines.<span class="hljs-title function_ invoke__">take</span>().<span class="hljs-title function_ invoke__">unwrap</span>().engines));<br> tikv.<span class="hljs-title function_ invoke__">stop</span>();<br>}<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">run_server</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, server_config: Arc<VersionTrack<ServerConfig>>) {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">server</span> = <span class="hljs-keyword">self</span>.servers.<span class="hljs-title function_ invoke__">as_mut</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> server<br> .server<br> .<span class="hljs-title function_ invoke__">build_and_bind</span>()<br> .<span class="hljs-title function_ invoke__">unwrap_or_else</span>(|e| fatal!(<span class="hljs-string">"failed to build server: {}"</span>, e));<br> server<br> .server<br> .<span class="hljs-title function_ invoke__">start</span>(server_config, <span class="hljs-keyword">self</span>.security_mgr.<span class="hljs-title function_ invoke__">clone</span>())<br> .<span class="hljs-title function_ invoke__">unwrap_or_else</span>(|e| fatal!(<span class="hljs-string">"failed to start server: {}"</span>, e));<br>}<br></code></pre></td></tr></table></figure><p>KVService 服务启动后,所有发往监听端口的请求便会路由到 KVService 对应的 handler 上。有关 KVService 目前支持的接口,可以直接查看 <a href="https://github.com/pingcap/kvproto/blob/master/proto/tikvpb.proto#L20">kvproto</a> 对应的 <code>service Tikv</code>,目前的 RPC 接口已经接近 60 个,每个接口在代码中都会对应一个 handler。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs proto3">// Key/value store API for TiKV.<br>service Tikv {<br> // Commands using a transactional interface.<br> rpc KvGet(kvrpcpb.GetRequest) returns (kvrpcpb.GetResponse) {}<br> rpc KvScan(kvrpcpb.ScanRequest) returns (kvrpcpb.ScanResponse) {}<br> rpc KvPrewrite(kvrpcpb.PrewriteRequest) returns (kvrpcpb.PrewriteResponse) {}<br> rpc KvPessimisticLock(kvrpcpb.PessimisticLockRequest) returns (kvrpcpb.PessimisticLockResponse) {}<br> rpc KVPessimisticRollback(kvrpcpb.PessimisticRollbackRequest) returns (kvrpcpb.PessimisticRollbackResponse) {}<br> ...<br>}<br></code></pre></td></tr></table></figure><p>当 KVService 收到请求之后,会根据请求的类型把这些请求转发到不同的模块进行处理。对于从 TiDB 下推的读请求,比如 sum,avg 操作,会转发到 Coprocessor 模块进行处理,对于 KV 请求会直接转发到 Storage 模块进行处理。</p><p>KV 操作根据功能可以被划分为 Raw KV 操作以及 Txn KV 操作两大类。Raw KV 操作包括 raw put、raw get、raw delete、raw batch get、raw batch put、raw batch delete、raw scan 等普通 KV 操作。 Txn KV 操作是为了实现事务机制而设计的一系列操作,如 prewrite 和 commit 分别对应于 2PC 中的 prepare 和 commit 阶段的操作。</p><p>与 <a href="https://pingcap.com/zh/blog/tikv-source-code-reading-7">TiKV 源码解析系列文章(七)gRPC Server 的初始化和启动流程</a> 中介绍的 handler example 不同,当前 KVService 对事务 API 例如 kv_prewrite, kv_commit 和 Raw API 例如 raw_get, raw_scan 进行了封装,由于他们都会被路由到 Storage 模块,所以接口无关的逻辑都被封装到了 <code>handle_request</code> 宏中,接口相关的逻辑则被封装到了 future_prewirte, future_commit 等 future_xxx 函数中。需要注意的是,对于 coprocessor API,raft API 等相关接口依然采用了原生对接 grpc-rs 的方式。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-built_in">macro_rules!</span> handle_request {<br> ($fn_name: ident, $future_name: ident, $req_ty: ident, $resp_ty: ident) => {<br> handle_request!($fn_name, $future_name, $req_ty, $resp_ty, no_time_detail);<br> };<br> ($fn_name: ident, $future_name: ident, $req_ty: ident, $resp_ty: ident, $time_detail: tt) => {<br> <span class="hljs-keyword">fn</span> $<span class="hljs-title function_ invoke__">fn_name</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, ctx: RpcContext<<span class="hljs-symbol">'_</span>>, <span class="hljs-keyword">mut</span> req: $req_ty, sink: UnarySink<$resp_ty>) {<br> forward_unary!(<span class="hljs-keyword">self</span>.proxy, $fn_name, ctx, req, sink);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin_instant</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">source</span> = req.<span class="hljs-title function_ invoke__">mut_context</span>().<span class="hljs-title function_ invoke__">take_request_source</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">resp</span> = $<span class="hljs-title function_ invoke__">future_name</span>(&<span class="hljs-keyword">self</span>.storage, req);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">resp</span> = resp.<span class="hljs-keyword">await</span>?;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">elapsed</span> = begin_instant.<span class="hljs-title function_ invoke__">saturating_elapsed</span>();<br> set_total_time!(resp, elapsed, $time_detail);<br> sink.<span class="hljs-title function_ invoke__">success</span>(resp).<span class="hljs-keyword">await</span>?;<br> GRPC_MSG_HISTOGRAM_STATIC<br> .$fn_name<br> .<span class="hljs-title function_ invoke__">observe</span>(elapsed.<span class="hljs-title function_ invoke__">as_secs_f64</span>());<br> <span class="hljs-title function_ invoke__">record_request_source_metrics</span>(source, elapsed);<br> ServerResult::<span class="hljs-title function_ invoke__">Ok</span>(())<br> }<br> .<span class="hljs-title function_ invoke__">map_err</span>(|e| {<br> log_net_error!(e, <span class="hljs-string">"kv rpc failed"</span>;<br> <span class="hljs-string">"request"</span> => <span class="hljs-built_in">stringify!</span>($fn_name)<br> );<br> GRPC_MSG_FAIL_COUNTER.$fn_name.<span class="hljs-title function_ invoke__">inc</span>();<br> })<br> .<span class="hljs-title function_ invoke__">map</span>(|_|());<br><br> ctx.<span class="hljs-title function_ invoke__">spawn</span>(task);<br> }<br> }<br>}<br><br><span class="hljs-keyword">impl</span><T: RaftStoreRouter<E::Local> + <span class="hljs-symbol">'static</span>, E: Engine, L: LockManager, F: KvFormat> Tikv<br> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Service</span><T, E, L, F><br>{<br> handle_request!(kv_get, future_get, GetRequest, GetResponse, has_time_detail);<br> handle_request!(kv_scan, future_scan, ScanRequest, ScanResponse);<br> handle_request!(<br> kv_prewrite,<br> future_prewrite,<br> PrewriteRequest,<br> PrewriteResponse,<br> has_time_detail<br> );<br><br> ...<br><br> handle_request!(raw_get, future_raw_get, RawGetRequest, RawGetResponse);<br> handle_request!(<br> raw_batch_get,<br> future_raw_batch_get,<br> RawBatchGetRequest,<br> RawBatchGetResponse<br> );<br> handle_request!(raw_scan, future_raw_scan, RawScanRequest, RawScanResponse);<br><br> ...<br><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">coprocessor</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, ctx: RpcContext<<span class="hljs-symbol">'_</span>>, <span class="hljs-keyword">mut</span> req: Request, sink: UnarySink<Response>) {<br> forward_unary!(<span class="hljs-keyword">self</span>.proxy, coprocessor, ctx, req, sink);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">source</span> = req.<span class="hljs-title function_ invoke__">mut_context</span>().<span class="hljs-title function_ invoke__">take_request_source</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">begin_instant</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">future</span> = <span class="hljs-title function_ invoke__">future_copr</span>(&<span class="hljs-keyword">self</span>.copr, <span class="hljs-title function_ invoke__">Some</span>(ctx.<span class="hljs-title function_ invoke__">peer</span>()), req);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">resp</span> = future.<span class="hljs-keyword">await</span>?.<span class="hljs-title function_ invoke__">consume</span>();<br> sink.<span class="hljs-title function_ invoke__">success</span>(resp).<span class="hljs-keyword">await</span>?;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">elapsed</span> = begin_instant.<span class="hljs-title function_ invoke__">saturating_elapsed</span>();<br> GRPC_MSG_HISTOGRAM_STATIC<br> .coprocessor<br> .<span class="hljs-title function_ invoke__">observe</span>(elapsed.<span class="hljs-title function_ invoke__">as_secs_f64</span>());<br> <span class="hljs-title function_ invoke__">record_request_source_metrics</span>(source, elapsed);<br> ServerResult::<span class="hljs-title function_ invoke__">Ok</span>(())<br> }<br> .<span class="hljs-title function_ invoke__">map_err</span>(|e| {<br> log_net_error!(e, <span class="hljs-string">"kv rpc failed"</span>;<br> <span class="hljs-string">"request"</span> => <span class="hljs-string">"coprocessor"</span><br> );<br> GRPC_MSG_FAIL_COUNTER.coprocessor.<span class="hljs-title function_ invoke__">inc</span>();<br> })<br> .<span class="hljs-title function_ invoke__">map</span>(|_| ());<br><br> ctx.<span class="hljs-title function_ invoke__">spawn</span>(task);<br> }<br><br> ...<br>}<br></code></pre></td></tr></table></figure><p>在事务相关 API 的 future_xxx 函数实现中,对于带有写语义的 future_prewrite, future_commit 等函数,由于它们会被统一调度到 Storage 模块的 sched_txn_command 函数中,当前又抽象出了 <code>txn_command_future</code> 宏来减少冗余代码;对于带有读语义的 future_get, future_scan 等函数,由于他们会分别调用 Storage 模块的 get/scan 等函数,因而目前并没有进行进一步抽象。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-built_in">macro_rules!</span> txn_command_future {<br> ($fn_name: ident, $req_ty: ident, $resp_ty: ident, ($req: ident) $prelude: stmt; ($v: ident, $resp: ident, $tracker: ident) { $else_branch: expr }) => {<br> <span class="hljs-keyword">fn</span> $fn_name<E: Engine, L: LockManager, F: KvFormat>(<br> storage: &Storage<E, L, F>,<br> $req: $req_ty,<br> ) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">Future</span><Output = ServerResult<$resp_ty>> {<br> $prelude<br> <span class="hljs-keyword">let</span> $tracker = GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">insert</span>(Tracker::<span class="hljs-title function_ invoke__">new</span>(RequestInfo::<span class="hljs-title function_ invoke__">new</span>(<br> $req.<span class="hljs-title function_ invoke__">get_context</span>(),<br> RequestType::Unknown,<br> <span class="hljs-number">0</span>,<br> )));<br> <span class="hljs-title function_ invoke__">set_tls_tracker_token</span>($tracker);<br> <span class="hljs-keyword">let</span> (cb, f) = <span class="hljs-title function_ invoke__">paired_future_callback</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = storage.<span class="hljs-title function_ invoke__">sched_txn_command</span>($req.<span class="hljs-title function_ invoke__">into</span>(), cb);<br><br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> defer!{{<br> GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">remove</span>($tracker);<br> }};<br> <span class="hljs-keyword">let</span> $v = <span class="hljs-keyword">match</span> res {<br> <span class="hljs-title function_ invoke__">Err</span>(e) => <span class="hljs-title function_ invoke__">Err</span>(e),<br> <span class="hljs-title function_ invoke__">Ok</span>(_) => f.<span class="hljs-keyword">await</span>?,<br> };<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">mut</span> $resp = $resp_ty::<span class="hljs-title function_ invoke__">default</span>();<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(err) = <span class="hljs-title function_ invoke__">extract_region_error</span>(&$v) {<br> $resp.<span class="hljs-title function_ invoke__">set_region_error</span>(err);<br> } <span class="hljs-keyword">else</span> {<br> $else_branch;<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>($resp)<br> }<br> }<br> };<br> ($fn_name: ident, $req_ty: ident, $resp_ty: ident, ($v: ident, $resp: ident, $tracker: ident) { $else_branch: expr }) => {<br> txn_command_future!($fn_name, $req_ty, $resp_ty, (req) {}; ($v, $resp, $tracker) { $else_branch });<br> };<br> ($fn_name: ident, $req_ty: ident, $resp_ty: ident, ($v: ident, $resp: ident) { $else_branch: expr }) => {<br> txn_command_future!($fn_name, $req_ty, $resp_ty, (req) {}; ($v, $resp, tracker) { $else_branch });<br> };<br>}<br><br>txn_command_future!(future_prewrite, PrewriteRequest, PrewriteResponse, (v, resp, tracker) {{<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Ok</span>(v) = &v {<br> resp.<span class="hljs-title function_ invoke__">set_min_commit_ts</span>(v.min_commit_ts.<span class="hljs-title function_ invoke__">into_inner</span>());<br> resp.<span class="hljs-title function_ invoke__">set_one_pc_commit_ts</span>(v.one_pc_commit_ts.<span class="hljs-title function_ invoke__">into_inner</span>());<br> GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">with_tracker</span>(tracker, |tracker| {<br> tracker.<span class="hljs-title function_ invoke__">write_scan_detail</span>(resp.<span class="hljs-title function_ invoke__">mut_exec_details_v2</span>().<span class="hljs-title function_ invoke__">mut_scan_detail_v2</span>());<br> tracker.<span class="hljs-title function_ invoke__">write_write_detail</span>(resp.<span class="hljs-title function_ invoke__">mut_exec_details_v2</span>().<span class="hljs-title function_ invoke__">mut_write_detail</span>());<br> });<br> }<br> resp.<span class="hljs-title function_ invoke__">set_errors</span>(<span class="hljs-title function_ invoke__">extract_key_errors</span>(v.<span class="hljs-title function_ invoke__">map</span>(|v| v.locks)).<span class="hljs-title function_ invoke__">into</span>());<br>}});<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">future_get</span><E: Engine, L: LockManager, F: KvFormat>(<br> storage: &Storage<E, L, F>,<br> <span class="hljs-keyword">mut</span> req: GetRequest,<br>) <span class="hljs-punctuation">-></span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">Future</span><Output = ServerResult<GetResponse>> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tracker</span> = GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">insert</span>(Tracker::<span class="hljs-title function_ invoke__">new</span>(RequestInfo::<span class="hljs-title function_ invoke__">new</span>(<br> req.<span class="hljs-title function_ invoke__">get_context</span>(),<br> RequestType::KvGet,<br> req.<span class="hljs-title function_ invoke__">get_version</span>(),<br> )));<br> <span class="hljs-title function_ invoke__">set_tls_tracker_token</span>(tracker);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">start</span> = Instant::<span class="hljs-title function_ invoke__">now</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">v</span> = storage.<span class="hljs-title function_ invoke__">get</span>(<br> req.<span class="hljs-title function_ invoke__">take_context</span>(),<br> Key::<span class="hljs-title function_ invoke__">from_raw</span>(req.<span class="hljs-title function_ invoke__">get_key</span>()),<br> req.<span class="hljs-title function_ invoke__">get_version</span>().<span class="hljs-title function_ invoke__">into</span>(),<br> );<br><br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">v</span> = v.<span class="hljs-keyword">await</span>;<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">duration_ms</span> = <span class="hljs-title function_ invoke__">duration_to_ms</span>(start.<span class="hljs-title function_ invoke__">saturating_elapsed</span>());<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">resp</span> = GetResponse::<span class="hljs-title function_ invoke__">default</span>();<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(err) = <span class="hljs-title function_ invoke__">extract_region_error</span>(&v) {<br> resp.<span class="hljs-title function_ invoke__">set_region_error</span>(err);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">match</span> v {<br> <span class="hljs-title function_ invoke__">Ok</span>((val, stats)) => {<br> ...<br> }<br> <span class="hljs-title function_ invoke__">Err</span>(e) => resp.<span class="hljs-title function_ invoke__">set_error</span>(<span class="hljs-title function_ invoke__">extract_key_error</span>(&e)),<br> }<br> }<br> GLOBAL_TRACKERS.<span class="hljs-title function_ invoke__">remove</span>(tracker);<br> <span class="hljs-title function_ invoke__">Ok</span>(resp)<br> }<br>}<br></code></pre></td></tr></table></figure><p>自 3.x 版本以来,KVService 利用了多个宏显著减少了不同 RPC handler 间的冗余代码,然而这些宏目前还不能被 Clion 等调试工具的函数调用关系链捕捉到,这可能会困惑刚开始查看函数调用链却无法找到对应 handler 的新同学。</p><p>通过本小节,希望您能够了解 KVService 的作用和 TiKV 的启动流程,不仅具备寻找全局重要结构体初始化代码片段的能力,还能够迅速找到 KVService 中需要的 RPC handler 开始从上到下追踪 RPC 请求的调用路径。</p><h3 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h3><p>Storage 模块位于 Service 与底层 KV 存储引擎之间,主要负责事务的并发控制。TiKV 端事务相关的实现都在 Storage 模块中。有关 3.x 版本的 Storage 模块可以参照 <a href="https://cn.pingcap.com/blog/tikv-source-code-reading-11">TiKV 源码解析系列文章(十一)Storage - 事务控制层</a>。</p><p>经过三个大版本的迭代,Storage 和 Scheduler 结构体已经发生了一些变化,本小节将基于之前的源码解析文档做一些更新和补充。</p><p>Storage 结构体:</p><ul><li>engine:代表的是底层的 KV 存储引擎,利用 Trait Bound 来约束接口,拥有多种实现。实际 TiKV 使用的是 RaftKV 引擎,当调用 RaftKV 的 async_write 进行写入操作时,如果 async_write 通过回调方式成功返回了,说明写入操作已经通过 raft 复制给了大多数副本,并且在 leader 节点(调用者所在 TiKV)完成写入,后续 leader 节点上的读就能够看到之前写入的内容</li><li>sched:事务调度器,负责并发事务请求的调度工作</li><li>readPool:读取线程池,所有只读 KV 请求,包括事务的和非事务的,如 raw get、txn kv get 等最终都会在这个线程池内执行。由于只读请求不需要获取 latches,所以为其分配一个独立的线程池直接执行,而不是与非只读事务共用事务调度器。值得注意的是,当前版本的 readPool 已经支持根据读请求中的 priority 字段来差别调度读请求,而不是全部看做相同优先级的任务来公平调度</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Storage</span><E: Engine, L: LockManager, F: KvFormat> {<br> <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Too many Arcs, would be slow when clone.</span><br> engine: E,<br><br> sched: TxnScheduler<E, L>,<br><br> <span class="hljs-comment">/// The thread pool used to run most read operations.</span><br> read_pool: ReadPoolHandle,<br><br> ...<br>}<br><br><span class="hljs-meta">#[derive(Clone)]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">ReadPoolHandle</span> {<br> FuturePools {<br> read_pool_high: FuturePool,<br> read_pool_normal: FuturePool,<br> read_pool_low: FuturePool,<br> },<br> Yatp {<br> remote: Remote<TaskCell>,<br> running_tasks: IntGauge,<br> max_tasks: <span class="hljs-type">usize</span>,<br> pool_size: <span class="hljs-type">usize</span>,<br> },<br>}<br></code></pre></td></tr></table></figure><p>Scheduler 结构体:</p><ul><li>id_alloc:到达 Scheduler 的请求都会被分配一个唯一的 command id</li><li>latches:写请求到达 Scheduler 之后会尝试获取所需要的 latch,如果暂时获取不到所需要的 latch,其对应的 command id 会被插入到 latch 的 waiting list 里,当前面的请求执行结束后会唤醒 waiting list 里的请求继续执行。至于为什么需要 latches,可以参考 <a href="https://pingcap.com/zh/blog/tikv-source-code-reading-12">TiKV 源码解析系列文章(十二)分布式事务</a> 中的 <code>Scheduler 与 Latch</code> 章节</li><li>task_slots:用于存储 Scheduler 中所有请求的上下文,比如暂时未能获取到所有所需 latch 的请求会被暂存在 task_slots 中</li><li>lock_mgr:悲观事务冲突管理器,当多个并行悲观事务之间存在冲突时可能会暂时阻塞某些事务。TiKV 悲观事务具体原理可参考博客 <a href="https://zhuanlan.zhihu.com/p/79034576">TiDB 新特性漫谈:悲观事务</a></li><li>pipelined_pessimistic_lock/in_memory_pessimistic_lock/enable_async_apply_prewrite:TiKV 悲观事务若干优化引入的新字段,具体优化可参考博客 <a href="https://baijiahao.baidu.com/s?id=1735680656672605012&wfr=spider&for=pc">TiDB 6.0 实战分享丨内存悲观锁原理浅析与实践</a></li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Scheduler which schedules the execution of `storage::Command`s.</span><br><span class="hljs-meta">#[derive(Clone)]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Scheduler</span><E: Engine, L: LockManager> {<br> inner: Arc<SchedulerInner<L>>,<br> <span class="hljs-comment">// The engine can be fetched from the thread local storage of scheduler threads.</span><br> <span class="hljs-comment">// So, we don't store the engine here.</span><br> _engine: PhantomData<E>,<br>}<br><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">SchedulerInner</span><L: LockManager> {<br> <span class="hljs-comment">// slot_id -> { cid -> `TaskContext` } in the slot.</span><br> task_slots: <span class="hljs-type">Vec</span><CachePadded<Mutex<HashMap<<span class="hljs-type">u64</span>, TaskContext>>>>,<br><br> <span class="hljs-comment">// cmd id generator</span><br> id_alloc: CachePadded<AtomicU64>,<br><br> <span class="hljs-comment">// write concurrency control</span><br> latches: Latches,<br><br> sched_pending_write_threshold: <span class="hljs-type">usize</span>,<br><br> <span class="hljs-comment">// worker pool</span><br> worker_pool: SchedPool,<br><br> <span class="hljs-comment">// high priority commands and system commands will be delivered to this pool</span><br> high_priority_pool: SchedPool,<br><br> <span class="hljs-comment">// used to control write flow</span><br> running_write_bytes: CachePadded<AtomicUsize>,<br><br> flow_controller: Arc<FlowController>,<br><br> control_mutex: Arc<tokio::sync::Mutex<<span class="hljs-type">bool</span>>>,<br><br> lock_mgr: L,<br><br> concurrency_manager: ConcurrencyManager,<br><br> pipelined_pessimistic_lock: Arc<AtomicBool>,<br><br> in_memory_pessimistic_lock: Arc<AtomicBool>,<br><br> enable_async_apply_prewrite: <span class="hljs-type">bool</span>,<br><br> resource_tag_factory: ResourceTagFactory,<br><br> quota_limiter: Arc<QuotaLimiter>,<br> feature_gate: FeatureGate,<br>}<br></code></pre></td></tr></table></figure><p>最开始看到 id_alloc 和 task_slots 的介绍时往往会好奇为每个 command 生成唯一 id 的意义是什么? task_slots 里面存的上下文到底是什么?实际上这与 TiKV 的异步执行框架有关系。</p><p>以下是 Storage 模块执行事务请求的关键函数 schedule_command,可以看到,每个请求一进入函数首先会申请一个递增唯一的 cid,接着依据该 cid 将本次请求的 command 包在一个 task 中,然后将该 task 附带 callback 生成一个 TaskContext 插入到 task_slot 中,之后便会尝试去申请 latches,如果成功便会继续调用 execute 函数去真正执行 task,否则便似乎没有下文了?那么如果 task 申请 latches 失败,之后该 task 会在什么时候被执行呢?</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">schedule_command</span>(&<span class="hljs-keyword">self</span>, cmd: Command, callback: StorageCallback) {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cid</span> = <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">gen_id</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tracker</span> = <span class="hljs-title function_ invoke__">get_tls_tracker_token</span>();<br> debug!(<span class="hljs-string">"received new command"</span>; <span class="hljs-string">"cid"</span> => cid, <span class="hljs-string">"cmd"</span> => ?cmd, <span class="hljs-string">"tracker"</span> => ?tracker);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tag</span> = cmd.<span class="hljs-title function_ invoke__">tag</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">priority_tag</span> = <span class="hljs-title function_ invoke__">get_priority_tag</span>(cmd.<span class="hljs-title function_ invoke__">priority</span>());<br> SCHED_STAGE_COUNTER_VEC.<span class="hljs-title function_ invoke__">get</span>(tag).new.<span class="hljs-title function_ invoke__">inc</span>();<br> SCHED_COMMANDS_PRI_COUNTER_VEC_STATIC<br> .<span class="hljs-title function_ invoke__">get</span>(priority_tag)<br> .<span class="hljs-title function_ invoke__">inc</span>();<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">task_slot</span> = <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">get_task_slot</span>(cid);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">tctx</span> = task_slot.<span class="hljs-title function_ invoke__">entry</span>(cid).<span class="hljs-title function_ invoke__">or_insert_with</span>(|| {<br> <span class="hljs-keyword">self</span>.inner<br> .<span class="hljs-title function_ invoke__">new_task_context</span>(Task::<span class="hljs-title function_ invoke__">new</span>(cid, tracker, cmd), callback)<br> });<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.inner.latches.<span class="hljs-title function_ invoke__">acquire</span>(&<span class="hljs-keyword">mut</span> tctx.lock, cid) {<br> fail_point!(<span class="hljs-string">"txn_scheduler_acquire_success"</span>);<br> tctx.<span class="hljs-title function_ invoke__">on_schedule</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = tctx.task.<span class="hljs-title function_ invoke__">take</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-title function_ invoke__">drop</span>(task_slot);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">execute</span>(task);<br> <span class="hljs-keyword">return</span>;<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">task</span> = tctx.task.<span class="hljs-title function_ invoke__">as_ref</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">deadline</span> = task.cmd.<span class="hljs-title function_ invoke__">deadline</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">cmd_ctx</span> = task.cmd.<span class="hljs-title function_ invoke__">ctx</span>().<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">fail_fast_or_check_deadline</span>(cid, tag, cmd_ctx, deadline);<br> fail_point!(<span class="hljs-string">"txn_scheduler_acquire_fail"</span>);<br>}<br><br><span class="hljs-comment">/// Task is a running command.</span><br><span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">super</span>) <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Task</span> {<br> <span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">super</span>) cid: <span class="hljs-type">u64</span>,<br> <span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">super</span>) tracker: TrackerToken,<br> <span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">super</span>) cmd: Command,<br> <span class="hljs-title function_ invoke__">pub</span>(<span class="hljs-keyword">super</span>) extra_op: ExtraOp,<br>}<br><br><span class="hljs-comment">// It stores context of a task.</span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">TaskContext</span> {<br> task: <span class="hljs-type">Option</span><Task>,<br><br> lock: Lock,<br> cb: <span class="hljs-type">Option</span><StorageCallback>,<br> pr: <span class="hljs-type">Option</span><ProcessResult>,<br> <span class="hljs-comment">// The one who sets `owned` from false to true is allowed to take</span><br> <span class="hljs-comment">// `cb` and `pr` safely.</span><br> owned: AtomicBool,<br> write_bytes: <span class="hljs-type">usize</span>,<br> tag: CommandKind,<br> <span class="hljs-comment">// How long it waits on latches.</span><br> <span class="hljs-comment">// latch_timer: Option<Instant>,</span><br> latch_timer: Instant,<br> <span class="hljs-comment">// Total duration of a command.</span><br> _cmd_timer: CmdTimer,<br>}<br><br><span class="hljs-comment">/// Latches which are used for concurrency control in the scheduler.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// Each latch is indexed by a slot ID, hence the term latch and slot are used</span><br><span class="hljs-comment">/// interchangeably, but conceptually a latch is a queue, and a slot is an index</span><br><span class="hljs-comment">/// to the queue.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Latches</span> {<br> slots: <span class="hljs-type">Vec</span><CachePadded<Mutex<Latch>>>,<br> size: <span class="hljs-type">usize</span>,<br>}<br></code></pre></td></tr></table></figure><p>进入 <code>Latches::acquire</code> 函数中去细究,可以看到其会渐进的去收集所有 latch,如果在本次函数调用中没有收集到所有的 latch, 当前线程不会受到任何阻塞而是直接返回 false。当然在返回 false 之前其也会利用 <code>latch.wair_for_wake</code> 函数将当前 task 的 id 放到对应 latch 的 waiting 队列里面,之后当前线程便可以处理其他的任务而不是阻塞在该任务上。由于每个获取到所有 latch 去执行的任务会在执行结束后调用 <code>scheduler::release_lock</code> 函数来释放所拥有的全部 latch,在释放过程中,其便能够获取到阻塞在这些 latch 且位于 waiting 队列首位的所有其他 task,接着对应线程会调用 <code>scheduler::try_to_wake_up</code> 函数遍历唤醒这些 task 并尝试再次获取 latch 和执行,一旦能够获取成功便去 execute,否则继续阻塞等待其他线程再次唤醒即可。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// Tries to acquire the latches specified by the `lock` for command with ID</span><br><span class="hljs-comment">/// `who`.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// This method will enqueue the command ID into the waiting queues of the</span><br><span class="hljs-comment">/// latches. A latch is considered acquired if the command ID is the first</span><br><span class="hljs-comment">/// one of elements in the queue which have the same hash value. Returns</span><br><span class="hljs-comment">/// true if all the Latches are acquired, false otherwise.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">acquire</span>(&<span class="hljs-keyword">self</span>, lock: &<span class="hljs-keyword">mut</span> Lock, who: <span class="hljs-type">u64</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span> {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">acquired_count</span>: <span class="hljs-type">usize</span> = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> &key_hash <span class="hljs-keyword">in</span> &lock.required_hashes[lock.owned_count..] {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">latch</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">lock_latch</span>(key_hash);<br> <span class="hljs-keyword">match</span> latch.<span class="hljs-title function_ invoke__">get_first_req_by_hash</span>(key_hash) {<br> <span class="hljs-title function_ invoke__">Some</span>(cid) => {<br> <span class="hljs-keyword">if</span> cid == who {<br> acquired_count += <span class="hljs-number">1</span>;<br> } <span class="hljs-keyword">else</span> {<br> latch.<span class="hljs-title function_ invoke__">wait_for_wake</span>(key_hash, who);<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br> <span class="hljs-literal">None</span> => {<br> latch.<span class="hljs-title function_ invoke__">wait_for_wake</span>(key_hash, who);<br> acquired_count += <span class="hljs-number">1</span>;<br> }<br> }<br> }<br> lock.owned_count += acquired_count;<br> lock.<span class="hljs-title function_ invoke__">acquired</span>()<br>}<br><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">wait_for_wake</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, key_hash: <span class="hljs-type">u64</span>, cid: <span class="hljs-type">u64</span>) {<br> <span class="hljs-keyword">self</span>.waiting.<span class="hljs-title function_ invoke__">push_back</span>(<span class="hljs-title function_ invoke__">Some</span>((key_hash, cid)));<br>}<br><br><span class="hljs-comment">/// Releases all the latches held by a command.</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">release_lock</span>(&<span class="hljs-keyword">self</span>, lock: &Lock, cid: <span class="hljs-type">u64</span>) {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">wakeup_list</span> = <span class="hljs-keyword">self</span>.inner.latches.<span class="hljs-title function_ invoke__">release</span>(lock, cid);<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">wcid</span> <span class="hljs-keyword">in</span> wakeup_list {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">try_to_wake_up</span>(wcid);<br> }<br>}<br><br><span class="hljs-comment">/// Releases all latches owned by the `lock` of command with ID `who`,</span><br><span class="hljs-comment">/// returns the wakeup list.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// Preconditions: the caller must ensure the command is at the front of the</span><br><span class="hljs-comment">/// latches.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">release</span>(&<span class="hljs-keyword">self</span>, lock: &Lock, who: <span class="hljs-type">u64</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Vec</span><<span class="hljs-type">u64</span>> {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">wakeup_list</span>: <span class="hljs-type">Vec</span><<span class="hljs-type">u64</span>> = <span class="hljs-built_in">vec!</span>[];<br> <span class="hljs-keyword">for</span> &key_hash <span class="hljs-keyword">in</span> &lock.required_hashes[..lock.owned_count] {<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">latch</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">lock_latch</span>(key_hash);<br> <span class="hljs-keyword">let</span> (v, front) = latch.<span class="hljs-title function_ invoke__">pop_front</span>(key_hash).<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-built_in">assert_eq!</span>(front, who);<br> <span class="hljs-built_in">assert_eq!</span>(v, key_hash);<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(wakeup) = latch.<span class="hljs-title function_ invoke__">get_first_req_by_hash</span>(key_hash) {<br> wakeup_list.<span class="hljs-title function_ invoke__">push</span>(wakeup);<br> }<br> }<br> wakeup_list<br>}<br><br><span class="hljs-comment">/// Tries to acquire all the necessary latches. If all the necessary latches</span><br><span class="hljs-comment">/// are acquired, the method initiates a get snapshot operation for further</span><br><span class="hljs-comment">/// processing.</span><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">try_to_wake_up</span>(&<span class="hljs-keyword">self</span>, cid: <span class="hljs-type">u64</span>) {<br> <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.inner.<span class="hljs-title function_ invoke__">acquire_lock_on_wakeup</span>(cid) {<br> <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-title function_ invoke__">Some</span>(task)) => {<br> fail_point!(<span class="hljs-string">"txn_scheduler_try_to_wake_up"</span>);<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">execute</span>(task);<br> }<br> <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-literal">None</span>) => {}<br> <span class="hljs-title function_ invoke__">Err</span>(err) => {<br> <span class="hljs-comment">// Spawn the finish task to the pool to avoid stack overflow</span><br> <span class="hljs-comment">// when many queuing tasks fail successively.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">this</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">clone</span>();<br> <span class="hljs-keyword">self</span>.inner<br> .worker_pool<br> .pool<br> .<span class="hljs-title function_ invoke__">spawn</span>(<span class="hljs-keyword">async</span> <span class="hljs-keyword">move</span> {<br> this.<span class="hljs-title function_ invoke__">finish_with_err</span>(cid, err);<br> })<br> .<span class="hljs-title function_ invoke__">unwrap</span>();<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>实际上一旦构造出 TaskContext 并插入到 task_slots 中,只要持有 id 便可以去 task_slots 中获取到该 task 和其对应的 callback,那么任何一个线程都可以去执行该任务并返回客户端对应的执行结果。</p><p>总体来看,这样的异步执行方案相当于在 command 级别抽象出了一套类协程调度逻辑,再辅以 Rust 原生的无栈协程,减少了很多 grpc 线程之间的同步阻塞和切换。</p><p>通过本小节,希望您能够了解 Storage 模块的组织结构,并对 scheduler 的异步并发请求调度方案有一定的认知,能够在正确的位置去追踪单个请求的异步调用路径。</p><h3 id="RaftStore"><a href="#RaftStore" class="headerlink" title="RaftStore"></a>RaftStore</h3><p>RaftStore 常被认为是 TiKV 最复杂,最晦涩的模块,劝退了相当一部分开发者。</p><p>在笔者看来这主要跟要保证 multi-raft + split/merge 在各种 case 下的一致性/正确性有关,本身的语义就十分复杂,那实现也就很难简单了。尽管有太多需要注意的细节,但如果仅要了解 RaftStore 的大体框架依然是可行的。</p><p><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-17">TiKV 源码解析系列文章(十七)raftstore 概览</a> 介绍了 3.x 版本的 RaftStore,目前 RaftStore 已经有了些许的变化,本小节将简单补充笔者的理解。</p><p>Batch System 是 RaftStore 处理的基石,是一套用来并发驱动状态机的机制。</p><p>状态机的核心定义如下:<br><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// A `Fsm` is a finite state machine. It should be able to be notified for</span><br><span class="hljs-comment">/// updating internal state according to incoming messages.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">trait</span> <span class="hljs-title class_">Fsm</span> {<br> <span class="hljs-keyword">type</span> <span class="hljs-title class_">Message</span>: <span class="hljs-built_in">Send</span>;<br><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">is_stopped</span>(&<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">bool</span>;<br><br> <span class="hljs-comment">/// Set a mailbox to FSM, which should be used to send message to itself.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">set_mailbox</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, _mailbox: Cow<<span class="hljs-symbol">'_</span>, BasicMailbox<<span class="hljs-keyword">Self</span>>>)<br> <span class="hljs-keyword">where</span><br> <span class="hljs-keyword">Self</span>: <span class="hljs-built_in">Sized</span>,<br> {<br> }<br> <span class="hljs-comment">/// Take the mailbox from FSM. Implementation should ensure there will be</span><br> <span class="hljs-comment">/// no reference to mailbox after calling this method.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">take_mailbox</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Option</span><BasicMailbox<<span class="hljs-keyword">Self</span>>><br> <span class="hljs-keyword">where</span><br> <span class="hljs-keyword">Self</span>: <span class="hljs-built_in">Sized</span>,<br> {<br> <span class="hljs-literal">None</span><br> }<br><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get_priority</span>(&<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> Priority {<br> Priority::Normal<br> }<br>}<br><br><span class="hljs-comment">/// A unify type for FSMs so that they can be sent to channel easily.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">FsmTypes</span><N, C> {<br> <span class="hljs-title function_ invoke__">Normal</span>(<span class="hljs-type">Box</span><N>),<br> <span class="hljs-title function_ invoke__">Control</span>(<span class="hljs-type">Box</span><C>),<br> <span class="hljs-comment">// Used as a signal that scheduler should be shutdown.</span><br> Empty,<br>}<br></code></pre></td></tr></table></figure></p><p>状态机通过 PollHandler 来驱动,定义如下:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// A handler that polls all FSMs in ready.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// A general process works like the following:</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// loop {</span><br><span class="hljs-comment">/// begin</span><br><span class="hljs-comment">/// if control is ready:</span><br><span class="hljs-comment">/// handle_control</span><br><span class="hljs-comment">/// foreach ready normal:</span><br><span class="hljs-comment">/// handle_normal</span><br><span class="hljs-comment">/// light_end</span><br><span class="hljs-comment">/// end</span><br><span class="hljs-comment">/// }</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// A [`PollHandler`] doesn't have to be [`Sync`] because each poll thread has</span><br><span class="hljs-comment">/// its own handler.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">trait</span> <span class="hljs-title class_">PollHandler</span><N, C>: <span class="hljs-built_in">Send</span> + <span class="hljs-symbol">'static</span> {<br> <span class="hljs-comment">/// This function is called at the very beginning of every round.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">begin</span><F>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, _batch_size: <span class="hljs-type">usize</span>, update_cfg: F)<br> <span class="hljs-keyword">where</span><br> <span class="hljs-keyword">for</span><<span class="hljs-symbol">'a</span>> F: <span class="hljs-title function_ invoke__">FnOnce</span>(&<span class="hljs-symbol">'a</span> Config);<br><br> <span class="hljs-comment">/// This function is called when the control FSM is ready.</span><br> <span class="hljs-comment">///</span><br> <span class="hljs-comment">/// If `Some(len)` is returned, this function will not be called again until</span><br> <span class="hljs-comment">/// there are more than `len` pending messages in `control` FSM.</span><br> <span class="hljs-comment">///</span><br> <span class="hljs-comment">/// If `None` is returned, this function will be called again with the same</span><br> <span class="hljs-comment">/// FSM `control` in the next round, unless it is stopped.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_control</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, control: &<span class="hljs-keyword">mut</span> C) <span class="hljs-punctuation">-></span> <span class="hljs-type">Option</span><<span class="hljs-type">usize</span>>;<br><br> <span class="hljs-comment">/// This function is called when some normal FSMs are ready.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">handle_normal</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, normal: &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = N>) <span class="hljs-punctuation">-></span> HandleResult;<br><br> <span class="hljs-comment">/// This function is called after [`handle_normal`] is called for all FSMs</span><br> <span class="hljs-comment">/// and before calling [`end`]. The function is expected to run lightweight</span><br> <span class="hljs-comment">/// works.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">light_end</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, _batch: &<span class="hljs-keyword">mut</span> [<span class="hljs-type">Option</span><<span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = N>>]) {}<br><br> <span class="hljs-comment">/// This function is called at the end of every round.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">end</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, batch: &<span class="hljs-keyword">mut</span> [<span class="hljs-type">Option</span><<span class="hljs-keyword">impl</span> <span class="hljs-title class_">DerefMut</span><Target = N>>]);<br><br> <span class="hljs-comment">/// This function is called when batch system is going to sleep.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">pause</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) {}<br><br> <span class="hljs-comment">/// This function returns the priority of this handler.</span><br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get_priority</span>(&<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> Priority {<br> Priority::Normal<br> }<br>}<br></code></pre></td></tr></table></figure><p>大体来看,状态机分成两种,normal 和 control。对于每一个 Batch System,只有一个 control 状态机,负责管理和处理一些需要全局视野的任务。其他 normal 状态机负责处理其自身相关的任务。每个状态机都有其绑定的消息和消息队列。PollHandler 负责驱动状态机,处理自身队列中的消息。Batch System 的职责就是检测哪些状态机需要驱动,然后调用 PollHandler 去消费消息。消费消息会产生副作用,而这些副作用或要落盘,或要网络交互。PollHandler 在一个批次中可以处理多个 normal 状态机。</p><p>在 RaftStore 里,一共有两个 Batch System。分别是 RaftBatchSystem 和 ApplyBatchSystem。RaftBatchSystem 用于驱动 Raft 状态机,包括日志的分发、落盘、状态跃迁等。已经提交的日志会被发往 ApplyBatchSystem 进行处理。ApplyBatchSystem 将日志解析并应用到底层 KV 数据库中,执行回调函数。所有的写操作都遵循着这个流程。</p><p><img src="https://img1.www.pingcap.com/prod/2_3696723f56.png" alt></p><p>具体一点来说:</p><ul><li>每个 PollHandler 对应一个线程,其在 poll 函数中会持续地检测需要驱动的状态机并进行处理,此外还可能将某些 hot region 路由给其他 PollHandler 来做一些负载均衡操作。</li><li>每个 region 对应一个 raft 组,而每个 raft 组在一个 BatchSystem 里就对应一个 normal 状态机,<ul><li>对于 RaftBatchSystem,参照 <a href="https://cn.pingcap.com/blog/tikv-source-code-reading-2">TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析</a> 中提到的 raft-rs 接口,每个 normal 状态机在一轮 loop 中被 PollHandler 获取一次 ready,其中一般包含需要持久化的未提交日志,需要发送的消息和需要应用的已提交日志等。对于需要持久化的未提交日志,最直接的做法便是将其暂时缓存到内存中进行攒批,然后在当前 loop 结尾的 end 函数中统一同步处理,这无疑会影响每轮 loop 的效率, TiKV 的 6.x 版本已经将 loop 结尾的同步 IO 抽到了 loop 外交给了额外的线程池去做,这进一步提升了 store loop 的效率,具体可参考该 <a href="https://github.com/tikv/tikv/issues/10540">issue</a>。对于需要发送的消息,则通过 Transport 异步发送给对应的 store。对于需要应用的已提交日志,则通过 applyRouter 带着回调函数发给 ApplyBatchSystem。</li><li>对于 ApplyBatchSystem,每个 normal 状态机在一轮 loop 中被 PollHandler 获取 RaftBatchSystem 发来的若干已经提交需要应用的日志,其需要将其攒批提交并在之后执行对应的回调函数返回客户端结果。需要注意的是,返回客户端结果之后 ApplyBatchSystem 还需要向 RaftBatchSystem 再 propose ApplyRes 的消息,从而更新 RaftBatchSystem 的某些内存状态,比如 applyIndex,该字段的更新能够推动某些阻塞在某个 ReadIndex 上的读请求继续执行。</li></ul></li></ul><p>如下便是 BatchSystem 的启动流程及 poll 函数:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// A system that can poll FSMs concurrently and in batch.</span><br><span class="hljs-comment">///</span><br><span class="hljs-comment">/// To use the system, two type of FSMs and their PollHandlers need to be</span><br><span class="hljs-comment">/// defined: Normal and Control. Normal FSM handles the general task while</span><br><span class="hljs-comment">/// Control FSM creates normal FSM instances.</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">BatchSystem</span><N: Fsm, C: Fsm> {<br> name_prefix: <span class="hljs-type">Option</span><<span class="hljs-type">String</span>>,<br> router: BatchRouter<N, C>,<br> receiver: channel::Receiver<FsmTypes<N, C>>,<br> low_receiver: channel::Receiver<FsmTypes<N, C>>,<br> pool_size: <span class="hljs-type">usize</span>,<br> max_batch_size: <span class="hljs-type">usize</span>,<br> workers: Arc<Mutex<<span class="hljs-type">Vec</span><JoinHandle<()>>>>,<br> joinable_workers: Arc<Mutex<<span class="hljs-type">Vec</span><ThreadId>>>,<br> reschedule_duration: Duration,<br> low_priority_pool_size: <span class="hljs-type">usize</span>,<br> pool_state_builder: <span class="hljs-type">Option</span><PoolStateBuilder<N, C>>,<br>}<br><br><span class="hljs-keyword">impl</span><N, C> BatchSystem<N, C><br><span class="hljs-keyword">where</span><br> N: Fsm + <span class="hljs-built_in">Send</span> + <span class="hljs-symbol">'static</span>,<br> C: Fsm + <span class="hljs-built_in">Send</span> + <span class="hljs-symbol">'static</span>,<br>{<br> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">start_poller</span><B>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, name: <span class="hljs-type">String</span>, priority: Priority, builder: &<span class="hljs-keyword">mut</span> B)<br> <span class="hljs-keyword">where</span><br> B: HandlerBuilder<N, C>,<br> B::Handler: <span class="hljs-built_in">Send</span> + <span class="hljs-symbol">'static</span>,<br> {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">handler</span> = builder.<span class="hljs-title function_ invoke__">build</span>(priority);<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">receiver</span> = <span class="hljs-keyword">match</span> priority {<br> Priority::Normal => <span class="hljs-keyword">self</span>.receiver.<span class="hljs-title function_ invoke__">clone</span>(),<br> Priority::Low => <span class="hljs-keyword">self</span>.low_receiver.<span class="hljs-title function_ invoke__">clone</span>(),<br> };<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">poller</span> = Poller {<br> router: <span class="hljs-keyword">self</span>.router.<span class="hljs-title function_ invoke__">clone</span>(),<br> fsm_receiver: receiver,<br> handler,<br> max_batch_size: <span class="hljs-keyword">self</span>.max_batch_size,<br> reschedule_duration: <span class="hljs-keyword">self</span>.reschedule_duration,<br> joinable_workers: <span class="hljs-keyword">if</span> priority == Priority::Normal {<br> <span class="hljs-title function_ invoke__">Some</span>(Arc::<span class="hljs-title function_ invoke__">clone</span>(&<span class="hljs-keyword">self</span>.joinable_workers))<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-literal">None</span><br> },<br> };<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">props</span> = tikv_util::thread_group::<span class="hljs-title function_ invoke__">current_properties</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">t</span> = thread::Builder::<span class="hljs-title function_ invoke__">new</span>()<br> .<span class="hljs-title function_ invoke__">name</span>(name)<br> .<span class="hljs-title function_ invoke__">spawn_wrapper</span>(<span class="hljs-keyword">move</span> || {<br> tikv_util::thread_group::<span class="hljs-title function_ invoke__">set_properties</span>(props);<br> <span class="hljs-title function_ invoke__">set_io_type</span>(IoType::ForegroundWrite);<br> poller.<span class="hljs-title function_ invoke__">poll</span>();<br> })<br> .<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">self</span>.workers.<span class="hljs-title function_ invoke__">lock</span>().<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">push</span>(t);<br> }<br><br> <span class="hljs-comment">/// Start the batch system.</span><br> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">spawn</span><B>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, name_prefix: <span class="hljs-type">String</span>, <span class="hljs-keyword">mut</span> builder: B)<br> <span class="hljs-keyword">where</span><br> B: HandlerBuilder<N, C>,<br> B::Handler: <span class="hljs-built_in">Send</span> + <span class="hljs-symbol">'static</span>,<br> {<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">i</span> <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..<span class="hljs-keyword">self</span>.pool_size {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">start_poller</span>(<br> thd_name!(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"{}-{}"</span>, name_prefix, i)),<br> Priority::Normal,<br> &<span class="hljs-keyword">mut</span> builder,<br> );<br> }<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">i</span> <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..<span class="hljs-keyword">self</span>.low_priority_pool_size {<br> <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">start_poller</span>(<br> thd_name!(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"{}-low-{}"</span>, name_prefix, i)),<br> Priority::Low,<br> &<span class="hljs-keyword">mut</span> builder,<br> );<br> }<br> <span class="hljs-keyword">self</span>.name_prefix = <span class="hljs-title function_ invoke__">Some</span>(name_prefix);<br> }<br>}<br><br> <span class="hljs-comment">/// Polls for readiness and forwards them to handler. Removes stale peers if</span><br> <span class="hljs-comment">/// necessary.</span><br> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">poll</span>(&<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) {<br> fail_point!(<span class="hljs-string">"poll"</span>);<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">batch</span> = Batch::<span class="hljs-title function_ invoke__">with_capacity</span>(<span class="hljs-keyword">self</span>.max_batch_size);<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">reschedule_fsms</span> = <span class="hljs-type">Vec</span>::<span class="hljs-title function_ invoke__">with_capacity</span>(<span class="hljs-keyword">self</span>.max_batch_size);<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">to_skip_end</span> = <span class="hljs-type">Vec</span>::<span class="hljs-title function_ invoke__">with_capacity</span>(<span class="hljs-keyword">self</span>.max_batch_size);<br><br> <span class="hljs-comment">// Fetch batch after every round is finished. It's helpful to protect regions</span><br> <span class="hljs-comment">// from becoming hungry if some regions are hot points. Since we fetch new FSM</span><br> <span class="hljs-comment">// every time calling `poll`, we do not need to configure a large value for</span><br> <span class="hljs-comment">// `self.max_batch_size`.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">run</span> = <span class="hljs-literal">true</span>;<br> <span class="hljs-keyword">while</span> run && <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">fetch_fsm</span>(&<span class="hljs-keyword">mut</span> batch) {<br> <span class="hljs-comment">// If there is some region wait to be deal, we must deal with it even if it has</span><br> <span class="hljs-comment">// overhead max size of batch. It's helpful to protect regions from becoming</span><br> <span class="hljs-comment">// hungry if some regions are hot points.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">max_batch_size</span> = std::cmp::<span class="hljs-title function_ invoke__">max</span>(<span class="hljs-keyword">self</span>.max_batch_size, batch.normals.<span class="hljs-title function_ invoke__">len</span>());<br> <span class="hljs-comment">// Update some online config if needed.</span><br> {<br> <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> rust 2018 does not support capture disjoint field within a closure.</span><br> <span class="hljs-comment">// See https://github.com/rust-lang/rust/issues/53488 for more details.</span><br> <span class="hljs-comment">// We can remove this once we upgrade to rust 2021 or later edition.</span><br> <span class="hljs-keyword">let</span> <span class="hljs-variable">batch_size</span> = &<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.max_batch_size;<br> <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">begin</span>(max_batch_size, |cfg| {<br> *batch_size = cfg.<span class="hljs-title function_ invoke__">max_batch_size</span>();<br> });<br> }<br> max_batch_size = std::cmp::<span class="hljs-title function_ invoke__">max</span>(<span class="hljs-keyword">self</span>.max_batch_size, batch.normals.<span class="hljs-title function_ invoke__">len</span>());<br><br> <span class="hljs-keyword">if</span> batch.control.<span class="hljs-title function_ invoke__">is_some</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">len</span> = <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">handle_control</span>(batch.control.<span class="hljs-title function_ invoke__">as_mut</span>().<span class="hljs-title function_ invoke__">unwrap</span>());<br> <span class="hljs-keyword">if</span> batch.control.<span class="hljs-title function_ invoke__">as_ref</span>().<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">is_stopped</span>() {<br> batch.<span class="hljs-title function_ invoke__">remove_control</span>(&<span class="hljs-keyword">self</span>.router.control_box);<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(len) = len {<br> batch.<span class="hljs-title function_ invoke__">release_control</span>(&<span class="hljs-keyword">self</span>.router.control_box, len);<br> }<br> }<br><br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">hot_fsm_count</span> = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> (i, p) <span class="hljs-keyword">in</span> batch.normals.<span class="hljs-title function_ invoke__">iter_mut</span>().<span class="hljs-title function_ invoke__">enumerate</span>() {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = p.<span class="hljs-title function_ invoke__">as_mut</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">handle_normal</span>(p);<br> <span class="hljs-keyword">if</span> p.<span class="hljs-title function_ invoke__">is_stopped</span>() {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::Remove);<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(i);<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> p.<span class="hljs-title function_ invoke__">get_priority</span>() != <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">get_priority</span>() {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::Schedule);<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(i);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">if</span> p.timer.<span class="hljs-title function_ invoke__">saturating_elapsed</span>() >= <span class="hljs-keyword">self</span>.reschedule_duration {<br> hot_fsm_count += <span class="hljs-number">1</span>;<br> <span class="hljs-comment">// We should only reschedule a half of the hot regions, otherwise,</span><br> <span class="hljs-comment">// it's possible all the hot regions are fetched in a batch the</span><br> <span class="hljs-comment">// next time.</span><br> <span class="hljs-keyword">if</span> hot_fsm_count % <span class="hljs-number">2</span> == <span class="hljs-number">0</span> {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::Schedule);<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(i);<br> <span class="hljs-keyword">continue</span>;<br> }<br> }<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">HandleResult</span>::StopAt { progress, skip_end } = res {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::<span class="hljs-title function_ invoke__">Release</span>(progress));<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(i);<br> <span class="hljs-keyword">if</span> skip_end {<br> to_skip_end.<span class="hljs-title function_ invoke__">push</span>(i);<br> }<br> }<br> }<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">fsm_cnt</span> = batch.normals.<span class="hljs-title function_ invoke__">len</span>();<br> <span class="hljs-keyword">while</span> batch.normals.<span class="hljs-title function_ invoke__">len</span>() < max_batch_size {<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Ok</span>(fsm) = <span class="hljs-keyword">self</span>.fsm_receiver.<span class="hljs-title function_ invoke__">try_recv</span>() {<br> run = batch.<span class="hljs-title function_ invoke__">push</span>(fsm);<br> }<br> <span class="hljs-comment">// When `fsm_cnt >= batch.normals.len()`:</span><br> <span class="hljs-comment">// - No more FSMs in `fsm_receiver`.</span><br> <span class="hljs-comment">// - We receive a control FSM. Break the loop because ControlFsm may change</span><br> <span class="hljs-comment">// state of the handler, we shall deal with it immediately after calling</span><br> <span class="hljs-comment">// `begin` of `Handler`.</span><br> <span class="hljs-keyword">if</span> !run || fsm_cnt >= batch.normals.<span class="hljs-title function_ invoke__">len</span>() {<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = batch.normals[fsm_cnt].<span class="hljs-title function_ invoke__">as_mut</span>().<span class="hljs-title function_ invoke__">unwrap</span>();<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">res</span> = <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">handle_normal</span>(p);<br> <span class="hljs-keyword">if</span> p.<span class="hljs-title function_ invoke__">is_stopped</span>() {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::Remove);<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(fsm_cnt);<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">HandleResult</span>::StopAt { progress, skip_end } = res {<br> p.policy = <span class="hljs-title function_ invoke__">Some</span>(ReschedulePolicy::<span class="hljs-title function_ invoke__">Release</span>(progress));<br> reschedule_fsms.<span class="hljs-title function_ invoke__">push</span>(fsm_cnt);<br> <span class="hljs-keyword">if</span> skip_end {<br> to_skip_end.<span class="hljs-title function_ invoke__">push</span>(fsm_cnt);<br> }<br> }<br> fsm_cnt += <span class="hljs-number">1</span>;<br> }<br> <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">light_end</span>(&<span class="hljs-keyword">mut</span> batch.normals);<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">index</span> <span class="hljs-keyword">in</span> &to_skip_end {<br> batch.<span class="hljs-title function_ invoke__">schedule</span>(&<span class="hljs-keyword">self</span>.router, *index);<br> }<br> to_skip_end.<span class="hljs-title function_ invoke__">clear</span>();<br> <span class="hljs-keyword">self</span>.handler.<span class="hljs-title function_ invoke__">end</span>(&<span class="hljs-keyword">mut</span> batch.normals);<br><br> <span class="hljs-comment">// Iterate larger index first, so that `swap_reclaim` won't affect other FSMs</span><br> <span class="hljs-comment">// in the list.</span><br> <span class="hljs-keyword">for</span> <span class="hljs-variable">index</span> <span class="hljs-keyword">in</span> reschedule_fsms.<span class="hljs-title function_ invoke__">iter</span>().<span class="hljs-title function_ invoke__">rev</span>() {<br> batch.<span class="hljs-title function_ invoke__">schedule</span>(&<span class="hljs-keyword">self</span>.router, *index);<br> batch.<span class="hljs-title function_ invoke__">swap_reclaim</span>(*index);<br> }<br> reschedule_fsms.<span class="hljs-title function_ invoke__">clear</span>();<br> }<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">Some</span>(fsm) = batch.control.<span class="hljs-title function_ invoke__">take</span>() {<br> <span class="hljs-keyword">self</span>.router.control_scheduler.<span class="hljs-title function_ invoke__">schedule</span>(fsm);<br> info!(<span class="hljs-string">"poller will exit, release the left ControlFsm"</span>);<br> }<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">left_fsm_cnt</span> = batch.normals.<span class="hljs-title function_ invoke__">len</span>();<br> <span class="hljs-keyword">if</span> left_fsm_cnt > <span class="hljs-number">0</span> {<br> info!(<br> <span class="hljs-string">"poller will exit, schedule {} left NormalFsms"</span>,<br> left_fsm_cnt<br> );<br> <span class="hljs-keyword">for</span> <span class="hljs-variable">i</span> <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..left_fsm_cnt {<br> <span class="hljs-keyword">let</span> <span class="hljs-variable">to_schedule</span> = <span class="hljs-keyword">match</span> batch.normals[i].<span class="hljs-title function_ invoke__">take</span>() {<br> <span class="hljs-title function_ invoke__">Some</span>(f) => f,<br> <span class="hljs-literal">None</span> => <span class="hljs-keyword">continue</span>,<br> };<br> <span class="hljs-keyword">self</span>.router.normal_scheduler.<span class="hljs-title function_ invoke__">schedule</span>(to_schedule.fsm);<br> }<br> }<br> batch.<span class="hljs-title function_ invoke__">clear</span>();<br> }<br>}<br></code></pre></td></tr></table></figure><p>通过本小节,希望您能够了解 BatchSystem 的大体框架,并知悉 PollHandler 和 FSM 的物理含义,以便结合之后的博客去熟悉全链路读写流程。</p><h2 id="调试方案"><a href="#调试方案" class="headerlink" title="调试方案"></a>调试方案</h2><p>断点调试是一种学习源码的有效手段。当不是很熟悉 gdb 等工具时,使用一些更现代的 IDE 会大幅提升调试代码的体验。</p><p>笔者平时采用了 CLion + intellij-rust 的工具链来调试 Rust 代码。需要注意的是,在使用 CLion 调试 TiKV 源码时,需要参照 <a href="https://doc.rust-lang.org/cargo/reference/profiles.html">Cargo book</a> 修改 TiKV cargo.toml 中 <code>[profile.test]</code> 和 <code>[profile.dev]</code> 的 <a href="https://github.com/tikv/tikv/blob/master/Cargo.toml#L327">debug 选项</a> 来开启调试信息,否则在 Clion 里断点调试时会无法看到对应的堆栈信息。</p><p>实际上如果要做到以上读写路径的全链路追踪,最简单的方法便是从集成测试里面寻找一些 case,接着从 Service 模块开始打断点,之后执行调试即可。在这里推荐 <code>integrations/server/kv_service.rs</code> 中的测试,里面的 test 都会构造 <code>TiKVClient</code> 发送真实的 RPC 请求,且服务端也基本不包含 Mock 组件,可以完整的去追踪一条 RPC 的全链路流程。</p><p>此外由于 TiKV 的代码中有比较多的 spawn 和回调函数,刚开始可能并不能很直接的串起来流程,但相信通过上文的介绍,您已经大致了解其异步框架的实现,从而可以在正确的闭包位置打下断点,进而熟悉地追踪单条请求的全链路实现。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇博客介绍了 TiKV 的基本概念,TiKV 读写路径上的三个重要模块(KVService,Storage,RaftStore)和断点调试 TiKV 学习源码的方案</p><p>希望本博客能够帮助对 TiKV 开发感兴趣的新同学尽快了解 TiKV 的 codebase。</p><p>感谢您的阅读~</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p><a href="https://github.com/tikv/tikv">TiKV</a> 是一个支持事务的分布式 Key-Value </summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="源码阅读" scheme="https://tanxinyu.work/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="TiKV" scheme="https://tanxinyu.work/tags/TiKV/"/>
</entry>
<entry>
<title>Talent-Plan:用 Rust 实现简易 KV 引擎</title>
<link href="https://tanxinyu.work/naive-kvengine-in-rust/"/>
<id>https://tanxinyu.work/naive-kvengine-in-rust/</id>
<published>2022-08-28T14:48:16.000Z</published>
<updated>2022-08-28T15:01:40.099Z</updated>
<content type="html"><![CDATA[<h2 id="版本"><a href="#版本" class="headerlink" title="版本"></a>版本</h2><ul><li><a href="https://github.com/pingcap/talent-plan/tree/master/courses/rust">官网版本</a></li></ul><h2 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h2><p>Rust 学习</p><ul><li><a href="https://fasterthanli.me/articles/a-half-hour-to-learn-rust">半小时学习 Rust</a></li><li><a href="https://github.com/rust-lang/rustlings">Rustling</a> 及 <a href="https://github.com/OneSizeFitsQuorum/rustlings/pull/1">解答</a></li><li><a href="https://course.rs/about-book.html">Rust 语言圣经</a> 及 <a href="https://zh.practice.rs/why-exercise.html">习题</a></li><li><a href="https://kaisery.github.io/trpl-zh-cn/title-page.html">Rust 官方文档</a></li><li><a href="https://zhuanlan.zhihu.com/p/558415847">Talent Plan Percolator Lab</a></li></ul><h2 id="过关过程"><a href="#过关过程" class="headerlink" title="过关过程"></a>过关过程</h2><h3 id="Rust-Project-1-The-Rust-toolbox"><a href="#Rust-Project-1-The-Rust-toolbox" class="headerlink" title="Rust Project 1: The Rust toolbox"></a>Rust Project 1: The Rust toolbox</h3><p>本 project 过关代码可参考该 <a href="https://github.com/OneSizeFitsQuorum/PracticalNetworkedApplications/commit/1a0ca3eb1af93c33a3bb9881dec782dbd623aa49">commit</a>。</p><p>主要参照了 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/blob/master/courses/rust/projects/project-1/README.md">README</a> 来完成本 project,具体过程比较 trivial 不再细述。主要工作如下:</p><ul><li>搭建项目基本目录结构。</li><li>使用 <a href="https://docs.rs/clap/3.2.15/clap/">clap</a> 来解析命令行参数,根据官方文档学习 crate 的具体使用方法。</li><li>使用 cargo.toml 中的若干参数,包括 dev-dependencies,条件编译等等。</li><li>完成基于内存 hashmap 的 KvStore 的增删改查接口。</li><li>增加包文档和函数文档并在文档中添加了文档测试</li><li>使用 cargo fmt 和 cargo clippy 来提升代码质量</li></ul><h3 id="Rust-Project-2-Log-structured-file-I-O"><a href="#Rust-Project-2-Log-structured-file-I-O" class="headerlink" title="Rust Project 2: Log-structured file I/O"></a>Rust Project 2: Log-structured file I/O</h3><p>本 project 过关代码可参考该 <a href="https://github.com/OneSizeFitsQuorum/PracticalNetworkedApplications/commit/fb097b172c46784ed7ebe24afdec0a8ab5d3d399">commit</a>。</p><h4 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h4><p>在阅读 failure crate 的 <a href="https://boats.gitlab.io/failure/">文档</a> 之后,在本 project 中采用了第二种错误处理方式——自定义错误结构。通过定义 KVStoreError 结构体并使用 failure crate 提供的能力,可以很轻易地捕捉不同的错误并列举他们的表示,调用者也可以直接通过模式匹配的方式得到错误类型。</p><p>此外,通过为 io:Error 和 serde_json:Error 添加转换到 KVStoreError 的函数,在主逻辑中可以轻松的使用 ? 来向上传递错误,从而避免对 Result 类型的暴力 unwrap。</p><p>此外,还定义了 Result<T> 类型别名来统一本项目中所有的 Result 返回类型。</T></p><p><img src="/naive-kvengine-in-rust/boxcnvQkj4GJmiTwJeV21SmMeFf.png" alt></p><h4 id="包结构"><a href="#包结构" class="headerlink" title="包结构"></a>包结构</h4><p>对于包含一个 lib 包和一个 bin 包的 crate ,在 lib 包中,需要引用所有新增文件的文件名当做其模块名将其引入,此外还需要使用 pub use 语法来将 bin 包会用到的结构公开导出。</p><p>在 lib 包的任何文件里,都可以通过 crate:: 的方式来引入本 lib 库被公开导出的结构。</p><p>在 bin 包中,需要通过实际 crate 名:: 的方式来引入同名 lib 库被公开导出的结构。</p><p><img src="/naive-kvengine-in-rust/boxcnYwXTYAHx39aKM0Pljd8R7b.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnnTJ4rUCXtxN59nGmDPclRc.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnwOPKgrmNT5kp3zgO5UB2Tc.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn1GHZz1EW2O6yQvws5Xz0Ah.png" alt></p><h4 id="结果捕捉"><a href="#结果捕捉" class="headerlink" title="结果捕捉"></a>结果捕捉</h4><p><img src="/naive-kvengine-in-rust/boxcn4BhJAA4AnwlfT6ksZkY7Ye.png" alt></p><p>结果捕捉中的正常/异常处理需要满足以上题意的要求,因而在 main 函数中原样实现了以上需求如下。</p><p><img src="/naive-kvengine-in-rust/boxcnFEf4FsRUzUJXg3v9CORCQc.png" alt></p><h4 id="结构体"><a href="#结构体" class="headerlink" title="结构体"></a>结构体</h4><p>KvStore 结构体中各个变量含义如下:</p><ul><li>Index :参照 bitcask 的模型,key 为 kv pair 的 key,value 并不存储对应的 value,而是存储该 value 在第 file_number 个文件的 offset 处,长度为 length。</li><li>current_readers:对于所有已经存在的文件,KvStore 都缓存了一个 BufReader 来便于 seek 到对应的 offset 去 read。实际上也可以没有该结构体每次需要 reader 时新建即可,但复用 reader 可以一定程度上减少资源的损耗。</li><li>current_writer:当前正在写入的 file,其每次写入只需要 append 即可,不需要 seek。新建一个 BufWriterWithPosition 结构体的原因是能够快速的获取当前写入的 offset,而不需要在通过 seek(SeekFrom::Current(0))(可能是系统调用) 的方式去获取。</li><li>current_file_number:当前最大的 file_number,每次 compaction 之后会新增 1。每个数据文件都会附带一个 file_number,file_number 越大的文件越新,该 version 能够保证恢复时的正确性。</li><li>dir_path:当前文件目录路径。</li><li>useless_size:当前无用的数据总和。当改值大于某一个阈值时,会触发一次 compaction。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnY6aDLMIS6gKAnE8vWIz1Gh.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnQyOZ7qGBDfLOSbKLQdpKXg.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnRZrLuatQsEySdLCweP9hJf.png" alt></p><h4 id="写流程"><a href="#写流程" class="headerlink" title="写流程"></a>写流程</h4><p>使用 serde_json 将 set 命令序列化,接着再写入到 current_writer 中,然后在 index map 中维护该 key 的索引。注意如果某 key 之前已在 KvStore 中存在,则 insert 函数会返回该 key 的旧 value,此时需要维护 useless_size。最后判断如果 useless_size 超过某一个阈值,则进行一次 compact。</p><p>需要注意许多返回 Result<T, error> 的函数都可以通过 ? 而直接向上传递异常,这得益于 Rust 错误处理的良好抽象。</T,></p><p><img src="/naive-kvengine-in-rust/boxcnvogcxYCj6ePDFug2C3BYUg.png" alt></p><h4 id="读流程"><a href="#读流程" class="headerlink" title="读流程"></a>读流程</h4><p>首先在 index 中获取该 key 的索引,如果不存在则说明该 key 不存在直接返回即可,否则根据索引中的 file_number 在 current_readers 中拿到对应的 reader,seek 到对应的 offset 并读取长度为 length 的数据。如果存在则返回 value,否则说明遇到了异常,返回错误即可。</p><p><img src="/naive-kvengine-in-rust/boxcnjPcxYncr6MzPFaF5vITIFf.png" alt></p><h4 id="删除流程"><a href="#删除流程" class="headerlink" title="删除流程"></a>删除流程</h4><p>首先在 index 中获取该 key 的索引,如果不存在则说明该 key 不存在返回 ErrNotFound 错误即可,否则移除该索引,接着将 rm 命令序列化并写入到 current_writer 中以保证该 key 能够被确定性删除。注意对于能够找到对应 key 的 rm 命令,useless_size 不仅需要增加 rm 命令本身的长度,还需要增加之前 set 命令的长度,因为此时他们俩都已经可以被一起回收。 最后判断如果 useless_size 超过某一个阈值,则进行一次 compact。</p><p><img src="/naive-kvengine-in-rust/boxcnxthxhEe0zV8eIkzz9VYGOf.png" alt></p><h4 id="重启流程"><a href="#重启流程" class="headerlink" title="重启流程"></a>重启流程</h4><p>重启时首先初始化若干重要结构,最重要的是调用 recover 函数,该函数将遍历当前所有的文件,不仅将索引维护到 index 结构中,还会将 reader 维护到 current_readers 结构中,最后返回(当前最大的文件版本,当前所有文件的 useless_size),接着利用 current_file_number 构建当前最大文件的 writer,需要注意由于 bitcask 模型是 append_only 的机制,所以在构建 writer 时需要使用 OpenOptions 来使得 append 属性为 true,这样重启后直接 append 即可。最后根据 use_less 判断是否需要 compact,最后返回即可。</p><p><img src="/naive-kvengine-in-rust/boxcnBf5TfEwrTv7qGI7BqvI1Lh.png" alt></p><p>对于 Recover 函数,其需要读取数据目录中的所有文件,按照 file_number 从小到大的顺序去按序 apply 从而保证重启的正确性。</p><p>对于排序,不能直接对文件名排序,因为这样的排序是按照字母编码而不是按照 file_number 大小。因此需要先将所有的 file_number 解析出来再对数字进行排序,之后再利用这些数字索引文件名即可。需要注意这里利用了许多文件操作的链式调用,需要查很多文档。</p><p>在获取到排序好的 versions 之后,可以按序读取文件并将其维护到 index 和 current_readers 中去,注意在该过程中也要注意维护 useless_size。此外得益于 serde_json 的 from_reader().into_iter() 接口,可以按照迭代器的方式去读取 command,而不用关注何时到了末尾,应该读多少字节才可以解析出一个 command,这极大的简化了读取流程。</p><p><img src="/naive-kvengine-in-rust/boxcnLi3nwF781srG69oyMI22Dh.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnBDm1Ma5cBgjhEakKRxtvMe.png" alt></p><h4 id="合并流程"><a href="#合并流程" class="headerlink" title="合并流程"></a>合并流程</h4><p>当前的合并流程采用了暴力的全部合并策略,同时将合并放在了客户端可感知延迟的执行流程中。</p><p>当 useless_size 大于某个阈值时,会触发一次合并,此时会增加 file_number 并将 index 中所有的数据都写入到当前新建的文件中,同时更新内存中的索引。接着再删除老文件和对应的 reader,最后再新建一个文件承载之后的写入即可。</p><p>需要注意按照这个流程即使在合并的写文件过程中出现了重启也不会出现正确性问题。如果新文件的所有数据尚未 flush 成功,老文件并不会被删除,那么只要重启时会按照 file_number 从小到大的顺序进行重放,数据便不会丢失。</p><p><img src="/naive-kvengine-in-rust/boxcnwANdnQiduaMqWuTfuqYqtf.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnfOG27BeMg3VNRx3DnIOySb.png" alt></p><h3 id="Rust-Project-3-Synchronous-client-server-networking"><a href="#Rust-Project-3-Synchronous-client-server-networking" class="headerlink" title="Rust Project 3: Synchronous client-server networking"></a>Rust Project 3: Synchronous client-server networking</h3><p>本 project 过关代码可参考该 <a href="https://github.com/OneSizeFitsQuorum/PracticalNetworkedApplications/commit/4bf43135e3f662bd7f3a40078d2e648f436ce632">commit</a>。</p><h4 id="命令行解析"><a href="#命令行解析" class="headerlink" title="命令行解析"></a>命令行解析</h4><p>在本 project 中,命令行分为了客户端 kvs-client 和服务端 kvs-server 两处,因此需要分别进行解析。</p><p>对于 kvs-client,基本继承了 project2 的命令行解析工具,仅仅增加了 addr 的解析。此外也按照题意将正常输出打印在了 stdout 中,将错误输出打印在了 stderr 中并以非 0 值结束进程</p><p><img src="/naive-kvengine-in-rust/boxcnlWsXGHwpQAAaeTPofjL0Kd.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn6NSpWGbC9UEkuyXIACdTJf.png" alt></p><p>对于 kvs-server,则是按照题意重新写了参数解析器,并对于 engine 增加了只能 2 选 1 的约束。同时还利用 judge_engine 函数实现了引擎选择的判断:对于第一次启动,按照用户参数来启动对应的引擎,如未指定则使用 kvs;对于之后的启动,必须按照之前的引擎启动,若与用户参数冲突则报错。在参数无问题之后打出对应的关键配置既可。</p><p><img src="/naive-kvengine-in-rust/boxcnI8W6PH9TwZcCP0DC53yUub.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn0ExuHszDnabB7fosTNAXXq.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnzfeeo29RWnUZNdMlRtSNKg.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnyJQWFUM6irMEkN22WYrW7c.png" alt></p><h4 id="日志打印"><a href="#日志打印" class="headerlink" title="日志打印"></a>日志打印</h4><p>在本 project 中对日志采用了集成轻量的 env_logger,参照 <a href="https://docs.rs/env_logger/0.9.0/env_logger/">文档</a> 仅仅需要在进程启动时指定日志的最低级别即可。</p><p><img src="/naive-kvengine-in-rust/boxcncl0pVAsp6LqYau0QRjXAbc.png" alt></p><h4 id="命令传输"><a href="#命令传输" class="headerlink" title="命令传输"></a>命令传输</h4><p>本 project 直接使用了 tcp 级别的网络接口来传输命令,因而会有黏包的问题需要处理。</p><p>一般的解决方案是在流上发送每段数据前先写入长度,再写入真实的数据;这样在流上读数据时便可以先读长度,再读对应长度的数据后解除阻塞返回了。</p><p>这样的思路可以自己手写,也可以使用 serde 现成的 reader/writer 接口去实现。因而在客户端构建了一个 Client 结构体对 socket 进行了简单的包装。对于 request,使用了一个 BufWriter 的装饰器配以每次写完数据后的 flush 来降低系统调用的开销啊,其在内部已经能够做到先写入长度再写入数据。对于 response,则是参照重启恢复时的逻辑使用 Deserializer 接口构建 reader,并指定对应的反序列化类型以达到先读长度再读数据的问题。这样便可以利用 serde 帮助解决黏包问题。</p><p><img src="/naive-kvengine-in-rust/boxcnGwjWKxNs7ZmeiWebu1lIoc.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnEArkStGv1C1Us4ew1SguPf.png" alt></p><p>对于服务端,获取 request 和发送 response 的流程和客户端类似。</p><p><img src="/naive-kvengine-in-rust/boxcnUw2Q2c0Bvoe3GjIFMHSE05.png" alt></p><h4 id="可扩展存储引擎"><a href="#可扩展存储引擎" class="headerlink" title="可扩展存储引擎"></a>可扩展存储引擎</h4><p>为了扩展存储引擎的多种实现,抽象出来了统一的 trait 接口 KvsEngine 以对上暴露 trait 的抽象而隐藏具体的实现细节。这样 kvs-server 在启动时便可以以 trait 的方式去访问 engine,而不需要在意其内部的实现细节。</p><p><img src="/naive-kvengine-in-rust/boxcnPj0lx11Jj3YB6h1REJY7Zf.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn5rZYcmvMfYnXcat5LSeCos.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnG99bQVGuok6gTqhr05Kmkn.png" alt></p><p>对于 KvStore,将其 set/get/remove 这三个方法抽象到了 KvsEngine 的实现中。</p><p><img src="/naive-kvengine-in-rust/boxcnzQkpPos1qxbygU4KvMx7Xc.png" alt></p><p>对于 Sled,同样实现了 KvsEngine 的三个方法。需要注意其默认接口的语义和格式与 KvsEngine 不一致,因而需要增加对应的转换。</p><p>此外在 set 时注释掉对应的 flush 操作是由于增加上之后性能过于慢,无法在之后的 bench 阶段跑出结果。</p><p><img src="/naive-kvengine-in-rust/boxcniO3GQy0JnxunBBQ3rH7ewg.png" alt></p><h4 id="性能测试"><a href="#性能测试" class="headerlink" title="性能测试"></a>性能测试</h4><p>参照 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/blob/master/courses/rust/projects/project-3/README.md">Project3 文档</a> 中的介绍创建了 benches/benches 文件并参照 criterion 的 <a href="https://bheisler.github.io/criterion.rs/book/getting_started.html">用户手册</a> 开始构建性能测试。</p><p><img src="/naive-kvengine-in-rust/boxcnkIOzLCHIFKMU89CnoFKRMc.png" alt></p><p>对于性能测试中的三个问题:</p><ul><li>如何精准测量想要测量的时间,而不包括初始化和清理的时间:参照 <a href="https://bheisler.github.io/criterion.rs/book/user_guide/timing_loops.html">criterion 计时迭代的文档</a> 选择了 iter_batched 接口来精准测量读写的时间,初始化的清理的时间并不会被包括在内。</li><li>尽管使用了 rand,如何使得每次迭代都确定性:这里通过在迭代之前利用 rand 的 <a href="https://rust-random.github.io/book/guide-seq.html">choose_multiple</a> 函数创建好对应的写入数据,使得每次迭代的操作数都具有相同的集合。</li><li>在读 benchmark 中,如何保证选到的读集合是写集合的子集:这里采用了同样的方法,读集合是在写集合的集成上去随机选择,从而保证了读取必然能够读到。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnH9edzFxF6Ey7CpeEELfj3d.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnjxp5q574LIf0hidMFs4E7d.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnq23CIUQa25i4rf4mFEbk5b.png" alt></p><p>最终性能对比如下:尽管已经去掉了 sled 每次写入时的 flush 操作来减少其随机 IO,在单线程客户端的情况下,sled 引擎的写延时大概是自写 bitcask 引擎写延时的 20 倍;sled 引擎的读延时大概是自写 bitcask 引擎读延时的 800 倍。</p><p><img src="/naive-kvengine-in-rust/boxcno48X01i1c9r5i1aeN1XUQh.png" alt></p><p>个人猜测产生如此悬殊对比的原因有可能是:</p><ul><li>Sled 专为多线程无锁设计,在单线程下无法体现其性能优势。</li><li>Sled 本质上是一种树状结构,其相比 hash 结构能够提供高效的范围查询,也能够在海量数据场景与磁盘结合起来提供稳定的读延时,因而在小数据量的单点查询场景相比 hash 结构并不占优势。</li><li>当前自写 bitcask 模型还没有引入并发处理的开销,而 sled 是并发安全的,如此对比并不公平。</li></ul><p>本来想用一些 profile 工具测量一下 sled 的火焰图查找一下原因,由于本人的电脑芯片是 M1Pro,许多 profile 工具类如 perf 安装还不是很方便。在参照 <a href="https://github.com/tikv/pprof-rs/blob/master/examples/criterion.rs"> pprof-rs 的文档</a> 为 criterion 配置之后依然无法打出火焰图,猜测可能跟环境有关系,便没有进一步再研究了,之后有机会在 Linux 下再进行 profile 吧。</p><h3 id="Rust-Project-4-Concurrency-and-parallelism"><a href="#Rust-Project-4-Concurrency-and-parallelism" class="headerlink" title="Rust Project 4: Concurrency and parallelism"></a>Rust Project 4: Concurrency and parallelism</h3><p>本 project 过关代码可参考该 <a href="https://github.com/OneSizeFitsQuorum/PracticalNetworkedApplications/commit/3d31896e832cef31e78380794f768375fc1bfc70">commit</a>。</p><h4 id="线程池"><a href="#线程池" class="headerlink" title="线程池"></a>线程池</h4><p>为了多线程需要抽象出线程池的概念,ThreadPool trait 定义如下:spawn 函数中的闭包 F 不仅需要满足 FnOnce() 的 bound 来满足近执行一次的语义,还要实现 Send + ‘static 的 bound 来实现线程安全的发送接收和足够长的生命周期。</p><p><img src="/naive-kvengine-in-rust/boxcnZcbACyBVBV9JlUYqicx0we.png" alt></p><p>对于最简单的 NaiveThreadPool,仅仅需要在 spawn 的时候创建一个线程让其执行即可。</p><p><img src="/naive-kvengine-in-rust/boxcnkG00kSYp5ZcIaGm4TmiKpe.png" alt></p><p>对于共享队列的 ThreadPool,参照 RustBook 中的 <a href="https://kaisery.github.io/trpl-zh-cn/ch20-02-multithreaded.html">举例</a> 即可实现。大体思路是用 channel 做通信,让多个子线程竞争 job 去执行即可。需要注意以下三点:</p><ul><li>std 库自带的 channel 是 MPSC 类型,因而可以支持并发写但不支持并发读。因而要想实现多个子 thread 对 channel 的监听便需要用 Arc<Mutex<receiver>> 来保证不存在并发读。此外也可以使用 crossbeam 的 <a href="https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel">mpsc channel</a> 来支持并发读,那样便直接 clone 即可。</Mutex<receiver></li><li>为了优雅停机,对于 Job 又包装了一层枚举和 Terminate 类型来支持子 thread 的优雅退出,此外还需要利用 Box 将闭包 F 放在堆上来支持线程安全的传递闭包。</li><li>由于单元测试中传入的闭包可能会 panic 但不想看到线程池中的线程减少,一种方案是检测到线程 panic 退出之后新增新的线程,另一种方式则是捕获可能得 panic。例如在 Java 中可以使用 try catch 捕捉一个 throwable 的错误,在 go 中可以 defer recover 一个 panic。在 rust 中类似的语法是 <a href="https://doc.rust-lang.org/std/panic/fn.catch_unwind.html">catch_unwind</a>,因而在执行真正的 job 闭包时,会使用 panic::catch_unwind(AssertUnwindSafe(job)) 的方式来确保该线程不会由于执行闭包而 panic。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnb0wlmKE1MrtOLIjPnjaiBd.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnkPconSZtYcnSV1O8K4loPk.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn1WUh1RyixFp1UHgsRv44TV.png" alt></p><p>对于 RayonThreadPool,直接参考官网的样例初始化对应的 pool 并直接 spawn 给其即可。</p><p><img src="/naive-kvengine-in-rust/boxcnsmyPz8LfrnLhML6AksCPDc.png" alt></p><h4 id="多线程服务端"><a href="#多线程服务端" class="headerlink" title="多线程服务端"></a>多线程服务端</h4><p>在 KvServer 初始化时使用了一个线程池来管理不同 tcp 连接的读写,这样便可以使得并发的请求能够在多核 CPU 的服务端并行执行而不是并发执行。</p><p><img src="/naive-kvengine-in-rust/boxcnnVupdcBFwenEaIuUvFhWDh.png" alt></p><p>注意在 KvServer 中还维护了一个 is_stop 的原子变量,该变量的作用是能够便于当前线程结束阻塞等待进而退出。之所以阻塞的原因是由于 tcplistener 的 incoming() 函数是阻塞的,因而一旦进入 serve 函数当前线程就阻塞了。在之后的性能测试中可能一个线程内想在启动 server 后开始迭代测试并最后关闭 server 并进行下一轮测试:此时如果是同步的写法就无法执行 serve 之后的函数,如果新建一个线程则无法在迭代测试之后通知该线程结束,因而加入了该原子变量之后不仅可以异步启动 server 从而在当前线程进行性能测试,又能够在当前线程的测试结束后以新建一个空 client 的方式关闭 server 以便下一轮测试不会再出现 address already in use 的错误。</p><p><img src="/naive-kvengine-in-rust/boxcn0lodECLFH3AjTT7CGOFGLg.png" alt></p><h4 id="KvsEngine-线程安全"><a href="#KvsEngine-线程安全" class="headerlink" title="KvsEngine 线程安全"></a>KvsEngine 线程安全</h4><p>KvsEngine trait 需要满足 Clone + Send + ‘static 的 bound,同时三个对应的接口也可以去掉 &mut,因为变量的所有权和可变性已经转移到了智能指针中。</p><p><img src="/naive-kvengine-in-rust/boxcnJFwgaF5IXUqkq7iPFqorWd.png" alt></p><h4 id="SledKvsEngine-线程安全"><a href="#SledKvsEngine-线程安全" class="headerlink" title="SledKvsEngine 线程安全"></a>SledKvsEngine 线程安全</h4><p>Sled 引擎本身支持并发读写,因而直接对结构体 derive(Clone) 即可,其 set/get/remove 函数仅需挪去 self 的 &mut 即可。</p><p><img src="/naive-kvengine-in-rust/boxcnzGfVRmcZ2kjZyKatOZ9uHb.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnPZjYAEANb7Z9O28Zm5rfIg.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnW993QBlpNeVJ1vocEYpCwe.png" alt></p><h4 id="KvStore-线程安全"><a href="#KvStore-线程安全" class="headerlink" title="KvStore 线程安全"></a>KvStore 线程安全</h4><p>KvStore 的线程安全则需要对之前的结构体做大量的改造,改造之后的 KvStore 不仅支持读写请求互相不阻塞,甚至对同一 FileReader 的读请求也可以不在应用层阻塞。</p><ul><li>对于 index 结构,将其转换为了并发安全的 DashMap 结构,同时又增加了 Arc 指针以便于在不同线程间共享。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnaTmdxQINxNh6bgoVnbVpIh.png" alt></p><ul><li>对于 writer 结构,由于对同一文件的 append 操作从语义上来说便不支持并行,因而便通过 Arc<Mutex<Writer>> 的方式将所有的线程串行起来</Mutex<Writer></li></ul><p><img src="/naive-kvengine-in-rust/boxcn35QJnz7gVjIWmD7xrNrTCh.png" alt></p><ul><li>对于 readers 结构,我参照了部分 project4 样例的源码设计进行了无锁实现。注意其内部的 readers 在 clone 时并不是拷贝指针,而是初始化一个全新的 map,因而当多线程读同一个文件时,会创建多个 reader,这些 reader 可以在应用层对同一个文件执行并发的 IO 读请求。使用 RefCell 包装的原因是由于接口并未提供可变引用,如果还想保留对 map 的更改权限就需要用到 RefCell 了。</li><li>最容易想到的一种读请求并发控制方案是在上层做一定的串行以使得每个文件最多同时只有一个读请求在执行,从而减少磁盘的随机 IO。但实际上这样的设计并不一定有效果,一方面由于在文件系统中一个 file 的所有 data block 不一定完全在连续的 block 上,因而仅仅限制对一个文件不能并发读而不限制对多个文件不能并发读不一定能够起到减少随机 IO 的效果,另一方面 linux VFS 下的 IO 调度层本来就已经会对 IO 请求通过电梯算法等方式来做一些乱序处理来减少随机 IO,如果完全在上层做了串行反而会丢失部分可优化吞吐的空间。因而如果系统还没到完全掌握磁盘快中数据的分布来减少随机 IO 的地步,可以先尽可能的将读请求并行起来让底层去串行,而不是在上层就做好串行。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnlZ4coU7mWqKybLfYC7pqgp.png" alt></p><h4 id="写流程-1"><a href="#写流程-1" class="headerlink" title="写流程"></a>写流程</h4><p>由于 append 的写请求语义上就不能并行,因而当前 KvsEngine 的 set 请求被全部串行了起来</p><p><img src="/naive-kvengine-in-rust/boxcnrM7s7ntdUZwS41euVNO2af.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnFBgZG4DdIytqlfeqrlmqxe.png" alt></p><h4 id="删除流程-1"><a href="#删除流程-1" class="headerlink" title="删除流程"></a>删除流程</h4><p>由于删除也需要顺序 append,因而其语义与 set 类似不能并行,因而当前 KvsEngine 的 remove 请求也被全部串行了起来</p><p><img src="/naive-kvengine-in-rust/boxcnDij7HnjT3xTO0tBYJBmyEg.png" alt></p><h4 id="读流程-1"><a href="#读流程-1" class="headerlink" title="读流程"></a>读流程</h4><p>读流程语义理论上可以并行执行,因而首先在可并发读的 DashMap 中获取到索引,接着在当前线程内读取对应的 file_number 的 reader,如果当前线程不存在该 reader 则创建出对应的 reader 读取即可(使用了 entry API 来避免两次 hash)。</p><p><img src="/naive-kvengine-in-rust/boxcnPQEGztT4pID88lJEVIpTKJ.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnRs4vfkVcwiEJoQLIJpHM7o.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcn9YBWH6fWTEqZroFFEmjfQd.png" alt></p><h4 id="合并流程-1"><a href="#合并流程-1" class="headerlink" title="合并流程"></a>合并流程</h4><p>在实现无锁读之后,reader 的清理便不再能够串行起来了,因而需要一个多线程共享的原子变量来记录最新 compaction 之后的 file_number,小于这个 file_number 的文件和对应的 reader 便都可以删除了。</p><p>compact 流程会始终持有 writer 的写锁,因而此时并不存在并发安全问题,其在结束后会尝试删除掉过时的文件。不过该删除并不会影响其他读线程的 reader 句柄继续读去文件,这与 linux 文件系统的实现原理有关,直到任何线程都不存在指向该文件对应 inode 的句柄时便可以安全的释放该文件了。</p><p><img src="/naive-kvengine-in-rust/boxcnj3SztibcQ3dVtk80AgHB5r.png" alt></p><p><img src="/naive-kvengine-in-rust/boxcnkFizeIlIHOkdtLdjiJXa1b.png" alt></p><p>对于 reader,在 compaction 中其执行的索引尽管可能文件已经被删除了,但由于其持有句柄因而始终能够读到数据,在 compaction 之后其执行的索引一定是更新的文件,因而老的 reader 便不会再被用到,如果这些老 reader 一直不被释放,则可能导致合并过后的老文件始终无法在文件系统被释放,最终导致磁盘变满。因此在每次查询时都可以判断一下该原子变量并尝试删除本线程的老 reader,这样便可以既实现 lock-less 的 reader 又满足 compaction 消息的无锁感知和对应的资源清理了。</p><p><img src="/naive-kvengine-in-rust/boxcnbIWT6ofbomscJjhYDsI3og.png" alt></p><h4 id="性能测试-1"><a href="#性能测试-1" class="headerlink" title="性能测试"></a>性能测试</h4><p>按照题意写出对应的六个 benchmark 即可,主要做了以下工作:</p><ul><li>使用 Once 接口来确保在多个函数中 logger 仅被初始化一次,从而避免报错。</li></ul><p><img src="/naive-kvengine-in-rust/boxcn2a3QFIu6KvawbPR6I8lb0d.png" alt></p><ul><li>使用了 <a href="https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_with_inputs.html">bench_with_input</a> 接口来支持不同线程数的对比</li></ul><p><img src="/naive-kvengine-in-rust/boxcncFWYwfER6WT9yFEYNoIIud.png" alt></p><ul><li>使用了之前提到的方式来异步启动 server 并在一轮迭代结束后回收 server。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnjhiY2nLL2YcXE6hcSQvbqg.png" alt></p><ul><li>使用了 waitGroup 来实现客户端线程和迭代线程的同步。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnUGMyrZgPdodAwns1WyUrwc.png" alt></p><ul><li>减少 sample_size 来加快性能测试时间。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnWKzKDZwn4IZ1VuXbeuicbg.png" alt></p><p>最终的测试结果如下:</p><ul><li>write_queue_kvstore:随着线程数增大,延时先降再升,但变化幅度不大,尽管 read/write socket 可以并行起来了,但 set 还是必须得串行起来,与实现基本相符。</li></ul><p><img src="/naive-kvengine-in-rust/boxcnrpuZM9Ienhb4A3R8PGHdAd.png" alt></p><ul><li>read_queued_kvstore:随着线程数增大,延时先大幅度降低再大幅度升高,大幅度降低符合预期,因为不同的读请求现在可以串行起来,大幅度升高则不太符合预期,观察到了客户端建立连接 Timeout 的现象,不确定是否与本地的 Mac M1 Pro 环境有关。</li></ul><p><img src="/naive-kvengine-in-rust/boxcn4HmL8lEVkATOr4kedonK3f.png" alt></p><p>其他测试:</p><ul><li>write_rayon_kvstore</li></ul><p><img src="/naive-kvengine-in-rust/boxcnqWEQYmfb87Ku1fwAHwutEg.png" alt></p><ul><li>read_rayon_kvstore</li></ul><p><img src="/naive-kvengine-in-rust/boxcn31tkUHW2shoqZ4XO6Mx3dR.png" alt></p><ul><li>write_rayon_sledkvengine</li></ul><p><img src="/naive-kvengine-in-rust/boxcnAgA1VT5aM02gLqyi9YHJ6d.png" alt></p><ul><li>read_rayon_sledkvengine</li></ul><p><img src="/naive-kvengine-in-rust/boxcnoPA82ks3t9N0DQJDlpDcIf.png" alt></p><p>测试总结:</p><ul><li>总体来看,不同的存储引擎,不同的线程池策略,随着服务端线程池的线程数增大,延时都能够在某点得到最小值,这说明并行能够在部分场景起到效果。</li><li>在 MacOS M1 Pro 的环境上测试性能不太稳定,还容易出现 timeout 的情况,因而便没有进行更详细分析,感觉要想真的对比出性能的差异,还是需要在 Linux 环境下配上稳定的 CPU,磁盘,网络的可观测性工具结合不同引擎和不同线程池的内部 metric 来分析原因。害,底层软件就是这么难测试。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过本 Rust Lab,总共写了大约 2000+ 行的 Rust 代码。从 Cargo 包管理到 Rust 的所有权机制,然后到错误管理和若干标准库三方库的使用,再到线程池和并发引擎的设计以及异步 Runtime 的学习,虽然在性能测试和对比部分做的并不完善,但这些内容已经涵盖了开发大型 Rust 项目的方方面面。</p><p>下一步计划开始从 TiKV 的小 issue 入手,进一步深入学习 Rust。</p>]]></content>
<summary type="html"><h2 id="版本"><a href="#版本" class="headerlink" title="版本"></a>版本</h2><ul>
<li><a href="https://github.com/pingcap/talent-plan/tree/master/cour</summary>
<category term="存储引擎" scheme="https://tanxinyu.work/tags/%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E/"/>
<category term="Rust" scheme="https://tanxinyu.work/tags/Rust/"/>
</entry>
<entry>
<title>Talent-Plan:用 Rust 实现 Percolator 算法</title>
<link href="https://tanxinyu.work/percolator-in-rust/"/>
<id>https://tanxinyu.work/percolator-in-rust/</id>
<published>2022-08-25T03:36:53.000Z</published>
<updated>2023-01-18T15:19:18.277Z</updated>
<content type="html"><![CDATA[<h2 id="版本"><a href="#版本" class="headerlink" title="版本"></a>版本</h2><ul><li><a href="https://github.com/pingcap/talent-plan/tree/master/courses/dss/percolator">官网版本</a></li></ul><h2 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h2><p>Rust 学习</p><ul><li><a href="https://fasterthanli.me/articles/a-half-hour-to-learn-rust">半小时学习 Rust</a></li><li><a href="https://github.com/rust-lang/rustlings">Rustling</a> 及 <a href="https://github.com/OneSizeFitsQuorum/rustlings/pull/1">解答</a></li><li><a href="https://course.rs/about-book.html">Rust 语言圣经</a> 及 <a href="https://zh.practice.rs/why-exercise.html">习题</a></li><li><a href="https://kaisery.github.io/trpl-zh-cn/title-page.html">Rust 官方文档</a></li></ul><p>Percolator 学习</p><ul><li><a href="https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36726.pdf">论文</a></li><li><a href="https://github.com/OneSizeFitsQuorum/tinykv/blob/test/solution/lab4.md">TinyKV lab4 文档</a></li></ul><h2 id="过关思路"><a href="#过关思路" class="headerlink" title="过关思路"></a>过关思路</h2><p>在基本环境搭建好之后,观察发现 lab 只有 13 个测试,测试全部 AC 即可通过项目。实际上一个生产级别的 Percolator 实现这些测试是远远不够的,需要考虑和测试的 case 很多。</p><p>由于本 lab 的主要目标是帮助学习 Rust,因此本次过关思路便是通过 TDD 的方式通关,将主要精力放在通过测试以及对应 Rust 语法的学习和练习上,对于实现的 Percolator 算法满足论文要求和当前的测试 case 即可,不再去新增更多的 test case 和并发情况。</p><p><img src="/percolator-in-rust/0.png" alt></p><h2 id="过关过程"><a href="#过关过程" class="headerlink" title="过关过程"></a>过关过程</h2><h3 id="TSO-测试"><a href="#TSO-测试" class="headerlink" title="TSO 测试"></a>TSO 测试</h3><ul><li>test_get_timestamp_under_unreliable_network</li></ul><p>该测试期望在不稳定网络下 client 的 get_timestamp 接口在超时重试时可以满足 backoff 幂增重试属性。<br><img src="/percolator-in-rust/1.png" alt></p><p>因此一方面在 TSO Server 端的 get_timestamp 接口处提供一个空实现,另一方面在 Client 中维护对应初始化时传入的 rpc client 字段并在 get_timestamp 函数中增加幂增 sleep 逻辑即可。<br><img src="/percolator-in-rust/2.png" alt></p><p>有关 async 和 await 的原理需要进一步理解学习,但仅就完成本 lab 而言,可以查看 labrpc example 中的使用样例来模仿使用,即使用 await 来驱动 async 的代码块执行,使用 block_on 达到同步执行的效果。<br><img src="/percolator-in-rust/3.png" alt></p><p>通过本测试的新增代码可查看 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/commit/2deab4fec9c2c6a925963dd65cd690507cd7f565">commit</a></p><h3 id="TXN-正常测试"><a href="#TXN-正常测试" class="headerlink" title="TXN 正常测试"></a>TXN 正常测试</h3><ul><li>test_predicate_many_preceders_read_predicates</li><li>test_predicate_many_preceders_write_predicates</li><li>test_lost_update</li><li>test_read_skew_read_only</li><li>test_read_skew_predicate_dependencies</li><li>test_read_skew_write_predicate</li><li>test_write_skew</li><li>test_anti_dependency_cycles</li></ul><p>以上测试要求在网络正常的情况下能够正确实现 Percolator 的事务提交,即可按照论文中的伪码实现即可。</p><h4 id="client"><a href="#client" class="headerlink" title="client"></a>client</h4><p>对于 Client 的 new 函数,保存两个 rpc client 并初始化 start_ts 和 mem_buffer。注意,为了在客户端进行去重,mem_buffer 使用了 hashmap 而不是 Vec。<br><img src="/percolator-in-rust/4.png" alt></p><p>对于 Client 的 get_timestamp 函数,参照上一小节实现发 RPC 和对应的重试以及错误处理逻辑即可。</p><p>对于 Client 的 begin 函数,使用 get_timestamp 函数更新本地的 start_ts 即可,start_ts 将作为快照读取的依据。</p><p>对于 Client 的 set 函数,直接缓存到 mem_buffer 中即可。</p><p>对于 Client 的 commit 函数,开始 Percolator 的提交流程,即先 prewrite 再 commit。注意不论是 prewrite 还是 commit 都是先对 primary 进行处理再对 secondary 处理。</p><p><img src="/percolator-in-rust/5.png" alt></p><h4 id="proto"><a href="#proto" class="headerlink" title="proto"></a>proto</h4><p>参照论文写出了对应的 proto 文件如下,以下将分别分析四种 RPC 请求:<br><figure class="highlight proto"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs proto"><span class="hljs-keyword">message </span><span class="hljs-title class_">TimestampRequest</span> {}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">TimestampResponse</span> {<br> <span class="hljs-type">uint64</span> timestamp = <span class="hljs-number">1</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">GetRequest</span> {<br> <span class="hljs-type">bytes</span> key = <span class="hljs-number">1</span>;<br> <span class="hljs-type">uint64</span> start_ts = <span class="hljs-number">2</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">GetResponse</span> {<br> <span class="hljs-type">bytes</span> value = <span class="hljs-number">1</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">PrewriteRequest</span> {<br> <span class="hljs-type">uint64</span> start_ts = <span class="hljs-number">1</span>;<br> <span class="hljs-type">bytes</span> primary = <span class="hljs-number">2</span>;<br> <span class="hljs-type">bytes</span> key = <span class="hljs-number">3</span>;<br> <span class="hljs-type">bytes</span> value = <span class="hljs-number">4</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">PrewriteResponse</span> {<br> <span class="hljs-type">bool</span> success = <span class="hljs-number">1</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">CommitRequest</span> {<br> <span class="hljs-type">bool</span> is_primary = <span class="hljs-number">1</span>;<br> <span class="hljs-type">uint64</span> start_ts = <span class="hljs-number">2</span>;<br> <span class="hljs-type">uint64</span> commit_ts = <span class="hljs-number">3</span>;<br> <span class="hljs-type">bytes</span> key = <span class="hljs-number">4</span>;<br>}<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">CommitResponse</span> {<br> <span class="hljs-type">bool</span> success = <span class="hljs-number">1</span>;<br>}<br></code></pre></td></tr></table></figure></p><h4 id="get-timestamp"><a href="#get-timestamp" class="headerlink" title="get_timestamp"></a>get_timestamp</h4><p>对于 TSO 请求,此时需要使用一个全局唯一 ID 的递增生成器而不能硬编码成 0 了。最暴力的方法便是采用 wall clock time 来生成,然而在高并发情况下 wall clock time 可能产生重复的 ID,因此一般需要 HLC 结合物理时间戳和逻辑时间戳的方式来保证全局唯一。出于时间原因,我直接采用了逻辑递增 ID 的方式来生成全局唯一的递增 ID。为了性能考虑,我采用了 Arc<AtmoicU64> 而不是 Arc<Mutex<u64>> 来生成 id,这样实现的理论性能会更高,同时由于此处仅仅需要生成 ID 没有其他逻辑,因此 Ordering 采用 Relaxed 即可。<br><img src="/percolator-in-rust/6.png" alt></Mutex<u64></AtmoicU64></p><h4 id="get"><a href="#get" class="headerlink" title="get"></a>get</h4><p>对于 Get 请求,需要携带 key 和对应的 start_ts 来针对某一全局快照读取。因此客户端构建好 request 后向服务端发送即可,注意此时依然实现了超时重试的逻辑。<br><img src="/percolator-in-rust/7.png" alt></p><p>在服务端的 RPC handler 里,首先使用 lock 进入临界区,接着判断 lock 列中是否存在与本事务冲突的其他事务,如果存在则 backoff 等待并在稍后重试。接着查找 write 列中该 key 最接近当前事务 start_ts 的 commit_ts,如果不存在则返回空字符串,否则获取到其 start_ts 将其从 data 列读出来即可。<br><img src="/percolator-in-rust/8.png" alt></p><p>在 KVTable 的 read 函数中,使用 BTreeMap 的 range 接口和 last 接口来获取某个区间的最大值,如果不存在则返回 None。<br><img src="/percolator-in-rust/9.png" alt></p><h4 id="prewrite"><a href="#prewrite" class="headerlink" title="prewrite"></a>prewrite</h4><p>对于 PreWrite 请求,需要携带 start_ts,primary 和对应的 KV 对来进行写入。客户端添加了对应的重试和错误处理逻辑,并将参数发送给服务端。<br><img src="/percolator-in-rust/10.png" alt></p><p>在服务端,首先使用 lock 进入临界区,接着判断 lock 列和 Write 列中是否存在与本事务冲突的其他事务,如果存在则返回错误,此时只能事务执行失败让应用层重试。如果没有冲突的事务,则可以向 data 列写入暂时不可见的数据,接着向 lock 列写入锁来保证不会出现 lost update。<br><img src="/percolator-in-rust/11.png" alt></p><p>在 KVTable 的 write 函数中,使用 BTreeMap 的 insert 接口来新增对应的 entry。<br><img src="/percolator-in-rust/12.png" alt></p><p>需要注意的是,该 Lab 中并未引入分片的概念,因此可以认为所有的数据均在一个分片,因此理论上可以直接使用 1PC 的方式将请求直接在服务端进行 PreWrite & Commit 处理。本人也进行了 1PC 的实现,在实现完后发现存在一个测试并不希望按照 1PC 去处理,因而便采用了效率最低但和论文伪码以及测试能够对应的方式,每个 key 都会发一次 rpc 且 prewrite 和 commit 会发两轮 rpc。</p><h4 id="commit"><a href="#commit" class="headerlink" title="commit"></a>commit</h4><p>对于 Commit 请求,需要携带 start_ts, commit_ts,是否为 primary 以及对应要 commit 的 key。客户端添加了对应的重试和错误处理逻辑,并将参数发送给服务端。<br><img src="/percolator-in-rust/13.png" alt></p><p>在服务端,首先使用 lock 进入临界区,接着判断 lock 列是否存在自己的锁,如果不存在则说明发生了不符合预期的错误(比如此次 RPC 延期到达,或者本事务已经被其他事务 abort 且清理),此时只能返回失败让应用层处理。如果自己的锁存在,则可以向 write 列写入 ((key, commit_ts), start_ts) 以让 data 列的数据可见,接着移除掉 lock 列写入的锁即可。<br><img src="/percolator-in-rust/14.png" alt></p><p>在 KVTable 的 erase 函数中,使用 BTreeMap 的 remove 接口来移除对应的 entry。<br><img src="/percolator-in-rust/15.png" alt></p><h4 id="测试结果"><a href="#测试结果" class="headerlink" title="测试结果"></a>测试结果</h4><p>以上逻辑通过了事务正常提交时的测试,然而网络异常或者协调者异常时的测试还未完全 AC,因此还需要进一步处理。<br><img src="/percolator-in-rust/16.png" alt></p><p>通过本测试的新增代码可查看 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/commit/5ca761e06f7eefe5da901ca2e37c7c3fb7c2a3b7">commit</a></p><h3 id="TXN-异常测试"><a href="#TXN-异常测试" class="headerlink" title="TXN 异常测试"></a>TXN 异常测试</h3><ul><li>test_commit_primary_drop_secondary_requests</li><li>test_commit_primary_success</li><li>test_commit_primary_success_without_response</li><li>test_commit_primary_fail</li></ul><p>异常测试会模拟 commit 请求的 req/resp 被丢弃的场景,但是 primary 的 commit 会始终被满足,因而 secondary 的请求有可能会被丢弃。</p><p>在客户端侧,首先需要针对 test_commit_primary_fail 和 test_commit_primary_success_without_response 返回的错误进行特判处理,同样是发 rpc 出错,但他们一个要求 client 返回 Err(Error::Other(“resphook”.to_owned())),另一个要求返回 Ok(False)。尽管并不统一,但可以通过修改特判 Err 类型的方式来通过测试<br><img src="/percolator-in-rust/17.png" alt></p><p>在服务端侧,异常测试主要实现的函数便是 back_off_maybe_clean_up_lock,即当遇到其他事务遗留下来的 prewrite 结果时要如何处理。</p><p>对于 percolator 这种事务模型,primary key 的提交与否便是整个事务提交与否的标志。任何事务在读某一 key 时,如果遇到遗留的 Lock 列锁,在 sleep 超过 TTL 时间后,可以接着获取该冲突 key1 在 lock 列 key 中的 start_ts 和 value 中存的 primary 值。然后再去 Write 列中寻找 (primarykey,0) 和 (primarykey, u64::MAX) 范围内是否有指向 start_ts 的记录。如果存在,则说明该事务已经提交且能够获取到 commit_ts,此时对该 key1 做 commit 处理即可,即清理 Lock 列并在 Write 列添加对应的记录。如果不存在,则说明该事务尚未提交,且其他任何 rpc 再执行的时候都能够确定性的判断出该事务并未提交(即便是乱序到达的 primary commit rpc,其也会检测 lock 记录是否存在,只有存在时才能 commit),此时只需要将当前 key1 的遗留 lock 清理即可。尽管也可以顺便检测清理其他的遗留 key,但让其他的遗留 key 在需要清理时再进行清理也不影响 safety,因而只用清理 key1 即可。在 key1 清理完之后,当前事务便可以正常读取 key 的值了。</p><p>代码如下,需要注意在进入 back_off_maybe_clean_up_lock 函数前需要 drop 掉锁,back_off_maybe_clean_up_lock 函数在 sleep 完之后需要先拿到锁再进行操作。<br><img src="/percolator-in-rust/18.png" alt></p><p>contains_in_write_column 函数遍历 write 列寻找 (primarykey,0) 和 (primarykey, u64::MAX) 范围内是否有指向 start_ts 的记录,如果存在则返回 commit_ts,否则返回 None。<br><img src="/percolator-in-rust/19.png" alt></p><p>此外 kvtable 中的 erase 函数的参数之前是 commit_ts,但由于在 lock 列中记录的是 start_ts,不论是正常 commit 还是 rollback 均需要按照 (key, start_ts) 去清理而不是 (key, commit_ts),因而感觉这里应该是论文中的笔误被照抄过来了,在此处从 commit_ts 改为了 start_ts。<br><img src="/percolator-in-rust/20.png" alt></p><p>通过本测试的新增代码可查看 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/commit/8a62c5bc6096408433bfb0e0b6e04d9040b4082a">commit</a>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>最终所有测试通过如下,本人过关代码可参考该 <a href="https://github.com/OneSizeFitsQuorum/talent-plan/pull/1">PR</a>。<br><img src="/percolator-in-rust/21.png" alt></p><p>通过本 lab,对 Rust 编程的若干关键工具和重要知识点有了一定的实践,包括但不限于 cargo 管理,模式匹配,流程控制,错误处理,所有权与借用,迭代器,宏编程等等。希望未来能够深入理解 rust 的异步编程,成为一名有经验的 Rustacean。</p>]]></content>
<summary type="html"><h2 id="版本"><a href="#版本" class="headerlink" title="版本"></a>版本</h2><ul>
<li><a href="https://github.com/pingcap/talent-plan/tree/master/cour</summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="Rust" scheme="https://tanxinyu.work/tags/Rust/"/>
</entry>
<entry>
<title>分布式事务概述和对应代码框架介绍</title>
<link href="https://tanxinyu.work/talent-plan-transaction-talk/"/>
<id>https://tanxinyu.work/talent-plan-transaction-talk/</id>
<published>2022-04-21T09:44:10.000Z</published>
<updated>2022-12-15T02:38:37.881Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分享一下前两天在 <a href="https://tidb.net/talent-plan">Talent Plan Community</a> 做的有关分布式事务和 Distributed-Txn 代码框架的介绍。</p><p>这次分享除了对 2021 VLDB summer school 中讲授的若干重要主题进行了概述,还着重介绍了事件排序这一很本质的问题,此外也参考了不少优质资料,现在 share 出来希望能对这块知识感兴趣的同学有帮助。由于本人水平有限,如有原理错误欢迎与我沟通~</p><p>注:以下仅为图片,可以在 <a href="https://pingcap.feishu.cn/drive/folder/fldcn9zPuLSTqoL2JDQOT5jbpQd">此处</a> 在线浏览 PPT 原件和录屏。</p><h2 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h2><p><img src="/talent-plan-transaction-talk/1.png" alt><br><img src="/talent-plan-transaction-talk/2.png" alt><br><img src="/talent-plan-transaction-talk/3.png" alt><br><img src="/talent-plan-transaction-talk/4.png" alt><br><img src="/talent-plan-transaction-talk/5.png" alt><br><img src="/talent-plan-transaction-talk/6.png" alt><br><img src="/talent-plan-transaction-talk/7.png" alt><br><img src="/talent-plan-transaction-talk/8.png" alt><br><img src="/talent-plan-transaction-talk/9.png" alt><br><img src="/talent-plan-transaction-talk/10.png" alt><br><img src="/talent-plan-transaction-talk/11.png" alt><br><img src="/talent-plan-transaction-talk/12.png" alt><br><img src="/talent-plan-transaction-talk/13.png" alt><br><img src="/talent-plan-transaction-talk/14.png" alt><br><img src="/talent-plan-transaction-talk/15.png" alt><br><img src="/talent-plan-transaction-talk/16.png" alt><br><img src="/talent-plan-transaction-talk/17.png" alt><br><img src="/talent-plan-transaction-talk/18.png" alt><br><img src="/talent-plan-transaction-talk/19.png" alt><br><img src="/talent-plan-transaction-talk/20.png" alt><br><img src="/talent-plan-transaction-talk/21.png" alt><br><img src="/talent-plan-transaction-talk/22.png" alt><br><img src="/talent-plan-transaction-talk/23.png" alt><br><img src="/talent-plan-transaction-talk/24.png" alt><br><img src="/talent-plan-transaction-talk/25.png" alt><br><img src="/talent-plan-transaction-talk/26.png" alt><br><img src="/talent-plan-transaction-talk/27.png" alt><br><img src="/talent-plan-transaction-talk/28.png" alt><br><img src="/talent-plan-transaction-talk/29.png" alt><br><img src="/talent-plan-transaction-talk/30.png" alt><br><img src="/talent-plan-transaction-talk/31.png" alt><br><img src="/talent-plan-transaction-talk/32.png" alt></p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分享一下前两天在 <a href="https://tidb.net/talent-plan">Talent Plan Community<</summary>
<category term="分享" scheme="https://tanxinyu.work/tags/%E5%88%86%E4%BA%AB/"/>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
</entry>
<entry>
<title>Raft 算法和对应代码框架介绍</title>
<link href="https://tanxinyu.work/talent-plan-raft-talk/"/>
<id>https://tanxinyu.work/talent-plan-raft-talk/</id>
<published>2022-03-02T02:10:42.000Z</published>
<updated>2022-12-15T02:38:29.721Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分享一下前不久在 <a href="https://tidb.net/talent-plan">Talent Plan Community</a> 做的有关 Raft 算法和 Etcd/TinyKV 代码框架的介绍。</p><p>这次分享对 Raft 算法和对应实现做了较为系统的调研整理,不仅对若干经典问题做了介绍,也提供了不少优质参考资料,现在 share 出来希望能对这块知识感兴趣的同学有帮助。</p><p>注:以下仅为图片,可以在 <a href="https://vevotse3pn.feishu.cn/file/boxcn6tQX2I5QxyLM4ZGGVu2wmd">此处</a> 在线浏览 PPT 原件。</p><h2 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h2><p><img src="/talent-plan-raft-talk/1.png" alt><br><img src="/talent-plan-raft-talk/2.png" alt><br><img src="/talent-plan-raft-talk/3.png" alt><br><img src="/talent-plan-raft-talk/4.png" alt><br><img src="/talent-plan-raft-talk/5.png" alt><br><img src="/talent-plan-raft-talk/6.png" alt><br><img src="/talent-plan-raft-talk/7.png" alt><br><img src="/talent-plan-raft-talk/8.png" alt><br><img src="/talent-plan-raft-talk/9.png" alt><br><img src="/talent-plan-raft-talk/10.png" alt><br><img src="/talent-plan-raft-talk/11.png" alt><br><img src="/talent-plan-raft-talk/12.png" alt><br><img src="/talent-plan-raft-talk/13.png" alt><br><img src="/talent-plan-raft-talk/14.png" alt><br><img src="/talent-plan-raft-talk/15.png" alt><br><img src="/talent-plan-raft-talk/16.png" alt><br><img src="/talent-plan-raft-talk/17.png" alt><br><img src="/talent-plan-raft-talk/18.png" alt><br><img src="/talent-plan-raft-talk/19.png" alt><br><img src="/talent-plan-raft-talk/20.png" alt><br><img src="/talent-plan-raft-talk/21.png" alt><br><img src="/talent-plan-raft-talk/22.png" alt><br><img src="/talent-plan-raft-talk/23.png" alt><br><img src="/talent-plan-raft-talk/24.png" alt><br><img src="/talent-plan-raft-talk/25.png" alt><br><img src="/talent-plan-raft-talk/26.png" alt><br><img src="/talent-plan-raft-talk/27.png" alt><br><img src="/talent-plan-raft-talk/28.png" alt><br><img src="/talent-plan-raft-talk/29.png" alt><br><img src="/talent-plan-raft-talk/30.png" alt><br><img src="/talent-plan-raft-talk/31.png" alt><br><img src="/talent-plan-raft-talk/32.png" alt><br><img src="/talent-plan-raft-talk/33.png" alt></p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分享一下前不久在 <a href="https://tidb.net/talent-plan">Talent Plan Community<</summary>
<category term="分享" scheme="https://tanxinyu.work/tags/%E5%88%86%E4%BA%AB/"/>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="共识算法" scheme="https://tanxinyu.work/tags/%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>2021 年终总结:记我在清华 Apache IoTDB 组的成长</title>
<link href="https://tanxinyu.work/2021-annual-summary/"/>
<id>https://tanxinyu.work/2021-annual-summary/</id>
<published>2022-01-24T15:37:53.000Z</published>
<updated>2023-01-18T15:10:31.830Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>2021 年即将结束,这一年忙忙碌碌收获许多,也认识到了定期总结定期反省的重要性。今年回家后意识到自己应该养成写年终总结的习惯了,于是断断续续用近一周的时间写了第一次年终总结,算给自己的一整年一个交代。希望我的经历和感悟能给大家一些启发。</p><p>首先进行一个简单的自我介绍,我叫谭新宇,清华本硕,现在清华大学软件学院 Apache IoTDB 组就读研二,师从王建民/黄向东老师,我对分布式系统,时序数据库和共识算法较感兴趣。</p><p>接着简单介绍一下我们组的工作:Apache IoTDB(物联网数据库)是一体化收集、存储、管理与分析物联网时序数据的软件系统。 Apache IoTDB 采用轻量式架构,具有高性能和丰富的功能,并与 Apache Hadoop、Spark 和 Flink 等进行了深度集成,可以满足工业物联网领域的海量数据存储、高速数据读取和复杂数据分析需求。截止 2021 年底,Apache IoTDB 是国内高校唯一在 Apache TLP 中的开源软件,目前已经被建设成了百余贡献者的开源社区。</p><h2 id="本科经历"><a href="#本科经历" class="headerlink" title="本科经历"></a>本科经历</h2><p>既然是第一次年终总结,在此简单提提自己的本科经历。</p><p>2016 年,高考完填清华志愿的我并不知道自己的专业兴趣是什么,也并不知道不同的专业有什么区别,于是随便报了个机械大类便入学了。</p><p>在大一下的时候,一方面是成绩尚可(前 10%),另一方面是大一下接触编程课的印象还不错。对游戏一直很感兴趣的我便萌生了转专业的想法,最终也顺利的平转到了对编程萌新相对友好的软件学院。</p><p>在大二刚转过来的时候,我感受到的课程压力是非常大的,当然主要原因还是自己的编程基础太差。因为大学之前从未接触过编程,而且大一只学了一门针对机械学院同学开的比较水的编程课,所以大二学数据结构操作系统的时候非常吃力。幸运的是,软件学院的大佬同学们(教主,杰神,方舟,冯老板,伟哥等等)和辅导员们(岳导,李导)帮助了我很多,再结合自己的努力,我的成绩也慢慢能够跟上来了。</p><p>在大三的时候,得益于学院这个大平台和自己的努力,我分别在旷视和微软进行了短期实习。在旷视,我主要在 Data 组打杂,也是在这个时候开始了解分布式系统领域,学习了 Google 的三篇马车(GFS/MapReduce/BigTable)和 Facebook 的小文件冷热存储解决方案(HayStack/F4)并逐渐对这个领域产生了浓厚的兴趣。此外,我在旷视的 mentor 庄大师(外号纯纯)也给我留下了非常深刻的印象,现在他已经是成功人士了(手动狗头)。在微软,我们跟着邹欣老师(现在已经跳槽到 CSDN 当副总裁了)组队完成了一个 NLP 领域的 CodeSearch 项目,主要功能是实现一个 VSCode 插件,能够输入自然语言输出对应的代码,这个工作还拿到微软内部 Hackathon 比赛的奖。不过我的任务主要是爬虫数据清理之类的工作,一方面是自己对 AI 不是很感兴趣,另一方面也是自己炼丹的能力不足。这段经历到现在留给我的印象就是微软内部和谐友好的氛围了,是真的很 WLB 啊。</p><p>到了大四做保研还是工作的决定时,有很厉害的学长劝我早点到工业界,说很多研究生的工作都是在虚度年华,贵清也不例外。我的父母则是坚持要求我接着读书。在平衡各方面利弊后,我最终决定继续读个硕士,给自己一些缓冲的时间,并希望能够找到一个不虚度年华的实验室。</p><p>最开始我找到了李振华老师的实验室,主要原因是振华老师在上本科课程《网络系统》时实在是太过于风趣幽默,吸引了一大批小学弟。在振华组里最开始我是希望能够做一段有意义的科研的,然而呆了半年后我逐渐感觉到,我所感兴趣的分布式系统方面振华老师做的工作不多,我自己也很难独立的搞出一份科研工作出来。此外,振华老师喜欢的学生特质是英文写作能力强,基础扎实,对计算机网络/操作系统感兴趣,而当时的我与这三个特质都不太搭边,因此我的离开便不是很意外了。不过 u1s1,振华老师写论文的功底真的非常强,MobiCom,NSDI 年年发,21 年他们组还中了软院第一篇 SIGCOMM,可谓是风光无限了。尽管会比较 push,但好歹 push 的结果对大家都好,满足上述三个特征的同学也可以去跟振华老师联系。</p><p>当时,我和同在振华组里的苏总相约好了一起润,我们几乎打听了学院的所有实验室,在过滤掉不感兴趣的搞前后端和 AI 的实验室后,我们最后面临了两个选择,一个是散养的实验室,去了之后可以在企业自由的实习三年。一个是学院里在做的时序数据库 Apache IoTDB(当时还是孵化器项目),尽管去了之后会比较忙,但由于主力是学生,相比在外实习可能能得到更多的锻炼,而且硕士期间也能够拥有三年的数据库内核开发经验,对找工作比较友好。最终经过深思熟虑,我们相约一起润去了 Apache IoTDB 实验室。</p><p>进组后,我当时给自己的目标便是利用研究生三年时间提升自己的理论知识水平和工程能力。因而我一进去就开始了解开源,学习数据库知识并加入了分布式模块进行实践。做了不到一年,2020 年底我便被社区接纳为了 committer,这里也很感谢组里分布式模块的实力大哥江天学长的举荐。</p><p>如果让我在 2020 年底反思读研是否值得,我可能还很难给出明确的答案。但如果让我现在反思读研是否值得的话,我的答案一定是值得。这与我进实验室后的经历有关,我不确定当时直接工作的话在企业是否也能得到这样的成长,但至少 2021 年在实验室的成长已经让我觉得读研不虚此行了。其实仅仅深度参与一个开源基础软件从雏形不断发展壮大再到可能的商业化,感受项目管理,产品功能,人员心态,宣传运营,社区交流等方面的进化就已经是非常难得的一段经历了,更不要谈在这期间借助这个平台我们自己的成长了。</p><p>当然,读研到底是不是正收益。家庭情况,实验室情况,个人兴趣,时代发展,专业方向,人脉关系这些都是影响很大的因素,不可一概而论。</p><h2 id="2021"><a href="#2021" class="headerlink" title="2021"></a>2021</h2><p>讲完了之前的经历,在这里谈谈 2021 年的经历。如果要给 2021 年的自己一个关键字的话,我觉得应该是<strong>成长</strong>。</p><p>卷了一整个本科的我在研究生选课时遵循了兴趣导向的原则。即对于自己不感兴趣的课水水即可,对于自己感兴趣的课付出大量精力。前半年,我了解到了贵系陈康老师的《分布式导论》课程,这门课基本 Follow 了 MIT 6.824 的课纲,大作业也是完成 6.824 的 lab。尽管听起来就很硬核,但我毅然决然的选了它。在上这门课的时候,我一方面同步听了线下陈康老师和线上 MIT Robert Morris 老师的课程,另一方面花费了大量的精力去完成大作业,尽管大作业不要求完成 challenge 的内容,但我还是完成了 lab1-4 和 challenge 1-2 的全部内容,最终我如愿以偿的拿到了 A+ 的成绩,这门课也成为了我在贵清收获最大的课。现在想想,这一门课上下来,我阅读了近 20 篇经典论文,学习了很多分布式经典理论,包括各种一致性级别,CAP,BASE,FLP,共识算法,偏序/全序关系,若干系统架构等等,基本重铸了分布式系统的知识体系。此外,我也将我的大作业文档在 Github 上进行了开源,短短半年已经收获近 300 个 star,新增 160+ follower。而且我在知乎上也给自己的文档打了广告,半年收获了 13 万+ 阅读,500+ 赞,1200+ 收藏,成为了”如何的才能更好地学习 MIT6.824 分布式系统课程?”排名第三的答案(第一是 PingCAP 的黄东旭老师)。通过这些关注,我在这半年里也认识了很多志同道合的朋友,这是一生的财富。</p><p>7 月,刷完 6.824 收获很大的我又开始关注另一门课 CMU 15-445,想要就此机会认真学习一下数据库理论。用时一个多月,我利用晚上和周末的时间学习完了 15-445 的线上课程并被 Andy 老师的个人魅力深深吸引。无论是上课时的 DJ drop table,还是 Andy 老师对数据库知识的热情都让我印象深刻。通过这门课程,我基本了解了如何实现一个支持 ACID 的单机关系型数据库。略感遗憾的是,其大作业 bustub 的 codebase 是 C++,我一方面对 C++ 没有技术积累,另一方面也对自己的代码有一定的洁癖,如果我不足够了解 C++ 的话,我也不想写出一堆很丑陋的 lab 代码出来。主要是基于这个原因,我放弃了刷 bustub,换为 MIT 6.830 数据库课程的 lab (codebase 是 Java)刷了刷,但后来刷到 lab2 便由于时间原因被搁置了,之后有机会的话希望能重捡起来。</p><p>8 月,经过一年多的开发,我们终于迎来了 Apache IoTDB 分布式版本的第一个线上用户,针对于 20TB 每天的入库流量,我们用 3 节点的分布式 IoTDB 集群替代了 20 节点的 HBase 集群, 经过初步统计,预约未来 5 年能为企业节省上百万的硬件成本。尽管在上线前的内测期间我们遇到了不少问题,但我们顶住了压力并都进行了修复,最终也顺利上线。通过这次经历,我不仅进一步认可了自己在做的工作,也理解了能为用户创造实际价值才是项目存亡的关键。</p><p>8 月底,对 TiDB 架构一直感兴趣的我发现 PingCAP 提供了免费的 PCTA (TiDB 系统管理基础能力认证)考试机会,于是看了看文档考了一个 PCTA 证书玩玩。</p><p>在暑假,我参加了中科院组织的开源之夏活动,题目为 《Apache IoTDB 分布式混沌测试框架》。这份工作基于阿里的 ChaosBlade 搭建了一个混沌测试平台并对 Apache IoTDB 进行了若干种异常 case 的混沌测试。通过测试,我们发现了 Apache IoTDB 当前分布式版本在异常环境下存在的一些问题,有一些容易解决的问题已经得到了修复,然而也有一些较复杂的问题到今天依然存在,这也多多少少间接引起了我们的一次大规模重构,勉强算是一件有意义的工作吧。令人略感遗憾的是,尽管该混沌测试框架在部署好之后可以用 Dashboard 的方式方便地注入特定的异常,然而该框架依然是基于物理节点来实现的,很难做到自动化。如果没有测试人员去维护并定期手动测试,如果没有开发人员愿意抽出时间来完全解决其中发现的问题,如果整个团队没有足够重视异常场景下系统的对外表现并愿意为之付出大量的精力,该框架就很难形成正向反馈,最终只能被遗忘在历史的角落里。作为一点反省,我现在觉得混沌测试还是应该尽可能的通过持续集成的方式自动化起来(参照 ChaosMesh),这样释放人力的方式是大家都喜爱的,也只有这样,混沌测试才能对项目产生持续的正向收益。</p><p>9 月,打算硕士开题的我阅读了 Raft 相关的近 20 篇论文,感觉近几年与 Raft 相关的论文主要还是在区块链,新硬件和一些特定场景的优化上,没有太多本质的变化。</p><p>10 月,抱着学习的心态,我和家贝一唱报名参加了第一届九坤并行程序优化大赛,这个比赛主要考验选手最大化压榨 CPU 和 IO 性能的能力。由于赛题是 C/C++ 的 codebase,然而我们基本都对 C/C++ 不太熟悉,于是我们起名叫做了”只会 JAVA 队”。作为三个在体系结构几乎一窍不通的小白,在一个多月断断续续的不到 10 次线下沟通中,我们逐渐对体系结构入了门,在 192 个队伍脱颖而出,并在决赛取得了第 4 名的成绩(离苹果周边只差一步)。个人认为,数据库做到极致就是硬件性能的一种体现。因此,一个优秀的数据库工程师应该对体系结构具有一定的了解,这样才有可能进一步压榨硬件性能,从而达到更好的数据库性能。一直以来,我希望分布式数据库能够成为自己的一个标签。通过这次比赛,我意识到高性能计算也是一个很有趣且硬核的方向,其不仅能够给企业迅速带来真金白银的收益(节约成本),而且也是很多领域做到极致的一种出路。</p><p>11 月,PingCAP 举办了第一届 Talent Plan KV 学习营。我和汉乙组队参与了这次比赛,由于我们俩之前都刷过 MIT 6.824,已经对教学级别的 raft 有一定的了解,所以参加此次比赛的目的就是去感受一下生产级别分布式 KV 的代码实现,学习实践一下 lsm, etcd, raftstore 和 percolator 的理论知识和 codebase。u1s1,刷 lab 的过程十分曲折,我们俩所在的实验室到年底的时候都非常忙,前几周基本每周都只能抽出顶多一两天的时间来写代码,而理解 lab2b/lab3b raftstore 的难度是非常大的,我们用了一周多的时间才勉强看懂 raftstore 的代码。这使得到还剩两周时间的时候,我们才刷到 lab2c。最后两周我们利用中午午休时间和晚上睡觉时间疯狂加班,在 lab 上花了更多的时间,最后才堪堪刷完。出人意料的是,我们得了第二名的好成绩。事后反省一下,在 safety 上我们遇到的问题都解决了;在 liveness 上我们没投入太多精力;在文档上,我们简单介绍了代码实现,但将重点放在了我们对相关知识的理解和思考上;在性能上,我们重点做了最容易做的 batching 优化,其本质上是使用 raft 的优化而不是 raft 自身的优化,但对性能的提升却异常关键,比如 tidb 对于一个事务打包的一堆写请求,到 tikv 的 region 之后,这些写请求同步成一条还是多条 raftlog 对于性能的影响是巨大的。</p><p>年底,我和祥威思屹其骏受到实验室的支持,前去参加了海口的 2021 VLDB summer school,今年的 topic 是分布式事务。于向遥,吴英骏和魏星达老师清晰地介绍了分布式事务的方方面面,李飞飞和黄东旭老师则是分享了很多工业界的思考。虽然 KeyNote 请了 Andy 过来我也很喜欢 Andy 老师,但 u1s1 topic 和分布式事务不搭边,感觉基本是 Andy 老师在狂荡不羁的给 ottertune 打广告哈哈哈。这次 VLDB 第一次采用了理论 + 实践 + 随机组队的方式,我们除了每天早上要去感受学术界的熏陶外,下午还会去学习 PingCAP 工程师有关实践 lab 的 talk。我们的 lab 要实现 tinykv + tinysql 中有关分布式事务的所有部分。由于我之前已经了解过 tinykv 的 codebase,所以我的 lab 完成的很顺利,我也尽我所能在下午的实践课程中帮助了很多不了解 codebase 的其他组同学,这也使得我最终拿到了积极参与奖的荣誉。此外我们组由于做了 3 份独立的作业而且最后汇报的表现也还不错,最终成为了排名第一的组,我也幸运的拿到了优秀学员的荣誉并领了一堆 PingCAP 周边。当然对于我来说收获最大的还是跟大佬们的交流。我有幸代表学员跟周傲英老师简单分享了这次 VLDB summer school 的学习方式。我也在吴英骏老师课后跟其请教了应该怎么看待 serializable 和 linearizable,实际上前者是在说事务之间的隔离性,并不需要具备满足时序的偏序关系,而后者是在说单个对象读写操作间的可见性,需要严格满足时序的偏序关系。如果将事务看成单个对象来理解,事务之间又有隔离性又能够满足时序的偏序关系,则就可以被称为 strict serializable(也被 Google Spanner 称为 external consistency)。黄东旭老师在 KeyNote 上很直白的分享了很多工业界分布式事务的真实挑战,比如超大事务(100GB+),Online DDL (有 Google F1 Online DDL 可能还不够)等,在课下交流时,东旭老师则觉得将复制和共识解耦很可能是一件有意义的工作。我们是不是一直以来用错了共识算法?即共识只应该是共识索引而不是共识数据,比如对于 100GB 的数据,是不是在架构上将其写到一个共享存储/内存池上,然后将轻量的索引共识即可。此外我还跟 PingCAP 的童牧老哥请教了不少问题,主要问题集中在 SEDA 模型和 TPC 模型的优劣上。TiKV 利用 rust 在应用态做了非公平业务感知的 CPU scheduler,使得大数据的聚合查询和小数据量的单点查询在高并发时后者的延时依然稳定,感觉是非常有意义的工作。除了技术方面的交流外,王岩广老师介绍的 Talent Plan 社区也让我印象深刻。就我所看到的情况而言,PingCAP 在国产数据库人才培养方面付出了大量的努力并且成果斐然,在这里真诚感谢一下 PingCAP 社区。</p><p>除了参加一些活动学习一些课程,这一年我在 Apache IoTDB 社区 Review 了近 140 个 PR,提交并被合并了 47 个 PR,但大多都是一些维护性重构性的工作。说来也惭愧,进组一年了直到今年后半年我才开始认真研究时序数据库并逐渐意识到了它的挑战。最后一个季度,我和思屹调研了若干个国产时序数据库的分布式架构并进行了多次分享,从中发现了许多优点和大家依然没有解决的痛点。这些优点和痛点的解决方案我们已经进行了大量的讨论和分析,这期间东哥,乔老师,田原学长,金锐学长,江天学长,荣钊学弟,洪胤学弟等也都结合一些论文和自己的思考提出了不少很有特点的想法。这些工作目前正在被逐步吸收进 Apache IoTDB 的分布式版本里面去。尽管短期内还没有成效,但我对 2022 年充满信心。</p><p>后半年,我们实验室引入了在管理方面很有经验的刘海老师,对我们实验室的管理模式进行了一系列改造,包括但不限于分组管理和汇报,利用 sprint 管理进度,利用 confluence 管理文档,目标驱动等等。整个实验室的管理模式发生了翻天覆地的变化,我也有幸成为了一个 3 人小组的小组长,负责分布式模块的维护和推进工作。实际上我们实验室由于一直在搞开源基础软件,所以学生的工程能力一般都还不错。今年引入成熟的管理模式之后,每个人都感受到了管理的力量,也能够在毕业之前提前学习到一些有关管理的知识。长远来看,这对于学生的成长是非常有帮助的。在企业里,调动积极性还可以通过期权,职级等手段来实现。在学校里,调动积极性就只能靠个人魅力和让大家都得到成长的能力了。在我个人看来,这是更富有挑战的。</p><p>之前我觉得在实验室里追求个人的发展和完成组里的工作一定程度上是冲突的,我也就此事跟刘海老师沟通过。渐渐的,我意识到这两者并不一定冲突,它反而促进了我对于高效率的追求。今年我参加的课外比赛和线上学习的课程基本都是利用晚上和周末的时间完成的,并没有影响实验室的工作,在上午和下午的工作时间我依然会专心的进行实验室的工作。由于时间紧张,我会更加专注于手头上的工作,减少摸鱼时间,并不断反思总结如何更高效的工作和交流。从结果来看,思路的转变使我在相同的时间内得到了更多的成长。当然,代价就是牺牲了一些休息时间。</p><p>今年我写了大约 25 篇技术博客,开始阶段性地记录自己的学习过程,字数合计在十万以上。我觉得分享的本质是让大家变得更好,所以我会尽可能的写一些技术总结或者思想感悟,希望读者看了后有所收获。通过博客,我也认识了不少新朋友,希望自己之后能继续坚持下去。</p><p>今年我抽了一些休息时间断断续续看了一些比较感兴趣的电视剧《朱元璋》,《走向共和》和《天道》,也阅读了《邓小平时代》,《原则》和《人生的智慧》等书籍。尽管感悟不少,但它们的主题都比较深沉,我也常常会陷入悲观历史主义中去。我特别感谢我的女朋友小杨同学能够纠正我一些过于悲观的思想,能够让我始终燃着对生活的热爱。如果自己能多抽出一些时间陪陪她就好了。</p><p>当然,以上都是有好结果的经历,今年我也有很多失败的经历,包括但不限于没刷完 6.830 lab,没好好参加 OceanBase 数据库比赛,没去成字节跳动暑期夏令营,没有坚持刷 leetcode,没有完全解决对 IoTDB 进行混沌测试后发现的问题等等。</p><p>除了自己的成长外,这一年 Apache IoTDB 也在我们团队的努力下成长了很多,不论从功能还是性能上都有了很大的提升。当然最重要的还是社区的良性成长:从 2020 年的 396 人增长到 1532 人,国内社群用户数量较 2020 年增长超 287 % !目前已经有 162 位贡献者为 IoTDB 主仓库贡献了代码,从 2020 年的 94 增长到 162,相比 2020 年初增长了近 70 人!目前已经有多家公司深度参与到 IoTDB 的开发中,如东方国信、阿里、云智慧、360、用友、华为、中冶赛迪等等。学生群体方面,去年一年新增了来自清华、北大、北航、西北工业大学、复旦大学、南京大学、厦门大学、威斯康星大学、新加坡国立大学等国内外高校学生的身影,更有同学选择 IoTDB 作为他们的毕业设计方向。在 IoTDB,我们不仅在个人能力上得到了锻炼,也通过社区认识了很多很厉害很友善的新朋友。</p><h2 id="一些感悟"><a href="#一些感悟" class="headerlink" title="一些感悟"></a>一些感悟</h2><p>谈完了这么多经历,也想谈谈自己的感悟,这些感悟不一定适用于每个人,但都是我个人在今年得到成长的诀窍,一年前的我要是能明白这些道理也许就能少踩很多坑了。</p><h3 id="从要我干什么到我能干什么"><a href="#从要我干什么到我能干什么" class="headerlink" title="从要我干什么到我能干什么"></a>从要我干什么到我能干什么</h3><p>今年感觉自己认知转变最大的一点就是从”要我干什么”过渡到了”我能干什么”。认知的变化彻底改变了我思考问题的方式。</p><p>我不再荒废太多时间在娱乐上,而是思考自己当前能做什么有意义的事情。我不再将一些工作视为是锅,而是将其视为成长的机会。我不再低头闷声学习,而是开始热衷于交流,反思,请教和提问。我不再认为自己当前仅仅专注技术就够了,而是开始学习一些有关管理,表达,领导力的技巧。</p><p>读研三年,仅仅拿个学位在我看来还是有些虚度年华的。在未来我希望自己能够继续跳出自己的舒适圈,继续折腾和挑战自己。</p><h3 id="人与人最大的区别是认知"><a href="#人与人最大的区别是认知" class="headerlink" title="人与人最大的区别是认知"></a>人与人最大的区别是认知</h3><p>这个感悟也是今年体会特别深的一点。人与人最大的区别不是出身,不是社会地位,而是认知。同样的一件事,认知不同的人会有不同的看法,有的人视之为机会,有的人视之为灾祸,最后当然会有不同的结果。</p><p>今年有幸跟很多大佬都请教过,他们对这个世界和行业的认知都让我印象深刻。我的大导师清华大学软件学院院长王建民教授就常会来我们组跟我们讲一些做研究做工程的方法论。在这样的熏陶下,我们组同学们对行业的认知也在不断的提高。</p><p>就我个人的想法而言,多跟大佬们请教,怀着开放的心态去学习,把批评当做进步的机会就是提升自己认知的绝好方式。</p><h3 id="找准自己的兴趣点并持之以恒的专注"><a href="#找准自己的兴趣点并持之以恒的专注" class="headerlink" title="找准自己的兴趣点并持之以恒的专注"></a>找准自己的兴趣点并持之以恒的专注</h3><p>今年的 VLDB summer school 有一个数据库人才培养论坛,交流的大牛有北大的崔斌老师,人大的陈红老师,浙大的陈刚老师,华东师大的钱卫宁老师,阿里的李飞飞老师和 PingCAP 的黄东旭老师,期间有这样一个问题”作为新一代的数据库人,现在应该怎么做才有可能在未来成为一个在数据库界有影响力的人?”。各位老师都分享了自己的看法,包括但不限于努力,专注,思考,交流等等,但所有老师均认为保持长期专注是很重要的一个特性:只有长期的专注才有可能做到顶尖,这一点上没有捷径。</p><p>这个回答也引起了我强烈的共鸣,不论是在学术界还是在工业界,很重要的一点就是找准自己的兴趣点。一定要找到一个自己感兴趣的方向,通俗点来说,就是找到那种自己在休息时间也愿意去学习去钻研的兴趣点。这样,我们不仅在工作学习时更有热情,而且自己的价值也会随着长期地专注而得到不断增长。</p><h3 id="拒绝无效内卷"><a href="#拒绝无效内卷" class="headerlink" title="拒绝无效内卷"></a>拒绝无效内卷</h3><p>尽管长期地专注很重要,但无效内卷是不可取的。从长远来看,无效内卷会影响我们的工作热情并破坏团队氛围,最终一定是得不偿失的。不同人对”无效内卷”的评判标准不同,但我觉得每个人都应该有意识的去反思自己到底是不是在无效内卷。如果是,那一定要想到缓解甚至拒绝它的方法,包括但不限于和 leader one-one,润等。世界这么大,要相信自己一定能够找到立足之地,没必要恶心自己。</p><p>这两天有一个比较火的新闻是腾讯企业微信的应届生怒怼领导恶意内卷。说实话看新闻看得我热血沸腾,我也很佩服这位老哥,要是我自己可能就默默润了,然而他却敢于发声,这是非常难能可贵的。祝愿他未来一切顺利。</p><h3 id="脑袋决定屁股,屁股驱动行为"><a href="#脑袋决定屁股,屁股驱动行为" class="headerlink" title="脑袋决定屁股,屁股驱动行为"></a>脑袋决定屁股,屁股驱动行为</h3><p>在其位谋其职,任何组织都有光明的一面和黑暗的一面。屁股在哪就要努力让它变得更好,而不是见到黑暗面后就只知道诋毁和抱怨。从长远来看,后者最终只会损坏自己的名誉浪费自己的时间,得不到什么好处。</p><p>当然,如果经过自己的努力依然无法改变任何东西,那就用脑袋挑选一个更符合自己价值观的位置去坐吧。</p><h3 id="营造团队氛围,每个人都有责任"><a href="#营造团队氛围,每个人都有责任" class="headerlink" title="营造团队氛围,每个人都有责任"></a>营造团队氛围,每个人都有责任</h3><p>今年阅读了桥水基金创始人瑞·达利欧的《原则》这本书,我被其中描述的创意择优像家一样的工作氛围所深深吸引,幻想着自己未来能到这样的公司工作。</p><p>然而,我和很多读者一样都进入了相同的思维误区,那就是等着别人来营造这种氛围而我们坐享其成。既然每个人都喜欢这样的氛围,那么为什么不能在自己力所能及的范围内行动呢?</p><p>在我们实验室的三人分布式小组里,我作为小组长和两位学弟就努力在组内营造了这种氛围。大家有不错的学习资料就相互分享,有棘手的脏活就平分来干,功能按照兴趣去推动,bug 按照难易尽量平均分配,有人周中有事的话其他人可以暂时帮忙顶住工程压力,而他在忙完之后往往周末也会主动加班赶上落下的进度。在这期间,我也与学弟们建立了良好的私人关系,这样的氛围让我觉得开心,也让我进一步确信每个人都可以去营造团队氛围。</p><p>尽管营造团队氛围每个人都有责任,但我还是觉得考虑自己职业规划时一定要优先考虑关注员工成长的公司,否则推进这种氛围可能会非常难,这会进而影响自己的热情和产出。我个人是非常不认同企业付高工资就可以将工程师当做工具人来使用的企业文化的,员工和项目只有共同成长才有可能建立一个长期高效持续创新的团队,也只有这样的团队才值得大家齐心协力为之奋斗。</p><h3 id="真诚坦率"><a href="#真诚坦率" class="headerlink" title="真诚坦率"></a>真诚坦率</h3><p>《原则》这本书里谈到了真诚坦率是很难得的高价值品质,它带给我们的收益往往比我们想象的还要大。</p><p>当我们能够真诚坦率的公开我们的工作内容和进度时,一方面会使我们更容易获得上级和同事的理解信任,另一方面也会促使我们减少摸鱼时间,更加重视自己的单位时间产出并努力提高自己的工作效率,而后者在我看来是更关键的。人都会存在惰性,这样的机制可以帮助自己克服惰性,追求效率至上。</p><h3 id="不要妄想别人帮自己指出最优解,路都是自己走出来的"><a href="#不要妄想别人帮自己指出最优解,路都是自己走出来的" class="headerlink" title="不要妄想别人帮自己指出最优解,路都是自己走出来的"></a>不要妄想别人帮自己指出最优解,路都是自己走出来的</h3><p>我在振华老师组的时候曾经请教他让他为我指一条发展最快的捷径。振华老师当时说他也不确定我走哪条路是最快的捷径,只能结合我自己的状态一步步走着看。</p><p>当时的我还不能够理解,现在的我逐渐明白了:对于大部分人来说,路都是要自己一步步走出来的,不要妄想有个大佬能直接给自己指出全局最优的捷径。保持开放的心态,一直学习一直反思,多向大佬们请教,能走出一条局部最优解的路就已经很不错了。</p><h3 id="养成定期反思的好习惯"><a href="#养成定期反思的好习惯" class="headerlink" title="养成定期反思的好习惯"></a>养成定期反思的好习惯</h3><p>犯错误不可怕,不反思错误的原因并进行针对性的分析和改进就很可怕了。</p><p>随着见识的增长,我逐渐意识到定期反思总结对于个人的发展至关重要。因此我们小组除了每周同步两次工作进度以外,每周每月还会做反思总结并在组间进行分享和互相评价,这些总结内容包括但不限于工作总结,个人发展,自我反思和想法建议等。每月抽一个小时反思下存在的问题,不论是自身的还是团队的,一经分享讨论下个月就可能已经进行了改进,这形成了一个正反馈效应,对大家都是有益无害的。</p><p>通过这些分享,我们每个人都在逐渐的进步,这些进步包括但不限于代码设计,时间管理,汇报技巧,工作方式,做事思维等等。就跟当年高考刷题一样,悲观的人认为题量是无尽的,怎么刷也刷不完;乐观的人认为题量是有尽的,刷一道少一道。我是乐观的人,我坚信这些定期反思加上我们的长期专注一定会迎来收获。</p><h3 id="了解技术细节最好的时机有两个,一个是过去,一个是现在"><a href="#了解技术细节最好的时机有两个,一个是过去,一个是现在" class="headerlink" title="了解技术细节最好的时机有两个,一个是过去,一个是现在"></a>了解技术细节最好的时机有两个,一个是过去,一个是现在</h3><p>在写代码的时候我们常会碰到一些技术细节(比如 git 命令,比如不同 IO 的区别),有些人认为这些细节无关紧要将其置之不理,而有些人非常重视这些细节并将其很快学懂。我觉得面对这些细节问题的态度是决定技术水平是否会停滞不前的重要因素。</p><p>了解技术细节最好的时机有两个,一个是过去,一个是现在。当遇到技术细节时,可以简单评估一下学习成本。如果很低(小于 1 小时),则可以在当天尽量将其搞明白。如果比较高,则可以将其放在预计学习的列表里面,定期抽出一些整块的时间集中学习。</p><p>诚然,在刚开始编程的时候就这样会比较累,因为什么都不会。但只要能够长期坚持学习,遇到不懂技术细节的概率会越来越低,自己的水平也会越来越高,形成的正反馈效应也会强化这一过程。</p><h3 id="不要给自己设限"><a href="#不要给自己设限" class="headerlink" title="不要给自己设限"></a>不要给自己设限</h3><p>在大型基础软件里,往往不同的人会负责不同的模块。如果模块解耦做的比较好的话,可能不同模块的同学不会有太多交流。比如在刚进组的时候我就只关注了分布式模块的内容,有关单机任何模块的功能和 bug 修复我基本都不会关注,这也使得我进组都一年了还不太了解时序数据库是什么。现在回想当时的自己还是太傻了。</p><p>之前振华组里的明亮学长在做秋招分享时介绍到,他认为互联网跳槽之所以容易涨薪涨职级,就是因为跳槽人往往对其所在组工作具有比较深刻的认知,这些认知可能是几十人的团队踩了数年的坑才提炼出来的经验,而只要在这个团队待几个月说不定就能够学懂大半了,这些经验可以为下家公司创造很大的价值。</p><p>基于这样的思路,我们就不应该给自己设限。在自己力所能及的范围内去关注一下其他模块的工作往往不需要很多时间,但对于自己的职业发展一定是有益无害的。这里我就非常佩服和我在一个组的苏总,他就是典型的不给自己设限的人,他一人为 IoTDB 带来了 UDF, Trigger, Select into, Continuous query, 算数表达式等高级特性,写入,查询,生态工具这些模块他都碰过。尽管他现在非常忙,但我们这一届的同学毕业后对 IoTDB 了解最深的人很可能就是他,那他的价值就会非常高了。</p><h3 id="多给自己一些正向反馈"><a href="#多给自己一些正向反馈" class="headerlink" title="多给自己一些正向反馈"></a>多给自己一些正向反馈</h3><p>这个感悟其实涉及到了一些心理学的技巧。在学习计算机的过程中,往往会遇到很多棘手的问题,而这些问题很可能会影响学习热情。很多小白在刚入门的时候遇到一个棘手的问题就被劝退了。因此我们需要定期给自己一些正向反馈,这些反馈包括但不限于做一个完整的 project/feature,解决一个别人解决不了的 bug,参加各种比赛获奖,做 talk,发论文等等。通过这种外在的对自己的认可,我们也就能继续保持着学习热情。这一点黄东旭老师在 VLDB summer school 的 panel 上也有提到。</p><h3 id="不能只有输入没有输出"><a href="#不能只有输入没有输出" class="headerlink" title="不能只有输入没有输出"></a>不能只有输入没有输出</h3><p>在工作学习中,不能只有输入没有输出。我对输入的理解是指花费的时间成本,对输出的理解则是明确的他人可以看到的工作量。</p><p>为什么要强调这一点?因为我刚到实验室就是这样的状态,后面才慢慢意识到并进行了改变。比如对于一个调研的工作,往往会花费很多时间,调研完后自己可能感觉学习到了很多东西,但也没有明确的输出(包括但不限于分享,阅读笔记,解决实际问题等等)让别人看到,过一段时间可能自己又忘了。这不仅会导致别人丧失对自己的信任,也会导致自己忽视自己的单位时间产出,形成过一天混一天的惰性思维。它们都会使得自己丧失进一步成长的机会。因此,定期适当地输出是非常重要的。</p><h3 id="敢于不卑不亢地分享自己的想法"><a href="#敢于不卑不亢地分享自己的想法" class="headerlink" title="敢于不卑不亢地分享自己的想法"></a>敢于不卑不亢地分享自己的想法</h3><p>之前振华老师跟我们分享过他在明尼苏达留学时的一个有趣现象。在每周全院的科研 idea 分享会上,尽管上台分享的印度人很多,但大多数都很 naive,相反上台分享的中国人很少,但大多都很厉害,这侧面反映了我们中国人可能受儒家思想的影响,大都不好意思去分享自己还不成熟的工作。不同的人对这种现象有不同的看法,但在振华老师看来,早分享可能早避坑,不畏惧批评反而会有更大的前景。在 VLDB summer school 上黄东旭老师也告诫我们不要畏惧权威,要敢于分享自己的想法。</p><p>对于技术问题,我们要敢于不卑不亢,就事论事的分享自己的想法。如果我们的想法是正确的,这样能够不断竖立自己的个人影响力。如果我们的想法是错误的,这样也能够早点遭致批评来纠正自己的错误想法。无论如何对自己都是有益无害的。</p><h3 id="养成闭环的做事习惯"><a href="#养成闭环的做事习惯" class="headerlink" title="养成闭环的做事习惯"></a>养成闭环的做事习惯</h3><p>养成闭环的做事习惯非常重要,这有利于自己在同事中建立信任,塑造自己靠谱的形象。</p><p>一个新功能的开发工作,从接受,需求,调研,设计,实现,测试,review,合并到最终汇报要形成一个闭环。我在刚开始参与社区的时候对这样繁琐的流程非常困惑,感觉没有什么意义。随着维护项目经验的积累,我逐步意识到了这些都是过来人的智慧。</p><p>乔老师常给我们介绍说做事要做到”事事有回应 件件有着落 凡事有交代”,务实的说,很难对所有的事情都做到这种程度,但我们可以怀抱着这样的期望尽力做到这样,至少也得把重要且紧急的事情做到这个地步。养成这样的习惯对于自己的职业发展绝对是有益无害的。</p><h3 id="开源铸就了数据库最好的时代"><a href="#开源铸就了数据库最好的时代" class="headerlink" title="开源铸就了数据库最好的时代"></a>开源铸就了数据库最好的时代</h3><p>不确定是不是幸存者偏差,感觉数据库近几年在国内越来越火了。但就算是幸存者偏差,开源基础软件在国内越来越火已经是毋庸置疑的事实了。不论是资本还是企业,都更青睐开源的基础软件。</p><p>作为学生,这也是我们迅速成长的最好时代。我们完全可以参与开源社区,迅速提升自己的能力并认识一批志同道合的朋友,我就是开源的典型受益者,不论是 Talent Plan 社区还是 Apache IoTDB 社区都让我受益良多。</p><p>此外,站在巨人的肩膀上非常重要。我们如今身处开源的时代,很多资料都是公开的,一定要避免闭门造车。多了解其他系统,多交流,多调研,这对于个人和项目的发展都是有益无害的。</p><h3 id="稳定性可维护性大于性能"><a href="#稳定性可维护性大于性能" class="headerlink" title="稳定性可维护性大于性能"></a>稳定性可维护性大于性能</h3><p>当修 bug 次数多了之后,我逐渐意识到软件工程的重要性,并开始思考项目不稳定的根因。</p><p>OceanBase 的杨传辉老师提过一个观点:每个系统设计时都需要考虑架构、稳定性和性能,这三者之间的关系是什么?一个经典的规律是“把稳定的系统做高效,远比把高效的系统做稳定更容易”。最难的是从 0 到 1 把系统做稳定。有了稳定的系统,接下来逐步优化性能往往会比较顺利,直到遇到系统架构的性能天花板。因此,系统架构设计之前,首先要考虑清楚系统的目标和性能天花板,接着基于正确的架构把系统做稳定,最后优化性能。</p><p>我个人非常认同这个观点,大型基础软件的性能和稳定性可维护性往往存在一个不那么明显的 trade-off。在开源软件里,要实现新功能或者重构,一定要优先关注可维护性和稳定性。实现一个性能最优但模块耦合不好维护的功能对项目的伤害是非常大的,甚至可以被称为技术债,这会大大影响开发者的热情并辜负客户的信任。先实现一个 naive 但好维护几乎没有 bug 的版本,逐步的去优化性能,对于开发者和客户来说都是一个正向反馈的过程,这对于社区的发展也是很有帮助的。这里我就非常喜欢 TiDB 社区的方式,模块解耦做的比较干净,从 3.0 到 4.0 再到 5.0。每个版本都有巨大的性能提升,社区也越来越好,这就是一个明显的正反馈过程。</p><p>至于如何进一步提升系统的稳定性,在 VLDB summer school 的 panel 上我请教了黄东旭老师,他分享了 PingCAP 现在已经有几百台测试服务器日夜不息的进行测试,同时每年 PingCAP 为了稳定性付出的成本(包括硬件成本,人力成本等)已经达到了总支出的 30%~40%,而这依然还没有让他满意。由此可见,稳定的产品一定是大量的测试打磨出来的,没有太多捷径可走。当然,作为写代码的工程师,多反思总结,不断提升自己的技术水平,也能够对项目的稳定性做出自己的贡献。</p><h3 id="创新往往来自假设的改变"><a href="#创新往往来自假设的改变" class="headerlink" title="创新往往来自假设的改变"></a>创新往往来自假设的改变</h3><p>在今年的 VLDB summer school 上,有一个观点始终被提及:”创新往往来自假设的改变”。这个世界本来并不存在什么假设,假设是人类描述自然规律时的前置条件,而这个前置条件并不一定绝对正确,也不一定一成不变。不论是做学术还是做产品,可以时常想想假设在部分场景是否已经发生了变化,这其中可能蕴含着巨大的创新。</p><h2 id="来年展望"><a href="#来年展望" class="headerlink" title="来年展望"></a>来年展望</h2><p>洋洋洒洒写了这么一大堆流水账,也是借着写总结的机会再反思下自己,给未来的自己一个警醒。</p><p>新的一年,希望自己能努力干好以下 6 件事情吧:</p><ul><li>和实验室同学们一起打造出一个稳定高效的分布式 IoTDB,在毕业之前不留遗憾。</li><li>和实验室同学们一起营造团队的技术氛围,希望能够将我们组塑造成高校做开源的标杆实验室,吸引一大批对分布式系统感兴趣的优秀同学过来,一起干有意义有挑战的事情。</li><li>实习和秋招的时候多找一些团队聊聊,最终选择一个能得到成长的增量赛道。</li><li>认真学习一下数据库引擎,真的动手实践一下 LLVM,JIT,CodeGen,向量化等技术,也许是学习一下 tinysql 3.0 版本或者 risinglight。</li><li>规律作息,多锻炼多运动,减减肥,让自己拥有健康的身体。</li><li>多抽一些时间陪陪我的小杨同学,祝愿她申请一切顺利。</li></ul><p>最后,在除夕这天,预祝大家新年万事如意。愿每个人在新的一年都学有所得,学有所获,学有所长。</p>]]></content>
<summary type="html"><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>2021 年即将结束,这一年忙忙碌碌收获许多,也认识到了定期总结定期反省的重要性。今年回家后意识到自己应该养成写年终总结的习惯了,于是断断续</summary>
<category term="IoTDB" scheme="https://tanxinyu.work/tags/IoTDB/"/>
<category term="年终总结" scheme="https://tanxinyu.work/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
</entry>
<entry>
<title>2021 Talent Plan KV 学习营结营总结</title>
<link href="https://tanxinyu.work/tinykv/"/>
<id>https://tanxinyu.work/tinykv/</id>
<published>2022-01-14T09:23:13.000Z</published>
<updated>2022-08-28T04:29:42.965Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>2021 年 11 月 ~ 2022 年 1 月 ,<strong>PingCAP </strong>举办了第一届 <strong>Talent Plan KV 学习营</strong>,相关介绍可参考 <a href="https://m.creatby.com/v2/manage/book/oa4occ/?from=singlemessage">推送</a>。</p><p>在本次比赛中,由于我们小组的两位成员之前都刷过 MIT 6.824,已经对教学级别的 raft 有一定的了解,所以参加此次比赛的目的就是去感受一下生产级别分布式 KV 的代码实现,学习实践一下 lsm, etcd, raftstore 和 percolator 的理论知识和 codebase。</p><p>u1s1,刷 lab 的过程十分曲折,我们俩所在的实验室到年底的时候都非常忙,前几周基本每周都只能抽出顶多一两天的时间来写代码,而理解 lab2b/lab3b raftstore 的难度是非常大的,我们用了一周多的时间才勉强看懂 raftstore 的代码。这使得到还剩两周时间的时候,我们才刷到 lab2c。最后两周我们利用中午午休时间和晚上睡觉时间疯狂加班,在 lab 上花了更多的时间,最后才堪堪刷完。</p><p>在刷 lab 的过程中,由于时间有限,我们始终秉持着<code>学习优先,成绩第二</code>的原则。即以 <strong>了解 codebase,学习知识,做最容易做且最有用的优化</strong> 为主,并没有去卷很多功能点。在处理 bug 的态度上,对于 safety 的问题比如错误读写的 bug 等,我们对这类问题进行了重点关注和解决;对于 liveness 的问题比如 request timeout 等,我们则是在有限的时间内尽力做了优化,但并没有投入太多精力,因为这种工作没有上限,tikv 的 raftstore 也一定在持续做这些工作,时间不够的情况下去卷这些就没有太大意义了。</p><p><img src="/tinykv/grade.png" alt></p><p>出人意料的是,我们得了第二名的好成绩,具体可参考 <a href="https://asktug.com/t/topic/393068">官宣</a>。事后反省一下,在 safety 上我们遇到的问题都解决了;在 liveness 上我们没投入太多精力;在文档上,我们简单介绍了代码实现,但将重点放在了我们对相关知识的理解和思考上;在性能上,我们重点做了最容易做的 batching 优化,其本质上是使用 raft 的优化而不是 raft 自身的优化,但对性能的提升却异常关键,比如 tidb 对于一个事务打包的一堆写请求,到 tikv 的 region 之后,这些写请求同步成一条还是多条 raftlog 对于性能的影响是巨大的。</p><p>从结果来看,我们的策略是正确的,我们在很有限的时间内拿到了很高的收益。</p><p>最后,出于对课程的保护,也出于跟大家分享一些刷 lab 的经验,让大家少踩坑,在此处我仅将文档公开,希望能为大家提供一些思路,欢迎一起交流。</p><h1 id="文档"><a href="#文档" class="headerlink" title="文档"></a>文档</h1><h2 id="lab1"><a href="#lab1" class="headerlink" title="lab1"></a>lab1</h2><h3 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h3><h4 id="Part-1-Implement-a-standalone-storage-engine"><a href="#Part-1-Implement-a-standalone-storage-engine" class="headerlink" title="Part 1 : Implement a standalone storage engine"></a>Part 1 : Implement a standalone storage engine</h4><p>本部分是对底层 badger api 的包装,主要涉及修改的代码文件是 standalone_storage.go, 需要实现 Storage 接口的 Write 和 Reader 方法,来实现对底层 badger 数据库的读写。</p><h5 id="1-Write-部分实现思路"><a href="#1-Write-部分实现思路" class="headerlink" title="1.Write 部分实现思路"></a>1.Write 部分实现思路</h5><p>Write 部分涉及到 Put 和 Delete 两种操作。</p><p>因为 write_batch.go 中已经实现了对 badger 中 entry 的 put 和 delete 操作,我们只需要判断 batch 中的每一个 Modify 的操作类型,然后直接调用 write_batch.go 中相对应的方法即可。</p><h5 id="2-Reader-部分实现思路"><a href="#2-Reader-部分实现思路" class="headerlink" title="2.Reader 部分实现思路"></a>2.Reader 部分实现思路</h5><p>Reader 部分会涉及到 point read 和 scan read 两种不同读方式。</p><p>因为提示到应该使用 badger.Txn 来实现 Reader 函数,所以我们声明了一个 badgerReader 结构体来实现 StorageReader 接口,badgerReader 结构体内部包含对 badger.Txn 的引用。</p><p>针对 point read,<br>我们直接调用 util.go 中的 GetCF 等函数,对 cf 中指定 key 进行读取。</p><p>针对 scan read,<br>直接调用 cf_iterator.go 中的 NewCFIterator 函数,返回一个迭代器,供 part2 中调用。</p><h4 id="Part-2-Implement-raw-key-value-service-handlers"><a href="#Part-2-Implement-raw-key-value-service-handlers" class="headerlink" title="Part 2 : Implement raw key/value service handlers"></a>Part 2 : Implement raw key/value service handlers</h4><p>本部分需要实现 RawGet/ RawScan/ RawPut/ RawDelete 四个 handlers,主要涉及修改的代码文件是 raw_api.go</p><p>针对 RawGet,<br>我们调用 storage 的 Reader 函数返回一个 Reader,然后调用其 GetCF 函数进行点读取即可,读取之后需要判断对应 key 是否存在。</p><p>针对 RawScan,<br>同样地调用 storage 的 Reader 函数返回一个 Reader,然后调用其 IterCF 函数返回一个迭代器,然后使用迭代器读取即可。</p><p>针对 RawPut 和 RawDelete,<br>声明对应的 Modify 后,调用 storage.Write 函数即可。</p><h3 id="相关知识学习"><a href="#相关知识学习" class="headerlink" title="相关知识学习"></a>相关知识学习</h3><p>LSM 是一个伴随 NoSQL 运动一起流行的存储引擎,相比 B+ 树以牺牲读性能的代价在写入性能上获得了较大的提升。</p><p>近年来,工业界和学术界均对 LSM 树进行了一定的研究,具体可以阅读 VLDB2018 有关 LSM 的综述:<a href="https://arxiv.org/pdf/1812.07527.pdf">LSM-based Storage Techniques: A Survey</a>, 也可直接阅读针对该论文我认为还不错的一篇 <a href="https://blog.shunzi.tech/post/vldbj-2018lsm-based-storage-techniques-a-survey/">中文概要总结</a>。</p><p>介绍完了 LSM 综述,可以简单聊聊 badger,这是一个纯 go 实现的 LSM 存储引擎,参照了 FAST2016 有关 KV 分离 LSM 的设计: <a href="https://www.usenix.org/system/files/conference/fast16/fast16-papers-lu.pdf">WiscKey</a> 。有关其项目的动机和一些 benchmark 结果可以参照其创始人的 <a href="https://dgraph.io/blog/post/badger/">博客</a>。</p><p>对于 Wisckey 这篇论文,除了阅读论文以外,也可以参考此 <a href="https://www.scienjus.com/wisckey/">阅读笔记</a> 和此 <a href="https://www.skyzh.dev/posts/articles/2021-08-07-lsm-kv-separation-overview/">总结博客</a>。这两篇资料较为系统地介绍了现在学术界和工业界对于 KV 分离 LSM 的一些设计和实现。</p><p>实际上对于目前的 NewSQL 数据库,其底层大多数都是一个分布式 KV 存储系统。对于 OLTP 业务,其往往采用行存的方式,即 key 对应的 value 便是一个 tuple。在这样的架构下,value 往往很大,因而采用 KV 分离的设计往往能够减少大量的写放大,从而提升性能。</p><p>之前和腾讯云的一个大佬聊过,他有说 TiKV 的社区版和商业版存储引擎性能差异很大。目前想一下,KV 分离可能便是 RocksDB 和 Titan 的最大区别吧。</p><h2 id="lab2"><a href="#lab2" class="headerlink" title="lab2"></a>lab2</h2><h3 id="解题思路-1"><a href="#解题思路-1" class="headerlink" title="解题思路"></a>解题思路</h3><h4 id="lab2a"><a href="#lab2a" class="headerlink" title="lab2a"></a>lab2a</h4><h5 id="Leader-election"><a href="#Leader-election" class="headerlink" title="Leader election"></a>Leader election</h5><p>本部分是对 raft 模块 leader 选举功能的实现,主要涉及修改的代码文件是 raft.go、log.go</p><p>raft 模块 leader 选举流程如下:</p><p><img src="/tinykv/leader%20election.jpg" alt></p><p>第一步,我们首先实现对 raft 的初始化。</p><p>实现 log.go 中的 newLog 方法,调用 storage 的 InitialState 等方法对 RaftLog 进行初始化,读取持久化在 storage 中 term、commit、vote 和 entries,为后面的 lab 做准备。完成 RaftLog 的初始化后,再填充 Raft 中的相应字段,即完成 Raft 对象的初始化。</p><p>第二步,我们实现 Raft 对象的 tick() 函数</p><p>上层应用会调用 tick() 函数,作为逻辑时钟控制 Raft 模块的选举功能和心跳功能。因此我们实现 tick() 函数,当 Raft 状态是 Follower 时,检查自上次接收心跳之后,间隔时间是否超过了 election timeout,如果超过了,将发送 MessageType_MsgHup;当 Raft 状态时 Leader 时,检查自上次发送心跳之后,间隔时间是否超过了 heartbeat timeout,如果超过了,将发送 MessageType_MsgBeat。</p><p>第三步,我们实现 raft.Raft.becomeXXX 等基本函数</p><p>实现了 becomeFollower(),becomeCandidate(),becomeLeader() 等 stub 函数,对不同状态下的属性进行赋值。</p><p>第四步,我们实现 Step() 函数对不同 Message 的处理</p><p>主要涉及到的 Message 有</p><ul><li><p>MessageType_MsgHup</p></li><li><p>MessageType_MsgRequestVote</p></li><li><p>MessageType_MsgRequestVoteResponse</p></li></ul><p>接下来分情况实现:</p><p>(1)MessageType_Msgup</p><p>当 Raft 状态为 Follower 和 Candidate 时,会先调用 becomeCandidate() 方法,将自己的状态转变为 Candidate,然后向所有 peer 发送 MessageType_MsgRequestVote 消息,请求他们的投票</p><p>(2)MessageType_MsgRequestVote</p><p>当 Raft 接收到此消息时,会在以下情况拒绝投票:</p><ul><li><p>当 Candidate 的 term 小于当前 raft 的 term 时拒绝投票</p></li><li><p>如果当前 raft 的 term 与 candidate 的 term 相等,但是它之前已经投票给其他 Candidate 时,会拒绝投票</p></li><li><p>如果当前 raft 发现 candidate 的日志不如自己的日志更 up-to-date 时,也会拒绝投票</p></li></ul><p>(3)MessageType_MsgRequestVoteResponse</p><p>Candidate 接收到此消息时,就会根据消息的 reject 属性来确定自己的得票,当自己的得票数大于一半以上,就会调用 becomeLeader() 函数,将状态转变为 Leader;当拒绝票数也大于一半以上时,就会转回到 Follower 状态。</p><h5 id="Log-replication"><a href="#Log-replication" class="headerlink" title="Log replication"></a>Log replication</h5><p>本部分是对 raft 模块日志复制功能的实现,主要涉及修改的代码文件是 raft.go、log.go</p><p>日志复制的流程如下:</p><p><img src="/tinykv/log%20replication.jpg" alt="Log Replication"></p><p>本部分主要实现不同状态的 raft 对以下 Message 的处理:</p><ul><li>MessageType_MsgBeat</li><li>MessageType_MsgHeartbeat</li><li>MessageType_MsgHeartbeatResponse</li><li>MessageType_MsgPropose</li><li>MessageType_MsgAppend</li><li>MessageType_MsgAppendResponse</li></ul><p>接下来分情况实现:</p><p>(1)MessageType_MsgBeat</p><p>当上层应用调用 tick() 函数时,Leader 需要检查是否到了该发送心跳的时候,如果到了,那么就发送 MessageType_MsgHeartbeat。</p><p>leader 会将自己的 commit 值赋给在 MsgHeartbeat 消息中响应值,以让 Follower 能够及时 commit 安全的 entries</p><p>(2)MessageType_MsgHeartbeat</p><p>当 Follower 接收到心跳时,会更新自己的 electionTimeout,并会将自己的 lastIndex 与 leader 的 commit 值比较,让自己能够及时 commit entry。</p><p>(3)MessageType_MsgHeartbeatResponse</p><p>当 Leader 接收到心跳回复时,会比较对应 Follower 的 Pr.Match, 如果发现 Follower 滞后,就会向其发送缺少的 entries</p><p> (4)MessageType_MsgPropose</p><p>当 Leader 要添加 data 到自己的 log entries 中时,会发送一个 local message—MsgPropose 来让自己向所有 follower 同步 log entries,发送 MessageType_MsgAppend</p><p>(5)MessageType_MsgAppend</p><p>当 Follower 接收到此消息时,会在以下情况拒绝 append:</p><ul><li>当 Leader 的 term 小于当前 raft 的 term 时拒绝 append</li><li>当 Follower 在对应 Index 处不含 entry,说明 Follower 滞后比较严重</li><li>当 Follower 在对应 Index 处含有 entry,但是 term 不相等,说明产生了冲突</li></ul><p>其他情况,Follower 会接收新的 entries,并更新自己的相关属性。</p><p>(6)MessageType_MsgAppendResponse</p><p>当 Leader 发现 Follower 拒绝 append 后,会更新 raft.Prs 中对应 Follower 的进度信息,并根据新的进度,重新发送 entries。</p><h5 id="Implement-the-raw-node-interface"><a href="#Implement-the-raw-node-interface" class="headerlink" title="Implement the raw node interface"></a>Implement the raw node interface</h5><p>本部分主要实现 raw node 的接口,涉及修改的代码文件为 rawnode.go</p><p>RawNode 对象中的属性除了 Raft 对象,还增加了 prevSoftState 和 preHardState 两个属性,用于在 HasReady() 函数中判断 node 是否 pending</p><p>此外还实现了 Advance() 函数,主要是对 Raft 内部属性进行更新。</p><h4 id="lab2b"><a href="#lab2b" class="headerlink" title="lab2b"></a>lab2b</h4><h5 id="Implement-peer-storage"><a href="#Implement-peer-storage" class="headerlink" title="Implement peer storage"></a>Implement peer storage</h5><p>本部分主要实现 peer_storage.go 中 SaveReadyState() 方法和 Append() 方法,涉及修改的代码文件为 peer_storage.go</p><p>peer storage 除了管理持久化 raft log 外,也会管理持久化其他元数据(RaftLocalState、RaftApplyState 和 RegionLocalState),因此我们需要实现 SaveReadyState() 方法,将 raft.Ready 中修改过的状态和数据保存到 badger 中。</p><p>首先我们通过实现 Append() 方法,保存需要持久化的 raft log。遍历 Ready 中 Entries,调用 SetMeta() 方法将他们保存到 raftWB,并删除可能未提交的 raft log,最后更新 raftState。</p><p>在处理完 raft log 后,我们还需要保存 Ready 中的 hardState,并在最后调用 WriteToDB() 方法保证之前的修改落盘。</p><h5 id="Implement-raft-ready-process"><a href="#Implement-raft-ready-process" class="headerlink" title="Implement raft ready process"></a>Implement raft ready process</h5><p>本部分主要实现 peer_storage_handler.go 中的 proposeRaftCommand() 和 HandleRaftReady() 方法,涉及修改的代码文件为 peer_storage_handler.go</p><p>proposeRaftCommand() 方法使得系统有能力将接收到的 client 请求通过 raft 模块进行同步,以实现分布式环境下的一致性。在本方法中,我们直接调用 raft 模块的 Propose 方法,将 client 请求进行同步,并为该请求初始化对应的 proposal,以便该请求 committed 后将结果返回给 client</p><p>当 msg 被 raft 模块处理后,会导致 raft 模块的一些状态变化,这时候需要 HandleRaftReady() 方法进行一些操作来处理这些变化:</p><ol><li>需要调用 peer_storage.go() 中的 SaveReadyState() 方法,将 log entries 和一些元数据变化进行持久化。</li><li>需要调用 peer_storage_handler 中的 send() 方法,将一些需要发送的消息,发送给同一个 region 中的 peer</li><li>我们需要处理一些 committed entries,将他们应用到状态机中,并把结果通过 callback 反馈给 client</li><li>在上述处理完后,需要调用 advance() 方法,将 raft 模块整体推进到下一个状态</li></ol><h4 id="lab2c"><a href="#lab2c" class="headerlink" title="lab2c"></a>lab2c</h4><p>因为 raft entries 不可能一直无限增长下去,所以本部分我们需要实现 snapshot 功能,清理之前的 raft entries。</p><p>整个 lab2c 的执行流程如下:</p><ol><li>gc log 的流程:</li></ol><p><img src="/tinykv/gc%20raftLog.png" alt="gc raftLog"></p><ol><li>发送和应用 snapshot 的流程:</li></ol><p><img src="/tinykv/send%20and%20apply%20Snapshot.png" alt="send and apply snapshot"></p><h5 id="Implement-in-raft"><a href="#Implement-in-raft" class="headerlink" title="Implement in raft"></a>Implement in raft</h5><p>当 leader 发现 follower 落后太多时,会主动向 follower 发送 snapshot,对其进行同步。在 Raft 模块内部,需要增加对 MessageType_MsgSnapshot 消息的处理,主要对以下两点进行处理:</p><ol><li>当 leader 需要向 follower 同步日志时,如果同步的日志已经被 compact 了,那么直接发送 snapshot 给 follower 进行同步,否则发送 MessageType_MsgAppend 消息,向 follower 添加 entries。通过调用 peer storage 的 Snapshot() 方法,我们可以得到已经制作完成的 snapshot</li><li>实现 handleSnapshot() 方法,当 follower 接收到 MessageType_MsgSnapshot 时,需要进行相应处理。</li></ol><p>在第二步中,follower 需要判断 leader 发送的 snapshot 是否会与自己的 entries 产生冲突,如果发送的 snapshot 是目前现有 entries 的子集,说明 snapshot 是 stale 的,那么要返回目前 follower 的进度,更新 leader 中相应的 Match 和 Next,以便再下一次发送正确的日志;如果没有发生冲突,那么 follower 就根据 snapshot 中的信息进行相应的更新,更新自身的 committed 等 index,如果 confstate 也产生变化,有新的 node 加入或者已有的 node 被移除,需要更新本节点的 confState,为 lab3 做准备。</p><h5 id="Implement-in-raftstore"><a href="#Implement-in-raftstore" class="headerlink" title="Implement in raftstore"></a>Implement in raftstore</h5><p>在本部分中,当日志增长超过 RaftLogGcCountLimit 的限制时,会要求本节点整理和删除已经应用到状态机的旧日志。节点会接收到类似于 Get/Put/Delete/Snap 命令的 CompactLogRequest,因此我们需要在 lab2b 的基础上,当包含 CompactLogRequest 的 entry 提交后,增加 processAdminRequest() 方法来对这类 adminRequest 的处理。</p><p>在 processAdminRequest() 方法中,我们需要更新 RaftApplyState 中 RaftTruncatedState 中的相关元数据,记录最新截断的最后一个日志的 index 和 term,然后调用 ScheduleCompactLog() 方法,异步让 RaftLog-gc worker 能够进行旧日志删除的工作。</p><p>另外,因为 raft 模块在处理 snapshot 相关的 msg 时,也会对一些状态进行修改,所以在 peer_storage.go 方法中,我们需要在 SaveReadyState() 方法中,调用 ApplySnapshot() 方法中,对相应的元数据进行保存。</p><p>在 ApplySnapshot() 方法中,如果当前节点已经处理过的 entries 只是 snapshot 的一个子集,那么需要对 raftLocalState 中的 commit、lastIndex 以及 raftApplyState 中的 appliedIndex 等元数据进行更新,并调用 ClearData() 和 ClearMetaData() 方法,对现有的 stale 元数据以及日志进行清空整理。同时,也对 regionLocalState 进行相应更新。最后,我们需要通过 regionSched 这个 channel,将 snapshot 应用于对应的状态机</p><h3 id="相关知识学习-1"><a href="#相关知识学习-1" class="headerlink" title="相关知识学习"></a>相关知识学习</h3><h4 id="Raft"><a href="#Raft" class="headerlink" title="Raft"></a>Raft</h4><p>Raft 是 2015 年以来最受人瞩目的共识算法,有关其前世今生可以参考我们总结的 <a href="https://tanxinyu.work/raft/">博客</a>,此处不再赘述。</p><p>etcd 是一个生产级别的 Raft 实现,我们在实现 lab2a 的时候大量参考了 etcd 的代码。这个过程不仅帮助我们进一步了解了 etcd 的 codebase,也让我们进一步意识到一个工程级别的 raft 实现需要考虑多少 corner case。整个学习过程收获还是很大的,这里贴一些 etcd 的优质博客以供学习。</p><ul><li><a href="https://www.codedump.info/post/20180922-etcd-raft/">etcd Raft 库解析</a></li><li><a href="https://www.codedump.info/post/20181125-etcd-server/">Etcd 存储的实现</a></li><li><a href="https://www.codedump.info/post/20210515-raft">Etcd Raft 库的工程化实现</a></li><li><a href="https://www.codedump.info/post/20210628-etcd-wal/">Etcd Raft 库的日志存储</a></li></ul><h4 id="KVRaft"><a href="#KVRaft" class="headerlink" title="KVRaft"></a>KVRaft</h4><p>在 Raft 层完成后,下一步需要做的便是基于 Raft 层搭建一个高可用的 KV 层。这里依然参考了 etcd KV 层驱动 Raft 层的方式。<br>即总体的思路如下所示:<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs Go"><span class="hljs-keyword">for</span> {<br> <span class="hljs-keyword">select</span> {<br> <span class="hljs-keyword">case</span> <-s.Ticker:<br> Node.Tick()<br> <span class="hljs-keyword">default</span>:<br> <span class="hljs-keyword">if</span> Node.HasReady() {<br> rd := Node.Ready()<br> saveToStorage(rd.State, rd.Entries, rd.Snapshot)<br> send(rd.Messages)<br> <span class="hljs-keyword">for</span> _, entry := <span class="hljs-keyword">range</span> rd.CommittedEntries {<br> process(entry)<br> }<br> s.Node.Advance(rd)<br> }<br>}<br></code></pre></td></tr></table></figure></p><p>做过 tinykv 的同学应该都能够感觉到 lab2b 的难度与之前有一个大 gap,我认为主要原因是需要看的代码实现是太多了。</p><p>如今回首,建议分三个步骤来做,这样效率可能会高一些:</p><ul><li>了解读写流程的详细步骤。对于 client 的请求,其处理和回复均在 raft_server.go 中进行了处理,然而其在服务端内部的生命周期如何,这里需要知根知底。(注意在遇到 channel 打断同步的执行流程时不能瞎猜,一定要明确找到 channel 的接收端和发送端继续把生命周期理下去)</li><li>仔细阅读 raft_server.go, router.go, raftstore.go, raft_worker.go, peer_storage.go, peer_msg_handle.go 等文件的代码。这会对了解整个系统的 codebase 十分有帮助。</li><li>仔细阅读 tinykv 的 lab2 文档,了解编码,存储等细节后便可以动手实现了。</li></ul><p>在实现 lab2b 中,由于时间有限,我们重点关注了 batching 的优化和 apply 时的 safety,以下进行简单的介绍:</p><ul><li><p>batching 优化:客户端发来的一条 command 可能包含多个读写请求,服务端可以将其打包成一条或多条 raft 日志。显然,打包成一条 Raft 日志的性能会更高,因为这样能够节省大量 IO 资源的消耗。当然这也需要在 apply 时对所有的 request 均做相应的业务和容错处理。</p></li><li><p>apply 时的 safety:要想实现基于 Raft 的 KV 服务,一大难点便是如何保证 applyIndex 和状态机数据的原子性。比如在 6.824 的框架中,Raft 层对于上层状态机的假设是易失的,即重启后状态机为空,那么 applyIndex 便可以不被持久化记录,因为一旦发生重启 Raft 实例可以从 0 开始重新 apply 日志,对于状态机来说这个过程保证不会重复。然而这样的实现虽然保证了 safety,但却不是一个生产可用的实现。对于 tinykv,其状态机为非易失的 LSM 引擎,一旦要记录 applyIndex 就可能出现与状态机数据不一致的原子性问题,即重启后可能会存在日志被重复 apply 到状态机的现象。为了解决这一问题,我们将每个 Index 下 entry 的应用和对应 applyIndex 的更新放到了一个事务中来保证他们之间的原子性,巧妙地解决了该过程的 safety 问题。</p></li></ul><h4 id="Snapshot"><a href="#Snapshot" class="headerlink" title="Snapshot"></a>Snapshot</h4><p>tinykv 的 Snapshot 几乎是一个纯异步的方案,在架构上有很多讲究,这里可以仔细阅读文档和一位社区同学分享的 <a href="https://asktug.com/t/topic/273859">Snapshot 流程</a> 后再开始编码。</p><p>一旦了解了以下两个流程,代码便可以自然而然地写出来了。</p><ul><li>log gc 流程</li><li>snapshot 的异步生成,异步分批发送,异步分批接收和异步应用。</li></ul><h2 id="lab3"><a href="#lab3" class="headerlink" title="lab3"></a>lab3</h2><h3 id="解题思路-2"><a href="#解题思路-2" class="headerlink" title="解题思路"></a>解题思路</h3><h4 id="lab3a"><a href="#lab3a" class="headerlink" title="lab3a"></a>lab3a</h4><p>本部分主要涉及 Raft 算法 leader transfer 和 conf change 功能的两个工作,主要涉及修改的代码文件是 raft.go</p><p>对于 leader transfer,注意以下几点即可:</p><ul><li>leader 在 transfer 时需要阻写。</li><li>当 leader 发现 transferee 的 matchIndex 与本地的 lastIndex 相等时直接发送 timeout 请求让其快速选举即可,否则继续发送日志让其快速同步。</li><li>当 follower 收到 leader transfer 请求时,直接发起选举即可</li></ul><p>对于 conf change,注意以下几点即可:</p><ul><li>只对还在共识组配置中的 raftnode 进行 tick。</li><li>新当选的 leader 需要保证之前任期的所有 log 都被 apply 后才能进行新的 conf change 变更,这有关 raft 单步配置变更的 safety,可以参照 <a href="https://groups.google.com/g/raft-dev/c/t4xj6dJTP6E/m/d2D9LrWRza8J">邮件</a> 和相关 <a href="https://zhuanlan.zhihu.com/p/342319702">博客</a>。</li><li>只有当前共识组的最新配置变更日志被 apply 后才可以接收新的配置变更日志。</li><li>增删节点时需要维护 PeerTracker。</li></ul><h4 id="lab3b"><a href="#lab3b" class="headerlink" title="lab3b"></a>lab3b</h4><p>本部分主要是在 3a 的基础上,在 raft store 层面实现对 TransferLeader、ChangePeer 和 Split 三种 AdminRequest 的处理,涉及修改的文件主要是 peer_msg_handler.go 和 peer.go</p><p>对于 TransferLeader,比较简单:</p><p>TransferLeader request 因为不需要复制到 follower 节点,所以在 peer_msg_handler.go 的 pproposeRaftCommand() 方法中直接调用 raw_node.go 中的 TransferLeader() 方法即可</p><p>对于 ConfChange,分 addNode 和 removeNode 两种行为处理。</p><p>当 addNode 的命令 commit 之后,不需要我们手动调用 createPeer() 或者 maybeCreatePeer() 来显式创建 peer。我们只需要对 d.ctx 中的 storeMeta 进行修改即可,新 peer 会通过心跳机制进行创建。</p><p>当 removeNode 的命令 commit 之后,与 addNode 命令不同的是,我们需要显式调用 destroyPeer() 函数来停止相应的 raft 模块。这时需要注意的一个点时,当 Region 中只剩下两个节点,要从这两个节点中移除一个时,如果有一个节点挂了,会使整个集群不可用,特别是要移除的节点是 leader 本身。</p><p>在测试中会遇到这样的问题:当 Region 中只剩下节点 A(leader)和 节点 B(follower),当 removeNode A 的命令被 commit 之后,leader 就进行自我销毁,如果这个时候进入了 unreliable 的状态,那么 leader 就有可能无法在 destory 之前通过 heartbeat 去更新 follower 的 commitIndex。这样使得 follower B 不知道 leader A 已经被移除,就算发起选举也无法收到节点 A 的 vote,最终无法成功,导致 request timeout。</p><p>对于 split, 需要注意:</p><ol><li>因为 Region 会进行分裂,所以需要对 lab2b 进行修改,当接收到 delete/put/get/snap 等命令时,需要检查他们的 key 是否还在该 region 中,因为在 raftCmd 同步过程中,可能会发生 region 的 split,也需要检查 RegionEpoch 是否匹配。</li><li>在比较 splitKey 和当前 region 的 endKey 时,需要使用 engine_util.ExceedEndKey(),因为 key range 逻辑上是一个环。</li><li>split 时也需要对 d.ctx 中的 storeMeta 中 region 相关信息进行更新。</li><li>需要显式调用 createPeer() 来创建新 Region 中的 peer。</li><li>在 3b 的最后一个测试中,我们遇到以下问题:<ol><li>达成共识需要的时间有时候比较长,这就会导致新 region 中无法产生 leade 与 Scheduler 进行心跳交互,来更新 Scheduler 中的 regions,产生 find no region 的错误。这一部分可能需要 pre-vote 来进行根本性地解决,但时间不够,希望以后有时间解决这个遗憾。</li><li>会有一定概率遇到“多数据”的问题,经排查发现 snap response 中会包含当前 peer 的 region 引用返回,但是这时可能会产生的一个问题时,当返回时 region 是正常的,但当 client 端要根据这个 region 来读的时候,刚好有一个 split 命令改变了 region 的 startKey 或者 endKey,最后导致 client 端多读。该问题有同学在群中反馈应该测试中对 region 进行复制。</li><li>会有一定概率遇到“少数据”的问题,这是因为当 peer 未初始化时,apply snapshot 时不能删除之前的元数据和数据。</li></ol></li></ol><h4 id="lab3c"><a href="#lab3c" class="headerlink" title="lab3c"></a>lab3c</h4><p>本部分主要涉及对收集到的心跳信息进行选择性维护和对 balance-region 策略的具体实现两个工作,主要涉及修改的代码文件是 cluster.go 和 balance_region.go</p><p>对于维护心跳信息,按照以下流程执行即可:</p><ul><li>判断是否存在 epoch,若不存在则返回 err</li><li>判断是否存在对应 region,如存在则判断 epoch 是否陈旧,如陈旧则返回 err;若不存在则选择重叠的 regions,接着判断 epoch 是否陈旧。</li><li>否则维护 region 并更新 store 的 status 即可。</li></ul><p>对于 balance-region 策略的实现,按照以下步骤执行即可:</p><ul><li>获取健康的 store 列表:<ul><li>store 必须状态是 up 且最近心跳的间隔小于集群判断宕机的时间阈值。</li><li>如果列表长度小于等于 1 则不可调度,返回空即可。</li><li>按照 regionSize 对 store 大小排序。</li></ul></li><li>寻找可调度的 store:<ul><li>按照大小在所有 store 上从大到小依次寻找可以调度的 region,优先级依次是 pending,follower,leader。</li><li>如果能够获取到 region 且 region 的 peer 个数等于集群的副本数,则说明该 region 可能可以在该 store 上被调度走。</li></ul></li><li>寻找被调度的 store:<ul><li>按照大小在所有 store 上从小到达依次寻找不存在该 region 的 store。</li><li>找到后判断迁移是否有价值,即两个 store 的大小差值是否大于 region 的两倍大小,这样迁移之后其大小关系依然不会发生改变。</li></ul></li><li>如果两个 store 都能够寻找到,则在新 store 上申请一个该 region 的 peer,创建对应的 MovePeerOperator 即可。</li></ul><h3 id="相关知识学习-2"><a href="#相关知识学习-2" class="headerlink" title="相关知识学习"></a>相关知识学习</h3><h4 id="Multi-Raft"><a href="#Multi-Raft" class="headerlink" title="Multi-Raft"></a>Multi-Raft</h4><p>Multi-Raft 是分布式 KV 可以 scale out 的基石。TiKV 对每个 region 的 conf change 和 transfer leader 功能能够将 region 动态的在所有 store 上进行负载均衡,对 region 的 split 和 merge 则是能够解决单 region 热点并无用工作损耗资源的问题。不得不说,后两者尽管道理上理解起来很简单,但工程实现上有太多细节要考虑了(据说贵司写了好几年才稳定),分析可能的异常情况实在是太痛苦了,为贵司能够啃下这块硬骨头点赞。</p><p>最近看到有一个基于 TiKV 的 hackathon <a href="https://github.com/TPC-TiKV/rfc">议题</a>,其本质是想通过更改线程模型来优化 TiKV 的写入性能、性能稳定性和自适应能力。这里可以简单提提一些想法,其实就我们在时序数据库方向的一些经验来说,每个 TSM(TimeSeries Merge Tree)大概能够用满一个核的 CPU 资源。只要我们将 TSM 引擎额个数与 CPU 核数绑定,写入性能基本是能够随着核数增加而线性提升的。那么对于 KV 场景,是否开启 CPU 个数的 LSM 引擎能够更好的利用 CPU 资源呢?即对于 raftstore,是否启动 CPU 个数的 Rocksdb 实例能够更好的利用资源呢?感觉这里也可以做做测试尝试一下。</p><h4 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h4><p>负载均衡是分布式系统中的一大难题,不同系统均有不同的策略实现,不同的策略可能在不同的 workload 中更有效。</p><p>相比 pd 的实现,我们在 lab3c 实现的策略实际上很 trivial,因此我们简单学习了 pd 调度 region 的 <a href="https://asktug.com/t/topic/242808">策略</a>。尽管这些策略道理上理解起来都比较简单,但如何将所有统计信息准确的量化成一个动态模型却是一件很难尽善尽美的事,这中间的很多指标也只能是经验值,没有严谨的依据。</p><p>有关负载均衡我们对学术界的相关工作还不够了解,之后有时间会进行一些关注。</p><h2 id="lab4"><a href="#lab4" class="headerlink" title="lab4"></a>lab4</h2><h3 id="解题思路-3"><a href="#解题思路-3" class="headerlink" title="解题思路"></a>解题思路</h3><p>本 Lab 整体相对简单,在基本了解 MVCC, 2PC 和 Percolator 后便可动手了,面向测试用例编程即可。</p><h4 id="lab4a"><a href="#lab4a" class="headerlink" title="lab4a"></a>lab4a</h4><p>本部分是对 mvcc 模块的实现,主要涉及修改的代码文件是 transaction.go。需要利用对 CFLock, CFDefault 和 CFWrite 三个 CF 的一些操作来实现 mvcc。</p><p>针对 Lock 相关的函数:</p><ul><li>PutLock:将 PUT <key, lock.tobytes()> 添加到 Modify 即可。</key,></li><li>DeleteLock:将 Delete <key> 添加到 Modify 即可。</key></li><li>GetLock:在 CFLock 中查找即可。</li></ul><p>针对 Value 相关的函数:</p><ul><li>PutValue:将 PUT <EncodeKey(key, txn.startts), value> 添加到 Modify 即可。</EncodeKey(key,></li><li>DeleteValue:将 Delete <EncodeKey(key, txn.startts)> 添加到 Modify 即可。</EncodeKey(key,></li><li>GetValue:首先从 CFWrite 中寻找在当前快照之前已经提交的版本。如果未找到则返回空,如果找到则正对不同的 Kind 有不同的行为:<ul><li>Put:根据 value 中的 StartTS 去 CFDefault 寻找即可。</li><li>Delete:返回空即可。</li><li>Rollback:继续寻找之前的版本。</li></ul></li></ul><p>针对 Write 相关的函数:</p><ul><li>PutWrite:将 PUT <EncodeKey(key, committs), write.tobytes()> 添加到 Modify 即可。</EncodeKey(key,></li><li>CurrentWrite:从 CFWrite 当中寻找当前 key 对应且值的 StartTS 与当前事务 StartTS 相同的行。</li><li>MostRecentWrite:从 CFWrite 当中寻找当前 key 对应且值的 StartTS 最大的行。</li></ul><h4 id="lab4b"><a href="#lab4b" class="headerlink" title="lab4b"></a>lab4b</h4><p>本部分是对 Percolator 算法 KVPreWrite, KVCommit 和 KVGet 三个方法的实现,主要涉及修改的代码文件是 server.go, query.go 和 nonquery.go。</p><ul><li>KVPreWrite:针对每个 key,首先检验是否存在写写冲突,再检查是否存在行锁,如存在则需要根据所属事务是否一致来决定是否返回 KeyError,最后将 key 添加到 CFDefault 和 CFLock 即可。</li><li>KVCommit:针对每个 key,首先检查是否存在行锁,如不存在则已经 commit 或 rollback,如存在则需要根据 CFWrite 中的当前事务状态来判断是否返回 KeyError,最后将 key 添加到 CFWrite 中并在 CFLock 中删除即可。</li><li>KVGet:首先检查行锁,如为当前事务所锁,则返回 Error,否则调用 mvcc 模块的 GetValue 获得快照读即可。</li></ul><h4 id="lab4c"><a href="#lab4c" class="headerlink" title="lab4c"></a>lab4c</h4><p>本部分是对 Percolator 算法 KvCheckTxnStatus, KvBatchRollback, KvResolveLock 和 KvScan 四个方法的实现,主要涉及修改的代码文件是 server.go, query.go 和 nonquery.go。</p><ul><li>KvCheckTxnStatus:检查 PrimaryLock 的行锁,如果存在且被当前事务锁定,则根据 ttl 时间判断是否过期从而做出相应的动作;否则锁很已被 rollback 或者 commit,从 CFWrite 中获取相关信息即可。</li><li>KvBatchRollback:针对每个 key,首先检查是否存在行锁,如果存在则删除 key 在 CFLock 和 CFValue 中的数并且在 CFWrite 中写入一条 rollback 即可。如果不存在或者不归当前事务锁定,则从 CFWrite 中获取当前事务的提交信息,如果不存在则向 CFWrite 写入一条 rollback,如果存在则根据是否为 rollback 判断是否返回错误。</li><li>KvResolveLock:针对每个 key,根据请求中的参数决定来 commit 或者 rollback 即可。</li><li>KvScan:利用 Scanner 扫描到没有 key 或达到 limit 阈值即可。针对 scanner,需要注意不能读有锁的 key,不能读未来的版本,不能读已删除或者已 rollback 的 key。</li></ul><h4 id="代码结构"><a href="#代码结构" class="headerlink" title="代码结构"></a>代码结构</h4><p>为了使得 server.go 逻辑代码清晰,在分别完成三个 lab 后对代码进行了进一步整理,针对读写请求分别抽象出来了接口,这样可以使得逻辑更为清晰。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><code class="hljs Go"><span class="hljs-keyword">type</span> BaseCommand <span class="hljs-keyword">interface</span> {<br>Context() *kvrpcpb.Context<br>StartTs() <span class="hljs-type">uint64</span><br>}<br><br><span class="hljs-keyword">type</span> Base <span class="hljs-keyword">struct</span> {<br>context *kvrpcpb.Context<br>startTs <span class="hljs-type">uint64</span><br>}<br><br><span class="hljs-keyword">type</span> QueryCommand <span class="hljs-keyword">interface</span> {<br>BaseCommand<br>Read(txn *mvcc.MvccTxn) (<span class="hljs-keyword">interface</span>{}, <span class="hljs-type">error</span>)<br>}<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ExecuteQuery</span><span class="hljs-params">(cmd QueryCommand, storage storage.Storage)</span></span> (<span class="hljs-keyword">interface</span>{}, <span class="hljs-type">error</span>) {<br>ctx := cmd.Context()<br>reader, err := storage.Reader(ctx)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {<br><span class="hljs-keyword">return</span> &kvrpcpb.ScanResponse{RegionError: util.RaftstoreErrToPbError(err)}, <span class="hljs-literal">nil</span><br>}<br><span class="hljs-keyword">defer</span> reader.Close()<br><span class="hljs-keyword">return</span> cmd.Read(mvcc.NewMvccTxn(reader, cmd.StartTs()))<br>}<br><br><span class="hljs-keyword">type</span> NonQueryCommand <span class="hljs-keyword">interface</span> {<br>BaseCommand<br>IsEmpty() <span class="hljs-type">bool</span><br>GetEmptyResponse() <span class="hljs-keyword">interface</span>{}<br>WriteKeys(txn *mvcc.MvccTxn) ([][]<span class="hljs-type">byte</span>, <span class="hljs-type">error</span>)<br>Write(txn *mvcc.MvccTxn) (<span class="hljs-keyword">interface</span>{}, <span class="hljs-type">error</span>)<br>}<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ExecuteNonQuery</span><span class="hljs-params">(cmd NonQueryCommand, storage storage.Storage, latches *latches.Latches)</span></span> (<span class="hljs-keyword">interface</span>{}, <span class="hljs-type">error</span>) {<br><span class="hljs-keyword">if</span> cmd.IsEmpty() {<br><span class="hljs-keyword">return</span> cmd.GetEmptyResponse(), <span class="hljs-literal">nil</span><br>}<br><br>ctx := cmd.Context()<br>reader, err := storage.Reader(ctx)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {<br><span class="hljs-keyword">return</span> &kvrpcpb.ScanResponse{RegionError: util.RaftstoreErrToPbError(err)}, <span class="hljs-literal">nil</span><br>}<br><span class="hljs-keyword">defer</span> reader.Close()<br>txn := mvcc.NewMvccTxn(reader, cmd.StartTs())<br><br>keys, err := cmd.WriteKeys(txn)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br>}<br><br>latches.WaitForLatches(keys)<br><span class="hljs-keyword">defer</span> latches.ReleaseLatches(keys)<br><br>response, err := cmd.Write(txn)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br>}<br><br>err = storage.Write(ctx, txn.Writes())<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br>}<br><br>latches.Validation(txn, keys)<br><br><span class="hljs-keyword">return</span> response, <span class="hljs-literal">nil</span><br>}<br></code></pre></td></tr></table></figure><h3 id="相关知识学习-3"><a href="#相关知识学习-3" class="headerlink" title="相关知识学习"></a>相关知识学习</h3><p>有关分布式事务,我们之前有过简单的 <a href="https://tanxinyu.work/distributed-transactions/">学习</a>,对 2PL, 2PC 均有简单的了解,因此此次在实现 Percolator 时只需要关注 2PC 与 MVCC 的结合即可,这里重点参考了以下博客:</p><ul><li><a href="https://zhuanlan.zhihu.com/p/77846678">TiKV 源码解析系列文章(十二)分布式事务</a></li><li><a href="https://pingcap.com/zh/blog/tidb-transaction-model">TiKV 事务模型概览,Google Spanner 开源实现</a></li><li><a href="http://mysql.taobao.org/monthly/2018/11/02/">Google Percolator 分布式事务实现原理解读</a></li><li><a href="https://pingcap.com/zh/blog/async-commit-principle">Async Commit 原理介绍</a></li></ul><p>实现完后,我们进一步被 Google 的聪明所折服,Percolator 基于单行事务实现了多行事务,基于 MVCC 实现了 SI 隔离级别。尽管其事务恢复流程相对复杂,但其本质上是在 CAP 定理中通过牺牲恢复时的 A 来优化了协调者正常写入时的 A,即协调者单点在 SQL 层不用高可用来保证最终执行 commit 或者 abort。因为一旦协调者节点挂掉,该事务在超过 TTL (TTL 的超时也是由 TSO 的时间戳来判断,对于各个 TiKV 节点来说均为逻辑时钟,这样的设计也避免了 Wall Clock 的同步难题)后会被其他事务 rollback,总体上来看 Percolator 比较优雅的解决了 2PC 的 safety 问题。</p><p>当然,分布式事务可以深究的地方还很多,并且很多思想都与 Lamport 那篇最著名的论文 <a href="https://tanxinyu.work/time-clock-order-in-distributed-system-thesis/"><code>Time, Clocks, and the Ordering of Events in a Distributed System</code></a> 有关。除了 TiDB 外,Spanner,YugaByte,CockroachDB 等 NewSQL 数据库均有自己的大杀器,比如 TrueTime,HLC 等等。总之这块儿挺有意思的,虽然在这儿告一段落,但希望以后有机会能深入做一些相关工作。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>实现一个稳定的分布式系统实在是太有挑战太有意思啦。</p><p>感谢 PingCAP 社区提供如此优秀的课程!</p>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>2021 年 11 月 ~ 2022 年 1 月 ,<strong>PingCAP </strong>举办了第一届 <strong>Tale</summary>
<category term="分布式系统理论" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA/"/>
<category term="共识算法" scheme="https://tanxinyu.work/tags/%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/"/>
<category term="分布式存储" scheme="https://tanxinyu.work/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AD%98%E5%82%A8/"/>
</entry>
<entry>
<title>第一届九坤并行程序优化大赛总结</title>
<link href="https://tanxinyu.work/jiu-kun-parallel-program-optimization-contest/"/>
<id>https://tanxinyu.work/jiu-kun-parallel-program-optimization-contest/</id>
<published>2021-11-24T14:15:08.000Z</published>
<updated>2022-01-30T03:42:14.000Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2021 年 9 月,量化头部公司<strong>九坤</strong>举办了其第一届<strong>并行程序优化大赛</strong>,相关介绍可参考 <a href="https://mp.weixin.qq.com/s/iaurj-1191SahJZ9uSk7RA">推送</a>。赛题如下:</p><p><img src="/jiu-kun-parallel-program-optimization-contest/title.png" alt></p><p>赛题是 C/C++ 的 codebase,然而我与一起组队的两位同学基本都对 C/C++ 不太熟悉,于是我们起名叫做了”只会 JAVA 队”。作为三个在体系结构几乎一窍不通的小白,在一个多月断断续续的不到 10 次线下沟通中,我们逐渐对体系结构入了门,在 192 个队伍脱颖而出,并在决赛取得了第 4 名的成绩(PS:离苹果周边只差一步真的好可惜),具体可以参考 <a href="https://mp.weixin.qq.com/s/Ct3XwD6zR_qNpvqLbQPs4Q">总结推送</a>。</p><p><img src="/jiu-kun-parallel-program-optimization-contest/result.jpeg" alt></p><p>这里简单做一总结,贴一些当前的资料和想法,以备之后回忆和反思。</p><h2 id="赛题"><a href="#赛题" class="headerlink" title="赛题"></a>赛题</h2><h3 id="第一题"><a href="#第一题" class="headerlink" title="第一题"></a>第一题</h3><p>在深度学习中,卷积操作在神经网络中扮演了重要的作用。2015 年,Andrew Lavin 等人提出了快速计算卷积的算法 Winograd,通过降低计算复杂度,相比直接卷积的算法提升 4 倍效率,成为了深度学习中非常重要的一个算法。本次比赛的第一题就是优化 Winograd 算法。各参赛队的通过优化比赛方给出的 Winograd 算法代码,缩短其运行时间,提升该算法时间的每秒浮点计算次数(FLOPS)。</p><h3 id="第二题"><a href="#第二题" class="headerlink" title="第二题"></a>第二题</h3><p>金融数据是真正的“大数据”。每天市场上的交易会产生海量的数据,这些数据对于预测未来市场走势只非常重要。因此负责高速储存、读取这些数据的 IO 系统成为了行业内重要的一环。目前金融数据中常用 HDF5 文件系统库进行大规模的数据存储。本次比赛第二题要求各参赛队探索 HDF5 文件系统,通过一个跑分程序 h5bench 来完成 IO 系统的性能研究和调优。</p><h2 id="代码-amp-文档"><a href="#代码-amp-文档" class="headerlink" title="代码 & 文档"></a>代码 & 文档</h2><p>从赛题可以看到,此两题能够检验选手最大化压榨 CPU 和 IO 性能的能力。</p><p>有关赛题的代码和文档均已开源,可移步 <a href="https://github.com/Sunny-Island/winograd-onlyJava">此处</a> 查看。欢迎交流~</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>就 CPU 题目而言,我们此次尝试了以下优化和工具:</p><ul><li>算法优化:winograd4x3-3D</li><li>指令级并行:循环展开,分支预测</li><li>数据级并行:AVX128,256,512</li><li>线程级并行:OPENMP</li><li>编译器:尝试 gcc 不同版本,对比 llvm</li><li>内存排布:36*STRIDE</li><li>冒险尝试:merge_array</li><li>Profiling:perf</li></ul><p>其实这里有好多思想都已经在数据库领域存在了。比如向量化引擎,比如 codegen 的 llvm 优化,比如对 cache 友好的 push 查询引擎等等。</p><p>个人认为,数据库做到极致便是对硬件性能的一种体现。因此,一个优秀的数据库工程师应该对体系结构具有一定的了解,这样才有可能进一步压榨硬件性能,从而达到更好的数据库性能。</p><p>一直以来,我希望分布式数据库能够成为自己的一个标签。通过这次比赛,我意识到高性能计算也是一个很有趣且硬核的方向,其不仅能够给企业迅速带来真金白银的收益(节约成本),而且也是很多领域做到极致的一种出路。</p><p>希望未来还能有契机去进一步深挖此方向吧。</p>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>2021 年 9 月,量化头部公司<strong>九坤</strong>举办了其第一届<strong>并行程序优化大赛</strong>,相</summary>
<category term="高性能计算" scheme="https://tanxinyu.work/tags/%E9%AB%98%E6%80%A7%E8%83%BD%E8%AE%A1%E7%AE%97/"/>
</entry>
<entry>
<title>2021 中科院开源之夏总结</title>
<link href="https://tanxinyu.work/2021-summer-of-code/"/>
<id>https://tanxinyu.work/2021-summer-of-code/</id>
<published>2021-09-30T14:33:56.000Z</published>
<updated>2022-01-30T03:39:56.000Z</updated>
<content type="html"><![CDATA[<p>2021 年暑假,我参与了中科院组织的开源之夏活动,题目为 《Apache IoTDB 分布式混沌测试框架》。</p><p>有关该项目的详细信息可以查看该 <a href="https://gitlab.summer-ospp.ac.cn/summer2021/210070607">文档</a>。</p><p>从结果来看,这份工作发现了 Apache IoTDB 当前分布式版本存在的很多问题,有一些容易解决的问题已经得到了修复,然而也有一些较复杂的问题到今天依然存在,这也多多少少间接引起了我们的一次大规模重构,勉强算是一件有意义的工作吧。</p><p>令人略感遗憾的是,尽管该混沌测试框架在部署好之后可以用 Dashboard 的方式方便地注入特定的异常,然而正如项目文档中所说的,该框架依然是基于物理节点来实现的,很难做到自动化。</p><p><strong>如果没有测试人员去维护并定期手动测试,如果没有开发人员愿意抽出时间来完全解决其中发现的问题,如果整个团队没有足够重视异常场景下系统的对外表现并愿意为之付出大量的精力,该框架就很难形成正向反馈,最终只能被遗忘在历史的角落里。</strong></p><p>作为一点反省,我现在觉得混沌测试还是应该尽可能的通过持续集成的方式自动化起来(参照 ChaosMesh),这样释放人力的方式是大家都喜爱的,也只有这样,混沌测试才能对项目产生持续的正向收益。</p><p>随意写点儿感想,仅做记录。</p>]]></content>
<summary type="html"><p>2021 年暑假,我参与了中科院组织的开源之夏活动,题目为 《Apache IoTDB 分布式混沌测试框架》。</p>
<p>有关该项目的详细信息可以查看该 <a href="https://gitlab.summer-ospp.ac.cn/summer2021/21007</summary>
<category term="测试" scheme="https://tanxinyu.work/tags/%E6%B5%8B%E8%AF%95/"/>
<category term="IoTDB" scheme="https://tanxinyu.work/tags/IoTDB/"/>
</entry>
<entry>
<title>15-445 数据库课程学习总结</title>
<link href="https://tanxinyu.work/15-445/"/>
<id>https://tanxinyu.work/15-445/</id>
<published>2021-09-12T07:12:32.000Z</published>
<updated>2022-01-30T03:39:50.000Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>众所周知,CMU 15-445/721 是数据库的入门神课,类似于 MIT 6.824 之于分布式系统一样。由于前半年学习了 MIT 6.824 课程后感觉个人收获很大,因此在今年暑假,我抽时间学习完了 CMU 15-445 的网课,现做一概要总结。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>15-445 可以当做数据库的入门课程,授课老师是著名网红教授 <a href="http://www.cs.cmu.edu/~pavlo/">Andy Pavlo</a>,以下是他的 Google Scholar 主页,还是非常厉害的。<br><img src="/15-445/pavlo.png" alt></p><p>本课程的组织方式采用了自底向上的方式,分别介绍了文件管理,缓冲池管理,索引管理,执行管理,查询优化,并发控制和容错恢复等内容,基本讲述了如何从 0 实现一个单机关系型数据库。由于时间有限,没来得及做课程笔记。因此在参考资料部分列出了课程所有的 PPT 资料以及一些从网上找到的优质课程笔记,以备日后温习之用。</p><p><img src="/15-445/outline.png" alt></p><p>当然,由于课程内容涉及的范围很广,所以每个章节都只是进行了相对简单的介绍。要想了解更多细节,建议结合大黑砖《数据库系统概念》来学习。2021 年 6 月,最新第七版的中文译版已经发行,赶紧买一本镇脑吧!</p><p><img src="/15-445/database.jpeg" alt></p><p>对于其作业 <a href="https://github.com/cmu-db/bustub">bustub</a>,由于其需要基于 C++17 实现,而本人在目前没有太多的 C++ 知识储备,所以就暂时搁置了,毕竟想学的是数据库而不是 C++。不过我也注意到,MIT 6.830 数据库课程的作业 <a href="https://github.com/MIT-DB-Class/simple-db-hw-2021">simple-db</a> 是基于 Java 的,且其 6 个 lab 的内容基本覆盖了 CMU 15-445 lab 的内容,所以刷一刷 MIT 6.830 的 lab 也挺有意义的,希望自己后半年能抽出些时间吧。</p><p>此外,简单看了一下 15-721 的 <a href="https://15721.courses.cs.cmu.edu/spring2020/schedule.html">课程主页</a>,感觉其更多的是在讲 research 方向的工作,基本是在讲各个方向的 sota,那么这门课可以等到工作之后再说吧,目前来看优先级不是很高。</p><p><img src="/15-445/15-721.png" alt></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://15445.courses.cs.cmu.edu/fall2019/schedule.html">官网资料</a></li><li><a href="https://www.bilibili.com/video/BV1rN411f7Ef">网课视频</a></li><li><a href="https://zhenghe.gitbook.io/open-courses/cmu-15-445-645-database-systems/relational-data-model">课程笔记 1</a></li><li><a href="https://www.jianshu.com/nb/36265841">课程笔记 2</a></li></ul>]]></content>
<summary type="html"><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>众所周知,CMU 15-445/721 是数据库的入门神课,类似于 MIT 6.824 之于分布式系统一样。由于前半年学习了 MIT 6.8</summary>
<category term="数据库" scheme="https://tanxinyu.work/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="网红课" scheme="https://tanxinyu.work/tags/%E7%BD%91%E7%BA%A2%E8%AF%BE/"/>
</entry>
<entry>
<title>Awesome 学习资料分享</title>
<link href="https://tanxinyu.work/awesome-blog/"/>
<id>https://tanxinyu.work/awesome-blog/</id>
<published>2021-07-30T04:33:57.000Z</published>
<updated>2022-01-30T03:40:05.000Z</updated>
<content type="html"><![CDATA[<p>计划长期维护一个自己学习过且觉得不错的资料列表,希望自己不断更新:</p><ul><li><a href="https://github.com/fuzhengwei/itstack-demo-design">重学 Java 设计模式</a></li><li><a href="https://icyfenix.cn/introduction/about-the-fenix-project.html">什么是“凤凰架构”</a></li><li><a href="https://www.kancloud.cn/kancloud/a-programmer-prepares/78160">程序员的自我修养</a></li><li><a href="https://draveness.me/">为什么系列</a></li><li><a href="https://github.com/Vonng/ddia">DDIA</a></li><li><a href="https://netsecurity.51cto.com/art/202005/616765.htm">线上故障排查全套路</a></li></ul>]]></content>
<summary type="html"><p>计划长期维护一个自己学习过且觉得不错的资料列表,希望自己不断更新:</p>
<ul>
<li><a href="https://github.com/fuzhengwei/itstack-demo-design">重学 Java 设计模式</a></li>
<li><a h</summary>
<category term="开源" scheme="https://tanxinyu.work/tags/%E5%BC%80%E6%BA%90/"/>
</entry>
</feed>