-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeed.xml
More file actions
1698 lines (1293 loc) · 101 KB
/
feed.xml
File metadata and controls
1698 lines (1293 loc) · 101 KB
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
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Coffee, Coke and Code</title>
<description>Archive diabloneo's articles.
</description>
<link>http://diabloneo.github.io//</link>
<atom:link href="http://diabloneo.github.io//feed.xml" rel="self" type="application/rss+xml"/>
<pubDate>Thu, 11 Apr 2024 22:09:44 +0800</pubDate>
<lastBuildDate>Thu, 11 Apr 2024 22:09:44 +0800</lastBuildDate>
<generator>Jekyll v4.3.1</generator>
<item>
<title>基于 PostgreSQL 逻辑复制和 CDC 实现企业级分布式系统</title>
<description><p>本文是 2023 年 3 月 4 日在第 12 届 PostgreSQL 中国技术大会发表主题演讲《基于 PostgreSQL 逻辑复制和 CDC 实现企业级分布式系统》的文本内容 (数据库管理运维与最佳实践专场)</p>
<p>大会视频回放和 PPT 下载链接:<em><a href="https://mp.weixin.qq.com/s/4azY8cm9iA_mcKWPw1jhBw">https://mp.weixin.qq.com/s/4azY8cm9iA_mcKWPw1jhBw</a></em></p>
<h2 id="背景与调研">背景与调研</h2>
<h3 id="产品背景">产品背景</h3>
<p>XSKY 有 SDS 和 SDDC 两款产品,SDS 诞生于 2015 年,SDDC 诞生于 2021 年。这次分享的是 SDDC 产品的管理面的架构设计。</p>
<p>SDS 产品基于 Postgres 9.6,为了控制产品的复杂性,我们没有引入数据库消息队列组件。但是在产品中又得依赖于消息队列这样的机制,因此我们使用了两个方案:</p>
<ul>
<li>任务表 + 定时轮询
<ul>
<li>消息传递及时性较低</li>
</ul>
</li>
<li>Trigger
<ul>
<li>效率低,性能消耗大
-因为没有直接回调,还是需要依赖于定时轮询</li>
</ul>
</li>
</ul>
<h3 id="逻辑复制的相关概念">逻辑复制的相关概念</h3>
<p><strong>逻辑复制</strong></p>
<ul>
<li>逻辑复制是根据复制标识(通常是主键)复制数据对象及其变更的一种方法。</li>
<li>传送的是数据库的一种与存储格式无关的表达格式
<ul>
<li>允许跨 Postgres 版本传递数据</li>
<li>甚至允许向非 Postgres 程序传递数据</li>
</ul>
</li>
</ul>
<p><strong>CDC (Change Data Capture)</strong></p>
<ul>
<li>近实时捕获数据源的变更并且发送给下游的数据消费者</li>
<li>不能等价于消息队列
<ul>
<li>只能表达和数据源有关的数据变化</li>
<li>产生的是顺序事件,不能按照随意顺序消费</li>
</ul>
</li>
</ul>
<h3 id="逻辑复制调研">逻辑复制调研</h3>
<p><strong>调研过程</strong></p>
<ul>
<li>Postgres 10 开始</li>
<li>Postgres 13 开始预研
<ul>
<li>设计并实现了一个小项目,验证逻辑复制和 CDC 方案的可行性</li>
<li>向 <em>&lt;github.com/jackc/pglogrepl&gt;</em> 贡献 pgoutput 协议解析代码
By @diabloneo</li>
<li>性能测试
<ul>
<li>Postgres 13</li>
<li>Intel(R) Xeon(R) Gold 5218R CPU @ 2.10GHz</li>
<li>每分钟可以发送超过 50,000 个简单的事务</li>
</ul>
</li>
</ul>
</li>
<li>生产版本使用的是 Postgres 14</li>
</ul>
<p><strong>问题预判</strong></p>
<ul>
<li>逻辑事件只能包含部分的数据库操作
<ul>
<li>缺少的那些,在我们的系统里都可以通过带外的方式来解决。</li>
</ul>
</li>
<li>逻辑复制事件不会包含一行记录的所有内容。
<ul>
<li>我们只会依赖消息中的 id 和几个时间戳字段,整个记录的内容会使用 ORM 从数据库重新读取。</li>
</ul>
</li>
<li>事件丢失
我们一定要做好事件可能会丢失的准备,提供后备方案。</li>
<li>处理阻塞导致 WAL 写满的情况</li>
</ul>
<h2 id="架构设计">架构设计</h2>
<h3 id="整体架构">整体架构</h3>
<p><img src="http://diabloneo.github.io//assets/imgs/00039_architecture.png" alt="architecture" /></p>
<ul>
<li>API Server
<ul>
<li>负责和用户交互,并进行数据库读写</li>
<li>消费 LR 消息,用于发送 websocket 消息等</li>
</ul>
</li>
<li>Controller
<ul>
<li>消费 LR 消息,用于实现业务逻辑</li>
</ul>
</li>
<li>Agent
<ul>
<li>LR 消息会触发 informer 重新载入数据</li>
<li>会根据 cache 中的数据对业务做收敛</li>
</ul>
</li>
</ul>
<h3 id="app-设计">App 设计</h3>
<p><img src="http://diabloneo.github.io//assets/imgs/00040_app_1.png" alt="app_1" /></p>
<ul>
<li>代表一个业务逻辑的分组,例如虚拟机,块存储等</li>
<li>App 是在另外一个维度上把 apiserver, controller 和 agent 联系在了一起</li>
<li>API server 和 controller 之间使用 LR 作为联系方式</li>
<li>Controller 和 agent 之间使用 informer 作为联系方式</li>
</ul>
<p><img src="http://diabloneo.github.io//assets/imgs/00041_app_2.png" alt="app_2" /></p>
<ul>
<li>每个 app 会注册一个独立的 publication + slot</li>
<li>App 按照顺序处理自己订阅的事件</li>
<li>不同的 app 会处理同一个事件
<ul>
<li>更新数据库时,需要使用 etag 这类乐观锁机制</li>
<li>遇到 etag 冲突时,自动重试</li>
</ul>
</li>
</ul>
<h2 id="cdc-封装与应用">CDC 封装与应用</h2>
<h3 id="cdc-event">CDC Event</h3>
<p><img src="http://diabloneo.github.io//assets/imgs/00042_cdc_event_flow.png" alt="cdc_event_flow" /></p>
<ul>
<li>Postgres 的 LR 消息过于原始,不利于应用开发</li>
<li>Event and EventGroup
<ul>
<li>Event: Insert/Update 消息,Relation 用于触发一个 cache 的更新,Commit 被映射为 FlushLSN</li>
<li>EventGroup: 一个事务中的所有 数据操作 Event 的集合</li>
<li>CDC Manager 会将 LR 消息转化为对应的 ORM Model</li>
</ul>
</li>
</ul>
<h3 id="cdc-的应用">CDC 的应用</h3>
<ul>
<li>App
<ul>
<li>消费 event,根据 event 执行数据库的 update 操作</li>
</ul>
</li>
<li>API Client Manager
<ul>
<li>监听 Node 和 Service 资源的 event,对所管理的 API Client 进行操作:创建、删除、failover 等</li>
</ul>
</li>
<li>Websocket 通知
<ul>
<li>监听所有资源的 event,一旦资源有变动就可以发送 websocket 通知。</li>
</ul>
</li>
<li>Informer Monitor
<ul>
<li>监听所有资源的 event,通过 etcd 通知 agent reload 相关的缓存数据</li>
<li>Agent 不直接消费 CDC event 的原因
<ul>
<li>为了实现 agent 的 scale-out,agent 不直接访问数据库</li>
<li>Agent 中的 executor 需要一次载入某个时刻 (RR Transaction) 的多个表的数据</li>
<li>Executor 的运行需要综合定时器触发和 CDC event 触发等多种原因</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="http://diabloneo.github.io//assets/imgs/00043_informer_architecture_1.png" alt="informer_architecture_1" /></p>
<h2 id="关键问题处理">关键问题处理</h2>
<h3 id="slot-的管理--未使用-patroni-时">Slot 的管理 – 未使用 Patroni 时</h3>
<ul>
<li>在我们的 controller 程序中进行管理(在 controller leader 节点进行管理)。
<ul>
<li>基本的做法是在 controller 启动时,检查 app 对应的 slot 是否存在,如果不存在则创建。</li>
</ul>
</li>
</ul>
<p>存在三个问题:</p>
<ol>
<li>第一次启动耗时间较长,可能会丢 CDC event</li>
<li>Controller failover 时会漏掉一些 CDC event</li>
<li>Patroni 会尝试 drop 掉它不认识的 slot</li>
</ol>
<h3 id="slot-的管理--使用-patroni">Slot 的管理 – 使用 Patroni</h3>
<ul>
<li>我们将 slot 管理交给 Patroni 来做,同时解决了上面这些问题:
<ul>
<li>我们实现了一个 slot sync 命令,会在系统安装时通过 Patroni 创建好 slot。因为所有的程序都是在这个过程之后启动的,所以避免了 CDC 事件的丢失。</li>
<li>Patroni 管理 slots 后,它会在 primary/replicas 之间自动同步 slot 的 restart_lsn(10s 一次)。
<ul>
<li>Failover 后会收到重复的 CDC 事件,需要做幂等处理。</li>
</ul>
</li>
<li>所有 slot 受 Patroni 管控,所以 Patroni 也不会再去 drop slots。</li>
</ul>
</li>
</ul>
<h3 id="临时-slot-的应用">临时 slot 的应用</h3>
<ul>
<li>临时 slot
<ul>
<li>不会将 slot 的信息持久化</li>
<li>会话结束或者发生错误时,会自动销毁。</li>
</ul>
</li>
<li>使用场景
<ul>
<li>允许 CDC 事件丢失的场景
<ul>
<li>Websocket</li>
<li>API Client Manager</li>
</ul>
</li>
</ul>
</li>
<li>更换为临时 slot 的原因
<ul>
<li>非 Patroni 管理的持久化 slot,会被 Patroni 尝试 drop,会产生很多 log。</li>
<li>Patroni 在管理 slot 的时候,会过滤掉所有的临时 slot。</li>
</ul>
</li>
</ul>
<h3 id="升级">升级</h3>
<p><img src="http://diabloneo.github.io//assets/imgs/00044_upgrade.png" alt="upgrade" /></p>
<p>Publication 必须在 slot 之前创建,否则 subscribe 时,server 会报找不到 publication (Postgres 实现导致的):</p>
<p><a href="https://postgrespro.com/list/thread-id/2471266">Thread: How is this possible “publication does not exist”</a></p>
<h3 id="cdc-事件丢失">CDC 事件丢失</h3>
<ul>
<li>丢失原因
<ul>
<li>WAL 满了,drop 掉老数据</li>
<li>Bug</li>
</ul>
</li>
<li>CDC 事件重放机制
<ul>
<li>避免线上出现数据丢失的情况时,需要手动去做数据库操作</li>
<li>需要能够区分原始事件和重放的事件</li>
</ul>
</li>
</ul>
<h4 id="cdc-事件重放机制---insert">CDC 事件重放机制 - Insert</h4>
<p>数据库的 insert 事件只能通过插入新的记录来触发。如果我们要触发一个 insert 事件,那么就得把记录先删除,然后再插入一次。这个方案有几个问题:</p>
<ul>
<li>因为重新插入记录,所以无法区分一条记录是否被重放了。</li>
<li>删除记录会导致一条需要回收的记录产生,这会增大数据库的空间。虽然这个数量不会很多,但是还是尽量避免。</li>
</ul>
<p><strong>方案:在 model 中统一加入一个字段 CdcInserted ,类型是 *time.Time 。重放的流程如下:</strong></p>
<p><img src="http://diabloneo.github.io//assets/imgs/00045_cdc_replay_insert.png" alt="cdc_replay_insert" /></p>
<h4 id="cdc-事件重放机制---update">CDC 事件重放机制 - Update</h4>
<p>Update event 的重放其实可以直接通过更新 UpdatedAt 字段来触发,不过这样不会保存重放的记录,也无法标记是一个重放的事件。</p>
<p><strong>方案:在 model 中统一加入一个字段 CdcUpdated ,类型是 *time.Time 。重放的流程如下:</strong></p>
<p><img src="http://diabloneo.github.io//assets/imgs/00046_cdc_replay_update.png" alt="cdc_replay_update" /></p>
<h4 id="cdc-事件重放机制--replay-命令">CDC 事件重放机制 – replay 命令</h4>
<p>为了可以在线上方便的进行操作,我们按照资源的视角开发了一个命令行工具:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sddc-manage cdc replay --help
NAME:
manage cdc replay - Replay CDC events
USAGE:
manage cdc replay [command options] [arguments...]
OPTIONS:
--object-type value, -t value Object type to be replayed (required)
--event value, -e value Replay event type (required) (insert|update)
--id value Filter: ID of object to be replayed
--from-id value Filter: Object id &gt;= from-id will be replayed
--to-id value Filter: Object id &lt;= to-id will be replayed
--help, -h show help
$ sddc-manage cdc replay --id 1 -t VmImageSpec -e insert
$ sddc-manage cdc replay --id 1 -t VirtualMachineSpec -e update
</code></pre></div></div>
<h2 id="总结">总结</h2>
<h3 id="运行数据">运行数据</h3>
<p>当前版本的 slot 数量:</p>
<ul>
<li>Persistent: 40</li>
<li>Temporary: 6</li>
</ul>
<p>一个内部使用的生产环境。Controller leader 运行了 18 天,接收的 LR 消息数量:</p>
<ul>
<li>Update: 966961</li>
<li>Insert: 6276</li>
<li>Relation: 1149</li>
</ul>
<h3 id="总结-1">总结</h3>
<p>亮点:</p>
<ul>
<li>Postgres 的逻辑复制很适合在分布式系统中使用
<ul>
<li>可以在很大程度上免去对消息队列的使用,简化系统架构</li>
<li>性能不错</li>
<li>稳定性不错</li>
</ul>
</li>
<li>Golang 的生态对于逻辑复制的支持已经比较不错</li>
</ul>
<p>需要注意的地方:</p>
<ul>
<li>需要了解逻辑复制的原理,并且能够管理 publication 和 slot</li>
<li>消费 LR 消息的时候,尽可能的不阻塞,避免 WAL 被 drop</li>
<li>需要理解 CDC 的思想,不能将逻辑复制当成消息队列来使用</li>
<li>LR 消息的消费者,尽可能实现幂等操作</li>
</ul>
</description>
<pubDate>Mon, 20 Mar 2023 00:00:00 +0800</pubDate>
<link>http://diabloneo.github.io//2023/03/20/PGConfChina-2023-logical-replication-and-cdc/</link>
<guid isPermaLink="true">http://diabloneo.github.io//2023/03/20/PGConfChina-2023-logical-replication-and-cdc/</guid>
<category>database</category>
<category>distributed-computing</category>
</item>
<item>
<title>Sysfs Note -- 2</title>
<description><p>本文的内容对于一个 block device 设备的 sysfs 文件,如何找到对应的代码。</p>
<h2 id="kobject-的相关函数">kobject 的相关函数</h2>
<p>在 kernel 的文档 <em>Documentation/core-api/kobject.rst</em> 中,可以读到 kobject 操作的一些函数,对于阅读内核代码来说,<code class="language-plaintext highlighter-rouge">kobject_add</code> 函数是比较重要的,它表示添加了一个目录到 sysfs 下。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
</code></pre></div></div>
<h2 id="如何找到一个-block-device-sysfs-文件对应的操作代码">如何找到一个 block device sysfs 文件对应的操作代码</h2>
<h3 id="queue-目录"><em>queue</em> 目录</h3>
<p>Sysfs 中的目录对应的是 kobject,该目录下的文件对应的是这个 kobject 的属性。</p>
<p>我们以一个 nvme 硬盘的 <em>queue/discard_max_bytes</em> 文件为例,来找到对应的代码。首先在 block 目录下搜索 <code class="language-plaintext highlighter-rouge">kobject_add</code> 的调用,找到将 queue 添加到 sysfs 的地方,在 <em>block/blk-sysfs.c</em> 文件中</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**
* blk_register_queue - register a block layer queue with sysfs
* @disk: Disk of which the request queue should be registered with sysfs.
*/
int blk_register_queue(struct gendisk *disk)
{
struct request_queue *q = disk-&gt;queue;
int ret;
mutex_lock(&amp;q-&gt;sysfs_dir_lock);
kobject_init(&amp;disk-&gt;queue_kobj, &amp;blk_queue_ktype);
ret = kobject_add(&amp;disk-&gt;queue_kobj, &amp;disk_to_dev(disk)-&gt;kobj, "queue");
if (ret &lt; 0)
goto out_put_queue_kobj;
</code></pre></div></div>
<p>然后你可以搜索调用 <code class="language-plaintext highlighter-rouge">blk_register_queue</code> 的地方,是 <code class="language-plaintext highlighter-rouge">device_add_disk</code> 函数,然后再到 nvme 驱动目录下搜索调用 <code class="language-plaintext highlighter-rouge">device_add_disk</code> 的地方,最终你会得到一个调用链:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvme_scan_work
-&gt; nvme_scan_ns_list
-&gt; nvme_scan_ns
-&gt; nvme_alloc_ns
-&gt; device_add_disk
-&gt; blk_register_queue
</code></pre></div></div>
<p>简单的说,驱动发现一个 nvme 硬盘后,就会将信息添加到内核中,然后会在 sysfs 里创建这个硬盘的下的 queue 目录。</p>
<h3 id="queuediscard_max_bytes-文件"><em>queue/discard_max_bytes</em> 文件</h3>
<p>我们以 <em>queue/discard_max_bytes</em> 属性为例,来看看它是如何在代码中实现的。</p>
<h4 id="device_attribute">device_attribute</h4>
<p>首先,了解一下 <code class="language-plaintext highlighter-rouge">attribute</code>。 <code class="language-plaintext highlighter-rouge">attribute</code> 是定义用来对应一个 sysfs 中的文件,即 kobject 的一个属性的。<code class="language-plaintext highlighter-rouge">attribute</code> 单独使用时没有意义,一般需要加上操作函数,所以 block device 自己定义了如下的 <code class="language-plaintext highlighter-rouge">device_attribute</code> ( <em>include/linux/device.h</em> ):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
</code></pre></div></div>
<p>这个是设备目录下通用的定义,除了基本的属性外,还定义了 <code class="language-plaintext highlighter-rouge">show</code> 和 <code class="language-plaintext highlighter-rouge">store</code> 两个方法属性,顾名思义,一个是用来展示,一个是用来设置的,也就对应到了一个 sysfs 文件的读写操作。</p>
<h4 id="discard_max_bytes-属性">discard_max_bytes 属性</h4>
<p>找代码的方式是在 <em>block/</em> 目录下搜索 <strong>discard_max_bytes</strong>,你就可以找到很多相关的代码。下面这行代码说明这个是 queue/ 目录下的一个 RW 属性:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QUEUE_RW_ENTRY(queue_discard_max, "discard_max_bytes");
QUEUE_RW_ENTRY 这个宏的定义如下:
#define QUEUE_RW_ENTRY(_prefix, _name) \
static struct queue_sysfs_entry _prefix##_entry = { \
.attr = { .name = _name, .mode = 0644 }, \
.show = _prefix##_show, \
.store = _prefix##_store, \
};
</code></pre></div></div>
<p>可以看到注册的函数名字应该是 <code class="language-plaintext highlighter-rouge">queue_discard_max_show</code> 和 <code class="language-plaintext highlighter-rouge">queue_discard_max_store</code>。</p>
<p>属性的 <code class="language-plaintext highlighter-rouge">show</code> 函数是被 <code class="language-plaintext highlighter-rouge">queue_attr_show</code> 函数使用的,<code class="language-plaintext highlighter-rouge">store</code> 函数则是被 <code class="language-plaintext highlighter-rouge">queue_attr_store</code> 函数使用:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static ssize_t
queue_attr_show(struct kobject *kobj, struct attribute *attr, char *page)
{
struct queue_sysfs_entry *entry = to_queue(attr);
struct gendisk *disk = container_of(kobj, struct gendisk, queue_kobj);
struct request_queue *q = disk-&gt;queue;
ssize_t res;
if (!entry-&gt;show)
return -EIO;
mutex_lock(&amp;q-&gt;sysfs_lock);
res = entry-&gt;show(q, page); /* 这行是调用各个属性 show 函数的地方 */
mutex_unlock(&amp;q-&gt;sysfs_lock);
return res;
}
static ssize_t
queue_attr_store(struct kobject *kobj, struct attribute *attr,
const char *page, size_t length)
{
struct queue_sysfs_entry *entry = to_queue(attr);
struct gendisk *disk = container_of(kobj, struct gendisk, queue_kobj);
struct request_queue *q = disk-&gt;queue;
ssize_t res;
if (!entry-&gt;store)
return -EIO;
mutex_lock(&amp;q-&gt;sysfs_lock);
res = entry-&gt;store(q, page, length); /* 这行是调用各个属性 store 函数的地方 */
mutex_unlock(&amp;q-&gt;sysfs_lock);
return res;
}
</code></pre></div></div>
<p>回过头来看 <code class="language-plaintext highlighter-rouge">queue_discard_max_show</code> 和 <code class="language-plaintext highlighter-rouge">queue_discard_max_store</code>。</p>
<p><code class="language-plaintext highlighter-rouge">show</code> 函数实现比较简单,就是将内容打印到提供的字符数组里:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static ssize_t queue_discard_max_show(struct request_queue *q, char *page)
{
return sprintf(page, "%llu\n",
(unsigned long long)q-&gt;limits.max_discard_sectors &lt;&lt; 9);
}
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">store</code> 函数比较长一点,但是逻辑也简单:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static ssize_t queue_discard_max_store(struct request_queue *q,
const char *page, size_t count)
{
unsigned long max_discard;
ssize_t ret = queue_var_store(&amp;max_discard, page, count);
if (ret &lt; 0)
return ret;
if (max_discard &amp; (q-&gt;limits.discard_granularity - 1))
return -EINVAL;
max_discard &gt;&gt;= 9; /* 512 bytes 一个 sector */
if (max_discard &gt; UINT_MAX)
return -EINVAL;
if (max_discard &gt; q-&gt;limits.max_hw_discard_sectors)
max_discard = q-&gt;limits.max_hw_discard_sectors;
/*
* 上面都是关于数据合法性的判断,这里做了设置
* 从这里就可以知道,后面要知道设置的 discard_max_bytes 如何使用,
* 要在代码里搜索 max_discard_sectors
*/
q-&gt;limits.max_discard_sectors = max_discard;
return ret;
}
</code></pre></div></div>
<h2 id="summary">Summary</h2>
<p>在 kernel 中查找 sysfs 的对应代码还是比较容易的,代码的接口都很规范,而且容易查找。
但是可以发现,sysfs 中的属性的名字和代码中的成员名字不一定一致,所以掌握这个代码查找技能是很必要的。</p>
</description>
<pubDate>Fri, 17 Mar 2023 00:00:00 +0800</pubDate>
<link>http://diabloneo.github.io//2023/03/17/sysfs-note-2/</link>
<guid isPermaLink="true">http://diabloneo.github.io//2023/03/17/sysfs-note-2/</guid>
<category>Linux</category>
</item>
<item>
<title>Sysfs Note -- 1</title>
<description><h2 id="sysfs">sysfs</h2>
<p><strong>sysfs</strong> 是一个内存文件系统,用于将内核的数据结构暴露出来。</p>
<p>任何 kobject 在系统中注册,就会有一个目录在 sysfs 中被创建出来。这个目录是作为 kobject 的父对象所在的目录的子目录创建的。每个 kobject 的属性,会以目录中的普通文件的形式出现。</p>
<h2 id="kobject">kobject</h2>
<p>Kobject 是一个类型为 <code class="language-plaintext highlighter-rouge">struct kobject</code> 的对象,它包含 name, ref count, parent pointer 等,使得 kobject 可以被组织成层次结构,并且通过 sysfs 暴露出来。Kobject 本身没有包含其他具体的功能,它主要通过被嵌入到其他的结构体中来使用。</p>
<h2 id="uevent">uevent</h2>
<p>Sysfs 的 device 目录下有个 <em>*uevent</em> 文件。uevent 的是 udev event 的缩写。</p>
<p>uevent 这个文件可读可写,当你读这个文件的时候,会返回这个设备最后一次被 kernel 发送的 udev 事件(这里就是这个设备被 add event,remove 的不会存在,因为 remove 发生后,整个设备目录都没了)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@centos8 block]# cat nvme0n1/uevent
MAJOR=259
MINOR=0
DEVNAME=nvme0n1
DEVTYPE=disk
DISKSEQ=1
[root@centos8 block]# cat sdc/uevent
MAJOR=8
MINOR=32
DEVNAME=sdc
DEVTYPE=disk
DISKSEQ=4
</code></pre></div></div>
<p>uevent 本身是一个 kernel 的机制,用于通知用户态的 udev 程序,关于设备的相关事件。在新的系统中,用户态的程序是在 systemd 中实现的,即 <strong>systemd-udevd</strong> :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@centos8 block]# systemctl cat systemd-udevd
# /usr/lib/systemd/system/systemd-udevd.service
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
ConditionPathIsReadWrite=/sys
[Service]
Type=notify
OOMScoreAdjust=-1000
Sockets=systemd-udevd-control.socket systemd-udevd-kernel.socket
Restart=always
RestartSec=0
ExecStart=/usr/lib/systemd/systemd-udevd
KillMode=mixed
WatchdogSec=3min
TasksMax=infinity
PrivateMounts=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallFilter=@system-service @module @raw-io
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
LockPersonality=yes
</code></pre></div></div>
<p>这个程序并不是通过逐个读取 sysfs 的 uevent 文件的内容来获取信息的,而是通过 <strong>uevent netlink</strong> 来获得内核发送的 uevent:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@centos8 block]# ps -ef | grep systemd-udevd
root 1830 1 0 Feb18 ? 00:00:01 /usr/lib/systemd/systemd-udevd
root 807799 3741741 0 23:03 pts/93 00:00:00 grep --color=auto systemd-udevd
[root@centos8 block]# lsof -p 1830 | grep -i uevent
systemd-u 1830 root 3u netlink 0t0 92397 KOBJECT_UEVENT
</code></pre></div></div>
<p><em>include/uapi/linux/netlink.h</em> 文件中定义了这个 netlink 类型</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
</code></pre></div></div>
<p><strong>既然 udev 是通过 <code class="language-plaintext highlighter-rouge">NETLINK_KOBJECT_UEVENT</code> 来接收事件的,为什么还需要 uevent 文件呢?</strong></p>
<p>这个是用于解决开机启动以及 udevd 重启的情况。当 udevd 启动时,它会扫描 sysfs 中所有设备的 uevent 文件,然后针对每个文件写入一个 ADD 内容,这样就可以触发 kernel 再一次通知和该设备有关的事件,然后 udevd 就可以收到该事件,并进行 udev rule 的处理。</p>
<h2 id="ref">Ref</h2>
<ul>
<li><em><a href="https://documentation.suse.com/sles/12-SP5/html/SLES-all/cha-udev.html">https://documentation.suse.com/sles/12-SP5/html/SLES-all/cha-udev.html</a></em></li>
<li><em><a href="https://lwn.net/Articles/646617/">https://lwn.net/Articles/646617/</a></em></li>
<li><em><a href="https://unix.stackexchange.com/questions/550037/how-does-udev-uevent-work">https://unix.stackexchange.com/questions/550037/how-does-udev-uevent-work</a></em></li>
</ul>
</description>
<pubDate>Wed, 01 Mar 2023 00:00:00 +0800</pubDate>
<link>http://diabloneo.github.io//2023/03/01/sysfs-note-1/</link>
<guid isPermaLink="true">http://diabloneo.github.io//2023/03/01/sysfs-note-1/</guid>
<category>Linux</category>
</item>
<item>
<title>Kubernetes 代码笔记 -- 2</title>
<description><h2 id="apimachinery-中的概念">apimachinery 中的概念</h2>
<p>Kubernetes 的 api 相关代码中有很多概念都是 k8s 独有的,需要专门理解一下,才方便研究 k8s 代码。</p>
<p>Kubebuilder 项目有一篇文章比较好的介绍了这些关键概念的理解,可以先阅读一下:<em><a href="https://book.kubebuilder.io/cronjob-tutorial/gvks.html">https://book.kubebuilder.io/cronjob-tutorial/gvks.html</a></em>。我这里写的是我个人的理解。</p>
<h3 id="gvk-groupversionkind">GVK: <code class="language-plaintext highlighter-rouge">GroupVersionKind</code></h3>
<p><em>k8s.io/apimachinery/pkg/runtime/schema/group_version.go</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion
// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
type GroupVersionKind struct {
Group string
Version string
Kind string
}
</code></pre></div></div>
<p>这个结构体包含了 API 的 group, version 和 kind 信息。这里的 kind 是对应的 Go 结构体的 type 名称。比如 <strong>StatefulSet</strong> 就是:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}
</code></pre></div></div>
<h3 id="gvr-groupversionresource">GVR: <code class="language-plaintext highlighter-rouge">GroupVersionResource</code></h3>
<p><em>k8s.io/apimachinery/pkg/runtime/schema/group_version.go</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
type GroupVersionResource struct {
Group string
Version string
Resource string
}
</code></pre></div></div>
<p>比如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"}
</code></pre></div></div>
<p>这个结构体包含了 API 的 group, version 和 resource 信息。这里的 resource 对应的是 API 路径里的名字。很容易会搞混 resource 和 kind 的区别,我觉得可以这么理解:</p>
<ul>
<li>Resource 是 API 侧的概念,是根据 API 路径推导出来的资源类型名称,例如 pods, deployments 等(下面会说单复数的问题)。</li>
<li>Kind 是 API 路径里得到这个资源类型名称所对应的 Go 的结构体的 type 名称。</li>
</ul>
<p>在现有的代码中,GVR 在 apiserver 端是比较少使用的,反而是在 controller 和 client 中会用得多一些。</p>
<h4 id="apiserver-中的使用">apiserver 中的使用</h4>
<p>下面这个函数中会添加 API 请求的 handler。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-&gt; k8s.io/apiserver/pkg/endpoints/installer.go: func (a *APIInstaller) registerResourceHandlers()
</code></pre></div></div>
<p>因为 <code class="language-plaintext highlighter-rouge">APIInstaller</code> 中已经包含了 <code class="language-plaintext highlighter-rouge">APIGroupVersion</code>,所以在添加的过程中,可以根据 <code class="language-plaintext highlighter-rouge">GroupVersion</code> 直接得到 GVK:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> fqKindToRegister, err := GetResourceKind(a.group.GroupVersion, storage, a.group.Typer)
if err != nil {
return nil, nil, err
}
...
reqScope := handlers.RequestScope{
# 这里也生成了 GVR
Resource: a.group.GroupVersion.WithResource(resource),
}
</code></pre></div></div>
<h4 id="restmapper"><code class="language-plaintext highlighter-rouge">RESTMapper</code></h4>
<p>其他地方的使用更多的是依赖于 <code class="language-plaintext highlighter-rouge">RESTMapper</code> 来根据 GVR 获得 GVK。</p>
<p>有好几种 <code class="language-plaintext highlighter-rouge">RESTMapper</code>,默认的如下:</p>
<p><em>k8s.io/apimachinery/pkg/api/meta/restmapper.go</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// DefaultRESTMapper exposes mappings between the types defined in a
// runtime.Scheme. It assumes that all types defined the provided scheme
// can be mapped with the provided MetadataAccessor and Codec interfaces.
//
// The resource name of a Kind is defined as the lowercase,
// English-plural version of the Kind string.
// When converting from resource to Kind, the singular version of the
// resource name is also accepted for convenience.
//
// TODO: Only accept plural for some operations for increased control?
// (`get pod bar` vs `get pods bar`)
type DefaultRESTMapper struct {
defaultGroupVersions []schema.GroupVersion
resourceToKind map[schema.GroupVersionResource]schema.GroupVersionKind
kindToPluralResource map[schema.GroupVersionKind]schema.GroupVersionResource
kindToScope map[schema.GroupVersionKind]RESTScope
singularToPlural map[schema.GroupVersionResource]schema.GroupVersionResource
pluralToSingular map[schema.GroupVersionResource]schema.GroupVersionResource
}
</code></pre></div></div>
<p>从它的内容可以看出,它是在 resource 和 kind 之间做映射的。同时,它还指出了,resource name 是根据 kind 来的,小写且是复数。不过,为了方便,也支持单数形式的 resource name。</p>
<p><code class="language-plaintext highlighter-rouge">DefaultRESTMapper</code> 实现了 <code class="language-plaintext highlighter-rouge">RESTMapper</code> interface。这个 interface 定义了一些方法用来实现转换,比如 <code class="language-plaintext highlighter-rouge">KindFor</code> 根据 GVR 得到 GVK:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> // KindFor takes a partial resource and returns the single match. Returns an error if there are multiple matches
KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)
</code></pre></div></div>
<p>根据使用场景补充,k8s 中还实现了好几个不同的 RESTMapper,比如 <code class="language-plaintext highlighter-rouge">MultiRESTMapper</code>, <code class="language-plaintext highlighter-rouge">DefferedDiscoveryRESTMapper</code> 等。</p>
<h3 id="scheme"><code class="language-plaintext highlighter-rouge">Scheme</code></h3>
<p><em>k8s.io/apimachinery/pkg/runtime/scheme.go</em></p>
<p><code class="language-plaintext highlighter-rouge">Scheme</code> 的主要工作就是保存 Go 类型和对应的 API 信息之间的关系。通过它的一些成员可以看出它的设计目标就是保存这种映射关系:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type Scheme struct {
// gvkToType allows one to figure out the go type of an object with
// the given version and name.
gvkToType map[schema.GroupVersionKind]reflect.Type
// typeToGVK allows one to find metadata for a given go object.
// The reflect.Type we index by should *not* be a pointer.
typeToGVK map[reflect.Type][]schema.GroupVersionKind
...
}
</code></pre></div></div>
<p>一般来说,一大堆的 API 可以共用一个 <code class="language-plaintext highlighter-rouge">Scheme</code>,比如 legacy API 都是共用下面这个文件中的 <code class="language-plaintext highlighter-rouge">Scheme</code> 对象:<em>pkg/api/legacyscheme/scheme.go</em>。</p>
<p>代码中一般是使用 <code class="language-plaintext highlighter-rouge">Scheme</code> 对象的 <code class="language-plaintext highlighter-rouge">AddKnownTypes</code> 方法把 Go 对象添加到 <code class="language-plaintext highlighter-rouge">Scheme</code> 中的。搜索这个方法可以找到 API 对象被添加的路径。以 rbac 为例:</p>
<p><em>pkg/apis/rbac/register.go</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GroupName is the name of this API group.
const GroupName = "rbac.authorization.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
// SchemeBuilder is a function that calls Register for you.
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&amp;Role{},
&amp;RoleBinding{},
&amp;RoleBindingList{},
&amp;RoleList{},
&amp;ClusterRole{},
&amp;ClusterRoleBinding{},
&amp;ClusterRoleBindingList{},
&amp;ClusterRoleList{},
)
return nil
}
</code></pre></div></div>
<p>另外,你可以根据上面代码中的 <code class="language-plaintext highlighter-rouge">AddToScheme</code> 方法推导出:当这个方法被调用时,就会执行这些添加操作。因此,也可以在代码中搜索 <code class="language-plaintext highlighter-rouge">rbac.*AddToScheme</code> 来找到添加的地方:</p>
<p><em>pkg/apis/rbac/install/install.go</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func init() {
Install(legacyscheme.Scheme)
}
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(rbac.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion))
}
</code></pre></div></div>
<p>所以,只要这个 pkg 被 import,rbac 的这些信息就会被注册到 legacy 的 <code class="language-plaintext highlighter-rouge">Scheme</code> 中。在这个 API Group 的 storage 被初始化的时候,这个 pkg 就会被 import:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-&gt; pkg/registry/rbac/rest/storage_rest.go: func (p RESTStorageProvider) NewRESTStorage()
</code></pre></div></div>
</description>
<pubDate>Mon, 13 Feb 2023 00:00:00 +0800</pubDate>
<link>http://diabloneo.github.io//2023/02/13/kubernetes-code-note-2/</link>
<guid isPermaLink="true">http://diabloneo.github.io//2023/02/13/kubernetes-code-note-2/</guid>
<category>kubernetes</category>
</item>
<item>
<title>Kubernetes 代码笔记 -- 1</title>
<description><h2 id="apiserver-中的路由注册">apiserver 中的路由注册</h2>
<h3 id="在哪里进行的路由注册">在哪里进行的路由注册?</h3>
<p>我们以 core API 为例 (也称为 legacy API),kube-apiserver 从启动开始,到开始注册 go-restful 之前的代码路径是:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-&gt; cmd/kube-apiserver/app/server.go: func CreateServerChain()
-&gt; cmd/kube-apiserver/app/server.go: func CreateKubeAPIServer()
-&gt; pkg/controlplane/instance.go: func (c *completedConfig) New()
-&gt; k8s.io/apiserver/pkg/server/config.go func (c completedConfig) New()
-&gt; k8s.io/apiserver/pkg/server/handler.go NewAPIServerHandler()
# 这里会初始化 restful.Container
-&gt; k8s.io/apiserver/pkg/server/config.go installAPI()
# 这里会添加 profile, metric 等固定的 API
-&gt; pkg/controlplane/instance.go: func (m *Instance) InstallLegacyAPI()
-&gt; k8s.io/apiserver/pkg/server/genericapiserver.go: func (s *GenericAPIServer) InstallLegacyAPIGroup()
-&gt; k8s.io/apiserver/pkg/server/genericapiserver.go: func (s *GenericAPIServer) installAPIResources()
-&gt; k8s.io/apiserver/pkg/endpoints/groupversion.go: func (g *APIGroupVersion) InstallREST()
-&gt; k8s.io/apiserver/pkg/endpoints/installer.go: func (a *APIInstaller) Install()
# 这里会创建 restful.WebService 对象
-&gt; k8s.io/apiserver/pkg/endpoints/installer.go: func (a *APIInstaller) registerResourceHandlers()
# 这个函数很长,大概有 800 行,就是根据 API 对象的信息,向 restful.WebService 中添加路由。
# 将得到的 restful.WebService 添加到 restful.Container 中。
</code></pre></div></div>
<p>上面是大概的流程结束之后,就会开始运行 apiserver,大概流程是如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-&gt; cmd/kube-apiserver/app/server.go: Run()
-&gt; k8s.io/kube-aggregator/pkg/apiserver/apiserver.go: func (s *APIAggregator) PrepareRun()
-&gt; k8s.io/apiserver/pkg/server/genericapiserver.go: func (s *GenericAPIServer) PrepareRun()
-&gt; k8s.io/kube-aggregator/pkg/apiserver/apiserver.go: func (s preparedAPIAggregator) Run()
-&gt; k8s.io/apiserver/pkg/server/genericapiserver.go: func (s preparedGenericAPIServer) Run()
# 这里最终根据 GenericAPIServer.APIServerHandler 来创建 http server
# GenericAPIServer.APIServerHandler 则会将请求路由到它内部的 restful.Container 中,
# 这个 container 包含了我们注册的 API
</code></pre></div></div>
<h3 id="注册了哪些路由">注册了哪些路由?</h3>
<p>上一小节提到了,每个资源的 <code class="language-plaintext highlighter-rouge">restful.WebService</code> 中注册的路由都在如下方法中实现:</p>
<p><code class="language-plaintext highlighter-rouge">k8s.io/apiserver/pkg/endpoints/installer.go: func (a *APIInstaller) registerResourceHandlers()</code></p>
<p>这个函数的主要工具就是根据 <code class="language-plaintext highlighter-rouge">APIGroupVersion</code> 的信息生成需要添加到 <code class="language-plaintext highlighter-rouge">restful.WebService</code> 中的 route 内容,最主要部分就是指定 <code class="language-plaintext highlighter-rouge">path</code> 和 <code class="language-plaintext highlighter-rouge">handler</code>,如下代码所示:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> route := ws.GET(action.Path).To(handler).
</code></pre></div></div>
<p>因为 kubernetes 的所有资源的 API 都是统一的,所以你可以在这个函数里看到所有 API 的路由实现。</p>
<h3 id="路由的-handler-在哪里">路由的 handler 在哪里?</h3>
<p>找到一个路由后,我们就知道了 path,解析来还需要知道它是如何被 handle 的,也就是要找到 handler 的实现。</p>
<p>只要你继续跟进 <code class="language-plaintext highlighter-rouge">func (a *APIInstaller) registerResourceHandlers()</code> 的代码,就会发现,所有的 handler 都是在</p>
<p><code class="language-plaintext highlighter-rouge">k8s.io/apiserver/pkg/endpoints/handlers</code> 这个模块中实现的。比如资源的 List 接口,就是在如下位置实现:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>k8s.io/apiserver/pkg/endpoints/handlers/get.go: func ListResource()
</code></pre></div></div>
<p>在这个函数里,你可以看到 List 的实现,以及 Watch 的实现。</p>
<h3 id="handler-和资源的实现是如何关联起来的">Handler 和资源的实现是如何关联起来的?</h3>
<p>上面提到的功能,都是 apiserver 统一实现的,也就是说,每个资源都不需要自己实现这些部分。每个资源需要实现的部分,主要是数据操作部分。</p>
<h4 id="registry">Registry</h4>
<p>这就要提到 <strong>registry</strong> 这个概念了,这个 registry 是 kubernetes 项目内部的代码上的概念,不是容器镜像那个概念。</p>
<p>在代码中可以找到这个概念的官方说明:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Package registry contains the generic implementation of the storage and system logic.
package registry // import "k8s.io/apiserver/pkg/registry"
</code></pre></div></div>
<p>再简化一点的说,就是 kubernetes 项目中的 model 层。因为 k8s 使用 etcd 作为存储,所以就是一个使用 etcd 作为存储的 model 层。</p>
<h4 id="storage-interface">Storage Interface</h4>
<p><em>k8s.io/apiserver/pkg/registry/rest/rest.go</em> 这个文件定义了存储的接口,代码中的一段注释说明了这个接口的定义:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Storage interfaces need to be separated into two groups; those that operate
// on collections and those that operate on individually named items.
// Collection interfaces:
// (Method: Current -&gt; Proposed)
// GET: Lister -&gt; CollectionGetter
// WATCH: Watcher -&gt; CollectionWatcher
// CREATE: Creater -&gt; CollectionCreater
// DELETE: (n/a) -&gt; CollectionDeleter
// UPDATE: (n/a) -&gt; CollectionUpdater
//
// Single item interfaces:
// (Method: Current -&gt; Proposed)
// GET: Getter -&gt; NamedGetter
// WATCH: (n/a) -&gt; NamedWatcher
// CREATE: (n/a) -&gt; NamedCreater
// DELETE: Deleter -&gt; NamedDeleter
// UPDATE: Update -&gt; NamedUpdater
</code></pre></div></div>
<p>还有一个 <code class="language-plaintext highlighter-rouge">Storage</code> 的 interface:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Storage is a generic interface for RESTful storage services.
// Resources which are exported to the RESTful API of apiserver need to implement this interface. It is expected
// that objects may implement any of the below interfaces.
type Storage interface {
// New returns an empty object that can be used with Create and Update after request data has been put into it.
// This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object)
New() runtime.Object
// Destroy cleans up its resources on shutdown.
// Destroy has to be implemented in thread-safe way and be prepared
// for being called more than once.
Destroy()
}
</code></pre></div></div>
<p>这个类型,就是传递给上面那个添加路由函数的第二个参数:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, *storageversion.ResourceInfo, error) {