-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
1524 lines (736 loc) · 511 KB
/
search.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
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"?>
<search>
<entry>
<title>scalable gc for in-memory mvcc sys</title>
<link href="//post/scalable-gc-for-in-memory-mvcc-sys.html"/>
<url>//post/scalable-gc-for-in-memory-mvcc-sys.html</url>
<content type="html"><![CDATA[<h1 id="现有问题"><a href="#现有问题" class="headerlink" title="现有问题"></a>现有问题</h1><p>在使用MVCC技术的数据库上,通常是在存储达到一定阈值或定时根据watermark来gc掉比它更旧版本的数据,所以会存在长时间运行的事务可能导致历史版本垃圾不能被及时回收,同时不断有新事务产生新的数据版本,从而导致版本链越来越长,版本链变长又会导致检索速度变慢,进而让原本就long-live的事务变得更长。从而成为一种恶性循环,令数据库性能大幅下降,特别是对于HTAP workload,GC通常是其中的瓶颈。所以论文提出了一个叫Steam的垃圾回收方式来应对这种场景。</p><p>将垃圾回收工作分散在不同的组件和工作线程中,尽可能以去中心化方式进行 GC</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240129145957966.png" alt="image-20240129145957966"></p><p>下面这个图直观展示了长事务对于GC的影响</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240129151048539.png" alt="image-20240129151048539"></p><p>对于mvcc版本可见性,B、C事务于v4提交之前开始,所以BC智能读到v1版本的数据,如果按通常的GC方式去维护一个watermark,那么就会导致v2-v999的版本都无法回收,而实际上可能这些版本早就无人问津。所以为了解决这个问题,论文提出一个Steam的垃圾回收策略</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240129152234793.png" alt="image-20240129152234793"></p><h1 id="GC实际影响"><a href="#GC实际影响" class="headerlink" title="GC实际影响"></a>GC实际影响</h1><p>当出现长事务的时候,version record就会堆积。当reader结束的时候,writer才能开始清理这些元组,并且在GC的时候新的事务不能进行写操作。并且随着版本的增多,读事务会越来越慢,导致更多的长事务</p><p>总结起来传统GC有3个缺点</p><ol><li>scalability due to global synchronization</li><li>vulnerability to long-living transactions</li><li>inaccuracy in garbage identification</li></ol><p>1我理解是多机器之间的gc版本同步问题?导致需要同步开销导致扩展性降低?(其实是线程之间的同步问题)</p><p>2为长事务导致版本过多</p><p>3太多无用版本未被回收</p><h1 id="GC策略调研"><a href="#GC策略调研" class="headerlink" title="GC策略调研"></a>GC策略调研</h1><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240129161820350.png" alt="image-20240129161820350"></p><p>steam是基于HyPer做的,也借鉴了<strong>HANA</strong>的部分思路,只不过steam的GC不是在后台完成,而是在事务处理时完成GC</p><p><strong>Hekaton的GC</strong>也是在事务处理时完成,但是他只GC事务处理中扫描过的那部分版本,而Steam会在reader可能读到过时版本之前就GC掉他</p><p>从几个角度去评估GC策略</p><h2 id="Tracking-Level"><a href="#Tracking-Level" class="headerlink" title="Tracking Level"></a>Tracking Level</h2><p>就是GC粒度</p><p>最细粒度的方法是<strong>tuple-level</strong>,GC线程会扫描每一个元组并识别他们是不是过期的元组</p><p>我们可以每过一段时间开启这个线程(bg),也可以在运行txn的时候去检查这些过时的元组(fg)</p><p>系统也可有以<strong>transaction</strong>为粒度去清理,因为一个txn中创建的所有版本都共享着同一个commit timestamp,所以如果他们不可见的时候,应该是同时不可见的。所以我们可以一次性识别并清除同一个txn中的版本。相比于tuple-level,这种粒度可能会导致某些版本删除的时间更晚一些。因为这个事务的所有相关数据的该版本不需要了才会被删除。</p><p><strong>Epoch-based</strong>的方法是让若干个事务成为一个Epoch,这样我们就可以一个Epoch一个Epoch的清理</p><p>最粗粒度的方法就是<strong>table-level</strong>,当整个table不再使用的时候我们会清理他,或者说是一次锁住一整个表,然后清理,然后释放。很少见,目前了解只有HANA</p><h2 id="Frequency-and-Precision"><a href="#Frequency-and-Precision" class="headerlink" title="Frequency and Precision"></a>Frequency and Precision</h2><h3 id="频率"><a href="#频率" class="headerlink" title="频率"></a>频率</h3><p>epoch based系统控制GC一般通过阈值来决定,当达到阈值就进行一次GC</p><p>也有很常见的GC后台线程。每过一段时间就开启一次去进行GC,例如HANA、Hekaton</p><p>还有batch-level的,每个batch结束都会确定这个batch的txn已经结束了,我们就可以安全的回收那些老版本,例如BOHM</p><h3 id="thoroughness"><a href="#thoroughness" class="headerlink" title="thoroughness"></a>thoroughness</h3><p>timestamp-based 目前大部分是这种策略,根据watermark来gc掉比它更旧版本的数据,所以会被长事务影响。</p><p>interval-based只会保留需要的版本数据。</p><h2 id="Version-Storage"><a href="#Version-Storage" class="headerlink" title="Version Storage"></a>Version Storage</h2><p>大部分系统会将数据存储在类似哈希表的全局数据结构中,这样就允许系统去精准独立的回收某一个版本数据。但是缺点就需要在这个全局区域扫描过时版本。意思就是如果想以事务纬度来做精准地回收比较困难</p><p>所以HyPer和Steam直接将这个版本与事务一同存储到undo log中,一旦一个事务的时间戳比watermark小,那就将整个事务的版本数据全部删除掉,并且,单个版本也可以被独立的回收。同时undo log并不是一个额外的工作,在数据库回滚时是都需要的。</p><p>总而言之全局数据结构的好处是GC时可以独立地回收每个单独的版本;但如果想以事务纬度来做精准地回收比较困难。比如说,现在想回收事务A产生的所有历史版本。在全局数据结构里就不好做,需要全局扫描,但如果是存在事务内部,很容易就能找到这个事务产生了哪些垃圾。</p><p>O2N(oldest to newest),版本链的顺序从旧到新,必须先访问旧的在访问新的</p><p>FULL/DELTA(全量/增量),即一行有多个数据时,只存储改变的数据,对于其他数据不存储,或完整的存储整行的数据</p><h2 id="Identification"><a href="#Identification" class="headerlink" title="Identification"></a>Identification</h2><p>如何找到过时版本数据。</p><p>常见的方式global txn map。虽然能快速找到某一个过时版本,并且简化了GC操作(比较timestamp与watermark),但是清理不干净。</p><p>Steam和HANA这种更细粒度的清理方法,能够对每一个版本链都做检查。</p><p>更加粗粒度的方法则是用Epoch来确定版本是否过期,类似watermark。但是epoch方法会让我们的memory management更加轻松。Epoch guard会等待这个epoch内所有的线程离开这个epoch后,才会回收这个epoch使用的内存</p><p>BG/FG 后台执行/前台执行</p><h2 id="Removal"><a href="#Removal" class="headerlink" title="Removal"></a>Removal</h2><p>HANA:后台线程定期触发GC</p><p>Hekaton:事务提交前执行GC,cleans all versions on-the-fly,就是只GC事务涉及到的数据版本(only works for O2N),同时存在后台线程定时扫描整个数据库,将需要GC的数据放到下次事务处理阶段的GC中清除</p><p>epoch-based systems:常见的方式是把该epoch之前已经提交的版本放在free list,当事务更新数据版本的时候,会检查是否可以GC</p><p>interspersing:事务执行阶段执行GC</p><p>Steam,则是在创建的时候就去检查是否可以回收,同时在commit的时候也会检查是否可以回收Undo buffer</p><h1 id="Steam-GC"><a href="#Steam-GC" class="headerlink" title="Steam GC"></a>Steam GC</h1><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130160037529.png" alt="image-20240130160037529"></p><p>Steam的实现是基于HyPer的</p><p>存在两个list</p><p>active transactions list:事务开始时加入</p><p>committed transactions:事务提交时从active list移除,加入commit list,如果是只读事务就直接从活跃事务中移除。</p><p>两个链表都是O2N,可以很快找到最老那个活跃事务的start_ts。然后去提交事务链表中遍历回收每一个事务的垃圾数据,直到遇到事务的commit ts > min start ts,就停下来</p><p>最主要解决三个问题Scalability,HTAP workload,历史数据的内存优化</p><h2 id="Scalable-Synchronization"><a href="#Scalable-Synchronization" class="headerlink" title="Scalable Synchronization"></a>Scalable Synchronization</h2><p>常规方法(watermark+ global txn map)虽然可以在常数时间内进行GC,但是他需要全局的txn map。从而导致无法scale,锁竞争、数据同步的问题。Hekaton使用无锁的txn map解决锁竞争的问题但是仍需要同步,Steam用了一种无需同步的方式。</p><p>Steam中每一个线程都维护了目前线程txn最小。每个thread只会把他的thread-local minimum共享出来(通过64bit atomic integer)。其他的线程就可以读这个值,从而获取全局最小值</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130155721731.png" alt="image-20240130155721731"></p><p>local minimum对应的是第一个活跃事务的时间戳。如果没有的话就设为一个极大值。要确定全局最小值,我们需要扫描每个线程的局部最小。</p><p>和latch free的txn map区别在于每个线程无需再维护全局最小,但是仍需要扫描全部线程以计算全局最小,<strong>感觉仍存在多核的一致性问题</strong>导致清理没那么干净完美</p><h2 id="Eager-Pruning-of-Obsolete-Versions(EPO)"><a href="#Eager-Pruning-of-Obsolete-Versions(EPO)" class="headerlink" title="Eager Pruning of Obsolete Versions(EPO)"></a>Eager Pruning of Obsolete Versions(EPO)</h2><p>前面的方案只是避免了全局同步,但是没有处理long running txn的问题</p><p>难点:如何解决长事务带来的问题?</p><p>这里提出来EPO,可以移除掉所有的不需要的版本</p><p>每个线程定期将活跃事务的timestamp存储到sorted list(N2O)中,当遇到版本链的时候,就执行以下算法</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130170121228.png" alt="image-20240130170121228"></p><p>例子:</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130171027765.png" alt="image-20240130171027765"></p><p>为什么要压缩成A25?而不是A50,因为sorted list是从大到小,当最新active txn都看不到A50了,那后面的更看不到,所以合并成A25</p><p>对于没有长事务运行的workload来说,平常使用的GC就已经够用了,使用EPO还会额外的增加负担</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130185411334.png" alt="image-20240130185411334"></p><h2 id="Layout-of-Version-Records"><a href="#Layout-of-Version-Records" class="headerlink" title="Layout of Version Records"></a>Layout of Version Records</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130185822997.png" alt="image-20240130185822997"></p><p>原子提交,Version中有一个lock bit,用来保证提交是原子的</p><p>然后用AttributeMask可以加速我们检查一个attribute是否在另一个中。并且还节省了存储attributeID的空间</p><p>这里的结构应该和HyPer是一样的,就是原始的表中有数据,然后一个指针指向version vector,表示他这次的操作</p><h1 id="EVALUATION"><a href="#EVALUATION" class="headerlink" title="EVALUATION"></a>EVALUATION</h1><p>在HyPer的基础上实现了5种GC方案,在只改变GC策略的条件下,进行性能测试对比,配置如下</p><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130191530528.png" alt="image-20240130191530528"></p><h2 id="CH-benchmark"><a href="#CH-benchmark" class="headerlink" title="CH benchmark"></a>CH benchmark</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130200728720.png" alt="image-20240130200728720"></p><p>前两个曲线以及steam base的曲线很像,因为都是基于watermark的,唯一的区别就是前两个是epoch based,而steam是txn based</p><p>而Hana是后台独立GC线程,GC时对于版本链是独占的,不允许读,所以这就导致quick write和slow read之间的gap。所以就导致版本记录增加的特别快,一个线程GC不过来了。</p><p>Hekaton中,后台线程负责更新全局最小值,并识别过期的版本。然后把任务分配给worker线程让他们去清理这些版本。这种操作让GC时间增加太多了,所以导致版本数量增加特别快</p><p>读性能差的原因,我认为是这两个GC太慢了,并且使用的是global mutex,没有去中心化</p><p>为什么写吞吐量提高了3x多,EPO的作用就是在long live txn存在的情况下,将无用的rec清除,这使得这一段数据的GC被分摊到多个事务处理结束时,这样虽然增加了每个事务处理的gc时间,却减少了对版本链占用的时间。而读吞吐量没怎么变,说明一个问题,长版本链影响最大的是GC的效率,而不是读的效率。GC速度变慢,导致写性能无法提升</p><h2 id="TPCC"><a href="#TPCC" class="headerlink" title="TPCC"></a>TPCC</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130212914127.png" alt="image-20240130212914127"></p><p>HANA 是global mutex,没有去中心化。</p><p>Hekaton也被这种 coordinate 限制,后台GC线程周期性的从global txn map 检索全局最小,维护watermark,然后分配给所有worker线程执行清理操作</p><h2 id="Scalability-in-Mixed-Workloads"><a href="#Scalability-in-Mixed-Workloads" class="headerlink" title="Scalability in Mixed Workloads"></a>Scalability in Mixed Workloads</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130214210166.png" alt="image-20240130214210166"></p><p>扩展性相当好,因为GC开销分摊给所有workerD</p><h2 id="Garbage-Collection-Frequency"><a href="#Garbage-Collection-Frequency" class="headerlink" title="Garbage Collection Frequency"></a>Garbage Collection Frequency</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130220533561.png" alt="image-20240130220533561"></p><p>按时间间隔GC远不如按epoch来GC,推断出按工作量划分GC间隔是最合理的,而Steam更进一步,把GC融入到事务处理中。</p><h2 id="Skew"><a href="#Skew" class="headerlink" title="Skew"></a>Skew</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130221006763.png" alt="image-20240130221006763"></p><h2 id="Varying-Read-Write-Ratios"><a href="#Varying-Read-Write-Ratios" class="headerlink" title="Varying Read-Write Ratios"></a>Varying Read-Write Ratios</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130221037694.png" alt="image-20240130221037694"></p><h2 id="Eager-Pruning-of-Obsolete-Versions"><a href="#Eager-Pruning-of-Obsolete-Versions" class="headerlink" title="Eager Pruning of Obsolete Versions"></a>Eager Pruning of Obsolete Versions</h2><p><img src="./scalable-gc-for-in-memory-mvcc-sys/image-20240130221300513.png" alt="image-20240130221300513"></p><p>用了EPO性能提升5x</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
</tags>
</entry>
<entry>
<title>adaptive-hybrid-indexes</title>
<link href="//post/adaptive-hybrid-indexes.html"/>
<url>//post/adaptive-hybrid-indexes.html</url>
<content type="html"><![CDATA[<p>index 是查询处理系统中性能影响的关键部分,DBMS 广泛采用 B-tree、trie、hash table,特别是 OLTP 系统的索引的存储开销非常大(一些场景下,一半以上的内存是由索引结构消耗的)。内存在现代数据库上加速效果非常明显,但很多场景下将所有数据放入内存已不可能。</p><p>作者把索引优化技术按介入的时间分为三个阶段:</p><ol><li><p>development-time:单类型数据结构</p></li><li><ol><li>设计 state-of-the-art 数据结构满足 CRUD 操作,整体均衡,重点对主要操作做优化。</li><li>compact index 技术,减少内存空间,通常比 state-of-the-art index 要慢。例如 succinct index 通过减少数据结构上的 pointer 并在运行时计算 offset,在 lookup、scan、udpate 上性能差一些。</li></ol></li><li><p>build-time:在一个 index(不同的数据块)上使用多种 encoding 组合,根据数据特征,在写入时选择数据结构。</p></li><li><p>run-time:build-time 是一种静态组合,单不考虑动态的 workload(工况),run-time 时可以拿到更多的 工况信息帮助优化索引结构。</p></li></ol><p>论文提出一个框架:workload-adaptive hybrid index,将 encoding 决策时机延后到 run-time 进行。</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
<category> storage </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
<tag> storage </tag>
</tags>
</entry>
<entry>
<title>graph-database-interface</title>
<link href="//post/graph-database-interface.html"/>
<url>//post/graph-database-interface.html</url>
<content type="html"><![CDATA[<p>首先定义了一个图数据库接口,然后用这个图数据库接口,为分布式RDMA架构设计一个图数据库,利用RDMA架构单边通信以及集合通信操作,提供高可扩展性,能够线性扩展十万核</p><h1 id="挑战"><a href="#挑战" class="headerlink" title="挑战"></a>挑战</h1><ol><li>数据集大而复杂</li><li>目前业界既有OLTP数据库也有OLAP,OLSP,即OLTP(online transactional processing)、OLAP(online analytical processing)、OLSP(online serving processing),如何满足高性能、可扩展的数据库的同时可以同时支持这三种工作环境</li><li>portability可移植性,将数据库代码移植到有不同硬件环境的机器的代价非常昂贵</li><li>满足上述所有条件的数据库很难理解、debug、维护、扩展,如何解决?</li></ol><h2 id="方式"><a href="#方式" class="headerlink" title="方式"></a>方式</h2><ol><li><p>分析Neo4j、TinkerPop、JanusGraph的源码,规范出了一套可移植、可编程的图数据库接口(GDI)</p><ul><li>数据存储层:包括事务、索引、数据、数据源信息等</li></ul><p>为什么可移植性高?因为解耦出了信息传递接口(MPI),用于数据交互</p></li><li><p>给出了一套高性能实现方案,RDMA实现分布式内存。</p><ul><li>RDMA在云数据中心以及超级计算领域一直是可扩展性问题的很好的解决方案</li><li>分布式存储层</li><li>one-sided non-blocking RDMA communication</li><li>collective communication</li></ul></li></ol><p>利用HPC(高性能计算)的技术,例如collectives,one-sided RDMA</p><h1 id="贡献"><a href="#贡献" class="headerlink" title="贡献"></a>贡献</h1><h2 id="第一"><a href="#第一" class="headerlink" title="第一"></a>第一</h2><p>分析Neo4j、TinkerPop、JanusGraph的源码,将其规范成了一套可移植、可编程的图数据库接口(GDI)</p><ul><li>数据存储层:包括事务、索引、数据、数据源信息等</li></ul><p>为什么可移植性高?因为解耦出了信息传递接口(MPI),用于数据交互</p><h2 id="第二"><a href="#第二" class="headerlink" title="第二"></a>第二</h2><p>给出了一套高性能实现方案,RDMA实现分布式内存。</p><ul><li>RDMA在云数据中心以及超级计算领域一直是可扩展性问题的很好的解决方案</li><li>分布式存储层</li><li>one-sided non-blocking RDMA communication</li><li>collective communication</li></ul><p>利用HPC(高性能计算)的技术,例如collectives,one-sided RDMA</p><h2 id="第三"><a href="#第三" class="headerlink" title="第三"></a>第三</h2><p>支持几乎所有function,并且可以提供独立的底层硬件的理论性能分析</p><h2 id="第四"><a href="#第四" class="headerlink" title="第四"></a>第四</h2><p>描述了怎么用GDI来跑一些工作负载,如LDBC,LinkBench</p><h2 id="第五"><a href="#第五" class="headerlink" title="第五"></a>第五</h2><p>开发了一个可以根据配置生成内存数据的工具,可以快速生成具有label、属性的图数据</p><h2 id="第六"><a href="#第六" class="headerlink" title="第六"></a>第六</h2><p>可扩展性,实验评估做了超大规模的实验</p><h2 id="第七"><a href="#第七" class="headerlink" title="第七"></a>第七</h2><p>开源!</p><h1 id="GDI"><a href="#GDI" class="headerlink" title="GDI"></a>GDI</h1><h2 id="GDI和图数据库的关系"><a href="#GDI和图数据库的关系" class="headerlink" title="GDI和图数据库的关系"></a>GDI和图数据库的关系</h2><p>查询从<strong>Client</strong>发起时,<strong>数据库中间层</strong>会将其分散成子查询到多个机器并收集多个机器的结果返回,这个中间层依赖于GDI所在的<strong>存储事务引擎</strong>。这部分直接与数据交互,所以GDI这一层会提供大部分如增删改查的接口,最后后端存储提供真实存储,如RAM、CSV、JSON</p><p><img src="./graph-database-interface/image-20231218150553781.png" alt="image-20231218150553781"></p><h2 id="GDI的结构与功能"><a href="#GDI的结构与功能" class="headerlink" title="GDI的结构与功能"></a>GDI的结构与功能</h2><ol><li>灰色为基础模块,必须被初始化</li><li>蓝色为CRUD标签与属性</li><li>紫色为CRUD节点与边 </li><li>绿色为事务处理</li><li>橙色为索引处理</li><li>暗红色为约束条件检查</li><li>红色为错误处理</li></ol><p><img src="./graph-database-interface/image-20231218151821164.png" alt="image-20231218151821164"></p><p>C(Collective):所有存活协程都作为一个参与者进行集合通信(Collective communications)</p><p>L(Local):无需通信,本机即可完成操作</p><h2 id="高性能事务"><a href="#高性能事务" class="headerlink" title="高性能事务"></a>高性能事务</h2><p>不对事务做任何限制</p><p>Collective transaction:涉及到多个处理的事务,类似分片2pc事务处理</p><p>Local transaction:单个processor即可处理,涉及少量数据</p><h2 id="快速高效存取数据"><a href="#快速高效存取数据" class="headerlink" title="快速高效存取数据"></a>快速高效存取数据</h2><p>GDI对存储层的节点id/边id等会进行内部重新编id,使得其更加portable,independent</p><p>volatileID:临时使用,用于图数据负载均衡</p><p>permanentID:减少远程操作数量,但是妨碍数据动态负载均衡</p><h2 id="句柄"><a href="#句柄" class="headerlink" title="句柄"></a>句柄</h2><p>提高可用性,使用句柄可以直接0拷贝使用数据</p><h2 id="一致性"><a href="#一致性" class="headerlink" title="一致性"></a>一致性</h2><p>提供了不同的一致性模型,为index以及meta数据提供最终一致性,为数据提供线性一致性。</p><p>更灵活,清晰定义了每种数据的一致性</p><h1 id="可伸缩性GDI-RDMA的实现"><a href="#可伸缩性GDI-RDMA的实现" class="headerlink" title="可伸缩性GDI RDMA的实现"></a>可伸缩性GDI RDMA的实现</h1><p>基于MPI并且利用RDMA单边通信(一个进程直接读取另一个进程的内存数据而不需要显式的通信),这让通信性能以及可伸缩性得到很大提升</p><p><img src="./graph-database-interface/image-20231219103622361.png" alt="image-20231219103622361"></p><p>实现内存图数据库</p><p>GMC三部分是在每个分片上都存在全副本的,其他数据是分片的</p><h2 id="Graph-data"><a href="#Graph-data" class="headerlink" title="Graph data"></a>Graph data</h2><h3 id="Logical-Layout"><a href="#Logical-Layout" class="headerlink" title="Logical Layout"></a>Logical Layout</h3><p>抽象出来的接口层,方便开发人员写代码,与BGDL存在地址映射</p><h3 id="Blocked-Graph-Data-Layout"><a href="#Blocked-Graph-Data-Layout" class="headerlink" title="Blocked Graph Data Layout"></a>Blocked Graph Data Layout</h3><p>一个节点的相关数据不要求存储在同一个节点,因为RDMA,可以低代价访问远端内存。</p><p>性能优化可以直接在这完成,而不需要修改上层代码</p><p>轻型边:每条边最多带一个lable</p><h2 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h2><p>抽象出一个模块,专门执行2pl,2pc,保存了当前事务的状态,GDA可编程性不会有影响</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>每个模块都可以单独分析性能,并且依赖深度并不深,即一个数据/事务不会涉及到非常多个模块,都是常数级,最多也就10-20个</p><h1 id="实验"><a href="#实验" class="headerlink" title="实验"></a>实验</h1><p>环境:瑞士国家超级计算机中心,1813台XC40(2x18 vcpu,64GB RAM) 5704台XC50(12vcpu,64GB RAM)</p><p><img src="./graph-database-interface/image-20231219160803809.png" alt="image-20231219160803809"></p>]]></content>
<categories>
<category> database </category>
<category> graphdb </category>
</categories>
<tags>
<tag> database </tag>
<tag> graphdb </tag>
</tags>
</entry>
<entry>
<title>ubuntu20从0开始安装环境</title>
<link href="//post/ubuntu20env.html"/>
<url>//post/ubuntu20env.html</url>
<content type="html"><![CDATA[<h1 id="完整从0配置新ubuntu环境"><a href="#完整从0配置新ubuntu环境" class="headerlink" title="完整从0配置新ubuntu环境"></a>完整从0配置新ubuntu环境</h1><p>vim /etc/apt/source.list</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse# deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse# deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse# deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse# deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse## Pre-released source, not recommended.# deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse# deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse# deb-src https://mirrors.ustc.edu.cn/ubuntu/ focal main restricted universe multiverse# deb-src https://mirrors.ustc.edu.cn/ubuntu/ focal-updates main restricted universe multiverse# deb-src https://mirrors.ustc.edu.cn/ubuntu/ focal-backports main restricted universe multiverse# deb-src https://mirrors.ustc.edu.cn/ubuntu/ focal-security main restricted universe multiverse# deb-src http://mirrors.163.com/ubuntu/ focal main restricted universe multiverse# deb-src http://mirrors.163.com/ubuntu/ focal-security main restricted universe multiverse# deb-src http://mirrors.163.com/ubuntu/ focal-updates main restricted universe multiverse# deb-src http://mirrors.163.com/ubuntu/ focal-backports main restricted universe multiverse<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>更新镜像源,更新完成后sudo apt update</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">sudo apt install git openssl -y<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><h2 id="Java安装"><a href="#Java安装" class="headerlink" title="Java安装"></a>Java安装</h2><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gztar -vxf jdk-17_linux-x64_bin.tar.gzmv jdk-17.0.7 jdk-17vim /etc/profile<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>在文件末尾新增以下,jdk目录在opt/jdk-17</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">JAVA_HOME=/opt/jdk-17PATH=$JAVA_HOME/bin:$PATHexport JAVA_HOME PATH<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>~/.bashrc中加source /etc/profile</p>]]></content>
<categories>
<category> tech </category>
<category> linux </category>
</categories>
<tags>
<tag> tech </tag>
<tag> linux </tag>
</tags>
</entry>
<entry>
<title>opengauss-odbc安装</title>
<link href="//post/opengauss-odbc.html"/>
<url>//post/opengauss-odbc.html</url>
<content type="html"><![CDATA[<h1 id="Linux下配置数据源"><a href="#Linux下配置数据源" class="headerlink" title="Linux下配置数据源"></a>Linux下配置数据源</h1><p><a href="https://docs.opengauss.org/zh/docs/2.0.0/docs/Developerguide/Linux下配置数据源.html">Linux下配置数据源 (opengauss.org)</a></p><ol><li><p>下载unixODBC</p></li><li><p>安装unixODBC</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">tar zxvf unixODBC-2.3.0.tar.gzcd unixODBC-2.3.0#修改configure文件(如果不存在,那么请修改configure.ac),找到LIB_VERSION#将它的值修改为"1:0:0",这样将编译出*.so.1的动态库,与psqlodbcw.so的依赖关系相同。vim configure./configure --enable-gui=no #如果要在鲲鹏服务器上编译,请追加一个configure参数: --build=aarch64-unknown-linux-gnu make#安装可能需要root权限make installcd ..tar zxvf openGauss-2.0.0-ODBC.tar.gz -C /usr/local/libcd /usr/local/libcp lib/* .cp odbc/lib/* .yum install libtool-ltdl -y<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></li><li><p>在“/usr/local/etc/odbcinst.ini”文件中追加以下内容</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">[GaussMPP]Driver64=/usr/local/lib/psqlodbcw.sosetup=/usr/local/lib/psqlodbcw.so<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>在“/usr/local/etc/odbc.ini ”文件中追加以下内容</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">[MPPODBC]Driver=GaussMPPServername=47.92.142.44Database=postgres Username=jackPassword=Test@123Port=26000Sslmode=allow<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div></li><li><p>gs_guc reload -N NodeName -I all -c “listen_addresses=’localhost,0.0.0.0’”</p><p>gs_guc reload -N all -I all -h “host all jack 39.98.81.111/32 sha256”</p></li><li><p>在客户端配置环境变量。</p><p>vim ~/.bashrc</p><p>export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH<br>export ODBCSYSINI=/usr/local/etc<br>export ODBCINI=/usr/local/etc/odbc.ini</p><p>source ~/.bashrc</p></li></ol><p>pg_hba.conf</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none"># "local" is for Unix domain socket connections onlylocal all all trust# IPv4 local connections:host all all 127.0.0.1/32 trusthost all all 47.92.209.140/32 sha256host all all 172.31.16.64/32 sha256host all all 0.0.0.0/0 sha256host all all 0.0.0.0/0 trust# IPv6 local connections:host all all ::1/128 trust<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="BUG"><a href="#BUG" class="headerlink" title="BUG"></a>BUG</h1><p>启动失败</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">[omm@iZ8vbc1ycxkzrasostw66mZ ~]$ gs_om -t startStarting cluster.==================================================================================[GAUSS-53600]: Can not start the database, the cmd is source /home/omm/.bashrc; python3 '/opt/huawei/install/om/script/local/StartInstance.py' -U omm -R /opt/huawei/install/app -t 300 --security-mode=off, Error:[GAUSS-51400] : Failed to execute the command: source /home/omm/.bashrc; python3 '/opt/huawei/install/om/script/local/StartInstance.py' -U omm -R /opt/huawei/install/app -t 300 --security-mode=off. Error:[FAILURE] node1_hostname:..<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>更改主机名</p><p>hostnamectl set-hostname node1_hostname</p><p>sudo yum install libtool-ltdl</p><h1 id="卸载Opengauss"><a href="#卸载Opengauss" class="headerlink" title="卸载Opengauss"></a>卸载Opengauss</h1><p>hostnamectl set-hostname node1_hostname<br>su - omm</p><p>gs_uninstall —delete-data</p><h1 id="切换到root用户下"><a href="#切换到root用户下" class="headerlink" title="切换到root用户下"></a>切换到root用户下</h1><p>userdel -r omm</p><p>rm -rf /opt/huawei<br>rm -rf /opt/software<br>rm -rf /root/gauss_om</p><h1 id="预安装"><a href="#预安装" class="headerlink" title="预安装"></a>预安装</h1><p>export LD_LIBRARY_PATH=/opt/software/openGauss/script/gspylib/clib:$LD_LIBRARY_PATH<br>cd /opt/software/openGauss/script<br>export LANG=en_US.UTF-8<br>export LANGUAGE=en_US.UTF-8</p><p>./gs_preinstall -U omm -G dbgrp -X /tmp/clusterconfig.xml</p><h1 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h1><p>chmod 777 -R /tmp<br>chown -R omm:dbgrp /tmp<br>\cp -rf /tmp/openGauss-server/simpleInstall/ /tmp/openGauss-server/mppdb_temp_install/<br>su - omm<br>rm -rf /tmp/openGauss-server/mppdb_temp_install/data/<br>cd /tmp/openGauss-server/mppdb_temp_install/simpleInstall/<br>sh install.sh -w “Pengqi1998” -p 26000</p><p>cp /tmp/postgresql.conf /tmp/openGauss-server/mppdb_temp_install/data/single_node/<br>cp /tmp/mot.conf /tmp/openGauss-server/mppdb_temp_install/data/single_node/<br>cp /tmp/pg_hba.conf /tmp/openGauss-server/mppdb_temp_install/data/single_node/<br>gs_ctl restart -D /tmp/openGauss-server/mppdb_temp_install/data/single_node -Z single_node</p><p>gsql -d postgres -p 26000<br>ALTER ROLE omm IDENTIFIED BY ‘pengqi.1998’ REPLACE ‘Pengqi1998’;<br>create user jack password ‘Test@123’;<br>grant usage on foreign server mot_server to jack;<br>GRANT ALL PRIVILEGES ON DATABASE postgres to jack;<br>ALTER ROLE jack CREATEDB;<br>GRANT ALL PRIVILEGES To jack;</p><h3 id="安装-1"><a href="#安装-1" class="headerlink" title="安装"></a>安装</h3><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">hostnamectl set-hostname node1_hostnameyum install -y libaio-devel flex bison ncurses-devel glibc-devel patch lsb_release redhat-lsb readline-devel bzip2yum install -y python36yum install -y bzip2 libaio-devel flex bison ncurses-devel glibc-devel patchyum install -y net-tools tar lrzszsystemctl disable firewalld.servicesystemctl stop firewalld.servicesystemctl stop firewalld.servicesed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/configsetenforce 0if [ "$LANG" != "en_US.UTF-8" ];thenexport LANG=en_US.UTF-8echo export LANG=en_US.UTF-8 >> /etc/profilefiln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimeswapoff -aifconfigifconfig ens192 mtu 8192sed -i 's/^Banner .*/Banner none/' /etc/selinux/configsed -i 's/^#PermitRootLogin .*/PermitRootLogin yes/' /etc/selinux/configsed -i 's/^PermitRootLogin no/PermitRootLogin yes/' /etc/selinux/configsystemctl restart sshdecho 5 > /proc/sys/net/ipv4/tcp_retries1echo 5 > /proc/sys/net/ipv4/tcp_syn_retriesecho 10 > /proc/sys/net/sctp/path_max_retransecho 10 > /proc/sys/net/sctp/max_init_retransmitsecho net.ipv4.tcp_retries1 = 5 >>/etc/sysctl.confecho net.ipv4.tcp_syn_retries = 5 >>/etc/sysctl.confecho net.sctp.path_max_retrans = 10 >>/etc/sysctl.confecho net.sctp.max_init_retransmits = 10 >>/etc/sysctl.confyum install -y ntpsystemctl start ntpd && systemctl enable ntpdyum - y install openssl-develcd /tmpvim cluster_conf.xmlchmod 777 cluster_conf.xml#omm 卸载gs_uninstall --delete-data#root卸载userdel -r ommrm -rf /tmp/huaweirm -rf /tmp/softwarerm -rf /root/gauss_omcd /optrm -rf *mkdir /opt/huaweichmod -R 777 /opt/huaweimkdir -p /opt/software/openGausschmod -R 777 /opt/softwarecd /opt/software/openGausscd /usr/bin/rm -f python2mv python python2.6.oriln -s python2.7 python2ln -s /usr/bin/python3.6 /usr/bin/pythoncd /optmkdir testcd testwget https://opengauss.obs.cn-south-1.myhuaweicloud.com/2.0.1/x86/openGauss-2.0.1-CentOS-64bit-all.tar.gztar -xzvf openGauss-2.0.1-CentOS-64bit-all.tar.gz cp openGauss-2.0.1-CentOS-64bit-om.tar.gz /opt/software/openGausscd /tmp/merge-200-Taas-motsh build.sh -m release -3rd /tmp/binarylibs/ -pkgcd outputcp * /opt/software/openGauss/tar -xzvf openGauss-2.0.1-CentOS-64bit-om.tar.gz# 一次性export LD_LIBRARY_PATH=/opt/software/openGauss/script/gspylib/clib:$LD_LIBRARY_PATHcd /opt/software/openGauss/scriptexport LANG=en_US.UTF-8export LANGUAGE=en_US.UTF-8./gs_preinstall -U omm -G dbgrp -X /tmp/cluster_conf.xmlsu - ommgs_install -X /tmp/cluster_conf.xmlgsql -d postgres -p 26000ALTER ROLE omm IDENTIFIED BY 'pengqi.1998' REPLACE 'Pengqi1998';create user jack password 'Test@123';grant usage on foreign server mot_server to jack;GRANT ALL PRIVILEGES ON DATABASE postgres to jack;ALTER ROLE jack CREATEDB;GRANT ALL PRIVILEGES To jack;vi /tmp/huawei/install/data/db1/postgresql.confenable_incremental_checkpoint = oncheckpoint部分需要改为off开启关闭服务:gs_om -t start/stop进入用户:su - omm 进入数据库:gsql -d postgres -p 26000退出数据库:ctrl+d退出用户:exitCREATE Foreign TABLE IF NOT EXISTS taas_odbc(c_sk INTEGER , c_name VARCHAR(32), c_tid VARCHAR(32));select count(*) from taas_odbc;insert into taas_odbc values(1,'123','qwe');<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>/opt/huawei/install/data/dn/postgresql.conf</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">listen_addresses = '*'enable_incremental_checkpoint = off<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>/opt/huawei/install/data/dn/pg_hba.conf</p>]]></content>
<categories>
<category> database </category>
<category> system </category>
</categories>
<tags>
<tag> database </tag>
<tag> system </tag>
</tags>
</entry>
<entry>
<title>leopard论文阅读记录</title>
<link href="//post/leopard.html"/>
<url>//post/leopard.html</url>
<content type="html"><![CDATA[<p>这篇论文题目是LEOPARD: Lightweight Edge-Oriented Partitioning and<br>Replication for Dynamic Graphs,翻译一下就是一个轻型的基于边的面向动态图的分区复制方案Leopard,是针对于大型变化图设计的。可以根据图结构的变化而切换分区方式以及尽量减少边切割数目来减少复制数据量,</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
</tags>
</entry>
<entry>
<title>VLDB 2010 Schism论文阅读</title>
<link href="//post/schism-vldb10.html"/>
<url>//post/schism-vldb10.html</url>
<content type="html"><![CDATA[<p>阅读Schism: a Workload-Driven Approach to Database Replication and Partitioning笔记。这篇论文,总体来说是讲分区方法的,如何平衡分区的同时尽可能提高数据库负载,这篇论文通过将事务转化成图的角度,通过图切割算法来分区,尽可能减少跨分区事务来达到优化效果,下面开始介绍。</p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>现在工作负载越来越大,扩展的很快,所以数据需要分区存放,但是如何数据分区才能让事务尽可能的在本地或者尽量少涉及节点呢,传统方法有三种</p><ul><li>Round-robin分区</li><li>Range分区</li><li>Hash分区</li></ul><p><img src="schism-vldb10/image-20230321201539842.png" alt="分区示意图"></p><p>但是这些方式可能适用于一些工作负载,但是还有一些工作负载,通过这些分区方式很难得到高性能,比如社交网络这种涉及少量数据的小事务的负载用以上方法就不能获得很好的性能了。因为hash以及round-robin就可能涉及多个node了,而一旦跨分区,那么吞吐量就会下降很多,range分区就会好很多,但是手动设定range也是一个很麻烦的事情,所以论文想在平衡分区负载的同时最小化跨分区事务数量,这就是Schism的目的。</p><h1 id="Schism"><a href="#Schism" class="headerlink" title="Schism"></a>Schism</h1><p>一个基于事务负载生成图的数据驱动的分区系统,这个系统分为几个步骤执行</p><ul><li>Data pre-processing<ul><li>输入:transactions & DB</li><li>输出读写集</li></ul></li><li>Creating the graph<ul><li>Nodes: tuples </li><li>Edges: transactions</li></ul></li><li>Partitioning the graph<ul><li>平衡的将图按边切割分区,并且按边切割复制tuples</li></ul></li><li>Explaining the partition<ul><li>用频率高的属性生成决策树的方式去表示节点如何分区</li></ul></li><li>Final validation<ul><li>比较出最好的分区方法,给出结果</li></ul></li></ul><h2 id="分布式事务开销"><a href="#分布式事务开销" class="headerlink" title="分布式事务开销"></a>分布式事务开销</h2><ul><li>事务从单个节点上获取数据<ul><li>无额外开销</li></ul></li><li>昂贵的分布式事务开销<ul><li>contention: 锁开销</li><li>distributed deadlocks:解决分布式死锁</li><li>需要多个节点数据的复杂查询语句</li></ul></li><li>实验查看性能对比,每个事务两个语句<ul><li>一个是所有事务的数据都在本地</li><li>一个是所有事务的数据都涉及多节点</li></ul></li></ul><p><img src="schism-vldb10/image-20230321205309869.png" alt="分布式事务实验"></p><h2 id="Graph-Partitioning"><a href="#Graph-Partitioning" class="headerlink" title="Graph Partitioning"></a>Graph Partitioning</h2><h3 id="BASIC-GRAPH-REPRESENTATION"><a href="#BASIC-GRAPH-REPRESENTATION" class="headerlink" title="BASIC GRAPH REPRESENTATION"></a>BASIC GRAPH REPRESENTATION</h3><p>节点:tuple</p><p>边 :事务涉及到的tuples相连</p><p>边权:多少个事务涉及到某对tuple</p><p><img src="schism-vldb10/image-20230321205905854.png" alt="The graph representation"></p><p>右上角事务涉及tuple1和tuple2的数据,所以1节点和2节点之间存在边,右下角事务涉及到tuple145,则这三个节点之间都存在边,在图中,边权为多少个事务涉及到某对tuple,例如tuple1和tuple2,只有右上角的事务同时涉及到tuple1和tuple2,所以边权为1。</p><h3 id="GRAPH-WITH-REPLICATION"><a href="#GRAPH-WITH-REPLICATION" class="headerlink" title="GRAPH WITH REPLICATION"></a>GRAPH WITH REPLICATION</h3><p>在上面的基础之上加了Tuple-level replication。</p><h4 id="复制逻辑"><a href="#复制逻辑" class="headerlink" title="复制逻辑"></a>复制逻辑</h4><p>n+1 nodes:一个tuple</p><p>n:多少个事务需要这个数据</p><p>Replication edge weights:在负载中多少个事务更新了这个tuple</p><p><img src="schism-vldb10/image-20230321211001262.png" alt="GRAPH WITH REPLICATION"></p><p>划分图时的几个点</p><ul><li>尽量让切的边权和小</li><li>尽量保证分区的权值平衡</li></ul><p>划分后,一个partition的多个相同node不需要重复存储,例如2号节点tuple只存一份,一号节点tuple存两份</p><p>并且不复制经常更新的节点</p><h5 id="Fine-grained-per-tuple-partitioning"><a href="#Fine-grained-per-tuple-partitioning" class="headerlink" title="Fine-grained per tuple partitioning"></a>Fine-grained per tuple partitioning</h5><p>红色的表,是Look-up Tables 基于经常出现在where子句中的属性构建,用于路由到数据分区</p><p>涉及到replication的时候,图变为Figure3所示,在图中某个tuple节点将出现n+1次,n是有多少个事务涉及到该tuple,而Replication edge weights是在负载中多少个事务更新了这个tuple。当图构建完成之后就需要进行图划分了,划分时需要尽量让切的边权和小以及尽量保证分区的权值平衡。划分后,一个partition的多个相同node不需要重复存储,例如2号节点tuple只存一份,一号节点tuple存两份;并且不复制经常更新的节点,例如,tuple1如果更新次数很多,那么就让tuple1只存在partition0,而不存两份,减少同步更新的开销。</p><h2 id="EXPLAINING-THE-PARTITION"><a href="#EXPLAINING-THE-PARTITION" class="headerlink" title="EXPLAINING THE PARTITION"></a>EXPLAINING THE PARTITION</h2><p>这个阶段想使用决策树(a machine learning classifier)通过收集(value, label)作为输入,输出一个结果range,然后按range分区 。</p><p>简而言之就是通过上一个阶段生成的数据(tuple,partition)映射来生成一个模型再分区。</p><h2 id="FINAL-VALIDATION"><a href="#FINAL-VALIDATION" class="headerlink" title="FINAL VALIDATION"></a>FINAL VALIDATION</h2><p>就是比较一下结果,再判断选哪个方案</p><p>输出拥有最小跨分区事务数量的方案</p><ul><li>Fine-grained per tuple partitioning,对图进行边切割的方式分区 </li><li>Range predicate partition,也就是使用决策树模型生成range的方式分区</li><li>hash partition</li><li>full replication</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>1.将工作负载抽象成图的形式,数据作为节点,事务涉及到的数据对作为边,边权为多少个事务涉及到某对tuple</p><p>如Figure 2右上角事务涉及tuple1和tuple2的数据,所以1节点和2节点之间存在边,右下角事务涉及到tuple1 4 5,则这三个节点之间都存在边,而边权,如tuple1和tuple2,只有右上角的事务同时涉及到tuple1和tuple2,所以边权为1。</p><p>涉及到replication的时候,图变为Figure3所示,在图中某个tuple节点将出现n+1次,n是有多少个事务涉及到该tuple,而Replication的边权是在负载中多少个事务更新了这个tuple。</p><p>2.当图构建完成之后就需要进行图划分了,划分时需要尽量让切的边权和小以及尽量保证分区的权值平衡。</p><p>划分后,一个partition的多个相同node不需要重复存储,例如2号节点tuple只存一份,一号节点tuple存两份;</p><p>并且不复制经常更新的节点,例如,tuple1如果更新次数很多,那么就让tuple1只存在partition0,而不存两份,减少同步更新的开销。</p><p>3.通过图切割的方式确定了数据的分区方式得到了lookup table,然后把lookup table中的(tuple,partition)作为决策树的输入,输出为按谓词划分range的分区方式。最后得到的这个分区方式是一种独立于抽象图后划分分区的分区方式,它可以给定一个没有确定分区的tuple,通过生成的决策树模型去找到对应的分区节点。</p><p>系统最后通过比较这两种分区方式以及哈希分区输出最优方案作为最终分区方案。</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
</tags>
</entry>
<entry>
<title>Dynamic Affinity Cluster Allocation in a Shared Disks Cluster论文阅读</title>
<link href="//post/dynamic-affinity-cluster-allocation-in-a-shared.html"/>
<url>//post/dynamic-affinity-cluster-allocation-in-a-shared.html</url>
<content type="html"><![CDATA[<p>06年的一篇论文,讲述了一种新的负载均衡方法,个人觉得比较巧妙,适用性可能比较广,思路简单清晰,利用事务分类cluster以及动态调整集群节点状态的方法达到一个负载均衡。</p><h1 id="论文贡献"><a href="#论文贡献" class="headerlink" title="论文贡献"></a>论文贡献</h1><blockquote><p>In this paper, we propose a new transaction routing algorithm, named Dynamic Affinity Cluster Allocation (DACA).DACA can make an optimal balance between the affinity-based routing and indiscriminate sharing of load in the SD cluster. As a result, DACA can increase the buffer hit ratio and reduce the frequency of internode buffer invalidations while achieving the dynamic load balancing.</p></blockquote><p>提出一种新的负载均衡的方法,提高缓存命中率以及降低内部节点缓存失效频率</p><h2 id="传统方法"><a href="#传统方法" class="headerlink" title="传统方法"></a>传统方法</h2><blockquote><p>In [19], incoming transactions of the rush class are spread across all nodes in a round-robin fashion. On the other hand, in [7],transactions are routed to the least loaded node.</p></blockquote><ol><li>传统方法一个就是所有节点轮流进事务,round-robin</li><li>事务分配到负载最低的节点上执行</li></ol><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319115242113.png" alt="传统方法缺点"></p><h3 id="存在的缺点"><a href="#存在的缺点" class="headerlink" title="存在的缺点"></a>存在的缺点</h3><ol><li>缓存中的数据可能来自于不同分区,在缓存不够的情况下,竞争缓存导致缓存命中率降低。</li><li>涉及相同数据的事务在不同节点执行,导致不同节点上缓存多份相同数据,导致在一个节点上更新数据后,其他节点缓存全部失效。</li></ol><h1 id="DACA算法"><a href="#DACA算法" class="headerlink" title="DACA算法"></a>DACA算法</h1><p>结合传统的两种算法设计了一个新算法DACA,首先介绍几个定义</p><p><strong>AC(affinity cluster):</strong>将涉及到数据类似的事务归成一个transaction class,这个transaction class会发送到AC中设置的节点。也就是说AC里涉及到的事务数据都是根据某种方法划分的。</p><p>AC由一个以上的Node组成,而一个Node可以属于多个AC。</p><p>初始化时,AC必须被分配,Node可以空闲</p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120046897.png" alt="一些定义"></p><p>上面定义的我们主要记住几个东西</p><p>$R$运算,代表AC到Node的映射,AC对应着哪些Node</p><p>$R^{-1}$,$R$的逆运算,代表Node到AC的映射,Node关联着哪些AC</p><p>$|R|$ 映射出来集合的大小</p><h1 id="T-代表负载大小"><a href="#T-代表负载大小" class="headerlink" title="$T$ 代表负载大小"></a>$T$ 代表负载大小</h1><p>$\overline{L}(N)$代表集群负载均值</p><h1 id="几种负载情况"><a href="#几种负载情况" class="headerlink" title="几种负载情况"></a>几种负载情况</h1><p>算法通过解决几种负载情况来设计</p><h2 id="AC-Overload"><a href="#AC-Overload" class="headerlink" title="AC Overload"></a>AC Overload</h2><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120253465.png" alt="AC Overload"></p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120624833.png" alt="解决方案"></p><p>简而言之,就是在AC中加入最低工作负载节点,并将这个最低工作负载节点与原先关联的AC断联。</p><p>看例子最好懂,原先N1-N4分别对应AC1-AC4,并且每个AC全为50个事务,现在AC1增加到150个事务,此时经过计算均值为75,AC1根据定义1,已经Overload,此时除了N1,其他节点都是50个事务,所以挑了N2进入AC1,并与之前的AC2断联,AC2将加入N3 N4选一个最小的加入。此时负载就变为下图</p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120722719.png" alt="例1"></p><h2 id="Node-Overload"><a href="#Node-Overload" class="headerlink" title="Node Overload"></a>Node Overload</h2><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120329092.png" alt="Node Overload"></p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319121317443.png" alt="解决方法"></p><p>概括一下如果$N<em>p$过载,那么找他相关的AC,将最大的AC和它断联,然后找没分配AC,或者处于ac underload的node,将该node和$AC</em>{max}$关联,如果没有这样的节点,那就找个#T(N)最小的,将$AC_{min}$和该节点关联。</p><p>继续上一个例子,将#T(AC2)从50加到84,此时的负载均值为83.5,设置此时的参数为1.6,根据定义2可得$134>83.5*1.6=133.6$ 。此时将AC3加入到N4,此时状态变为下图</p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319121559017.png" alt="例2"></p><h2 id="AC-Underload"><a href="#AC-Underload" class="headerlink" title="AC Underload"></a>AC Underload</h2><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319120345732.png" alt="AC Underload"></p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319121616794.png" alt="解决方案"></p><p>如果$AC_q$负载太小了,并且由AC过载或者Node过载,此时会将一个节点与$AC_q$断联,通过Node expansion或者AC distribution方式加入一个AC。</p><p>还是续上一个例子,将#T(AC1)减到50,此时的负载均值为 58.5,AC1根据定义3计算$50/(2-1)=50<58.5$已经underload,并且N4已经过载此时就会将AC1中断联一个,给N4的$AC_{max}$关联,此时变为下面的状态</p><p><img src="dynamic-affinity-cluster-allocation-in-a-shared/image-20230319121822665.png" alt="例3"></p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
</tags>
</entry>
<entry>
<title>刷题记录</title>
<link href="//post/code-solution.html"/>
<url>//post/code-solution.html</url>
<content type="html"><![CDATA[<p>记录从3.5号开始的刷题记录,正在连载</p><p>最近还要干nebula的活,只能抽小部分时间先找到做题的感觉,等nebula的事情忙完了,就专心找工作吧,加油。</p><h1 id="Leetcode-1599"><a href="#Leetcode-1599" class="headerlink" title="Leetcode 1599"></a>Leetcode 1599</h1><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">class Solution {public: int minOperationsMaxProfit(vector<int>& customers, int boardingCost, int runningCost) { int maxProfit=-1; int res=0; int profitSum=0; for(int i=0;i<customers.size();i++){ int tmp=0; if(customers[i]>4&&i!=(customers.size()-1)){ customers[i+1] +=(customers[i]-4); tmp=4; }else{ tmp=customers[i]; } int round =0; if(tmp<4||i+1==customers.size()){ round = tmp/4; profitSum+=((boardingCost*(tmp-tmp%4))-(round)*runningCost); // printf("stop round %d tmp = %d ceil = %d profit = %d\n",i,tmp,round,profitSum); if(maxProfit<profitSum){ maxProfit=profitSum; res=i+round; } profitSum-=((boardingCost*(tmp-tmp%4))-(round)*runningCost); } round = (int)ceil(tmp/4.0); if(round==0 ) round=1; profitSum+=((boardingCost*tmp)-(round)*runningCost); // printf("continue round %d tmp = %d ceil = %d profit = %d\n",i,tmp,round,profitSum); if(maxProfit<profitSum){ maxProfit=profitSum; res=i+round; } } if(maxProfit<=0) return -1; return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>简单的模拟,每轮最多4个人,能坐满就继续运行,大于4个人就把剩下的人放在后一个轮次直到最后一轮,不能坐满就有两种情况,一种停止,一种继续,中间记录最大收益以及所在轮次即可。</p><p><strong>优化空间</strong>:本身题目数据量较小,如果数据大,那么可以使用bool数组标记每个轮次的人是否到达,不再累加,因为可能溢出。一个剪枝,坐满了也不能盈利就直接返回-1。时间复杂度都为O(n)</p><h1 id="LeetCode-1653"><a href="#LeetCode-1653" class="headerlink" title="LeetCode 1653"></a>LeetCode 1653</h1><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">class Solution {public: int minimumDeletions(string s) { int len = s.length(); printf("len = %d\n",len); int a[len+1],b[len+1];//i前面的b和i后面的A int Afront=0,behindB=0; for(int i=0 ;i<len ;i++){ a[i]=Afront; if(s[i]=='b'){ Afront++; } } for(int i=len-1 ;i>=0 ;i--){ b[i]=behindB; if(s[i]=='a'){ behindB++; } } int minn = len; for(int i=0 ;i<len ;i++){ minn = min(a[i]+b[i],minn); } return minn; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>平衡的条件是a前面没b,b后面没a。那么思路很简单,删除该位置前面的b和后面的a就可以让字符串平衡。先跑两遍数组,得到位置i前面的b和i后面的A,然后找两个之和最小的就行。</p><p><strong>优化思路</strong> 就是个最长上升子序列问题,用两个变量维护就行,O(n)复杂度,维护一个前面的b数量,再维护一个目前的操作数量就行,因为每一步只有保留和删除两个选择,即当是a时,op = min(op+ 1, countB),是b时countB+1</p><h1 id="剑指offer-09"><a href="#剑指offer-09" class="headerlink" title="剑指offer 09"></a>剑指offer 09</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class CQueue {public: std::stack<int> stIn,stOut; CQueue() { } void appendTail(int value) { stIn.push(value); } int deleteHead() { if(stOut.empty()){ while(!stIn.empty()){ int val = stIn.top(); stIn.pop(); stOut.push(val); } } if(stOut.empty()){ return -1; } int res = stOut.top(); stOut.pop(); return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>两个栈实现队列,利用栈的特性,后进先出,制造一个输入栈一个输出栈即可,当输出栈为空时,将输入栈全部倒入输出栈,这样就可以有一个先进先出的效果,从而实现队列。</p><h1 id="剑指offer-30"><a href="#剑指offer-30" class="headerlink" title="剑指offer 30"></a>剑指offer 30</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class MinStack {public: /** initialize your data structure here. */ stack<int> stk; stack<int> minNum; int minn; MinStack() { minn=0x7fffffff; } void push(int x) { stk.push(x); minn = minNum.empty()?x:std::min(x,minNum.top()); minNum.push(minn); } void pop() { stk.pop(); minNum.pop(); } int top() { return stk.top(); } int min() { return minNum.top(); }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>多加个最小值,那么就直接维护一个最小栈就行,每次进栈时,加入一个当前值和最小栈栈顶元素更小的元素。想要查看栈最小值时直接输出最小栈的栈顶。还有一个思路就是每次push两个值进栈,用一个minn变量保存当前栈的最小值,pop时pop两次即可。</p><h1 id="LeetCode-1096"><a href="#LeetCode-1096" class="headerlink" title="LeetCode 1096"></a>LeetCode 1096</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution{public: map<int, int> mp; //{} pos bool isLetter(char s) { if ('a' <= s && s <= 'z') return true; return false; } vector<string> multiString(vector<string> const &first, vector<string> const &second) { vector<string> res; for (int i = 0; i < first.size(); i++) { for (int j = 0; j < second.size(); j++) { res.push_back(first[i] + second[j]); } } return res; } vector<string> Process(string expression, int st) { vector<string> res; string tmp; bool flag = false; vector<string> strTmp; for (int i = 0; i < expression.length(); i++) { if (isLetter(expression[i])) { tmp += expression[i]; if (strTmp.size() != 0) { if (tmp != "") { for (int j = 0; j < strTmp.size(); j++) { strTmp[j] = strTmp[j] + tmp; } tmp = ""; } } } else { if (expression[i] == ',') { if (strTmp.size() == 0) { if (tmp != "") { res.push_back(tmp); tmp = ""; } } else { for (int j = 0; j < strTmp.size(); j++) { strTmp[j] = strTmp[j] + tmp; res.push_back(strTmp[j]); } tmp = ""; strTmp.clear(); } } else if (expression[i] == '{') { if (strTmp.size() != 0) { if (tmp != "") { for (int j = 0; j < strTmp.size(); j++) { strTmp[j] = strTmp[j] + tmp; } tmp = ""; } strTmp = multiString(strTmp, Process(expression.substr(i + 1, mp[st + i] - st - i - 1), st + i + 1)); } else { strTmp = Process(expression.substr(i + 1, mp[st + i] - st - i - 1), st + i + 1); } i = mp[st + i] - st; if (tmp != "") { for (int j = 0; j < strTmp.size(); j++) { strTmp[j] = tmp + strTmp[j]; } tmp = ""; } } } } if (strTmp.size() != 0) { for (int j = 0; j < strTmp.size(); j++) { strTmp[j] = tmp + strTmp[j]; res.push_back(strTmp[j]); } strTmp.clear(); tmp = ""; } if (tmp != "") { res.push_back(tmp); tmp = ""; } return res; } vector<string> braceExpansionII(string expression) { stack<int> st; for (int i = 0; i < expression.length(); i++) { if (expression[i] == '{') { st.push(i); } if (expression[i] == '}') { int left = st.top(); st.pop(); mp[left] = i; } } vector<string> res = Process(expression, 0), ans; set<string> myset; for (int i = 0; i < res.size(); i++) { myset.insert(res[i]); } for (auto iter = myset.begin(); iter != myset.end(); iter++) { ans.push_back(*iter); } return ans; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>大体思路很好想,就是递归展开括号内容进行处理,我首先用map保存了每对括号的位置,然后在递归处理,写的时候思路没理清楚,写的比较乱。后续代码其实可以优化的更清晰,就是找到第一个’}’位置j,如果没找到那就把其中所有字符串输出即可。如果找到了,那么找左边第一个’{‘位置i,那么此时字符被分成三部分,第一部分是exp[0,i-1],第二部分是该大括号内容exp[i+1,j-1],第三部分是exp[j+1,n-1]。那么此时再按照逗号分隔第二部分,再进行重新组合内容,进行递归Process(expA,expB[i],expC)即可得到结果。</p><h1 id="剑指Offer-47"><a href="#剑指Offer-47" class="headerlink" title="剑指Offer 47"></a>剑指Offer 47</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int maxValue(vector<vector<int>>& grid) { int dp[grid.size()+1][grid[0].size()+1]; memset(dp,0,sizeof(dp)); dp[0][0]=grid[0][0]; for(int i=0;i<grid.size();i++){ for(int j=0;j<grid[0].size();j++){ if(i>0&&j>0){ dp[i][j]=std::max(dp[i-1][j],dp[i][j-1])+grid[i][j]; }else if(i>0){ dp[i][j]=dp[i-1][j]+grid[i][j]; }else if(j>0){ dp[i][j]=dp[i][j-1]+grid[i][j]; } //printf("dp[%d][%d] = %d\n",i,j,dp[i][j]); } } return dp[grid.size()-1][grid[0].size()-1]; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>简单dp,$dp[i][j]$代表的含义为在坐标(i,j)下,所能拿到的最大值,由题目可知,这个状态只能从$dp[i-1][j],dp[i][j-1]$得到,那么很容易推出dp公式$dp[i][j]=max(dp[i-1][j],dp[i][j-1])+grid[i][j]$</p><h1 id="LeetCode-2379"><a href="#LeetCode-2379" class="headerlink" title="LeetCode 2379"></a>LeetCode 2379</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int minimumRecolors(string blocks, int k) { int white[blocks.length()+5]; int num = 0; memset(white,0,sizeof(white)); for(int i=0;i<blocks.length();i++){ if(blocks[i]=='W') num++; white[i]=num; } int minn = k; for(int i=k-1;i<blocks.length();i++){ int now =0; if(i==k-1) now = white[i]; else now = white[i]-white[i-k]; minn = std::min(minn,now); } return minn; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>滑动窗口,或者前缀和,简单思路就是滑动窗口。我写的空间复杂度是O(n),可以<strong>优化</strong>成O(1)的空间复杂度,把white数组变成两个cnt即可,记录最左边位置之前的白色数量和最右边位置之前的白色数量,每次滑动都维护一下就行。</p><h1 id="LeetCode-704"><a href="#LeetCode-704" class="headerlink" title="LeetCode 704"></a>LeetCode 704</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int search(vector<int>& nums, int target) { int mid = nums.size()/2; int l=0,r=nums.size()-1; while(nums[mid]!=target){ if(l==r) return -1; if(nums[mid]<target) l = mid+1; else if(nums[mid]>target) r = mid-1; mid = (l+r)/2; } return mid; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>简单有序数组二分查找,记得lr每次需要收缩即可</p><h1 id="LeetCode-203"><a href="#LeetCode-203" class="headerlink" title="LeetCode 203"></a>LeetCode 203</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: ListNode* removeElements(ListNode* head, int val) { ListNode *now = head; ListNode *pre = NULL,*next = NULL; while(now!=NULL){ next = now->next; if(now->val == val){ if(pre!=NULL) pre->next = next; else head = next; }else{ pre = now; } now = next; } return head; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>简单链表删除</p><h1 id="LeetCode-27"><a href="#LeetCode-27" class="headerlink" title="LeetCode 27"></a>LeetCode 27</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int removeElement(vector<int>& nums, int val) { int slow=0; for(int i=0;i<nums.size();i++){ if(nums[i]==val){ continue; }else { nums[slow]=nums[i]; slow++; } } return slow; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>快慢指针简单题,慢指针为新数组的下标值</p><h1 id="LeetCode-977"><a href="#LeetCode-977" class="headerlink" title="LeetCode 977"></a>LeetCode 977</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int squares(int num){ return num*num; } vector<int> sortedSquares(vector<int>& nums) { vector<int>res; int negPos=0; int pre=0,next=0; for(int i =0;i<nums.size();i++){ negPos=i; if(nums[i]>=0){ break; } } pre = negPos-1; next = negPos; while(pre>=0&&next<nums.size()){ if(squares(nums[pre])>squares(nums[next])){ res.push_back(squares(nums[next])); next++; }else{ res.push_back(squares(nums[pre])); pre--; } } if(pre>=0){ for(;pre>=0;pre--){ res.push_back(squares(nums[pre])); } } if(next<nums.size()){ for(;next<nums.size();next++){ res.push_back(squares(nums[next])); } } return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>简单题,暴力做法O($n^2$),先全平方后排序。还有个比较好想的做法,还是双指针,找到第一个大于0的数的位置,然后两个指针从中间往两边扫,每次把平方数小的放入数组即可。</p><p>都想到双指针了,怎么没从两边往中间扫。。。。尬住了。代码还更简单</p><h1 id="LeetCode-1605"><a href="#LeetCode-1605" class="headerlink" title="LeetCode 1605"></a>LeetCode 1605</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: vector<vector<int>> restoreMatrix(vector<int>& rowSum, vector<int>& colSum) { int row_len = rowSum.size(),col_len = colSum.size(); vector<vector<int>> res(row_len,vector<int>(col_len,0)); for(int row = 0;row<row_len;row++){ for(int col = 0;col<col_len;col++){ if(rowSum[row]<colSum[col]){ res[row][col] = rowSum[row]; rowSum[row]=0; colSum[col]-=res[row][col]; } else{ res[row][col] = colSum[col]; colSum[col]=0; rowSum[row]-=res[row][col]; } } } return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>就是个贪心,我直接选该行该列更小的那个就行了,每次更新一下rowsum和colsum即可。能填就填</p><p>有个小优化思路,即当rowsum[row]=0的时候,直接break就行,因为后面的全是0</p><h1 id="LeetCode-059"><a href="#LeetCode-059" class="headerlink" title="LeetCode 059"></a>LeetCode 059</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: bool check(int p,int n){ if(p==n||p==-1) return true; return false; } vector<vector<int>> generateMatrix(int n) { vector<vector<int>> res(n,vector<int>(n,0)); int dir[4][2]={{0,1},{1,0},{0,-1},{-1,0}}; int x=0,y=-1; int cnt=0; int flag=0; while(cnt<n*n){ cnt++; x+=dir[flag][0]; y+=dir[flag][1]; res[x][y]=cnt; if((check(x+dir[flag][0],n)||check(y+dir[flag][1],n))||res[x+dir[flag][0]][y+dir[flag][1]]!=0){ flag++; flag%=4; } } return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>每次撞到边界或已经存在数的位置就换方向,简单模拟题。注意越界问题即可</p><h1 id="LeetCode-1616"><a href="#LeetCode-1616" class="headerlink" title="LeetCode 1616"></a>LeetCode 1616</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: bool checkPalindromeFormation(string a, string b) { bool front=true,end=true; bool resafront=true,resbend=true; bool resaend=true,resbfront=true; int mid = a.length()/2; for(int i=0;i<mid;i++){ if(front){ if(a[i]!=b[a.length()-1-i]){ front=false; if(b[i]!=b[a.length()-1-i]) resafront=false; if(a[i]!=a[a.length()-1-i]) resaend=false; } }else{ if(b[i]!=b[a.length()-1-i]) resafront=false; if(a[i]!=a[a.length()-1-i]) resaend=false; } if(end){ if(b[i]!=a[a.length()-1-i]){ end=false; if(a[i]!=a[a.length()-1-i]) resbend=false; if(b[i]!=b[a.length()-1-i]) resbfront=false; } }else{ if(a[i]!=a[a.length()-1-i]) resbend=false; if(b[i]!=b[a.length()-1-i]) resbfront=false; } } return resafront||resbend||resbfront||resaend; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>思路简单,题目要求aprefix + bsuffix或者bprefix + asuffix为回文串,那么可以确定的就是a的前面部分和b的后面部分或者b的前部分和a的后部分一定是相同的,然后中间那块可能是a的也可能是b的,分类讨论,共4种情况,apre+amid+bsuf,apre+bmid+bsuf,bpre+amid+asuf,bpre+bmid+asuf,都判断下就好了</p><p>代码优化,其实代码重复部分过多,可以写成函数</p><h1 id="LeetCode-1625"><a href="#LeetCode-1625" class="headerlink" title="LeetCode 1625"></a>LeetCode 1625</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: string addSum(string now,int a){ for(int i=0;i<now.length();i++){ if(i&1) { now[i]=((now[i]-'0'+a)%10) + '0'; } } return now; } string shuffledString(string now,int b){ string res=""; for(int i=now.length()-b;i<2*now.length()-b;i++){ res+=now[i%now.length()]; } return res; } string findLexSmallestString(string s, int a, int b) { queue<string> q; set<string> res; unordered_map<string,bool> mp; q.push(s); mp[s]=true; while(!q.empty()){ string now = q.front(); res.insert(now); q.pop(); string tmp = addSum(now,a); if(mp.find(tmp)==mp.end()){ mp[tmp]=true; q.push(tmp); } tmp = shuffledString(now,b); if(mp.find(tmp)==mp.end()){ mp[tmp]=true; q.push(tmp); } } return *res.begin(); }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>想不到O(n)的做法,但是感觉可能存在,因为如果b是奇数那么字符串每个都可以为头,如果b是偶数,那么只有偶数位可以为头。可能可以根据序列差值处理出结果。</p><p>暴力模拟,BFS最好写,还有个模拟方法就是纯暴力,很容易知道,a最多加10次就加到原来的值,b最多挪n次,也回到原来的位置,如果b是奇数就是$n<em>10</em>10$种情况,偶数就为$n*10$种情况,时间复杂度就是在这基础上乘个n,因为需要处理字符串</p><h1 id="LeetCode-15"><a href="#LeetCode-15" class="headerlink" title="LeetCode 15"></a>LeetCode 15</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int check(int a,int b,int c){ if(a+b+c==0) return 0; else if(a+b+c>0) return 1; else return -1; } vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> res; sort(nums.begin(),nums.end()); for(int i=0;i<nums.size();i++){ int left=i+1,right=nums.size()-1; if(nums[i]>0) return res; if(i>0&&nums[i]==nums[i-1]) continue; while(left<right){ int flag = check(nums[left],nums[i],nums[right]); if(flag>0)right--; else if(flag<0)left++; else { res.push_back({nums[i],nums[left],nums[right]}); while(left<right&&nums[left]==nums[left+1]) left++; while(left<right&&nums[right]==nums[right-1]) right--; right--; left++; } } } return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>$O(n^3)$是纯暴力做法,下面双指针和哈希表都是$O(n^2)$的做法</p><p>双指针或者哈希表,我写的是双指针,固定一个值,两个指针不停调整,如果三数和大于0,那么就把大的调小,小于0则小的调大,等于0 就是答案,期间去重就行</p><p>哈希表写起来更简单,双重for循环,然后用哈希表判断-(nums[i]+nums[j])在表中有没有,有就是答案,没有就不是答案,当然,还得去重</p><h1 id="LeetCode-18"><a href="#LeetCode-18" class="headerlink" title="LeetCode 18"></a>LeetCode 18</h1><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">class Solution {public: int check(long long a,long long b,long long c,long long d,long long target){ if(a+b+c+d-target==0) return 0; else if(a+b+c+d-target>0) return 1; else return -1; } vector<vector<int>> fourSum(vector<int>& nums, int target) { vector<vector<int>> res; sort(nums.begin(),nums.end()); for(int k=0;k<nums.size();k++){ if(k>0&&nums[k]==nums[k-1])continue; for(int i=k+1;i<nums.size();i++){ int left=i+1,right=nums.size()-1; if(i>k+1&&nums[i]==nums[i-1]) continue; while(left<right){ int flag = check(nums[left],nums[i],nums[right],nums[k],target); if(flag>0)right--; else if(flag<0)left++; else { res.push_back({nums[k],nums[i],nums[left],nums[right]}); while(left<right&&nums[left]==nums[left+1]) left++; while(left<right&&nums[right]==nums[right-1]) right--; right--; left++; } } } } return res; }};<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>同一个做法,复杂度$O(n^3)$,但是有个细节,因为num范围是1e9所以会爆int,定义成longlong就可以了。固定两个值nums[k]和nums[i],两个指针left和right,不停调整,因为遍历两个值需要两个for所以复杂度多个n</p>]]></content>
<categories>
<category> tech </category>
<category> cpp </category>
<category> job </category>
</categories>
<tags>
<tag> tech </tag>
<tag> cpp </tag>
<tag> job </tag>
</tags>
</entry>
<entry>
<title>VLDB 2023 treeline</title>
<link href="//post/treeline.html"/>
<url>//post/treeline.html</url>
<content type="html"><![CDATA[<h1 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h1><p>LSM Tree将随机写变为顺序写,提高了写性能,但是它必须依赖于压缩以及布隆过滤器来维持读性能。但是NVMe SSD的出现,读写性能的Tradeoff就不需要再考虑了,在并发的情况下,在原地更新的方案也可以提供优异的读写性能,是一个可以替代LSM Tree的方案。</p><p>3个点</p><ol><li>record caching for efficient point operation<ul><li>将热数据记录尽可能缓存,并且存储到尽可能一个page上</li></ul></li><li>page grouping for high-performance range scans<ul><li>对page分组,将key值相邻的优先分到一起,让他们连续存储,对scan更友好。</li><li>对于索引只需要存page group的索引,可以让内存中的索引表变得更小</li></ul></li><li>insert forecasting to reduce the reorganization costs of accommodating new records<ul><li>根据观察到的插入,预测在page需要预留出多大的空间</li></ul></li></ol><p><img src="treeline/image-20221205110529342.png" alt="TreeLine design"></p><hr><p>RocksDB和LevelDB都是使用LSM树的,LSM树的基础概念就是writebatch,先在内存写,memtable到一定规模了再全部写入到存储,到存储就不可变了,并且存储会进行异步的压缩。核心思想就是将随机写变为顺序写,但是影响读性能,一条记录可能出现在多个块上。但是这种trade off在传统磁盘是个很好的选择,但是现在NVMe SSD,在并行度足够高时,随机写是可以达到顺序写入吞吐量峰值的。</p><h1 id="TreeLine"><a href="#TreeLine" class="headerlink" title="TreeLine"></a>TreeLine</h1><p><strong>查询流程</strong>:先查内存的缓存,如果缓存未命中那么查内存index,映射到对应的数据段,把对应的page读到内存,如果还是没找到,就去OverFlow page找,如果还是没找到,就返回未找到。</p><p><strong>数据更改流程</strong>:insert,update,delete先创建或者更新缓存中的record的entry,如果没在内存,那TreeLine先用index找对应的page,读入内存,然后执行操作</p><p><strong>范围查询</strong>:和查询一样,但是缓存和磁盘都得查,然后以key的顺序来合并,如果有重复的,缓存数据将覆盖disk的数据</p><h2 id="1-Record-Cache-Key-Idea-A"><a href="#1-Record-Cache-Key-Idea-A" class="headerlink" title="1. Record Cache(Key Idea A)"></a>1. Record Cache(Key Idea A)</h2><h3 id="Cache-Admittance"><a href="#Cache-Admittance" class="headerlink" title="Cache Admittance"></a>Cache Admittance</h3><p>当TreeLine同意记录进入缓存时,都会给一个优先级$0\rightarrow P_{max} $后续的每次访问都会增加优先级。</p><p>数据记录在三种情况下可以进入缓存,</p><blockquote><ol><li>用户的任意数据修改(insert,update,delete),赋予初始优先级$P<em>{mid}=P</em>{max}/2$,如果缓存未缓存这个entry,TreeLine会清理一些entry</li><li>查询未缓存的记录,赋予初始优先级$P_{mid}$并缓存</li><li>在查询或修改时,缓存同一个page的数据,赋予初始优先级$P_1$</li></ol></blockquote><h3 id="Cache-Evivtion"><a href="#Cache-Evivtion" class="headerlink" title="Cache Evivtion"></a>Cache Evivtion</h3><p>使用clock algorithm,周期性的降低优先级直到找到一个entry优先级为0,如果该entry被修改过,那么找下一个,因为如果被修改过,就需要先IO将其刷回到磁盘,我们希望减少IO操作,如果entry全是脏的,就把所有脏entry一起写入,到一个page</p><h2 id="2-In-Memory-Index"><a href="#2-In-Memory-Index" class="headerlink" title="2. In-Memory Index"></a>2. In-Memory Index</h2><p>B+树索引,在换出脏页时会映射到合适的位置去写,以及查询时,映射到合适的位置去读</p><p>index存在$key \rightarrow segment$的映射segment到page直接用二分查,因为是线性模型,segment有序</p><p>index的更新只在reorganization时发生</p><h3 id="Pages-and-Segments"><a href="#Pages-and-Segments" class="headerlink" title="Pages and Segments"></a>Pages and Segments</h3><p>Page,沿用LeanStore的Page设计,该Page负责一个范围的key值,该范围在创建时定义</p><p>Segment是1个或多个page组成</p><h2 id="3-Supproting-a-Growing-Database-Key-Idea-B"><a href="#3-Supproting-a-Growing-Database-Key-Idea-B" class="headerlink" title="3. Supproting a Growing Database(Key Idea B)"></a>3. Supproting a Growing Database(Key Idea B)</h2><h3 id="Overflow-Pages"><a href="#Overflow-Pages" class="headerlink" title="Overflow Pages"></a>Overflow Pages</h3><p>当data page满了,TreeLine分配一个Overflow page,这个page不被index索引,这个Overflow page布局类似base page,继承负责相同的key range,当Overflow page满了,这个Segment就得reorganized</p><h3 id="Page-Orderings"><a href="#Page-Orderings" class="headerlink" title="Page Orderings"></a>Page Orderings</h3><p>physical order,物理存储的顺序</p><p>logical order ,ssd driver自己抽象出来的逻辑上的顺序</p><p>key order,TreeLine中每个page负责一个key range,以这个key的下界排序</p><h3 id="Reorganization"><a href="#Reorganization" class="headerlink" title="Reorganization"></a>Reorganization</h3><p>4个阶段,range detection后是一次或者多次model building和segment write-out,再是index update</p><p><img src="treeline/image-20221205174251911.png" alt="Reorganization"></p><h4 id="Phase-1:Range-detection"><a href="#Phase-1:Range-detection" class="headerlink" title="Phase 1:Range detection"></a>Phase 1:Range detection</h4><p>在某个Segment的overflow page满了之后需要reorganization,会将相邻的r(默认为5)个Segment检查,如果存在overflow page就一起reorganize,也就是一次性最多reorganization 2r+1个segment</p><h4 id="Phase-2:Model-building"><a href="#Phase-2:Model-building" class="headerlink" title="Phase 2:Model building"></a>Phase 2:Model building</h4><p>PGM index’s piecewise linear regression algorithm,我的理解是用这个可以将page更合理的放在尽可能少的Segment中。</p><p>线性模型让空间效率提高,并且提供了可接受的准确率。我们可以通过配置错误阈值 epsilon来实现一个正确率和空间利用率的trade off</p><p>一旦到错误阈值,Phase 2结束,已经处理过的数据records会发送到Phase 3作为write out set,并且在重复Phase 2之前移出reorganization set。</p><p>第二阶段的迭代会在3个情况下停止</p><ol><li>reorganization set适合单个页面,无需任何模型,直接转发到Phase3</li><li>已经超过为reorganization预留的内存空间了,在模型构建期间,保存处理过record的page都在内存里,此时直接转发records和模型到Phase3</li><li>处理了足够的record来适应16 page segment</li></ol><h4 id="Phase-3:-Segment-Write-out"><a href="#Phase-3:-Segment-Write-out" class="headerlink" title="Phase 3: Segment Write-out"></a>Phase 3: Segment Write-out</h4><p>从Phase2接收到Write out set和model,一个linear model就是一个segment,在磁盘上找一个连续空间将其存储。page数量由write out set 和自定义参数goal来确定,goal的含义是,一个page的存储率,也就是刚重组时填多少个record。而且Treeline的Segment大小只能为1,2,4,8,16pages,所以如果发过来10个page,只会分配8page的segment,将剩下的两个page的record返回reorganization set。并且生成segmentID</p><h4 id="Phase-4:Index-Update"><a href="#Phase-4:Index-Update" class="headerlink" title="Phase 4:Index Update"></a>Phase 4:Index Update</h4><p>当reorganization set为空时则更新in-memory index,表明reorganization结束了。之前的所有entries相关的数据全部删除,将SegmentID标为0,并加入到free list中,以后reorganization可能会将其覆盖</p><h2 id="4-Insert-Forecasting-Key-Idea-C"><a href="#4-Insert-Forecasting-Key-Idea-C" class="headerlink" title="4. Insert Forecasting(Key Idea C)"></a>4. Insert Forecasting(Key Idea C)</h2><h3 id="Tracking-Insert"><a href="#Tracking-Insert" class="headerlink" title="Tracking Insert"></a>Tracking Insert</h3><p>把Workload分为Epochs,每x个insert(默认100000)为一个epoch,每个epoch建立b个partitions的直方图,每个epoch使用前一个epoch生成的直方图预测未来的可能插入,分区按照上一个epoch的数据来确定上下界。</p><h3 id="Generating-Forecasts"><a href="#Generating-Forecasts" class="headerlink" title="Generating Forecasts"></a>Generating Forecasts</h3><p>通过这个直方图预测未来f个epoch的直方图</p><h3 id="Utilizing-Forecast"><a href="#Utilizing-Forecast" class="headerlink" title="Utilizing Forecast"></a>Utilizing Forecast</h3><p>设置目标Goal,尽可能减少reorganization的同时减少空间浪费</p><h3 id="Interaction-with-Page-Grouping"><a href="#Interaction-with-Page-Grouping" class="headerlink" title="Interaction with Page Grouping"></a>Interaction with Page Grouping</h3><p>预测插入可能会影响Goal参数的设定,进而影响epsilon参数,再影响到线性模型的构建</p><h2 id="5-Thread-Synchronization"><a href="#5-Thread-Synchronization" class="headerlink" title="5. Thread Synchronization"></a>5. Thread Synchronization</h2><p>两种锁,Segment lock和Page lock</p><p><img src="treeline/image-20221205215339031.png" alt="相容性矩阵"></p><p>Segment locks can be acquired in <strong>I</strong>ntention <strong>R</strong>ead, <strong>I</strong>ntention <strong>W</strong>rite, re<strong>O</strong>rganization or re<strong>O</strong>rganization e<strong>X</strong>clusive mode. Page locks can be acquired in <strong>S</strong>hared or e<strong>X</strong>clusive mode.</p><p>reorganization在第一阶段结束加O锁,第四阶段开始加OX锁</p><h1 id="Evaluation"><a href="#Evaluation" class="headerlink" title="Evaluation"></a>Evaluation</h1><p>和LSM-Based system及其他update in place system比较</p><p><img src="treeline/image-20221205225018846.png" alt="image-20221205225018846"></p><p>数据集线性与否,累计分布函数,越是一条直线,越容易被linear model拟合</p><h2 id="系统对比"><a href="#系统对比" class="headerlink" title="系统对比"></a>系统对比</h2><p>RocksDB:LSM-Based system<br>LeanStore:先进的 update-in-place KV system</p><p><img src="treeline/image-20221205221201746.png" alt="image-20221205221201746"></p><p><img src="treeline/image-20221205221217565.png" alt="image-20221205221217565"></p><ol><li>利用缓存record来减少写放大问题</li><li>提供高效的读</li></ol><p>hot key不在同一个page,并且record 64B,page4KB,读写放大太严重,hot key的record不全在内存。</p><p>RocksDB的LSM设计导致需要读放大</p><p><img src="treeline/image-20221205223031062.png" alt="image-20221205223031062"></p><p><strong>三个真实数据集下跑YCSB E</strong></p><p>当record是1024B,page是4KB的时候,LeanStore的性能就上来了,因为读写放大问题不再严重</p><h2 id="Record-Cache和Page-Grouping的性能提升"><a href="#Record-Cache和Page-Grouping的性能提升" class="headerlink" title="Record Cache和Page Grouping的性能提升"></a>Record Cache和Page Grouping的性能提升</h2><p><img src="treeline/image-20221205223446015.png" alt="image-20221205223446015"></p><p>AMZON的数据集,Record大小为1024,16个线程</p><p>结论</p><ol><li>Record Cache在读写负载中减少了读写放大</li><li>Page grouping的开销不大,不会造成性能影响</li><li>Page grouping可以加速Scan</li></ol><h2 id="Page-Grouping的两个参数对性能的影响"><a href="#Page-Grouping的两个参数对性能的影响" class="headerlink" title="Page Grouping的两个参数对性能的影响"></a>Page Grouping的两个参数对性能的影响</h2><p><img src="treeline/image-20221205223933610.png" alt="image-20221205223933610"></p><p>Epsilon越大,意味着错误阈值越高,可以有更多的record来拟合模型,并且可以一个Segment可以有更多的page</p><p>Goal越大,意味着一个page预留的位置越小,后续reorganization次数越多</p><h2 id="Insert-forecasting的影响"><a href="#Insert-forecasting的影响" class="headerlink" title="Insert forecasting的影响"></a>Insert forecasting的影响</h2><p><img src="treeline/image-20221205225140253.png" alt="image-20221205225140253"></p><p>50读50插入,Epoch为100000,直方图分区数b为20000,预测的为未来100个epoch。</p><p>在record为64B时更有效,这意味着,reorganization次数会因为预测减少的效果更显著。</p><p>epoch粒度越细,不仅预测不准而且影响性能</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
<category> storage </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
<tag> storage </tag>
</tags>
</entry>
<entry>
<title>linux-command</title>
<link href="//post/linux-command.html"/>
<url>//post/linux-command.html</url>
<content type="html"><![CDATA[<p>记录我常用的linux命令</p><h1 id="scp"><a href="#scp" class="headerlink" title="scp"></a>scp</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">scp local_file remote_username@remote_ip:remote_folder scp local_file remote_username@remote_ip:remote_file scp local_file remote_ip:remote_folderscp local_file remote_ip:remote_file<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>没有指定用户名,命令执行后需要输入用户名和密码,仅指定了远程的目录,文件名字不变。</p><h1 id="查看linux信息"><a href="#查看linux信息" class="headerlink" title="查看linux信息"></a>查看linux信息</h1><h2 id="查看内核版本"><a href="#查看内核版本" class="headerlink" title="查看内核版本"></a>查看内核版本</h2><ul><li><code>cat /proc/version</code></li><li><code>uname -a</code></li></ul><h2 id="查看linux版本信息"><a href="#查看linux版本信息" class="headerlink" title="查看linux版本信息"></a>查看linux版本信息</h2><ul><li><p><code>lsb_release -a</code></p></li><li><p><code>cat /etc/issue</code></p></li></ul><h2 id="查看系统的架构"><a href="#查看系统的架构" class="headerlink" title="查看系统的架构"></a>查看系统的架构</h2><ul><li><code>dpkg --print-architecture</code></li><li><code>arch</code></li><li><code>file /lib/systemd/systemd</code></li></ul><blockquote><p>x86_64,x64,AMD64基本上是同一个东西</p><ul><li>x86是intel开发的一种32位指令集</li><li>x84_64是CPU迈向64位的时候</li><li>x86_64是一种64位的指令集,x86_64是x86指令的超集,在<code>x86</code>上可以运行的程序,在<code>x86_64</code>上也可以运行,<code>x86_64是AMD发明的,也叫AMD64</code></li></ul><p>现在用的<code>intel/amd的桌面级CPU</code>基本上都是<code>x86_64</code>,与之相对的arm、pcc等都不是<code>x86_64</code></p></blockquote>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> tools </tag>
</tags>
</entry>
<entry>
<title>zeromq</title>
<link href="//post/zeromq.html"/>
<url>//post/zeromq.html</url>
<content type="html"><![CDATA[<p>zmq安装</p><p>我安装的是cppzmq 4.7.1 libzmq 4.3.3</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt-get install libtool pkg-config build-essential autoconf automake#install libsodiumgit clone https://gitee.com/Codebells/libsodium.gitcd libsodium./autogen.sh -s./configure && make checkcd ..make installsudo ldconfig#install libzmqunzip libzmq-4.3.3.zip./autogen.sh./configure && make checksudo make installsudo ldconfigcd ..#install cppzmqunzip cppzmq-4.7.1.zipcd cppzmq-4.7.1/mkdir buildcd buildcmake ..make -j4 install#或者直接拷贝zmq.cppgit clone https://github.com/zeromq/cppzmq.gitcd cppzmqsudo cp zmq.hpp /usr/local/include/cd ..<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>测试demo server</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <stdio.h>#include <unistd.h>#include <string.h>#include <assert.h>#include <zmq.h>int main (void) { // Socket to talk to clients void *context = zmq_ctx_new (); void *responder = zmq_socket (context, ZMQ_REP); int rc = zmq_bind (responder, "tcp://*:5555"); assert (rc == 0); while (1) { char buffer [10]; zmq_recv (responder, buffer, 10, 0); printf ("Received Hello\n"); sleep (1); // Do some 'work' zmq_send (responder, "World", 5, 0); } return 0; }<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>测试 client.cpp</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <zmq.h>#include <string.h>#include <stdio.h>#include <unistd.h>int main (void){ printf ("Connecting to hello world server…\n"); void *context = zmq_ctx_new (); void *requester = zmq_socket (context, ZMQ_REQ); zmq_connect (requester, "tcp://localhost:5555"); int request_nbr; for (request_nbr = 0; request_nbr != 10; request_nbr++) { char buffer [10]; printf ("Sending Hello %d…\n", request_nbr); zmq_send (requester, "Hello", 5, 0); zmq_recv (requester, buffer, 10, 0); printf ("Received World %d\n", request_nbr); } zmq_close (requester); zmq_ctx_destroy (context); return 0;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">g++ zserver.cpp -o server -lzmq g++ zclient.cpp -o client -lzmq<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> 日常 </tag>
</tags>
</entry>
<entry>
<title>protobuf</title>
<link href="//post/protobuf.html"/>
<url>//post/protobuf.html</url>
<content type="html"><![CDATA[<p>安装的是c++版本protobuf</p><p>到下面去下载想要的版本</p><blockquote><p><a href="https://github.com/protocolbuffers/protobuf/releases">https://github.com/protocolbuffers/protobuf/releases</a></p></blockquote><p>我下载的是<code>protobuf-cpp-3.20.1.tar.gz</code></p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt-get install autoconf automake libtool curl make g++ unziptar xvfz protobuf-cpp-3.20.1.tar.gzcd protobuf-3.20.1# $(nproc) ensures it uses all cores for compilation./configuremake -j$(nproc) make checksudo make install# refresh shared library cache.sudo ldconfig <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p><img src="protobuf/image-20221105105736939.png" alt="make check显示"></p><p>在2.2.0版本后,在编译时需要传递各种链接才能完成编译,protobuf使用pkg-config管理</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">pkg-config --cflags protobuf # print compiler flagspkg-config --libs protobuf # print linker flagspkg-config --cflags --libs protobuf # print both<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>For example:</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">c++ my_program.cc my_proto.pb.cc `pkg-config --cflags --libs protobuf`<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>protobuf Example</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cd examplesmake cpp./add_person_cpp addressbook.data./list_people_cpp addressbook.data<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>通过查看代码,在.proto文件里有数据的最基本定义</p><div class="code-wrapper"><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf">syntax = "proto3";package tutorial;import "google/protobuf/timestamp.proto";//导入想要使用的包message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; google.protobuf.Timestamp last_updated = 5;}// Our address book file is just one of these.message AddressBook { repeated Person people = 1;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>在使用时,使用protoc </p><h1 id="使用记录"><a href="#使用记录" class="headerlink" title="使用记录"></a>使用记录</h1><h2 id="mutable"><a href="#mutable" class="headerlink" title="mutable_*()"></a>mutable_*()</h2><p>protobuf mutable_* 函数</p><p>从该函数的实现上来看,该函数返回指向该字段的一个指针。同时将该字段置为被设置状态。</p><p>若该对象存在,则直接返回该对象,若不存在则新new 一个。</p><h2 id="string和bytes类型"><a href="#string和bytes类型" class="headerlink" title="string和bytes类型"></a>string和bytes类型</h2><p>protobuf中有string 和 bytes两种数据类型, 相对应于python中的 string和 bytes类型。但在C++</p><p>中有std::string 却没有bytes类型。他们之间怎么转换。</p><p>看了一些介绍得到的结论是:</p><p>(1)在C++中,protobuf的<a href="https://so.csdn.net/so/search?q=string类&spm=1001.2101.3001.7020">string类</a>型和bytes类型都对应与C++的std::string类型</p><p>(2)区别是,protobuf中string 对应的 std::string 类型需进行UTF8字符的检查,而bytes对应的std::string类型三不进行UTF8字符检查的</p><h2 id="proto对象定义"><a href="#proto对象定义" class="headerlink" title="proto对象定义"></a>proto对象定义</h2><div class="code-wrapper"><pre class="line-numbers language-protobuf" data-language="protobuf"><code class="language-protobuf">package proto;message Message{ oneof type { Transaction txn = 1; }}message Transaction{ repeated Row row = 1; uint64 start_epoch = 2; TxnType txn_type = 3;}enum TxnType { ClientTxn = 0; RemoteServerTxn = 1; EpochEndFlag = 2; CommittedTxn = 3;}message Row{ string key = 1; bytes data = 2;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">auto msg = std::make_unique<proto::Message>();// 定义一个Message对象 auto apply = msg->mutable_txn();//oneof使用mutable_name()调用,定义该对象是哪种类型apply->set_txn_type(proto::TxnType::ClientTxn);//添加一个enum值apply->set_start_epoch(1);//设置一个uint64值auto row = apply->add_row();//添加repeated对象row->set_table_name("table");row->set_key("key1");//bytes 和 string的区别在于序列化,string类型在序列化utf-8时会出现问题auto msg_ptr = std::make_unique<proto::Message>();if (msg_ptr->type_case() == proto::Message::TypeCase::kTxn){//oneof前要加个k txn_ptr = std::make_unique<proto::Transaction>(*(msg_ptr->release_txn())); uint64_t epoch = txn_ptr->start_epoch(); for(auto i = 0; i < txn_ptr->row_size(); i ++){ const auto& row = txn_ptr->row(i); printf("%s %s\n",row.key(),row.data()); }}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> tech </tag>
</tags>
</entry>
<entry>
<title>cmake日常</title>
<link href="//post/cmake.html"/>
<url>//post/cmake.html</url>
<content type="html"><![CDATA[<h1 id="ubuntu20-04-cmake-升级"><a href="#ubuntu20-04-cmake-升级" class="headerlink" title="ubuntu20.04 cmake 升级"></a>ubuntu20.04 cmake 升级</h1><p>阿里云自带的是3.16的,</p><p><strong>有坑!</strong></p><p>不要升级内核</p><p><code>sudo apt install build-essential libssl-dev</code>不推荐执行,升级内核有可能造成服务器异常</p><p><code>apt-get autoremove cmake</code>不推荐执行,会把之前安装的一些库包都删除了,以后如果要卸载什么包,最好用命令:<code>sudo apt remove package_name</code>,<code>atutoremove</code>命令不要轻易用</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">sudo apt remove cmakewget https://cmake.org/files/v3.22/cmake-3.22.1-Linux-x86_64.tar.gztar zxvf cmake-3.22.1-linux-x86_64.tar.gz#建立软连接sudo ln -s /root/cmake-3.22.1-linux-x86_64/bin/cmake /usr/bin/cmakecmake --version<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p><a href="https://github.com/richardchien/modern-cmake-by-example">richardchien/modern-cmake-by-example: IPADS 实验室新人培训第二讲:CMake(2021.11.3) (github.com)</a></p><h2 id="CentOS7-6-Cmake-升级"><a href="#CentOS7-6-Cmake-升级" class="headerlink" title="CentOS7.6 Cmake 升级"></a>CentOS7.6 Cmake 升级</h2><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">wget https://cmake.org/files/v3.22/cmake-3.22.0.tar.gztar -zxvf cmake-3.22.0.tar.gzcd cmake-3.22.0yum install -y openssl openssl-devel./bootstrap && make && sudo make installcmake --version<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cmake -B build #在build目录建立makefile等文件cmake --build build#在build目录开始编译#或者直接并行执行makefile的编译make -j$(nproc)#把标准输出重定向到文件中make -j 32 2>&1 | tee log.txt<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h1><p>最基本的是将源码构建成可执行文件,最简单的就是一个三行的<code>CMakelist.txt</code>分别定义了cmake的最低版本号,项目名,可执行文件</p><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake">cmake_minimum_required(VERSION 3.20.1)project(hello)add_executable(hello hello.cpp)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><h2 id="生成-amp-构建-amp-运行命令:"><a href="#生成-amp-构建-amp-运行命令:" class="headerlink" title="生成 & 构建 & 运行命令:"></a>生成 & 构建 & 运行命令:</h2><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cmake -B build # 生成构建目录,-B 指定生成的构建系统代码放在 build 目录cmake --build build # 执行构建./build/hello # 运行 hello 程序<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><h2 id="将文件看成library"><a href="#将文件看成library" class="headerlink" title="将文件看成library"></a>将文件看成library</h2><p>例如许多文件都需要answer.cpp依赖我们就可以将其定义为library</p><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake">add_library(libanswer STATIC answer.cpp)#当需要使用时,使用target_link_libraries就可以add_executable(answer main.cpp)target_link_libraries(answer libanswer)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="将文件放到单独子目录"><a href="#将文件放到单独子目录" class="headerlink" title="将文件放到单独子目录"></a>将文件放到单独子目录</h2><p>当项目不断变大时,多级目录是必不可少的,这时需要将每个文件整理的更加结构化,功能独立的模块可以放到一个子目录中<br>├── answer<br>│ ├── answer.cpp<br>│ ├── CMakeLists.txt<br>│ └── include<br>│ └── answer<br>│ └── answer.hpp<br>├── CMakeLists.txt<br>└── main.cpp</p><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake"># CMakeLists.txtadd_subdirectory(answer)add_executable(answer_app main.cpp)target_link_libraries(answer_app libanswer) # libanswer 在 answer 子目录中定义# answer/CMakeLists.txtadd_library(libanswer STATIC answer.cpp)target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p><code>${CMAKE_CURRENT_SOURCE_DIR}</code>是内置变量,代表当前<code>CMakeLists.txt</code>所在的目录名</p><p><code>PUBLIC</code>代表这个包含目录是libraries公开的,在其他<code>CMakeList.txt</code>使用libanswer可以直接#include该目录的文件</p><p><code>PRIVATE</code>代表这个是libraries私有的,只有libraries能看到,对于使用libraries的文件不可见</p><h2 id="Cache变量"><a href="#Cache变量" class="headerlink" title="Cache变量"></a>Cache变量</h2><p>私密的 App ID、API Key 等不应该直接放在代码里,应该做成可配置的项,从外部传入。除此之外还可通过可配置的变量来控制程序的特性、行为等。在 CMake 中,通过 cache 变量实现:</p><p><code>set(WOLFRAM_APPID "" CACHE STRING "WolframAlpha APPID")</code></p><p><code>set</code> 第一个参数是变量名,第二个参数是默认值,第三个参数 <code>CACHE</code> 表示是 cache 变量,第四个参数是变量类型,第五个参数是变量描述。</p><p><code>BOOL</code> 类型的 cache 变量还有另一种写法:</p><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake">set(ENABLE_CACHE OFF CACHE BOOL "Enable request cache")option(ENABLE_CACHE "Enable request cache" OFF) # 和上面基本等价<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>Cache 变量的值可在命令行调用 <code>cmake</code> 时通过 <code>-D</code> 传入:</p><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">cmake -B build -DWOLFRAM_APPID=xxx<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>也可用 <code>ccmake</code> 在 TUI 中修改</p><p>要让 C++ 代码能够拿到 CMake 中的变量,可添加编译时宏定义:</p><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake">target_compile_definitions(libanswer PRIVATE WOLFRAM_APPID="${WOLFRAM_APPID}")<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>这会给 C++ 代码提供一个 <code>WOLFRAM_APPID</code> 宏。<code>${WOLFRAM_APPID}</code>是CMakeLists.txt文件中cache变量的值</p><h2 id="header-only"><a href="#header-only" class="headerlink" title="header only"></a>header only</h2><p>也就是.h文件</p><p>Header-only 的库可以添加为 <code>INTERFACE</code> 类型的 library:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">add_library(libanswer INTERFACE)target_include_directories(libanswer INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)target_compile_definitions(libanswer INTERFACE WOLFRAM_APPID="${WOLFRAM_APPID}")target_link_libraries(libanswer INTERFACE wolfram)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>通过 <code>target_xxx</code> 给 <code>INTERFACE</code> library 添加属性都要用 <code>INTERFACE</code>。</p><h2 id="target-complie-features"><a href="#target-complie-features" class="headerlink" title="target_complie_features"></a>target_complie_features</h2><p>可以针对 target 要求编译 feature(即指定要使用 C/C++ 的什么特性):</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">target_compile_features(libanswer INTERFACE cxx_std_20)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>和直接设置 <code>CMAKE_CXX_STANDARD</code> 的区别:</p><ol><li><code>CMAKE_CXX_STANDARD</code> 会应用于所有能看到这个变量的 target,而 <code>target_compile_features</code> 只应用于单个 target</li><li><code>target_compile_features</code> 可以指定更细粒度的 C++ 特性,例如 <code>cxx_auto_type</code>、<code>cxx_lambda</code> 等。</li></ol><h1 id="随笔"><a href="#随笔" class="headerlink" title="随笔"></a>随笔</h1><h2 id="find-package"><a href="#find-package" class="headerlink" title="find_package()"></a>find_package()</h2><p>两种模式 module和config cmake默认采取Module模式,如果Module模式未找到库,才会采取Config模式。也可以显式的使用Config模式指定</p><p><code>find_package(protobuf CONFIG REQUIRED)</code></p><p>不推荐使用指定模式,除非很了解这个包。</p><p> Module模式(模块模式)下是要查找到名为<strong>FindXXX.cmake</strong>的文件。这个文件负责找到库所在的路径,为我们的项目引入头文件路径和库文件路径。cmake搜索这个文件的路径有两个,一个是cmake安装目录下的Modules目录(如root/cmake-3.22.1-linux-x86_64/share/cmake-3.22/Modules),另一个使我们指定的CMAKE_MODULE_PATH的所在目录。</p><p>每一个FindXXX.cmake模块都会定义以下几个变量:</p><ul><li>\<LibaryName>_FOUND:用来判断FindXXX.cmake模块是否被找到;</li><li>\<LibaryName>_INCLUDE_DIR or \<LibaryName>_INCLUDES;</li><li>\<LibaryName>_LIBRARY or \<LibaryName>_LIBRARIES;</li></ul><h2 id="list"><a href="#list" class="headerlink" title="list()"></a>list()</h2><div class="code-wrapper"><pre class="line-numbers language-cmake" data-language="cmake"><code class="language-cmake">list(LENGTH <list><output variable>) list(GET <list> <elementindex> [<element index> ...]<output variable>) list(APPEND <list><element> [<element> ...]) list(FIND <list> <value><output variable>) list(INSERT <list><element_index> <element> [<element> ...]) list(REMOVE_ITEM <list> <value>[<value> ...]) list(REMOVE_AT <list><index> [<index> ...]) list(REMOVE_DUPLICATES <list>) list(REVERSE <list>) list(SORT <list>)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>LENGTH 返回list的长度</p><p>GET 返回list中index的element到value中</p><p>APPEND 添加新element到list中</p><p>FIND 返回list中element的index,没有找到返回-1</p><p>INSERT 将新element插入到list中index的位置</p><p>REMOVE_ITEM 从list中删除某个element</p><p>REMOVE_AT 从list中删除指定index的element</p><p>REMOVE_DUPLICATES 从list中删除重复的element</p><p>REVERSE 将list的内容反转</p><p>SORT 将list按字母顺序排序</p><h2 id="file"><a href="#file" class="headerlink" title="file()"></a>file()</h2><p><strong><em>\</em>file(GLOB variable [RELATIVE path] [globbingexpressions]…)**</strong></p><p>GLOB 会产生一个由所有匹配globbing表达式的文件组成的列表,并将其保存到变量中。Globbing 表达式与正则表达式类似,但更简单。如果指定了RELATIVE 标记,返回的结果将是与指定的路径相对的路径构成的列表。 (通常不推荐使用GLOB命令来从源码树中收集源文件列表。原因是:如果CMakeLists.txt文件没有改变,即便在该源码树中添加或删除文件,产生的构建系统也不会知道何时该要求CMake重新产生构建文件。globbing 表达式包括:</p><p> <em>.cxx - match all files with extension cxx </em>.vt? - match all files with extension vta,…,vtz<br> f[3-5].txt - match files f3.txt,f4.txt, f5.txt</p>]]></content>
<categories>
<category> tech </category>
<category> cpp </category>
</categories>
<tags>
<tag> tech </tag>
<tag> cpp </tag>
</tags>
</entry>
<entry>
<title>mit6-824-lab3</title>
<link href="//post/mit6-824-lab3.html"/>
<url>//post/mit6-824-lab3.html</url>
<content type="html"><![CDATA[<p>对于3A来说的话,整体实现并不是很难,在paper中主要对应的是 section8。这次的实验就是实现在lab2中<a href="https://so.csdn.net/so/search?q=raft&spm=1001.2101.3001.7020">raft</a>服务层的上一层service与client的交互。</p><p><img src="mit6-824-lab3/a5b08ca2241d4ec9968fd3543b13f948.png" alt="处理逻辑"></p><ul><li>我们需要进行在client中去编写make,put/get/append等关于RPC又或者clerk初始化的函数。</li><li>然后这个函数的RPC会传到server中对应的put/get/append函数中,再由这些函数调用raft服务层,在raft进行共识。</li><li>最后由raft服务层apply到server中的applyCh中,但是这里返回的msg为raft封装好的command我们需要用自定义的Loop将command封装回传进来的op结构体给server,最后再返回回去。</li></ul><h1 id="代码结构"><a href="#代码结构" class="headerlink" title="代码结构"></a>代码结构</h1><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">type Clerk struct {servers []*labrpc.ClientEnd// You will have to modify this struct.seqId intleaderId int // 确定哪个服务器是leader,下次直接发送给该服务器clientId int64}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><ul><li><strong>对于seqId其实是为了这种情况:</strong></li></ul><blockquote><p>if the leader crashes after committing the log entry but before responding to the client, the client will retry the command with a new leader, causing it to be executed a second time。</p></blockquote><ul><li>如果这个leader在commit log后crash了,但是还没响应给client,client就会重发这条command给新的leader,这样就会导致这个op执行两次。</li><li>而这种解决办法就是每次发送操作时附加一个唯一的序列号去为了标识操作,避免op被执行两次。</li></ul><blockquote><p>If it receives a command whose serial number has already been executed, it responds immediately without re-executing the request.</p></blockquote><ul><li><strong>而leaderId其实是为了下次能够直接发给正确leader(在hint中也有提到)</strong></li></ul><blockquote><p>You will probably have to modify your Clerk to remember which server turned out to be the leader for the last RPC, and send the next RPC to that server first. This will avoid wasting time searching for the leader on every RPC, which may help you pass some of the tests quickly enough.</p></blockquote><p><img src="mit6-824-lab3/73a9980d9ec34ecaa8121ad9dd9078f9.png" alt="3B逻辑"></p><p>对于lab3B来说就是要引入raft2D的快照,去尽可能的减少时间。这里重新画下加上snapshot的结构图.其实就只要在写入操作时,判断持久化的大小需不需要进行快照存储就行。</p>]]></content>
<categories>
<category> database </category>
<category> lab </category>
</categories>
<tags>
<tag> database </tag>
<tag> lab </tag>
</tags>
</entry>
<entry>
<title>cpp随笔</title>
<link href="//post/cpp.html"/>
<url>//post/cpp.html</url>
<content type="html"><![CDATA[<h1 id="CentOS7-6-安装g-11-和gcc-11"><a href="#CentOS7-6-安装g-11-和gcc-11" class="headerlink" title="CentOS7.6 安装g++ 11 和gcc 11"></a>CentOS7.6 安装g++ 11 和gcc 11</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">yum install centos-release-sclyum list dev\*gcc //用于查看可以安装的版本yum install devtoolset-11-gcc devtoolset-11-gcc-c++source /opt/rh/devtoolset-11/enablegcc -vg++ -vecho "source /opt/rh/devtoolset-11/enable" >> /etc/bashrcsource /etc/bashrc <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="智能指针"><a href="#智能指针" class="headerlink" title="智能指针"></a>智能指针</h1><p>C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。</p><p>C++ 智能指针底层是采用<strong>引用计数</strong>的方式实现的。</p><p>简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。</p><h2 id="shared-ptr"><a href="#shared-ptr" class="headerlink" title="shared_ptr\"></a>shared_ptr\<T></h2><p>shared_ptr\<T> 类模板中,提供了多种实用的构造函数</p><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">std::shared_ptr<int> p1; //不传入任何实参std::shared_ptr<int> p2(nullptr); //传入空指针 nullptrstd::shared_ptr<int> p3(new int(10));// 在构建 shared_ptr 智能指针,明确其指向。std::shared_ptr<int> p3 = std::make_shared<int>(10);//C++11 标准中还提供了 std::make_shared<T> 模板函数,可以用于初始化 shared_ptr 智能指针//调用拷贝构造函数std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;//调用移动构造函数std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>&(*xxx_ptr\<T>)将智能指针转换为普通指针,与xxx_ptr\<T>.get()相同</p><h1 id="std标准库"><a href="#std标准库" class="headerlink" title="std标准库"></a>std标准库</h1><h2 id="左值右值"><a href="#左值右值" class="headerlink" title="左值右值"></a>左值右值</h2><p>简单意义上来说,左值就是等式左边的值,右值就是等式右边的值。</p><p>一般意义上来说,</p><p>左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)</p><p>右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)</p><p>便携方法:对表达式取地址,如果能,则为左值,否则为右值</p><p><strong>无论左值引用类型的变量还是右值引用类型的变量,都是左值,因为它们有名字。</strong></p><h3 id="左值引用"><a href="#左值引用" class="headerlink" title="左值引用"></a>左值引用</h3><div class="code-wrapper"><pre class="line-numbers language-text" data-language="text"><code class="language-text">int a = 10;int &b = a; // 定义一个左值引用变量b = 20; // 通过左值引用修改引用内存的值// int &b = 10;是无法编译的,因为10是右值const int &var = 10;//可以通过编译,因为这属于是常引用来引用常量数字10//此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。</p><h3 id="右值引用"><a href="#右值引用" class="headerlink" title="右值引用"></a>右值引用</h3><div class="code-wrapper"><pre class="line-numbers language-text" data-language="text"><code class="language-text">int &&var = 10;<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。</p><p>在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。</p><p>右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。</p><h2 id="std-move"><a href="#std-move" class="headerlink" title="std::move()"></a>std::move()</h2><p>通过调用std::move函数来<strong>获得绑定到左值上的右值引用</strong></p><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include <iostream>#include <utility>#include <vector>#include <string>int main(){ std::string str = "Hello"; std::vector<std::string> v; // uses the push_back(const T&) overload, which means // we'll incur the cost of copying str v.push_back(str); std::cout << "After copy, str is \"" << str << "\"\n"; // uses the rvalue reference push_back(T&&) overload, // which means no strings will be copied; instead, the contents // of str will be moved into the vector. This is less // expensive, but also means str might now be empty. v.push_back(std::move(str)); std::cout << "After move, str is \"" << str << "\"\n"; std::cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n";}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>move告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着:除了对 str 赋值或销毁外,我们再使用它。<strong>在调用move之后,我们不能对moved-from对象(即str)做任何假设。</strong></p><p>上述程序执行结果为</p><p><img src="cpp/20190419114208815.png" alt="move example"></p><h2 id="std-forward"><a href="#std-forward" class="headerlink" title="std::forward"></a>std::forward</h2><p>完美转发</p><p>std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。</p><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p><code>std::forward</code>定义如下:</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">template<class _Ty>_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept{// forward an lvalue as either an lvalue or an rvaluereturn (static_cast<_Ty&&>(_Arg));}CData* Creator(T&& t){return new CData(std::forward<T>(t));}12345678910<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><ol><li>如果T为<code>std::string&</code>,那么<code>std::forward<T>(t)</code> 返回值为<code>std::string&&&</code>, 折叠为<code>std::string&</code>,左值引用特性不变。</li><li>如果T为<code>std::string&&</code>,那么<code>std::forward<T>(t)</code> 返回值为<code>std::string&&&&</code>, 折叠为<code>std::string&&</code>,右值引用特性不变。</li><li>如果调用者为<code>std::string</code>,调用将会转换成为<code>std::string&</code>,为类型1.</li></ol><h1 id="folly库"><a href="#folly库" class="headerlink" title="folly库"></a>folly库</h1><p>Facebook开源的基于C++14的库,在facebook内部广泛使用。Folly的全称为Facebook Open-source Library,目的不是为了替代标准库,而是对标准库的一种补充,尤其是大规模下的性能。</p><p>边记录接触到的组件</p><h2 id="Futures"><a href="#Futures" class="headerlink" title="Futures"></a>Futures</h2><p><a href="https://github.com/facebook/folly/blob/main/folly/docs/Futures.md">promise/future</a> 是一个非常重要的异步编程模型,它可以让我们摆脱传统的回调陷阱,从而使用更加优雅、清晰的方式进行异步编程。c++11中已经开始支持std::future/std::promise,但是提供的future过于简单,而folly的实现中最大的改进就是可以为future添加回调函数(比如then)</p><h3 id="回调函数"><a href="#回调函数" class="headerlink" title="回调函数"></a>回调函数</h3><p>当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为<strong>回调函数</strong>(callback function)。</p><p><img src="cpp/0ef3106510e2e1630eb49744362999f8_720w.png" alt="回调函数示意图"></p><p>回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再<strong>回</strong>过头来<strong>调</strong>用高层的过程。</p><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include <folly/futures/Future.h>using namespace folly;using namespace std;void foo(int x) { // do something with x cout << "foo(" << x << ")" << endl;}// ... cout << "making Promise" << endl; Promise<int> p; Future<int> f = p.getFuture(); f.then(foo); cout << "Future chain made" << endl;// ... now perhaps in another event callback cout << "fulfilling Promise" << endl; p.setValue(42); cout << "Promise fulfilled" << endl;<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p> 代码非常简洁,首先定义一个Promise,然后从这个Promise获取它相关联的Future(通过getFuture接口),之后通过then为这个Future设置了一个回调函数foo,最后当为Promise赋值填充时(setValue),相关的Future就会变为ready状态(或者是completed状态),那么它相关的回调(这里为foo)会被执行。这段代码的打印结果如下:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">making PromiseFuture chain madefulfilling Promisefoo(42)Promise fulfilled<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="虚函数和纯虚函数"><a href="#虚函数和纯虚函数" class="headerlink" title="虚函数和纯虚函数"></a>虚函数和纯虚函数</h1><p>定义一个函数为虚函数,不代表函数为不被实现的函数</p><p>定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。</p><p>定义一个函数为纯虚函数,才代表函数没有被实现。</p><p>定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。</p><p>我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为<strong>动态链接</strong>,或<strong>后期绑定</strong>。</p><h2 id="虚函数"><a href="#虚函数" class="headerlink" title="虚函数"></a>虚函数</h2><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">class A{public: virtual void foo() { cout<<"A::foo() is called"<<endl; }};class B:public A{public: void foo() { cout<<"B::foo() is called"<<endl; }};int main(void){ A *a = new B(); a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的! return 0;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>虚函数它虚就虚在所谓”推迟联编”或者”动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为”虚”函数。</p><p>那什么是纯虚函数呢</p><h2 id="纯虚函数"><a href="#纯虚函数" class="headerlink" title="纯虚函数"></a>纯虚函数</h2><p>纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 <strong>=0</strong>:</p><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">virtual void funtion1()=0<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>纯虚函数存在的意义</p><ol><li>为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。</li><li>在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理</li></ol><p>为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:<strong>virtual ReturnType Function()= 0;</strong>),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。</p><p>声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。</p><p><strong>纯虚函数最显著的特征是</strong>:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。</p><p>定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。</p><p>纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,”你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。</p><p>类中<strong>至少有一个函数被声明为纯虚函数,则这个类就是抽象类</strong>。纯虚函数是通过在声明中使用 “= 0” 来指定的</p><p><<<<<<< HEAD</p><h1 id="读写文件"><a href="#读写文件" class="headerlink" title="读写文件"></a>读写文件</h1><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">std::string getIp(){ ifstream infile; //读文件流 infile.open("ip.txt"); infile>>ipAddress;//读一行数据 infile.close();}void Send_ok(std::vector<KV> &kv){ ofstream outfile;//写文件流 outfile.open("out.txt"); for(auto it : kv){ outfile<<it.first<<"-----"<<it.second<<endl;//写一行数据 } outfile<<"------------------------------------"<<endl; outfile.close();}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="读写IO流"><a href="#读写IO流" class="headerlink" title="读写IO流"></a>读写IO流</h1><ul><li>ofstream:该数据类型表示输出文件流,用于创建文件并向文件写入信息。</li><li>ifstream:该数据类型表示输入文件流,用于从文件读取信息。</li><li>fstream:该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。</li></ul><p>模式</p><ul><li>ios::app:追加模式。所有写入都追加到文件末尾。</li><li>ios::ate:文件打开后定位到文件末尾。</li><li>ios::in:打开文件用于读取。</li><li>ios::out:打开文件用于写入。</li><li>ios::trunc:如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。</li></ul><h2 id="读文件"><a href="#读文件" class="headerlink" title="读文件"></a>读文件</h2><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">#include <fstream>#include <iostream>ifstream infile; infile.open("/root/codetest/ip.txt"); infile>>destIpAddress;infile>>selfIpAddress;infile.close();std::cout<<destIpAddress<<" "<<selfIpAddress<<endl;<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="写文件"><a href="#写文件" class="headerlink" title="写文件"></a>写文件</h2><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">fstream outfile;outfile.open("/root/codetest/out.txt",ios::out|ios::app);//追加for(auto it : kv){ outfile<<it.first<<"-----"<<it.second<<endl;}outfile<<"------------------------------------"<<endl;outfile.close();<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="int-string转换"><a href="#int-string转换" class="headerlink" title="int string转换"></a>int string转换</h1><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">sstring s = "12";int a = atoi(s.c_str());s = std::to_string(a);<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><h1 id="string-分割"><a href="#string-分割" class="headerlink" title="string 分割"></a>string 分割</h1><h2 id="string-char-转换"><a href="#string-char-转换" class="headerlink" title="string char*转换"></a>string char*转换</h2><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">string str="abc";//char*p=(char*)str.data(); 都行char *p=(char*)str.c_str();string x = p;<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="strtok"><a href="#strtok" class="headerlink" title="strtok()"></a>strtok()</h2><p>char <em>strtok(char </em>str, const char *delim)</p><ul><li><strong>str</strong>—要被分解的字符串</li><li><strong>delim</strong>—用作分隔符的字符(可以是一个,也可以是集合)</li></ul><p>该函数返回被分解的<strong>第一个</strong>子字符串,若无可检索的字符串,则返回空指针</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">vector<string> split(const string& str, const string& delim) {vector<string> res;if("" == str) return res;//先将要切割的字符串从string类型转换为char*类型char * strs = new char[str.length() + 1] ; //不要忘了strcpy(strs, str.c_str()); char * d = new char[delim.length() + 1];strcpy(d, delim.c_str()); char *p = strtok(strs, d);while(p) {string s = p; //分割得到的字符串转换为string类型res.push_back(s); //存入结果数组p = strtok(NULL, d);} return res;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="ifdef-ifndef"><a href="#ifdef-ifndef" class="headerlink" title="#ifdef #ifndef"></a>#ifdef #ifndef</h1><p>#ifndef和#ifdef都是一种宏定义判断,作用是防止多重定义。</p><p>使用格式如下:</p><div class="code-wrapper"><pre class="language-none"><code class="language-none">#ifdef 标识符程序段1#else程序段2#endif</code></pre></div><p> 一般的使用场景如下:</p><ol><li>头文件中使用,防止头文件被多重调用</li><li>作为测试使用,省去注释代码的麻烦</li><li>作为不同角色或者场景的判断使用</li></ol><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <iostream>#include "A.h"#define INIT_Busing namespace std;B::B(){#ifdef INIT_Bthis->b=200;#endif}//有时候当代码比较多的时候,要做测试,但是全部注释很麻烦,这时候使用#ifdef非常好用,如果我不想执行//this->b=200;这段程序,只需要将上面的#define INIT_B注释就可以了。<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="static-cast、reinterpret-cast、const-cast、dynamic-cast"><a href="#static-cast、reinterpret-cast、const-cast、dynamic-cast" class="headerlink" title="static_cast、reinterpret_cast、const_cast、dynamic_cast"></a>static_cast、reinterpret_cast、const_cast、dynamic_cast</h1><p>C++ 引入了四种功能不同的强制类型转换运算符以进行强制类型转换主要为了克服C语言强制类型转换的以下三个缺点</p><ol><li><p>没有从形式上体现转换功能和风险的不同。</p><p>例如,将 int 强制转换成 double 是没有风险的,而将常量指针转换成非常量指针,将基类指针转换成派生类指针都是高风险的,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。</p></li><li><p>将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。</p></li><li><p>难以在程序中寻找到底什么地方进行了强制类型转换。</p></li></ol><p>用法</p><blockquote><p>强制类型转换运算符 <要转换到的类型> (待转换的表达式)</p></blockquote><h2 id="static-cast"><a href="#static-cast" class="headerlink" title="static_cast"></a>static_cast</h2><p>static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。</p><p>static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <iostream>using namespace std;class A{public: operator int() { return 1; } operator char*() { return NULL; }};int main(){ A a; int n; char* p = "New Dragon Inn"; n = static_cast <int> (3.14); // n 的值变为 3 n = static_cast <int> (a); //调用 a.operator int,n 的值变为 1 p = static_cast <char*> (a); //调用 a.operator char*,p 的值变为 NULL n = static_cast <int> (p); //编译错误,static_cast不能将指针转换成整型 p = static_cast <char*> (n); //编译错误,static_cast 不能将整型转换成指针 return 0;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="reinterpret-cast"><a href="#reinterpret-cast" class="headerlink" title="reinterpret_cast"></a>reinterpret_cast</h2><p>reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <iostream>using namespace std;class A{public: int i; int j; A(int n):i(n),j(n) { }};int main(){ A a(100); int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a r = 200; //把 a.i 变成了 200 cout << a.i << "," << a.j << endl; // 输出 200,100 int n = 300; A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n pa->i = 400; // n 变成 400 pa->j = 500; //此条语句不安全,很可能导致程序崩溃 cout << n << endl; // 输出 400 long long la = 0x12345678abcdLL; pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u cout << hex << u << endl; //输出 5678abcd typedef void (* PF1) (int); typedef int (* PF2) (int,char *); PF1 pf1; PF2 pf2; pf2 = reinterpret_cast<PF2>(pf1); //两个不同类型的函数指针之间可以互相转换}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>第 19 行的代码不安全,因为在编译器看来,pa->j 的存放位置就是 n 后面的 4 个字节。 本条语句会向这 4 个字节中写入 500。但这 4 个字节不知道是用来存放什么的,贸然向其中写入可能会导致程序错误甚至崩溃。</p><h2 id="const-cast"><a href="#const-cast" class="headerlink" title="const_cast"></a>const_cast</h2><p>const_cast 运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。</p><p>将 const 引用转换为同类型的非 const 引用,将 const 指针转换为同类型的非 const 指针时可以使用 const_cast 运算符</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">const string s = "Inception";string& p = const_cast <string&> (s);string* ps = const_cast <string*> (&s); // &s 的类型是 const string*<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><h2 id="dynamic-cast"><a href="#dynamic-cast" class="headerlink" title="dynamic_cast"></a>dynamic_cast</h2><p>用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象。dynamic_cast专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。</p><p>dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。</p><div class="code-wrapper"><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">#include <iostream>#include <string>using namespace std;class Base{ //有虚函数,因此是多态基类public: virtual ~Base() {}};class Derived : public Base { };int main(){ Base b; Derived d; Derived* pd; pd = reinterpret_cast <Derived*> (&b); if (pd == NULL) //此处pd不会为 NULL。reinterpret_cast不检查安全性,总是进行转换 cout << "unsafe reinterpret_cast" << endl; //不会执行 pd = dynamic_cast <Derived*> (&b); if (pd == NULL) //结果会是NULL,因为 &b 不指向派生类对象,此转换不安全 cout << "unsafe dynamic_cast1" << endl; //会执行 pd = dynamic_cast <Derived*> (&d); //安全的转换 if (pd == NULL) //此处 pd 不会为 NULL cout << "unsafe dynamic_cast2" << endl; //不会执行 return 0;}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>第 20 行,通过判断 pd 的值是否为 NULL,就能知道第 19 行进行的转换是否是安全的。第 23 行同理。</p>]]></content>
<categories>
<category> tech </category>
<category> cpp </category>
</categories>
<tags>
<tag> tech </tag>
<tag> cpp </tag>
</tags>
</entry>
<entry>
<title>学习makefile</title>
<link href="//post/makefile.html"/>
<url>//post/makefile.html</url>
<content type="html"><![CDATA[<p>先给个我的<a href="https://seisman.github.io/how-to-write-makefile/index.html">makefile学习链接</a></p><hr><h1 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">make -j$(nproc) <span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><h1 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h1><p>学习makefile是关于程序编译的必经之路,对于C++来说,编译的过程是这样的,先将C++的代码编译成object 中间文件,在windows是.obj,Linux是.o文件,这就是编译,有了中间文件后,把中间文件整合到一起,变成可执行文件,这个过程叫链接。这个过程涉及到程序的全局变量,函数声明,语言语法语义正确性检查之类的,在正常情况来说,每个cpp文件都会有对应的.o文件,对于这些来说,就需要链接去找寻这些变量是否定义,函数是否声明,语法语义是否正确。但是因为在大型项目文件太多,为了方便起见,就会给中间文件打个包,这就是库文件,在windows是.lib library 文件,Linux是.a archive 文件</p><p>makefile就定义了项目如何编译,怎么链接程序,文件编译的先后顺序,是否需要重新编译等,简化了编译的过程,makefile写得好,一个make指令就可以编译完成。</p><h1 id="makefile规则"><a href="#makefile规则" class="headerlink" title="makefile规则"></a>makefile规则</h1><p>粗略地看一看makefile的规则</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">target ... : prerequisites ... command ... ...<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><ul><li><strong>target</strong>:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。</li><li><strong>prerequisites:</strong>生成该target所依赖的文件和/或target</li><li><strong>command</strong>:该target要执行的命令(任意的shell命令)</li></ul><p>这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:</p><blockquote><p>prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。</p></blockquote><p>这就是makefile的规则,也就是makefile中最核心的内容。</p><p>看个例子</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">edit : main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o cc -o edit main.o kbd.o command.o display.o \ insert.o search.o files.o utils.omain.o : main.c defs.h cc -c main.ckbd.o : kbd.c defs.h command.h cc -c kbd.ccommand.o : command.c defs.h command.h cc -c command.cdisplay.o : display.c defs.h buffer.h cc -c display.cinsert.o : insert.c defs.h buffer.h cc -c insert.csearch.o : search.c defs.h buffer.h cc -c search.cfiles.o : files.c defs.h buffer.h command.h cc -c files.cutils.o : utils.c defs.h cc -c utils.cclean : rm edit main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>‘\‘是换行的意思,在该项目目录下,就可以直接执<code>make</code>就可以生成可执行文件edit,执行<code>make clean</code>就可以删除中间目标文件及可执行文件edit。</p><p>每一个 <code>.o</code> 文件都有一组依赖文件,而这些 <code>.o</code> 文件又是执行文件 <code>edit</code> 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。</p><p>在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 <code>Tab</code> 键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。</p><p><code>clean</code> 不是一个文件,它只不过是一个动作名字,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。</p><h2 id="makefile过程"><a href="#makefile过程" class="headerlink" title="makefile过程"></a>makefile过程</h2><p>在默认的方式下,也就是我们只输入 <code>make</code> 命令。那么,</p><ol><li>make会在当前目录下找名字叫“Makefile”或“makefile”的文件。</li><li>如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。</li><li>如果edit文件不存在,或是edit所依赖的后面的 <code>.o</code> 文件的文件修改时间要比 <code>edit</code> 这个文件新,那么,他就会执行后面所定义的命令来生成 <code>edit</code> 这个文件。</li><li>如果 <code>edit</code> 所依赖的 <code>.o</code> 文件也不存在,那么make会在当前文件中找目标为 <code>.o</code> 文件的依赖性,如果找到则再根据那一个规则生成 <code>.o</code> 文件。(这有点像一个堆栈的过程)</li><li>当然,你的C文件和H文件是存在的啦,于是make会生成 <code>.o</code> 文件,然后再用 <code>.o</code> 文件生成make的终极任务,也就是执行文件 <code>edit</code> 了。</li></ol><h2 id="makefile变量"><a href="#makefile变量" class="headerlink" title="makefile变量"></a>makefile变量</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">edit : main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o cc -o edit main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>我们可以看到 <code>.o</code> 文件的字符串被重复了两次,如果我们的工程需要加入一个新的 <code>.o</code> 文件,就需要手动更改两个地方,如果涉及的地方一多,改起来就很麻烦,还容易出错。所以引入变量的概念。</p><p><strong>变量定义</strong></p><p>定义objects变量</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>定义后,就可以如此使用$(Var)</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.oedit : $(objects) cc -o edit $(objects)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="makefile的自动推导"><a href="#makefile的自动推导" class="headerlink" title="makefile的自动推导"></a>makefile的自动推导</h2><p>其实makefile默认会将.o同名的.c文件进行链接,加入到依赖关系中,<code>cc -c *.c</code>也就自动添加进命令中执行编译.c文件了,所以也就不用每个都写<code>cc -c *.c</code>了</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.oedit : $(objects) cc -o edit $(objects)main.o : defs.hkbd.o : defs.h command.hcommand.o : defs.h command.hdisplay.o : defs.h buffer.hinsert.o : defs.h buffer.hsearch.o : defs.h buffer.hfiles.o : defs.h buffer.h command.hutils.o : defs.h.PHONY : cleanclean : rm edit $(objects)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>.PHONY意思是clean是伪目标文件</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.oedit : $(objects) cc -o edit $(objects)$(objects) : defs.hkbd.o command.o files.o : command.hdisplay.o insert.o search.o files.o : buffer.h.PHONY : cleanclean : rm edit $(objects)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>这是新风格makefile,以.h文件为主,看每个.h文件被哪些.o文件依赖,传统风格是以.o文件为主,看每个.o文件依赖哪些.h文件,其实我也觉得传统风格比较清晰。</p><p>每个Makefile中都应该写一个清空目标文件( <code>.o</code> 和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。一般的风格都是:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">clean: rm edit $(objects)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>更为稳健的做法是:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">.PHONY : cleanclean : -rm edit $(objects)<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>前面说过, <code>.PHONY</code> 表示 <code>clean</code> 是一个“伪目标”。而在 <code>rm</code> 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, <code>clean</code> 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。</p><h3 id="伪目标"><a href="#伪目标" class="headerlink" title="伪目标"></a>伪目标</h3><p>“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。</p><p>当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。</p><p>伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。</p><p>一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">all : prog1 prog2 prog3.PHONY : allprog1 : prog1.o utils.o cc -o prog1 prog1.o utils.oprog2 : prog2.o cc -o prog2 prog2.oprog3 : prog3.o sort.o utils.o cc -o prog3 prog3.o sort.o utils.o<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于默认目标的特性是,总是被执行的,但由于“all”又是一个伪目标,伪目标只是一个标签不会生成文件,所以不会有“all”文件产生。于是,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。 <code>.PHONY : all</code> 声明了“all”这个目标为“伪目标”。</p><h2 id="makefile里有什么"><a href="#makefile里有什么" class="headerlink" title="makefile里有什么"></a>makefile里有什么</h2><p>Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释</p><ol><li>显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。</li><li>隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。</li><li>变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。</li><li>文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。</li><li>注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 <code>#</code> 字符,这个就像C/C++中的 <code>//</code> 一样。如果你要在你的Makefile中使用 <code>#</code> 字符,可以用反斜杠进行转义,如: <code>\#</code> 。</li></ol><p>最后,还值得一提的是,在Makefile中的<strong>命令,必须要以 <code>Tab</code> 键开始</strong>。</p><h2 id="makefile的文件名"><a href="#makefile的文件名" class="headerlink" title="makefile的文件名"></a>makefile的文件名</h2><p>默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。</p><p>当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的 <code>-f</code> 和 <code>--file</code> 参数,如: <code>make -f Make.Linux</code> 或 <code>make --file Make.AIX</code> 。</p><h2 id="makefile的引用"><a href="#makefile的引用" class="headerlink" title="makefile的引用"></a>makefile的引用</h2><p>在Makefile使用 <code>include</code> 关键字可以把别的Makefile包含进来,这很像C语言的 <code>#include</code> ,被包含的文件会原模原样的放在当前文件的包含位置。 <code>include</code> 的语法是:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">include <filename><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p><code>filename</code> 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。</p><p>在 <code>include</code> 前面可以有一些空字符,但是绝不能是 <code>Tab</code> 键开始。 <code>include</code> 和 <code><filename></code> 可以用一个或多个空格隔开。举个例子,你有这样几个Makefile: <code>a.mk</code> 、 <code>b.mk</code> 、 <code>c.mk</code> ,还有一个文件叫 <code>foo.make</code> ,以及一个变量 <code>$(bar)</code> ,其包含了 <code>e.mk</code> 和 <code>f.mk</code> ,那么,下面的语句:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">include foo.make *.mk $(bar)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>等价于:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">include foo.make a.mk b.mk c.mk e.mk f.mk<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>make命令开始时,会找寻 <code>include</code> 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 <code>#include</code> 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:</p><ol><li>如果make执行时,有 <code>-I</code> 或 <code>--include-dir</code> 参数,那么make就会在这个参数所指定的目录下去寻找。</li><li>如果目录 <code><prefix>/include</code> (一般是: <code>/usr/local/bin</code> 或 <code>/usr/include</code> )存在的话,make也会去找。</li></ol><h2 id="makefile的工作流程"><a href="#makefile的工作流程" class="headerlink" title="makefile的工作流程"></a>makefile的工作流程</h2><p>GNU的make工作时的执行步骤如下:</p><ol><li>读入所有的Makefile。</li><li>读入被include的其它Makefile。</li><li>初始化文件中的变量。</li><li>推导隐晦规则,并分析所有规则。</li><li>为所有的目标文件创建依赖关系链。</li><li>根据依赖关系,决定哪些目标要重新生成。</li><li>执行生成命令。</li></ol><h2 id="makefile多目标及静态模式"><a href="#makefile多目标及静态模式" class="headerlink" title="makefile多目标及静态模式"></a>makefile多目标及静态模式</h2><h3 id="多目标"><a href="#多目标" class="headerlink" title="多目标"></a>多目标</h3><p>Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的生成规则的执行命令不是同一个,这可能会给我们带来麻烦,不过好在我们可以使用一个自动化变量 <code>$@</code>表示目标的集合,就像一个数组, <code>$@</code> 依次取出目标,并置于命令。</p><p>例子</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">bigoutput littleoutput : text.g generate text.g -$(subst output,,$@) > $@<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>上述规则等价于:</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">bigoutput : text.g generate text.g -big > bigoutputlittleoutput : text.g generate text.g -little > littleoutput<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>其中, <code>-$(subst output,,$@)</code> 中的 <code>$</code> 表示执行一个Makefile的函数,函数名为subst,后面的为参数。这个函数是替换字符串的意思,将$@中的’output’替换为空</p><h3 id="静态模式"><a href="#静态模式" class="headerlink" title="静态模式"></a>静态模式</h3><p>静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none"><targets ...> : <target-pattern> : <prereq-patterns ...> <commands> ...<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。</p><p>target-pattern是指明了targets的模式,也就是的目标集模式。</p><p>prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。</p><p>举个例子如果我们的<target-pattern>定义成 <code>%.o</code> ,意思是我们的<target>;集合中都是以 <code>.o</code> 结尾的,而如果我们的<div class="code-wrapper"><prereq-patterns>定义成 <code>%.c</code> ,意思是对<target-pattern>所形成的<strong>目标集</strong>进行二次定义,其计算方法是,取<target-pattern>模式中的 <code>%</code> (也就是去掉了 <code>.o</code> 这个结尾),并为其加上 <code>.c</code> 这个结尾,形成的新集合。</p><p>例子:</p><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">objects = foo.o bar.oall: $(objects)$(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>上面的例子中,指明了我们的目标从$object中获取, <code>%.o</code> 表明要所有以 <code>.o</code> 结尾的目标,也就是 <code>foo.o bar.o</code> ,也就是变量 <code>$object</code> 集合的模式,而依赖模式 <code>%.c</code> 则取模式 <code>%.o</code> 的 <code>%</code> ,也就是 <code>foo bar</code> ,并为其加下 <code>.c</code> 的后缀,于是,我们的依赖目标就是 <code>foo.c bar.c</code> 。而命令中的 <code>$<</code> 和 <code>$@</code> 则是自动化变量, <code>$<</code> 表示第一个依赖文件, <code>$@</code> 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">foo.o : foo.c $(CC) -c $(CFLAGS) foo.c -o foo.obar.o : bar.c $(CC) -c $(CFLAGS) bar.c -o bar.o<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>另一个例子</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">files = foo.elc bar.o lose.o$(filter %.o,$(files)): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@$(filter %.elc,$(files)): %.elc: %.el emacs -f batch-byte-compile $<<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。其它的内容,我就不用多说了吧。这个例子展示了Makefile中更大的弹性。</p><h2 id="命令"><a href="#命令" class="headerlink" title="命令"></a>命令</h2><p>当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:</p><ul><li>示例一:</li></ul><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">exec: cd /home/hchen pwd<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><ul><li>示例二:</li></ul><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">exec: cd /home/hchen; pwd<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre></div><p>当我们执行 <code>make exec</code> 时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。</p><h1 id="小知识"><a href="#小知识" class="headerlink" title="小知识"></a>小知识</h1><p>make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被 make 显示出来</p><div class="code-wrapper"><pre class="line-numbers language-makefile" data-language="makefile"><code class="language-makefile">$(BUILD_DIR)/.prepared: Makefile@mkdir -p $$(dirname $@)@touch $@<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>$$ 代表需要使用真实的$字符,这也就不会打印mkdir命令及touch命令</p>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> tools </tag>
</tags>
</entry>
<entry>
<title>yaml基础</title>
<link href="//post/yaml.html"/>
<url>//post/yaml.html</url>
<content type="html"><![CDATA[<hr><h3 id="基本语法"><a href="#基本语法" class="headerlink" title="基本语法"></a>基本语法</h3><ul><li>大小写敏感</li><li>使用缩进表示层级关系</li><li>缩进不允许使用tab,只允许空格</li><li>缩进的空格数不重要,只要相同层级的元素左对齐即可</li><li>‘#’表示注释</li></ul><h3 id="数据类型"><a href="#数据类型" class="headerlink" title="数据类型"></a>数据类型</h3><p>YAML 支持以下几种数据类型:</p><ul><li>对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)</li><li>数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)</li><li>纯量(scalars):单个的、不可再分的值</li></ul><h3 id="YAML-对象"><a href="#YAML-对象" class="headerlink" title="YAML 对象"></a>YAML 对象</h3><p>对象键值对使用冒号结构表示 <strong>key: value</strong>,冒号后面要加一个空格。</p><p>也可以使用 <strong>key:{key1: value1, key2: value2, …}</strong>。</p><p>还可以使用缩进表示层级关系;</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">key: child-key: value child-key2: value2<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>较为复杂的对象格式,可以使用问号加一个空格代表一个复杂的 key,配合一个冒号加一个空格代表一个 value:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">? - complexkey1 - complexkey2: - complexvalue1 - complexvalue2<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>意思即对象的属性是一个数组 [complexkey1,complexkey2],对应的值也是一个数组 [complexvalue1,complexvalue2]</p><h3 id="YAML-数组"><a href="#YAML-数组" class="headerlink" title="YAML 数组"></a>YAML 数组</h3><p>以 <strong>-</strong> 开头的行表示构成一个数组:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">- A- B- C<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></div><p>YAML 支持多维数组,可以使用行内表示:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">key: [value1, value2, ...]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><p>数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">- - A - B - C<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><p>一个相对复杂的例子:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">companies: - id: 1 name: company1 price: 200W - id: 2 name: company2 price: 500W<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>意思是 companies 属性是一个数组,每一个数组元素又是由 id、name、price 三个属性构成。</p><p>数组也可以使用流式(flow)的方式表示:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div><h3 id="复合结构"><a href="#复合结构" class="headerlink" title="复合结构"></a>复合结构</h3><p>数组和对象可以构成复合结构,例:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">languages: - Ruby - Perl - Python websites: YAML: yaml.org Ruby: ruby-lang.org Python: python.org Perl: use.perl.org<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>转换为 json 为:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">{ languages: [ 'Ruby', 'Perl', 'Python'], websites: { YAML: 'yaml.org', Ruby: 'ruby-lang.org', Python: 'python.org', Perl: 'use.perl.org' } }<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h3 id="纯量"><a href="#纯量" class="headerlink" title="纯量"></a>纯量</h3><p>纯量是最基本的,不可再分的值,包括:</p><ul><li>字符串</li><li>布尔值</li><li>整数</li><li>浮点数</li><li>Null</li><li>时间</li><li>日期</li></ul><p>使用一个例子来快速了解纯量的基本使用:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">boolean: - TRUE #true,True都可以 - FALSE #false,False都可以float: - 3.14 - 6.8523015e+5 #可以使用科学计数法int: - 123 - 0b1010_0111_0100_1010_1110 #二进制表示null: nodeName: 'node' parent: ~ #使用~表示nullstring: - 哈哈 - 'Hello world' #可以使用双引号或者单引号包裹特殊字符 - newline newline2 #字符串可以拆成多行,每一行会被转化成一个空格date: - 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dddatetime: - 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><p><strong>&</strong> 锚点和 <strong>*</strong> 别名,可以用来引用:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">defaults: &defaults adapter: postgres host: localhostdevelopment: database: myapp_development <<: *defaultstest: database: myapp_test <<: *defaults<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>相当于:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">defaults: adapter: postgres host: localhostdevelopment: database: myapp_development adapter: postgres host: localhosttest: database: myapp_test adapter: postgres host: localhost<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p><strong>&</strong> 用来建立锚点(defaults),<strong><<</strong> 表示合并到当前数据,<strong>*</strong> 用来引用锚点。</p><p>下面是另一个例子:</p><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">- &showell Steve - Clark - Brian - Oren - *showell <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>转为 JavaScript 代码如下:</p><div class="code-wrapper"><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">[</span> <span class="token string">'Steve'</span><span class="token punctuation">,</span> <span class="token string">'Clark'</span><span class="token punctuation">,</span> <span class="token string">'Brian'</span><span class="token punctuation">,</span> <span class="token string">'Oren'</span><span class="token punctuation">,</span> <span class="token string">'Steve'</span> <span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></div>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> 日常 </tag>
</tags>
</entry>
<entry>
<title>docker常用指令基础</title>
<link href="//post/docker.html"/>
<url>//post/docker.html</url>
<content type="html"><$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-composesudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-composedocker-compose version<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="docker基础"><a href="#docker基础" class="headerlink" title="docker基础"></a>docker基础</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">#搜索ubuntu系统镜像docker search ubuntu#拉取最新的ubuntu镜像docker pull ubuntu#列出镜像docker images#列出当时运行的容器docker psdocker stop <ID/Name>docker start#docker run是新建容器并启动,docker start 是启动停止的容器docker rundocker restartdocker rmdocker cp#例子以命令行模式进入ubuntu容器docker run -it ubuntu /bin/bash<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><ul><li>-d 后台运行</li><li>-P 选项(大写):随机端口映射</li><li>-p 选项(小写):指定端口映射,<strong>前面是宿主机端口后面是容器端口</strong>,如<code>docker run nginx -p 8080:80</code>,将容器的 80 端口映射到宿主机的 8080 端口,然后使用<code>localhost:8080</code>就可以查看容器中 nginx 的欢迎页了</li><li>-t: 在新容器内指定一个伪终端或终端。</li><li>-i: 允许你对容器内的标准输入 (STDIN) 进行交互。</li><li>-v 选项:挂载宿主机目录,<strong>前面是宿主机目录,后面是容器目录</strong>,如<code>docker run -d -p 80:80 -v /dockerData/nginx/conf/nginx.conf:/etc/nginx/nginx.conf nginx</code> 挂载宿主机的<code>/dockerData/nginx/conf/nginx.conf</code>的文件,这样就可以在宿主机对<code>nginx</code>进行参数配置了,注意目录需要用绝对路径,不要使用相对路径,如果宿主机目录不存在则会自动创建。</li><li>—rm : 停止容器后会直接删除容器,这个参数在测试是很有用,如<code>docker run -d -p 80:80 --rm nginx</code></li><li>—name : 给容器起个名字,否则会出现一长串的自定义名称如 <code>docker run -name niginx -d -p 80:80 - nginx</code></li></ul><h3 id="docker-ps的各个输出的含义"><a href="#docker-ps的各个输出的含义" class="headerlink" title="docker ps的各个输出的含义"></a>docker ps的各个输出的含义</h3><p>CONTAINER ID: 容器 ID。</p><p>IMAGE: 使用的镜像。</p><p>COMMAND: 启动容器时运行的命令。</p><p>CREATED: 容器的创建时间。</p><p>STATUS: 容器状态。</p><p>状态有7种:</p><p>created(已创建)<br>restarting(重启中)<br>running 或 Up(运行中)<br>removing(迁移中)<br>paused(暂停)<br>exited(停止)<br>dead(死亡)<br>PORTS: 容器的端口信息和使用的连接类型(tcp\udp)。</p><p>NAMES: 自动分配的容器名称。</p><h2 id="docker-d后如何进入"><a href="#docker-d后如何进入" class="headerlink" title="docker -d后如何进入"></a>docker -d后如何进入</h2><ul><li><strong>docker attach</strong>:进入容器后exit,容器也会停止</li><li><strong>docker exec</strong>:推荐大家使用 docker exec 命令,因为此命令在exit时只会退出容器终端,但不会导致容器的停止</li></ul><h2 id="docker-导入导出"><a href="#docker-导入导出" class="headerlink" title="docker 导入导出"></a>docker 导入导出</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">#导出容器 1e560fca3906 快照到本地文件 ubuntu.tardocker export 1e560fca3906 > ubuntu.tar#将快照文件 ubuntu.tar 导入到镜像 test/ubuntu镜像tag为v1cat docker/ubuntu.tar | docker import - test/ubuntu:v1<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> tools </tag>
</tags>
</entry>
<entry>
<title>tugraph</title>
<link href="//post/tugraph.html"/>
<url>//post/tugraph.html</url>
<content type="html"><![CDATA[<hr><p>key-value pair可以存点数据,入边数据以及出边数据,用5byte的vid和1byte作为VERTEX_ONLY和PACKED_DATA数据的key,以及2个byte的空间作为数据对齐。</p><p>一共有四种key,PACKED_DATA,VERTEX_ONLY,OUT_EDGE,IN_EDGE,当一个点刚加入的时候,是PACKED_DATA node,到后来这个PACKED_DATA对应的value值会因为边的插入而变大,当这个数据量超出限制时,这个数据就会拆分成三个数据,VERTEX_ONLY,OUT_EDGE,IN_EDGE。其中,OUT_EDGE,IN_EDGE还能再拆分。</p><p><img src="tugraph/image-20221027193933714.png" alt="image-20221027193933714"></p><h1 id="代码架构总览"><a href="#代码架构总览" class="headerlink" title="代码架构总览"></a>代码架构总览</h1><p>解析引擎查询引擎存储引擎的代码都在src中,当客户端来了一个Query时,首先进入解析引擎,开始语义分析语法分析,抽象成AST树,经过validator验证正确后,生成执行计划,再进行执行计划的优化,优化后的AST树,进入执行引擎执行,通过并发控制算法进行事务的并发处理,和下层存储进行交互。下层存储氛围meta service和data service,分别是源信息管理和数据管理。Storage Service 共有三层:最底层是 Store Engine;之上便是我们的 Consensus 层,实现了 Multi Group Raft;最上层,便是我们的 Storage interfaces,这一层定义了一系列和图相关的 API。</p><hr><ul><li>src/:源码目录<ul><li>src/client/: 内置客户端</li><li>src/codec/: 序列化反序列化工具</li><li>src/common/: 内核工具包</li><li>src/console/: 命令台安装脚本</li><li>src/daemons/: 存储引擎和元数据引擎及图引擎主进程</li><li>src/graph/: 查询引擎源码<ul><li>src/graph/context/: 查询的上下文信息,包括 AST(抽象语法树),Execution Plan(执行计划),执行结果以及其他计算相关的资源。</li><li>src/graph/executor/:执行器,各个算子的实现</li><li>src/graph/optimizer/:RBO(基于规则的优化)实现,以及优化规则</li><li>src/graph/planner/:算子,以及执行计划生成</li><li>src/graph/scheduler/:执行计划的调度器</li><li>src/graph/service/:查询引擎服务层,提供鉴权,执行 Query 的接口</li><li>src/graph/session/:Session 管理</li><li>src/graph/stats/:执行统计,比如 P99、慢查询统计等</li><li>src/graph/util/:工具函数</li><li>src/graph/validator/:语义分析实现,用于检查语义错误,并进行一些简单的改写优化</li><li>src/graph/visitor/:表达式访问器,用于提取表达式信息,或者优化</li></ul></li><li>src/interface/: Thrift外接RPC框架,graph、meta、storage 服务的接口定义</li><li>src/kvstore/:基于 raft 的分布式 KV 存储实现</li><li>src/meta/:基于 KVStore 的元数据管理服务实现,用于管理元数据信息,集群管理,长耗时任务管理等</li><li>src/mock/:</li><li>src/parser/:词法解析,语法解析,:AST结构定义</li><li>src/storage/:基于 KVStore 的图数据存储引擎实现</li><li>src/tools/:一些小工具实现</li><li>src/version/:</li><li>src/webservice/:</li></ul></li><li>tests/:基于 BDD 的集成测试框架,测试所有 NebulaGraph 提供的功能</li></ul><h2 id="内核工具包"><a href="#内核工具包" class="headerlink" title="内核工具包"></a>内核工具包</h2><ul><li>src/common/clients/:meta,storage 客户端的 CPP 实现</li><li>src/common/datatypes/:NebulaGraph 中数据类型及计算的定义,比如 string,int,bool,float,Vertex,Edge 等。</li><li>rc/common/expression/:nGQL 中表达式的定义</li><li>src/common/function/:nGQL 中的函数的定义</li><li>src/common/interface/:graph、meta、storage 服务的接口定义</li></ul><h1 id="代码笔记"><a href="#代码笔记" class="headerlink" title="代码笔记"></a>代码笔记</h1><p>在执行阶段,执行引擎通过 Scheduler(调度器)将 Planner 生成的物理执行计划转换为一系列 Executor,驱动 Executor 的执行。 Executor,即执行器,物理执行计划中的每个 PlanNode 都会对应一个 Executor。</p><hr><h3 id="Scheduler"><a href="#Scheduler" class="headerlink" title="Scheduler"></a>Scheduler</h3><ul><li>src/graph/scheduler/:<ul><li>AsyncMsgNotifyBasedScheduler.cpp</li><li>AsyncMsgNotifyBasedScheduler.h</li><li>CMakeLists.txt</li><li>Scheduler.cpp</li><li>Scheduler.h</li></ul></li></ul><p>Scheduler 抽象类定义了调度器的公共接口,可以继承该类实现多种调度器。 目前实现了 AsyncMsgNotifyBasedScheduler 调度器,它基于异步消息通信与广度优先搜索避免栈溢出</p>]]></content>
<categories>
<category> database </category>
<category> graphdb </category>
</categories>
<tags>
<tag> database </tag>
<tag> graphdb </tag>
</tags>
</entry>
<entry>
<title>nebula源码框架</title>
<link href="//post/nebula.html"/>
<url>//post/nebula.html</url>
<content type="html"><![CDATA[<h1 id="Nebula编译安装"><a href="#Nebula编译安装" class="headerlink" title="Nebula编译安装"></a>Nebula编译安装</h1><h2 id="源码安装build"><a href="#源码安装build" class="headerlink" title="源码安装build"></a>源码安装build</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">apt-get install -y m4 git wget unzip xz-utils curl lsb-core build-essential libreadline-dev ncurses-dev bzip2git clone --branch release-3.3 https://gitee.com/Codebells/nebula.gitcd nebulamkdir build && cd buildcmake -DCMAKE_INSTALL_PREFIX=/usr/local/nebula -DENABLE_TESTING=OFF -DCMAKE_BUILD_TYPE=Release ..make -j 30 2>&1 | tee log.txtmake installrename "s/.default//g" /usr/local/nebula/etc/*.conf.default <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="源码安装的nebula卸载"><a href="#源码安装的nebula卸载" class="headerlink" title="源码安装的nebula卸载"></a>源码安装的nebula卸载</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">/usr/local/nebula/scripts/nebula.service stop allrm -rf /usr/local/nebulaps -aux|grep nebularm -rf /usr/local/nebula/data<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="Run-Nebula"><a href="#Run-Nebula" class="headerlink" title="Run Nebula"></a>Run Nebula</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">rename 's/\.default$//' *.conf.default/usr/local/nebula/scripts/nebula.service start all/usr/local/nebula/scripts/nebula.service status all/usr/local/nebula/scripts/nebula.service stop all<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="安装nebula-console"><a href="#安装nebula-console" class="headerlink" title="安装nebula console"></a>安装nebula console</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">chmod 111 nebula-consolepushd ~./nebula-console -addr 127.0.0.1 -port 9669 -u root -p wucaiyiADD HOSTS 127.0.0.1:9779ADD HOSTS 172.31.16.44:9779,172.31.16.45:9779,172.31.16.46:9779show hostsBALANCE LEADER;DROP HOSTS 127.0.0.1:9779<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="nGQL语句"><a href="#nGQL语句" class="headerlink" title="nGQL语句"></a>nGQL语句</h2><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">CREATE SPACE nebula (vid_type = FIXED_STRING(30));USE nebula;CREATE TAG IF NOT EXISTS person (name string, age int , tid string);CREATE TAG INDEX IF NOT EXISTS person_index on person(name(10));INSERT VERTEX person(name,age,tid) VALUES "vid1" :("wcy",23,"vid1");INSERT VERTEX person(name,age,tid) VALUES "vid2" :("hsj",24,"vid2");INSERT VERTEX person(name,age,tid) VALUES "vid3" :("ych",25,"vid3");CREATE EDGE IF NOT EXISTS relate(relation string);INSERT EDGE relate(relation) VALUES "vid1"->"vid2":("homate");INSERT EDGE relate(relation) VALUES "vid1"->"vid3":("homate2");FETCH PROP ON person "vid1" YIELD properties(VERTEX);REBUILD TAG INDEX person_index;MATCH (v:person{name:"wcy"})--(v2:person) WHERE id(v) =='vid1' RETURN v2 AS AllProp;MATCH (v:person{name:"wcy"})-->(v2:person) RETURN v2 AS AllProp;SUBMIT JOB STATS;SHOW STATS;SHOW JOB $(jobId);CREATE SPACE IF NOT EXISTS stress_test_0331(PARTITION_NUM = 24, REPLICA_FACTOR = 1, vid_type = int64);USE stress_test_0331;CREATE TAG IF NOT EXISTS `Person`(`firstName` string,`lastName` string,`gender` string,`birthday` string,`creationDate` datetime,`locationIP` string,`browserUsed` string);CREATE TAG INDEX IF NOT EXISTS `person_first_name_idx` on `Person`(firstName(10));INSERT VERTEX Person(firstName, lastName, gender, birthday, creationDate, locationIP, browserUsed) VALUES 9333:("Mahinda", "Perera", "male", "1989-12-03", datetime("2010-02-14T15:32:10.447"), "119.235.7.103", "Firefox");<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="安装NebulaGraph-CPP"><a href="#安装NebulaGraph-CPP" class="headerlink" title="安装NebulaGraph-CPP"></a>安装NebulaGraph-CPP</h1><div class="code-wrapper"><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">git clone --branch release-3.3 https://github.com/vesoft-inc/nebula-cpp.gitcd nebula-cppmkdir build && cd buildcmake -DCMAKE_INSTALL_PREFIX=/usr/local/nebulacpp -DCMAKE_BUILD_TYPE=Release ..make -j8make installsudo ldconfigexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/nebulacpp/lib<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="k6-LDBC测试"><a href="#k6-LDBC测试" class="headerlink" title="k6 LDBC测试"></a>k6 LDBC测试</h1><div class="code-wrapper"><pre class="line-numbers language-none"><code class="language-none">sudo apt-get install -y \ git \ wget \ python3-pip \ python \ openjdk-8-jdk \ maven export JAVA_HOME=/usr/lib/jvm/default-java/git clone https://github.com/vesoft-inc/nebula-bench.git cd nebula-benchpip3 install --user -r requirements.txtpython3 run.py --helpwget https://dl.google.com/go/go1.17.8.linux-amd64.tar.gztar -xf go1.17.8.linux-amd64.tar.gz -C /usr/localsudo vim /etc/profileexport GOROOT=/usr/local/goexport GOPATH=/home export GOBIN=$GOPATH/binexport PATH=$PATH:$GOROOT/binexport PATH=$PATH:$GOPATH/binsource /etc/profilego versionexport GOPROXY=https://goproxy.cn/bin/bash scripts/setup.shpython3 run.py data -s 1python3 run.py nebula importer -a 172.31.16.44:9669,172.31.16.45:9669,172.31.16.46:9669./scripts/nebula-importer --config ./importer_config.yamlpython3 run.py stress scenarios -a 172.31.16.44:9669,172.31.16.45:9669,172.31.16.46:9669python3 run.py stress run -scenario insert.InsertScenario --space='stress_test_0401' --args='-u 50 -d 3s'python3 run.py stress run -scenario fetch.FetchEdge --args='-u 50 -d 60s'CREATE SPACE IF NOT EXISTS stress_test_0331(PARTITION_NUM = 24, REPLICA_FACTOR = 1, vid_type = int64);USE stress_test_0331;CREATE TAG IF NOT EXISTS `Person`(`firstName` string,`lastName` string,`gender` string,`birthday` string,`creationDate` datetime,`locationIP` string,`browserUsed` string);CREATE TAG INDEX IF NOT EXISTS `person_first_name_idx` on `Person`(firstName(10));INSERT VERTEX Person(firstName, lastName, gender, birthday, creationDate, locationIP, browserUsed) VALUES 9333:("Mahinda", "Perera", "male", "1989-12-03", datetime("2010-02-14T15:32:10.447"), "119.235.7.103", "Firefox"); INSERT VERTEX `Tagclass`(`name`,`url`) VALUES 155: ("Royalty","http://dbpedia.org/ontology/Royalty"), 141: ("NascarDriver","http://dbpedia.org/ontology/NascarDriver"), 233: ("EurovisionSongContestEntry","http://dbpedia.org/ontology/EurovisionSongContestEntry");<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h2 id="Docker-nebula安装"><a href="#Docker-nebula安装" class="headerlink" title="Docker nebula安装"></a>Docker nebula安装</h2><p>git clone -b release-3.1 <a href="https://github.com/vesoft-inc/nebula-docker-compose.git">https://github.com/vesoft-inc/nebula-docker-compose.git</a></p><p>cd nebula-docker-compose/</p><p>docker-compose up -d</p><p>docker exec -it nebula-docker-compose-console-1 /bin/sh</p><p>./usr/local/bin/nebula-console -u root -p wucaiyi —address=graphd —port=9669</p><h3 id="Docker-compose安装"><a href="#Docker-compose安装" class="headerlink" title="Docker-compose安装"></a>Docker-compose安装</h3><p>sudo curl -L “<a href="https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-">https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-</a>$(uname -s)-$(uname -m)” -o /usr/local/bin/docker-compose</p><p>sudo chmod +x /usr/local/bin/docker-compose</p><p>sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose</p><p>docker-compose version</p><h1 id="代码架构总览"><a href="#代码架构总览" class="headerlink" title="代码架构总览"></a>代码架构总览</h1><p>解析引擎查询引擎存储引擎的代码都在其中,当客户端来了一个Query时,首先进入解析引擎,开始语义分析语法分析,抽象成AST树,经过validator验证正确后,生成执行计划,再进行执行计划的优化,优化后的AST树,进入执行引擎执行,通过并发控制算法进行事务的并发处理,和下层存储进行交互。下层存储分为meta service和data service,分别是源信息管理和数据管理。</p><p><img src="nebula/nebula-graph-architecture-1.png" alt="Nebula架构"></p><p><img src="nebula/nebula-reading-qe-architecture.png" alt="查询引擎架构"></p><p>Storage 包含两个部分, 一是 meta 相关的存储, 我们称之为 Meta Service ,另一个是 data 相关的存储, 我们称之为 Storage Service。</p><p>Storage Service 共有三层:最底层是 Store Engine;之上便是我们的 Consensus 层,实现了 Multi Group Raft;最上层,便是我们的 Storage interfaces,这一层定义了一系列和图相关的 API。</p><p><img src="nebula/nebula-reading-storage-architecture.png" alt="存储引擎"></p><hr><ul><li>conf/:查询引擎配置文件目录</li><li>package/:nebula打包脚本</li><li>resources/:资源文件</li><li>scripts/:启动脚本</li><li>src/:源码目录<ul><li>src/client/: 内置客户端</li><li>src/codec/: 序列化反序列化工具</li><li>src/common/: 内核工具包</li><li>src/console/: 命令台安装脚本</li><li>src/daemons/: 存储引擎和元数据引擎及图引擎主进程</li><li>src/graph/: 查询引擎源码<ul><li>src/graph/context/: 查询的上下文信息,包括 AST(抽象语法树),Execution Plan(执行计划),执行结果以及其他计算相关的资源。</li><li>src/graph/executor/:执行器,各个算子的实现</li><li>src/graph/optimizer/:RBO(基于规则的优化)实现,以及优化规则</li><li>src/graph/planner/:算子,以及执行计划生成</li><li>src/graph/scheduler/:执行计划的调度器</li><li>src/graph/service/:查询引擎服务层,提供鉴权,执行 Query 的接口</li><li>src/graph/session/:Session 管理</li><li>src/graph/stats/:执行统计,比如 P99、慢查询统计等</li><li>src/graph/util/:工具函数</li><li>src/graph/validator/:语义分析实现,用于检查语义错误,并进行一些简单的改写优化</li><li>src/graph/visitor/:表达式访问器,用于提取表达式信息,或者优化</li></ul></li><li>src/interface/: Thrift外接RPC框架,graph、meta、storage 服务的接口定义</li><li>src/kvstore/:基于 raft 的分布式 KV 存储实现</li><li>src/meta/:基于 KVStore 的元数据管理服务实现,用于管理元数据信息,集群管理,长耗时任务管理等</li><li>src/mock/:</li><li>src/parser/:词法解析,语法解析,:AST结构定义</li><li>src/storage/:基于 KVStore 的图数据存储引擎实现</li><li>src/tools/:一些小工具实现</li><li>src/version/:</li><li>src/webservice/:</li></ul></li><li>tests/:基于 BDD 的集成测试框架,测试所有 NebulaGraph 提供的功能</li></ul><h2 id="内核工具包"><a href="#内核工具包" class="headerlink" title="内核工具包"></a>内核工具包</h2><ul><li>src/common/clients/:meta,storage 客户端的 CPP 实现</li><li>src/common/datatypes/:NebulaGraph 中数据类型及计算的定义,比如 string,int,bool,float,Vertex,Edge 等。</li><li>rc/common/expression/:nGQL 中表达式的定义</li><li>src/common/function/:nGQL 中的函数的定义</li><li>src/common/interface/:graph、meta、storage 服务的接口定义</li></ul><h1 id="代码笔记"><a href="#代码笔记" class="headerlink" title="代码笔记"></a>代码笔记</h1><p>在执行阶段,执行引擎通过 Scheduler(调度器)将 Planner 生成的物理执行计划转换为一系列 Executor,驱动 Executor 的执行。 Executor,即执行器,物理执行计划中的每个 PlanNode 都会对应一个 Executor。</p><hr><h2 id="Scheduler"><a href="#Scheduler" class="headerlink" title="Scheduler"></a>Scheduler</h2><ul><li>src/graph/scheduler/:<ul><li>AsyncMsgNotifyBasedScheduler.cpp</li><li>AsyncMsgNotifyBasedScheduler.h</li><li>CMakeLists.txt</li><li>Scheduler.cpp</li><li>Scheduler.h</li></ul></li></ul><p>Scheduler 抽象类定义了调度器的公共接口,可以继承该类实现多种调度器。 目前实现了 AsyncMsgNotifyBasedScheduler 调度器,它基于异步消息通信与广度优先搜索避免栈溢出</p><h2 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h2><p>src/storage/BaseProcessor :</p><blockquote><p>BaseProcessor定义了Promise以及获取它相关联的Future(通过getFuture接口),以及处理时的一些记录结果</p></blockquote><p>src/storage/transaction/TransactionManager: </p><blockquote><p>worker是处理工作的线程,一个事务的处理过程是prepareLocal,processRemote,processLocal,finish</p><p>onFinished()记录latency之类的数据</p><p>commit时,首先addChainTask,再通过future来异步执行事务处理流程</p></blockquote><p>src/storage/mutate:</p><blockquote><p>是transaction文件夹中的cpp文件使用的公共父类</p></blockquote><h1 id="系统架构总览"><a href="#系统架构总览" class="headerlink" title="系统架构总览"></a>系统架构总览</h1><p><img src="nebula/image-20221117165735392.png" alt="nebula-architecture"></p><p>上层计算层对下层存储层分片不感知,Meta Service管理分片状态,上层计算层无状态</p><p>Partition是逻辑分区,每个Partition存在Raft组,读写数据是到分区raft组的leader节点读写。每个Storage Engine保存多个分区数据,可能有主分区也可能没有</p><h1 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h1><div class="code-wrapper"><pre class="line-numbers language-c++" data-language="c++"><code class="language-c++">/** * @brief Write multiple key/values to kvstore asynchronously * * @param spaceId * @param partId * @param keyValues Key/values to put * @param cb Callback when has a result */ void asyncMultiPut(GraphSpaceID spaceId, PartitionID partId, std::vector<KV>&& keyValues, KVCallback cb) override; /** * @brief Remove a key from kvstore asynchronously * * @param spaceId * @param partId * @param key Key to remove * @param cb Callback when has a result */ void asyncRemove(GraphSpaceID spaceId, PartitionID partId, const std::string& key, KVCallback cb) override; /** * @brief Remove multible keys from kvstore asynchronously * * @param spaceId * @param partId * @param key Keys to remove * @param cb Callback when has a result */ void asyncMultiRemove(GraphSpaceID spaceId, PartitionID partId, std::vector<std::string>&& keys, KVCallback cb) override; /** * @brief Remove keys in range [start, end) asynchronously * * @param spaceId * @param partId * @param start Start key * @param end End key * @param cb Callback when has a result */ void asyncRemoveRange(GraphSpaceID spaceId, PartitionID partId, const std::string& start, const std::string& end, KVCallback cb) override; /** update * @brief Async commit multi operation, difference between asyncMultiPut or asyncMultiRemove * is this method allow contains both put and remove together, difference between asyncAtomicOp is * that asyncAtomicOp may have CAS * * @param spaceId * @param partId * @param batch Encoded write batch * @param cb Callback when has a result */ void asyncAppendBatch(GraphSpaceID spaceId, PartitionID partId, std::string&& batch, KVCallback cb) override; /** * @brief Do some atomic operation on kvstore * * @param spaceId * @param partId * @param op Atomic operation * @param cb Callback when has a result */ void asyncAtomicOp(GraphSpaceID spaceId, PartitionID partId, MergeableAtomicOp op, KVCallback cb) override;<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>实验服务器和压测机为同一台物理机配置为32 vCPU 64 GiB,测试数据采用 LDBC-SNB SF10数据集,SF10据集大小为 10G,共有 29,987,835 个点以及 176,623,382 条边。测试用的图空间分区数为 24,节点数为 3。vu表示的是 k6 使用的概念“virtual user”,即性能测试中的并发数;测试使用50_vu 表示 50 个并发用户,目前,以Nebula3.3版本性能作为基准,Taas测试为单taas节点连接三台nebula节点,即将nebula看成单机数据库连接同一台taas节点作为分布式事务处理模块,Nebula原始的 InsertVertex的吞吐量在59.1k,平均latency为2.2ms,FetchEdge的吞吐量在101k,latency为1.1ms,加入Taas后,InsertVertex性能为35.3k,平均latency为8.35ms平均性能下降40%,FetchEdge的吞吐量在96.1k,latency为1.03ms,平均性能下降6%左右。</p>]]></content>
<categories>
<category> database </category>
<category> graphdb </category>
</categories>
<tags>
<tag> database </tag>
<tag> graphdb </tag>
</tags>
</entry>
<entry>
<title>EDBT 2020 qstore论文阅读</title>
<link href="//post/qstore.html"/>
<url>//post/qstore.html</url>
<content type="html"><![CDATA[<p>和Quecc一个思路,将该思路实现到数据库上进行实验测试,简单来说可以看成是一个将原先确定性数据库的事务单线程加锁或排序问题看成并发加锁排序。论文对比Calvin确定性数据库利用单线程进行schedule事务的思路,利用提高batchsize来达到Calvin的单点瓶颈,以展现自身性能。</p><span id="more"></span><p>思路建议看Quecc这篇论文,Qstore这篇论文对思路解释的没有那么清晰,偏向去证明各个隔离级别的思路正确性。<a href="https://expolab.org/papers/QStore-EDBT20-final.pdf">PPT</a>也比较清晰的解释了思路。这篇博客就用PPT来记录一下它的思路。</p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>提出很多分布式数据库事务处理存在问题,并且随着现在硬件CPU核数越来越多,大部分数据库都无法完全利用CPU的性能。而且分布式系统的需要跨分区操作数据,为了达成各分区并发执行的结果一致性,就必须存在某种规则协议来约束。通常来说,我们使用2PC来保证。但是2PC是一个瓶颈,因为它需要节点之间的协调,存在单点瓶颈。后提出确定性数据库来解决这个问题,确定性数据库提前知道事务读写集,确定性数据库的概念可以看我<a href="https://codebells.github.io/post/deterministic-database.html">前一篇文章</a>,这样各节点就不需要协调了。但是在确定性数据库中也存在并发度不够的问题,这篇论文就是充分利用CPU的并行能力执行事务。</p><h1 id="Calvin和Qstore对比"><a href="#Calvin和Qstore对比" class="headerlink" title="Calvin和Qstore对比"></a>Calvin和Qstore对比</h1><p><img src="qstore/image-20221007161442502.png" alt="Calvin VS Qstore"></p><p><a href="https://codebells.github.io/post/distribute-transaction.html#Calvin%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1">Calvin流程</a>看之前的就行。</p><p>Qstore流程和Calvin的不同就是,Calvin需要经过事务排序并且batch打包后,经过单线程的Scheduler来调度事务的执行。而Qstore可以多线程的对事务进行排序并且拆分分区,排列出优先级顺序后,高优先级顺序的并且不在同一个分区的事务就可以并行执行了。这样在调度和执行阶段都可以并行了。下面详细看看这个过程。</p><p><img src="qstore/image-20221007162632940.png" alt="Qstore Step1"></p><p>接收到客户端的事务,进入到planing线程,planning线程是存在优先级的,进入优先级高的线程就可以先并行执行。</p><p><img src="qstore/image-20221007162937649.png" alt="Step2"></p><p>将planning线程中的事务拆分成子事务,加入到优先级队列中,低优先级planning线程对应加入到低优先级队列,低优先级队列会映射分区,同一个分区的子事务会按照在planning线程中的顺序进行排序串行执行,不同分区的子事务可以并行执行。接下来将分类好的子事务分发到对应的各个分区上去。</p><p><img src="qstore/image-20221007163310089.png" alt="Step3"></p><p>首先需要将子事务存储在分区的BatchMetaData中,按照之前分好的低优先级高优先级以及分区来标注好,分类存储。接下来就可以进行并行执行阶段了。</p><p><img src="qstore/image-20221007163500826.png" alt="Step4"></p><p>先将高优先级的子事务放到执行线程去执行,对于read,可以直接读本地,而无需等待,高优先级队列中的所有事务都执行完了后,才会执行低优先级事务。</p><p><img src="qstore/image-20221007164329978.png" alt="Step5"></p><p>Ack $q{_n}{_m}$各个符号代表的意思是,n为优先级为多少的planning线程,m代表哪个分区提交的,默认为节点的最后一个分区。当执行线程执行完后会给planning线程发送ACK信号。代表执行完成,可以放对应分区的下一个优先级了。</p><p><img src="qstore/image-20221007164843962.png" alt="Step6"></p><p><img src="qstore/image-20221007164951969.png" alt="Step7"></p><p><img src="qstore/image-20221007165018945.png" alt="Step8"></p><p><img src="qstore/image-20221007165104641.png" alt="Step9"></p><p>这样依次类推,将所有事务执行完成后有不同的提交策略。第一个比较麻烦,当事务的所有子事务都执行完成后,提交事务,让结果可见,还有一种就比较简单,以batch为粒度提交事务。图中是以事务为粒度提交,</p><p>这样就可以在事务流程中全部并行,让Cpu的利用率达到最大。</p><p>这就是Qstore和quecc的基本思路。</p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
<category> transaction </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
<tag> txn </tag>
</tags>
</entry>
<entry>
<title>图数据库存储及事务处理</title>
<link href="//post/graphdb.html"/>
<url>//post/graphdb.html</url>
<content type="html"><![CDATA[<h2 id="图数据库的内部结构"><a href="#图数据库的内部结构" class="headerlink" title="图数据库的内部结构"></a>图数据库的内部结构</h2><h3 id="原生图处理"><a href="#原生图处理" class="headerlink" title="原生图处理"></a>原生图处理</h3><p>如果图数据库存在免索引邻接属性,那么就说它有原生处理能力,也就是使用免索引邻接的数据库引擎中每个节点都会维护其对相邻节点的引用。所以每个节点都是其附近节点的微索引,这种比使用全局索引代价小很多。也就意味着查询时间和图的整体规模无关,仅仅与搜索图的数量成正比。</p><p>而非原生图数据库引擎使用全局索引连接各个节点,这些索引对每个遍历都添加一个间接层,就会导致更大的计算成本。</p><p>找Alice必须查找索引表,找到物理位置,再去读取Alice对应数据,通过Alice找到Davina后,想要级联查找,就必须再查找一次索引表,找到Davina的物理位置读取数据。一般来说查找索引的时间复杂度为$O(log(n))$,查找物理联系的复杂度为$O(log(1))$,所以在这方面使用原生图存储查找数据会更有优势</p><p><img src="graphdb/image-20220916224714944.png" alt="非原生图全局索引"></p><p><img src="graphdb/image-20220916230738958.png" alt="原生图全局索引"></p><p>以上是原生图处理高效遍历查询写入的关键。</p><h3 id="原生图存储"><a href="#原生图存储" class="headerlink" title="原生图存储"></a>原生图存储</h3><p>Neo4j把图数据存储在不同的文件中,每个文件包含图的特定部分的数据</p><p>存储还是以图的形式存储,宏观上来讲,分四块</p><ul><li>节点:可以看成ER图中的实体,每个节点有多个属性,属性以KV键值对的形式存在,每个节点有不同的label标记,也是以KV存在</li><li>关系:关系是有向的,存在开始节点和终止节点,也可以有属性</li><li>属性</li><li>标签</li></ul><p>一个重要的设计点是 store 中存储的 record 都是<strong>固定大小的</strong>,固定大小带来的好处是:因为每个 record 的大小固定,因此给定 id就能快速进行定位。</p><p><img src="graphdb/c1b2ce5a8f764e78a24d967afc5a4808tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="每个文件包含图的特定部分数据"></p><p><strong>节点</strong>(指向联系和属性的单向链表,neostore.nodestore.db):第一个字节,表示是否被使用的标志位,后面4个字节,代表关联到这个节点的第一个关系的ID,再接着的4个字符,代表第一个属性ID,后面紧接着的5个字符是代表当前节点的标签,指向该节点的标签存储,最后一个字符作为保留位.</p><p><strong>关系</strong>(双向链表,neostore.relationshipstore.db):第一个字节,表示是否被使用的标志位,后面4个字节,代表起始节点的ID,再接着的4个字符,代表结束个节点的ID,然后是关系类型占用5个字节,然后依次接着是起始节点的上下联系和结束节点的上下节点,以及一个指示当前记录是否位于联系链的最前面.</p><p><img src="graphdb/cc860cda232440d5a16eb510ea364e77tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="存储格式"></p><p><img src="graphdb/003f201fa7664bd8bd219eb7ae7c6376tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="存储格式"></p><p><img src="graphdb/image-20220917143619063.png" alt="类似这样"></p><p><img src="graphdb/image-20220917144647468.png" alt="relation"></p><p><img src="graphdb/ec533bf8a7dc45e5b9359caeb7813bbatplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="示例"></p><p><img src="graphdb/d0f56830180a4672a969a14fed330975tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="示例"></p><p><img src="graphdb/d10f5382d5c0486f93e4ed1ab6bda03ctplv-k3u1fbpfcp-zoom-in-crop-mark3024000.awebp" alt="示例"></p><p><img src="graphdb/v2-384e3b6957393fdc3c0d20ca6fca60f0_r.jpg" alt="非原生图存储"></p>]]></content>
<categories>
<category> database </category>
<category> graphdb </category>
</categories>
<tags>
<tag> database </tag>
<tag> graphdb </tag>
</tags>
</entry>
<entry>
<title>mit6-824 lab1 mapreduce</title>
<link href="//post/mapreduce.html"/>
<url>//post/mapreduce.html</url>
<content type="html"><![CDATA[<p>懒得写了</p><p><a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf">论文链接</a></p><p><a href="https://www.cnblogs.com/fuzhe1989/p/3413457.html">中翻链接</a></p><p>就说一下mapreduce的思路吧</p><p><img src="mapreduce/image-20221015150844432.png" alt="MapReduce Overview"></p><p>首先MapReduce大体来看就是一个分治思想,将任务拆分成小任务交给Worker,Worker经过处理后得到各自的结果,然后通过再经过Reduce将结果输出。</p><p>详细过程如上图。用户程序将拆分好的文件作为inputFiles,每个inputFile可以看成一个MapTask,由Worker执行。但是这个分配任务的过程是由Mapreduce集群中的Master节点来控制的,可以将每个Worker看成一个机器节点,由Worker向Master请求任务,Master向空闲的Worker节点发送Map任务或者Reduce任务。Worker读取对应的输入文件的内容,将其根据自定义的规则解析出Key/Value值,然后将这些值作为中间文件存储在内存中。缓存的数据对会被周期性的由划分函数分成Reduce节点的数量,并写入本地磁盘中。这些缓存对在本地磁盘中的位置会被传回给主节点,主节点负责将这些位置再传给reduce工作节点。</p><p>当一个reduce工作节点得到了主节点的这些位置通知后,它使用RPC调用去读map工作节点的本地磁盘中的缓存数据。当reduce工作节点读取完了所有的中间数据,它会将这些数据按中间key排序,这样相同key的数据就被排列在一起了。同一个reduce任务经常会分到有着不同key的数据,因此这个排序很有必要。如果中间数据数量过多,不能全部载入内存,则会使用外部排序。reduce工作节点遍历排序好的中间数据,并将遇到的每个中间key和与它关联的一组中间value传递给用户的reduce函数。reduce函数的输出会写到由reduce划分过程划分出来的最终输出文件的末尾。</p><p>当所有的map和reduce任务都完成后,主节点唤醒用户程序。此时,用户程序中的MapReduce调用返回到用户代码中。成功完成后,MapReduce执行的输出都在R个输出文件中(每个reduce任务产生一个,文件名由用户指定)。通常用户不需要合并这R个输出文件——他们经常会把这些文件当作另一个MapReduce调用的输入,或是用于另一个可以处理分成多个文件输入的分布式应用。</p><p>看看论文中的例子就大概懂了。</p><p>例举了一些有趣的程序,它们都可以很轻松的用MapReduce模型表达。</p><p><strong>分布式Grep:</strong>map函数在匹配到给定的pattern时输出一行。reduce函数只是将给定的中间数据复制到输出上。</p><p><strong>URL访问频次统计:</strong>map函数处理网页请求的日志,对每个URL输出〈URL, 1〉。reduce函数将相同URL的所有值相加并输出〈URL, 总次数〉对。</p><p><strong>倒转Web链接图:</strong>map函数在source页面中针对每个指向target的链接都输出一个〈target, source〉对。reduce函数将与某个给定的target相关联的所有source链接合并为一个列表,并输出〈target, list(source)〉对。</p><p><strong>每个主机的关键词向量:</strong>关键词向量是对出现在一个文档或一组文档中的最重要的单词的概要,其形式为〈单词, 频率〉对。map函数针对每个输入文档(其主机名可从文档URL中提取到)输出一个〈主机名, 关键词向量〉对。给定主机的所有文档的关键词向量都被传递给reduce函数。reduce函数将这些关键词向量相加,去掉其中频率最低的关键词,然后输出最终的〈主机名, 关键词向量〉对。</p><p><strong>倒排索引:</strong>map函数解析每个文档,并输出一系列〈单词, 文档ID〉对。reduce函数接受给定单词的所有中间对,将它们按文档ID排序,再输出〈单词, list(文档ID)〉对。所有输出对的集合组成了一个简单的倒排索引。用户可以很轻松的扩展这个过程来跟踪单词的位置。</p><p><strong>分布式排序:</strong>map函数从每条记录中提取出key,并输出〈key, 记录〉对。reduce函数不改变这些中间对,直接输出。这个过程依赖于4.1节介绍的划分机制和4.2节介绍的排序性质。</p><p><a href="https://github.com/Codebells/Raft/tree/go_imp/src/mr">源码地址</a></p><p>可以看看<a href="https://github.com/Codebells/Raft/tree/go_imp/src/mrapps">mrapps</a>文件夹的代码,里面有各种map和reduce的实现,很简单,帮助理解mapreduce</p>]]></content>
<categories>
<category> database </category>
<category> mapreduce </category>
<category> lab </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> mapreduce </tag>
<tag> lab </tag>
</tags>
</entry>
<entry>
<title>确定性数据库</title>
<link href="//post/deterministic-database.html"/>
<url>//post/deterministic-database.html</url>
<content type="html"><![CDATA[<p>在基于 Percolator 提交协议的分布式数据库被提出的时期,学术研究上还出现了一种叫确定性数据库的技术。确定性数据库就是输入事务集合,数据库执行后会是固定的结果。</p><p>例如,$T_1,T_2$可能是并发执行的,没有偏序关系,这时如果$T_1$比$T_2$先执行,结果就是(x,2),(y,3),而如果$T_2$比$T_1$先执行,那么结果就是就是(x,2),(y,2),这就导致了不同的结果。为了达到确定性的结果,在事务执行前,我们就需要对其进行排序。</p><p><img src="deterministic-database/1_00c8960ad9.png" alt="不存在偏序关系时的不确定性.png"></p><p>而如何确定偏序关系呢,一般来说需要一个管理者来管理这个先后顺序,如图,在事务执行之前,需要对其排序,向事务管理器申请ID,那么事务的执行顺序就可以按照ID的顺序机型,所以事务必须按照$T_1\rightarrow T_2 \rightarrow T_3$的顺序执行才能产生正确的结果。</p><p><img src="https://img1.www.pingcap.com/prod/2_e0d5568aa3.png" alt="使用事务管理器为排序输入事务排序.png"></p><p>还有就是避免死锁的产生,死锁产生的原因就是交互式事务存在中间过程,$T_1,T_2$都尝试写入key,然后$T_1$ 尝试写入 $T_2$ 的 Key,$T_2$ 尝试写入 $T_1$ 的 Key,这时就产生了死锁,需要abort其中之一。 而确定性数据库可以看作,数据库在事务开始的时候就知道事务会做什么操作,那么在上面的例子中就能知道$T_1,T_2$ 存在依赖关系。需要等待$T_1$ 后才能执行$T_2$ ,那么此时就不会有死锁。而且,死锁发生时对于abort的事务没有要求,因此这也可能产生不确定的结果。</p><p>确定性是一个约束非常强的协议,一旦事务的先后顺序被确定,结果就被确定了,基于这一特点,确定性数据库能够<strong>优化副本复制协议所带来的开销</strong>。因为能保证写入成功,在有些实现中还能够预测读的结果。</p><h1 id="Calvin"><a href="#Calvin" class="headerlink" title="Calvin"></a>Calvin</h1><p>Calvin 提出于 2012 年,和 Spanner 出现于同一时期,尝试利用确定性数据库的特点解决解决当时数据库的扩展性问题,在前一篇讲了个大致的事务执行流程。</p><p>这次讲如何在这个架构下replica内保证确定性输入,replica间保证一致性结果的。</p><p><img src="deterministic-database/4_Calvin_2fc38b78ed.png" alt="Calvin 的架构图.png"></p><p>如图的事务,假设3个sequencer,现在已经打包了两个batch,目前以上帝视角可以看出$T<em>1,T</em>{10}$存在冲突,根据确定性协议的要求,顺序必须是$T<em>1\rightarrow T</em>{10}$,因为ID号小的优先,这些事务 batch 会在被发送到 scheduler 之前通过 Paxos 进行复制,然后batch 被发送到对应的 scheduler 之上,在scheduler上进行锁分配。此时有两种情况,1. 如果$T<em>{10}$持有锁,那么$T</em>{1}$抢占该锁,并执行。2. 如果$T<em>{10}$不持有锁,那么$T</em>{1}$加锁并且开始执行。反正都是$T_{1}$先执行。</p><p><img src="deterministic-database/5_Calvin_21f4b68018.png" alt="Calvin事务例.png"></p><p>但是可能存在另一种情况,就是$T<em>{10}$先发送过去,过了很久$T</em>{1}$才到,但是此时$T_{10}$已经执行完了,这时就发生了不确定性问题。Calvin解决这个问题的方法十分粗暴,使用全局的Coordinator协调,强制要求当scheduler没有收齐当前batch的事务之前不能进行执行,这样就解决了,但是这样在当前Calvin的架构会出现新的问题,就是你怎么确定事务应该发送到哪个分片?因为有些 predicate 语句的读写集合在被执行前是没有被确定的,这种情况下 Calvin 无法对事务进行分析,所以Calvin会在sequencer中有一个试探读取阶段,来确定读写集,如果这个试探读取的读取集被改变了,那么会将事务重启。</p><p><img src="deterministic-database/7_Calvin_f7848d184d.png" alt="Calvin 的不确定性问题.png"></p><p>上述的问题可能出现在replica之间,这会造成replica的不一致,这是不合理的,所以为了解决这个问题,所有的副本同步都需要在一个 Paxos 组之内进行以保证<strong>全局的顺序性</strong>,这些都可能成为一个瓶颈。Coordinator可能还存在降低可用性的问题,即它崩了,有可能整个集群就不可靠了。</p><h1 id="BOHM-amp-PWV"><a href="#BOHM-amp-PWV" class="headerlink" title="BOHM & PWV"></a>BOHM & PWV</h1><p>首先先解释什么是依赖分析,写后写,写后读,读后写,由此来定义事务之间的先后关系,通过依赖图是否出现环来判断事务执行是否破坏隔离性。只要执行过程中避免依赖图中的环,那么执行的过程就是满足事先给定的隔离级别要求的,从这个思路出发,就可以让原本无法并行执行的事务并发执行。</p><p><img src="deterministic-database/1_29eaf57092.png" alt="存在依赖关系的事务.png"></p><p>如图,$T<em>{1}$和$T</em>{2}$具有写后读依赖,$T<em>{2}$和$T</em>{3}$具有读后写依赖,$T<em>{1}$和$T</em>{3}$具有写后写依赖,那么这三个事务无法并发执行,需要按照$T<em>{1}\rightarrow T</em>{2}\rightarrow T_{3}$这个顺序执行,并发度降低。</p><p>BOHM就通过对MVCC的改进来解决这个问题,使得其可以并发执行,它记录每条数据的有效时间以及上一个版本的指针,如图所示,$T<em>{100}$数据的有效期是$100 \leq id \leq 200$,而$T</em>{200}$的数据有效期为$200 \leq id \leq \infty$,这就实现了写事务的并发。</p><p>而PWV是基于BOHM的读可见的优化,让事务能更早的被读到。 为了实现这个目的,PWV对事务的可见性方式和abort原因进行分析。</p><ul><li>提交可见。BOHM,延迟高</li><li>释放锁后可见。存在级联abort</li></ul><p><img src="deterministic-database/10_abort_7c955f5383.png" alt="级联abort.png"></p><p>$T<em>{1}$ 写入的 x 被 $T</em>{2}$读取到,$T<em>{2}$的写入进一步的被$T</em>{3}$ 读取到,之后$T<em>{1}$ 在 y 上的写入发现违反了约束(value < 10),因此 $T</em>{1}$ 必须 abort。但是根据事务的原子性规则,$T<em>{1}$ 对 x 的写入也需要回滚,因此读取了 x 的 $T</em>{2}$需要跟着 abort,而读取到$T<em>{2}$ 的$T</em>{3}$ 也需要跟着 $T_{2}$被 abort。</p><p>abort产生有几种因素</p><ul><li>逻辑原因,违反约束条件</li><li>系统原因,产生死锁,系统问题,写冲突等</li></ul><p><strong>确定性数据库能够排除因系统原因产生的 abort</strong>,例如死锁和写冲突。那么只要确保逻辑原因的 abort 不发生,一个事务就一定能够在确定性数据库中成功提交。PWV基于这点,想到一个办法,先对事务分割成piece,然后寻找其中的Commit点,规定提交点含义为,在提交点后的就不可能发送逻辑原因的abort。</p><p><img src="deterministic-database/11_piece_019c1d24c4.png" alt="PWV提交点.png"></p><p>图中 $T_2$ 需要读取$T_1$ 的写入结果,只需要等待 $T_1$ 执行到 Commit Point 之后在进行读取,而不需要等待$T_1$完全执行成功。通过对事务执行过程的进一步细分,PWV 降低了读操作的延迟,相比于 BOHM 进一步提升了并发度。BOHM 和 PWV 通过对事务间依赖的分析来获取冲突场景下的高性能,但是这一做法需要知道全局的事务信息,计算节点是一个无法扩展的单点。</p><h1 id="ARIA"><a href="#ARIA" class="headerlink" title="ARIA"></a>ARIA</h1><p>Calvin 的实现具有扩展性,但是基于依赖分析的 BOHM 和 PWV 在这方面的表现不好;而得益于依赖分析,BOHM 和 PWV 在冲突场景下防止性能回退的表现较好,但 Calvin 在这一情况下的表现不理想。</p><p>在分布式系统中为了并发执行而进行依赖分析是比较困难的,所以 Aria 使用了一个预约机制,完整的执行过程是:</p><ul><li>一个 sequence 层为事务分配全局递增的 id;</li><li>将输入的事务持久化;</li><li>执行事务,将 mutation 存在执行节点的内存中;</li><li>对持有这个 key 的节点进行 reservation;</li><li>在 commit 阶段进行冲突检测,是否允许 commit,没有发生冲突的事务则返回执行成功;</li><li>异步的写入数据。</li></ul><p>输入事务在经过 sequencer 层之后被分配了全局递增的事务 id,此时执行结果就已经是确定性的了。经过 sequencer 层之后,事务被发送到 node 上,如图,$T<em>1$和 $T_2$在 node1 上,$T_3$ 和 $T_4$ 在 node2 上。先不管$T</em>{5-8}$,假设第一个batch只有这四个事务,在执行时,batch 中的事务执行结果会放在所属 node 的内存中,然后进行下一步。</p><p><img src="deterministic-database/14_Aria_c83c19e758.png" alt="Aria 执行过程一.png"></p><p><img src="https://img1.www.pingcap.com/prod/15_Aria_608bccd0f2.png" alt="图 15 - Aria 执行过程二.png"></p><p><img src="deterministic-database/15_Aria_608bccd0f2.png" alt="Aria 执行过程二.png"></p><p><img src="deterministic-database/16_Aria_d1c0225f5e.png" alt="Aria 执行过程三.png"></p><p>上图是 batch1 中的事务进行 reservation 的结果,需要注意的是执行事务的 node 不一定是拥有这个事务数据,但 reservation 的请求会发送到拥有数据的 node 上,所以 node 一定能知道和自身所存储的 Key 相关的所有 reservation 信息。在 commit 阶段,会发现在 node1 上 $T_2$ 的读集合与$T_1$ 的写集合冲突了,因此$T_2$ 需要被 abort 并且放到下一个 batch 中进行执行。对于没有冲突的 $T_1$,$T_3$和 $T_4$,则会进入写入的阶段。因为在 sequencer 层已经持久化了输入结果,所以 Aria 会先向客户端返回事务执行成功并且异步进行写入。</p><p><img src="deterministic-database/17_Aria_935f8d6b55.png" alt="Aria 执行过程四.png"></p><p>$T_2$ 加入到了 batch2 之中。但是在 batch2 中,$T_2$ 享有最高的执行优先级(在 batch 中的 id 最小),不会无限的因为冲突而被推迟执行,而且这一策略是能够保证唯一结果的。</p><p>和Calvin一样,也存在不确定性问题,如果$T_2$在$T_1$还没开始reservation之前就开始尝试提交,那么就无法发现冲突,$T_2$就比$T_1$先执行了,破坏了确定性的要求。为了解决这个问题,Aria也存在coordinator,利用coordinator保证所有节点处于同阶段。在$T_1$ 所在的 node1 完成 reservation 之前,node2 不能够进入 commit 阶段。</p><p>确定性数据库的优势是只需要有一个确定的事务顺序即可,所以可以根据输入事务进行重排序,Aria认为可以安全的并行执行WAR(读后写)依赖,所以它在commit阶段时对reservation的结果进行冲突检测时将RAW转化为转化为WAR。如图,只要不形成依赖环,那么就不需要abort,提高并行度的。</p><p>但是Aria存在barrier 限制,具体表现为,如果一个 batch 中存在一个事务的执行过程很慢,例如大事务,那么这个事务会拖慢整个 batch。</p><p><img src="deterministic-database/19_Aria_378f746fd5.png" alt="Aria 的重排序.png"></p><p>基于依赖分析的 BOHM 和 PWV:</p><ul><li>充分利用 MVCC 的并发性能</li><li>能够防止冲突带来的性能回退</li><li>单节点扩展困难,不适合大规模数据库</li></ul><p>分布式设计的 Calvin 和 Aria:</p><ul><li>单版本,存储的数据简单</li><li>长事务、大事务可能拖慢整个集群</li><li>barrier 机制需要 coordinator 进行实现,存在 overhead</li><li>如果一个节点出现故障,整个集群都将进入等待状态</li></ul><p>相比之下,基于 Percolator 提交协议的分布式数据库,只需要单调递增的时钟就能够实现分布式事务,对事务的解耦做的更加好。</p><blockquote><p><a href="https://pingcap.com/zh/blog/transaction-frontiers-research-article-talk3">https://pingcap.com/zh/blog/transaction-frontiers-research-article-talk3</a></p></blockquote>]]></content>
<categories>
<category> database </category>
<category> transaction </category>
</categories>
<tags>
<tag> database </tag>
<tag> txn </tag>
</tags>
</entry>
<entry>
<title>反熵和传播</title>
<link href="//post/communicate.html"/>
<url>//post/communicate.html</url>
<content type="html"><![CDATA[<p>为了在整个分布式系统可靠的传播数据记录,我们需要传播节点本身是可用,并且可访问其他节点,这种的瓶颈明显在于传播数据的节点的吞吐量和带宽。快速可靠的传播所有数据在分布式系统就显得很难实现,但是我们可用仅快速传输某些急需的,重要的数据,来保证系统的性能,例如成员信息,节点状态,结构变更等,这些信息一般出现频率不高,但是需要快速被传播,这种更新传播一般有三种方法</p><ul><li>广播,一个点广播给所有其他节点</li><li>反熵,定期点对点交换,存在一些信息交换组,在信息交换组中两点互相更新消息</li><li>Gossip,合作广播,类似树状结构,一层一层的广播,每层可并行,这样可用把广播的时间复杂度N降为LogN</li></ul><p>广播虽然最简单直接,但是一旦节点数量过多,广播的代价就显得很大,而且过度依赖一个节点,而且广播信息也很不可靠。</p><p>反熵可用允许某些节点传输失败,因为反熵会让节点之间重新同步,这样每个节点都有传播的责任。熵是一种衡量系统无序程度的属性,一般来说希望熵越低越好。</p><p><img src="communicate/image-20220709205033237.png" alt="通信"></p><h1 id="读修复"><a href="#读修复" class="headerlink" title="读修复"></a>读修复</h1><p>读取时最容易检测副本的差异,因此可以读多个副本,对比每个副本的查询结果,对结果进行检查,一般只限于查询客户端的请求,协调者执行这种读取时一般是乐观的去读取,如果响应不同,会将结果更新给相应的副本,这就是读修复。和<a href="https://codebells.github.io/post/distribute-basic.html#%E5%8F%AF%E8%B0%83%E4%B8%80%E8%87%B4%E6%80%A7">可调一致性</a>很像,但是读修复需要将不一致的副本修复,而可调一致性不需要修复数据副本。读修复可以阻塞或者异步,因实现方法而异。</p><h1 id="摘要读"><a href="#摘要读" class="headerlink" title="摘要读"></a>摘要读</h1><p>实际上就是检查副本之间是否存在差异,通过发送请求并且通过本地副本计算哈希值,然后发送给协调者,协调者比对所有收到的哈希值是否相同,如果相同,那么所有副本一致,如果不同,则能判断存在副本不一致情况,但是不能确定在哪个节点滞后。通常使用非加密哈希来实现,因为需要尽快算出摘要判断。</p><h1 id="提示移交"><a href="#提示移交" class="headerlink" title="提示移交"></a>提示移交</h1><p>是一种写侧修复机制,当目标节点没有成功写入的时候,写入协调者或者某个副本会存放一条记录表示目标节点未写入成功,作为一个提示,当目标节点恢复后,这个记录会被立刻重放过去。</p><h1 id="Merkle树"><a href="#Merkle树" class="headerlink" title="Merkle树"></a>Merkle树</h1><p>在许多数据库中用于降低数据比对的成本。Merkle树是一个由哈希值构成的树。最底层的哈希值是通过扫描整个表的对应范围数据进行哈希得到的,而高层的哈希值是通过对下一层的某个范围进行哈希得到,从而建立一个层次结构,能够时间复杂度log级别检测不一致,缩小不一致的范围。</p><p>要确定两个副本之间是否存在不一致,只需要比较Merkle树的根节点哈希值即可,通过自顶向下比较,则可找到节点间存在差异的数据范围,进行修复。有一个缺点,因为Merkle树是自底向上进行生成的,所以当数据变更后,会触发整个子树的哈希值变化。</p><p><img src="communicate/image-20220710211340566.png" alt="Merkle树"></p><h1 id="位图版本向量"><a href="#位图版本向量" class="headerlink" title="位图版本向量"></a>位图版本向量</h1><p>Bitmap version vector,基于最新更新情况来解决数据冲突。</p><p>每次写入由某个协调者协调,由节点n协调的节点本地序列号为i的事件表示为(i,n)。序列号i从1开始,每次节点执行写则递增。节点本地使用本地逻辑时钟,表示该节点直接看到(该节点作为协调者)或者间接看到(其他节点协调并复制过来的)的写入。本身协调的事件是不会存在间隙的,而由别的节点协调复制过来的,则会包含间隙,为了弥补间隙,会让两个节点同步逻辑时钟,并将缺失的间隙补全。例如图,3个节点P1-P3,P1的3个数据无间隙,从别的节点复制过来5 7两个逻辑时钟的数据,这时存在间隙,那么将和其他节点进行同步,补全这间隙,此时的状态可表示为$ P_1 {\rightarrow}(3,0101_2) $其中,3代表同步到逻辑时钟3,后续的二进制0101代表3之后的4个位置分别是无数据,有数据,无数据,有数据,即看到其他节点的逻辑时钟为5 7的值,同理可知$ P_2 {\rightarrow}(1,011001_2) $$ P_3 {\rightarrow}(2,0011_2) $</p><p><img src="communicate/image-20220710212658423.png" alt="位图版本向量"></p><p>上述都是反熵的各种方法。</p><h1 id="Gossip传播"><a href="#Gossip传播" class="headerlink" title="Gossip传播"></a>Gossip传播</h1><p>Gossip协议是一种概率性的通信过程,类似于谣言和疾病传播方式,只要还有想听(易感染)的人,谣言(疾病)就会继续传播。持有需要传播记录的进程称之为有<strong><em>传染性的</em></strong>,而任何尚未收到更新的进程称为<strong><em>易感染的</em></strong>。传染性的进程经过一段时间后的主动传播后,不再传播新的状态,称之为<strong><em>已删除的</em></strong>。所有进程都从易感染的开始,每当某个数据记录到达之后,状态转换为有传染性的,开始将更新分发给其他随机的相邻进程,一旦传染性的进程确定更新已经传播,那么就会变成已删除状态。</p><p>Gossip协议的效率取决于在将发送冗余消息的开销保持最低的情况下,能多快的感染尽可能多的节点。</p><p>多用于在大规模系统中可靠的分发消息,检测故障,维护成员信息。</p><blockquote><p>参考《数据库系统内幕》 Alex Petrov</p></blockquote>]]></content>
<categories>
<category> database </category>
<category> 通信 </category>
</categories>
<tags>
<tag> database </tag>
<tag> 通信 </tag>
</tags>
</entry>
<entry>
<title>一些没啥用的脚本</title>
<link href="//post/script.html"/>
<url>//post/script.html</url>
<content type="html"><![CDATA[<p>一些自己用的脚本,网易云日常及单曲听歌次数,微信步数,贴吧签到,B站日常等</p><p>没啥用,但是挂着玩玩</p><p>尽量使用真实QQ注册吧,当账号失效时会发送QQ邮件提醒</p><p><strong>仅自用,非盈利,勿分享</strong>,注册送3个月使用,到期可以给我发邮件,<strong>免费</strong>给续</p><span id="more"></span><p><strong>[email protected]</strong></p><p>网站的支付功能没开,本来想收费的,但是需要易支付的API,要花钱,想想估计也没啥人买,就懒得搞了,有啥需求邮件联系</p><p>刚开始挂的时候,自己的网易云音乐会经常掉线,重新登陆一下即可,后面就不会了</p><h1 id="网易云日常"><a href="#网易云日常" class="headerlink" title="网易云日常"></a>网易云日常</h1><p><img src="script/image-20220709160606235.png" alt="网易云音乐日常任务"></p><h1 id="微信支付宝步数"><a href="#微信支付宝步数" class="headerlink" title="微信支付宝步数"></a>微信支付宝步数</h1><p>我主要是为了搞蚂蚁森林能量</p><p><img src="script/image-20220709161512706.png" alt="image-20220709161512706"></p><h1 id="网址"><a href="#网址" class="headerlink" title="网址"></a>网址</h1><p><a href="http://codebells.uvgg.com/">http://codebells.uvgg.com/</a></p><p>使用不收费,支持运行服务器可以随缘给个噢</p><p><img src="script/image-20220709161854783.png" alt="image-20220709161854783"></p>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> 日常 </tag>
</tags>
</entry>
<entry>
<title>浅谈分布式事务</title>
<link href="//post/distribute-transaction.html"/>
<url>//post/distribute-transaction.html</url>
<content type="html"><![CDATA[<p>首先简单回顾事务的定义。事务包含了一个序列的对数据库的读/写操作,这些操作构成一个逻辑的整体,这个整体要么都执行成功,要么都执行不成功,数据库从一个一致性状态转移到另一个一致性状态,即事务的操作要么全对数据库产生影响,要么全不对数据库产生影响,事务就是数据库最小的逻辑执行单元。由此引出事务的ACID特性,即原子性,一致性,隔离性,持久性。维基百科对<a href="https://en.wikipedia.org/wiki/ACID">ACID</a>的描述如下:</p><blockquote><p>Atomicity(原子性):一个事务(Transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。</p><p>Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。</p><p>Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。</p><p>Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。</p></blockquote><p>一般来说原子性和一致性通过UndoLog实现,隔离性通过锁或者MVCC实现,持久性通过RedoLog实现。</p><p>分布式事务和单机事务定义一样,分布式事务就是由单机事务演变而来的,因为单机数据库无法承担过大数据量的负载,所以必须将数据拆分到不同的节点上去进行执行,这就涉及到一个问题,涉及多台数据库的事务如何保证ACID特性?</p><p>一般来说,微观的看事务,例如银行转账的例子,希望同时完成A账户减10,B账户加10的操作,这明显是多个操作,步骤涉及多个子操作,节点收到请求,解析请求,磁盘查找数据,写操作,最后确认,这是一个很繁琐的过程,要保证事务的特性就意味着我们必须先执行事务,再让结果可见。</p><h1 id="数据库分区"><a href="#数据库分区" class="headerlink" title="数据库分区"></a>数据库分区</h1><p>首先我们定义什么是数据库分区,单机数据库将数据存储在一个节点上,但是一台机器总有存不下的时候,很自然的我们想到,每个节点存一部分数据,最直接的方法就是将数据划分范围,并且允许每个副本集只管理特定的分区。执行查询的时候,客户端或者查询协调者将读写请求路由到正确的副本集即可。这种分区的方案通常称为分片,每个副本集作为数据某个子集的单个来源。</p><p>为了让集群的运行更加有效率,考虑负载的分布并确定分区,将负载较重的节点再分区,从而分散负载。而当集群添加或者删除节点时,数据库必须重新分区数据以保持均衡,为了保证数据迁移的一致性,我们必须让请求等待,直到数据迁移完毕。一般使用哈希来进行路由查找某些键的存储位置,使用哈希确定副本位置可以减少范围热点问题,因为哈希比较分散,最简单的方式就是对键值的哈希值取模,但是每当节点数量变动时,大部分数据都要被移动。这时候,一致性哈希就能派上用场。</p><h2 id="一致性哈希"><a href="#一致性哈希" class="headerlink" title="一致性哈希"></a>一致性哈希</h2><p>Cassandra和Riak就使用了一致性哈希的分区方案。还是同样的方法,每个键计算哈希值,返回的哈希值被映射到环上,以便在超过最大值后回到最小值。每个节点在环上拥有自己的位置,负责前一个节点到当前节点之间的值范围。</p><p>使用一致性哈希能减少维持数据均衡所需要的移动次数,节点离开或者加入指挥影响到环上与该节点直接相邻的节点,而不是整个集群,当哈希表大小变化时,如果我们有K和可能的哈希值,和n个节点,平均只需要移动K/n个键。这样影响就小了许多。</p><h1 id="多个操作的原子性"><a href="#多个操作的原子性" class="headerlink" title="多个操作的原子性"></a>多个操作的原子性</h1><p>为了让多个节点的操作看起来是原子的,我们需要一类称为原子提交的算法。也就是不允许参与者出现不一致的情况,即A提交,B不提交。这就意味着只要有一个参与者反对,那么整个事务就不能提交,也就是所有参与者必须达成共识。但是当存在拜占庭故障时(节点可能发送错误消息),这种算法就无法正常工作了,因为一旦一个节点发送错误消息,即导致整个事务出现不同的结果。</p><h2 id="两阶段提交-2PC"><a href="#两阶段提交-2PC" class="headerlink" title="两阶段提交(2PC)"></a>两阶段提交(2PC)</h2><p>最简单,最经典的分布式提交协议,保证多分区的原子更新。整个执行分为两阶段,第一阶段分发决定的值并收集投票,第二阶段,节点仅需要修改指针,让第一阶段的结果可见即可。</p><p>2PC中存在一个Leader或者Coordinator负责保存状态、收集投票、并作为协商的主要参考依据,其他节点称为参与者。通常来说,每个参与者负责一个分区,在这些数据上执行事务。协调者和每个参与者都会未所有执行过的步骤保留本地操作日志。</p><p>每个步骤中,协调者和参与者都必须将各个操作的结果写入持久性存储中,以便在发送了本地故障的情况下重建状态并恢复,并且可以帮助其他参与者重放操作。</p><h3 id="故障"><a href="#故障" class="headerlink" title="故障"></a>故障</h3><h4 id="参与者故障问题"><a href="#参与者故障问题" class="headerlink" title="参与者故障问题"></a>参与者故障问题</h4><p>如果其中一个参与者在准备阶段故障,也就是还没返回投票信息的时候故障了,那么协调者无法继续进行提交,因为2PC要求所有投票都是赞成票。如果一个节点不可用,那么协调者将中止事务,2PC会影响可用性,单节点故障即阻止事务提交。一些系统,例如Spanner,在Paxos组而非2PC的核心思想在于参与者的承诺,一旦做出了同意的投票就无法反悔,因此只有协调者能中止事务。</p><p>如果其中一个参与者在同意投票后发生故障,它必须到协调者去了解实际结果才能直到正确的值,因为有可能其他节点不同意投票,导致事务回滚。因此当参与者节点恢复时,必须和协调者的决定保持一致,为此通常将决策保留在协调者端,并且将决定的值复制给故障的参与者,在此之前,参与者无法处理请求,因为还处于不一致的状态。</p><p>并且在协议中可能出现阻塞状态,进程需要等待,那么如果消息丢失,进程会无限等待,如果协调者没有收到某个节点的投票,不会无限等待,而会触发超时机制,进行中止事务。</p><h4 id="协调者故障问题"><a href="#协调者故障问题" class="headerlink" title="协调者故障问题"></a>协调者故障问题</h4><p>两种情况,协调者在收集投票信息并传播了决定给部分节点后故障,那么此时,协调者已经做出了决定,但是没有发送给某个副本,这种情况,参与者可以从其他参与者的事务日志或者是备份协调者的日志中找到决策。因为决定一定是一致的,在一个参与者中提交,意味着其他所有参与者都必须提交。</p><p>如果协调者在发送决策之前故障,那么此时,没有参与者知道协调者是否做出了决策,而协调者只要不恢复,结果就永远不知道。因为这个原因,我们说2PC是一个阻塞原子提交算法。如果协调者始终无法恢复,那么只能重新为该事务执行2PC。三阶段提交解决了这个问题。</p><h2 id="三阶段提交-3PC"><a href="#三阶段提交-3PC" class="headerlink" title="三阶段提交(3PC)"></a>三阶段提交(3PC)</h2><p>为了在协调者故障的情况下保持可用性,避免进入阻塞状态,3PC增加了一个额外的步骤,并且参与者和协调者都有超时机制,使得参与者在协调者发生故障时仍然可以继续提交或中止。3PC假定同步网络模型,并且不存在通信故障。</p><p>3PC在提交阶段之前加了一个准备阶段,也就是用于协调者将投票的结果通知参与者的,通过投票,并且所有的参与者都决定要提交,则协调者会发送一条准备消息,指示他们准备提交,否则将Abort,并退出3PC流程。</p><p>我认为3PC和2PC最大的区别在于参与者和协调者都有超时机制,这样可以当协调者在发送决策之前故障时,直接超时Abort,而不用等待恢复了,网上说的2PC无法从协调者故障恢复,我认为协调者故障的第一种情况可以恢复,第二种情况引入协调者超时机制也能和3PC一样的效果。不太理解,希望有懂哥教我一下。</p><h1 id="Calvin分布式事务"><a href="#Calvin分布式事务" class="headerlink" title="Calvin分布式事务"></a>Calvin分布式事务</h1><p>传统数据库使用两阶段锁或者乐观并发控制来执行事务,并没有确定的事务顺序。这就意味着必须协调各节点以保证事务顺序。而确定性事务顺序消除了执行阶段的协调开销,因为所有副本有相同的输入,所以输出也是相同的。这种方法通常称为Calvin一种快速的分布式事务协议。FaunaDB是一个著名的例子。</p><p>为了获得确定性顺序,Calvin使用了sequencer,他是所有事物的入口点。这个sequencer确定了事务的执行顺序,为了最大限度的减少竞争以及批量的进行决策,Calvin将时间线切分成epoch。Sequencer收集事务并将其分组到短时间窗口(10ms),这些窗口称为复制单元,因此不需要为每个事务单独通信。</p><p>而当一批事务被成功复制后,sequencer会将其转发给scheduler,负责编排事务的执行。调度器使用确定性调度协议,可以并行的执行部分事务,同时保留sequencer指定的串行执行顺序。</p><p><strong>总结:</strong></p><ul><li>Sequencer 负责副本复制,并在每 10ms 打包所收到的事务,发送到相应的 scheduler 之上;</li><li>Scheduler 负责执行事务并且确保确定性的结果;</li><li>Storage 是一个单机的存储数据库,只需要支持 KV 的 CRUD 接口即可。</li></ul><p>Calvin中每个事务有读写集,读集即依赖,写集即结果。</p><p>事务执行线程分为四个步骤执行</p><ol><li>分析事务的读写集,用读集确定节点本地的数据记录,并创建活跃参与者(包含写集元素并且将对这些数据修改)的列表</li><li>收集执行事务所需的本地数据,收集到的数据转发给相应的活跃参与者。</li><li>如果当前执行线程在一个活跃参与者上执行,他将接收其他参与者发送的数据记录,并收集事务所需本地数据。</li><li>以Epoch为粒度持久化到本地存储。不用讲结果转发到其他节点,因为事务是确定性的</li></ol><p><img src="distribute-transaction/image-20220713212604600.png" alt="Calvin架构"></p><h1 id="Spanner分布式事务"><a href="#Spanner分布式事务" class="headerlink" title="Spanner分布式事务"></a>Spanner分布式事务</h1><p>Calvin和Spanner常常放在一起说,Spanner也是很有名的一个数据库,基于Spanner实现的数据库最有名的是CockroachDB和YugaByteDB。Calvin通过在Sequencer上达成共识顺序来建立全局事务执行顺序,Spanner则在每个分区的共识组使用2PC。Spanner的架构相当复杂。</p><p><img src="distribute-transaction/image-20220713214117476.png" alt="Spanner架构"></p><p>每个Spanner的server也就是副本,包含了多个tablet,每个tablet对应一个Paxos状态机。副本被分组为副本集,也就是paxos组,这是数据放置和复制的单元,每个paxos组有一个长期的领导者,在处理跨分片的事务时,领导者相互通信即可。</p><p>每个写入操作必须通过Paxos组的领导者,而读取可以在最新副本的tablet上进行,领导者上有锁表,和事务管理器,锁表用于使用两阶段锁机制来实现并发控制,事务管理器负责跨分片的分布式事务。需要同步的事务(事务内的读写操作)必须先从锁表中获取锁,而其他操作(快照读)就可以直接访问数据。</p><p>对于跨分片事务,Paxos组领导者必须协调并且执行两阶段提交来保证一致性,并且使用两阶段锁保证隔离性。2PC算法需要所有参与者都存活才能成功提交,因而可能会损害可用性。而Spanner使用Paxos组代替单个节点作为参与者,解决了这个问题,意味着拥有了容错能力。在Paxos组中,只有领导者会参与2PC。</p><p>Paxos组用于在多个节点之间一致的复制事务管理器的状态。在执行事务时,Paxos组的领导者先获取写锁,并且选择一个写入时间戳,这个时间戳必须大于之前任何事务的时间戳,然后通过paxos记录一条2PC的prepare日志。事务协调者收集时间戳,然后生成一个大于任何准备时间戳的提交时间戳,并通过paxos记录commit日志,然后,事务协调者需要等待直到提交时间戳过后,因为必须保证客户端只能看到时间戳已经过去的事务结果。之后将这个时间戳发送给客户端和各个领导者,领导者将commit日志和新的时间戳一同记录到paxos后,释放锁。</p><p>Spanner读写事务提供了外部一致性的序列化顺序,事务时间戳反映了序列化顺序,外部一致性具有可线性化等效的实时属性,$T_1$在$T_2$之前提交,那么$T_2$时间戳大于$T_1$的时间戳。</p><p>总结,Spanner使用Paxos进行一致性的事务日志复制,使用两阶段提交进行跨分片事务,使用TrueTime进行确定性事务排序。相比于Calvin,Spanner跨分区事务成本更高,因为有2PC。</p><h1 id="Percolator分布式事务"><a href="#Percolator分布式事务" class="headerlink" title="Percolator分布式事务"></a>Percolator分布式事务</h1><p>先了解什么时快照隔离,快照隔离保证事务内的所有读取结果和数据库中的某个快照一致。快照包含了在事务的开始时间戳之前提交的所有值。如果存在写写冲突,那么只有一个能够提交成功。这种策略通常称为首个提交者胜利(First committer wins)。</p><p>快照隔离能避免读偏斜(read skew),但是不能避免写偏斜(write skew),简单来说就是两个事务,修改的集合不相同,每个事务单独本身也不违反约束条件,但是两个都执行成功并提交就会违反约束。在读已提交级别会存在读偏斜,例如存在约束x+y必须等于100,$T_1$时刻x和y的值都为50,$Txn_1$ 读x,读到50,$Txn_2$更新$x=30,y=70$,这时如果$Txn_1$继续读y,就会读取到70,就出现了问题,而快照隔离就能解决该问题,$Txn_1$和$Txn_2$读$T_1$时刻快照,基于$T_1$只会读到$x=50,y=50$。写偏斜就不一样了。比如说约束条件为$x+y\leq100$,$T_1$时$x=30,y=50$,此时不管是读快照还是读已提交,$Txn_1$更新x=50,$Txn_2$更新y=70,都可以成功,但是最终得到的结果为$x=50,y=70$不满足约束。快照隔离的优势就是读的效率高,因为无需加锁。</p><h2 id="Percolator"><a href="#Percolator" class="headerlink" title="Percolator"></a>Percolator</h2><p><a href="https://tikv.org/deep-dive/distributed-transaction/percolator/">Percolator</a>就是一个在分布式数据库BigTable上实现事务API的库,实现了BigTable不支持的ACID事务以及Snapshot isolation隔离级别。Percolator使用不同的列来保存数据记录,已提交的数据点的位置和锁信息还有其他一些信息。</p><p>每个事务必须和同步时钟节点(timestamp oracle)通信两次,一次是获取事务开始时间戳,另一次是在提交的时候。写入会先被缓存,最后有客户端驱动进行2PC。</p><p><img src="distribute-transaction/image-20220715110818518.png" alt="初始时"></p><h3 id="写入"><a href="#写入" class="headerlink" title="写入"></a>写入</h3><p>看一看percolator如何使用2PC进行写入</p><ul><li><p>PreWrite阶段。事务尝试为所有的写入涉及的数据单元格加锁,其中一个被标记为主锁(primary lock),用于客户端恢复。事务会检查是否存在可能的冲突:是否存在其他食物已经用了更新的时间戳写入了数据,或者是还存在未释放的锁,如果检测到,那么中止事务。</p><ol><li>先从同步时钟拿个时间戳作为事务的start_ts</li><li>为事务涉及到的每个数据在lock列加锁,并且写入数据并附带start_ts,其中一个锁被选择为主锁,剩下的为副锁(secondary lock),额外记录主锁的位置。</li></ol><p><img src="distribute-transaction/image-20220715110757438.png" alt="经过PreWrite阶段"></p></li><li><p>Commit阶段。成功获取所有锁,执行完成事务,客户端开始释放锁,先放主锁,用写记录替换锁,通过该操作让写入对外可见,并且更新写入源数据作为最新数据节点的时间戳。</p><ol><li>从同步时钟获取commit_ts。</li><li>释放主锁,写记录进入write列并附带start_ts,</li><li>重复操作,处理所有副锁</li></ol><p><img src="distribute-transaction/image-20220715110852576.png" alt="经过Commit阶段"></p><p>例如上图,8:data@7,8是commit_ts,7是start_ts。数据存在start_ts的data列。</p></li></ul><h3 id="读取"><a href="#读取" class="headerlink" title="读取"></a>读取</h3><p>读操作步骤如下</p><ol><li>先从同步时钟获取一个时间戳ts</li><li>检查读取的行在[0,ts]中有无上锁<ul><li>有锁,意味着被别的更早开始的事务上锁了,我们不能确定那个事务是否会在ts之前提交,这时,就回退读事务,过段时间再进行。</li><li>无锁,可以继续下面的操作</li></ul></li><li>通过commit_ts读取最新的数据,也就是找[0,ts]中最新的commit_ts.</li><li>找对应的start_ts行的data列读取数据。</li></ol><p>例如</p><p><img src="distribute-transaction/image-20220715111720789.png" alt="读取示例"></p><ol><li>获得时间戳9</li><li>查看Bob行没上锁,所以继续下一步</li><li>在[0,9]找最新的commit_ts,找到8,有start_ts=7</li><li>所以去7时间戳对应的行去读取data,读到$3</li></ol><p>这种读取方式就可以无锁的进行读取,以及可以读取历史版本。例如,如果读的时间戳是7,那么我们看到的就是$10。</p><blockquote><p>参考《数据库系统内幕》 Alex Petrov</p></blockquote>]]></content>
<categories>
<category> database </category>
<category> transaction </category>
</categories>
<tags>
<tag> database </tag>
<tag> txn </tag>
</tags>
</entry>
<entry>
<title>分布式系统中的Data Consistency</title>
<link href="//post/distribute-basic.html"/>
<url>//post/distribute-basic.html</url>
<content type="html"><![CDATA[<p>一致性模型很重要,他解释了多数据副本系统的数据可见性的语义和行为。</p><ul><li>容错。当系统中某个组件出现故障时,系统仍能正确运行。主要目标就是使用冗余的部件来消除单点故障。</li><li>数据复制。通过在系统中维护多个副本来引入冗余,如何快速的原子性的更新多个副本数据成为问题。允许节点之间存在某种程度上的差异,而数据在用户感知下是完全一致。</li></ul><p><strong>CAP定理</strong></p><p>通常,分布式系统可能会出现故障,而为了系统的高可用,尽可能减少系统的停机时间,我们需要容忍一台节点或者多台节点出现问题不可用的情况,这样就需要引入冗余,而一旦添加了冗余机制,那么就会涉及到多副本的同步问题以及节点恢复问题。</p><p>而CAP定理就将实际问题抽象出3个概念,一致性(consistency),可用性(Availability),分区容忍性(Partition tolerance)。首先理解这三个概念</p><ul><li>一致性:各个数据副本数据的一致情况</li><li>可用性:客户端的访问能得到回应</li><li>分区容忍性:在一定时间内各个节点能够通信,达成数据一致</li></ul><p>CAP定理指出,无法同时满足上述三个特性,因为如果要满足高一致性,那么必须等待数据副本的数据同步,那么可用性即降低,如果保证高可用性,那么就无法花太多时间等待数据副本同步。而如果想要同时满足CA,那么,节点之间需要互相通信以同步数据,这样分区容忍性无法保证,但是实际情况下,网络分区无法控制,所以,只能于CA中选择一个,或者CP,或者AP。</p><p>CAP中的C和ACID中的C是完全不一样的,ACID中的C是,事务将数据库从一个有效状态到另一个有效状态,这个有效状态即没有违反任何完整性约束条件。而在CAP中的C意味着事务的原子性和一致性,不让数据处于不一致的状态;CAP中的A和数据库的高可用性也是不一样的。CAP的A对延迟没有限制,而是需要客户端的访问都能得到响应。而系统中的高可用性并不要求每个非故障节点响应每个请求。</p><h1 id="数据一致性"><a href="#数据一致性" class="headerlink" title="数据一致性"></a>数据一致性</h1><p>一致性。比较复杂,涉及理论一致性和并发模型,在这精简描述一下,一致性模型可以看成参与者之间的约定,及用户在发出读写请求时期望得到什么结果,描述了在存在多份并发访问的情况下可能出现的结果。这是从值传播的角度考虑一致性。</p><h2 id="严格一致性"><a href="#严格一致性" class="headerlink" title="严格一致性"></a>严格一致性</h2><p>任何进程的任何写入都可以立即被任何进程的后续读操作读取。涉及全局时钟的概念,即在全局情况下,时间戳靠后的事务一定能够读取到时间戳靠前的事务的数据。不可能实现,理论模型。</p><h2 id="可线性化"><a href="#可线性化" class="headerlink" title="可线性化"></a>可线性化</h2><p>是最强的单对象、单操作一致性模型。写操作的效果严格一次性的对所有读取者可见,可线性化可能存在不同的方式来确定事件的全序关系,在一个系统中,这个顺序应该一致的,也就是读取到的值必须是至少与之前读取的值是一样新的,也就是单调的读取。</p><p>原子原语,例如原子写和原子比较-交换(CAS)操作。原子写无需考虑当前寄存器值,这和CAS仅在前一个值没有变化的时候才能从一个值变为另一个值,也就是交换值,因为需要避免ABA问题。ABA问题就是A被存入寄存器中的时候,可能会又两个并发写入操作,设置了值B并切换回值A,也就是A不变,并不能保证A从上次读取后就没有更改。</p><p><strong>线性化点</strong>也就是,在线性化点之前,旧值可见,之后新值可见。</p><p>目前大部分系统避免可线性化,因为开销过高,即使是CPU访问主存也不提供可线性化,因为同步指令开销太大了,严重影响速度,但是可以使用CAS来引入可线性化,可以先准备结果,准备好后,交换指针即可使结果可见。</p><h2 id="顺序一致性"><a href="#顺序一致性" class="headerlink" title="顺序一致性"></a>顺序一致性</h2><p>可线性化的代价太高,可以放松模型的同时仍提供相当强的一致性保证。顺序一致性允许对事务进行排序,保证了同属一台进程的顺序不变,但是全局的顺序有可能不按照真实时间排序,但是要注意一点,其他节点观测到的顺序也必须是一致的。</p><h2 id="因果一致性"><a href="#因果一致性" class="headerlink" title="因果一致性"></a>因果一致性</h2><p>存在因果关系的操作保证顺序,其他的可以不保证相同的顺序被读取者观测。</p><p>因果一致性可以使用逻辑时钟实现,而且使用因果一致性可以在消息无序传递的情况下重新构建事务序列,填补消息空隙,直到收集所有的操作依赖项并恢复因果顺序。Dynamo和Riak使用向量时钟建立因果关系。</p><p><strong>向量时钟</strong>是一种用于在事件中建立偏序关系,检测和解决事件链分歧的结构。向量时钟是更细粒度的在数据上追加,而逻辑时钟是在本地进行维护</p><p>如图Dynamo</p><p><img src="distribute-basic/image-20220703210216966.png" alt="Dynamo Vector clock"></p><ol><li>client 端写入数据,该请求被 Sx 处理并创建相应的 vector ([Sx, 1]),记为数据 D1</li><li>第 2 次请求也被 Sx 处理,数据修改为 D2,vector 修改为([Sx, 2])</li><li>第 3、4 次请求分别被 Sy、Sz 处理,client 端先读取到 D2,然后 D3、D4 被写入 Sy、Sz</li><li>第 5 次更新时 client 端读取到 D2、D3 和 D4 3个数据版本,通过类似向量时钟判断同时发生关系的方法可判断 D3、D4 是同时发生的事件,因此存在数据冲突,最终通过一定方法解决数据冲突并写入 D5</li></ol><p>版本向量借鉴了向量时钟中利用向量来判断事件的因果关系的思想,用于检测数据冲突,和向量时钟不是一个东西。</p><h1 id="会话模型"><a href="#会话模型" class="headerlink" title="会话模型"></a>会话模型</h1><p>也成为以客户端为中心的一致性模型,从客户端的角度看分布式系统的状态,</p><p><strong>读自己写</strong>(read own write)一致性模型,当写操作完成后,在相同或其他副本上的读操作必须可以观测到写入的值。</p><p><strong>单调读</strong>模型读到的值一定不会是旧的。</p><p><strong>单调写</strong>模型同一客户端写入操作的执行顺序和写入的值的顺序一致。</p><p><strong>读后写</strong>确保写入被排在之前的读取操作返回结果之后。</p><p>将单调读单调写读自己写结合,就可以提供流水线随机访问存储器(PRAM)一致性,保证来自一个进程的写操作将按照进程执行的顺序传播。顺序一致性来自不同进程的写入可以以不同的顺序被观测</p><h1 id="最终一致性"><a href="#最终一致性" class="headerlink" title="最终一致性"></a>最终一致性</h1><p>更新被异步的传播,最终所有的访问都会返回最新的写入结果,没有时间限制多久必须达成一致,非常不可靠。</p><h1 id="可调一致性"><a href="#可调一致性" class="headerlink" title="可调一致性"></a>可调一致性</h1><p>使用三个参数调节数据复制读取和写入。</p><p>复制因子 N,存储数据副本的节点数</p><p>写入一致性 W,需要确认成功写入的节点数</p><p>读取一致性 R,需要确认成功读取的节点数。</p><p>通过调整参数保证W+R>N来达成一致性。保证读取到的一定是最新的值,类似抽屉原理,读取和写入的集合总是存在重叠。</p><p>Quorum协议是可以容忍不超过半数节点故障的可调一致性。即N和W为N/2+1或N/2.</p><p>为了降低存储成本,可以将副本分为见证者副本和拷贝副本,见证者副本仅存储部分记录,表示写操作发生过的关键源信息。</p><h2 id="见证者副本"><a href="#见证者副本" class="headerlink" title="见证者副本"></a>见证者副本</h2><p>正常存储副本需要大量单调数据冗余,我们可以通过见证者副本来降低存储成本,我们不需要在每个副本上存储数据记录的拷贝,而是将副本划分成两种,一个是拷贝副本,一个是见证者副本,见证者副本仅存储一些关键记录,表示写操作曾经发生过,可调一致性的写入因子就必须小于等于拷贝副本数量,但是当拷贝副本宕机过多,导致拷贝副本小于写入因子时,那么此时可以将见证者副本临时代替拷贝副本存储数据,等到原来的拷贝副本恢复再将数据迁移并且恢复原来状态,或者其他操作。</p><h1 id="强最终一致性和CRDT"><a href="#强最终一致性和CRDT" class="headerlink" title="强最终一致性和CRDT"></a>强最终一致性和CRDT</h1><p>在可线性化和最终一致性之间,还存在一个可能的中间地带,强最终一致性,提供了两种模式各自的好处。在这种一致性下,更新可能会延迟或者无序的传播到其他节点,但当所有更新都发送过去的时候,冲突可以被解决,他们最终将合并产生相同的有效状态,也就是conflict-free replicated data type(CRDT),无冲突复制数据类型。</p><p>可能存在网络分区,但是可以独立的在每个节点执行操作,当通信恢复时,所有节点的数据可以进行协调合并,网络分区期间执行的操作不会丢失。</p><p>最简单的例子<strong>CmRDT</strong>(Commutative Replicated Data Type),基于操作的可交换复制数据类型。需要满足三点</p><ul><li>无副作用,操作不会改变系统状态</li><li>可交换,顺序无关紧要,即操作满足交换律</li><li>按照因果关系排序,操作成功传递依赖的前提条件是,确保系统已经到达操作可以应用的状态</li></ul><p>CvRDT(Convergent Replicated Data Type),基于状态的同步,需要满足</p><ul><li>幂等律,不管发送多少次,结果总是一样的</li><li>单调的</li></ul><blockquote><p>参考<a href="https://highlyscalable.wordpress.com/2012/09/18/distributed-algorithms-in-nosql-databases/">DISTRIBUTED ALGORITHMS IN NOSQL DATABASES</a></p><p>《数据库系统内幕》 Alex Petrov</p></blockquote>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
<category> transaction </category>
<category> concurrency_control </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
<tag> 并发控制 </tag>
<tag> OCC </tag>
<tag> txn </tag>
</tags>
</entry>
<entry>
<title>SIGMOD2022 Natto论文阅读</title>
<link href="//post/natto.html"/>
<url>//post/natto.html</url>
<content type="html"><![CDATA[<p>Natto: Providing Distributed Transaction Prioritization for High-Contention Workloads论文阅读记录</p><p>Natto,支持事务划分优先级的Geo-distributed database system。<br>每个分片处理事务的顺序不是按照到达的顺序,而是用网络估计事务到最远分片的时刻,根据该时刻来为事务分配时间戳建立全局顺序,其中引入某些优化建立事务优先级。</p><span id="more"></span><p>Q:数据库如何自动给事务赋优先级?</p><ul><li>对某些时间敏感的事务分配高优先级,避免它与实时性要求不高的事务并发导致的影响</li><li>初始低优先级但abort多次的事务,避免饥饿现象</li></ul><h1 id="主要贡献"><a href="#主要贡献" class="headerlink" title="主要贡献"></a>主要贡献</h1><p>Natto,基于Carousel数据库完成,通过实现对事务划分优先级来降低具有高优先级事务的延迟</p><ul><li>使用网络测量来估计事务到达最远分片的时间,根据这个物理时间去为每个事务分配一个物理时间戳,在分配该时间戳之前,不会在任何分片处理该事务</li><li>引入conditional prepare以及priority abort和提前转发提交结果(减少事务延迟,降低事务完成时间)</li></ul><p>Natto和Carousel都是以2FI事务为前提的数据库</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分片复制到多数据中心,分片的Leader可能不在同一个数据中心,事务Coordinator一般选事务发起的客户端所在的DataCenter,可能不是事务涉及的数据分区的Leader,但是必须是某个分区的Leader</p><p>可能DC1:<strong>1shard</strong> 2shard,DC2:1shard <strong>3shard</strong>,DC3:<strong>2shard</strong> 3shard</p><h1 id="2FI事务"><a href="#2FI事务" class="headerlink" title="2FI事务"></a>2FI事务</h1><p>即2-round Fixed-set interactive 事务,读写涉及的key必须提前知道</p><p>一个特殊的读写事务,第一轮是读,读后跟着一轮写,在第一轮读完成后,读的值就都知道了,有些Read modified write可以直接借助于第一轮的结果实现</p><p>所有的读操作可以在第一轮时并发执行</p><p><em>为什么要2FI事务?</em></p><ul><li>直接在事务中实现read modified write,而非依赖于CPU架构</li><li>事务的读操作在第一轮可以并行执行</li></ul><h1 id="Carousel的事务流程"><a href="#Carousel的事务流程" class="headerlink" title="Carousel的事务流程"></a>Carousel的事务流程</h1><p>Coordinator是replica的Leader,分区的Leader可能和Coordinator不同</p><p>通过2PC和2FI事务的第二轮写重合来缩短事务完成时间</p><p>如图是Carousel的2FI事务执行流程</p><blockquote><p>首先客户端有一个涉及两个分区的事务,客户端首先发送 ①read-and-prepare request给数据涉及分区的Leader来执行事务,这个request包含了事务读和写涉及的所有key,因为所有key都知道,所以这轮就可以知道事务是否存在冲突,进而进行并发控制,②将读取的结果返回给客户端,然后③各个分区将准备阶段的数据同步到副本,同时,当客户端收到读的结果后④客户端将写入的数据随着commit request一起发送给协调者,⑤当协调者将写入的数据同步给副本后,而事务涉及到的数据分区Leader也已经将准备阶段的数据同步完,并且无冲突,返回prepared,⑥Coordinator接收到所有涉及的分区Leader返回的信息后,⑦则确定是否可以提交,因为此时已经将要写的数据同步给了Coordinator的副本,所以已经具有容错能力,则可以返回给客户端提交信息。此时异步的将这些数据同步给涉及的数据分区,数据分区Leader将所有数据同步给副本后,则该写入可见。</p></blockquote><p><img src="natto/image-20220624200654603.png" alt="Carousel's basic protocol"></p><h1 id="估计请求到达时间"><a href="#估计请求到达时间" class="headerlink" title="估计请求到达时间"></a>估计请求到达时间</h1><p>复用Domino的技术,通过定期心跳探测服务器的本地时间,利用两次差值计算服务器的单向延迟,将一段时间内收集到的延迟信息中的95分位点作为估计值</p><h1 id="Natto"><a href="#Natto" class="headerlink" title="Natto"></a>Natto</h1><p>基于Carousel实现Natto,类似于Carousel,Natto数据分区存储,数据分区可以分布在不同的数据中心。每个分区在不同的数据中心存放分区副本来实现容错,对于每个分区都会选择一个副本来作为分区的Leader。</p><p>Participant Leader:分区Leader<br>Participant Follower:分区Raft的Follower</p><p>只分两个优先级:低优先级,高优先级</p><h2 id="基于时间戳的优先级排序"><a href="#基于时间戳的优先级排序" class="headerlink" title="基于时间戳的优先级排序"></a>基于时间戳的优先级排序</h2><p>收到事务的第一轮读取和准备请求后,分区Leader在到时间戳之前不会处理该请求,如下图,预估到最远的分区Leader需要25ms,那么将事务时间戳设置为当前时间戳$T_0$ +25ms,那么在这个时间戳之前,不会处理该事务。如果有多个事务具有相同时间戳,那么则按照事务ID先后顺序执行。 </p><p><img src="natto/image-20220625190319035.png" alt="ordered by furthest timestamp"></p><p>对于不同优先级事务,使用不同的并发控制算法,对于低优先级事务,使用OCC,高优先级事务使用锁机制,避免不必要的中止高优先级事务。</p><p>处理高优先级事务的时候,需要获取所有读写涉及到的key的锁,当没有任何涉及到该key的其他事务处于Prepared阶段时,才能获取该key的锁。</p><p>如果事务晚于预期时间到达,如果和现在正在执行的事务有冲突,那么直接abort,因为可能造成死锁</p><h2 id="基于优先级的准备和中止"><a href="#基于优先级的准备和中止" class="headerlink" title="基于优先级的准备和中止"></a>基于优先级的准备和中止</h2><h3 id="priority-abort"><a href="#priority-abort" class="headerlink" title="priority abort"></a>priority abort</h3><p>基于时间戳会存在一个abort window,在abort window中的低优先级事务如果和到来的高优先级事务有冲突,就不让它开始,或者abort,以此来减少高优先级事务的延迟。</p><blockquote><p>例,$Txn_1$和$Txn_2$在分区A上存在冲突,当$Txn_1$预计时间戳为$T_0$ +25ms,$Txn_2$预计时间戳为$T_0$+30ms,但是$Txn_1$在$T_0$+10ms就到达了分区A,需要等待到$T_0$+25ms才能执行,而$Txn_2$在$T_0$+20ms到达分区A,发现与$Txn_1$存在冲突,此时$Txn_1$还没开始执行,所以,直接预先将其abort掉,防止延长高优先级事务的完成时间。</p></blockquote><p><img src="natto/image-20220625194012853.png" alt="Example of priority abort"></p><p>但是存在一种情况,就是可能高优先级事务的预计开始执行时间很大,这时候无需中止低优先级事务,因为低优先级事务很可能在高优先级事务开始前就执行完成,而此时根据Natto,是会中止低优先级事务的,这样就有可能导致不断中止低优先级事务,造成饥饿现象,即一直无法执行低优先级事务,可以使用现有的方法解决饥饿问题,如中止多次则提高优先级。</p><h3 id="conditional-prepare"><a href="#conditional-prepare" class="headerlink" title="conditional prepare"></a>conditional prepare</h3><p>如果在事务返回中止请求之前就能确定该事务就要被abort,那么和这个事务存在冲突的高优先级事务可以乐观的进行准备,以此来减少高优先级事务的延迟。</p><p>由于事务到达每个分区Leader的时间不一样,所以有可能存在一种情况就是,分区A知道存在高优先级事务与其冲突,需要priority abort,交予高优先级事务先执行,但是在分区B中,高优先级事务还未到达,而低优先级事务已经开始执行了,那么此时,分区B无法对低优先级事务进行中止,只能等待2PC的abort命令才能让高优先级事务持有锁。这就导致高优先级事务的等待。为了解决这个问题,引入conditional prepare,根据事务预计到达每个分区Leader的时间来判断,其他分区Leader会abort该低优先级事务,如果会,那么就乐观的进行准备</p><blockquote><p>例,低优先级$Txn_1$和高优先级$Txn_2$在分区A和B上存在数据冲突,预计$Txn_1$最远到达时间为$T_0$+25ms,而预计$Txn_2$最远到达时间为$T_0$+30ms,分区A上$Txn_1$到达时间为$T_0$+10ms,$Txn_2$到达时间为$T_0$+20ms,发生priority abort。而分区B在$T_0$+20ms收到$Txn_1$时,一直到$T_0$+25ms还没有收到$Txn_2$,此时$Txn_1$到达执行时间,开始执行,当时间到达$T_0$+30ms时,$Txn_2$终于到达,此时$Txn_1$已经执行,根据Client传来的预计到达各分区Leader的信息,发现在分区A上已经发生Priority Abort,那么,无需等待,进行conditional prepare</p></blockquote><p><img src="natto/image-20220625195631374.png" alt="Example of conditional prepare"></p><h2 id="提前提交状态转发"><a href="#提前提交状态转发" class="headerlink" title="提前提交状态转发"></a>提前提交状态转发</h2><p>当无法priority abort低优先级事务的时候,高优先级事务必须等待,直到低优先级事务提交后并将数据同步至follower才能进行。为了解决这个问题,提出Early committed state forwarding(ECSF),来允许提交的事务更新,尽早的对后续冲突事务可见。</p><h3 id="Local-ECSF"><a href="#Local-ECSF" class="headerlink" title="Local ECSF"></a>Local ECSF</h3><p>一旦分区Leader收到一个从Coordinator的已提交事务的数据更新,他会将数据对下一个冲突的事务可见,以此来减少所有事务的延迟</p><blockquote><p>例,$T<em>0$时刻$Txn_1$开始写A,在$ T</em>{20} $开始执行$Txn_2$但是因为有数据冲突,必须等待$Txn_1$执行完,并且对$Txn_2$可见才能执行$Txn_2$,一旦分区Leader收到协调者发来的$Txn_1$提交请求,那么使用LECSF,使$Txn_2$提前拿到这个值,而不需要等待数据同步以维持容错的过程,这就减少了事务延迟。</p></blockquote><p><img src="natto/image-20220625203410240.png" alt="Example of LECSF"></p><h3 id="Remote-ECSF"><a href="#Remote-ECSF" class="headerlink" title="Remote ECSF"></a>Remote ECSF</h3><p>提交的事务更新首先要在Coordinator上同步,实现容错,然后再转发给分区Leader。分区Leader必须等待更新的commit信息,才能处理后续的高优先级事务。RECSF目的为了减少高优先级事务的等待时间。</p><p>方法就是,当分区Leader处理事务的时候,它知道在等待的高优先级事务的前一个冲突事务,分区Leader将其读取请求转发给冲突事务的Coordinator,让Coordinator返回提交的结果即可。</p><blockquote><p>例,前段类似于LECSF,但是当处理$Txn_2$时,分区Leader不再等待Coordinator的提交更新数据传来,而是会将读取$Txn_1$的请求先转发给Coordinator,一旦Coordinator确定了要提交$Txn_1$,那么就会回应$Txn_2$的读取请求,这样就节省了时间。</p></blockquote><p><img src="natto/image-20220625205019501.png" alt="Example of RECSF"></p><p>LECSF和RECSF的区别在于,LECSF是被动的,Coordinator将commit命令发送给分区Leader时,其他事务再来这个Leader读数据就已经是可见的了,不需要其他操作,所以对于使用OCC的低优先级事务也可以适用;而RECSF是主动的,因为高优先级事务是2PL的,所以必须等待事务释放锁,才可以进行执行,但是在等待途中,就可以取Coordinator去取这个数据,一旦Coordinator上发出这个事务的commit命令,就可以直接将结果返回给客户端。</p><h1 id="实验"><a href="#实验" class="headerlink" title="实验"></a>实验</h1><p>和Carousel(2PC+OCC),TAPIR(2PC+OCC),类似Spanner(2PC+2PL)的系统对比,在本地集群模拟广域网延迟以及云服务器集群,使用三种不同负载,YCSB-T(KV事务测试),Retwis(类似Twitter的工作负载),SmallBank(模拟银行工作负载)</p><p>5分区3副本,每个数据中心最多存放一个分区的一个副本</p><p><img src="natto/image-20220626153400907.png" alt="DataPlaced"></p><p>10%的事务被标记为高优先级事务,每100ms获取一次各分区的延迟信息</p><h2 id="YCSB-T"><a href="#YCSB-T" class="headerlink" title="YCSB-T"></a>YCSB-T</h2><p>当事务输入速率从50增加到350txn/s时,carousel和2PL+2PC及TAPIR的高优先级事务的延迟显著增加,Natto增加的就不明显</p><p><img src="natto/image-20220626170106558.png" alt="image-20220626170106558"></p><p><img src="natto/image-20220626170117211.png" alt="image-20220626170117211"></p><p><img src="natto/image-20220626170143731.png" alt="image-20220626170143731"></p><p><img src="natto/image-20220626170156954.png" alt="image-20220626170156954"></p><p><img src="natto/image-20220626170215059.png" alt="image-20220626170215059"></p><p>xxxxxxxxxx sstring s = “12”;int a = atoi(s.c_str());s = std::to_string(a);c++</p><p><img src="natto/image-20220626170257428.png" alt="image-20220626170257428"></p><p><img src="natto/image-20220626170311588.png" alt="image-20220626170311588"></p><p><img src="natto/image-20220627093814891.png" alt="image-20220627093814891"></p><p><img src="natto/image-20220627093823986.png" alt="image-20220627093823986"></p><p><img src="natto/image-20220627093909234.png" alt="image-20220627093909234"></p><p><img src="natto/image-20220627093858698.png" alt="image-20220627093858698"></p><p><img src="natto/image-20220627093932034.png" alt="image-20220627093932034"></p><p><img src="natto/image-20220627093947242.png" alt="image-20220627093947242"></p>]]></content>
<categories>
<category> database </category>
<category> paper_read </category>
</categories>
<tags>
<tag> paper_read </tag>
<tag> database </tag>
<tag> system </tag>
</tags>
</entry>
<entry>
<title>ETL(Extract,Transform,Load)</title>
<link href="//post/etl-glue.html"/>
<url>//post/etl-glue.html</url>
<content type="html"><![CDATA[<p>ETL就是抽取转换加载,是一个数据集成的过程,它是一个将来自多个数据源的数据组合到单一的,一致的数据存储中,然后再添加到数据仓库或者别的什么系统中的工具。</p><span id="more"></span><h1 id="ETL基础概念"><a href="#ETL基础概念" class="headerlink" title="ETL基础概念"></a>ETL基础概念</h1><p>ETL五大模块分别是:数据抽取、数据清洗、库内转换、规则检查、数据加载。</p><h2 id="数据抽取"><a href="#数据抽取" class="headerlink" title="数据抽取"></a>数据抽取</h2><ul><li>确定数据源,需要确定从哪些源系统进行数据抽取</li><li>定义数据接口,对每个源文件及系统的每个字段进行详细说明</li><li>确定数据抽取的方法:是主动抽取还是由源系统推送?是增量抽取还是全量抽取?是按照每日抽取还是按照每月抽取?</li></ul><h2 id="数据清洗与转换"><a href="#数据清洗与转换" class="headerlink" title="数据清洗与转换"></a>数据清洗与转换</h2><ul><li><p>数据清洗<br> 主要将不完整数据、错误数据、重复数据进行处理</p></li><li><p>数据转换</p><ul><li><p>空值处理:可捕获字段空值,进行加载或替换为其他含义数据,或数据分流问题库</p></li><li><p>数据标准:统一元数据、统一标准字段、统一字段类型定义</p></li><li><p>数据拆分:依据业务需求做数据拆分,如身份证号,拆分区划、出生日期、性别等</p></li><li><p>数据验证:时间规则、业务规则、自定义规则</p></li><li><p>数据替换:对于因业务因素,可实现无效数据、缺失数据的替换</p></li><li><p>数据关联:关联其他数据或数学,保障数据完整性</p></li></ul></li></ul><h2 id="数据加载"><a href="#数据加载" class="headerlink" title="数据加载"></a>数据加载</h2><p>将数据缓冲区的数据直接加载到数据库对应表中,如果是全量方式则采用 LOAD 方式,如果是增量则根据业务规则 MERGE 进数据库</p><h1 id="AWS-Glue"><a href="#AWS-Glue" class="headerlink" title="AWS Glue"></a>AWS Glue</h1><p>Glue提供ETL服务,由Glue Data Catalog(中央元数据存储库),ETL引擎,以及处理的作业系统</p><h2 id="Data-Catalog"><a href="#Data-Catalog" class="headerlink" title="Data Catalog"></a>Data Catalog</h2><p>有时候会有从不同的数据源统一数据的需求,这时可能字段名不同,但是实际存的数据意义相同,这样需要对其进行统一,在glue中可以通过<a href="https://docs.aws.amazon.com/glue/latest/dg/add-classifier.html">crawler+classifier</a>实现。</p><p>存储了数据的位置、架构和运行时指标的索引,一般来说从不同的数据源爬取了程序之后,会通过Classifier对其进行分类,分类器检查给定文件的格式是否可以处理。如果可以处理,分类器将以与该数据格式匹配的对象的形式创建一个模式。</p><p><a href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-crawler-classifiers.html">Classifier</a>在爬虫任务的时候触发,通过爬虫+classifiers自动识别应该到哪个字段去,流程如下图。</p><p><img src="etl-glue/PopulateCatalog-overview-16559517552513.png" alt="爬网程序如何填充 AWS Glue Data Catalog"></p><h2 id="AWS-Lake-Formation-FindMatches"><a href="#AWS-Lake-Formation-FindMatches" class="headerlink" title="AWS Lake Formation FindMatches"></a>AWS Lake Formation FindMatches</h2><p>Glue内置了<a href="https://docs.aws.amazon.com/zh_cn/glue/latest/dg/machine-learning.html">机器学习功能</a>,可以通过 FindMatches 转换,可以识别数据集中的重复或匹配记录,即使记录没有公共唯一标识符且没有完全匹配的字段也是如此创建自定义转换来清理数据。</p>]]></content>
<categories>
<category> tech </category>
</categories>
<tags>
<tag> tools </tag>
</tags>
</entry>
<entry>
<title>mit6.824 raft实现 2D部分</title>
<link href="//post/raft-mit6-824-2d.html"/>
<url>//post/raft-mit6-824-2d.html</url>
<content type="html"><![CDATA[<p>将lab2最后一个部分结束,经过前三个部分,Raft基本已经实现,还剩一个2D,保存快照,其实实现起来并不困难,但是目前还存在Bug没有找到,仅Pass一个,等有时间再来吧,Debug太痛苦了,完成这一个lab,花了一个多礼拜,期间写代码的时间不长,Debug的时间占据大部分。写完之后,对并发以及raft有了更深的了解。</p><span id="more"></span><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><p>实现快照的思路也很简单,在前面的<a href="https://codebells.github.io/post/raft-paper.html#%E5%BF%AB%E7%85%A7">论文部分</a>也说到过了,Raft 的日志在正常运行时会增长,以容纳更多的客户端请求,但在实际系统中,<strong>它不能无限制地增长</strong>。 随着日志变长,它会占用更多空间并需要更多时间来重播。 如果没有某种机制来丢弃日志中积累的过时信息,这最终会导致<strong>可用性问题</strong>。因此我们使用Snapshot(快照)来简单的实现日志压缩。在快照部分,除了客户端会主动发起快照请求,当follower节点的日志小于Leader节点的快照时,那么直接通过同步快照,将Follower节点同步,同步时将快照index之前的日志删除,对于日志所代表的index也得变成快照的index+日志index,才是真实的index。</p><p><img src="raft-mit6-824-2d/image-20220616202934727.png" alt="Snapshot"></p><h1 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h1><p>这一章仅作参考,因为我自己也没有通过测试,希望发现代码问题的朋友评论交流一下。</p><p>InstallSnapshot和LeaderSendSnapshot是RPC函数,这时2D实现比较多坑的点,目前我报的错是failed to reach agreement,2D的测试会将网络和RPC都搞的乱七八糟,而且日志很多,查起来很困难,暂时先写到这吧。详细思路还是看代码注释</p><p>再说一点,在2D,日志真实Index会根据快照的lastIncludedIndex计算,所以在使用时,需要进行一个index的转换,代码得改动</p><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">//结构体的含义见上图type InstallSnapshotArgs struct{Term int LeaderIdintLastIncludedIndexintLastIncludedTermint// Offsetint//Figure 13 对快照进行拆分需要OffsetData[]byte// Donebool}type InstallSnapshotReply struct{Termint}func (rf *Raft) CondInstallSnapshot(lastIncludedTerm int, lastIncludedIndex int, snapshot []byte) bool { //测试用的函数,如果其他的Leadersnapshot实现的完整,这个可以不用实现 //to avoid the requirement that snapshots and log entries sent on applyCh are coordinated。 //你发送了快照,那么你发送的快照就要上传到applyCh,而同时你的appendEntries也需要进行上传日志,可能会导致冲突。return true}func (rf *Raft) InstallSnapshot(args *InstallSnapshotArgs,reply *InstallSnapshotReply){rf.mu.Lock()defer rf.mu.Unlock()if args.Term<rf.currentTerm||args.LastIncludedIndex<rf.lastIncludedIndex{ // 正常情况不会出现,但是乱七八糟的并发时有可能出现发过来的快照是旧快照reply.Term=rf.currentTermreturn }rf.currentTerm=args.Term//更新Termreply.Term=args.Termif args.LastIncludedIndex<rf.getLastIndex(){ //如果快照的index比server的最后一条entry的index小,说明server应用了快照之后,日志还存在数据rf.lastIncludedIndex=args.LastIncludedIndexrf.lastIncludedTerm=args.LastIncludedTermnewLogs := make([]LogEntry,0)newLogs = append(newLogs,LogEntry{Term:rf.lastIncludedTerm,})for snapIndex:=1;snapIndex<rf.getLogLenth();snapIndex++{if rf.getReorderedLogIndex(snapIndex)>args.LastIncludedIndex{ //getReorderdLogIndex(i)就是 lastIncludedIndex+i,得到应用snapshot后的indexnewLogs=append(newLogs,rf.logs[snapIndex])}}if rf.commitIndex<args.LastIncludedIndex{rf.commitIndex=args.LastIncludedIndex}if rf.lastApplied<args.LastIncludedIndex{rf.lastApplied=args.LastIncludedIndex}rf.logs=newLogs}else{// 如果快照的index不小于server的最后一条entry的index,说明server应用了快照之后,日志不存在数据,直接清空即可rf.lastIncludedIndex=args.LastIncludedIndexrf.lastIncludedTerm=args.LastIncludedTermnewLogs := make([]LogEntry,0)newLogs = append(newLogs,LogEntry{Term:rf.lastIncludedTerm,})if rf.commitIndex<args.LastIncludedIndex{rf.commitIndex=args.LastIncludedIndex}if rf.lastApplied<args.LastIncludedIndex{rf.lastApplied=args.LastIncludedIndex}rf.logs=newLogs} //应用快照后,发送ApplyMsgmsg := ApplyMsg{SnapshotValid: true,Snapshot: args.Data,SnapshotTerm: rf.lastIncludedTerm,SnapshotIndex: rf.lastIncludedIndex,}rf.applyCh<-msgrf.persister.SaveStateAndSnapshot(rf.persistData(),args.Data)}func (rf *Raft) leaderSendInstallSnapshot(server int){args:=InstallSnapshotArgs{Term :rf.currentTerm,LeaderId:rf.me,LastIncludedIndex:rf.lastIncludedIndex,LastIncludedTerm:rf.lastIncludedTerm,Data:rf.persister.ReadSnapshot(),}reply:=InstallSnapshotReply{}ok := rf.peers[server].Call("Raft.InstallSnapshot", &args, &reply)if rf.state != Leader || rf.currentTerm != args.Term {return //防止并发修改问题}if reply.Term > rf.currentTerm{//发送了旧快照,当前是旧leader,变为followerrf.becomeFollower()rf.currentTerm=reply.Termrf.persist()}rf.updateNextAndMatch()//next和match数组记得更新if !ok{return }}//index代表是快照apply应用的index,而snapshot代表的是上层service传来的快照字节流,包括了Index之前的数据// 这个函数的目的是把安装到快照里的日志抛弃,并安装快照数据,同时更新快照下标,属于peers自身主动更新,与leader发送快照不冲突func (rf *Raft) Snapshot(index int, snapshot []byte) { rf.mu.Lock()defer rf.mu.Unlock()if rf.commitIndex<index || rf.lastIncludedIndex>=index{ // 如果commitIndex < index ,那就不能打快照,因为还没提交到这个位置//如果rf.lastIncludedIndex>=index,那就是已经打过这个快照或者更新的快照了,不需要再打这个快照了return }newLogs := make([]LogEntry,0)newLogs = append(newLogs,LogEntry{Term : rf.getLogTerm(index)})for i:=index+1;i<=rf.getLastIndex();i++{newLogs=append(newLogs,rf.logs[rf.getLogIndex(i)])} //丢弃index之前的日志rf.lastIncludedTerm=rf.getLogTerm(index)rf.lastIncludedIndex=indexif rf.commitIndex<index{rf.commitIndex=index}if rf.lastApplied<index{rf.lastApplied=index} //更新commitIndex和lastAppliedrf.logs=newLogsrf.persister.SaveStateAndSnapshot(rf.persistData(), snapshot)}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><p>还有一点就是Leader发送快照的时机需要把握,我的判断是当nextIndex[]小于rf.lastIncludedIndex时就发送快照,快速同步,这个判断在发送日志同步的RPC中进行,也就是sendAppendEntries。</p><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">if rf.nextIndex[server]-1 < rf.lastIncludedIndex{go rf.leaderSendInstallSnapshot(server)return }<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="框架总览"><a href="#框架总览" class="headerlink" title="框架总览"></a>框架总览</h1><p>多理解Raft的框架,还是有助于理解整个raft集群的交互过程的,首先是客户机会发送查询给状态机,在单个节点上状态机会根据客户机的请求以及节点状态判断发送命令。我们编写的代码就是单个节点Service和RaftCode以及Persistent Storage的交互过程,主要还是RaftCode,剩下的大部分都是框架自己实现的。对于2D,Service节点调用Snapshot、与CondInstallSnap两个函数,所以需要我们来编写这个进行测试。对于2B,Service发送Start(cmd),RaftCode接收到,和其他节点进行RPC调用,可能当前节点超时了,那就变为Candidate发送RequestVote,进行选举。要不然就是Leader节点进行发送AppendEntriesRPC进行广播同步数据。期间,RaftNode会对数据和快照持久化到Persistent Storage中,当有需要的时候,就会读取持久化的数据,读快照的过程由状态机完成。</p><p><img src="raft-mit6-824-2d/45450bd6100f4765b716ff8490839d46.png" alt="总体框架"></p><p>因为snapshot其实就是service对raft节点调用的,使raft节点更新自身的快照信息。这样有的人可能会认为这样违反了Raft的强领导者原则。 因为跟随者可以在领导者不知情的情况下更新自己的快照。但是其实这种情况其实是合理的,更新快照只是为了更新数据,与达成共识并不冲突。数据仍然只是从领导者流向下层,followers只是通过快照去减轻它们的存储负担。这个在论文中有提到。</p><h1 id="Debug踩坑"><a href="#Debug踩坑" class="headerlink" title="Debug踩坑"></a>Debug踩坑</h1><p>对于2D,花了很多时间Debug,但是找出来的bug寥寥无几,2D的代码其实并不难,只能说是比较繁琐,对于ABC中使用的下标的更改,改完进行回归测试,然后对于SendSnapshot的时机需要把握</p><ol><li>NextIndex数组更改的时机,其实这个bug,我很困惑,为什么在发送日志同步的时候同步nextIndex还有在Leader发送快照时更新发送目标节点的nextIndex还不够,需要将整个nextIndex都更新才能通过,这个bug倒是好找,但是不理解</li><li>还是要细心吧,apply了snapshot之后,新的日志的第0个也应该是空,并且最好设为lastIncludedTerm</li></ol><p>没有通过全部测试,不放截图了</p><p>放个自己实现的<a href="https://github.com/Codebells/Raft/tree/go_imp">Github链接</a>仅供参考吧</p>]]></content>
<categories>
<category> database </category>
<category> lab </category>
</categories>
<tags>
<tag> database </tag>
<tag> lab </tag>
<tag> raft </tag>
</tags>
</entry>
<entry>
<title>mit6.824 raft实现 2C部分</title>
<link href="//post/raft-mit6-824-2c.html"/>
<url>//post/raft-mit6-824-2c.html</url>
<content type="html"><![CDATA[<p>继续上次,将lab2C部分实现,2C部分C虽然只是持久化,代码量很小,但是测试数据变强了很多,想要通过2C的测试,需要对前两个lab的实验做到基本无错。记录一下踩的坑。</p><span id="more"></span><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><p>做到这一步的时候,对于Lab2应该已经是思路十分清晰了,对于2C来说实现的话就是其状态的persist(持久化)为了能在机器crash的时候能够restore原来的状态。在论文中也指出了,需要持久化的只有这三个变量,日志,VotedFor还有CurrentTerm,其他不需要持久化,因为都可以根据这三个计算出来。</p><p>在这里的Figure 8 (unreliable)会测试之前的<a href="https://codebells.github.io/post/raft-mit6-824-2b.html#%E6%97%A5%E5%BF%97%E5%A2%9E%E9%87%8FRPC%E5%AE%9E%E7%8E%B0">冲突优化</a>有没有完成。也就是在RPC的时候,如果有冲突,直接返回冲突的下标,或者term,然后返回冲突的下标,进行修改nextIndex[i]亦或者是term,达到减少RPC的次数。原文在page7最下方的引用。</p><blockquote><p>If desired, the protocol can be optimized to reduce thenumber of rejected AppendEntries RPCs. For example,when rejecting an AppendEntries request, the follower.can include the term of the conflicting entry and the first index it stores for that term. With this information, the leader can decrement nextIndex to bypass all of the conflicting entries in that term; one AppendEntries RPC will be required for each term with conflicting entries, rather than one RPC per entry. In practice, we doubt this optimization is necessary, since failures happen infrequently and it is unlikely that there will be many inconsistent entries.</p></blockquote><p>代码上实现persist、与readPersist函数即可。</p><p><img src="raft-mit6-824-2c/image-20220616171458260.png" alt="PersistentState"></p><h1 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h1><p>实际上这实现的persist和readPersist没啥看的,就是个编码解码的过程,编码后调用实现好的SaveRaftState即可。</p><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (rf *Raft) persistData() []byte {w := new(bytes.Buffer)e := labgob.NewEncoder(w)e.Encode(rf.currentTerm)e.Encode(rf.votedFor)e.Encode(rf.logs)e.Encode(rf.lastIncludedIndex)e.Encode(rf.lastIncludedTerm)data := w.Bytes()return data}func (rf *Raft) persist() {data :=rf.persistData()rf.persister.SaveRaftState(data)}func (rf *Raft) readPersist(data []byte) {if data == nil || len(data) < 1 { // bootstrap without any state?return}r := bytes.NewBuffer(data)d := labgob.NewDecoder(r)var currentTerm intvar votedForint var logs[]LogEntryvar lastIncludedIndexintvar lastIncludedTermintif d.Decode(&currentTerm) != nil || d.Decode(&votedFor) != nil || d.Decode(&logs) != nil || d.Decode(&lastIncludedIndex) != nil|| d.Decode(&lastIncludedTerm) != nil{log.Println("DecodeError")} else { rf.currentTerm = currentTerm rf.votedFor = votedFor rf.logs=make([]LogEntry, len(logs)) rf.lastIncludedIndex=lastIncludedIndex rf.lastIncludedTerm=lastIncludedTerm copy(rf.logs,logs)}}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="Debug"><a href="#Debug" class="headerlink" title="Debug"></a>Debug</h1><p>在lab2C里面,出现的错会很多,一遍一遍的捋,找到自己并发的错误,有时候就是某个数据没有及时更新的问题,一定要注意nextindex数组和MatchIndex数组还有commitIndex及时更新,否则会出现奇奇怪怪的错误。网络不稳定,可能会延迟、RPC乱序、丢失等因素。</p><p>处理乱序RPC可以在接收到RPC回复的时候,还要检查一下自己的状态。比如是不是还是leader;比如leader发送时候的term和收到时候的term是否一致,如果不符合,就说明由于延时等情况,收到了过期的RPC消息,这时候不应该处理任何消息</p><blockquote><p>TestPersist12C:基础测试,就是全部crash掉,然后再起来,看看你的数据掉没掉</p><p>TestPersist22C:网络整乱一点,多台机器起了又宕,宕了又起</p><p>TestPersist32C:3节点,网络分区,然后把2个节点的分区crash掉,边重启边插数据</p><p>TestFigure82C:测试Figure 8 的安全提交</p><p>TestUnreliableAgree2C:乱序提交</p><p>TestFigure8Unreliable2C:疯狂乱序网络分区,整的一团糟,再测试你的Figure8,并且必须10s内选出主节点</p><p>internalChurn:RPC不能太多,RPC检查</p></blockquote><p><img src="raft-mit6-824-2c/image-20220616174046144.png" alt="pass"></p><p><img src="raft-mit6-824-2c/image-20220616174125007.png" alt="pass 500"></p><p>附上我的<a href="https://github.com/Codebells/Raft/tree/go_imp">Github实现代码</a>仅供参考</p>]]></content>
<categories>
<category> database </category>
<category> lab </category>
</categories>
<tags>
<tag> database </tag>
<tag> lab </tag>
<tag> raft </tag>
</tags>
</entry>
<entry>
<title>mit6.824 raft实现 2B部分</title>
<link href="//post/raft-mit6-824-2b.html"/>
<url>//post/raft-mit6-824-2b.html</url>
<content type="html"><![CDATA[<p>继续将Mit6.824的2b部分实现,以及踩的坑,对于2B,也是运行了上千次,都能pass,比较稳健,记录一下踩的坑以及思路解释</p><span id="more"></span><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><p>对于日志增量的思路其实论文中也很多,大体上和前文讲到的<a href="https://codebells.github.io/post/raft-paper.html#%E6%97%A5%E5%BF%97%E5%A4%8D%E5%88%B6">日志增量</a>区别不大,但是对于很多细节需要好好把握,在其中,最重要的还是并发问题。首先当节点成为Leader时,需要立刻广播AppendEntries,在没有日志时,我们可以理解成心跳,在存在日志时,这个心跳包应当携带Follower需要的日志,将其Append到Follower节点的日志中。而如何对Follower所需要的日志进行判断,需要raft的Leader节点维护一个NextIndex[]数组,以及MatchIndex[]数组,分别代表各节点下一个插入的日志应该在哪个index以及各节点已经确定同步的日志index位置。这两个数据在新Leader出现时初始化值NextIndex为leader的log长度+1,MatchIndex为0,通过心跳包的试探,不断回退NextIndex值,直到匹配上日志。节点的日志都是相同顺序,在相同index上一定是相同日志,所以一旦从后往前匹配到相同日志,说明之前日志都是相同的,这时即可找到MatchIndex和NextIndex的值,再对后续的日志数据进行同步。</p><p>同步的过程可以一条日志一条日志的同步,也可以多条日志放在同一个心跳包中一起同步,找匹配NextIndex时,也有一个小优化点,可以尽快达到同步点,放在后面代码说。</p><p><img src="raft-mit6-824-2b/image-20220616101659461.png" alt="AppendEntriesRpcStruct"></p><h1 id="Start"><a href="#Start" class="headerlink" title="Start"></a>Start</h1><p>通过对test代码的查看,知道2B测试时,先是调用start函数,对leader的日志进行插入,然后检查日志是否成功写入是通过applyChan实现,通过查看applych这个Channel信息来检测是否成功同步日志,具体可以对测试源码进行查看,对于返回值,注释说的很清楚,第一个是该日志的index,第二个int是当前term,第三个返回当前Server是否为Leader。</p><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (rf *Raft) Start(command interface{}) (int, int, bool) {rf.mu.Lock()defer rf.mu.Unlock()if rf.state != Leader {return -1, rf.currentTerm, false}term := rf.currentTermrf.logs = append(rf.logs, LogEntry{term, command})rf.persist()return rf.getLastIndex(), term, true}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="日志增量RPC实现"><a href="#日志增量RPC实现" class="headerlink" title="日志增量RPC实现"></a>日志增量RPC实现</h1><p>该增量实现为2C之前的版本,到2D版本会有一些index的修改以及部分日志检查的修改。</p><div class="code-wrapper"><pre class="line-numbers language-go" data-language="go"><code class="language-go">func (rf *Raft) applyLogs() {//将日志发送给applych,告知该日志已经applyfor i := rf.lastApplied + 1; i <= rf.commitIndex; i++ {//可以Apply到CommitIndex为止rf.lastApplied = i//更新lastApplied值rf.applyCh <- ApplyMsg{CommandValid: true,Command: rf.logs[rf.getLogIndex(i)].Command,CommandIndex: i,}}}func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {rf.mu.Lock()defer rf.mu.Unlock()defer rf.persist()if args.Term < rf.currentTerm { //当前节点Term大,Leader是旧Leader,废弃reply.Term = rf.currentTermreply.RollBackIndex = -1reply.RollBackTerm = -1reply.Success = falsereturn}if args.Term > rf.currentTerm {//当前节点是旧节点,先变为Follower再说rf.becomeFollower() // 顺序一定不能变,会因为这个出现并发问题,见Debug1rf.currentTerm=args.Term} //更新raft节点lastIndex := rf.getLastIndex()rf.noBlockChan(rf.heartbeatCh, true)reply.Term = rf.currentTermreply.Success = false//先全为falsereply.RollBackIndex = -1//冲突检查优化reply.RollBackTerm = -1//日志检查if args.PrevLogIndex > lastIndex {//Follower的日志比发过来的PrevLogIndex更短,这时RollBackTerm为-1, //可以借这个来判断是节点日志更短reply.RollBackIndex = lastIndex + 1 //返回当前节点最后的日志再判断一下 return}if rf.logs[args.PrevLogIndex].Term != args.PrevLogTerm { // 再PrevLogIndex这个位置上和Leader的对应的日志term不同,说明有冲突// 开始检查这个节点的日志该Term的日志,如果有的话,因为Log是完全一致的,Leader一定是包含所有最新的日志条目 // 所以,可以快速检查这个Term的日志是否Leader中存在,如果Leader存在,即可快速定位, // 如果不存在,就可以省去检查该Term日志的时间// 如果有这样的日志条目,那么RollBackIndex更新为这条条目的index,否则RollBackIndex为-1reply.RollBackTerm = rf.logs[args.PrevLogIndex].Term//更新RollBackTerm为这个位置上的Termfor i := args.PrevLogIndex; i >= 0 && rf.logs[i].Term == rf.logs[args.PrevLogIndex].Term ; i-- { reply.RollBackIndex = i}return}//如果通过了日志检查,Server在PrevLogIndex存在和Leader相同的日志 //那么后面的日志全部清除,开始Append所有Leader的后续日志 //这个地方是存在问题的,但是仍然能稳定过2b的case,我们需要在网络一团糟的时候,选出leader,并且同步 //有可能会存在append的日志当前节点全部存在的情况,也就是只能清空出现冲突的日志之后的日志 //如果没有冲突,是不能无脑清空的,在后续2D的时候发现该问题后改正startClean := args.PrevLogIndex + 1rf.logs = rf.logs[:startClean] rf.logs = append(rf.logs, args.Entries...)reply.Success = true//只有匹配成功并且Append后返回truerf.checkCommitIndex(args.LeaderCommit)//更新server的CommitIndex}func (rf *Raft) checkCommitIndex(commit int){//更新Follower的CommitIndex变为LeaderCommitIndexif commit> rf.commitIndex {if commit < rf.getLastIndex() {rf.commitIndex = commit} else {rf.commitIndex = rf.getLastIndex()}go rf.applyLogs()}}func (rf *Raft) sendAppendEntries(server int,args *AppendEntriesArgs, reply *AppendEntriesReply) {ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)if !ok {return}rf.mu.Lock()defer rf.mu.Unlock()defer rf.persist()if rf.state == Leader && args.Term == rf.currentTerm{ //和前面的RequestVote一样,防止并发更改了当前节点状态,使其变为旧数据,旧节点if reply.Term > rf.currentTerm {//若Leader的Term比Followers的Term低,说明是旧Leader,降级成为Followerrf.becomeFollower()rf.currentTerm=args.Termreturn}else if reply.Term == rf.currentTerm{if reply.Success {//如果Server匹配PrevLogIndex并且已经append了Args所携带的后续日志,返回的truenewMatchIndex := args.PrevLogIndex + len(args.Entries)//更新matchIndexif newMatchIndex > rf.matchIndex[server] {//防止并发rf.matchIndex[server] = newMatchIndex}rf.nextIndex[server] = rf.matchIndex[server] + 1rf.updateCommit()//更新Leader的CommitIndex} else{//不匹配PrevLogIndex,有两种情况if reply.RollBackTerm < 0 {//1.Server日志太短rf.nextIndex[server] = reply.RollBackIndex//快速定位rf.matchIndex[server] = rf.nextIndex[server] - 1} else {//2.该index日志不匹配newNextIndex := rf.getLastIndex()//从后往前找,找和RollBackfor ; newNextIndex >= 0; newNextIndex-- {if rf.logs[newNextIndex].Term == reply.RollBackTerm { //找到和冲突term相同的index,找到了说明这个是应当同步的点立刻breakbreak}}if newNextIndex < 0 {rf.nextIndex[server] = reply.RollBackIndex} else {rf.nextIndex[server] = newNextIndex}rf.matchIndex[server] = rf.nextIndex[server] - 1}rf.updateCommit()//更新CommitIndex// args.PrevLogIndex=rf.nextIndex[server]-1// args.PrevLogTerm=rf.logs[args.PrevLogIndex].Term// args.LeaderCommit=rf.commitIndex// go rf.sendAppendEntries(server,args,reply)}}}}func (rf *Raft) broadcastAppendEntries() { if rf.state == Leader {for server := range rf.peers {if server != rf.me {args := AppendEntriesArgs{Term:rf.currentTerm,LeaderId:rf.me,PrevLogIndex:rf.nextIndex[server]-1,PrevLogTerm:rf.logs[rf.nextIndex[server]-1].Term,LeaderCommit:rf.commitIndex,}entries := rf.logs[rf.nextIndex[server]:]args.Entries = make([]LogEntry, len(entries))copy(args.Entries, entries)//深拷贝reply :=AppendEntriesReply{Term:rf.currentTerm,Success:false,}go rf.sendAppendEntries(server, &args, &reply)}}}}func (rf *Raft) updateCommit(){//更新LeaderCommitIndexfor n := rf.getLastIndex(); n >= rf.commitIndex; n-- {count := 1if rf.logs[n].Term == rf.currentTerm {//只能提交当前Term的,否则会出现Figure8的问题for i := 0; i < len(rf.peers); i++ {if i != rf.me && rf.matchIndex[i] >= n {count++}}}if count > len(rf.peers) / 2 {//有一半以上节点提交,那么就可以提交rf.commitIndex = nrf.applyLogs()break}}}<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></div><h1 id="Debug及踩坑"><a href="#Debug及踩坑" class="headerlink" title="Debug及踩坑"></a>Debug及踩坑</h1><p>在实现lab2时,最难的不是代码怎么写,而是并发错误如何Debug,如何找出并发错误,很多时候,你根本不知道这个错误到底是怎么出现的,这时候还是老老实实打日志,一条条耐心看。</p><ol><li><p>在旧Leader变成Follower的时候,Term的位置有很大的影响,如果存在两个并发线程,这边刚改完Term,那边Client又发送了一个Command,这时旧Leader又刚好发出了心跳包,这时的旧Leader的Term,已经是改完之后的Term了,这就会导致旧Leader突然变成了新Leader,而当前的Leader也依旧存在,有两个Leader的情况导致日志混乱。Term一定要和Follower一起改,或者在状态改完之后再改Term。</p></li><li><p>很多条件在第一遍写的时候可能会很乱,需要将其捋一遍,代码改的更加清晰些,对所有的情况都详细考虑,2b是lab2最难的一个。</p></li><li><p>apply error测试代码的工作原理: 如果某t个节点向applyCh发送了一条日志,那么在这个Index下,其他节点要么还没发送,要么发送了一条相同的日志.</p><p>出现apply error表示节点s1先提交了日志A,另一个节点s2在相同index提交了不同的日志B。也就是乱序情况,对于加锁解锁需要排查。</p></li></ol><p>测试</p><blockquote><ul><li>TestBasicAgree2B:最基础的追加日志测试。先使用nCommitted()检查有多少的server认为日志已经提交(在执行Start()函数之前,所有的服务器都不应该提交日志),若满足条件则调用cfg.one(),其通过调用rf.Start(cmd)来追加日志。rf.Start(cmd)用于模拟Raft实例从Client接收实例的情况。</li><li>TestRPCBytes2B:基于RPC的字节数检查保证每个cmd都只对每个peer发送一次。</li><li>TestFailAgree2B:断连小部分,不影响整体Raft集群的情况检测追加日志。</li><li>TestFailNoAgree2B:断连过半数节点,保证无日志可以正常追加。然后又重新恢复节点,检测追加日志情况。</li><li>TestConcurrentStarts2B:模拟客户端并发发送多个命令</li><li>TestRejoin2B:Leader 1断连,再让旧leader 1接受日志,再给新Leader 2发送日志,2断连,再重连旧Leader 1,提交日志,再让2重连,再提交日志。</li><li>TestBackup2B:先给Leader 1发送日志,然后断连3个Follower(总共1Ledaer 4Follower),网络分区。提交大量命令给1。然后让leader 1和其Follower下线,之前的3个Follower上线,向它们发送日志。然后在对剩下的仅有3个节点的Raft集群重复上面网络分区的过程。</li><li>TestCount2B:检查无效的RPC个数,不能过多。rpc间隔大概为100ms即可。</li></ul></blockquote><p><img src="raft-mit6-824-2b/image-20220616114020109.png" alt="Pass"></p><p><img src="raft-mit6-824-2b/image-20220616113955373.png" alt="pass500"></p><p>附上我的<a href="https://github.com/Codebells/Raft/tree/go_imp">Github实现代码</a>仅供参考</p>]]></content>
<categories>
<category> database </category>
<category> lab </category>
</categories>
<tags>
<tag> database </tag>
<tag> lab </tag>
<tag> raft </tag>
</tags>
</entry>
<entry>
<title>mit6.824 raft实现 2A部分</title>
<link href="//post/raft-mitlab6-824.html"/>