-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
executable file
·3347 lines (2381 loc) · 255 KB
/
index.html
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
<!DOCTYPE html>
<!--[if IEMobile 7 ]><html class="no-js iem7"><![endif]-->
<!--[if lt IE 9]><html class="no-js lte-ie8"><![endif]-->
<!--[if (gt IE 8)|(gt IEMobile 7)|!(IEMobile)|!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
<head>
<meta charset="utf-8">
<title>Hexiaoqiao</title>
<meta name="author" content="Hexiaoqiao">
<meta name="description" content="一、背景 HDFS NameNode重启效率是另一个被长期诟病的问题,尤其对超大规模集群,动辄数小时的重启时间对整个集群的稳定性和可用性都存在极大的潜在风险,HDFS NameNode重启优化一文对NameNode启动效率提升的优化办法做过简单梳理和探讨,但是从实践情况来看,虽然有提升, …">
<!-- http://t.co/dKP3o1e -->
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="canonical" href="http://hexiaoqiao.github.io/">
<link href="/favicon.png" rel="icon">
<link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css">
<link href="/atom.xml" rel="alternate" title="Hexiaoqiao" type="application/atom+xml">
<script src="/javascripts/modernizr-2.0.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>!window.jQuery && document.write(unescape('%3Cscript src="/javascripts/libs/jquery.min.js"%3E%3C/script%3E'))</script>
<script src="/javascripts/octopress.js" type="text/javascript"></script>
<!--Fonts from Google"s Web font directory at http://google.com/webfonts -->
<link href="//fonts.googleapis.com/css?family=PT+Serif:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=PT+Sans:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-72478952-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<body >
<header role="banner"><hgroup>
<h1><a href="/">Hexiaoqiao</a></h1>
<h2>Focus on BigData,Distributed System,Hadoop Ecosystem</h2>
</hgroup>
</header>
<nav role="navigation"><ul class="subscription" data-subscription="rss">
<li><a href="/atom.xml" rel="subscribe-rss" title="subscribe via RSS">RSS</a></li>
</ul>
<form action="https://www.google.com/search" method="get">
<fieldset role="search">
<input type="hidden" name="sitesearch" value="hexiaoqiao.github.io">
<input class="search" type="text" name="q" results="0" placeholder="Search"/>
</fieldset>
</form>
<ul class="main-navigation">
<li><a href="/">Blog</a></li>
<li><a href="/blog/archives">Archives</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<div id="main">
<div id="content">
<div class="blog-index">
<article>
<header>
<h1 class="entry-title"><a href="/blog/2021/02/27/namenode-fsimage-loading-optimization/">NameNode FSImage加载优化</a></h1>
<p class="meta">
<time class='entry-date' datetime='2021-02-27T10:45:00+08:00'><span class='date'><span class='date-month'>Feb</span> <span class='date-day'>27</span><span class='date-suffix'>th</span>, <span class='date-year'>2021</span></span> <span class='time'>10:45 am</span></time>
</p>
</header>
<div class="entry-content"><h2>一、背景</h2>
<p>HDFS NameNode重启效率是另一个被长期诟病的问题,尤其对超大规模集群,动辄数小时的重启时间对整个集群的稳定性和可用性都存在极大的潜在风险,<a href="https://hexiaoqiao.github.io/blog/2017/02/12/namenode-restart-optimization/">HDFS NameNode重启优化</a>一文对NameNode启动效率提升的优化办法做过简单梳理和探讨,但是从实践情况来看,虽然有提升,但是依然存在优化空间。</p>
<p>从线上一组NameNode重启的抽样数据为例来看,整个重启时间依然非常可观,具体到重启每阶段的时间分布如图1所示(时间占比分布情况受元数据量、NameNode本地存储介质和集群规模等诸多因素影响,不具备一般性,仅供参考)。从时间分布来看,占比较大的是加载FSImage和BlockReport两个阶段,其中FSImage加载的实际时间开销超过小时。所以优化和提升FSImage加载效率对整个进程重启有很大帮助。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/fsimage/restarttime.png" align="center"><br />
<label class=“pic_title” align="center">图1 NameNode重启过程时间分布</label>
</div>
<p>本文将在介绍FSImage原有加载方式基础上,分析效率低的原因,并结合加载方式的演进和实践效果对FSImage的并行加载优化进行简单说明,期望能有借鉴和参考价值。</p>
<h2>二、重启流程</h2>
<p>在HDFS的整个运行期里,整个文件系统的目录树均在NameNode内存中集中管理,但由于内存易失特性,一旦出现进程退出、宕机等异常情况,所有元数据都会丢失,将给整个系统的数据安全会造成不可恢复的灾难。为了更好的容错能力,NameNode(SBN/Secondary)会周期进行Checkpoint,将文件系统的目录树持久化到外部设备,即二进制文件FSImage,这样即使NameNode出现异常也能从持久化设备上恢复元数据,保证了数据的安全可靠。详细分析参考<a href="https://hexiaoqiao.github.io/blog/2017/02/12/namenode-restart-optimization/">HDFS NameNode重启优化</a>一文。</p>
<p>在HA with QJM架构下,NameNode重启始终以SBN(StandbyNameNode)角色开始。启动过程大致分成以下几个阶段:<br/>
0、加载FSImage:从最新持久化的FSImage中恢复文件系统的目录树结构;<br/>
1、回放EditLog:通过回放EditLog对齐最新的目录树结构;<br/>
2、执行Checkpoint:可选操作;<br/>
3、收集所有DataNode的注册和数据块汇报:重建文件的数据块内容具体分布;</p>
<p>FSImage完整记录了文件系统目录树相关的数据。从Hadoop-2.4.0起,FSImage开始使用<a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>编码格式描述(HDFS-5698),详细描述文件见<a href="https://github.com/apache/hadoop/blob/branch-2.4.0/hadoop-hdfs-project/hadoop-hdfs/src/main/proto/fsimage.proto">fsimage.proto</a>,当然在后续的版本中也有调整,但整体上没有本质差异。根据描述文件和实现逻辑,FSImage文件组织格式如图2所示。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/fsimage/fsimageformat.svg" align="center"><br />
<label class=“pic_title” align="center">图2 FSImage文件格式</label>
</div>
<p>从fsimage.proto和FSImage文件存储格式容易看到,除了文件头部校验(MAGIC)和尾部文件索引(FILESUMMARY)等管理信息外,核心数据都是与文件系统目录树强相关。其中INODE和INODE_DIR是整个FSImage文件中最核心且也是规模最大的两个部分。</p>
<p>NameNode重启的第一个步骤就是加载FSImage,传统的加载完全按照串行方式执行:<br/>
(1)FSImage文件MD5值校验;<br/>
(2)读取FSImage文件的Summary数据;<br/>
(3)根据Summary的信息对每个Section依次读取、反序列化并构建内存数据结构;<br/>
其中两个规模最大的两个Section即INODE和INODE_DIR。假设针对包含1亿个INode目录树的加载过程:需要先将1亿个INode从FSImage文件中按序读入并反序列化;完成后再将包含1亿条父子关系的INODE_DIR按序读入并反序列化,根据反序列化结果将所有子节点的引用按照二分查找的方式插入到父节点维护的数组中,到这里基本上目录树就构建起来(当然如果开启了如SNAPSHOT和Cache等特性的话,还需要将这部分数据加载完成),目录树构建完成后的内存组织情况详情参考<a href="https://hexiaoqiao.github.io/blog/2016/07/06/namenode-memory-overview/">NameNode内存全景</a>。</p>
<p>使用传统的FSImage加载模式,测试验证~3亿节点规模的目录树,FSImage文件大小~30GB,加载过程时间开销统计:<br/>
- MD5校验耗时~125sec;<br/>
- FSImage加载时间~811sec;</p>
<h2>三、并行加载优化</h2>
<p>如果分析FSImage的整个加载过程,尤其是占比最大的INODE和INODE_DIR两个Section容易发现两个特点:<br/>
(1)INODE_DIR加载依赖INODE完成,即INODE和INODE_DIR两个Section之间存在严格的先后顺序;<br/>
(2)INODE和INODE_DIR两个Section内部Entry(目录树节点数据和节点之间父子关系信息)相互之间其实完全独立;<br/>
根据这两个特点,我们可以把INODE和INODE_DIR内部结构进一步做逻辑拆分,切割成多个INODE_SUB和多个INODE_DIR_SUB便于后续并行处理,其中:</p>
<p><img src="/images/fsimage/inode.png"></p>
<p>将INODE和INODE_DIR两个Section进行逻辑拆分后其实不影响FSImage物理上的组织结构。为了能把INODE_SUB和INODE_DIR_SUB真正的分配给独立的线程且不重不漏,只需要在FSImage文件的FILESUMMARY索引里对逻辑SUB_SECTION(INODE_SUB+INODE_DIR_SUB)做好记录:偏移量+长度。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/fsimage/fsimageformat2.svg" align="center"><br />
<label class=“pic_title” align="center">图3 FSImage文件格式差异对比</label>
</div>
<p>FSImage文件索引数据就绪后,当再次重启触发加载时,根据SUB_SECTION的个数及配置的加载线程数进行均衡拆分:<br/>
(1)确保多个线程之间分配到的SUB_SECTION尽可能相同;<br/>
(2)每一个SUB_SECTION只被一个线程独立消费;<br/>
通过这种方式拆分,FSImage的加载过程可以演变成如下图4所示流程。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/fsimage/load.svg" align="center"><br />
<label class=“pic_title” align="center">图4 FSImage并行加载流程</label>
</div>
<p>从整个优化思路可以看到,FSImage文件物理结构没有大调整,仅对FILESUMMARY做了简单扩展,核心数据组织上没有做任何改变,所以兼容能力上相对更好。<br/>
(1)使用原有逻辑加载新格式的FSImage文件时仅需要在读取FILESUMMARY时将INODE_SUB和INODE_DIR_SUB两类SECTION的索引过滤掉就可以,事实上在实现时也是这么处理的,即使是较早版本的实现也非常容易修复;<br/>
(2)并行逻辑加载原有格式FSImage文件时因为在FILESUMMARY中没有SUB_SECTION描述,所以及时升级到并行加载逻辑在真正执行时也是单线程完成;</p>
<p>整体上看,这种并行加载方案在效率和兼容性上能做到兼顾。</p>
<h2>四、其他优化</h2>
<p>除了对INODE和INODE_DIR并行加载优化外,其实社区参与者还提出了其他实现逻辑的优化,其中效果较好的主要有:</p>
<p>(1)异步MD5检查;<br/>
在图4的FSImage加载流程里,为了检查FSImage文件合法性,第一步需要对其进行MD5校验。这个步骤需要执行一次完整的FSImage文件读操作,如果文件较大,IO开销比较可观。为了提升加载效率,<a href="https://issues.apache.org/jira/browse/HDFS-13694">HDFS-13694</a>提出将MD5检查逻辑从主流程中摘出来,用独立的线程异步执行检查,减少文件合法性检查引入的时间开销。</p>
<p>(2)加载INODE_DIR/INODE_DIR_SUB去掉二分查找逻辑;<br/>
如前述,INODE_DIR Section实际上是为了构建目录树节点之间的父子关系。为了提升检索效率,父节点使用数组按照字符序维护子节点引用的集合,这样处理后读取时容易通过二分查找的方式在O(logn)时间复杂度内就完成检索;为了满足子节点有序的条件,传统方式当加载INODE_DIR/INODE_DIR_SUB时也是先按照二分查找的方法定位到每一个子节点引用应该插入到数组的具体位置,再执行插入操作。但事实上,对目录树序列化操作时(即执行Checkpoint)子节点本身都已经是有序持久化到FSImage的INODE_DIR Section内,所以加载的时候每一次二分查找的目标位置一定是数组尾部。这种情况下其实二分查找定位目标位置的逻辑完全没有必要。<a href="https://issues.apache.org/jira/browse/HDFS-13693">HDFS-13693</a>提出按照INODE_DIR内Entry的加载顺序逐个插入子节点数组的尾部,去掉二分查找逻辑。</p>
<h2>五、效果验证</h2>
<h3>测试环境</h3>
<p>1、基础环境: <br/>
CPU: Intel® Xeon® CPU 2.60GHz<br/>
OS: CentOS 6.6<br/>
FS: EXT4 on HDD<br/>
JDK: Java HotSpot™ 64-Bit Server VM 1.8.0</p>
<p>2、数据规模:<br/>
INode总数:~3亿<br/>
Block数:~3亿<br/>
FSImage大小:~30GB</p>
<p>3、测试场景:<br/>
原生加载<br/>
并行加载(默认线程数12)<br/>
并行加载+异步MD5校验<br/>
并行加载+异步MD5校验+跳过二分查找</p>
<h3>结果对比</h3>
<p>下图5是针对同一份元数据使用不同策略执行加载过程耗时情况。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/fsimage/result.png" align="center"><br />
<label class=“pic_title” align="center">图5 不同加载策略时间开销对比</label>
</div>
<p>结果说明:<br/>
(1)将INODE和INODE_DIR两个最大规模的元数据并行化加载收益最明显,使用12个线程数加载效率有~40%的提升;实际场景中具体应该使用多少线程执行并行加载需要综合考虑服务器核数和元数据规模等。<br/>
(2)MD5检查是容易被忽略的部分,尤其针对较大规模元数据场景,因为需要一次完整IO过程,所以开销也比较可观~10%;这里的主要开销在IO,所以与FSImage文件大小强相关。<br/>
(3)虽然在加载INODE_DIR构建节点之间父子关系时可以跳过二分查找,直接进入数组尾部,但是效果并不明显。原因在于:一般场景子节点的规模都不大,另外二分查找本身的开销非常低;在常规服务器上即使对1亿规模的数据执行1亿次二分查找的耗时不超过10秒;</p>
<h3>使用参数</h3>
<p>FSImage并行加载特性使用的主要参数如下:<br/>
1、<code>dfs.image.parallel.load</code>: 描述是否开启并行加载特性;<br/>
2、<code>dfs.image.parallel.target.sections</code>: 描述开启并行加载特性后新生成的FSImage里包含的SUB_SECTION个数,一般建议设置为<code>dfs.image.parallel.threads</code>的整数倍;<br/>
3、<code>dfs.image.parallel.inode.threshold</code>: 描述开启并行加载特性所需节点数的最小阈值,对小规模元数据并行加载并不会有很好的效果,所以默认在1000000节点规模下,并行加载特性不会开启; <br/>
4、<code>dfs.image.parallel.threads</code>: 描述并行加载使用的线程数,需要综合考虑元数据规模和NameNode进程所在服务器的承载能力适当调整。</p>
<figure class='code'><figcaption><span>hdfs-site.xml </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
<span class='line-number'>34</span>
<span class='line-number'>35</span>
<span class='line-number'>36</span>
<span class='line-number'>37</span>
<span class='line-number'>38</span>
<span class='line-number'>39</span>
<span class='line-number'>40</span>
<span class='line-number'>41</span>
<span class='line-number'>42</span>
<span class='line-number'>43</span>
<span class='line-number'>44</span>
<span class='line-number'>45</span>
<span class='line-number'>46</span>
<span class='line-number'>47</span>
<span class='line-number'>48</span>
<span class='line-number'>49</span>
<span class='line-number'>50</span>
<span class='line-number'>51</span>
<span class='line-number'>52</span>
<span class='line-number'>53</span>
<span class='line-number'>54</span>
<span class='line-number'>55</span>
</pre></td><td class='code'><pre><code class='xml'><span class='line'> <span class="nt"><property></span>
</span><span class='line'> <span class="nt"><name></span>dfs.image.parallel.load<span class="nt"></name></span>
</span><span class='line'> <span class="nt"><value></span>true<span class="nt"></value></span>
</span><span class='line'> <span class="nt"><description></span>
</span><span class='line'> If true, write sub-section entries to the fsimage index so it can
</span><span class='line'> be loaded in parallel. Also controls whether parallel loading
</span><span class='line'> will be used for an image previously created with sub-sections.
</span><span class='line'> If the image contains sub-sections and this is set to false,
</span><span class='line'> parallel loading will not be used.
</span><span class='line'> Parallel loading is not compatible with image compression,
</span><span class='line'> so if dfs.image.compress is set to true this setting will be
</span><span class='line'> ignored and no parallel loading will occur.
</span><span class='line'> Enabling this feature may impact rolling upgrades and downgrades if
</span><span class='line'> the previous version does not support this feature. If the feature was
</span><span class='line'> enabled and a downgrade is required, first set this parameter to
</span><span class='line'> false and then save the namespace to create a fsimage with no
</span><span class='line'> sub-sections and then perform the downgrade.
</span><span class='line'> <span class="nt"></description></span>
</span><span class='line'> <span class="nt"></property></span>
</span><span class='line'>
</span><span class='line'> <span class="nt"><property></span>
</span><span class='line'> <span class="nt"><name></span>dfs.image.parallel.target.sections<span class="nt"></name></span>
</span><span class='line'> <span class="nt"><value></span>12<span class="nt"></value></span>
</span><span class='line'> <span class="nt"><description></span>
</span><span class='line'> Controls the number of sub-sections that will be written to
</span><span class='line'> fsimage for each section. This should be larger than
</span><span class='line'> dfs.image.parallel.threads, otherwise all threads will not be
</span><span class='line'> used when loading. Ideally, have at least twice the number
</span><span class='line'> of target sections as threads, so each thread must load more
</span><span class='line'> than one section to avoid one long running section affecting
</span><span class='line'> the load time.
</span><span class='line'> <span class="nt"></description></span>
</span><span class='line'> <span class="nt"></property></span>
</span><span class='line'>
</span><span class='line'> <span class="nt"><property></span>
</span><span class='line'> <span class="nt"><name></span>dfs.image.parallel.inode.threshold<span class="nt"></name></span>
</span><span class='line'> <span class="nt"><value></span>1000000<span class="nt"></value></span>
</span><span class='line'> <span class="nt"><description></span>
</span><span class='line'> If the image contains less inodes than this setting, then
</span><span class='line'> do not write sub-sections and hence disable parallel loading.
</span><span class='line'> This is because small images load very quickly in serial and
</span><span class='line'> parallel loading is not needed.
</span><span class='line'> <span class="nt"></description></span>
</span><span class='line'> <span class="nt"></property></span>
</span><span class='line'>
</span><span class='line'> <span class="nt"><property></span>
</span><span class='line'> <span class="nt"><name></span>dfs.image.parallel.threads<span class="nt"></name></span>
</span><span class='line'> <span class="nt"><value></span>12<span class="nt"></value></span>
</span><span class='line'> <span class="nt"><description></span>
</span><span class='line'> The number of threads to use when dfs.image.parallel.load is
</span><span class='line'> enabled. This setting should be less than
</span><span class='line'> dfs.image.parallel.target.sections. The optimal number of
</span><span class='line'> threads will depend on the hardware and environment.
</span><span class='line'> <span class="nt"></description></span>
</span><span class='line'> <span class="nt"></property></span>
</span></code></pre></td></tr></table></div></figure>
<p>社区在优化和解决FSImage加载问题的讨论持续了较长时间,其中比较典型的解决方案还有如<a href="https://issues.apache.org/jira/browse/HDFS-7784">HDFS-7784</a>和<a href="https://issues.apache.org/jira/browse/HDFS-13700">HDFS-13700</a>等,这些解决方案虽然没有最终合入主干代码,但是都提供了非常不错的想法。综合考虑性能、稳定性和兼容能力,HDFS-14617优势明显,另外从实践效果上看HDFS-14617也有较好的表现。如果集群规模和元数据规模较大,且重启加载FSImage阶段耗时严重,并行加载特性值得一试。</p>
<h2>六、参考</h2>
<p>[1] <a href="https://issues.apache.org/jira/browse/HDFS-14617">https://issues.apache.org/jira/browse/HDFS-14617</a><br>
[2] <a href="https://issues.apache.org/jira/browse/HDFS-13694">https://issues.apache.org/jira/browse/HDFS-13694</a><br>
[3] <a href="https://issues.apache.org/jira/browse/HDFS-13693">https://issues.apache.org/jira/browse/HDFS-13693</a></p>
</div>
</article>
<article>
<header>
<h1 class="entry-title"><a href="/blog/2019/04/26/discussion-on-the-optimization-of-hdfs-global-lock-mechanism/">HDFS锁机制优化方向讨论</a></h1>
<p class="meta">
<time class='entry-date' datetime='2019-04-26T10:45:00+08:00'><span class='date'><span class='date-month'>Apr</span> <span class='date-day'>26</span><span class='date-suffix'>th</span>, <span class='date-year'>2019</span></span> <span class='time'>10:45 am</span></time>
</p>
</header>
<div class="entry-content"><h2>一、背景</h2>
<p>众所周知,NameNode全局锁(FSNamesystemLock)问题一直是制约HDFS性能尤其是NameNode处理能力的主要原因。为此,社区和业界经过多次尝试,试图解决NameNode全局锁问题,但是从结果来看,都不理想。</p>
<p>本文将首先梳理NameNode当前的锁机制以及解决全局锁问题所面临的困难,结合经典分布式文件系统在这个问题上的一般解法,尝试给出可能的解决思路。</p>
<h2>二、全局锁机制</h2>
<p>NameNode是整个HDFS的核心组件<sup>[1]</sup>,集中管理HDFS集群的所有元数据,主要包括文件系统的目录树、数据块集合和分布以及整个集群的拓扑结构。</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/hausingqjm.png" align="center"><br />
<label class="pic_title" align="center">图1 HDFS HA using QJM架构图</label>
</div>
<p></p>
<p>同GFS一样HDFS采用了”一次写多次读“的读写模型来满足离线数据处理场景的存储需求,在此基础上,进一步放松一致性模型简化文件系统。在具体实现上,相比GFS1.0,HDFS做了更大胆取舍,锁机制上使用全局锁来统一来控制并发读写。这样处理的优势非常明显,全局锁进一步简化锁模型,不需要额外考虑锁依赖关系,同时降低复杂度,减少工程量。但是问题比优势更加突出,核心问题就是全局唯一锁制约性能提升。</p>
<p>为了更好地理解使用全局锁存在的问题,首先梳理全局锁管理的主要数据结构,大致分成三类:<br/>
(1)目录树:文件系统的全局目录视图。获取目录树上任一节点的信息必须先拿到全局读锁;目录树上任一节点新增、删除、修改都必须先拿到全局写锁。<br/>
(2)数据块集合:文件系统的全量数据信息。获取其中任一数据块信息必须先拿到全局读锁;新增、删除,修改都必须先拿到全局写锁。<br/>
(3)集群信息:HDFS集群节点信息的集合。获取节点信息等必须先拿到全局读锁;注册,下线或者变更节点信息请求处理时必须先拿到全局写锁。当然为了减少对全局影响,后续版本里少数如生命线等RPC请求不再获取全局锁,部分不适合使用全局锁的处理逻辑,将并发控制下放到具体的节点信息,尝试提升处理能力。</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/globallock.png" align="center"><br />
<label class="pic_title" align="center">图2 NameNode全局锁作用范围</label>
</div>
<p></p>
<p>具体实现上,NameNode使用了JDK提供的可重入读写锁(ReentrantReadWriteLock),我们知道ReentrantReadWriteLock对并行请求有严格限制,简单来说:读锁并行写锁排它。</p>
<p>针对不同RPC请求的处理逻辑,按照需要获取锁粒度,我们可以把所有请求抽象为读(Read Handler,获取全局读锁)和写(Write Handler,获取全局写锁)两类。<br/>
Read Handler:客户端请求(getListing/getBlockLocations/getFileInfo)、服务管理接口(monitorHealth/getServiceStatus)和主从节点之间请求(getTransactionID)等;<br/>
Write Handler:客户端请求(create/mkdir/rename/append/truncate/complete/recoverLease)、服务管理接口(transitionToActive/transitionToStandby/setSafeMode)和主从节点之间请求(rollEditLog)等;<br/>
这里只列了一些常用请求类型,其他如Cache/Snapshot/ACL/XAttr/Quota/Lease及NameNode内部线程调用等需要获取锁的逻辑没有再详细列出和归类。</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/concurrent.svg" align="center"><br />
<label class="pic_title" align="center">图3 NameNode全局锁对并行请求处理</label>
</div>
<p></p>
<p>NameNode的锁控制如图3所示。核心处理逻辑路径上有两把锁:FSNamesystemLock和FSEditLogLock。其中FSNamesystemLock即为通常所说的全局锁,采用ReentrantReadWriteLock机制实现;另外为了实现高可靠/高可用的目的,NameNode需要将对部分元数据的修改实时同步到EditLog,为了提升性能,EditLog读写不在FSNamesystemLock锁内执行,独立维护锁控制并行读写,暂称为FSEditLogLock,采用Synchronized排它机制实现。<br/>
(1)获取全局锁(FSNamesystemLock)入口:外部RPC请求从IPC层进入NameNode和内部线程请求;<br/>
(2)获取局部锁(FSEditLogLock)入口:主要来源外部RPC请求对元数据的写操作;</p>
<p>以RPC请求#mkdir为例:<br/>
(1)RPC请求从IPC层进入NameNode;<br/>
(2)获取全局写锁(FSNamesystemLock#writeLock#lock),如果持有读锁或者写锁的请求正在被处理,排队等待;<br/>
(3)更新内存目录树结构;<br/>
(4)释放全局写锁(FSNamesystemLock#writeLock#unlock);<br/>
(5)获取EditLog排它锁;<br/>
(6)写EditLog;<br/>
(7)释放EditLog排它锁;<br/>
(8)通过IPC层将结果返回客户端;</p>
<p>可以看到,单个RPC请求处理流程经过了两次获取锁阶段。虽然二者相互独立,但其中任意一处如果不能及时获取到锁,RPC将处于排队等待状态,直到成功获得锁。等锁时间直接影响请求响应性能,极端场景下如果长时间不能获得锁,将造成IPC队列堆积,TCP连接队列被打满,客户端出现请求超时或者失败重试,新建连接超时失败等各种异常问题。<br/>
另外从全局来看,写锁因为排它对性能影响更加明显。如图3所示,如果当前有写请求正在被处理,其他所有请求都必须排队等待,直到写请求被处理完成释放锁后再竞争全局锁。</p>
<p>通常情况下,FSNamesystemLock锁范围要远大于FSEditLogLock锁范围。考虑负载较高的大规模集群,按照9:1读写比预估,只有10%请求需要同时获取FSNamesystemLock和FSEditLogLock,但是100%请求需要获取全局锁FSNamesystemLock。再加上新型硬件(SSD/3DPoint/PM)对IO性能的支持,EditLog写入性能远高于实际需求。所以从整体上看,当集群规模增加和负载增高后,全局锁FSNamesystemLock将逐渐成为NameNode性能瓶颈。如果能彻底解决NameNode全局锁问题,HDFS性能将得到极大提升。</p>
<h2>三、拆锁复杂度</h2>
<p>如前述,NameNode全局锁的拆分能带来非常可观的收益,Hadoop社区和业界也尝试过多次,但是从结果来看,效果都不理想。就我个人理解,其中问题复杂度客观存在,当然也有一些主观因素。总结下来有几个方面:</p>
<p><strong>1、问题复杂度</strong><br/>
Hadoop发展到今天已经超过十年,其中HDFS经过多次迭代演进,架构已经非常复杂。图4所示为HDFS项目包含和依赖的不完全组件列表,即使从事HDFS开发和运维的专业人员,想要完整了解和掌握HDFS的所有组件绝非易事。</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/modules.svg" align="center"><br />
<label class="pic_title" align="center">图4 HDFS不完全组件图</label>
</div>
<p></p>
<p>仅针对NameNode组件,架构上模块划分不够清晰,内部核心数据结构和工作线程之间耦合非常严重。比如:<br/>
(1)INodeFile通过对象引用关联Block,这种引用关系存在天然的耦合,很难通过不同锁进行并发访问控制;<br/>
(2)数据块写入完成后除了直接更新Block状态外,还需要再次回去更新文件属性,比如存储空间占用;<br/>
(3)在可靠性上使用到的FSImage和FSEditLog两份持久化数据内Namespace和BlocksMap数据共存;<br/>
实现细节上,还存在大量相互依赖,不一而足。</p>
<p>除了问题本身的复杂度,工程复杂度也比较高,据不完全统计,trunk分支上仅HDFS项目代码量超过1000K LOC,其中非测试代码量超过760K LOC,包括了超过2000类文件,要想优雅实现锁粒度拆分工程量很大。</p>
<p><strong>2、实际需求</strong><br/>
以社区版本branch-2.7为例,经过性能优化,NameNode处理能力可以达到5000TPS(写请求)或200000QPS(读请求),这种处理能力能够满足大多数公司的实际需求。如果负载超过这个量级一般也能通过Federation架构做横向扩展解决(虽然Federation架构在使用上会遇到很多意想不到的问题)。真正有实际需求,并需要尝试降低NameNode全局锁粒度解决性能问题的场景并不多。<br/>
<em>NOTE:性能数据是具体场景读写比例压测结果,不具备通用性,请谨慎参考。</em></p>
<p><strong>3、社区动力不足</strong><br/>
社区在全局锁和扩展性问题上做过多次尝试。比较有代表性的几类工作如下:</p>
<blockquote><p><a href="https://issues.apache.org/jira/browse/HDFS-8966">HDFS-8966</a>:Separate the lock used in namespace and block management layer<br/>
<a href="https://issues.apache.org/jira/browse/HDFS-5453">HDFS-5453</a>:Support fine grain locking in FSNamesystem<br/>
<a href="https://issues.apache.org/jira/browse/HDFS-8286">HDFS-8286</a>:Scaling out the namespace using KV store<br/>
<a href="https://issues.apache.org/jira/browse/HDFS-7836">HDFS-7836</a>:BlockManager Scalability Improvements</p></blockquote>
<p>几类方案中都描述了非常好的愿景,但是这些工作多数只推进了其中一部分,有的甚至还处于方案讨论阶段。总之,从几次尝试工作的结果来看,社区在这个方向上的动力并不足,投入有限。</p>
<p><strong>4、历史问题</strong><br/>
HDFS最初设计时为了实现简单方便做了很多取舍,其中全局锁是对后续的发展影响较大的一个。之后架构迭代中,大量工程实现都在全局锁基础上构建,确实对开发工作有很多便捷,但是如果想尝试梳理清楚和优雅拆分难度较大。</p>
<h2>四、拆锁讨论</h2>
<p>事实上,在分布式文件系统中,为实现解决数据一致性,通常都会不可避免遇到锁问题。不同的是,对于适合不同场景的文件系统,做的妥协或采用的方法有很大差异。借鉴成熟文件系统的锁模型,可以为HDFS拆锁工作提供一些参考和借鉴。其中Alluxio是非常好的参考对象,本章在调研Alluxio锁模型基础上,分析降低NameNode全局锁粒度的可能发展方向。</p>
<h3>4.1 Alluxio内存锁模型</h3>
<p>Alluxio<sup>[2]</sup>是一个基于内存的分布式文件系统,得益于云计算场景下的良好表现,被广泛部署和应用。<br/>
同HDFS类似,Alluxio也使用了Master-Slave的架构,其中Master管理Alluxio集群所有的元数据,包括目录树结构、数据块集合和分布及集群节点信息。实现上,FileSystemMaster负责管理整个目录树,BlockMaster管理数据块集合和分布,集群节点信息由BlockMaster中单独的集合数据结构mWorkers独立管理。</p>
<p>整体框架上与HDFS非常相似,但是具体到实现上,差异比较明显。<br/>
(1)FileSystemMaster和BlockMaster完全独立,通过blockid关联;<br/>
(2)mWorkers与FileSystemMaster/BlockMaster之间不存在复杂的耦合关系;<br/>
为了实现数据一致性,FileSystemMaster/BlockMaster/mWorkers之间独立加锁,以达到最好的并行性能。具体来看:</p>
<p>1、FileSystemMaster中目录树上所有节点各自维护读写锁(ReentrantReadWriteLock),控制并发读写:<br/>
(1)一元操作符:按照路径从根目录开始顺序加锁,写锁只加到最后一级目录,其他目录均加读锁;<br/>
(2)二元操作符:对公共目录非最后一级加读锁,最后一级根据操作符加读/写锁,剩余目录按照最后一级公共目录顺序加锁。<br/>
(3)为避免死锁,对于二元或者多元操作符先按路径排序,根据排序结果顺序对路径分别加锁;</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/dirlock.svg" align="center"><br />
<label class="pic_title" align="center">图5 Alluxio FileSystemMaster锁机制图示</label>
</div>
<p></p>
<p>2、BlockMaster完全独立于FileSystemMaster,核心数据结构mBlocks使用ConcurrentHashMap控制并发读写,具体到单个Block操作使用synchronized控制;</p>
<p>3、mWorker本身使用线程安全的集合数据结构管理,涉及到注册心跳等操作时,为每一个worker独立加锁;</p>
<p>从整个锁逻辑上看,有几点非常值得借鉴的地方:<br/>
(1)所有模块之间耦合度极低,核心逻辑不存在排它锁影响性能;<br/>
(2)为了将锁影响控制到最低,使用了大量在具体对象(block/worker)上加锁逻辑,而不是全局;</p>
<p>当然,凡是都有利弊,降低锁冲突提升性能一定是需要付出代价的:<br/>
(1)内存开销,因为在FileSystemMaster中目录树所有节点上独立使用读写锁(ReentrantReadWriteLock),会存在大量的内存对象的开销,制约Alluxio集群规模;在64bit环境上统计数据结构ReentrantReadWriteLock的footprint:</p>
<figure class='code'><figcaption><span>ReentrantReadWriteLock footprint </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
</pre></td><td class='code'><pre><code class='xml'><span class='line'>java.util.concurrent.locks.ReentrantReadWriteLock@29444d75d footprint:
</span><span class='line'> COUNT AVG SUM DESCRIPTION
</span><span class='line'> 1 24 24 java.util.concurrent.locks.ReentrantReadWriteLock
</span><span class='line'> 1 48 48 java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync
</span><span class='line'> 1 16 16 java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock
</span><span class='line'> 1 16 16 java.util.concurrent.locks.ReentrantReadWriteLock$Sync$ThreadLocalHoldCounter
</span><span class='line'> 1 16 16 java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock
</span><span class='line'> 5 120 (total)
</span></code></pre></td></tr></table></div></figure>
<p>对于一个10亿节点的目录树,仅ReentrantReadWriteLock对象的内存开销就将到~120GB,显然是一个巨大的开销。<br/>
(2)Alluxio的Master节点为了实现高可用,本身采用集群方式部署,为了保证一致性,所有元数据必须同步。这里涉及到FileSystemMaster/BlockMaster的Journal独立持久化逻辑,Alluxio实现时,将这部分逻辑都放在了锁内,对写请求处理的性能影响较大。</p>
<h3>4.2 GFS锁模型</h3>
<p>重新回顾GFS1.0是如何管理目录树和目录锁。下面是从论文《The Google File System》<sup>[4]</sup>中摘抄的有关目录树和锁机制的描述段落。</p>
<blockquote><p>Many master operations can take a long time: for example, a snapshot operation has to revoke chunkserver leases on all chunks covered by the snapshot. We do not want to delay other master operations while they are running. Therefore, we allow multiple operations to be active and use locks over regions of the namespace to ensure proper serialization. Unlike many traditional file systems, GFS does not have a per-directory data structure that lists all the files in that directory. Nor does it support aliases for the same file or directory (i.e, hard or symbolic links in Unix terms). GFS logically represents its namespace as a lookup table mapping full pathnames to metadata. With prefix compression, this table can be efficiently represented in memory. Each node in the namespace tree (either an absolute file name or an absolute directory name) has an associated read-write lock. Each master operation acquires a set of locks before it runs. Typically, if it involves /d1/d2/…/dn/leaf, it will acquire read-locks on the directory names /d1, /d1/d2, …, /d1/d2/…/dn, and either a read lock or a write lock on the full pathname /d1/d2/…/dn/leaf. Note that leaf may be a file or directory depending on the operation. We now illustrate how this locking mechanism can prevent a file /home/user/foo from being created while /home/user is being snapshotted to /save/user. The snapshot operation acquires read locks on /home and /save, and write locks on /home/user and /save/user. The file creation acquires read locks on /home and /home/user, and a write lock on /home/user/foo. The two operations will be serialized properly because they try to obtain conflicting locks on /home/user. File creation does not require a write lock on the parent directory because there is no “directory”, or inode-like, data structure to be protected from modification. The read lock on the name is sufficient to protect the parent directory from deletion. One nice property of this locking scheme is that it allows concurrent mutations in the same directory. For example, multiple file creations can be executed concurrently in the same directory: each acquires a read lock on the directory name and a write lock on the file name. The read lock on the directory name suffices to prevent the directory from being deleted, renamed, or snapshotted. The write locks on file names serialize attempts to create a file with the same name twice. Since the namespace can have many nodes, read-write lock objects are allocated lazily and deleted once they are not in use. Also, locks are acquired in a consistent total order to prevent deadlock: they are first ordered by level in the namespace tree and lexicographically within the same level.</p></blockquote>
<p>从这段描述里我们可以看到GFS对锁的管理:<br/>
(1)目录树中节点各自独立管理锁来控制并发;<br/>
(2)对锁模型更加激进,比如创建文件在整条路径上只使用读锁;<br/>
(3)为了避免死锁,对多元操作符按照路径排序后顺序加锁;<br/>
整体来看,Alluxion目录树锁机制与GFS锁机制异曲同工,将并行处理能力最大化。</p>
<h3>4.3 HDFS拆锁讨论</h3>
<p>借鉴和参考前面两类文件系统锁机制实现并结合HDFS现状,我个人认为HDFS降低全局锁粒度的可能发展路线:<br/>
<strong>1、垂直拆分<sup>[3]</sup></strong><br/>
NameNode内存几个核心数据结构里,DataNodeManager管理的内容相对独立,比较容易独立拆分出去,事实上社区现在基本完成了这个工作,下面只考虑两个核心数据结构Namespace和BlocksMap:<br/>
(1)按照HDFS Federation架构的思路,在单NameNode进程内实施Federation;<br/>
(2)将Namespace按照Range进行垂直切分;<br/>
(3)Namespace变化成两级管理结构;</p>
<figure class='code'><figcaption><span>Double-level-struction </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
</pre></td><td class='code'><pre><code class='xml'><span class='line'>RangeMap:Range-GSet
</span><span class='line'>GSet:key-INode/BlockInfo
</span></code></pre></td></tr></table></div></figure>
<p>
(4)Range内独享锁,Range之间可并行访问;<br/>
(5)跨Range多元操作符按照Range排序后顺序加锁避免死锁;<br/>
(6)当单进程整体负载较高时,Range重新分配独立进程,实现动态切分目录树的效果;</p>
<p>目录树的垂直切分思路到最后可以跟HDFS Federation很好的结合起来(虽然HDFS Federation架构存在很多问题)实现类似Ceph中简化版Dynamic Subtree Partitioning目标。</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/layerlock.png" align="center"><br/>
</div>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/partitiontree.png" align="center"><br/>
<label class=“pic_title” align="center">图6 NameNode全局锁垂直切分</label>
</div>
<p></p>
<p><strong>2、水平拆分</strong><br/>
NameNode全局锁水平拆分的思路可以借鉴GFS1.0和Alluxio解决思路,按照两个阶段降低NameNode锁粒度:<br/>
第一阶段:对NameNode核心数据结构进行分层解耦,不同层独立持锁;<br/>
第二阶段:降低Namespace层锁粒度;</p>
<p>第一阶段分层解耦:<br/>
(1)Namespace层维护与目录树有关的所有数据结构(INodeMap,Lease等),核心是INodeMap,目录树文件节点上通过List<Long> BlockIds即数据块序号维护与数据块的关系,取代对象索引;<br/>
(2)BlocksManager层维护与数据块相关的所有数据结构(BlocksMap,ReplicationMonitor,NetworkTopology等),核心是BlocksMap:GSet<BlockId, BlockInfo>;将副本数和存储策略等与数据块有关的属性统一下沉到BlockInfo内,降低Namespace与BlocksManager的耦合;(一部分工作社区已经完成)<br/>
(3)DataNodeManager层仅维护集群节点数据结构,不维护拓扑结构(非重点,当前的实现已经不在锁内);<br/>
(4)每一层维护独立锁,开放接口以线程安全方式对外暴露。</p>
<p>拆分后同一进程内会出现多把独立锁,不可避免会存在锁内相互调用的问题,为了避免出现死锁,可以做简单约束:<br/>
(1)单次请求处理涉及数据结构<Namespace>, <BlocksMap>或者<Namespace,BlocksMap>;<br/>
(2)尽可能减少或避免锁内跨层调用(如Alluxio);<br/>
(3)特殊场景需要锁内跨层调用时,仅允许Namespace到BlocksMap单向调用;</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/exlayerlock.png" align="center"><br />
<label class="pic_title" align="center">图7 NameNode全局锁水平切分</label>
</div>
<p></p>
<p>第二阶段降低锁粒度:<br/>
(1)目录树全局锁下沉到目录树节点,不过如前述因为ReentrantReadWriteLock的footprint较大,直接使用容易造成内存瓶颈。可以选择以下优化和改进:<br/>
* 按照满足读写锁能力的最小资源重新实现Lock,降低整颗目录树节点使用锁后的内存占用;<br/>
* 维护独立的目录锁动态子树;因为NameNode进程内提供的请求处理线程数有限,目录锁子树规模非常小,几乎没有管理和遍历的成本;<br/>
(2)写锁仅持有写操作的最后一级目录,其他父目录均加读锁;<br/>
(3)多元操作符按照请求目录排序后顺序加锁避免死锁;</p>
<div class="pic" align="center" padding="0">
<img src="/images/hdfslock/exdirloc.svg" align="center"><br />
<label class="pic_title" align="center">图8 目录树全局锁下沉到节点 </label>
</div>
<p></p>
<p>全局操作类型,比如safemode/haadmin/metasave因为都是superuser类请求,频率非常低,不需要再维护独立锁。为了简化,对大部分superuser管理类型的请求可以同时获取两把写锁,对整体性能不会有影响。</p>
<h2>五、总结</h2>
<p>NameNode全局锁一直是影响HDFS性能的关键问题,尽管社区在这方面做过多次尝试,但是结果都不是很理想。其中的问题难度客观存在。<br/>
(1)HDFS架构快速迭代和演进,丰富的功能和更加复杂组件让HDFS内部模块之间存在千丝万缕的耦合关系,完全梳理清楚成本较高;<br/>
(2)HDFS项目代码量除单元测试外接近780K LOC,工程量很大;<br/>
(3)设计之初为实现简单做了很多取舍,比如全局锁及在此基础上的大量工程实现(“战术的勤奋掩盖战略的懒惰”的实例);<br/>
虽有难度但也存在办法,本文在参考其他分布式文件系统锁模型的基础上,结合当前HDFS实际情况和业界正在尝试的方向,期望提供两种降低全局锁粒度的思路和可能演进方向,两种演进方向相互没有依赖,可以并行演进。</p>
<p>当然,提升性能或者扩展能力,拆分NameNode全局锁并不是唯一解。比如由LinkedIn和Hortorworks分别在推进的Observer NameNode和OZone都是非常好的思路。<br/>
Observer NameNode通过开放Standby读能力提升NameNode整体QPS。<br/>
Hadoop OZone通过引入对象存储思路,将文件系统的元数据进行分解下沉,期望能够实现良好的性能和扩展能力。</p>
<h2>六、参考</h2>
<p>[1] <a href="https://hadoop.apache.org/docs/r3.2.0/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html">https://hadoop.apache.org/docs/r3.2.0/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html</a><br/>
[2] <a href="https://www.alluxio.org/">https://www.alluxio.org/</a><br/>
[3] <a href="https://engineering.linkedin.com/blog/2019/02/the-present-and-future-of-apache-hadoop--a-community-meetup-at-l">https://engineering.linkedin.com/blog/2019/02/the-present-and-future-of-apache-hadoop--a-community-meetup-at-l</a><br/>
[4] <a href="https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf">https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf</a></p>
</div>
</article>
<article>
<header>
<h1 class="entry-title"><a href="/blog/2018/10/05/recruit/">大数据职位招聘</a></h1>
<p class="meta">
<time class='entry-date' datetime='2018-10-05T10:45:00+08:00'><span class='date'><span class='date-month'>Oct</span> <span class='date-day'>5</span><span class='date-suffix'>th</span>, <span class='date-year'>2018</span></span> <span class='time'>10:45 am</span></time>
</p>
</header>
<div class="entry-content"><h2>数据平台 - Hadoop实习工程师</h2>
<h3>职位描述</h3>
<p>1、搭建维护Hadoop、HBase、Spark等集群并进行必要的troubleshooting,保障系统正常运行;<br/>
2、提高集群的使用效率及稳定性,并对整个系统做合理分离,满足各种应用场景的使用;<br/>
3、构建集群监控体系、可视化体系、调度系统,保障集群安全和数据安全;<br/>
4、负责搭建BI系统,数据仓库模型的ETL实施;<br/>
5、数据使用开发平台;<br/>
6、各个业务线的数据接入及数据一致性建设;<br/>
7、不断解决规模增长带来的技术和业务问题,确保数据平台稳定性可靠性和线性扩展能力,驱动业务发展。</p>
<h3>职位要求</h3>
<p>1、对技术有着永无止境的追求,自认为是技术Geek,具备很强的问题解决能力;<br/>
2、熟悉Hadoop生态系统开源项目,至少精读过其中某一个的源码,对大规模数据处理具有独到的理解,有Patch源代码经验者优先;<br/>
3、有Hadoop经验优先。</p>
<h2>金融安全部 - 数据PM岗</h2>
<h3>职位描述</h3>
<p>1、负责数据治理体系的规划,设计,和实现落地;<br/>
2、负责数据合法合规、数据安全、内外部业务方面的需求管理和分析,识别需求本质,采用产品化思维解决问题;<br/>
3、沟通和协调各部门资源,组织跨团队协作并推进产品实施,驱动项目进度,按期保质完成项目各阶段目标;<br/>
4、其他数据相关工作等。</p>
<h3>职位要求</h3>
<p>1、有数据分析、产品经理或金融行业工作经验;<br/>
2、数学、统计、金融、计算机或信息技术等相关专业本科及以上学历;<br/>
3、具有一定的产品文档能力,能够产出标准的产品需求文档,熟练掌握Axure等原型设计工具;<br/>
4、具备优秀的沟通、协调能力,和团队合作精神;思路清晰,善于提炼、分析并解决问题;<br/>
5、具备极强的责任心、抗压能力、沟通理解能力及协同推动能力。</p>
<h3>加分项</h3>
<p>1、对数据驱动业务有一定理解,对数据与安全业务有一定敏感性,有较强的逻辑分析能力、独立思考能力和创新能力;<br/>
2、具有互联网行业的数据产品经验者,或安全业务数据分析经验,或金融行业从业经验者优先;<br/>
3、熟悉Oracle、Mysql等数据库,精通SQL;<br/>
4、熟悉数据挖掘基本原理,数据挖掘方法论和基本算法;精通至少一种数据分析/挖掘工具,如SAS等。</p>
<h2>金融安全部 - 数据流通岗</h2>
<h3>职位描述</h3>
<p>1、负责内外部数据项目管理工作,组织推进各部门各角色快速高效完成数据类项目;<br/>
2、负责数据管理制度的拟定、发布及推动落地;<br/>
3、其他数据相关工作等。</p>
<h3>职位要求</h3>
<p>1、至少1年以上项目管理或数据安全管理工作经验;<br/>
2、处理过跨部门、多角色合作的复杂事务,思路清晰,善于提炼、分析并解决问题;<br/>
3、具备极强的责任心、抗压能力、沟通理解能力及协同推动能力。</p>
<h3>加分项</h3>
<p>1、具备传统金融或互联网金融企业从业背景者优先;<br/>
2、有安全技术背景或数据安全管理经验者优先。</p>
<p>联系方式:xq.he2009#gmail.com(#替换@)</p>
</div>
</article>
<article>
<header>
<h1 class="entry-title"><a href="/blog/2018/07/13/a-brief-introduction-of-hdfs-blocktoken-mechanism/">HDFS BlockToken机制解析</a></h1>
<p class="meta">
<time class='entry-date' datetime='2018-07-13T10:45:00+08:00'><span class='date'><span class='date-month'>Jul</span> <span class='date-day'>13</span><span class='date-suffix'>th</span>, <span class='date-year'>2018</span></span> <span class='time'>10:45 am</span></time>
</p>
</header>
<div class="entry-content"><h2>一、背景</h2>
<p>敏感信息和隐私数据的安全保障是互联网公司非常关心的问题,尤其进入大数据时代,稍有不慎就会出现重大安全事故,所以数据安全问题就变得越来越重要。</p>
<p>Hadoop作为数据平台的基础设施,需要优先关注和解决好安全问题。虽然安全特性对Hadoop非常重要,不过社区直到2011年末随Hadoop-1.0.0才第一次正式发布Hadoop Security,在这之前Hadoop社区版存在较大的安全隐患,需要用户自行解决。</p>
<p>当然数据安全本身是一个复杂的系统工程,想要描述清楚和完美解决几乎不可能。尽管如此,合理有效的安全保障是必要的。本文就Hadoop中数据块安全问题,从设计权衡和实现原理进行简单分析和梳理,简要阐述当前方案在实践中可能遇到的问题,同时提供可借鉴的解决思路。</p>
<h2>二、Hadoop安全概述</h2>
<p>Hadoop安全需要解决两个问题:<br/>
(1)认证:解决用户身份合法性验证问题;<br/>
(2)授权:解决认证用户的操作范围问题;<br/>
其中认证问题通过Kerberos能够很好地解决,并通过<a href="https://issues.apache.org/jira/browse/HADOOP-4487">HADOOP-4487</a>在Hadoop内部设计了一套Token机制完美实现了安全认证问题,同时在性能上得到保证,图1为Hadoop安全认证体系概要图示。关于Hadoop Security特性的细节参考<a href="https://issues.apache.org/jira/browse/HADOOP-4487">HADOOP-4487</a>,这里不再展开。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/blocktoken/security.png" align="center"><br />
<label class=“pic_title” align="center">图1 Hadoop安全认证体系</label>
</div>
<p></p>
<p>社区针对这个问题在2008.10与Hadoop Security特性同步开始设计BlockToken方案HADOOP-4359,经过半年左右时间在2009.05完成并发布,BlockToken特性可以非常好地保护数据块安全。可以说HADOOP-4487和HADOOP-4359构建起整个Hadoop安全体系,本文重点关注HADOOP-4359。</p>
<p>社区针对这个问题在2008.10与Hadoop Security特性同步开始设计BlockToken方案<a href="https://issues.apache.org/jira/browse/HADOOP-4359">HADOOP-4359</a>,经过半年左右时间在2009.05完成并发布,通过BlockToken数据块安全问题也得到了很好的解决。可以说<a href="https://issues.apache.org/jira/browse/HADOOP-4487">HADOOP-4487</a>和<a href="https://issues.apache.org/jira/browse/HADOOP-4359">HADOOP-4359</a>构建起了整个Hadoop安全体系。</p>
<h2>三、安全基础简介</h2>
<p>BlockToken方案使用HMAC(Hash Message Authentication Code)[1]技术实现对合法请求的访问认证检查。</p>
<p>HMAC是一种基于HASH函数和共享密钥的消息安全认证协议,它可以有效地防止数据在传输的过程中被截取和篡改,维护数据的安全性、完整性和可靠性。HMAC可以与任何迭代HASH函数结合使用,MD5和SHA-1就是这种HASH函数。实现原理是用公开函数和共享密钥对原始数据产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用密钥生成一个固定大小的消息摘要小数据块即HMAC,并加入到消息中一起传输。接收方利用与发送方共享的密钥对接收到的消息进行认证和合法性检查。这种算法不可逆,无法通过消息摘要反向推导出消息,因此又称为单向HASH函数。通过这种技术可以有效保证数据的安全性、完整性和可靠性。</p>
<p>HMAC算法流程:
(1)消息传递前,Alice和Bob约定共享密钥和HASH函数;
(2)Alice把要发送的消息使用共享密钥计算出HMAC值,然后将消息和HMAC发送给Bob;
(3)Bob接收到消息和HMAC值后,使用共享密钥独立计算消息本身的HMAC值,与接收到的HMAC值对比;
(4)如果二者的HMAC值相同,说明接收到的消息是完整的,且是Alice发送;</p>
<p>BlockToken方案默认使用了经典的HMAC-SHA1算法,对照前面的流程,Alice代表的是NameNode,Bob代表DataNode,客户端在整个过程中仅作为数据流转的节点。因为HMAC能够保证数据传输过程中不被截取和篡改,只要NameNode给客户端发放了BlockToken,即可认为该客户端申请对单个数据块的访问权限是可信赖的,DataNode只要对BlockToken检查通过就必须接受客户端表述的所有权限。</p>
<h2>四、HDFS BlockToken机制</h2>
<p>Token机制是整个Hadoop生态里安全协议的重要组成部分,在HDFS内部包括两个部分:<br/>
(1)客户端经过初始认证(Kerberos),从NameNode获取DelegationToken,作为后续访问HDFS的凭证;<br/>
(2)客户端读写数据前,请求NameNode获取对应数据块Block信息和BlockToken,根据结果向对应DataNode真正请求读写数据。请求到达DataNode端,根据客户端提供的BlockToken进行安全认证检查,通过后继续后续步骤,否则请求失败;</p>
<p>第二部分就是<a href="https://issues.apache.org/jira/browse/HADOOP-4359">HADOOP-4359</a>和本文主要关注的内容。</p>
<h3>4.1 HDFS读写流程</h3>
<p>开始详细梳理BlockToken原理之前,首先简单梳理下如图2所示的HDFS读写流程:</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/blocktoken/read.png" align="center">
<img src="/images/blocktoken/write.png" align="center"><br />
<label class=“pic_title” align="center">图2 HDFS读写流程示意图</label>
</div>
<p></p>
<p>(1)客户端读写操作(open/create)需首先获取数据块Block分布,根据文件路径请求NameNode获取LocatedBlock;<br/>
(2)如果是读操作,根据返回LocatedBlock集合,从中选择合适的DataNode进行读数据请求,若需要读取的数据分布在多个Block,按顺序逐个切换到对应DataNode读取;<br/>
(3)如果是写操作,首先将返回的LocatedBlock中所有DataNode建立数据管道(Pipeline),然后开始向数据管道里写数据,若写出的数据不能在一个Block内完成,再次向NameNode申请LocatedBlock,直到所有数据成功写出;<br/>
(4)读写操作完成,关闭数据流;</p>
<p>LocatedBlock是衔接整个读写流程的关键数据结构:</p>
<figure class='code'><figcaption><span>LocatedBlock.java </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
</pre></td><td class='code'><pre><code class='java'><span class='line'><span class="kd">public</span> <span class="kd">class</span> <span class="nc">LocatedBlock</span> <span class="o">{</span>
</span><span class='line'> <span class="kd">private</span> <span class="kd">final</span> <span class="n">ExtendedBlock</span> <span class="n">b</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">long</span> <span class="n">offset</span><span class="o">;</span> <span class="c1">// offset of the first byte of the block in the file</span>
</span><span class='line'> <span class="kd">private</span> <span class="kd">final</span> <span class="n">DatanodeInfoWithStorage</span><span class="o">[]</span> <span class="n">locs</span><span class="o">;</span>
</span><span class='line'> <span class="cm">/** Cached storage ID for each replica */</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">String</span><span class="o">[]</span> <span class="n">storageIDs</span><span class="o">;</span>
</span><span class='line'> <span class="cm">/** Cached storage type for each replica, if reported. */</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">StorageType</span><span class="o">[]</span> <span class="n">storageTypes</span><span class="o">;</span>
</span><span class='line'> <span class="c1">// corrupt flag is true if all of the replicas of a block are corrupt.</span>
</span><span class='line'> <span class="c1">// else false. If block has few corrupt replicas, they are filtered and </span>
</span><span class='line'> <span class="c1">// their locations are not part of this object</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">corrupt</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">Token</span><span class="o"><</span><span class="n">BlockTokenIdentifier</span><span class="o">></span> <span class="n">blockToken</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Token</span><span class="o"><</span><span class="n">BlockTokenIdentifier</span><span class="o">>();</span>
</span><span class='line'> <span class="o">......</span>
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>
<h3>4.2 BlockToken数据结构</h3>
<p>前一节提到的LocatedBlock除了标识数据块Block信息外,还包含了认证流程中的核心数据结构blockToken:</p>
<figure class='code'><figcaption><span>Token.java </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
</pre></td><td class='code'><pre><code class='java'><span class='line'><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Token</span><span class="o"><</span><span class="n">T</span> <span class="kd">extends</span> <span class="n">TokenIdentifier</span><span class="o">></span> <span class="kd">implements</span> <span class="n">Writable</span> <span class="o">{</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">identifier</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">password</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">Text</span> <span class="n">kind</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">Text</span> <span class="n">service</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">TokenRenewer</span> <span class="n">renewer</span><span class="o">;</span>
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>blockToken的主要属性如下:<br/>
(1)kind标识的是Token的类型,这里为常量“HDFS_BLOCK_TOKEN”;<br/>
(2)service用来描述请求的服务,一般由服务端的”host:port”组成,对blockToken一般置空;<br/>
(3)TokenRenewer在客户端生命周期内周期Renew,避免因为Token过期造成请求失败,对BlockToken未见Renew的显性实现,所以BlockToken只在有效期内生效;<br/>
(4)identifier是BlockTokenIdentifier的序列化结果:</p>
<figure class='code'><figcaption><span>BlockTokenIdentifier.java </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
</pre></td><td class='code'><pre><code class='java'><span class='line'><span class="kd">public</span> <span class="kd">class</span> <span class="nc">BlockTokenIdentifier</span> <span class="kd">extends</span> <span class="n">TokenIdentifier</span> <span class="o">{</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">long</span> <span class="n">expiryDate</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">int</span> <span class="n">keyId</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">String</span> <span class="n">userId</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="n">String</span> <span class="n">blockPoolId</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="kt">long</span> <span class="n">blockId</span><span class="o">;</span>
</span><span class='line'> <span class="kd">private</span> <span class="kd">final</span> <span class="n">EnumSet</span><span class="o"><</span><span class="n">AccessMode</span><span class="o">></span> <span class="n">modes</span><span class="o">;</span>
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>包含了当前请求来源userId,数据块标识blockId,数据块所在的BlockPool(用于HDFS Federation架构),本次请求的权限标识modes(READ, WRITE, COPY, REPLACE),Token的过期时间及keyId;<br/>
(5)password即是使用共享密钥SecretKey应用HMAC算法对identifier计算得到的密码。<br/>
需要说明的是,keyId和SecretKey存在对应关系,通过keyId可以索引到SecretKey,后续详细介绍。</p>
<h3>4.3 BlockToken流程</h3>
<p>BlockToken体现在HDFS读写流程的以下几个步骤里:</p>
<p>1、客户端使用文件路径向NameNode发送读写请求,其中请求接口如下:</p>
<figure class='code'><figcaption><span>interface </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
</pre></td><td class='code'><pre><code class='java'><span class='line'><span class="kd">public</span> <span class="n">LocatedBlocks</span> <span class="nf">getBlockLocations</span><span class="o">(</span><span class="n">String</span> <span class="n">clientName</span><span class="o">,</span> <span class="n">String</span> <span class="n">src</span><span class="o">,</span> <span class="kt">long</span> <span class="n">offset</span><span class="o">,</span> <span class="kt">long</span> <span class="n">length</span><span class="o">);</span>
</span><span class='line'><span class="kd">public</span> <span class="n">LocatedBlock</span> <span class="nf">addBlock</span><span class="o">(</span><span class="n">String</span> <span class="n">src</span><span class="o">,</span> <span class="n">String</span> <span class="n">clientName</span><span class="o">,</span> <span class="n">ExtendedBlock</span> <span class="n">previous</span><span class="o">,</span> <span class="n">DatanodeInfo</span><span class="o">[]</span> <span class="n">excludedNodes</span><span class="o">,</span> <span class="kt">long</span> <span class="n">fileId</span><span class="o">,</span> <span class="n">String</span><span class="o">[]</span> <span class="n">favoredNodes</span><span class="o">);</span>
</span></code></pre></td></tr></table></div></figure>
<p>2、NameNode经过权限检查后,搜索到文件对应的数据块信息,结合激活的keyId组织出完整的BlockTokenIdentifier,使用keyId对应密钥SecretKey加密BlockTokenIdentifier得到密码,BlockToken数据就绪,加上已经获取到的数据块信息即是LocatedBlock返回给客户端;</p>
<p>3、客户端从NameNode获取到LocatedBlock后,带着BlockToken请求对应DataNode执行数据读写操作;</p>
<p>4、DataNode端接收到读写请求,首先进行BlockToken检查,目的是检查客户端的真实性和权限。主要有两个步骤:<br/>
(1)将BlockToken里的identifier反序列化,检查客户端请求的数据块、访问权限及用户名是否与BlockToken的表达一致,如果检查通过进入下一步,否则直接失败;<br/>
(2)从identifier反序列化结果里取出keyId,在本地索引对应的共享密钥SecretKey,使用与NameNode端相同的HMAC算法计算password,之后与BlockToken中的password进行比较,如果相等开始真正的数据读写流程,否则请求失败。</p>
<p>上述流程中,NameNode和DataNode计算密码时使用的密钥SecretKey均是以BlockTokenIdentifier.keyid作为索引在本地内存中获取。要想对相同的BlockTokenIdentifier使用同样的加密算法计算得到相同的结果,密钥SecretKey必须完全一致。所以核心问题是,NameNode和DataNode如何保证密钥SecretKey同步,使符合预期的请求通过验证。</p>
<p>最简单的办法就是NameNode和DataNode初始化固定的密钥,到期后NameNode重新生成并同步给DataNode问题解决。</p>
<p>但是事实并没有这么简单,我们知道DataNode与NameNode之间信息交互最频繁的渠道是Heartbeat(默认3s一次),如果NameNode更新了SecretKey,但是DataNode心跳3s后才上报,在这3s时间内,两端存在密钥不一致的问题,也就是在这个时段内即使合法请求也会检查失败,所以“最简单的办法”显然还不能完全解决问题。</p>
<p>虽然“最简单的办法”存在问题,但是提供了一种简单高效解决问题的思路,既然只维护一份共享密钥SecretKey会出现“黑障区”问题,那么同一时刻始终保持两份在线,这样就可以完全避免3s的黑障时间段。</p>
<p>事实上,HDFS更进一步同时维护三份共享密钥,NameNode一旦发现有SecretKey过期,马上生成新SecretKey补充进来并向前滚动当前激活SecretKey,DataNode心跳过来后及时下发更新后的SecretKey集合,如图3所示。维护三份密钥的代价是NameNode需要同时检查三份数据有效期,但是通常情况过期时间较大(默认是10h)且数据量极小,所以完全不会给NameNode或者DataNode带来负担。</p>
<div class=“pic” align="center" padding=“0”>
<img src="/images/blocktoken/blocktoken.png" align="center"><br />
<label class=“pic_title” align="center">图3 HDFS BlockToken流程图示</label>
</div>
<p></p>
<h3>4.4 BlockToken密钥HA</h3>
<p>前面提到了NameNode和DataNode同步密钥的流程,在HDFS HA架构里通常还存在Active NameNode和Standby NameNode同步数据的问题。</p>
<p>事实上,Active与Standby之间不对SecretKey通过EditLog或其他方式同步。这样带来的新问题是:如何保证操作主从切换后,当前正常读写请求的Token验证通过。如前面提到,NameNode定期更新SecretKey后及时将更新后的SecretKey集合同步给DataNode,DataNode更新以保证正常读写请求通过验证,这种方式对Active和Standby同样适用。所以单从DataNode来看,同一个BlockPool实际上同一时间本地缓存至少6份共享密钥,其中3份来自Active NameNode,另外3份来自Standby NameNode。这样的话,不管客户端请求携带的keyId来自Active NameNode或者Standby NameNode,只要是正常请求均能验证通过,与是否操作主从切换或者从Standby NameNode请求无关。</p>
<p>接下来的问题,DataNode维护了多份<keyId,SecretKey>数据,如何避免来自Active和Standby之间的keyId冲突,以及HDFS Federation架构下,来自多个Namespace的keyId冲突。先来看HDFS Federation架构,与BlockPool类似,共享密钥相关信息也按照这个维度组织就不会相互干扰。来自同Namespace下Active和Standby的keyId确实存在冲突的可能,为了避免出现这种情况,实现时结合Active和Standby的nnId分配独立的keyId序号段即可解决。</p>
<p>除了以上问题,服务重启时还存在其他问题:<br/>
(1)NameNode重启:当NameNode重启会重置,由于NameNode重启后所有DataNode需要重新注册,注册完成后返回的CMD指令中包含了NameNode的集合,保证了DataNode与NameNode之间完成同步;<br/>
(2)DataNode重启:DataNode重启比较简单,向Active NameNode和Standby NameNode分别注册,成功后会收到Active和Standby的所有集合,更新内存状态即可。</p>
<p>为什么NameNode之间不像其他的WRITE操作,通过EditLog在Active与Standby之间保持同步?原因有两个:<br/>
1、SecretKey更新频率很低(10h);<br/>
2、数据量非常小(可忽略);<br/>
根据这两条不管是NameNode端还是DataNode端都完全可以承载,另外如果通过EditLog同步会增加复杂度,同时如果持久化SecretKey安全性上大打折扣,与Token设计的初衷相悖。</p>
<p>至此,BlockToken的整个流程简单梳理完成,可以看出BlockToken与Kerberos体系的架构和核心流程有很多相似的地方。</p>
<h2>五、BlockToken的问题及解决思路</h2>
<p>前面BlockToken流程分析可以看出,设计思路和实现方案都比较优雅,但是实践过程中还是可能会遇到一些问题:<br/>
(1)NameNode重启完成后DataNode没有成功更新SecretKey造成客户端读写失败;<br/>
(2)NameNode滚动SecretKey后DataNode没有及时同步造成后续读写失败;</p>
<figure class='code'><figcaption><span>datanode.log </span></figcaption>
<div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>