-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
552 lines (332 loc) · 457 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>LoveZhy</title>
<link href="/atom.xml" rel="self"/>
<link href="https://blog.lovezhy.cc/"/>
<updated>2021-02-02T11:48:50.878Z</updated>
<id>https://blog.lovezhy.cc/</id>
<author>
<name>zhy</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>Async的循环依赖</title>
<link href="https://blog.lovezhy.cc/2021/02/02/Async%E7%9A%84%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/"/>
<id>https://blog.lovezhy.cc/2021/02/02/Async%E7%9A%84%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/</id>
<published>2021-02-01T16:00:00.000Z</published>
<updated>2021-02-02T11:48:50.878Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>用了Spring这么多年,以为我对循环依赖的问题已经了如指掌,但是现实还是给我了一巴掌。</p><p>最近线上服务报了错误:</p><blockquote><p>Bean with name xx has been injected into other beans [ x ] in its raw version as part of a circular reference, </p><p>but has eventually been wrapped. This means that said other beans do not use the final version of the bean. </p><p>This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.</p></blockquote><p>Exception是BeanCurrentlyInCreationException,看起来是个循环依赖的问题,但是日志的报错是我从来没见过的。</p><h2 id="正常的循环依赖"><a href="#正常的循环依赖" class="headerlink" title="正常的循环依赖"></a>正常的循环依赖</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> B b;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">A</span><span class="params">(B b)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.b = b;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> A a;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">B</span><span class="params">(A a)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.a = a;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上,我们创建了两个Service,并且通过构造函数的方式注入。</p><p>启动时,Spring会报错:</p><blockquote><p>Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘a’: Requested bean is currently in creation: Is there an unresolvable circular reference?</p></blockquote><p>并且还会贴心的会你画个图:</p><blockquote><p>The dependencies of some of the beans in the application context form a cycle:</p><p>┌─────┐<br>| a defined in file [A.class]<br>↑ ↓<br>| b defined in file [B.class]<br>└─────┘</p></blockquote><p>这种也是我理解中的循环依赖问题,解决方案也是比较简单,我们换成set方法注入,或者成员变量注入就行。</p><h2 id="复现上文的异常"><a href="#复现上文的异常" class="headerlink" title="复现上文的异常"></a>复现上文的异常</h2><p>上文的异常,我一开始以为是AOP导致的问题,于是在A上加了个Aop的方法,发现Aop能够很好的解决这种循环依赖。</p><p>解决方案是三级缓存:</p><p>参考文章: <a href="https://segmentfault.com/a/1190000039134606" target="_blank" rel="noopener">https://segmentfault.com/a/1190000039134606</a></p><p>那我就疑惑了,到底是什么情况导致的呢?</p><p>后来搜到了文章:<a href="https://segmentfault.com/a/1190000018835760" target="_blank" rel="noopener">https://segmentfault.com/a/1190000018835760</a></p><p>发现@Async可以复现,于是我修改了代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> B b;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setB</span><span class="params">(B b)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.b = b;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Async</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doAsync</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"hello"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> A a;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setA</span><span class="params">(A a)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.a = a;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>发现确实可以复现了,这就暴露出我的知识盲区了,难道@Async不是通过Aop去解决的吗?</p><p>为什么@Aspect注解切的方法,不会报循环依赖,但是@Async的方法会呢?</p><p>带着疑惑,我又尝试了@Transectional会不会导致循环依赖问题,发现并不会。</p><p>所以,为啥@Async如此特殊呢?</p><h2 id="对比"><a href="#对比" class="headerlink" title="对比"></a>对比</h2><p>于是我得对比,@Async和@Transectional在实现上的区别</p><p>为此,我们把A的方法声明称这样:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> B b;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setB</span><span class="params">(B b)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.b = b;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="meta">@Async</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doAsync</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"hello"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时配上一个手写的Aspect:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AspectA</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Before</span>(<span class="string">"execution(public A.doAsync())"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeA</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"beforeA 执行"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在DefaultAdvisorChainFactory的getInterceptorsAndDynamicInterceptionAdvice方法中</p><p>我们可以查到这个方法所有的Advisor。</p><p><img src="http://log.lovezhy.cc/hVSBxt.png" alt=""></p><p>可以看到一共有三个</p><ol><li>ExposeInvocationInterceptor:先不管,和我们的业务无关</li><li>BeanFactoryTransactionAttributeSourceAdvisor:就是我们@Transactional的相关Aop</li><li>AspectA:自定义的Aop</li></ol><p>可以发现,并没有@Async相关的Aop代码。</p><p>而通过对DefaultAdvisorChainFactory的调用链,可以分析到,这里的执行,对原来类的Aop织入,其实在</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">doCreateBean#</span><br><span class="line">addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</span><br></pre></td></tr></table></figure><p>的逻辑中已经生成</p><p>所以也就导致</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"> Object earlySingletonReference = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line"> <span class="keyword">if</span> (earlySingletonReference != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">//这里拿到的exposedObject和Bean其实是一样的,不会抛出异常。</span></span><br><span class="line"> <span class="keyword">if</span> (exposedObject == bean) {</span><br><span class="line"> exposedObject = earlySingletonReference;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (!<span class="keyword">this</span>.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {</span><br><span class="line"> String[] dependentBeans = getDependentBeans(beanName);</span><br><span class="line"> Set<String> actualDependentBeans = <span class="keyword">new</span> LinkedHashSet<>(dependentBeans.length);</span><br><span class="line"> <span class="keyword">for</span> (String dependentBean : dependentBeans) {</span><br><span class="line"> <span class="keyword">if</span> (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {</span><br><span class="line"> actualDependentBeans.add(dependentBean);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!actualDependentBeans.isEmpty()) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> BeanCurrentlyInCreationException(beanName,</span><br><span class="line"> <span class="string">"Bean with name '"</span> + beanName + <span class="string">"' has been injected into other beans ["</span> +</span><br><span class="line"> StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +</span><br><span class="line"> <span class="string">"] in its raw version as part of a circular reference, but has eventually been "</span> +</span><br><span class="line"> <span class="string">"wrapped. This means that said other beans do not use the final version of the "</span> +</span><br><span class="line"> <span class="string">"bean. This is often the result of over-eager type matching - consider using "</span> +</span><br><span class="line"> <span class="string">"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那为什么加了@Async就会不一样呢?</p><p>通过上文,我们知道了@Async的逻辑和传统的Aop逻辑不太一样。</p><p>@Async是通过AsyncAnnotationBeanPostProcessor去织入自己的逻辑。</p><p>这个BeanPostProcessor在<code>applyBeanPostProcessorsAfterInitialization</code>方法中被调用</p><p>导致生成的是一个新的代理Bean,和原来的Bean就会不一样,也就是走到了抛出异常的逻辑。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>还是不能想当然,Aop是一个广泛的概念,但是在Spring中,其实还是冗余了不同的实现。</p><p>并不是所有加了Annotation的实现,都是一样的。</p><p>有可能是Aspect的实现,也有可能是类似BeanPostProcessor的实现。</p><p>需要看源码具体原因具体分析。</p>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>用了Spring这么多年,以为我对循环依赖的问题已经了如指掌,但是现实还是给我了一巴掌。</p>
<p>最近线上服务报了错误:</p>
<b
</summary>
<category term="Spring" scheme="https://blog.lovezhy.cc/categories/Spring/"/>
<category term="Spring" scheme="https://blog.lovezhy.cc/tags/Spring/"/>
</entry>
<entry>
<title>论文翻译-偏向锁</title>
<link href="https://blog.lovezhy.cc/2020/10/11/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-%E5%81%8F%E5%90%91%E9%94%81/"/>
<id>https://blog.lovezhy.cc/2020/10/11/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-%E5%81%8F%E5%90%91%E9%94%81/</id>
<published>2020-10-10T16:00:00.000Z</published>
<updated>2020-10-12T08:42:08.269Z</updated>
<content type="html"><![CDATA[<p>论文名字:<strong>Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing</strong></p><a id="more"></a><p>Bulk Rebiasing,查了一些博客,一般叫做<em>批量重偏向</em></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ol><li>偏向锁撤销,这里的意思是升级的意思。而重偏向,就把锁对象重置为可偏向的状态。</li><li>正常的博客中,对于HotSpot,其实没有提过重偏向,只有偏向锁撤销,升级为轻量级锁。</li><li>在一些对象中,使用重偏向是很有价值的。同样的,在一些对象中,禁用偏向锁是很有价值的。</li><li>这个论文介绍了一些方法,启发式的对一些对象禁用偏向锁,或者启用重偏向。<ol><li>监测对象分配地点或者监测某一类class的对象</li><li>计算成本,超过阈值就禁用偏向,同时对所有class的对象禁用偏向</li></ol></li><li>对批量重偏向和撤销的实现,论文中也提供了两种实现。基于Epoch的实现还是比较巧妙的。</li></ol><p>对于HotSpot中的批量重偏向和批量撤销,可以看看这篇文章:</p><p><a href="https://segmentfault.com/a/1190000023665056" target="_blank" rel="noopener">https://segmentfault.com/a/1190000023665056</a></p><h1 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h1><p>The Java programming language contains built-in synchronization primitives for use in constructing multithreaded programs. Efficient implementation of these synchronization primitives is necessary in order to achieve high performance.</p><p>Java 语言内置了一些同步原语,用于构建多线程程序。为了达到高性能的目的,必须高效地实现这些同步原语。</p><p>Recent research has focused on the runtime elimination of the atomic operations required to implement object monitor synchronization primitives. This paper describes a novel technique called store-free biased locking which eliminates all synchronization-related atomic operations on uncontended object monitors. The technique supports the bulk transfer of object ownership from one thread to another, and the selective disabling of the optimization where unprofitable, using epoch-based bulk rebiasing and revocation. It has been implemented in the production version of the Java HotSpot VM and has yielded signification performance improvements on a range of benchmarks and applications. The technique is applicable to any virtual machine-based programming language implementation with mostly block-structured locking primitives.</p><p>最近的研究都集中在如何在运行时消除一些原子操作,这些原子操作用来实现对象监视器的同步原语。</p><p>本论文讲解了一个叫做无需占用存储的偏向锁的新技术,该技术消除了在无竞争的对象监视器上的所有与同步相关的原子操作。</p><p>该技术支持将对象所有权从一个线程批量转移到另一个线程,并在使用偏向锁不会产生性能提升的情况下,使用基于epoch的批量重偏向和批量撤销,选择性地禁用优化。</p><p>该技术已在Java HotSpot虚拟机的生产版本中实现,并在一系列性能测试和应用中获得了明显的性能改进。</p><p>该技术适用于任何基于虚拟机的编程语言实现,主要是块结构的锁定原语。</p><h1 id="背景和动机"><a href="#背景和动机" class="headerlink" title="背景和动机"></a>背景和动机</h1><p>The Java programming language contains built-in support for monitors to facilitate the construction of multithreaded programs. Much research has been dedicated to decreasing the execution cost of the associated synchronization primitives.</p><p>Java编程语言包含对监控器的内置支持,以促进多线程程序的构建。</p><p>很多研究都致力于降低相关同步原语的执行成本。</p><p>A class of optimizations which can be termed lightweight locking are focused on avoiding as much as possible the use of “heavy-weight” operating system mutexes and condition variables to implement Java monitors. The assumption behind these techniques is that most lock acquisitions in real programs are un- contended. Lightweight locking techniques use atomic operations upon monitor entry, and sometimes upon exit, to ensure correct synchronization. These techniques fall back to using OS mutexes and condition variables when contention occurs.</p><p>一类可以被称为轻量级锁的优化技术集中在尽可能避免使用 “重量级 “的操作系统互斥锁和条件变量来实现Java监视器。</p><p>这些技术背后的假设是,实际程序中的大部分锁获取都是无竞争的。</p><p>轻量级锁技术在监视器进入时使用原子操作,有时在退出时也会使用,以确保正确的同步。</p><p>当发生锁争夺时,这些技术又回到了使用操作系统的互斥和条件变量。</p><p>A related class of optimizations which can be termed biased locking rely on the further property that not only are most monitors uncontended, they are only entered and exited by one thread during the lifetime of the monitor. Such monitors may be profitably biased toward the owning thread, allowing that thread to enter and exit the monitor without using atomic operations. If another thread attempts to enter a biased monitor, even if no contention occurs, a relatively expensive bias revocation operation must be performed. The profitability of such an optimization relies on the benefit of the elimination of atomic operations being higher than the penalty of revocation.</p><p>另一类相关的优化可以被称为偏向锁,依赖于进一步的程序运行特征,即大多数监视器不仅是无竞争的,而且在监视器的生命周期内只会由一个线程进入和退出。</p><p>这样的监控器可以偏向拥有的线程以获取高性能的执行效果,即允许该线程在不使用原子操作的情况下进入和退出监控器。</p><p>如果另一个线程试图进入一个偏向的监视器,即使没有发生竞争,也必须执行一个相对昂贵的偏向撤销操作。</p><p>这种优化的是否能带来收益,依赖于消除原子操作的收益高于撤销操作的消耗。</p><p>Current refinements of biased locking techniques decrease or eliminate the penalty of bias revocation, but do not optimize certain synchronization patterns which occur in practice, and also impact peak performance of the algorithm</p><p>目前对偏向锁技术的改进,减少或消除了撤销偏向锁的消耗,但没有优化实际中出现的某些同步情况,也影响了算法的峰值性能。</p><p>Multiprocessor systems are increasingly prevalent; so much so that uniprocessors are now the exception rather than the norm. Atomic operations are significantly more expensive on multi-processors than uniprocessors, and their use may impact scalability and performance of real applications such as javac by 20% or more (Section 6). It is crucial at this juncture to enable biased locking optimizations for industrial applications, and to optimize as many patterns of synchronization in these applications as possible.</p><p>多处理器系统越来越普遍;以至于单处理器现在是例外而不是标准。</p><p>原子运算在多处理器上的成本明显高于单处理器,使用原子运算可能会对实际应用(如javac)的可扩展性和性能产生20%以上的影响(第6节)。</p><p>在这个关头,为企业级应用进行偏向锁优化,并尽可能地优化这些应用中的同步模式是至关重要的。</p><h2 id="我们的贡献"><a href="#我们的贡献" class="headerlink" title="我们的贡献"></a>我们的贡献</h2><p>This paper presents a novel technique for eliminating atomic oper- ations associated with the Java language’s synchronization primitives called <em>store-free biased locking</em> (SFBL). It is similar to, and is inspired by, the <em>lock reservation</em> technique and its refinements. The specific contributions of our work are:</p><ul><li>We build upon invariants preserved by the Java HotSpot VM to <em>eliminate repeated stores</em> to the object header. Store elimination makes it easier to transfer bias ownership between threads.</li></ul><ul><li>We introduce <em>bulk rebiasing</em> and <em>revocation</em> to amortize the cost of per-object bias revocation while retaining the benefits of the optimization.</li><li>An <em>epoch-based</em> mechanism which invalidates previously held biases facilitates the bulk transfer of bias ownership from one thread to another.</li></ul><p>改论文提供了一个新技术,叫做无存储的偏向锁(store-free biased locking,SFBL),该技术可消除Java语言的同步原语中的原子操作。</p><p>它和锁保留技术以及其改进技术类似。</p><p>我们的贡献是:</p><ol><li>我们在Java HotSpot虚拟机保留的不变性基础上,<em>消除对象头的重复存储</em>。存储消除使得线程之间更容易转移偏向所有权。</li><li>我们引入批量重偏向和批量撤销机制,在保留优化收益的前提下,摊销每个对象的偏向撤销成本。</li><li>使用基于epoch的机制失效偏向,优化了偏向锁所有权从一个线程到另一个线程的批量转移。</li></ol><p>Our technique is the first to support <em>efficient transfer of bias ownership</em> from one thread to another for sets of objects. Previous techniques do not optimize the situation in which more than one thread locks a given object. The approaches above support optimization of more synchronization patterns in applications than previous techniques, and allow biased locking to be enabled by default for all applications.</p><p>我们是第一个支持高效转移偏向锁所有权的技术。</p><p>之前的技术没有优化多线程锁定指定的对象的场景。</p><p>与之前的技术相比,上述方法支持优化更多应用使用的同步模式,并允许所有应用默认启用偏向锁。</p><h2 id="论文的结构"><a href="#论文的结构" class="headerlink" title="论文的结构"></a>论文的结构</h2><p>The rest of this paper is organized as follows. Section 2 describes the lightweight locking technique in the Java HotSpot VM and its invariants. Section 3 describes the basic version of our biased locking technique. Section 4 describes the bulk rebiasing and revocation techniques used to amortize the cost of bias revocation. Section 5 improves the scalability of bulk rebiasing and revocation using epochs. Section 6 discusses results from various benchmarks. Section 7 provides detailed comparisons to earlier work. Section 8 describes how to obtain our implementation, and Section 9 concludes.</p><p>本文的其余部分组织如下。</p><p>第2节描述了Java HotSpot虚拟机中的轻量锁技术及其不变性(invariants,不知道咋翻译)。</p><p>第3节描述了我们偏向锁技术的基本版本。</p><p>第4节描述了用于摊销偏向撤销成本的批量重偏向和撤销技术。</p><p>第5节使用任期提高了批量重偏向和撤销的可扩展性。</p><p>第6节讨论了各种基准测试的结果。</p><p>第7节提供了与早期工作的详细比较。</p><p>第8节描述了如何获得我们的实现,第9节是结论。</p><h1 id="Java-HotSpot虚拟机中轻量级锁介绍"><a href="#Java-HotSpot虚拟机中轻量级锁介绍" class="headerlink" title="Java HotSpot虚拟机中轻量级锁介绍"></a>Java HotSpot虚拟机中轻量级锁介绍</h1><p>The lightweight locking technique used by the Java HotSpot VM has not been described in the literature. Because knowledge of some of its aspects is required to understand store-free biased locking (SFBL), we present a brief overview here.</p><p>Java HotSpot虚拟机使用的轻量级锁技术还没有论文描述。</p><p>由于理解无存储偏向锁(SFBL)需要了解它的一些方面的知识,我们在这里做一个简单的概述。</p><p>The Java HotSpot VM uses a two-word object header. The first word is called the <em>mark word</em> and contains synchronization, garbage collection and hash code information. The second word points to the class of the object. See figure 1 for an overview of the layout and possible states of the mark word.</p><p>Java HotSpot虚拟机使用两个字的对象头。第一个字称为<em>Mark Word</em>,包含同步、垃圾收集和哈希码信息。第二个字指向对象的类。请参见图1,了解MarkWord的布局和其可能的状态。</p><p><img src="/images/偏向锁/1.png" alt="image-20200914194144325" style="zoom:50%;"></p><p>Our biased locking technique relies on three invariants. First, the locking primitives in the language must be mostly block-structured. Second, optimized compiled code, if it is produced by the virtual machine, must only be generated for methods with block-structured locking. Third, interpreted execution must detect unstructured locking precisely. We now show how these invariants are maintained in our VM.</p><p>我们的偏向锁技术依赖于三个不变条件。</p><p>第一,编程语言中的锁定原语必须大部分是块结构的。</p><p>第二,由虚拟机产生的优化后的编译代码,必须只为具有块结构化锁定的方法生成。</p><p>第三,解释执行必须精确地检测非结构化的锁。</p><p>现在我们展示一下在我们的虚拟机中是如何维护这些不变性的。</p><p>Whenever an object is lightweight locked by a monitorenter bytecode, a lock record is either implicitly or explicitly allocated on the stack of the thread performing the lock acquisition operation. The lock record holds the original value of the object’s mark word and also contains metadata necessary to identify which object is locked. During lock acquisition, the mark word is copied into the lock record (such a copy is called a displaced mark word), and an atomic compare-and-swap (CAS) operation is performed to attempt to make the object’s mark word point to the lock record. If the CAS succeeds, the current thread owns the lock. If it fails, because some other thread acquired the lock, a slow path is taken in which the lock is inflated, during which operation an OS mutex and condition variable are associated with the object. During the inflation process, the object’s mark word is updated with a CAS to point to a data structure containing pointers to the mutex and condition variable.</p><p>每当一个对象被monitorenter字节码轻量级锁锁定时,在执行锁获取操作的线程的堆栈上就会隐式或显式地分配一个锁记录(Lock Record)。</p><p>锁记录保存着对象的MarkWord的原始值,同时还必须包含一些元数据,用于识别是哪个对象被锁定。</p><p>在锁获取过程中,MarkWord被复制到锁记录中(这样的复制称为替换MarkWord),并执行原子(CAS)操作,试图使对象的MarkWord指向锁记录。</p><p>如果CAS成功,则当前线程拥有该锁。如果失败了,则表示有其他线程获得了锁,则采取缓慢加锁路径,即锁膨胀,在这个操作过程中,一个OS mutex和条件变量与锁对象相关联。</p><p>在膨胀过程中,对象的MarkWord会被CAS更新,指向一个包含mutex和条件变量指针的数据结构。</p><p>During an unlock operation, an attempt is made to CAS the mark word, which should still point to the lock record, with the displaced mark word stored in the lock record. If the CAS succeeds, there was no contention for the monitor and lightweight locking remains in effect. If it fails, the lock was contended while it was held and a slow path is taken to properly release the lock and notify other threads waiting to acquire the lock.</p><p>在解锁操作过程中,会尝试用存储在Lock Record中的数据对对象的MarkWord进行CAS。如果CAS成功,则说明监控器没有被争用,轻量级锁定仍然有效。</p><p>如果失败,则说明锁在被持有时被争夺,并采取缓慢解锁方法正确释放锁,并通知其他等待获取锁的线程。</p><p>Recursive locking is handled in a straightforward fashion. If during lightweight lock acquisition it is determined that the current thread already owns the lock by virtue of the object’s mark word pointing into its stack, a zero is stored into the on-stack lock record rather than the current value of the object’s mark word. If zero is seen in a lock record during an unlock operation, the object is known to be recursively locked by the current thread and no update of the object’s mark word occurs. The number of such lock records implicitly records the monitor recursion count. This is a significant property to the best of our knowledge not attained by most other JVM</p><p>锁重入的处理方式很简单。如果在轻量级锁获取过程中,当前线程发现其对象的MarkWord指向其堆栈,那么就会在堆栈上的Lock Record中存储一个零,而不是对象的标记字的当前值。</p><p>如果在解锁操作过程中,在Lock Record中看到零,则知道对象被当前线程递归锁定,对象的标记字不会发生更新。</p><p>这种锁记录的数量隐含了监控器递归计数。</p><p>据我们所知,这是一个重要的属性,大多数其他JVM没有实现。</p><p>The Java HotSpot VM contains both a bytecode interpreter and an optimizing compiler. The interpreter and compiler-generated code create activation records called <em>frames</em> on a thread’s native stack during activation (i.e., execution) of Java methods. We designate these frames as <em>interpreted</em> or <em>compiled</em>. Interpreted frames contain data from exactly one method, while due to inlining, compiled frames may include data from more than one method.</p><p>Java HotSpot虚拟机包含一个字节码解释器和一个优化编译器。</p><p>解释器和编译器生成的代码在执行Java方法的过程中,会在线程的原生栈上创建执行记录,称为栈帧。</p><p>我们说这些栈帧为解释的或编译的。是因为解释帧仅包含一个方法的数据,而编译帧,由于内联,可能包含一个以上方法的数据。</p><p>Interpreted frames contain a region which holds the lock records for all monitors owned by the activation. During interpreted method execution this region grows or shrinks depending upon the number of locks held. In compiled frames, there is no such region. Instead, lock records are allocated by the compiler in a fashion similar to register spill stack slots. During compilation, metadata is generated which describes the set of locks held and the location of their lock records at each potential safepoint in compiled code. The presence of lock records allows the runtime system to enumerate the locked objects and their displaced mark words within each frame. This information is used during various operations internal to the JVM, including bias revocation, which will be described later.</p><p>解释帧包含一个区域,该区域保存着方法执行过程中所拥有的Lock Record。</p><p>在解释执行过程中,这个区域会根据所持有的锁的数量而增长或缩小。</p><p>在编译的框架中,没有这样的区域。相反,锁记录是由编译器以类似于寄存器溢出堆栈插槽(register spill stack slots,啥意思?)的方式分配的。</p><p>元数据在编译过程中被生成,它描述了所持有的锁的集合,以及它们在编译代码中每个潜在安全点的Lock Record的位置。</p><p>Lock Records的存在使得运行时系统可以枚举出每个栈帧内被锁定的对象和它们的位移标记字。</p><p>这些信息在JVM内部的各种操作过程中被使用,包括后面将介绍的偏向撤销。</p><p>The Java Virtual Machine Specification requires that an IllegalMonitorStateException be thrown if a monitorexit bytecode is executed without having previously executed a matching monitorenter. The interpreter detects this situation by checking that a lock record exists for an object being unlocked. It is not specified what happens when a monitorenter bytecode is executed in a method followed by removal of the corresponding frame from the stack without executing a monitorexit bytecode. In this case a JVM may legally either throw an exception or not. The Java Hotspot VM’s interpreter eagerly detects this situation by iterating through the lock records when removing an interpreted frame and forcibly unlocking the corresponding objects. It then throws an exception if any locked objects were found.</p><p>Java虚拟机规范要求,如果执行monitorexit字节码时,之前没有匹配的monitorenter,就会抛出IllegalMonitorStateException。</p><p>解释器通过检查被解锁的对象是否存在Lock Record来检测这种情况。</p><p>当一个monitorenter字节码在方法中执行后,没有执行monitorexit字节码就从堆栈中删除相应的栈帧时,会发生什么情况呢?</p><p>在这种情况下,JVM可以合法地抛出异常或不抛出异常。</p><p>Java Hotspot虚拟机的解释器急切地检测到这种情况,在删除一个解释帧时,通过迭代锁记录,强行解锁相应的对象。</p><p>然后,如果发现任何锁定的对象,它就会抛出一个异常。</p><p>The Java HotSpot client and server optimizing compilers will only compile and inline methods if dataflow analysis has proven that all monitorenter and monitorexit operations are properly paired; in other words, every lock of a given object has a matching unlock on the same object. Attempts to leave an object locked after the method returns, or to unlock an object not locked by that method, are detected by dataflow analysis. Such methods, which almost never occur in practice, are never compiled or inlined but always interpreted.</p><p>Java HotSpot客户端和服务器优化编译器只有在数据流分析证明所有的monitorenter和monitorexit操作都是成对的情况下才会编译和内联方法;</p><p>换句话说,一个给定对象的每一个锁都有一个匹配的解锁在同一个对象上。</p><p>在方法返回后试图让一个对象被锁定,或者解锁一个没有被该方法锁定的对象,都会被数据流分析检测到。</p><p>这样的方法,在实践中几乎不会出现,从来不会被编译或内联,而总是被解释。</p><p>Because interpreted execution precisely detects unstructured locking, and because compiled execution is proven through monitor matching to perform correct block-structured locking, it is guaranteed that an object’s locking state matches the program’s execution at all times. It is never the case that an object’s locking state claims that it is owned by a particular thread when in fact the method which performed the lightweight lock has already exited. A method may not unlock an object unless precisely that activation, and not one further up the stack, locked the object. These are essential properties enabling both the elimination of the recursion count described above as well as our biased locking technique in general. Complications arise in monitor-related optimizations such as lock coarsening in JVMs which do not maintain such invariants.</p><p>由于解释执行能够精确地检测到非结构化锁定,而且编译执行通过监控器匹配证明能够执行正确的块结构锁定,因此可以保证对象的锁定状态与程序的执行始终匹配。</p><p>在任何情况下,如果执行轻量级锁定的方法已经退出了,锁定对象的状态都不会还被该线程持有。</p><p>一个方法可能不会解锁一个对象,除非恰恰是那个激活,而不是堆栈上的进一步激活锁定了这个对象。(没看懂)</p><p>这些都是必不可少的特性,既能消除上面描述的递归数,也能消除我们一般的偏向锁定技术。</p><p>在与监控相关的优化中会出现一些复杂的情况,比如在JVM中的锁粗化,因为JVM并不维护这种不变性。</p><p>In summary, the following invariants in a programming language and virtual machine are essential prerequisites of our biased locking technique. First, the locking primitives in the language must be mostly block-structured. Second, compiled code, if it ex- ists in the VM, must only be produced for methods with block- structured locking. Third, interpreted execution must detect illegal locking states eagerly. These three invariants imply that an explicit recursion count for the lock is not necessary. Additionally, some mechanism must be present to record a “lock record” for the object externally to the object. In the Java HotSpot VM a lock record is allocated on the stack, although it might be allocated elsewhere.</p><p>综上所述,编程语言和虚拟机中的以下不变条件是我们的偏向锁技术的基本前提。</p><p>首先,语言中的锁定基元必须大部分是块结构的。</p><p>第二,编译后的代码,如果在虚拟机中运行,必须只为具有块结构锁的方法生成。</p><p>第三,解释执行必须急切地检测非法锁定状态。</p><p>这三个不变量意味着,锁的显式递归计数是不必要的。此外,必须存在某种机制,以便在对象外部记录对象的 “锁记录”。</p><p>在Java HotSpot虚拟机中,锁记录是在堆栈上分配的,尽管它可能在其他地方分配。</p><h1 id="不占存储-Store-free-的偏向锁"><a href="#不占存储-Store-free-的偏向锁" class="headerlink" title="不占存储(Store free)的偏向锁"></a>不占存储(Store free)的偏向锁</h1><p>Assuming the invariants in Section 2, the SFBL algorithm is simple to describe. When an object is allocated and biasing is enabled for its data type (discussed further in Section 4), a bias pattern is placed in the mark word indicating that the object is biasable (figure 1). The Java HotSpot VM uses the value 0x5 in the low three bits of the mark word as the bias pattern.</p><p>如果假设第2节中提到的不变条件,不占存储的偏向锁讲解起来很简单。</p><p>当分配一个对象并且偏向锁是启用状态时(在第4节中进一步讨论),一个偏向模式被放置在标记字中,表示该对象是可偏向的(图1)。</p><p>Java HotSpot虚拟机使用标记字低三位中的值0x5作为偏向模式。</p><p>The thread ID may be a direct pointer to the JVM’s internal representation of the current thread, suitably aligned so that the low bits are zero. Alternatively, a dense numbering scheme may be used to allow better packing of thread IDs and potentially more fields in the biasable object mark word.</p><p>线程ID可以是指向JVM的当前线程的内部表示的指针,适当地对齐,使低位为零。</p><p>另外,可以使用编号方案,以便更好地包装线程ID,并在可偏向对象标记字中可以存储更多的字段。</p><p>During lock acquisition of a biasable but unbiased object, an attempt is made to CAS the current thread ID into the mark word’s thread ID field. If this CAS succeeds, the object is now biased to- ward the current thread, as in figure 2. The current thread becomes the bias owner. The bias pattern remains in the mark word along- side the thread ID.</p><p>在一个可偏向但目前还不是偏向状态的对象的锁获取过程中,一个尝试是将当前线程ID CAS到markword的线程ID字段中。</p><p>如果CAS成功,对象就会偏向于当前线程,如图2所示。当前线程成为偏向所有者。偏向模式与线程ID一起保留在标记字中。</p><p><img src="/images/偏向锁/2.png" alt="image-20200930105508981" style="zoom:50%;"></p><p>If the CAS fails, another thread is the bias owner, so that thread’s bias must be revoked. The state of the object will be made to appear as if it had been locked by the bias owner using the JVM’s underlying lightweight locking scheme. To do this, the thread attempting to bias the object toward itself must manipulate the stack of the bias owner. To enable this a global safepoint is reached, at which point no thread is executing bytecodes. The bias owner’s stack is walked and the lock records associated with the object are filled in with the values that would have been produced had lightweight locking been used to lock the object. Next, the object’s mark word is updated to point to the oldest associated lock record on the stack. Finally, the threads blocked on the safepoint are released. Note that if the lock were not actually held at the present moment in time by the bias owner, it would be correct to revert the object back to the “biasable but unbiased” state and re-attempt the CAS to acquire the bias. This possibility is discussed further in section 4.</p><p>如果CAS操作失败了,另外一个线程是偏向锁持有者,导致必须撤销那个线程的偏向。</p><p>对象的状态要被弄成好像它已经被偏向所有者使用JVM的底层轻量级锁定方案锁定了一样。</p><p>要做到这一点,试图将对象偏向自己的线程必须操纵偏向所有者的栈。</p><p>为了实现这一点,需要达到一个全局安全点,此时没有线程在执行字节码。</p><p>偏向所有者的堆栈被遍历,与对象相关联的锁记录被填入轻量级锁的值。</p><p>接下来,对象的markword被更新为指向锁持有者堆栈上最古老的锁记录。</p><p>最后,安全点上被阻塞的线程被释放。</p><p>需要注意的是,如果锁在当前时间点没有被偏向所有者实际持有,那么正确的做法是将对象恢复到 “可偏向但无偏置 “的状态,并重新尝试CAS来获取偏向。</p><p>这种可能性将在第4节中进一步讨论。</p><p>If the CAS succeeded, subsequent lock acquisitions examine the object’s mark word. If the object is biasable and the bias owner is the current thread, the lock is acquired with no further work and no updates to the object header; the displaced mark word in the lock record on the stack is left uninitialized, since it will never be examined while the object is biasable. If the object is not biasable, lightweight locking and its fallback paths are used to acquire the lock. If the object is biasable but biased toward another thread, the CAS failure path described in the previous paragraph will be taken, including the associated bias revocation.</p><p>如果CAS成功,后续的锁获取会检查对象的标记字。如果对象是可偏向的,且偏向所有者是当前线程,则锁的获取不需要再做任何工作,也不需要更新对象头;</p><p>堆栈上的锁记录中的位移标记字不被初始化,因为在对象可偏向时,它永远不会被检查。</p><p>如果对象不可偏置,则使用轻量级锁及其后备方案来获取锁。</p><p>如果对象是可偏向的,但偏向另一个线程,则会采取上一段描述的CAS失败路径,包括相关的偏向撤销。</p><p>When an object is unlocked, the state of its mark word is tested to see if the bias pattern is still present. If it is, the unlock operation succeeds with no other tests. It is not even necessary to test whether the thread ID is equal to the current thread’s ID. If another thread had attempted to acquire the lock while the current thread was actually holding the lock and not just the bias, the bias revocation process would have ensured that the object’s mark word was reverted to the unbiasable state.</p><p>当一个对象被解锁时,将查看其标记字的状态,以确定偏向模式是否仍然存在。</p><p>如果存在,则解锁操作成功,无需其他操作。</p><p>甚至不需要测试线程ID是否等于当前线程的ID。</p><p>如果另一个线程试图在当前线程实际持有锁而不仅仅是偏向的情况下获取锁,偏向撤销过程就会确保对象的markword恢复到不可偏向的状态。</p><p>Since the SFBL unlock path does no error checking, the correctness of the unlock path hinges on the interpreter’s detection of unstructured locking. The lock records in interpreter activations ensure that the body of the monitorexit operation will not be executed if the object was not locked in the current activation. The guarantee of matched monitors in compiled code implies that no error check- ing is required in the SFBL unlock path in compiled code.</p><p>由于SFBL解锁路径不进行错误检查,所以解锁路径的正确性取决于解释器对非结构化锁定的检测。</p><p>解释执行中的锁记录保证了如果对象在当前执行中没有被锁定,那么monitorexit操作的主体将不会被执行。</p><p>保证监视器的匹配,意味着在编译代码中的SFBL解锁路径中不需要进行错误检查。</p><p>Figure 2 shows the state transitions of the mark word of an object under the biased locking algorithm. The bulk rebiasing edge, which is described further in sections 4 and 5, is only an effective, not an actual, transition and does not necessarily involve an update to the object’s mark word. Recursive locking edges, which update the on-stack lock records but not the mark word, and the heavy-weight locking state, which involves contention with one or more other threads, are omitted for clarity.</p><p>图2显示了偏向锁定算法下对象的标记字的状态转换。</p><p>在第4节和第5节将进一步描述的批量重偏向锁技术,只是一种有效的转换,而不是实际的转换,不一定会对对象的markword进行更新。</p><p>为了清楚起见,省略了更新栈上锁记录但不更新markword的递归锁技术,以及处理多个其他线程争夺的重量级锁状态。</p><h1 id="批量偏向和撤销"><a href="#批量偏向和撤销" class="headerlink" title="批量偏向和撤销"></a>批量偏向和撤销</h1><p>Analysis of execution logs of SFBL for the SPECjvm98, SPECjbb- 2000, SPECjbb2005 and SciMark benchmark suites yields two insights. First, there are certain objects for which biased locking is obviously unprofitable, such as producer-consumer queues where two or more threads are involved. Such objects necessarily have lock contention, and many such objects may be allocated during a program’s execution. It would be ideal to be able to identify such objects and disable biased locking only for them. Second, there are situations in which the ability to rebias a set of objects to another thread is profitable, in particular when one thread allocates many objects and performs an initial synchronization operation on each, but another thread performs subsequent work on them.</p><p>我们使用SPECjvm98、SPECjbb-2000、SPECjbb2005和SciMark的基准套件对SFBL进行测试,查看和分析执行日志,得到了两个启示。</p><p>首先,对某些对象来说,使用偏向锁定显然是无利可图的,比如涉及两个或多个线程的生产者-消费者队列。这样的对象必然存在锁争用,而且很多这样的对象可能在程序执行过程中被分配。</p><p>如果能够识别出这样的对象,并且只将它们禁用偏向,那将是非常理想的。</p><p>其次,在某些情况下,能够将一组对象重新偏向给另一个线程是有利可图的,特别是当一个线程分配了许多对象,并对每个对象执行了初始同步操作,但另一个线程对它们执行了后续工作。</p><p>When attempting to selectively disable biased locking, we must be able to identify objects for which it is unprofitable. If one were able to associate an object with its allocation site, one might find patterns of shared objects; for example, all objects allocated at a particular site might seem to be shared between multiple threads. Experiments indicate this correlation is present in many programs[6]. Being able to selectively disable the insertion of the biasable mark word at that site would be ideal. However, due to its overhead, allocation site tracking is to the best of our knowledge not currently exploited in production JVMs.</p><p>当有选择地禁用偏向锁时,我们必须能够识别使用偏向锁无利可图的对象。</p><p>如果能够将一个对象与它的分配点关联起来,我们可能会发现共享对象的模式;</p><p>例如,所有分配在特定地方的对象可能看起来是在多个线程之间共享的。</p><p>实验表明,这种关联性存在于许多程序中。</p><p>能够有选择地禁止在该地方插入可偏向的标记字是非常理想的。</p><p>然而,据我们所知,由于其开销,分配点跟踪目前在生产的JVM中还没有被利用。</p><p>We have found empirically that selectively disabling SFBL for a particular data type is a reasonable way to avoid unprofitable situations. We therefore amortize the cost of rebiasing and individual object bias revocation by performing such rebiasing and revoking in bulk on a per-data-type basis.</p><p>同时,根据经验我们发现,有选择地禁用某类的SFBL是避免无利可图情况的合理方法。</p><p>因此,我们通过在每个类的基础上批量执行这种重偏向和批量撤销,来摊销重偏向和单个对象偏向撤销的成本。</p><p>Heuristics are added to the basic SFBL algorithm to estimate the cost of individual bias revocations on a per-data-type basis. When the cost exceeds a certain threshold, a bulk rebias operation is attempted. All biasable instances of the data type have their bias owner reset, so that the next thread to lock the object will reacquire the bias. Any biasable instance currently locked by a thread may optionally have its bias revoked or left alone.</p><p>我们在基本的SFBL算法中增加了启发式算法,以估计每个类的单个偏向撤销的成本。</p><p>当成本超过某个阈值时,就会尝试进行批量重偏向操作。</p><p>该类的所有可偏向实例的偏向所有者都会被重置,这样下一个锁定对象的线程就会重新获得偏向。</p><p>任何当前被线程锁定的可偏向实例都可以选择撤销其偏向或什么也不做。</p><p>If bias revocations for individual instances of a given data type persist after one or more bulk rebias operations, a bulk revocation is performed. The mark words of all biasable instances of the data type are reset to the lightweight locking algorithm’s initial value. For currently-locked and biasable instances, the appropriate lock records are written to the stack, and their mark words are adjusted to point to the oldest lock record. Further, SFBL is disabled for any newly allocated instances of the data type.</p><p>如果给定类的单个实例的偏向撤销在一个或多个批量重偏向操作后持续存在,则执行批量撤销。</p><p>该数据类型的所有可偏向实例的标记字被重置为轻量级锁定算法的初始值。</p><p>对于当前锁定的和可偏向的实例,锁记录会被写入堆栈,它们的markword被调整为指向最老的锁定记录。</p><p>此外,对于该类的任何新分配的实例,SFBL被禁用。</p><p>The most obvious way of finding all instances of a certain data type is to walk through the object heap, which is how these techniques were initially implemented (Section 5 describes the current implementation). Despite the computational expense involved, bulk rebiasing and revocation are surprisingly effective.</p><p>寻找某种类的所有实例的最明显的方法是遍历堆,这就是这些技术最初的实现方式(第5节介绍了当前的实现)。尽管涉及到计算费用,但对批量重偏向和撤销却出奇的有效。</p><p><img src="/images/偏向锁/3.png" alt="image-20200930150410456" style="zoom:50%;"></p><p>Figure 3 illustrates the benefits of the bulk revocation and rebiasing heuristics compared to the basic biased locking algorithm . The javac sub-benchmark from SPECjvm98 computes many identity hash codes, forcing bias revocation of the affected objects since there are no bits available to store the hash code in the biasable state (see figure 1). Bulk revocation benefits this and similar situations, here in particular because our early implementations performed relatively inefficient bias revocation in this case. SPECjbb2000 and SPECjbb2005 transfer a certain number of objects between threads as each warehouse is added to the benchmark, not enough to impact scores greatly but enough to trigger the bulk revocation heuristic. The addition of bulk rebiasing, which is then triggered at the time of addition of each warehouse, reclaims the gains to be had.</p><p>图3说明了与基本的偏向锁定算法相比,批量撤销和重偏向启发式算法的好处。<br>SPECjvm98中的javac子基准计算了许多哈希码,强制受影响对象进行偏向撤销,因为没有存储空间可用于在可偏向状态下存储哈希码(见图1)。</p><p>批量撤销有利于这种情况和类似的情况,这里特别是因为我们的早期实现在这种情况下执行了相对低效的偏向撤销。</p><p>SPECjbb2000和SPECjbb2005在每个仓库加入基准时,都会在线程之间传输一定数量的对象,虽然不足以对分数产生很大影响,但足以触发批量撤销启发式算法。</p><p>在每个仓库增加的时候再触发批量重偏向,就可以把要获得的收益收回来。</p><p>Note that the addition of both bulk revocation and rebiasing does not reduce the peak performance of biased locking compared to the basic algorithm without these operations. This is discussed further in Section 7.</p><p>请注意,与不进行这些操作的基本算法相比,增加批量撤销和重偏向并不会降低偏向锁定的峰值性能。这将在第7节中进一步讨论。</p><h1 id="基于epoch的批量重偏向和撤销"><a href="#基于epoch的批量重偏向和撤销" class="headerlink" title="基于epoch的批量重偏向和撤销"></a>基于epoch的批量重偏向和撤销</h1><p>Though walking the object heap to implement bulk rebias and revocation algorithms is workable for relatively small heaps, it does not scale well as the heap grows. To address this problem, we introduce the concept of an <em>epoch</em>, a timestamp indicating the validity of the bias. As shown in figure 1, the epoch is a bitfield in the mark word of biasable instances. Each data type has a corresponding epoch as long as the data type is biasable. An object is now considered biased toward a thread T if both the bias owner in the mark word is T, and the epoch of the instance is equal to the epoch of the data type.</p><p>虽然遍历对象堆来实现批量重偏向和撤销对于相对较小的堆来说是可行的,但随着堆的增长,它的扩展性并不好。</p><p>为了解决这个问题,我们引入了epoch的概念,epoch是一个表示偏向有效性的时间戳。</p><p>如图1所示,epoch是可偏向对象的markword中的一个bit位。</p><p>只要类是可偏向的,每个类都有一个相应的epoch。</p><p>如果markword中的偏向所有者是线程T,并且实例的epoch等于该数据类型的epoch,那么现在就认为一个对象偏向于线程T。</p><p>With this scheme, bulk rebiasing of objects of class C becomes much less costly. We still stop all mutator threads at a safe-point; without stopping the mutator threads we cannot reliably tell whether or not a biased object is currently locked. The thread performing the rebiasing:</p><p>有了这个方案,类C的对象的批量重偏向成本就会大大降低。</p><p>我们仍然在一个安全点停止所有的突变器线程;如果不停止突变器线程,我们就无法可靠地判断一个偏向的对象当前是否被锁定。线程执行重偏向操作:</p><ol><li><p>Increments the epoch number of class C. This is a fixed-width integer, with the same bit-width in the class as in the object headers. Thus, the increment operation may cause wrapping, but as we will argue below, this does not compromise correct- ness.</p><p>增大class C的epoch数字。这是一个固定宽度的整数,在类中的位宽与对象头中的位宽相同。因此,增量操作可能会导致封装,但正如我们在下面所论证的,这并不影响正确性。</p></li><li><p>Scans all thread stacks to locate objects of class C that are currently locked, updating their bias epochs to the new current bias epoch for class C. Alternatively, based on heuristic consideration, these objects’ biases could be revoked.</p><p>扫描所有线程的堆栈,定位当前被锁定的C类对象,将它们的偏向epoch更新为C类新的当前偏向epoch,另外,基于启发式考虑,可以撤销这些对象的偏向。</p></li></ol><p>No heap scan is necessary; objects whose epoch numbers were not changed will, for the most part, now have a different epoch number than their class, and will be considered to be in the biasable but unbiased state.</p><p>不进行堆扫描是十分有必要的。</p><p>大部分情况下,其epoch未被改变的对象,现在的epoch将与它们的class不同,并将被认为是处于可偏向但无偏向的状态。</p><p>The pseudocode for the lock-acquisition operation then looks much like:</p><p>然后,锁获取操作的伪代码即这样:</p><p><img src="/images/偏向锁/4.png" alt="image-20201009194051670" style="zoom:50%;"></p><p>Above we made the qualification that incrementing a class’s bias epoch will “for the most part” rebias all objects of the given class. This qualification is necessary because of the finite width of the epoch field, which allows integer wrapping. If the epoch field is N bits wide, and X is an object of type T, then if 2ˆN bulk rebiasing operations for class T occur without any lock operation updating the bias epoch of X to the current epoch, then it will appear that X is again biased in the current epoch, that is, that its bias is valid. Note that this is purely a performance concern – it is perfectly permissible, from a correctness viewpoint, to consider X biased. It may mean that if a thread other than the bias holder attempts to lock X, an individual bias revocation operation may be required. But a sufficiently large value of N can decrease the frequency of this situation significantly: objects that are actually locked between one epoch and the next have their epoch updated to the current epoch, so this situation only occurs with infrequently-locked objects. Further, we could arrange for operations that naturally visit all live objects, namely garbage collection, to normalize lock states, convert- ing biased objects with invalid epochs into biasable-but-unbiased objects. (If done in a stop-world collection this can be done with non-atomic stores; in a concurrent marker, however, the lock word would have to be updated with an atomic operation, since the marking thread would potentially compete with mutator threads to modify the lock word.) Therefore, wrapping issues could also be prevented by choosing N large enough to make it highly likely that a full-heap garbage-collection would occur before 2ˆN bulk rebias operations for a given type can occur.</p><p>上面我们做了一个假设,即递增一个类的偏向epoch将 “在大多数情况下 “导致重偏向给定类的所有对象。</p><p>这个假设是必要的,因为epoch字段的宽度是有限的,它允许整数包装。如果epoch字段是N位宽,X是T类型的对象,那么如果对T类进行2ˆN次批量重偏向操作,而没有任何锁操作将X的偏置epoch更新为当前epoch,那么X在当前epoch中又会出现偏向,也就是说,它的偏向是有效的。</p><p>请注意,这纯粹是对性能的考虑,从正确性的角度来看,完全可以认为X是有偏差的。</p><p>这可能意味着,如果偏置持有者以外的线程试图锁定X,可能需要进行单独的偏置撤销操作。</p><p>但是一个足够大的N值可以大大降低这种情况的频率:在一个epoch和下一个epoch之间被实际锁定的对象会将其epoch更新为当前epoch,所以这种情况只发生在不经常锁定的对象上。</p><p>此外,我们可以安排自然访问所有实时对象的操作,即垃圾收集,以规范化锁定状态,将具有无效纪元的偏向对象转换为可偏向但无偏向的对象。(如果在STW阶段,这可以用非原子存储来完成;但是在并发标记中,markword必须用原子操作来更新,因为标记线程有可能与突变器线程竞争修改锁字)。</p><p>因此,包装问题也可以通过选择足够大的N来预防,以使在给定类型的2ˆN批量重偏向操作发生之前,极有可能发生fullGC。</p><p>In practice, wrapping of the epoch field can be ignored. Bench-marking has not uncovered any situations where individual bias revocations are provoked due to epoch overflow. The current implementation of biased locking in the Java HotSpot VM normalizes object headers during GC, so the mark words of biasable objects with invalid epochs are reverted to the unbiased state. This is done purely to reduce the number of mark words preserved during GC, not to counteract epoch overflow.</p><p>在实践中,对epoch的包装可以被忽略。</p><p>压测过程中并没有发现任何由于epoch溢出而引发个别偏向撤销的情况。</p><p>目前Java HotSpot VM中的偏置锁定的实现在GC过程中对对象头进行了归一化处理,因此具有无效epoch的可偏置对象的标记字会恢复到无偏置状态。</p><p>这样做纯粹是为了减少GC过程中保存的标记字数量,而不是为了抵消epoch溢出。</p><p>It is a straightforward extension to support bulk revocation of biases of a given data type. Recall that in bulk revocation, unlike bulk rebiasing, it is desired to completely disable the biased locking optimization for the data type, instead of allowing the object to be potentially rebiased to a new thread. Rather than incrementing the epoch in the data type, the “biasable” property for that data type may be disabled, and a dynamic test of this property added to the lock sequence:</p><p>支持对给定class的批量撤销偏向是一种直接的扩展功能。</p><p>回想一下,在批量撤销中,与批量重偏向不同的是,批量撤销希望完全禁用某数据类型的偏向锁定优化,而不是让对象有可能被重偏向到一个新的线程。</p><p>所以需要直接禁用该class的“可偏向”属性,而不是递增该class的epoch,同时一个判断需要加在加锁流程中:</p><p><img src="/images/偏向锁/5.png" alt="image-20201009195651147" style="zoom:50%;"></p><p>This variant of the lock sequence is the one currently implemented in the Java HotSpot VM.</p><p>这个锁流程的变体是目前在Java HotSpot虚拟机中实现的。</p><p>Epoch-based rebiasing and revocation may also be extended to rebias objects at a granularity between the instance and class level. For example, we might distinguish between objects of a given class based on their allocation site; JIT-generated allocation code could be modified to insert an allocation site identifier in the object header. Each allocation site could have its own epoch, and the locking sequence could check the appropriate epoch for the object:</p><p>基于epoch的重偏向和撤销也可以扩展到在实例和class之间的粒度上重偏向对象。</p><p>例如,我们可以根据对象的分配点来区分给定类的对象;</p><p>JIT生成的分配代码可以被修改为在对象头中插入一个分配点标识符。</p><p>每个分配点都可以有自己的epoch,锁定流程可以检查对象的纪元。</p><p><img src="/images/偏向锁/6.png" alt="image-20201009200253511" style="zoom:50%;"></p><p>To simplify the allocation path for new instances as well as storage of the per-data-type epochs, a prototype mark word is kept in each data type. This is the value to which the mark word of new instances will be set. The epoch is stored in the prototype mark word as long as the prototype is biasable.</p><p>为了简化新实例的分配路径以及每个class的epoch的存储,每个class中都保留了一个原型markword。</p><p>这是新实例的markword将被设置为的值。只要原型是可偏向的,就会在原型标记字中存储纪元。</p><p>In practice, a single logical XOR operation in assembly code computes the bitwise difference between the instance’s mark word and the prototype mark word of the data type. A sequence of tests are performed on the result of the XOR to determine whether the bias is held by the current thread and currently valid, whether the epoch has expired, whether the data type is no longer biasable, or whether the bias is assumed not held, and the system reacts appropriately. Listing 4 shows the complete SPARC assembly code for the lock acquisition path of SFBL with epochs.</p><p>在实际应用中,汇编代码中的一个逻辑XOR操作就可以计算出实例的markword和class的原型标记字之间的位差。</p><p>对XOR的结果进行一系列的测试,以确定偏向是否被当前线程持有且当前有效,是否过期,class是否不再可偏向,或者假设偏向不持有,系统做出适当的反应。</p><p>清单4显示了SFBL带纪元的锁获取路径的完整SPARC汇编代码。</p><p>(注:清单4有点长,就不贴了,都是汇编)</p><h1 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h1><p>(注:都是测试结果对比,非原理性讲解,就不翻译了)</p><h1 id="与前人工作的对比"><a href="#与前人工作的对比" class="headerlink" title="与前人工作的对比"></a>与前人工作的对比</h1><p>SFBL is similar to, and is inspired by, lock reservation and its refinements. Lock reservation is directly comparable to our basic biased locking technique described in Section 3. Both techniques eliminate all atomic operations for uncontended synchronization and have a severe penalty for bias revocation. Our technique avoids subtle race conditions because objects’ headers are not repeatedly updated with non-atomic stores. However, because an explicit recursion count is not maintained, it is more difficult in our technique to determine at any given point in time whether a biased lock is actually held by a given thread.</p><p>SFBL与锁保留及其改进方案类似,并受其启发。</p><p>锁保留与我们在第3节中描述的基本偏向锁定技术直接相当。这两种技术都消除了所有无竞争同步的原子操作,并且对偏向撤销有严重的惩罚。</p><p>我们的技术避免了微妙的竞争条件,因为对象头不会用非原子存储重复更新。</p><p>然而,由于没有维护明确的递归计数,在我们的技术中,在任何给定的时间点确定一个偏向锁是否真的被一个给定的线程持有是比较困难的。</p><p>The global safepoint required for bias revocation in our technique is more expensive than the signal used in lock reservation. It can be a barrier to scalability in applications such as Volano with many threads, many contended lock operations, and ongoing dynamic class loading. However, our experience has been that the combination of these characteristics in an application is rare. We have prototyped a per-thread safepoint mechanism and are investigating its performance characteristics. We also believe a less ex- pensive per-object bias revocation technique is possible for uncontended locks while maintaining the useful locking invariants in the Java HotSpot VM, and plan to investigate this in the future.</p><p>在我们的技术中,偏向撤销所需的全局安全点比锁保留中使用的信号更昂贵。</p><p>在Volano这样有许多线程、许多争夺的锁操作和持续的动态类加载的应用中,这可能是一个扩展性的障碍。</p><p>然而,我们的经验是,在一个应用程序中这些特性的组合是罕见的。我们已经建立了一个每个线程安全点机制的原型,并且正在研究它的性能特点。</p><p>我们还认为,对于无争夺锁,同时保持Java HotSpot虚拟机中有用的锁定不变性,可以采用一种不那么昂贵的每对象偏向撤销技术,并计划在未来对此进行研究。</p><p>Reservation-based spin locks [12, 10] are comparable to our addition of bulk rebiasing and revocation described in Section 4. Both techniques build on top of an underlying biased locking algorithm to reduce the impact of bias revocation. An advantage of reservation-based spin locks is that they largely eliminate, rather than reduce or amortize, the cost of bias revocation. However, reservation-based spin locks do not support transfer of bias ownership between threads. The first thread to lock a given object will always be the bias owner, and other threads will still need to use atomic operations to enter and exit the lock, eliminating the benefits of the optimization for these other threads. In contrast, epoch-based bulk rebiasing allows direct transfer of biases in the aggregate from one thread to another, at the cost of a small number of per-object revocations. Our experience indicates this supports optimization of significantly more synchronization patterns in real programs.</p><p>基于保留的自旋锁与我们在第4节中所描述的增加了大量的偏向和撤销技术相类似。</p><p>这两种技术都建立在底层的偏向锁算法之上,以减少偏置撤销的影响。</p><p>基于保留的自旋锁的一个优点是,它们在很大程度上消除而不是减少或摊销了偏向撤销的成本。</p><p>但是,基于保留的自旋锁不支持线程之间转移偏向所有权。</p><p>第一个锁定给定对象的线程将始终是偏向所有者,其他线程仍然需要使用原子操作来进入和退出锁,从而消除了这些其他线程的优化好处。</p><p>相比之下,基于epoch的批量重偏向允许直接将集合中的偏置从一个线程转移到另一个线程,而代价是少量的每个对象撤销。</p><p>我们的经验表明,这支持在实际程序中对更多的同步模式进行优化。</p><p>Neither reservation-based spin locks nor our algorithm optimize the case of a single object or small set of objects being locked and unlocked multiple times sequentially by two or more threads, but always in uncontended fashion. Our bulk rebiasing technique optimizes this case in the aggregate, when many such objects are locked in this pattern. Efficient optimization of this synchronization pattern is an important area for future research.</p><p>基于保留的自旋锁和我们的算法都不能优化单个对象或一小组对象被两个或多个线程连续多次锁定和解锁的情况,但总是以无竞争的方式。</p><p>当许多这样的对象被锁定在这种模式下时,我们的批量重偏向技术对这种情况进行了总体优化。</p><p>高效优化这种同步模式是未来研究的一个重要领域。</p><p>Reservation-based spin locks appear to adversely impact the peak performance of the lock reservation optimization as can be seen in the published results for db and jack [12, 10]. In contrast, epoch-based bulk rebiasing and revocation appear to reduce the adverse impacts of the biased locking optimization without impact- ing peak performance, as shown in Sections 4 and 6. We believe the high cost of per-object bias revocation in our system is responsible for the negative impact on the Volano benchmark, and plan to reduce this cost in the future. Nonetheless, feedback from customers indicates that our current biased locking implementation yields good results in the field with no pathological performance problems.</p><p>基于保留的自旋锁似乎会对锁保留优化的峰值性能产生不利影响,这可以从已发布的db和jack的结果中看出。</p><p>相比之下,如第4节和第6节所示,基于epoch的批量重偏向和撤销似乎可以减少偏向锁优化的不利影响,而不影响峰值性能。</p><p>我们认为,我们系统中每个对象偏向撤销的高成本是造成Volano基准负面影响的原因,并计划在未来降低这一成本。</p><p>尽管如此,来自客户的反馈表明,我们目前的偏置锁定实施在现场产生了良好的结果,没有出现病理性能问题。</p><p>Speculative locking [7], another biased locking technique, eliminates all synchronization-related atomic operations, but requires a separate field in each object instance to hold the thread ID. This space increase makes the technique unsuitable for most data types. Additionally, speculative locking does not support the transfer of bias ownership from one thread to another, nor selective disabling of the optimization where unprofitable.</p><p>推测性锁定是另一种偏向性锁定技术,它消除了所有与同步相关的原子操作,但需要在每个对象实例中单独设置一个字段来保存线程ID。</p><p>这种空间的增加使得该技术不适合大多数数据类型。</p><p>此外,推测性锁定不支持将偏向所有权从一个线程转移到另一个线程,也不支持在无利可图的情况下选择性地禁用优化。</p><p>Previous lightweight locking techniques[1, 2, 5] exhibit quite different performance characteristics for contended and uncontended locking and contain very different techniques for falling back to heavyweight operating system locks under contention. Some of these techniques use only one atomic operation per pair of lock/unlock operations rather than two. Nonetheless, all of these techniques use at least one atomic operation per lock/unlock sequence so are not directly comparable to SFBL. Potentially, any of these techniques could be used as the underlying synchronization technique for SFBL or a similar biased locking technique.</p><p>以前的轻量级锁定技术在有争夺和无争夺的锁定中表现出完全不同的性能特征,并且包含了在争夺下回落到重量级操作系统锁的非常不同的技术。其中一些技术在每对锁/解锁操作中只使用一个原子操作,而不是两个。尽管如此,所有这些技术都在每个锁/解锁序列中使用至少一个原子操作,因此不能直接与SFBL进行比较。这些技术中的任何一种都有可能被用作SFBL或类似的偏向锁定技术的基础同步技术。</p><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><p>Our technique is implemented in the current development version of the Java HotSpot VM. Binaries for various architectures and source code can be downloaded from <a href="http://mustang.dev.java.net/" target="_blank" rel="noopener">http://mustang.dev.java.net/</a>. The current build contains the per-data-type epoch-based rebiasing and revocation presented here. The biased locking optimization is currently enabled by default and can be disabled for comparison purposes by specifying -XX:-UseBiasedLocking on the command line.</p><p>我们的技术是在当前开发版本的Java HotSpot VM中实现的。</p><p>各种架构的二进制文件和源代码可以从 <a href="http://mustang.dev.java.net/" target="_blank" rel="noopener">http://mustang.dev.java.net/</a> 下载。</p><p>当前的构建版本包含了这里介绍的基于数据类型的epoch的重偏向和撤销。</p><p>偏向锁定优化目前是默认启用的,可以通过在命令行指定-XX:-UseBiasedLocking来禁用,以便进行比较。</p><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>Current trends toward multiprocessor systems in the computing industry make synchronization-related atomic operations an increasing impediment to the scalability of applications. Biased locking techniques are crucial to continued performance improvement of programming language implementations.</p><p>目前计算行业中多处理器系统的趋势使得与同步相关的原子操作越来越阻碍应用程序的可扩展性。</p><p>偏向锁技术是持续提高编程语言实现性能的关键。</p><p>We have presented a new biased locking technique which optimizes more synchronization patterns than previous techniques:</p><p>我们提出了一种新的偏向锁定技术,它比以前的技术优化了更多的同步模式。</p><ul><li><p>It eliminates repeated stores to the object header. Store elimination makes it easier to transfer bias ownership between threads.</p><p>它消除了对对象头的重复存储。存储优化使得线程之间转移偏向所有权更容易。</p></li><li><p>It introduces bulk rebiasing and revocation to amortize the cost of per-object bias revocation while retaining the benefits of biased locking.</p><p>它引入了批量重偏向和撤销,以摊销每个对象偏向撤销的成本,同时保留了偏向锁定的优点。</p></li><li><p>Epoch-based bulk rebiasing and revocation yield efficient bulk transfer of bias ownership from one thread to another.</p><p>基于epoch的批量重偏向和撤销产生了从一个线程到另一个线程的高效批量转移偏向所有权。</p></li></ul><p>Our technique is applicable to any programming language and virtual machine with mostly block-structured locking and a few invariants in the interpreter and dynamic compiler. It yields good performance increases on a range of benchmarks with few penalties, and customer feedback indicates that it performs well on Java programs in the field. We believe our technique can be extended to optimize even more synchronization patterns.</p><p>我们的技术适用于任何编程语言和虚拟机,只依赖解释器和动态编译器中使用块结构化锁和一些不变条件。</p><p>它在一系列基准上产生了很好的性能提升,而且几乎没有惩罚,客户的反馈表明它在现场的Java程序上表现良好。</p><p>我们相信我们的技术可以扩展到优化更多的同步模式。</p>]]></content>
<summary type="html">
<p>论文名字:<strong>Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing</strong></p>
</summary>
<category term="论文翻译" scheme="https://blog.lovezhy.cc/categories/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91/"/>
<category term="JVM" scheme="https://blog.lovezhy.cc/tags/JVM/"/>
</entry>
<entry>
<title>论文翻译-CMS</title>
<link href="https://blog.lovezhy.cc/2020/09/05/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-CMS/"/>
<id>https://blog.lovezhy.cc/2020/09/05/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-CMS/</id>
<published>2020-09-04T16:00:00.000Z</published>
<updated>2020-09-05T15:25:19.021Z</updated>
<content type="html"><![CDATA[<p><strong>A Generational Mostly-concurrent Garbage Collector</strong><br>(题目不知道咋翻,算了),论文发布在2000年</p><p>注:本文讲的不完全是CMS垃圾收集器,其实讲的是并发垃圾收集器的思路。</p><p>而Java中的并发垃圾收集器包含CMS和G1。</p><a id="more"></a><p>Mostly-concurrent:不知道咋翻译,这里就翻译成<code>近似并发</code></p><p>mutator:也不知道咋翻译,就按照软件给我叫突变器吧,类似于JVM的用户运行程序</p><p>看到个博客,讲的挺好的</p><p><a href="https://www.cnblogs.com/Leo_wl/p/5393300.html" target="_blank" rel="noopener">https://www.cnblogs.com/Leo_wl/p/5393300.html</a></p><h2 id="译者总结"><a href="#译者总结" class="headerlink" title="译者总结"></a>译者总结</h2><ol><li><p>CMS的垃圾收集算法没有选择Mark-Compact的方式,是因为Compact后,要修改所有指向该对象的引用,这个操作无法并发进行。</p><p>导致CMS老年代的内存分配是使用Free-List的方式进行分配,类似于伙伴关系算法。</p></li><li><p>接着1的说,在年轻代的收集中,对象晋升时,需要把对象复制到老年代。但是因为使用Free-List的方式,这种复制很慢。官方注意到晋升时,垃圾收集器线程和突变器线程都是暂停的,所以让CMS进行了优化,支持了线性分配模式,这种模式下,使用线性分配来进行对象晋升复制。(论文中似乎没有写出具体的实现方式)</p></li><li><p>卡表:CMS中的卡表有两个作用,或者说其实CMS运用了两个卡表,作用却是不同的。</p><ol><li>用来标记老年代指向新生代对象的页,这样新生代收集的时候,也要Mark被老年代对象引用的新生代对象</li><li>垃圾收集的并发收集阶段,如果这个时候对象的引用产生了变化,通过写屏障,把对象的页对应的卡表的位置,置为脏页,让重新标记阶段扫描这些脏页的对象。</li></ol></li><li><p>标记对象时,CMS使用一系列的BitMap,每一个Bit表示地址的4Byte,也就是一个对象的位置,来记录初始标记阶段从GCRoot可直接到达的对象的地址。</p><p>这样比使用容器存储这些对象的引用更省空间</p></li><li><p>并发标记阶段,突变器新分配的对象,可以是unmark的,因为重新标记阶段肯定会标记到这个对象。</p><p>但是在并发收集阶段,突变器新分配的对象,应该是mark的,防止被收集掉。</p></li><li><p>重新标记阶段,是假定并发标记阶段,引用产生变化的对象不会很多。但是如果很多的话,就会造成STW的时间过长,使用并发预清理来应对这个情况。</p><p>并发预清理,就是查看在并发标记阶段,查看卡表,对dirty的表项进行处理,然后置为clean。</p><p>这样即使降低在重新标记阶段要处理的卡表项过多。</p></li></ol><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>本文报告了我们在Java编程语言的高性能虚拟机背景下实现的近似并发增量垃圾收集器的经验。</p><p>该垃圾收集器基于Boehm等人的 “近似并行(parallel) “收集算法,可以作为分代内存系统的老年代代垃圾收集器。</p><p>我们重写了目前的分代垃圾收集的写屏障代码,使之也可以识别在并发标记期间被修改的对象。</p><p>这些对象必须重新扫描,以确保并发标记阶段标记所有的活对象。</p><p>这种算法最大限度地减少了垃圾收集的暂停时间,同时对垃圾收集的平均暂停时间和整体执行时间只有很小的影响。</p><p>我们将用实验结果来支持我们的观点,包括合成基准和真实程序。</p><h2 id="1-介绍"><a href="#1-介绍" class="headerlink" title="1. 介绍"></a>1. 介绍</h2><p>在20世纪50年代末,依靠垃圾回收的编程语言就已经存在了。</p><p>尽管垃圾回收对程序的简单性和健壮性的好处是众所周知的并且也是被认同的,但大多数软件开发者仍然依赖于传统的显式内存管理,这主要是出于对性能的考虑。</p><p>直到最近Java语言被广泛接受,才使垃圾收集进入主流,并在大型系统中得到应用。</p><p><br></p><p>开发者对垃圾收集持怀疑态度,原因有二:吞吐量和延迟。</p><p>也就是说,他们担心收集垃圾会减慢其系统的性能,或者导致长时间的暂停,或者两者兼而有之。</p><p>计算能力的大幅提高并没有消除这些担忧,因为这些担忧通常会被内存需求的相应增加所抵消。</p><p><br></p><p>分代垃圾收集技术可以解决这两个性能问题。</p><p>他们根据对象的年龄将堆分成几代。将集中收集在 “年轻 “一代的垃圾会增加吞吐量,因为(在大多数程序中)年轻的对象更有可能是垃圾,所以每单位的收集工作可以回收更多的可用空间。</p><p>由于年轻一代相对于总的堆大小来说通常是很小的,所以年轻一代的收集工作通常是短暂的,从而解决了延迟的问题。</p><p>然而,在足够多的年轻一代收集中存活下来的对象被认为是长寿命的,并被 “推广 “到老一代。</p><p>即使老一代通常比较大,但最终还是会被填满,需要垃圾回收。老一代收集的延迟和吞吐量与全堆收集类似,因此,分代技术只能推迟而不能解决这个问题。</p><p><br></p><p>在本文中,我们提出了一种垃圾收集算法,该算法被设计成工作在分代内存系统的老年代。</p><p>它试图减少最坏情况下的垃圾收集暂停时间,同时利用分代系统的优势。</p><p>它是对Boehm等人的 “近似并行 “算法的改编。</p><p>它通常与突变器(mutator)并发运行,只是偶尔会短时间暂停突变器(mutator)。</p><h3 id="1-1-术语说明"><a href="#1-1-术语说明" class="headerlink" title="1.1 术语说明"></a>1.1 术语说明</h3><p>在本文中,如果一个收集器能够与突变器(mutator)交错运行,或者真正地并发执行,或者通过小的增量工作即可以在频繁的操作(如对象分配)上搭载,我们称它为并发收集器。</p><p>相对于并行收集器而言,并行收集器使用多个合作线程来完成收集,因此可以在共享内存多处理器上实现并行加速。</p><p><br></p><p>不幸的是,这个术语与Boehm等人使用的术语发生冲突,因为他们用 “并行 “来表示我们命名为 “并发 “的概念。</p><p>这个选择是歧义的;一个当地传统导致了我们的选择。因此,我们用 “近似并发 “来表示Boehm等人所说的 “近似并行”。</p><h3 id="1-2-论文概览"><a href="#1-2-论文概览" class="headerlink" title="1.2 论文概览"></a>1.2 论文概览</h3><p>第2节简要介绍了我们的实现和实验所基于的平台,第3节介绍了最初的近似并发算法。第4节描述了我们对该算法的调整,第5节包含了我们为了评估我们的实现而进行的实验结果。</p><p>最后,第6节给出了增量垃圾收集器的相关工作,第7节给出了结论和未来工作。</p><h2 id="2-实验平台"><a href="#2-实验平台" class="headerlink" title="2. 实验平台"></a>2. 实验平台</h2><p>Sun Microsystems Laboratories Virtual Machine for Research,以下简称ResearchVM,是Sun Microsystems开发的高性能Java虚拟机。</p><p>这个虚拟机之前被称为 “Exact VM”,并已被整合到产品中;例如,用于Solaris操作环境的Java 2 SDK (1.2.1 05)生产版1就采用了优化的即时编译器[8]和快速同步机制[2]。</p><p><br></p><p>更为重要的是,它具有高性能的精确(即非保守[6],也称为精确)内存管理[1]。</p><p>内存系统与虚拟机的其他部分通过一个定义良好的GC接口[27]分开。</p><p>这个接口允许不同的垃圾收集器被 “接入”,而不需要改变系统的其他部分。</p><p>目前已经构建了多种实现该接口的收集器。</p><p>除了GC接口之外,还有第二层,称为分代框架,方便实现分代垃圾收集器。</p><p><br></p><p>其中一位作者学会了这些接口,并在几周内实现了一个垃圾收集器,所以有一些证据表明,实现上述接口是比较容易的。</p><h2 id="3-近似收集"><a href="#3-近似收集" class="headerlink" title="3. 近似收集"></a>3. 近似收集</h2><p>Boehm等人提出的最初的近似并发算法,是一种并发的 “三色 “收集器。</p><p>它使用写屏障使堆对象的字段更新,使包含对象呈灰色。</p><p>它的主要创新之处在于,它通过允许根位置(globals、栈、寄存器)(通常比堆位置更新更频繁)被写入,而不使用屏障来维持三色不变,从而以完全的并发性换取更好的吞吐量。</p><p>算法会暂停突变器来正确处理根基(Root)的问题,但通常只是短时间的。详细来说,该算法由四个阶段组成:</p><ol><li><p><strong>初始标记暂停</strong>。暂停所有的突变器,并记录所有从系统的根(globals、栈、寄存器)直接到达的对象。</p></li><li><p><strong>并发标记阶段</strong>。恢复突变器操作。同时,启动并发标记阶段,标记可触及对象的闭包。这个闭包不保证标记结束时,能标记到所有可达到对象,因为突变器对引用字段的并发更新可能会阻止标记阶段标记一些存活对象。</p><p>为了处理这个复杂的问题,算法还安排跟踪堆对象中引用字段的更新。这是突变者和垃圾收集器之间唯一的互动。</p></li><li><p><strong>最后标记暂停</strong>。再次暂停突变器,从GC Roots开始标记,完成标记阶段,将标记对象中修改后的引用字段视为附加Roots。由于这类字段只包含并发标记阶段可能没有观察到的引用对象,这就确保了最后的过渡性闭包包括了最后标记阶段开始时可以到达的所有对象。它还可能包括一些在标记后变得无法触及的引用。这些垃圾将在下一个垃圾收集周期收集。</p></li><li><p><strong>并发收集阶段</strong>。再次恢复突变器,并同时扫过堆,去掉未标记的对象。必须注意不要重新分配新对象。在这个阶段,这可以通过分配 “活 “的对象(即标记)来实现。(没看懂,盲猜大意是这个阶段的JVM分配的对象要自带Mark标记,避免被并发收集掉)</p></li></ol><p>这个描述从Boehm等人给出的描述中抽象出来:跟踪单个修改字段是最细的跟踪粒度;注意这个粒度可以变粗,可以通过降低精度来换取更高效(或方便)的修改跟踪。</p><p>事实上,Boehm等人使用了相当粗的粒度,在4.2节中讨论过。</p><p><br></p><p>该算法假设堆对象中引用字段的在并发收集阶段变化的概率较低,否则,最后的标记阶段将不得不重新扫描许多脏的引用字段,导致长时间的、可能是破坏性的暂停。</p><p>即使有些程序会打破这种假设,但Boehm等人报告说,在实践中这种技术表现良好,特别是对于交互式应用。</p><h3 id="3-1-一个具体案例"><a href="#3-1-一个具体案例" class="headerlink" title="3.1 一个具体案例"></a>3.1 一个具体案例</h3><p><img src="/images/论文翻译cms/1.png" alt="image-20200617230346559"></p><p>图1说明了近似并发算法的操作。</p><p>在这个简单的例子中,堆中包含7个对象,并被分成4页。</p><p>在最初的标记暂停期间(未图示),所有4页都被标记为干净,对象a被标记为有效,因为它可以从线程堆栈中到达。</p><p><br></p><p>图1a显示了并发标记阶段中途的堆。</p><p>物体b、c和e已被标记。</p><p>此时,突变器执行两次更新:对象g放弃对d的引用,对象b的引用字段指向c,用对d的引用覆盖,这些更新的结果如图1b所示。</p><p>另外要注意的是,更新后导致第1页和第3页被弄脏了。</p><p><br></p><p>图1c显示了并发标记阶段结束时的堆。</p><p>显然,标记是不完整的,因为一个标记的对象b指向一个未标记的对象d。</p><p>在最终标记暂停期间,将处理这个问题:重新扫描脏页(第1页和第3页)上的所有标记对象。</p><p>这就导致b被扫描,从而对象d被标记。</p><p>图1d显示了最后标记暂停后堆的状态,现在标记已经完成。</p><p>随后将同时进行扫尾阶段,并将回收未标记的对象f。</p><p><br></p><p>在垃圾收集周期开始时无法到达的对象,如f,保证被回收。</p><p>但是,像c这样的对象,如果在一个收集周期内变得无法到达,就不能保证在该周期内收集,而是会在下一个周期内收集。</p><h2 id="4-分代系统中近似并发垃圾收集"><a href="#4-分代系统中近似并发垃圾收集" class="headerlink" title="4. 分代系统中近似并发垃圾收集"></a>4. 分代系统中近似并发垃圾收集</h2><p>本节详细介绍了我们的分代近似并发垃圾收集器,并记录了我们在其设计和实现过程中的一些具体方案。</p><p>我们也会尽量提出可能更适合不同系统的替代解决方案。</p><p>我们设计的垃圾收集器大部分方面都与使用该垃圾收集器作用于哪一代无关,我们将首先讲解这些。</p><p>稍后,我们将描述在分代的背景下使用该收集器的具体特性。</p><h3 id="4-1-内存分配器"><a href="#4-1-内存分配器" class="headerlink" title="4.1 内存分配器"></a>4.1 内存分配器</h3><p>对于老年代,ResearchVM的默认配置使用的是标记扫描收集,并进行压实(compaction)传递,以实现后面的高效分配。</p><p>我们将把这个收集器的实现称为mark-compact。</p><p>压实也带来对象重定位问题,即需要更新对重定位对象的引用,这种引用更新很难并发进行。</p><p>因此,近似并发垃圾收集不会尝试对象重新定位。</p><p>因此,它的分配器使用了自由列表(free lists),按对象的大小进行隔离,对于小对象(最多100个4字节的字),每个大小的对象有一个自由列表,对于大对象,每个大小的组有一个自由列表(这些组是用类似斐波那契的序列选择的)。</p><p><br></p><p>有人会觉得可以使用一个更智能的分配器,在速度/碎片化之间进行更好的权衡。</p><p>事实上,Johnstone和Wilson宣称,分离式自由列表分配策略是导致最严重的碎片化的策略之一。</p><p>然而,这项工作假设了显式的 “在线 “再分配,正如C的malloc/free接口所代表的那样。</p><p>在 “离线 “的垃圾收集器中,用一个遍历整个堆的清扫阶段来凝聚连续的空闲区域以减少碎片,是比较容易和有效的。</p><h3 id="4-2-使用卡表"><a href="#4-2-使用卡表" class="headerlink" title="4.2 使用卡表"></a>4.2 使用卡表</h3><p>分代垃圾收集需要跟踪从老一代对象到年轻一代对象的引用。</p><p>这对于正确性来说是必要的,因为一些年轻一代的对象可能是无法到达的,除非通过这种引用。</p><p>需要一个比简单地遍历整个老一代更好的方案,因为那样会使年轻一代集合的工作类似于整个堆的集合的工作。</p><p><br></p><p>使用了几种方案来跟踪这种老年代到新生代的对象引用后,我们在成本/精度上进行了不同的权衡。</p><p>最终ResearchVM的分代框架(见第2节)使用卡表来进行这种跟踪。</p><p>卡表是一个数组,每个条目对应于堆的一个子区域,称为卡。</p><p>通过突变器代码对堆对象内的引用字段的每一次更新都会执行一个写屏障,将包含引用字段的卡表条目对应的卡设置为脏值,</p><p>在编译后的突变器代码中,我们采用了Ho ̈lzle提出的两指令写屏障,卡表更新的额外代码可以相当高效。</p><p><br></p><p>我们如此设计的一个根本原因是利用这种让人开心的巧合,即这种基于卡表的高效写屏障,几乎不需要修改就可以用来执行近似并发收集所需的引用更新跟踪。因此,对老年代使用近似并发收集,除了已经产生的分代写屏障外,不会增加额外的突变器开销。</p><p><br>Boehm等人使用虚拟内存保护技术来跟踪指针更新,粒度是虚拟内存页粒度:一个 “脏 “页包含一个或多个被修改的引用字段。与这种方法相比,使用基于卡表的写屏障有几个优点。</p><ul><li><strong>开销少</strong>:在大多数操作系统中,调用自定义处理程序进行内存保护陷阱的成本相当高。Hosking和Moss发现使用五条指令的卡片标记屏障比基于页保护的屏障更有效率;ResearchVM中使用的两条或三条指令的实现仍然会更有效率。</li><li><strong>更细粒度的信息</strong>。卡表的粒度可以根据精度/空间开销的权衡来选择。虚拟内存保护方案中的 “卡表大小 “就是页面大小,选择这个大小是因为系统优化,比如磁盘传输的效率,而这些系统属性与垃圾收集的考虑完全无关。一般来说,这些优化会导致页面大于参考更新跟踪的最佳值,通常至少为4KB。相比之下,ResearchVM的卡片大小是512字节。</li><li><strong>更准确的类型信息</strong>。ResearchVM只在一张卡上的引用字段更新时才会对该卡进行脏化处理。基于虚拟内存的系统无法区分标量字段和引用字段的更新,因此可能会弄脏更多的页面,而不是跟踪修改后的指针所需要的。此外,他们的方法在其他地方也是保守的:它假设所有的字(理解是char*)都是潜在的指针。</li></ul><p>Hosking、Moss和Stefanovic详细讨论了软件和基于页保护的屏障实现之间的权衡。他们的基本结论是,软件机制比使用虚拟内存保护的机制更有效。</p><p><br>平心而论,我们应该注意到,Boehm等人的系统试图满足我们系统中不存在的另一个约束:在没有编译器支持的情况下,完成不合作语言(C和C++)的垃圾收集。这个约束导致了保守的收集方案[6],而近似并发扩展正是基于这个方案,同时也有利于使用虚拟内存技术进行引用更新跟踪,因为这个技术不需要修改突变器代码。</p><p><br></p><p>根据分代近似并发算法的需要,对卡表进行调整是很直接的。事实上,正如上面所讨论的,写屏障和卡表数据结构都没有改变。然而,我们仔细地注意到,卡表被两个可能同时运行的垃圾收集算法以微妙的不同方式使用。</p><ol><li><p>近似并发算法需要跟踪自当前标记阶段开始以来更新的所有引用</p></li><li><p>而年轻一代的垃圾收集需要识别所有老年代到年轻代的指针。</p></li></ol><p>在基础分代系统中,年轻代集合扫描所有脏的旧的卡表,搜索进入年轻代的指针。如果没有找到,在下一次采集中就不需要扫描这张卡,所以这张卡被标记为干净。在年轻代垃圾收集清理脏卡之前,必须记录该卡已被修改的信息,以备近似并发收集器使用。</p><p><br></p><p><img src="/images/论文翻译cms/2.png" alt="image-20200617230346559"></p><p>这是通过添加一个新的数据结构,the mod union table来实现的,如图2所示,之所以如此命名,是因为它代表了并发标记过程中发生的每一次年轻代垃圾收集之间修改的卡表条目的联合。</p><p>卡表本身在ResearchVM中每个条目都包含一个字节;这就允许使用字节存储来快速实现写屏障。</p><p>另一方面,mod-union表是一个位向量,每个条目只有一个位。</p><p>因此,它在卡表之外增加了很少的空间开销,并且还能在卡表条目很少的时候快速遍历找到修改过的卡表条目。</p><p>我们在mod union和卡片表上维护了一个不变性:任何包含自当前并发标记阶段开始以来被修改的引用的卡表条目,要么在mod union表中被设置了位,要么在卡片表中被标记为dirty,或者两者都有。</p><p>这个不变性是由年轻代的集合来维护的,它在扫描这些脏卡之前,会设置卡表中所有脏卡的mod union位。</p><h3 id="4-3-标记对象"><a href="#4-3-标记对象" class="headerlink" title="4.3 标记对象"></a>4.3 标记对象</h3><p>我们的并发垃圾收集器使用了一系列外部的标记位(bitmap)。</p><p>对于堆中每4个字节,这个bitmap使用1bit与之对应。</p><p>这种使用外部标记位,而不是对象头中的内部标记位,可以防止突变器和收集器同时使用对象头,对对方造成干扰。</p><p><em>译者:这个用来表示地址,间接的表示某个对象</em></p><p><br></p><p>根扫描是一个有趣的设计,因为它受到两个相互矛盾的问题的影响。</p><p>如我们在第3节中所描述的,近似并发算法扫描根的时候,突变器会被暂停。</p><p>因此,我们希望这个过程尽可能的快。</p><p>另一方面,任何标记过程都需要对已经标记但尚未扫描的对象集(以下简称待扫描集)进行记录。</p><p>通常,这个集合是用堆外的一些数据结构来表示的,例如堆栈或队列。</p><p>一个最小化STW时间的策略是简单地把所有从GCRoots可以直接到达的对象放在这个外部数据结构中。</p><p>然而,由于垃圾回收的目的是在内存是稀缺资源的情况下回收内存,所以这种外部数据结构的大小始终是重要的关注点。</p><p>由于Java语言是多线程的,所以根集可能包括许多线程的寄存器和堆栈。</p><p>在我们的分代系统中,除了被收集的对象外,其他代的对象也被认为是根。</p><p>所以根集的内存占用可能相当大。</p><p><em>译者:这就导致GCRoots可直接到达的对象可能相当多,需要消耗太多的额外内存,所以这个方案不适合。</em></p><p><br></p><p>另一种能使空间成本最小化的策略是,在处理根时,立即标记所有可从根到达的对象。</p><p>许多对象可能是可以从根部到达的,但我们每次都将这些对象放在待扫描集合中,在任何给定的时间都将这个数据结构(因为根部)所需的空间最小化。</p><p>虽然这种策略适用于非并发收集,但与大部分并发算法不兼容,因为它将所有标记作为根扫描的一部分来完成。</p><p><em>译者:就是迭代GCRoot,慢慢迭代完,局限性很大</em></p><p><br></p><p>我们使用了这两种方法之间的折衷方案。</p><p><em>译者:其实不算是这种方案,就是优化了第一种方案的存储模式</em></p><p>折衷方案是利用外部标记位的优点。</p><p>根扫描只是简单地标记从根部直接到达的对象。</p><p>通过使用标记位向量来表示待扫描的集合,这最大限度地减少了STW的时间,并且没有额外的空间成本。</p><p>因此,并发标记阶段包括对堆的线性遍历,搜索标记位,找出活对象。这个过程的成本与堆大小成正比,和活的数据量无关,但由于清理阶段的存在,整体算法的复杂度已经不低)。</p><p>每找到一个活对象cur,我们就把cur推到一个待扫描的栈中,然后进入一个循环,从这个栈中弹出对象,并扫描它们的引用,直到栈为空。</p><p>对一个引用值ref的扫描过程(进入近似并发阶段)工作原理如下:</p><p><img src="/images/论文翻译cms/3.png" alt="image-20200617230346559"></p><ul><li><p>如果 ref 指向 cur 的前面,那么对应的对象就会被简单地标记,而不会被推到堆栈上;它将在线性遍历的后面被访问。</p></li><li><p>如果 ref 指向 cur 后面,则对应的对象既被标记,又被推到栈上。</p></li></ul><p>图3说明了这个过程。标记遍历刚刚发现了一个标记对象c,其地址成为cur的值。扫描c发现了两个引用,分别是a和e。对对象e进行简单的标记,因为它的地址在cur之后。对象a在cur之前,所以它既被标记又被扫描。这将导致b,它也在cur之前,所以它也被标记和扫描。然而,对象b对d的引用只导致d被标记,因为它在cur之后,因此将在后面的遍历中被扫描。</p><p><br></p><p>这种技术减少了对待扫描栈的需求,因为从根集直接到达的对象永远不会超过一个。</p><p>这种方法的一个潜在缺点是线性遍历搜索活的对象,这使得标记的算法复杂度包含一个与堆的大小成正比的成分,而不仅仅是指针图中的节点和边的数量。</p><p>只有当搜索标记对象的成本大于扫描它们时的成本时,这才是一个实际的问题,但是只有在活对象稀疏的情况下才会出现这种情况。</p><p>需要注意的是,如果活对象是稀疏的,使用位图可以通过检测位向量中的零字,有效地跳过没有活物体的大区域。Printezis在磁盘垃圾收集器中使用了类似的技术。</p><h3 id="清理阶段"><a href="#清理阶段" class="headerlink" title="清理阶段"></a>清理阶段</h3><p>当并发标记阶段完成后,清理阶段必须识别所有不可达的对象,回收他们的内存占用。</p><p>内存分配过程通常会切分一个空闲的内存块,分成2块,包括已经分配的内存块和剩下的空闲内存块,切分后的2块都比之前的空闲内存块小。</p><p>因此,为了防止平均内存块的大小不断的减小,清理阶段还必须执行某种形式的合并,即把一系列空闲内存块合并成一个大的空间内存块。</p><p><br></p><p>在一个非并发垃圾收集器中,如果使用了free-list的方案,通过丢弃现有的free-list,在清理阶段重新构造他们的方法,清理和合并就很容易完成。</p><p>但是在并发收集器中,这种方式就行不通了,因为在并发收集器中,在并发收集阶段,也要能够分配新的内存出去。</p><p><br></p><p>并发分配在两个方面使清理变得复杂。</p><p>首先,一个突变器线程可能正试图从一个空闲列表中分配内存,而清理进程正试图向该空闲列表中添加。</p><p>这种问题用互斥锁相当容易处理。</p><p>更微妙的是,清扫进程也可能与突变器线程竞争从空闲列表中移除内存块。</p><p>考虑一个块a、b和c相邻的情况。块b在空闲列表上;块a和块c曾被分配到包含对象,但都被发现无法到达。</p><p>我们希望将凝聚的块abc放到一个自由列表上。</p><p>然而,要做到这一点,我们必须首先将块b从它的空闲列表中删除,否则块b可能会被清理线程和分配线程同时使用。</p><p><br></p><p>互斥锁仍然可以处理这种竞争。</p><p>但是,请注意,这种情况对堆的自由列表数据结构提出了新的要求:</p><p>我们必须能够从其自由列表中删除一个任意块。分配可以从自由列表中删除对象,但只能在列表的头部删除。</p><p>当自由块只从头部被删除时,单链自由列表是高效的,而任意块的删除则更倾向于双链自由列表,它允许在恒定而非线性的时间内完成这一操作。</p><p>请注意,这不会增加任何空间开销,因为当一个块被分配时,相同的内存被用来包含对象信息,而当它没有被分配时,自由列表链接被用来包含对象信息。</p><h3 id="4-5-垃圾收集线程"><a href="#4-5-垃圾收集线程" class="headerlink" title="4.5 垃圾收集线程"></a>4.5 垃圾收集线程</h3><p>我们的系统使用一个专门的垃圾收集线程。</p><p>这种方法使我们可以利用多个CPU。</p><p>例如,一个单线程程序可以在双处理器机器上运行,并且大部分垃圾收集工作在第二个处理器上完成。</p><p>同样,收集活动可以在突变器处于非活动状态时进行,例如,在执行I/O时。</p><p>相比之下,Boehm等人以增量的方式形成收集功能,”搭载 “在由突变器线程执行的频繁操作上,例如对象分配。</p><p>我们认为选择这种方法是为了增加可移植性。</p><p><br></p><p>我们还决定将垃圾收集器线程标记为 “假的 “突变器线程,这意味着它在年轻代收集过程中被暂停。这有两个好处:它不会减慢需要快速收集的年轻一代的收集速度,而且它能最大限度地减少与系统其他部分的同步。</p><h3 id="4-6-与年轻代垃圾回收的交互"><a href="#4-6-与年轻代垃圾回收的交互" class="headerlink" title="4.6 与年轻代垃圾回收的交互"></a>4.6 与年轻代垃圾回收的交互</h3><p>有一些方法可以让我们的近似并发垃圾收集器被优化或修改为在分代收集器中为老年代工作。</p><p>首先,我们认识到,对于大多数程序来说,老年代的大部分分配是由于年轻一代的晋升。</p><p>(其余的是由老年代中的突变器 “直接 “分配,这通常只发生在太大而无法在年轻一代中分配的对象上)。</p><p>而晋升发生在mutator线程和并发垃圾收集器线程被暂停的时候,这一点简化了垃圾收集器的设计。</p><p>我们利用这种简化,在年轻代垃圾收集期间支持线性分配模式。</p><p>线性分配比基于自由列表的分配快得多(特别是当使用双向自由列表时),因为比较和修改的指针较少。</p><p>当线性分配模式生效时,只要有足够大的自由块存在,我们就会对小的分配请求保持线性分配。</p><p>这大大加快了晋升的分配速度,而晋升可能是年轻一代垃圾收集成本的主要组成部分。</p><p><br></p><p>在ResearchVM的默认配置中,使用压实(compact)收集方式的老年代的方案简化了年轻代收集所需的一个函数的实现。</p><p>Cheney式复制收集最优雅的一个方面是,仍然要扫描的对象集是连续的。</p><p>在一个分代系统中,一些from空间的对象可能被复制到to空间,而另一些可能被晋升到老年代,被晋升的对象也是待扫描集的一部分。</p><p>当老年代的系统使用压实,从而使用线性分配时,被晋升但尚未扫描的对象集是连续的。</p><p>但是,在非压实收集器中,被晋升的对象可能不连续。</p><p>这使得定位它们以便扫描它们的问题变得复杂。</p><p><br></p><p>我们通过用一个双向链表来表示被晋升但未扫描的对象集来解决这个问题。</p><p>每一个被晋升的对象都是从年轻一代的当前from区域中推广出来的,而对象的from区域版本包含一个转发指针,指向对象在老年代的新地址。</p><p>这些被推广对象的to空间副本被用作链接列表的 “节点”。</p><p>转发指针表示该集合的元素,随后的头字被用作 “下一个 “字段。</p><p>(没看懂这段)</p><h3 id="4-7-控制启发式方法"><a href="#4-7-控制启发式方法" class="headerlink" title="4.7 控制启发式方法"></a>4.7 控制启发式方法</h3><p><img src="/images/论文翻译cms/4.png" alt="image-20200617230346559"></p><p>图4显示了垃圾收集器线程执行的伪代码。</p><p>第一条语句初始化initFrac,即启动新的收集周期的堆占用阈值。</p><p>在ResearchVM收集器框架中,用户指定一个所需的堆占用率(heapOccupancyFrac)来控制堆使用。</p><p>在程序的稳定状态下,这个分数在一个收集周期结束时被占用;</p><p>当空闲空间的分数 allocBeforeCycleFrac 被分配完毕后,我们就会启动一个新的周期。</p><p><br></p><p>线程定期唤醒(SLEEP_INTERVAL设置为50毫秒)并检查堆内存占用率。如果已经达到initFrac,则新的循环开始,初始标记暂停。然后是并发标记阶段,接着是并发预清理(见4.9节),最后是标记暂停(见3节)。最后,这个循环由并发清除阶段完成,该阶段回收所有未标记的对象。</p><p>实际上,测试会保护最后标记暂停和并发清扫的执行:如果标记的堆占用率已经太高,清除将无法回收足够的存储空间来证明其消耗成本。所以,如果标记的堆的分数超过98%,这两步都不会执行。</p><p><em>译者:难道说的就是CMS并发失败时,调用单线程垃圾收集器,STW的进行FullGC</em></p><p><br></p><p>需要注意的是,”最大暂停时间 “本身并不能充分衡量垃圾收集器对系统的影响。考虑一个增量系统,将GC暂停时间限制在一个相对较小的最大值,比如50毫秒。</p><p>在一个单核处理器上,这可能仍然允许垃圾收集器大幅度的影响系统:如果在每个GC暂停之间只做了10毫秒的突变器工作,用户将观察到程序在垃圾收集过程中只以其正常速度的20%运行。</p><p>我们在后面介绍的测量是在多处理器上进行的,有一个额外的处理器可用于垃圾收集工作,因此忽略了这个问题。然而,该实现确实有一套启发式方法,旨在控制单处理器上的GC入侵。</p><p>These heuristics view concurrent collection as a race in which the collector is trying to finish collection before mutator activity allocates the free space available at the start of the collection。</p><p>(这句好难翻,原文摘出来你们自己看吧)</p><p>收集器线程以一系列的步骤完成标记和清除,每一步之后都会在一个由收集和分配的相对进度决定的时期内睡觉。突变器填充内存的速度越快,那么收集活动发生的频率就越高。</p><p><br></p><p>偶尔,尽管有这些启发式方法或有额外的处理器可用,但收集器线程会 “输掉比赛”:突变器线程等待一个并发收集才能继续运行。</p><p>当这种情况发生时,收集线程的剩余部分就会被非并发地执行。这将导致更长的停顿,但通常比在STW的情况下执行整个垃圾收集更短。</p><p>另外,我们可以选择在这种情况下扩大堆;</p><p>我们正在探索启发式方法来控制这种行为。</p><h3 id="4-8-并发问题"><a href="#4-8-并发问题" class="headerlink" title="4.8 并发问题"></a>4.8 并发问题</h3><p>我们已经提到了几个并发问题;例如,上一节讨论了老年代分配和清理之间的并发管理。</p><p>本节将探讨仍然存在的问题。</p><p>正如前面所讨论的,由于我们希望大部分的老年代分配是因为在年轻代收集过程由于对象晋升完成的,因此在年轻代收集过程中暂停老年代收集的决定(见第4.5节)处理了许多这样的问题。</p><p>但并不是所有的,突变器线程仍然可能偶尔直接在老年代中分配对象,而且在其数据结构不一致的关键部分,老一代收集线程必须不被打断进行年轻代收集。</p><p><br></p><p>老年代的对象分配,无论是直接分配还是晋升分配,都会带来两个问题。</p><p>首先,如果后台垃圾收集器处于清理阶段,我们不仅要保证对空闲列表的访问一致,还要保证对标记位的访问一致。</p><p>如果在清理期间分配了空闲块b,我们必须防止清理线程将b认为是已分配但未标记的块。</p><p>因此,在清除过程中,我们使用分配活的策略:已分配的块在位图中被标记为活块。</p><p>这种标记必须与清除线程对标记位的检查相协调。</p><p><br></p><p>我们也会在并发标记期间进行活对象分配,但原因有些不同。</p><p>一个可行的替代策略是分配未被标记的对象。</p><p>最后的标记暂停阶段仍然会达到一个正确的闭包:如果一个在标记期间分配的对象在标记结束时是可以到达的,那么存在着从根到标记结束时的对象的一些路径。</p><p>每一条这样的路径要么完全由标记期间分配的未标记对象组成,要么至少包含一个标记对象。</p><p>在前一种情况下,最后的标记暂停肯定会标记对象。在后一种情况下,考虑路径中的最后一个标记对象。它一定是在扫描后(因此在它被标记后)被修改成为路径的一部分,否则路径上的下一个对象将被标记为扫描的一部分。</p><p>因此,使其成为路径的一部分的修改使该对象成为脏对象。</p><p>所以,路径上最后一个被标记的对象一定是脏的,也就是说,并发标记期间分配的对象,最后肯定会被标记到。</p><p><br></p><p><em>译者:这里还有一段,大致意思是在并发标记阶段,即使新分配的对象是unmark的,也没事。但是由于在重新标记阶段会浪费一些时间进行mark,所以系统设计中,新分配的对象还是设置为marked的</em></p><h3 id="4-9-并发预清理"><a href="#4-9-并发预清理" class="headerlink" title="4.9 并发预清理"></a>4.9 并发预清理</h3><p>我们在第3节中指出,高效的近似并发收集需要堆对象的引用字段具有较低的突变率。</p><p>一个突变率高的程序会创建许多脏对象,这些脏对象必须在最后的标记暂停期间重新扫描,这使得这个暂停对程序的影响很大。</p><p><br></p><p>我们已经开发了一种叫做并发预清理的技术,可以部分缓解这个问题。</p><p>观察到的情况相当简单:在最后的标记阶段,针对脏对象所做的很多工作都可以在之前进行,只要以一种谨慎的方式完成,以保持正确性。</p><p>在并发标记结束时,有些对象集是脏的。在不停止突变器的情况下,我们找到所有这样的脏对象;对于每一个对象,我们将该对象进行标记,然后将对应的卡表标记为clean。</p><p>由于任何进一步的突变器更新仍然会弄脏相应的对象,并且需要在最后的标记阶段对其进行处理,因此保持了正确性。</p><p>然而,我们希望的是,并发清理过程所花费的时间将大大少于之前的并发标记阶段,允许更少的时间给突变器弄脏对象。</p><p>因此,最后的标记阶段需要做的非并发工作就会减少。第5.3节将衡量这种技术的有效性。</p><p><br></p><p>在进一步的实验中(不包括在这些措施中),我们以两种方式扩展了并发预清洗。</p><p>首先,预清洗的最初实现只在mod-union表上工作,假设两个年轻代垃圾收集之间反映在卡片表上的修改数量相对较少。</p><p>事实证明,这对于具有高指针突变率的现实世界程序来说并不正确。因此,我们将该技术扩展到也对卡表进行预清理。</p><p>这需要创建一个新的卡表值:脏卡被改为预清洁,它被分代垃圾收集认为是脏的,但在最后的标记阶段被认为是干净的。</p><p>其次,只要遇到的脏卡数量减少一个足够的系数(目前是1/3),或者直到这个数量足够小(目前是小于1000),我们就会迭代预清洗过程。</p><p>这两个扩展在满足5.4节讨论的电信应用需求方面是有用的。</p><h2 id="五-实验结果"><a href="#五-实验结果" class="headerlink" title="五. 实验结果"></a>五. 实验结果</h2><p>略了,实在是不想翻译了</p>]]></content>
<summary type="html">
<p><strong>A Generational Mostly-concurrent Garbage Collector</strong><br>(题目不知道咋翻,算了),论文发布在2000年</p>
<p>注:本文讲的不完全是CMS垃圾收集器,其实讲的是并发垃圾收集器的思路。</p>
<p>而Java中的并发垃圾收集器包含CMS和G1。</p>
</summary>
<category term="论文翻译" scheme="https://blog.lovezhy.cc/categories/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91/"/>
<category term="JVM" scheme="https://blog.lovezhy.cc/tags/JVM/"/>
</entry>
<entry>
<title>从Mpsc到RingBuffer(三)- Disruptor</title>
<link href="https://blog.lovezhy.cc/2020/08/30/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%B8%89%EF%BC%89-%20Disruptor/"/>
<id>https://blog.lovezhy.cc/2020/08/30/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%B8%89%EF%BC%89-%20Disruptor/</id>
<published>2020-08-29T16:00:00.000Z</published>
<updated>2020-09-05T15:24:35.874Z</updated>
<content type="html"><![CDATA[<h1 id="一-前言"><a href="#一-前言" class="headerlink" title="一. 前言"></a>一. 前言</h1><p>Disruptor几乎是每个Java开发绕不过去的坎,其实我想学习这个框架很久了,之前打开看了几次,但是有点复杂就放弃了。</p><p>这一次看到了Mpsc,心里在构思多生产者多消费者的队列怎么怎么做,自然就想到了RingBuffer。</p><p>有了上文的基础,下面我们就Disruptor来看看多生产者多消费者是怎么实现的。</p><p>注意:这个文章并不是特别的分析Disruptor是怎么实现高性能的,诸如网上说的那些伪共享之类,</p><p>就是带大家看看源码实现。</p><a id="more"></a><h1 id="二-使用"><a href="#二-使用" class="headerlink" title="二. 使用"></a>二. 使用</h1><p>先定义Event和它的Factory,就是承载在队列中的元素</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LongEvent</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> value;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">set</span><span class="params">(<span class="keyword">long</span> value)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.value = value;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LongEventFactory</span> <span class="keyword">implements</span> <span class="title">EventFactory</span><<span class="title">LongEvent</span>> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> LongEvent <span class="title">newInstance</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> LongEvent();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>之所以要定义这2个,是因为RingBuffer在初始化的时候,会为数组中的每个元素预先分配Event。</p><p>这样我们加入元素的时候,实际上RingBuffer会直接返回LongEvent给你,你要做的就是把Value给Set进去就行了。</p><p>下面你要定义EventHandler,就是处理这个事件的类,重写他的onEvent方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">LongEventHandler</span> <span class="keyword">implements</span> <span class="title">WorkHandler</span><<span class="title">LongEvent</span>> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onEvent</span><span class="params">(LongEvent longEvent)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> System.out.println(<span class="string">"Event: "</span> + event);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面就可以把整个Disruptor跑起来了:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">public static void run() {</span><br><span class="line"> EventFactory<LongEvent> eventFactory = new LongEventFactory();</span><br><span class="line"> ExecutorService executor = Executors.newFixedThreadPool(3); //3个线程</span><br><span class="line"> int ringBufferSize = 1024 * 1024; // RingBuffer 大小,必须是 2 的 N 次方;</span><br><span class="line"> Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(eventFactory,</span><br><span class="line"> ringBufferSize, executor, ProducerType.SINGLE,</span><br><span class="line"> new YieldingWaitStrategy());</span><br><span class="line"> WorkHandler<LongEvent> eventHandler = new LongEventHandler();</span><br><span class="line"> disruptor.handleEventsWithWorkerPool(eventHandler, eventHandler,eventHandler);</span><br><span class="line"> disruptor.start();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li><p>这个方法的原始定义是<code>public EventHandlerGroup<T> handleEventsWithWorkerPool(final WorkHandler<T>... workHandlers)</code></p><p>也就是说我们是传一个Handler数组进去,具体有什么区别,是disruptor使用的区别。</p><p>我们这里就假设,我们这么传入,就是有3个Consumer并发的去消费就好。</p></li></ol><p>生产事件并且写入RingBuffer:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();</span><br><span class="line"></span><br><span class="line"><span class="keyword">long</span> sequence = ringBuffer.next();<span class="comment">//请求下一个事件序号;</span></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> LongEvent event = ringBuffer.get(sequence);<span class="comment">//获取该序号对应的事件对象;</span></span><br><span class="line"> <span class="keyword">long</span> data = <span class="number">12</span>;</span><br><span class="line"> event.set(data);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> ringBuffer.publish(sequence);<span class="comment">//发布事件;</span></span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="三-Sequence和数组序号"><a href="#三-Sequence和数组序号" class="headerlink" title="三. Sequence和数组序号"></a>三. Sequence和数组序号</h1><p>这个类,单独领出来说,因为容易和Sequencer混淆。</p><p>其实我感觉这里的取名不是很好。</p><p>Sequence简单说就是个AtomicLong,主要就是个计数器。</p><p>但是它不做减法,只做加法。</p><p>这里所有用到Sequence的地方,无一例外的都是用来标志数组的位置的。</p><p>假设数组的长度是N,那么某一时刻Sequence指向的数组的第(seq & mask)个元素</p><p>其中mask = length - 1</p><h1 id="四-Sequencer"><a href="#四-Sequencer" class="headerlink" title="四. Sequencer"></a>四. Sequencer</h1><p>这里的Sequencer是核心组件,主要是为生产者使用,我们知道循环数组是维护在RingBuffer中的。</p><p>但是生产者原子占领Position,都在Sequencer中,在Sequencer中执行成功了,</p><p>就可以直接去RingBuffer得到相应的数组元素,往里面set数值。</p><p>对应这句话:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">long</span> sequence = ringBuffer.next();</span><br></pre></td></tr></table></figure><p>就是托管至Sequencer操作的。</p><p>正常我们来想,对ReadIndex需要维护自己的Seq,所有的消费者都只要一个就够了</p><p>但是Disruptor并不是,他为每个消费者都维护了一个Seq。</p><p>在消费者中的Seq表示从它的视角中的ReadIndex。</p><p><img src="/images/ringbuffer/9.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>如上图,我们有三个Consumer,对于每个Consumer而言,因为每次获取的一批的可消费事件不同,所以在他们眼中的ReadIndex也是不同的。</p><p>我们如果要知道整体的ReadIndex是啥,就是取这三个中最大的一个Seq就行。</p><p>在Sequencer初始化的时候,它会收集所有的Consumer的Seq,放在gatingSequences数组中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractSequencer</span> <span class="keyword">implements</span> <span class="title">Sequencer</span> </span>{</span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">volatile</span> Sequence[] gatingSequences = <span class="keyword">new</span> Sequence[<span class="number">0</span>];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面我们来看看<code>public long next(int n)</code>的具体实现。</p><h2 id="4-1-SingleProducerSequencer"><a href="#4-1-SingleProducerSequencer" class="headerlink" title="4.1 SingleProducerSequencer"></a>4.1 SingleProducerSequencer</h2><p>这个Sequencer表示的是单生产者,所以它的生产方法不用考虑线程并发的问题。</p><h3 id="next"><a href="#next" class="headerlink" title="next"></a>next</h3><p>他只要往下分配就行了,但是因为是循环数组,所以他需要考虑不能把还没来得及消费的Position给覆盖了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> <span class="keyword">long</span> nextValue = -<span class="number">1</span>;</span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">long</span> cachedValue = -<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">next</span><span class="params">(<span class="keyword">int</span> n)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> nextValue = <span class="keyword">this</span>.nextValue;</span><br><span class="line"> <span class="keyword">long</span> nextSequence = nextValue + n;</span><br><span class="line"> <span class="keyword">long</span> wrapPoint = nextSequence - bufferSize;</span><br><span class="line"> <span class="keyword">long</span> cachedGatingSequence = <span class="keyword">this</span>.cachedValue;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">long</span> minSequence;</span><br><span class="line"> <span class="keyword">while</span> (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))</span><br><span class="line"> {</span><br><span class="line"> LockSupport.parkNanos(<span class="number">1L</span>); <span class="comment">// <span class="doctag">TODO:</span> Use waitStrategy to spin?</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>.cachedValue = minSequence;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>.nextValue = nextSequence;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> nextSequence;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我说实话,不太好理解。写的真绕人。</p><p>这里的nextValue就是上一次Produce完的Position位置,而nextSequence表示我们要取下N个后的位置,</p><p>比如下图中n=4时,next和nextSequence的关系,而wrapPoint指的是上一级的位置。</p><p><img src="/images/ringbuffer/10.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>这里的cacheValue,其实就是minSequence的缓存,而minSequence的值是</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Util.getMinimumSequence(gatingSequences, next)</span><br></pre></td></tr></table></figure><p>就是所有消费者的Seq和nextValue中的最小的一个。</p><p>我们照着上面这个bufferSize=8的图,其中紫色的格子表示我们想要获取的,而红色格子表示还未被消费的。</p><p>我们申请成功,至少要保证这种关系的正确。</p><ol><li>WrapPoint要小于所有消费者的Seq</li></ol><p>但是为什么swapPoint还要小于nextValue呢?这个不是显而易见的吗?</p><p>其实我们n是有限制的,对于n的限制,也是不能大于bufferSize的长度的。</p><p><img src="/images/ringbuffer/11.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>比如上面这种情况,直接bufferSize + 3个position,导致swapPoint比next还大,显然是不行的。</p><p>所以条件2:</p><ol><li>wrapPoint要小于nextValue,也即保证n不能大于bufferSize。</li></ol><p>这里比较困惑的地方可能就是cachedGatingSequence这个值了,</p><p>这个值就是上述两个条件的缓存,这里缓存一下估计也是为了优化吧。</p><p>不过写的确实看不太明白。</p><p>注意这里的<code>AbstractSequencer</code>中的<code>Sequence cursor</code>这个变量。</p><p>这个就是我们说的Sequencer这个变量。</p><p>但是在Next方法中,完全没用到这个变量,而是直接用的<code>SingleProducerSequencer</code>中的<code>nextValue</code></p><p>但是在MultiProducerSequencer的实现中,确实强依赖<code>cursor</code>这个变量的。</p><p>怎么说呢,可以说设计得不是很友好吧。</p><p>我感觉<code>SinleProducerSequencer</code>其实也可以用<code>cursor</code>标记producer的位置,但是这样就引入了CAS,性能并没有那么好。</p><h3 id="publish"><a href="#publish" class="headerlink" title="publish"></a>publish</h3><p>获取next成功,下面就是publish操作了,让我们看看publish中做了什么</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">publish</span><span class="params">(<span class="keyword">long</span> sequence)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> cursor.set(sequence);</span><br><span class="line"> waitStrategy.signalAllWhenBlocking();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的publish比较简单,就是直接设置新的cursor指针就行。</p><p><img src="/images/ringbuffer/12.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>如果你看过上一篇文章,你可能会记得这张图,这里的cursor是大于两个Producer线程的。</p><p>但是WriteCursor之前的元素可能还没写入进入。</p><p>但是在本地的SingleProducer中,其实流程和这个还不太一致。</p><p><img src="/images/ringbuffer/13.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>在SingleProducerSequencer中,虽然Producer已经调用成功了next,但是writeCursor仍然停留在之前的位置,每次publish一个position的元素后,writeCusor就往前加一格。</p><p>所以使用SingleProducerSequencer时,消费者每次读取的位置中,元素肯定已经被写入了。不会出现读到空的情况。</p><h3 id="getHighestPublishedSequence"><a href="#getHighestPublishedSequence" class="headerlink" title="getHighestPublishedSequence"></a>getHighestPublishedSequence</h3><p>这个方法,其实是为了对照下面的MultiProducerSequencer而加的。</p><p>这个方法的原型是</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getHighestPublishedSequence</span><span class="params">(<span class="keyword">long</span> lowerBound, <span class="keyword">long</span> availableSequence)</span></span></span><br></pre></td></tr></table></figure><p>传入lowerBound和availableSequence,返回最大可用的Seq是什么。</p><p>显然就是给消费者使用的。</p><p>对于SingleProducer而言,显然是直接返回availableSequence就行。</p><h2 id="4-2-MultiProducerSequencer"><a href="#4-2-MultiProducerSequencer" class="headerlink" title="4.2 MultiProducerSequencer"></a>4.2 MultiProducerSequencer</h2><p>这个实现类是针对多个Producer。</p><p>这里多个Producer争夺Index的变量,就是<code>AbstractSequencer</code>中的<code>Sequence cursor</code>。</p><h3 id="next-1"><a href="#next-1" class="headerlink" title="next"></a>next</h3><p>来看看它的next方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">next</span><span class="params">(<span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">long</span> current;</span><br><span class="line"> <span class="keyword">long</span> next;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> current = cursor.get();</span><br><span class="line"> next = current + n;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">long</span> wrapPoint = next - bufferSize;</span><br><span class="line"> <span class="keyword">long</span> cachedGatingSequence = gatingSequenceCache.get();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (wrapPoint > cachedGatingSequence || cachedGatingSequence > current)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">long</span> gatingSequence = Util.getMinimumSequence(gatingSequences, current);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (wrapPoint > gatingSequence)</span><br><span class="line"> {</span><br><span class="line"> LockSupport.parkNanos(<span class="number">1</span>); </span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> gatingSequenceCache.set(gatingSequence);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (cursor.compareAndSet(current, next))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">while</span> (<span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> next;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是不是感觉很亲切。</p><p>其实这里和SinpleProducer的方法并没有什么大的区别。</p><p>唯一的问题就是这里的current是可能被多线程访问的,所以每次<code>wrapPoint > gatingSequence</code>,都要重新获取一次。</p><p>满足条件后,设置新的WriteIndex,要使用cursor的cas操作,防止多线程操作。</p><p><img src="/images/ringbuffer/14.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>注意这里,在获取next的时候,就已经改动了WriteCursor,所以和SingleProducerSequencer相比,这里有可能出现上图的状态。</p><p>如果消费者这个时候,直接读WriteCursor之前的元素,很可能还没有写入成功。</p><p>那怎么办呢?</p><p>在MultiProducerSequencer中,额外定义了一个数组</p><p><code>private final int[] availableBuffer;</code></p><p>这个数组表示的就是对应的循环数组中元素的写入情况。</p><p>具体得我们看看它的publish方法做了什么?</p><h3 id="publish-1"><a href="#publish-1" class="headerlink" title="publish"></a>publish</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">publish</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> sequence)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> setAvailable(sequence);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setAvailable</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> sequence)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> setAvailableBufferValue(calculateIndex(sequence), calculateAvailabilityFlag(sequence));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">calculateAvailabilityFlag</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> sequence)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">return</span> (<span class="keyword">int</span>) (sequence >>> indexShift);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setAvailableBufferValue</span><span class="params">(<span class="keyword">int</span> index, <span class="keyword">int</span> flag)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">long</span> bufferAddress = (index * SCALE) + BASE;</span><br><span class="line"> UNSAFE.putOrderedInt(availableBuffer, bufferAddress, flag);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>怎么用一个int表示这个位置的元素已经被写入成功了呢?</p><p>我们记得我们的offset计算公式<code>((int) sequence) & indexMask;</code></p><p>而Sequence是每次递增的,不会重复的。</p><p>所以原理类似,<code>calculateAvailabilityFlag</code>方法,就是把将Seq稍作变化。</p><p>在整个publish方法中,就是把<code>availableBuffer</code>中响应的位置置为写入成功。</p><h3 id="getHighestPublishedSequence-1"><a href="#getHighestPublishedSequence-1" class="headerlink" title="getHighestPublishedSequence"></a>getHighestPublishedSequence</h3><p>对于SingleProducer而言,直接返回availableSequence。</p><p>而对于MultiProducer,虽然WriteCursor已经分配好了,但是可能Producer还没有完成赋值。</p><p>所以我们需要查阅<code>availableBuffer</code>,看看具体有没有被赋值成功。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getHighestPublishedSequence</span><span class="params">(<span class="keyword">long</span> lowerBound, <span class="keyword">long</span> availableSequence)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">long</span> sequence = lowerBound; sequence <= availableSequence; sequence++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (!isAvailable(sequence))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> sequence - <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> availableSequence;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>具体的实现就是从lowerBound开始,查看每个Seq是否已经可用了,找到第一个不可用的Position。</p><h1 id="五-Consumer"><a href="#五-Consumer" class="headerlink" title="五. Consumer"></a>五. Consumer</h1><p>这里的Consumer的设计比较复杂,因为需要支持chain的操作。</p><p>类似于first().then().finally()的链式处理。</p><p>同时还支持不同的EventHandler都处理到同一个消息。</p><p>类似于消息队列的消费组的概念。</p><p>这里我们简单点,就看三个消费者并发消费的模型。</p><p>源码的使用中,我们传入了三个一样的WorkHandler。</p><p>最后每个WorkHandler都会通过WorkPool被包装成一个WorkProcessor。</p><p>在每个WorkProcessor中,都会有2个Seq。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">WorkProcessor</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Sequence sequence = <span class="keyword">new</span> Sequence(Sequencer.INITIAL_CURSOR_VALUE); <span class="comment">// 1</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Sequence workSequence; <span class="comment">// 2</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>第一个是自己的,每一个WorkProcessor都会有一个自己的。</li><li>第二个是所有的WorkProcessor都共享的一个。</li></ol><h2 id="SequenceBarrier"><a href="#SequenceBarrier" class="headerlink" title="SequenceBarrier"></a>SequenceBarrier</h2><p>这个叫做序号栅栏。</p><p>他是消费者和生产者沟通的桥梁。</p><p>消费者不能直接读取到生产者的循环数组和WriteCursor。</p><p>而是通过Barrier来获取下一段的消费序号。</p><p>在这个类中,有个重要的方法叫<code>long waitFor(long sequence)</code></p><p>就是消费者在需要消费时调用的。</p><p>比如消费者目前的seq是12,他想要消费13的数据,于是调用waitFor(13)去申请。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">waitFor</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> sequence)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, <span class="keyword">this</span>);</span><br><span class="line"> <span class="keyword">if</span> (availableSequence < sequence)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> availableSequence;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> sequencer.getHighestPublishedSequence(sequence, availableSequence);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的<code>cursorSequence</code>就是<code>AbstractSequencer</code>中的<code>cursor</code>,而<code>dependentSequence</code>是为了支持链式调用而传入的,这里没有相关的依赖,所以它的值和<code>cursorSequence</code>一样。</p><p>对于<code>waitStrategy</code>而言,其实就是Consumer等待Producer生产消息的过程。</p><p>主要功能就是等待Cursor的Seq是否已经到了我们所要申请的Seq。</p><p>并不会做任何同步的逻辑。</p><p>但是具体怎么等待,其实是个策略。具体的实现分为下面集中:</p><p><img src="/images/ringbuffer/15.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>有等待超时,忙等待,不满足条件自动block的等待,Sleep的等待方式。</p><p>我们简单看一下<code>BusySpinWaitStrategy</code>等待方式:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">BusySpinWaitStrategy</span> <span class="keyword">implements</span> <span class="title">WaitStrategy</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">BusySpinWaitStrategy</span><span class="params">()</span> </span>{</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">waitFor</span><span class="params">(<span class="keyword">long</span> sequence, Sequence cursor, Sequence dependentSequence, SequenceBarrier barrier)</span> <span class="keyword">throws</span> AlertException, InterruptedException </span>{</span><br><span class="line"> <span class="keyword">long</span> availableSequence;</span><br><span class="line"> <span class="keyword">while</span>((availableSequence = dependentSequence.get()) < sequence) {</span><br><span class="line"> barrier.checkAlert();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> availableSequence;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">signalAllWhenBlocking</span><span class="params">()</span> </span>{</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>代码也比较简单,就是不断的轮询Cursor的值,看是否已经生产到想要获取的Cursor。</p><p>看完了waitStrategy,我们再回过头看查看序号栅栏的waitFor方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">waitFor</span><span class="params">(<span class="keyword">final</span> <span class="keyword">long</span> sequence)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, <span class="keyword">this</span>); <span class="comment">// 1</span></span><br><span class="line"> <span class="keyword">if</span> (availableSequence < sequence) <span class="comment">// 2</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> availableSequence;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> sequencer.getHighestPublishedSequence(sequence, availableSequence); <span class="comment">// 3</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li><p>调用waitStrategy查看Cursor是否已经生产到sequence的序列了</p></li><li><p>因为有Timeout的waitStrategy,所以也可能是超时返回了,并没有满足条件,这里需要做一个判断。</p><p><strong>说到这儿,再思考个问题,availableSequence会不会大于sequence呢?</strong></p><p><strong>答案是会的,因为这里有Block的waitStrategy,就是等待Producer生产消息后唤醒自己,唤醒完自己后,去读取Cursor的位置,很可能已经比sequence大了。</strong></p></li><li><p>如果满足了条件,但是因为前文提到过的MultiProducer的问题,我们要找出这段Seq中已经被赋值的最早的位置并返回。</p></li></ol><h2 id="WorkProcessor"><a href="#WorkProcessor" class="headerlink" title="WorkProcessor"></a>WorkProcessor</h2><p>WorkProcessor实现了Run方法,在Disruptor调用了start之后,就会提交一份死循环的任务给ThreadPool,在ThreadPool中调用WorkProcessor的run方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">boolean</span> processedSequence = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">long</span> cachedAvailableSequence = Long.MIN_VALUE;</span><br><span class="line"> <span class="keyword">long</span> nextSequence = sequence.get();</span><br><span class="line"> T event = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">while</span> (<span class="keyword">true</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (processedSequence)</span><br><span class="line"> {</span><br><span class="line"> processedSequence = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> nextSequence = workSequence.get() + <span class="number">1L</span>;</span><br><span class="line"> sequence.set(nextSequence - <span class="number">1L</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">while</span> (!workSequence.compareAndSet(nextSequence - <span class="number">1L</span>, nextSequence));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (cachedAvailableSequence >= nextSequence)</span><br><span class="line"> {</span><br><span class="line"> event = ringBuffer.get(nextSequence);</span><br><span class="line"> workHandler.onEvent(event);</span><br><span class="line"> processedSequence = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>整体的逻辑分为两段:</p><ol><li>因为workSequence是所有的WorkProcessor共享的,所以先去获取下一个Position的权限,具体的实现就是CAS不断的读取当前的WorkSequence的值,然后尝试设置下一个值。</li><li>调用WaitFor,得到availableSequence,看能否执行步骤一获取的Position的元素。</li></ol><p>这里的cachedAvailableSequence,就是序号栅栏返回值的缓存。</p>]]></content>
<summary type="html">
<h1 id="一-前言"><a href="#一-前言" class="headerlink" title="一. 前言"></a>一. 前言</h1><p>Disruptor几乎是每个Java开发绕不过去的坎,其实我想学习这个框架很久了,之前打开看了几次,但是有点复杂就放弃了。</p>
<p>这一次看到了Mpsc,心里在构思多生产者多消费者的队列怎么怎么做,自然就想到了RingBuffer。</p>
<p>有了上文的基础,下面我们就Disruptor来看看多生产者多消费者是怎么实现的。</p>
<p>注意:这个文章并不是特别的分析Disruptor是怎么实现高性能的,诸如网上说的那些伪共享之类,</p>
<p>就是带大家看看源码实现。</p>
</summary>
<category term="RingBuffer" scheme="https://blog.lovezhy.cc/categories/RingBuffer/"/>
<category term="Java" scheme="https://blog.lovezhy.cc/tags/Java/"/>
</entry>
<entry>
<title>从Mpsc到RingBuffer(二)- RingBuffer</title>
<link href="https://blog.lovezhy.cc/2020/08/27/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%BA%8C%EF%BC%89-%20RingBuffer/"/>
<id>https://blog.lovezhy.cc/2020/08/27/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%BA%8C%EF%BC%89-%20RingBuffer/</id>
<published>2020-08-26T16:00:00.000Z</published>
<updated>2020-08-29T16:50:34.977Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>前面我们聊完了Mpsc,在提一下,Mpsc主要是针对的单消费者多生产者的情况。</p><p>对于消费者而言,因为只有一个消费者,所以不需要任何同步。</p><p>对于生产者而言,为了防止多线程下会出现问题,所以使用CAS操作。</p><p>但是上一篇文章中Mpsc使用的是链表的结构,不加以控制容易OOM。</p><p>为了避免这个问题,我们可以使用数组来作为底层存储。</p><p>原理其实和这边文章的RingBuffer讲的类似。</p><p>这里的RingBuffer其实就是Disruptor的实现,不过我单独抽成了一个文章来讲Disruptor的源码。</p><p>这里就划一些示意图,解释RingBuffer的原理。</p><a id="more"></a><h1 id="位置的二阶段写入"><a href="#位置的二阶段写入" class="headerlink" title="位置的二阶段写入"></a>位置的二阶段写入</h1><p>想象一下,如果我们有一个数组,我们要添加一个元素,单线程写入</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">put</span><span class="params">(T obj)</span> </span>{</span><br><span class="line"> arr[index++] = obj;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样当然是没有问题的,如果是多线程呢,这样就会出现问题,因为Index++不是原子操作。</p><p>可能两个线程进入后,写入的其实是同一个位置的元素。</p><p>那怎么办呢?</p><p>我们可以简单的加锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">put</span><span class="params">(T obj)</span> </span>{</span><br><span class="line"> arr[index++] = obj;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是这样性能就会下降很多。</p><p>有没有其他的办法,就要借助简单的CAS就可以实现呢?</p><p>前面提到,问题在于Index++不是原子的操作,那么我们将Index++这个操作,变成原子的不就行了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">put</span><span class="params">(T obj)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> position = index.inc(); <span class="comment">//inc假设是原子操作 // 1</span></span><br><span class="line"> arr[position] = obj; <span class="comment">// 2</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样就行了。</p><p>这里我们把操作分为了两步</p><ol><li>第一步,占位,这个操作是原子的</li><li>第二步,写入元素</li></ol><h1 id="RingBuffer的写入"><a href="#RingBuffer的写入" class="headerlink" title="RingBuffer的写入"></a>RingBuffer的写入</h1><p><img src="/images/ringbuffer/6.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>有了上述的两阶段写入的基础,对于RingBuffer的写入就好理解了。</p><p>假设我们的写入偏移指针指向<code>arr[1]</code>。</p><p>这个时候,有一个Producer想要写入一个数据咋办。</p><p>我们CAS这个Cursor,获取下一个位置的写入权限</p><p>伪代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">nextPosition</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (;;) {</span><br><span class="line"> <span class="keyword">int</span> currentIndex = current;</span><br><span class="line"> <span class="keyword">boolean</span> success = unsafe.cas(address(<span class="string">"currentIndex"</span>), currentIndex, currentIndex + <span class="number">1</span>);</span><br><span class="line"> <span class="keyword">if</span> (success) {</span><br><span class="line"> <span class="keyword">return</span> currentIndex + <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>获取成功后,往这个位置写入数据就行。</p><p><img src="/images/ringbuffer/7.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>如果这个生产者同时有N个元素需要写入,那我们就直接申请下面N个位置的权限,然后依次写入就行。</p><p>代码上只要修改CAS的第三个参数就行。</p><p>所以,对于写入而言,多线程写入,只要CAS这个写入的偏移指针,先获取写入的位置信息,下面再塞入数据,可以极大的避免Lock。</p><p><img src="/images/ringbuffer/8.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>如图所示,显示有2个线程同时在写入数据。</p><p>注意,这里存在一种状态,数据还未被完全写入成功,Write Cursor之前的格子里并不是全部都有数据了。</p><h1 id="RingBuffer读取"><a href="#RingBuffer读取" class="headerlink" title="RingBuffer读取"></a>RingBuffer读取</h1><p>对于RingBuffer的读取,其实也比较简单,也是一个二阶段的读取。</p><p>先对readIndex,进行CAS的加n。</p><p>成功后,读取返回的值的Position的值。</p><p>但是这里注意一个问题:</p><ol><li>返回的Position位置的值可能还未被写入,所以读取的可能是个空。</li></ol><p>怎么解决这个问题呢?</p><p>其实也不算是个问题,反复读取几遍,直到不为空就行。</p><p>但是在Disruptor中,使用了完全不同的做法,和我的方法略有不同。在下一篇文章中会讲。</p><h1 id="ReadIndex和WriteIndex的冲突"><a href="#ReadIndex和WriteIndex的冲突" class="headerlink" title="ReadIndex和WriteIndex的冲突"></a>ReadIndex和WriteIndex的冲突</h1><p>从上面的文章,我们可以理解为没有冲突,也就是这个数组的长度是无限长的。</p><p>所以我们仅仅维护了一个WriteIndex。</p><p>但是在实际的情况中,数组的长度都固定的,到了末尾之后就要从头开始写。</p><p>所以这里仅仅维护一个WriteIndex是不够的,还需要维护一个ReadIndex。</p><p>使用这两个Index来判断数组是否是空的或者已经满了。</p><h1 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h1><p><a href="https://www.jianshu.com/p/297819b95770" target="_blank" rel="noopener">https://www.jianshu.com/p/297819b95770</a></p><p><a href="https://juejin.im/post/6844903840156745742" target="_blank" rel="noopener">https://juejin.im/post/6844903840156745742</a></p>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>前面我们聊完了Mpsc,在提一下,Mpsc主要是针对的单消费者多生产者的情况。</p>
<p>对于消费者而言,因为只有一个消费者,所以不需要任何同步。</p>
<p>对于生产者而言,为了防止多线程下会出现问题,所以使用CAS操作。</p>
<p>但是上一篇文章中Mpsc使用的是链表的结构,不加以控制容易OOM。</p>
<p>为了避免这个问题,我们可以使用数组来作为底层存储。</p>
<p>原理其实和这边文章的RingBuffer讲的类似。</p>
<p>这里的RingBuffer其实就是Disruptor的实现,不过我单独抽成了一个文章来讲Disruptor的源码。</p>
<p>这里就划一些示意图,解释RingBuffer的原理。</p>
</summary>
<category term="RingBuffer" scheme="https://blog.lovezhy.cc/categories/RingBuffer/"/>
<category term="Java" scheme="https://blog.lovezhy.cc/tags/Java/"/>
</entry>
<entry>
<title>从Mpsc到RingBuffer(一)- Mpsc</title>
<link href="https://blog.lovezhy.cc/2020/08/26/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%B8%80%EF%BC%89-%20Mpsc/"/>
<id>https://blog.lovezhy.cc/2020/08/26/%E4%BB%8EMpsc%E5%88%B0RingBuffer%EF%BC%88%E4%B8%80%EF%BC%89-%20Mpsc/</id>
<published>2020-08-25T16:00:00.000Z</published>
<updated>2020-08-29T16:50:13.621Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>其实一开始没有接触过这个,但是看Netty源码的时候,发现Netty的对象池使用了MpscQueue,感觉还挺有意思的。</p><p>MpscQueue主要针对的是单消费者,多生产者的情况,实现上是LockFree的,这里的Lock Free,一般都是指用了CAS解决并发问题。</p><a id="more"></a><h1 id="单消费者单生产者"><a href="#单消费者单生产者" class="headerlink" title="单消费者单生产者"></a>单消费者单生产者</h1><p>我们先一步一步来,先考虑单消费者单生产者的情况。</p><p>这种情况下,除了Lock Free,还可以做到Wait Free。</p><h2 id="数组存储"><a href="#数组存储" class="headerlink" title="数组存储"></a>数组存储</h2><p>如果我们使用数组作为存储的话,维护一个ReadIndex和WriteIndex。</p><p>需要保证WriteIndex > ReadIndex就行。</p><p>每次读的时候,判断队列是否为空,每次写的时候判断队列是否已经满了。</p><h2 id="链表存储"><a href="#链表存储" class="headerlink" title="链表存储"></a>链表存储</h2><p>如果我们使用链表作为存储的话,原理和数组类似,可以维护一个Head节点和一个Tail节点。</p><p>每次写入的时候</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Node node = newNode();</span><br><span class="line">tail.next = node;</span><br><span class="line">tail = node;</span><br></pre></td></tr></table></figure><p>每次读取的时候</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (head == tail) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>; <span class="comment">//队列为空</span></span><br><span class="line">} </span><br><span class="line">Node node = head;</span><br><span class="line">head = head.next;</span><br><span class="line"><span class="keyword">return</span> node;</span><br></pre></td></tr></table></figure><h1 id="单消费者,多生产者"><a href="#单消费者,多生产者" class="headerlink" title="单消费者,多生产者"></a>单消费者,多生产者</h1><p>这里我只想对于链表的实现,因为这是Netty的默认实现方式。</p><p>对于数组的实现方式,等大家看了RingBuffer的实现方式之后,想必自然就懂了。</p><p>在链表的实现上,对于单消费者,多生产者,其实对于消费者端的代码而言,区别和单消费者单生产者不大。</p><p>但是由于生产者可能有多个,所以对于tail指针的操作,多线程下是不安全的。</p><p>在Netty的Mpsc中,引入CAS操作,对tail指针进行原子操作:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">MpscLinkedNode tail = <span class="keyword">new</span> MpscLinkedNode();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> MpscLinkedQueueNode<E> <span class="title">replaceTail</span><span class="params">(MpscLinkedQueueNode<E> node)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> getAndSet(node);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> MpscLinkedNode <span class="title">getAndSet</span><span class="params">(MpscLinkedNode newValue)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> (MpscLinkedNode)unsafe.getAndSetObject(<span class="keyword">this</span>, valueOffset, newValue);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的<code>valueOffset</code>就是tail对象的地址。</p><p>replaceTail这个函数,接收一个新的Node节点,将Tail节点替换成新的Node阶段,同时返回旧的Tail节点。</p><h2 id="Offer操作"><a href="#Offer操作" class="headerlink" title="Offer操作"></a>Offer操作</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">offer</span><span class="params">(E value)</span> </span>{</span><br><span class="line"> MpscLinkedQueueNode<E> newTail = <span class="keyword">new</span> DefaultNode<E>(value)</span><br><span class="line"> MpscLinkedQueueNode<E> oldTail = replaceTail(newTail); <span class="comment">// 1</span></span><br><span class="line"> oldTail.setNext(newTail); <span class="comment">// 2</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>生产者加元素的代码不多,算起来就2行。</p><p>我们用图例来演示这2步究竟做了什么,首先,我们有一个单链表的结构。</p><p><img src="/images/ringbuffer/1.png" alt="image-20200826221709219" style="zoom:50%;"></p><h3 id="第一步,单线程"><a href="#第一步,单线程" class="headerlink" title="第一步,单线程"></a>第一步,单线程</h3><p><img src="/images/ringbuffer/2.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>第一步,原子替换Tail节点,将新的节点设置为Tail节点。</p><p>但是这一步操作完之后,其实并没有改变之前的节点的Next指向,上一个节点的Next还是指向的之前的Tail节点。</p><h3 id="第二步,单线程"><a href="#第二步,单线程" class="headerlink" title="第二步,单线程"></a>第二步,单线程</h3><p><img src="/images/ringbuffer/3.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>第二步执行完,才完成整个Next指针的链接。</p><h3 id="第一步,多线程"><a href="#第一步,多线程" class="headerlink" title="第一步,多线程"></a>第一步,多线程</h3><p><img src="/images/ringbuffer/4.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>假设第一步被多线程并发了,现在有2个线程同时执行完了第一步。</p><p>这个时候,整个链表的结构看起来就是这样的。</p><p>后面两个接待是断开的。</p><h3 id="第二步,多线程"><a href="#第二步,多线程" class="headerlink" title="第二步,多线程"></a>第二步,多线程</h3><p><img src="/images/ringbuffer/5.png" alt="image-20200826221709219" style="zoom:50%;"></p><p>但是没关系,因为对于每个线程而言,都有自己的newTail和oldTail,这些newTail和oldTail相互串联了起来。</p><p>这2个线程结束之后,又是一个完整的链表。</p><h2 id="take"><a href="#take" class="headerlink" title="take"></a>take</h2><p>对于消费者而言,因为没有竞争,其实连CAS都不需要。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> MpscLinkedQueueNode<E> <span class="title">peekNode</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (;;) {</span><br><span class="line"> <span class="keyword">final</span> MpscLinkedQueueNode<E> head = headRef.get();</span><br><span class="line"> <span class="keyword">final</span> MpscLinkedQueueNode<E> next = head.next();</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (next != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> next;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (head == getTail()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>先看peekNode方法,因为head不保存数据,同时在多线程并发的情况下,可能会出现节点之间还没有被串联起来的情况。</p><p>这里使用了<code>for(;;)</code>去等待数据。</p><p>同时如果<code>head == tail</code>,表示队列中没有数据。</p><p>看完了peek,我们再来看poll方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> E <span class="title">poll</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> MpscLinkedQueueNode<E> next = peekNode();</span><br><span class="line"> <span class="keyword">if</span> (next == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> MpscLinkedQueueNode<E> oldHead = headRef.get();</span><br><span class="line"> headRef.lazySet(next);</span><br><span class="line"> oldHead.setNext(<span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">return</span> next.clearMaybe();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里就是拿到就的Head节点,把他置空,将next节点置为新的Head节点。</p><h1 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h1><p><a href="https://blog.csdn.net/dingguohang/article/details/55252969" target="_blank" rel="noopener">https://blog.csdn.net/dingguohang/article/details/55252969</a></p>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>其实一开始没有接触过这个,但是看Netty源码的时候,发现Netty的对象池使用了MpscQueue,感觉还挺有意思的。</p>
<p>MpscQueue主要针对的是单消费者,多生产者的情况,实现上是LockFree的,这里的Lock Free,一般都是指用了CAS解决并发问题。</p>
</summary>
<category term="RingBuffer" scheme="https://blog.lovezhy.cc/categories/RingBuffer/"/>
<category term="Java" scheme="https://blog.lovezhy.cc/tags/Java/"/>
</entry>
<entry>
<title>LevelDB源码解析(七)- Compact与Version</title>
<link href="https://blog.lovezhy.cc/2020/08/19/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%83%EF%BC%89-%20Compact%E4%B8%8EVersion/"/>
<id>https://blog.lovezhy.cc/2020/08/19/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%83%EF%BC%89-%20Compact%E4%B8%8EVersion/</id>
<published>2020-08-18T16:00:00.000Z</published>
<updated>2020-08-18T07:44:52.874Z</updated>
<content type="html"><![CDATA[<p>20200818补充</p><ol><li><a href="https://zhuanlan.zhihu.com/p/112574579" target="_blank" rel="noopener">LSM Tree的Leveling 和 Tiering Compaction</a> 文章讲了LSM的多种Compact策略,写的很好</li></ol><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这一节讲述最复杂的Version以及Compact的部分</p><a id="more"></a><h2 id="一-Version和VersionSet"><a href="#一-Version和VersionSet" class="headerlink" title="一. Version和VersionSet"></a>一. Version和VersionSet</h2><p>LevelDB算是实现了功能上的多版本控制。利用的是Version和VersionSet两个类。</p><p>当然这里的多版本控制肯定不是MVCC那么复杂,这里实现多版本的功能,我理解是为了解决两个操作的冲突:</p><ol><li>读取和迭代操作</li><li>后台异步Compact操作</li></ol><p>因为LevelDB的compact操作,其实是把多个文件合并成多个文件。</p><p>从SSTable那一章节,我们其实知道,一个SSTable就是对应一个文件。</p><p>如果我们直接修改文件,那么如果前台有线程正在迭代这个SSTable,就会出现不可预知的错误。</p><p>所以这就导致我们的Compact结果的文件需要重新生成,不能动以前的文件。</p><p>结果就是每个Level的SSTable集合是随着Compact操作会变化的。</p><p>迭代可能是个很长的操作,这中间不能保证不会发生Compact,所以干脆就搞个Version,把每次Compact后的SSTable集合存储起来。</p><p>这样迭代的时候,先拿到当前的Version。</p><p>DBImpl的Versions和CurrentVersion的初始化变量如下:</p><p><img src="/images/leveldb/compact.png" style="zoom:50%;"></p><p>我们先来看Version类中存储的变量:</p><ol><li><p>存储每个Level的SSTable集合:</p><p>其中Level0比较特殊,是个单独的类,其他的Level是用了个List存储。</p></li><li><p>retained表示目前这个Version有没有被正在运行的迭代器使用</p></li><li><p>剩余的四个都是和Compact有关,下面再讲</p></li></ol><p>再来看看VersionSet中的存储的变量:</p><ol><li>全局的文件名变量:nextFileNumber,LevelDB创建文件时,根据文件类型和FileNumber就可以定位到具体的File。这里存储这个,类似于数据库中的主键生成器,这里是自增的文件名生成器。</li><li>ManifestFileNumber:Manifest文件可以理解为是存储的当前的Version的持久化信息</li><li>lastSequence:每个写入的Key,都有一个唯一的序号与之对应</li><li>Log模块的配置:logNumber和prevLogNumber,类似于数据库中的WAL模块</li><li>activeVersions:当前被使用的Version有哪些</li><li>compactPointers:进行Compact时,为了保证每个SSTable都有被Compact机会,这个类似于游标,对于同一个Level,每次新Compact时,选择下一批SSTable。</li></ol><h2 id="二-Compact"><a href="#二-Compact" class="headerlink" title="二. Compact"></a>二. Compact</h2><p>讲完了Version,还有好多坑没填,主要是因为Version和Compact的联系太紧密了,这里将Compact的流程顺便把Version的坑填了。</p><p>LevelDB的Compact的代码在DbImpl的<code>backgroundCompaction()</code>中。</p><p>具体的Compact其实分为两步:</p><ol><li>找出需要compact的SSTable集合</li><li>对这些SSTable进行compact</li></ol><h3 id="2-1-SSTable多路归并"><a href="#2-1-SSTable多路归并" class="headerlink" title="2.1 SSTable多路归并"></a>2.1 SSTable多路归并</h3><p>其中第一步的代码主要是<code>VersionSet::pickCompaction</code>中,但是触发Compact的情况比较多,这里先不谈了。</p><p>直接先来看第二步,我们经过<code>pickCompaction</code>已经找到了需要Compact的SSTable,并且已经生成了<code>Compaction</code>对象:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Compaction</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> level; <span class="comment">// 需要compact的Level</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<FileMetaData> levelInputs; <span class="comment">//level对应的需要compact的SSTable</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<FileMetaData> levelUpInputs; <span class="comment">//下一层Level对应的需要compact的SSTable</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<FileMetaData> grandparents; <span class="comment">//再下一层level对应的需要compact的SSTable</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>得到了这些文件之后,下一步就是对这么文件进行合并。</p><p>合并的过程其实就是多路归并的过程。</p><p><img src="/images/leveldb/compact1.png" style="zoom:50%;"></p><p><strong>从iterator的视角来看,其实相同的UserKey已经按照seqNum的顺序从大到小排列好了。</strong></p><p>所以我们进行迭代的时候,看到的数据大致如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>. UserKey = <span class="string">"foo123"</span>, seq = <span class="number">34</span>,Type=VALUE</span><br><span class="line"><span class="number">2</span>. UserKey = <span class="string">"foo123"</span>, seq = <span class="number">20</span>,Type=VALUE</span><br><span class="line"><span class="number">3</span>. UserKey = <span class="string">"foo123"</span>, seq = <span class="number">18</span>,Type=DELETE</span><br><span class="line"><span class="number">4</span>. UserKey = <span class="string">"foo123"</span>, seq = <span class="number">8</span>, Type=VALUE</span><br><span class="line"></span><br><span class="line"><span class="number">5</span>. UserKey = <span class="string">"foo456"</span>, seq = <span class="number">213</span>,Type=DETELE</span><br><span class="line"><span class="number">6</span>. UserKey = <span class="string">"foo456"</span>, seq = <span class="number">200</span>,Type=VALUE</span><br><span class="line"><span class="number">7</span>. UserKey = <span class="string">"foo456"</span>, seq = <span class="number">93</span>, Type=VALUE</span><br></pre></td></tr></table></figure><p>这里我们根据UserKey把数据分为两段,第一段是1-4,UserKey都是”foo123”。</p><p>同时由于1的SEQ最大,剩余的都要被drop掉。</p><p>第二段是5-7,UserKey都是”foo456”。</p><p>这里由于5的seq最大,所以6-7需要被drop掉。</p><p>但是5能不能drop呢?</p><p>能不能drop需要查找下面所有的Level,是否是该UserKey了,如果没有了,那么可以drop,否则需要保留。</p><p>所以这里对迭代的每行数据而言,都需要判断是否能drop。</p><p>简略代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">boolean</span> hasCurrentUserKey = <span class="keyword">false</span>;</span><br><span class="line">Slice currentUserKey = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">long</span> lastSequenceForKey = MAX_SEQUENCE_NUMBER;</span><br><span class="line"><span class="keyword">while</span> () {</span><br><span class="line"> <span class="keyword">boolean</span> drop = <span class="keyword">false</span>;</span><br><span class="line"> InternalKey key = iterator.peek().getKey();</span><br><span class="line"> <span class="keyword">if</span> (!hasCurrentUserKey || !equals(currentUserKey, key)) {</span><br><span class="line"> <span class="comment">//这个key是第一次出现,类似于上述的1和5</span></span><br><span class="line"> currentUserKey = key.getUserKey();</span><br><span class="line"> hasCurrentUserKey = <span class="keyword">true</span>;</span><br><span class="line"> lastSequenceForKey = MAX_SEQUENCE_NUMBER;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//对特定UserKey的第一次循环不会进入,从第二次开始进入</span></span><br><span class="line"> <span class="comment">//目的也就是保留最早的Seq的记录</span></span><br><span class="line"> <span class="comment">//对于剩余的,一律都丢掉</span></span><br><span class="line"> <span class="keyword">if</span> (lastSequenceForKey <= compactionState.smallestSnapshot) {</span><br><span class="line"> <span class="comment">// Hidden by an newer entry for same user key</span></span><br><span class="line"> drop = <span class="keyword">true</span>; </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//如果 </span></span><br><span class="line"> <span class="comment">//1:这个记录是删除记录</span></span><br><span class="line"> <span class="comment">//2: 且key的seqNum小于compact时分配出去的最小seq(感觉这个永远为true)</span></span><br><span class="line"> <span class="comment">//3: 下面level中没有这个Key了</span></span><br><span class="line"> <span class="comment">//就可以drop,简单讲就是如果第一个就是DELETE记录,且下面的Level没有此UserKey了,那么第一个记录也就可以丢了</span></span><br><span class="line"> <span class="keyword">if</span> (key.getValueType() == DELETION</span><br><span class="line"> && key.getSequenceNumber() <= compactionState.smallestSnapshot</span><br><span class="line"> && compactionState.compaction.isBaseLevelForKey(key.getUserKey())) {</span><br><span class="line"> drop = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> lastSequenceForKey = key.getSequenceNumber();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="2-2-新的Table生成"><a href="#2-2-新的Table生成" class="headerlink" title="2.2 新的Table生成"></a>2.2 新的Table生成</h3><p>讲完了合并是,判断某个Key是否能丢,下面就是生成新SSTable的逻辑了。</p><p>对于一个KV,如果drop=false,那么建立一个Table,把它放进去就行了。</p><p>这里我们主要关注:</p><ol><li>新的SSTable在哪一层</li><li>Version是怎么链接到新的SSTable的</li></ol><p>顺着源码往下看:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!drop) {</span><br><span class="line"> <span class="keyword">if</span> (compactionState.builder == <span class="keyword">null</span>) {</span><br><span class="line"> openCompactionOutputFile(compactionState); <span class="comment">// 1</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (compactionState.builder.getEntryCount() == <span class="number">0</span>) {</span><br><span class="line"> compactionState.currentSmallest = key;</span><br><span class="line"> }</span><br><span class="line"> compactionState.currentLargest = key;</span><br><span class="line"> compactionState.builder.add(key.encode(), iterator.peek().getValue());</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (compactionState.builder.getFileSize() >=</span><br><span class="line"> compactionState.compaction.getMaxOutputFileSize()) {</span><br><span class="line"> finishCompactionOutputFile(compactionState); <span class="comment">// 2</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li><p>这里的builder就是TableBuilder,如果TableBuilder为空,则新建一个TableBuilder。</p><p>openCompactionOutputFile这个方法属于DbIMPL。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">openCompactionOutputFile</span><span class="params">(CompactionState compactionState)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> fileNumber = versions.getNextFileNumber();</span><br><span class="line"> </span><br><span class="line"> compactionState.currentFileNumber = fileNumber;</span><br><span class="line"> compactionState.currentFileSize = <span class="number">0</span>;</span><br><span class="line"> compactionState.currentSmallest = <span class="keyword">null</span>;</span><br><span class="line"> compactionState.currentLargest = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line"> File file = <span class="keyword">new</span> File(databaseDir, Filename.tableFileName(fileNumber));</span><br><span class="line"> compactionState.outfile = <span class="keyword">new</span> FileOutputStream(file).getChannel();</span><br><span class="line"> compactionState.builder = <span class="keyword">new</span> TableBuilder(options, compactionState.outfile, <span class="keyword">new</span> InternalUserComparator(internalKeyComparator)); </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法中主要做了一件事:初始化新的SSTable的一些信息到compactionState中</p></li><li><p>当当前的SSTable的大小超过阈值时,结束往这个SSTable添加,准备重启一个SSTable</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">finishCompactionOutputFile</span><span class="params">(CompactionState compactionState)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> outputNumber = compactionState.currentFileNumber;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">long</span> currentEntries = compactionState.builder.getEntryCount();</span><br><span class="line"> compactionState.builder.finish();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">long</span> currentBytes = compactionState.builder.getFileSize();</span><br><span class="line"> compactionState.currentFileSize = currentBytes;</span><br><span class="line"> compactionState.totalBytes += currentBytes;</span><br><span class="line"></span><br><span class="line"> FileMetaData currentFileMetaData = <span class="keyword">new</span> FileMetaData(compactionState.currentFileNumber,</span><br><span class="line"> compactionState.currentFileSize,</span><br><span class="line"> compactionState.currentSmallest,</span><br><span class="line"> compactionState.currentLargest);</span><br><span class="line"> </span><br><span class="line"> compactionState.outputs.add(currentFileMetaData); <span class="comment">//1</span></span><br><span class="line"></span><br><span class="line"> compactionState.builder = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line"> compactionState.outfile.force(<span class="keyword">true</span>);</span><br><span class="line"> compactionState.outfile.close();</span><br><span class="line"> compactionState.outfile = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (currentEntries > <span class="number">0</span>) {</span><br><span class="line"> tableCache.newIterator(outputNumber);</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>这个方法中,我们主要关注位置1的代码,在CompactionState中,有一个List,用来存储新生成的SSTable的信息。</p></li></ol><p>到这儿,合并过程结束了,我们生成了多个SSTable,在CompactionState的outputs中保存</p><p>然后进行最后一步:<code>installCompactionResults(compactionState);</code></p><p>说到这儿,你可能猜到了,下面就是与Version构建连接关系的过程了。</p><h3 id="2-3-保存Compact结果"><a href="#2-3-保存Compact结果" class="headerlink" title="2.3 保存Compact结果"></a>2.3 保存Compact结果</h3><p>这里不得不插入一个很重要的类,就是VersionEdit。</p><p>我们知道后一个Version和前一个Version的主要区别就是SSTable会发生变化,而VersionEdit就是记录这个变化的。</p><p>我们可以理解为 <code>NewVersion = OldVersion + VersionEdit</code>;</p><p>而在VersionEdit中,也存有两个变量表示SSTable的增减。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">VersionEdit</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Multimap<Integer, FileMetaData> newFiles;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Multimap<Integer, Long> deletedFiles;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在<code>installCompactionResults</code>中:主要就是构建VersionEdit。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">installCompactionResults</span><span class="params">(CompactionState compact)</span> </span>{</span><br><span class="line"> compact.compaction.addInputDeletions(compact.compaction.getEdit()); <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">int</span> level = compact.compaction.getLevel();</span><br><span class="line"> <span class="keyword">for</span> (FileMetaData output : compact.outputs) {</span><br><span class="line"> compact.compaction.getEdit().addFile(level + <span class="number">1</span>, output); <span class="comment">// 2</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> versions.logAndApply(compact.compaction.getEdit());</span><br><span class="line"> deleteObsoleteFiles();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>将之前的输入的SSTable放到<code>VersionEdit</code>的<code>deletedFiles</code>中</li><li>将新产生的SSTable放到<code>newFiles</code>中,这里可以看到,新生成的SSTable,放到了level+1层,也就是levelUp层,也就是下一层。</li></ol><p>下面就是进入<code>versions.logAndApply</code>方法了,这个方法属于VersionSet类。</p><p>在这个方法中,最重要的有几句话:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">logAndApply</span><span class="params">(VersionEdit edit)</span> </span>{</span><br><span class="line"> edit.setLogNumber(logNumber);</span><br><span class="line"> edit.setNextFileNumber(nextFileNumber.get());</span><br><span class="line"> edit.setLastSequenceNumber(lastSequence);</span><br><span class="line"></span><br><span class="line"> Version version = <span class="keyword">new</span> Version(<span class="keyword">this</span>);</span><br><span class="line"> Builder builder = <span class="keyword">new</span> Builder(<span class="keyword">this</span>, current);</span><br><span class="line"> builder.apply(edit);</span><br><span class="line"> builder.saveTo(version);</span><br><span class="line"></span><br><span class="line"> finalizeVersion(version);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">boolean</span> createdNewManifest = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (descriptorLog == <span class="keyword">null</span>) {</span><br><span class="line"> edit.setNextFileNumber(nextFileNumber.get());</span><br><span class="line"> descriptorLog = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.descriptorFileName(manifestFileNumber)), manifestFileNumber);</span><br><span class="line"> writeSnapshot(descriptorLog);</span><br><span class="line"> createdNewManifest = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> Slice record = edit.encode();</span><br><span class="line"> descriptorLog.addRecord(record, <span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (createdNewManifest) {</span><br><span class="line"> Filename.setCurrentFile(databaseDir, descriptorLog.getFileNumber());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> appendVersion(version);</span><br><span class="line"> logNumber = edit.getLogNumber();</span><br><span class="line"> prevLogNumber = edit.getPreviousLogNumber();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">appendVersion</span><span class="params">(Version version)</span> </span>{</span><br><span class="line"> Version previous = current;</span><br><span class="line"> current = version;</span><br><span class="line"> activeVersions.put(version, <span class="keyword">new</span> Object());</span><br><span class="line"> <span class="keyword">if</span> (previous != <span class="keyword">null</span>) {</span><br><span class="line"> previous.release();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的逻辑之前已经分析过了,这里再过一遍整体的流程,主要就是2步:</p><ol><li>根据当前的Version和传入的VersionEdit,构造新的Version。并且在appendVersion方法中替换current。</li><li>把VersionEdit写入Manifest文件</li></ol><h3 id="2-4-彩蛋"><a href="#2-4-彩蛋" class="headerlink" title="2.4 彩蛋"></a>2.4 彩蛋</h3><p>彩蛋:</p><p>前面的多路归并生成新的SSTable的流程,具体的某个SSTable结束Build逻辑其实就是判定新的SSTable的大小。</p><p>其实源代码中不然,还有一种情况也会触发SSTable生成结束。</p><p>不过这种场景,我想了很久都没想通,这里先抛出来:TODO</p><h3 id="2-5-Compact触发策略"><a href="#2-5-Compact触发策略" class="headerlink" title="2.5 Compact触发策略"></a>2.5 Compact触发策略</h3><p>写到这里,Version,VersionSet和Compact的耦合流程已经理清楚了。</p><p>下面就是重点了,设计到策略方面的:如何选择需要合并的SSTable。</p><p>我们知道,LevelDB最多有7层Level,每个Level可能有很多SSTable,其中Level0的SSTable的KV是无序的。</p><p>如果我们每次都合并某个Level,或者我们每次都合并每个Level的前几个SSTable,必然会导致KV不均的情况。</p><p>同时我们还需要机制触发合并流程,不能是配死的规则。</p><h4 id="2-5-1-针对Level0的searchMiss的情况"><a href="#2-5-1-针对Level0的searchMiss的情况" class="headerlink" title="2.5.1 针对Level0的searchMiss的情况"></a>2.5.1 针对Level0的searchMiss的情况</h4><p>我们知道Level0的SSTable之间的Key并不是顺序的,互相之间可能overlap。那么在查找一个Key的时候,仅仅从最大key和最小key才判断,可能命中好几个SSTable。</p><p>比如如下场景的Level0:</p><p><img src="/images/leveldb/compact2.png" style="zoom:50%;"></p><p>我们要查找19这个Key,会发现符合条件的有3个。</p><p>我们要查找22这个Key,会发现符合条件的有2个。</p><p>遇到超过一个SSTable需要查找的情况,我们认为情况不太好,但是只谈性质不谈次数就是耍流氓。</p><p>所以我们给每个SSTable维护一个计数器,指示SearchMiss的次数。</p><p>比如我们搜索20,最终在第二个SSTable中搜索到了,那么第一个SSTable就是SearchMiss了,计数器减一。</p><p>但是注意,每次搜索,只会将第一个SearchMiss的SSTable的计数器减一。</p><p>比如我们查找19这个Key,最终在第三个SSTable中搜索到了,或者3个都没搜索到,也只会将第一个SSTable的SearchMiss计数器减一。</p><p>来看源代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">Version::get(LookupKey key)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> LookupResult <span class="title">get</span><span class="params">(LookupKey key)</span></span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> ReadStats readStats = <span class="keyword">new</span> ReadStats(); <span class="comment">// 1</span></span><br><span class="line"> LookupResult lookupResult = level0.get(key, readStats); <span class="comment">// 2</span></span><br><span class="line"> <span class="keyword">if</span> (lookupResult == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">for</span> (Level level : levels) {</span><br><span class="line"> lookupResult = level.get(key, readStats); <span class="comment">// 3</span></span><br><span class="line"> <span class="keyword">if</span> (lookupResult != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> updateStats(readStats.getSeekFileLevel(), readStats.getSeekFile()); <span class="comment">// 4</span></span><br><span class="line"> <span class="keyword">return</span> lookupResult;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">updateStats</span><span class="params">(<span class="keyword">int</span> seekFileLevel, FileMetaData seekFile)</span></span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> <span class="keyword">if</span> (seekFile == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> seekFile.decrementAllowedSeeks(); <span class="comment">// 5</span></span><br><span class="line"> <span class="keyword">if</span> (seekFile.getAllowedSeeks() <= <span class="number">0</span> && fileToCompact == <span class="keyword">null</span>) {</span><br><span class="line"> fileToCompact = seekFile; </span><br><span class="line"> fileToCompactLevel = seekFileLevel; <span class="comment">// 6</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ol><li>这里的ReadStats保存的是第一个SearchMiss的SSTable。</li><li>搜索Level0</li><li>搜索Level1往下的,但是这里面因为SSTable都是有序的,所有不会出现SearchMiss的情况,也就是不会更新ReadStats</li><li>更新stats,传入的是SearchMiss的SSTable的Level和FileMetaData</li><li>在updateStats方法中,将SearchMiss的数值减一,这个值初始为1 << 30,不是很小其实,这个阈值还是很难达到的,达到了表示有热点Key了。</li><li>fileToCompact和fileToCompactLevel是Version的成员变量,在下一个pickCompact时会考虑这两个值。</li></ol><h4 id="2-5-2-根据每个Level的文件个数和字节大小"><a href="#2-5-2-根据每个Level的文件个数和字节大小" class="headerlink" title="2.5.2 根据每个Level的文件个数和字节大小"></a>2.5.2 根据每个Level的文件个数和字节大小</h4><p>从Level0到Level7,作为LSM来看的话,越往下的Level的KV数应该是越多的。所以如果中间某个Level的KV数超过某个阈值,就要Compact到下一个Level。</p><p>同时Level0因为无序,对他而言,SSTable如果超过一定的个数,也要进行Compact。</p><p>总结一下就是,选择下一次的Compact的SSTable:</p><ol><li>Level0的SSTable个数</li><li>其他Level的KV个数,也就是等价于Bytes个数</li></ol><p>于是我们给每个Level打个分,分越高表示越需要尽快Compact。</p><p>在VersionSet的finalizeVersion方法中,就是给每个Level打分,选出下一个需要Compact的Level。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">finalizeVersion</span><span class="params">(Version version)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> bestLevel = -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">double</span> bestScore = -<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> level = <span class="number">0</span>; level < version.numberOfLevels() - <span class="number">1</span>; level++) {</span><br><span class="line"> <span class="keyword">double</span> score;</span><br><span class="line"> <span class="keyword">if</span> (level == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">//level0是根据文件数计算score</span></span><br><span class="line"> score = <span class="number">1.0</span> * version.numberOfFilesInLevel(level) / L0_COMPACTION_TRIGGER;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">//其他的level是根据文件总大小计算score</span></span><br><span class="line"> <span class="keyword">long</span> levelBytes = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (FileMetaData fileMetaData : version.getFiles(level)) {</span><br><span class="line"> levelBytes += fileMetaData.getFileSize();</span><br><span class="line"> }</span><br><span class="line"> score = <span class="number">1.0</span> * levelBytes / maxBytesForLevel(level);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (score > bestScore) {</span><br><span class="line"> bestLevel = level;</span><br><span class="line"> bestScore = score;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> version.setCompactionLevel(bestLevel);</span><br><span class="line"> version.setCompactionScore(bestScore);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>假如我们选出了得分最高的,比如Level3,但是Level3中可能有100个SSTable,具体怎么选择呢?</p><p>是固定选择第一个SSTable,但是最后一个,还是中间1个?</p><p>或者选择其中的几个?随机几个吗?</p><p>LevelDB为为每个Level维护了一个Compact进度的游标,这个变量叫<code>compactPointers</code>,维护在VersionSet和VersionEdit中,因为维护在了VersionEdit中了,所以每次Compact完会写入Manifest文件中,重新启动的时候会恢复出来。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map<Integer, InternalKey> compactPointers = <span class="keyword">new</span> TreeMap<>();</span><br></pre></td></tr></table></figure><p>定义如上,Key是Level的值,Value是具体的InternalKey。</p><p>具体流程如下:</p><ol><li>根据finalizeVersion的结果,找个该Level。</li><li>遍历该Level下的所有文件,找到第一个LargestKey大于compactPointers中该Level的Value的SSTable</li><li>以这一个SSTable作为Base,寻找Level + 1 层有overlap的SSTable</li><li>更新compactPointers该Level的结果,Value更新为pick中的SSTable的LargestKey。下一次查找的时候就是顺位的下一个SSTable。</li><li>将这些SSTable进行Compact。</li></ol><p>这里为什么维护这种游标呢?是让每个SSTable都有机会进行Compact吗?</p><p>其实我理解是为了LevelDB中所有的Key的分布在每个Level都更均匀。</p><p>如果每次都Compact第一个SSTable,那么所有Key靠前的都会被优先Compact到下一层。</p><p>查询的时候很容易就找到了最后一个Level。</p><p>维护了这种游标之后,每个Level的Key分布都均匀的向下Compact。</p><h3 id="2-6-pickCompact方法源码"><a href="#2-6-pickCompact方法源码" class="headerlink" title="2.6 pickCompact方法源码"></a>2.6 pickCompact方法源码</h3><p>综合上面的两种情况,我们最后可以来看pickCompact方法了,代码比较多,但是如果上面的都理解了,还是比较好懂的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Compaction <span class="title">pickCompaction</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">boolean</span> sizeCompaction = (current.getCompactionScore() >= <span class="number">1</span>); <span class="comment">// 对应上面的第二种触发Compact情况</span></span><br><span class="line"><span class="keyword">boolean</span> seekCompaction = (current.getFileToCompact() != <span class="keyword">null</span>);<span class="comment">// 对应上面第一种触发情况</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">int</span> level;</span><br><span class="line">List<FileMetaData> levelInputs;</span><br><span class="line"><span class="keyword">if</span> (sizeCompaction) { <span class="comment">//如果第二种情况满足条件</span></span><br><span class="line">level = current.getCompactionLevel();</span><br><span class="line">levelInputs = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> </span><br><span class="line"><span class="keyword">for</span> (FileMetaData fileMetaData : current.getFiles(level)) {</span><br><span class="line"> <span class="comment">//下面就是compactPointer找到大于Value的第一个SSTable</span></span><br><span class="line"><span class="keyword">if</span> (!compactPointers.containsKey(level) ||</span><br><span class="line">internalKeyComparator.compare(fileMetaData.getLargest(), compactPointers.get(level)) > <span class="number">0</span>) {</span><br><span class="line">levelInputs.add(fileMetaData); </span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (levelInputs.isEmpty()) {</span><br><span class="line">levelInputs.add(current.getFiles(level).get(<span class="number">0</span>));</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (seekCompaction) { <span class="comment">//如果第一种情况满足条件</span></span><br><span class="line">level = current.getFileToCompactLevel();</span><br><span class="line">levelInputs = ImmutableList.of(current.getFileToCompact());</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">null</span>; <span class="comment">//都不满足直接返回</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Level0的overlap情况要特殊处理</span></span><br><span class="line"><span class="keyword">if</span> (level == <span class="number">0</span>) {</span><br><span class="line">Entry<InternalKey, InternalKey> range = getRange(levelInputs);</span><br><span class="line">levelInputs = getOverlappingInputs(<span class="number">0</span>, range.getKey(), range.getValue());</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">Compaction compaction = setupOtherInputs(level, levelInputs);</span><br><span class="line"><span class="keyword">return</span> compaction;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//这个方法是找到level + 1 和level + 2的overlap的情况</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> Compaction <span class="title">setupOtherInputs</span><span class="params">(<span class="keyword">int</span> level, List<FileMetaData> levelInputs)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">Entry<InternalKey, InternalKey> range = getRange(levelInputs);</span><br><span class="line">InternalKey smallest = range.getKey();</span><br><span class="line">InternalKey largest = range.getValue();</span><br><span class="line"></span><br><span class="line">List<FileMetaData> levelUpInputs = getOverlappingInputs(level + <span class="number">1</span>, smallest, largest);</span><br><span class="line"></span><br><span class="line">range = getRange(levelInputs, levelUpInputs);</span><br><span class="line">InternalKey allStart = range.getKey();</span><br><span class="line">InternalKey allLimit = range.getValue();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//expand,这里看看就好</span></span><br><span class="line"><span class="keyword">if</span> (!levelUpInputs.isEmpty()) {</span><br><span class="line">List<FileMetaData> expanded0 = getOverlappingInputs(level, allStart, allLimit);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (expanded0.size() > levelInputs.size()) {</span><br><span class="line">range = getRange(expanded0);</span><br><span class="line">InternalKey newStart = range.getKey();</span><br><span class="line">InternalKey newLimit = range.getValue();</span><br><span class="line"></span><br><span class="line">List<FileMetaData> expanded1 = getOverlappingInputs(level + <span class="number">1</span>, newStart, newLimit);</span><br><span class="line"><span class="keyword">if</span> (expanded1.size() == levelUpInputs.size()) {</span><br><span class="line">smallest = newStart;</span><br><span class="line">largest = newLimit;</span><br><span class="line">levelInputs = expanded0;</span><br><span class="line">levelUpInputs = expanded1;</span><br><span class="line"></span><br><span class="line">range = getRange(levelInputs, levelUpInputs);</span><br><span class="line">allStart = range.getKey();</span><br><span class="line">allLimit = range.getValue();</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">List<FileMetaData> grandparents = ImmutableList.of();</span><br><span class="line"><span class="keyword">if</span> (level + <span class="number">2</span> < NUM_LEVELS) {</span><br><span class="line">grandparents = getOverlappingInputs(level + <span class="number">2</span>, allStart, allLimit);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">Compaction compaction = <span class="keyword">new</span> Compaction(current, level, levelInputs, levelUpInputs, grandparents);</span><br><span class="line"></span><br><span class="line">compactPointers.put(level, largest); <span class="comment">// 更新游标,重点</span></span><br><span class="line">compaction.getEdit().setCompactPointer(level, largest);</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> compaction;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<p>20200818补充</p>
<ol>
<li><a href="https://zhuanlan.zhihu.com/p/112574579" target="_blank" rel="noopener">LSM Tree的Leveling 和 Tiering Compaction</a> 文章讲了LSM的多种Compact策略,写的很好</li>
</ol>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这一节讲述最复杂的Version以及Compact的部分</p>
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(六)- WAL和Log文件</title>
<link href="https://blog.lovezhy.cc/2020/08/18/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E5%85%AD%EF%BC%89-%20WAL%E5%92%8CLog%E6%96%87%E4%BB%B6/"/>
<id>https://blog.lovezhy.cc/2020/08/18/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E5%85%AD%EF%BC%89-%20WAL%E5%92%8CLog%E6%96%87%E4%BB%B6/</id>
<published>2020-08-17T16:00:00.000Z</published>
<updated>2020-08-15T10:55:53.439Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>数据库中基本都有WAL功能,LevelDB也不例外,不过这里的WAL功能实现比较简单</p><a id="more"></a><h2 id="Log文件初始化"><a href="#Log文件初始化" class="headerlink" title="Log文件初始化"></a>Log文件初始化</h2><p>在DbImpl的构造函数中,申请了一个新的FileNumber,然后对log对象进行了初始化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">long</span> logFileNumber = versions.getNextFileNumber();</span><br><span class="line"><span class="keyword">this</span>.log = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.logFileName(logFileNumber)), logFileNumber);</span><br></pre></td></tr></table></figure><p>所以这个Log文件每次启动都会创建一个新的。</p><h2 id="写入"><a href="#写入" class="headerlink" title="写入"></a>写入</h2><p>Log的写入也比较简单,每次进行put的时候,将KV序列化特定的格式,然后append进日志系统。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Snapshot <span class="title">writeInternal</span><span class="params">(WriteBatchImpl updates, WriteOptions options)</span> </span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="comment">// Log write</span></span><br><span class="line"> Slice record = writeWriteBatch(updates, sequenceBegin);</span><br><span class="line"> log.addRecord(record, options.sync()); </span><br><span class="line"> <span class="comment">//... </span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="更迭"><a href="#更迭" class="headerlink" title="更迭"></a>更迭</h2><p>在正常的运行中,Log日志会更迭吗?</p><p>因为如果不更迭的话,日志会越来越多,单文件可能会越来越多。</p><p>同时我们知道Log格式的文件仅仅支持从头开始遍历的,在Recover的时候,从头到尾遍历一个大文件也是个问题。</p><p>在<code>makeRoomForWrite</code>中,如果MemTable需要进行Compact的时候,就会强制关闭当前的Log,再创建一个新的Log。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">makeRoomForWrite</span><span class="params">(<span class="keyword">boolean</span> force)</span> </span>{</span><br><span class="line"> <span class="comment">// ... </span></span><br><span class="line"> log.close();</span><br><span class="line"> <span class="keyword">long</span> logNumber = versions.getNextFileNumber();</span><br><span class="line"> <span class="keyword">this</span>.log = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.logFileName(logNumber)), logNumber); </span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意:makeRoomForWrite并不是每次调用都会走到这个逻辑的。</p><h2 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h2><p>什么时候删除呢?</p><p>前面我们看到每次启动时,会新生成一个新的Log,每次CompactMemTable的时候,也会新生成一个新的,那么旧的什么时候删除呢?</p><p>答案在<code>deleteObsoleteFiles</code>方法中。</p><p>方法内容如名字,其实这个方法不止删除了旧的Log文件。</p><p>这个方法会遍历数据目录下的所有文件,如果是log文件,判断fileNumber是否小于当前的LogFileNumber,如果小于,就可以删除。</p><p>了解了内容,我们来想想这个方法会在什么时候调用呢?</p><p>前面提到过,每次Compact新的MemTable的时候,都会生成新的Log文件,也就是说,这个Log文件包含了当前MemTable中的内容,也就是未持久化的内容。</p><p>如果Compact后,持久化了,自然就不需要这个文件了。</p><p>所以在VersionSet::logAndApply后,都会清理旧的Log文件。</p><p>因为每次VersionEdit生成后,NewVersion的所有SSTable就已经确定了,新的KV记录则在新的Log日志中,也就不需要再知道旧的Log日志文件是啥了。</p><h2 id="Recover"><a href="#Recover" class="headerlink" title="Recover"></a>Recover</h2><p>结合了WAL的性质,我们来了解下,recover的内容。</p><p>按照理解,Recover时,主要把有内容还是MemTable中,还没持久化到磁盘上,这个时候需要找到这个MemTable对于的Log文件,遍历这个文件内容,再Append一遍就行。</p><p>我们结合DbImpl的构造函数一起来看看:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">DbImpl</span><span class="params">(Options options, File databaseDir)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> versions = <span class="keyword">new</span> VersionSet(databaseDir, tableCache, internalKeyComparator); </span><br><span class="line"> <span class="comment">// load (and recover) current version</span></span><br><span class="line"> versions.recover(); <span class="comment">// 1</span></span><br><span class="line"> <span class="keyword">long</span> minLogNumber = versions.getLogNumber();</span><br><span class="line"> <span class="keyword">long</span> previousLogNumber = versions.getPrevLogNumber();</span><br><span class="line"> </span><br><span class="line"> List<File> filenames = Filename.listFiles(databaseDir);</span><br><span class="line"> List<Long> logs = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">for</span> (File filename : filenames) {</span><br><span class="line"> FileInfo fileInfo = Filename.parseFileName(filename);</span><br><span class="line"> <span class="keyword">if</span> (fileInfo != <span class="keyword">null</span> && fileInfo.getFileType() == FileType.LOG && ((fileInfo.getFileNumber() >= minLogNumber) || (fileInfo.getFileNumber() == previousLogNumber))) {</span><br><span class="line"> logs.add(fileInfo.getFileNumber()); <span class="comment">// 2</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Recover in the order in which the logs were generated</span></span><br><span class="line"> VersionEdit edit = <span class="keyword">new</span> VersionEdit();</span><br><span class="line"> Collections.sort(logs);</span><br><span class="line"> <span class="keyword">for</span> (Long fileNumber : logs) {</span><br><span class="line"> <span class="keyword">long</span> maxSequence = recoverLogFile(fileNumber, edit); <span class="comment">// 3</span></span><br><span class="line"> <span class="keyword">if</span> (versions.getLastSequence() < maxSequence) {</span><br><span class="line"> versions.setLastSequence(maxSequence);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// open transaction log</span></span><br><span class="line"> <span class="keyword">long</span> logFileNumber = versions.getNextFileNumber(); </span><br><span class="line"> <span class="keyword">this</span>.log = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.logFileName(logFileNumber)), logFileNumber);</span><br><span class="line"> edit.setLogNumber(log.getFileNumber());</span><br><span class="line"> <span class="comment">// apply recovered edits</span></span><br><span class="line"> versions.logAndApply(edit); <span class="comment">// 4</span></span><br><span class="line"> <span class="comment">// cleanup unused files</span></span><br><span class="line"> deleteObsoleteFiles(); <span class="comment">// 5 </span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>读取CURRENT文件执行的Manifest文件,恢复出最后的LogNumber。</li><li>这里我不理解为什么Logs是个数组,讲道理每次MemTable都是串行Compact的话,那么Manifest中最后一个VersionEdit中的LogNumber,就是最新的MemTable的内容,不会出现多个文件的情况。</li><li>这里的recoverLogFile方法,就是顺序遍历,然后Put进去,但是这里和正常的Put流程不一样,虽然会生成新的SSTable,但是并不会调用VersionSet的logAndApply方法。</li><li>这里对Edit进行了LogAndApply,提交到了Manifest中,数据恢复成功</li><li>可以清理旧的文件了</li></ol>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>数据库中基本都有WAL功能,LevelDB也不例外,不过这里的WAL功能实现比较简单</p>
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(五)- CURRENT和Manifest</title>
<link href="https://blog.lovezhy.cc/2020/08/17/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%BA%94%EF%BC%89-%20CURRENT%E5%92%8CManifest/"/>
<id>https://blog.lovezhy.cc/2020/08/17/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%BA%94%EF%BC%89-%20CURRENT%E5%92%8CManifest/</id>
<published>2020-08-16T16:00:00.000Z</published>
<updated>2020-08-15T11:01:43.808Z</updated>
<content type="html"><![CDATA[<p>Manifest文件,简单的看就是当前VersionEdit的持久化信息,其中包含了:</p><ol><li>Comparator:全局的比较方法</li><li>LogNumber:下一个MemTable的WAL日志文件的FileNumber</li><li>PreviousLogNumber:已经被废弃,之前版本有用到</li><li>NextFileNumber:下一个File的数字</li><li>LastSequence:最新的Seq</li><li>Compact_Pointer:在Compact一章中统一讲。</li><li>Deteted_Files:相对于上一个Version,删除的文件</li><li>New_Files:相对于上一个Version,新增的文件</li></ol><p>以上内容都在<code>VersionEditTag</code>中进行读写。</p><p>其中Manifest文件只会存在一个,但是名字中的FileNumber不是一定的,在数据目录下,文件名可能是</p><p><code>MANIFEST-000540</code>。个人猜想可能是版本问题导致的。</p><p>初始化:</p><p>在DbIMPL进行初始化时,会创建VersionSet,在VersionSet的构造函数中,调用了initializeIfNeeded对Manifest进行了初始化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">initializeIfNeeded</span><span class="params">()</span> </span>{</span><br><span class="line"> File currentFile = <span class="keyword">new</span> File(databaseDir, Filename.currentFileName()); <span class="comment">// 1</span></span><br><span class="line"> <span class="keyword">if</span> (!currentFile.exists()) { </span><br><span class="line"> VersionEdit edit = <span class="keyword">new</span> VersionEdit(); <span class="comment">// 2</span></span><br><span class="line"> edit.setComparatorName(internalKeyComparator.name());</span><br><span class="line"> edit.setLogNumber(prevLogNumber); </span><br><span class="line"> edit.setNextFileNumber(nextFileNumber.get()); </span><br><span class="line"> edit.setLastSequenceNumber(lastSequence); </span><br><span class="line"></span><br><span class="line"> LogWriter log = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.descriptorFileName(manifestFileNumber)), manifestFileNumber); <span class="comment">// 3</span></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> writeSnapshot(log); <span class="comment">// 4</span></span><br><span class="line"> log.addRecord(edit.encode(), <span class="keyword">false</span>); <span class="comment">// 5</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">finally</span> {</span><br><span class="line"> log.close();</span><br><span class="line"> }</span><br><span class="line"> Filename.setCurrentFile(databaseDir, log.getFileNumber()); <span class="comment">// 5</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>找出名为”CURRENT”文件,前面提到这个文件的内容就是Manifest文件的文件名。</li><li>这里CURRENT文件不存在,所以new一个VersionEdit,其中prevLogNumber是0,nextFileNumber是2,lastSeq也是0。</li><li>这里的manifestFileNumber文件是1,这里知道为什么nextFileNumber默认是2开始了吧,因为1是ManifestFile的初始化FileNumber</li><li>这个方法里,new了一个VersionEdit,基本上什么值也没设,这里不知道为什么要先把这个VersionEdit放进去。</li><li>把上面的VersionEdit写入到Manifest中</li><li>这里把上面的Manifest文件名,写入CURRENT文件,这个方法中用到了Temp文件。</li></ol><p>每次Compact过后,就会调用VersionSet的logAndApply方法,把Edit的信息传入,加入到Manifest文件中。</p><p>这个方法下面会详细描述。</p><p>那么一个疑问就来了,每次Compact后都会往里面Append新的VersionEdit信息,那么这个文件不就会越来越大吗?就像Redis的Compact一样?是不是有什么机制,会导致创建新的Manifest文件,把当前的Version的快照放进去,然后丢弃旧的Manifest文件呢?</p><p>答案是有的:其中每次启动后,触发的第一次Compact,会导致旧的Manifest文件被丢弃,生成新的Manifest文件,把当前Version的快照信息放入。</p><p>这里其实有个问题的,只有每次重新启动后才会触发,如果一直在运行的话,其实不会触发重新清理的。</p><p>虽然在运行中并不会去读取这个Manifest文件,但是下次启动恢复Version信息时,需要从头到尾遍历这个文件,速度可能会很慢。</p><p>代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">logAndApply</span><span class="params">(VersionEdit edit)</span> </span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="keyword">boolean</span> createdNewManifest = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (descriptorLog == <span class="keyword">null</span>) { <span class="comment">// 1</span></span><br><span class="line"> edit.setNextFileNumber(nextFileNumber.get());</span><br><span class="line"> descriptorLog = Logs.createLogWriter(<span class="keyword">new</span> File(databaseDir, Filename.descriptorFileName(manifestFileNumber)), manifestFileNumber); <span class="comment">// 2</span></span><br><span class="line"> writeSnapshot(descriptorLog); <span class="comment">// 3</span></span><br><span class="line"> createdNewManifest = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> Slice record = edit.encode();</span><br><span class="line"> descriptorLog.addRecord(record, <span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (createdNewManifest) {</span><br><span class="line"> Filename.setCurrentFile(databaseDir, descriptorLog.getFileNumber()); <span class="comment">// 4</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">writeSnapshot</span><span class="params">(LogWriter log)</span> </span>{</span><br><span class="line"> <span class="comment">// Save metadata</span></span><br><span class="line"> VersionEdit edit = <span class="keyword">new</span> VersionEdit();</span><br><span class="line"> edit.setComparatorName(internalKeyComparator.name());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Save compaction pointers</span></span><br><span class="line"> edit.setCompactPointers(compactPointers);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Save files</span></span><br><span class="line"> edit.addFiles(current.getFiles());</span><br><span class="line"></span><br><span class="line"> Slice record = edit.encode();</span><br><span class="line"> log.addRecord(record, <span class="keyword">false</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>descriptorLog除了在这个方法,没有在其他地方被赋值过,所以第一次进来,肯定是null的</li><li>创建一个新的Manifest文件</li><li>调用writeSnapshot方法,生成VersionEdit,把当前所有的文件写入Manifest文件</li><li>设置CURRENT文件,指向新的Manifest文件</li></ol><p>VersionSet的恢复:</p><p>前面提到过一个公式:<code>OldVersion + VersionEdit = NewVersion</code></p><p>如果重新启动应用,要恢复到最新的Version,只要把Manifest文件中的VersionEdit全部apply一遍就行了。</p><p>方法在<code>VersionSet::recover</code>中,代码浅显易懂,这里就不展开了。</p>]]></content>
<summary type="html">
<p>Manifest文件,简单的看就是当前VersionEdit的持久化信息,其中包含了:</p>
<ol>
<li>Comparator:全局的比较方法</li>
<li>LogNumber:下一个MemTable的WAL日志文件的FileNumber</li>
<li>Pr
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(四)- SSTable解析</title>
<link href="https://blog.lovezhy.cc/2020/08/16/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E5%9B%9B%EF%BC%89-%20SSTable%E8%A7%A3%E6%9E%90/"/>
<id>https://blog.lovezhy.cc/2020/08/16/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E5%9B%9B%EF%BC%89-%20SSTable%E8%A7%A3%E6%9E%90/</id>
<published>2020-08-15T16:00:00.000Z</published>
<updated>2020-08-15T11:01:28.241Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>SSTable就是把一个跳表放入一个文件,但是我们不仅仅是把KeyValue写入文件就结束了,还需要做一些其他的事:</p><ol><li>写入KV</li><li>SSTable的一些元信息,如最大Key,最小Key,KV的个数等。</li><li>为KV维护一定的索引,加速查找</li><li>进行一定比例的数据压缩</li><li>数据完整性校验</li></ol><p>文件的大体格式如下:</p><p><img src="/images/leveldb/sstable1.png" style="zoom:50%;"></p><p>文件的格式大致分为一个一个的Block</p><ol><li>Data Block存储的是数据,就是KV。</li><li>MetaIndex Block</li><li>Index Block</li><li>Footer</li></ol><p>其中在源码中,DataBlock是大头,2和3叫Footer。</p><p>MetaIndex在Java版本的实现中是个空Block。</p><p>这三个Block的底层实现都是BlockBuilder,BlockBuilder是个存储KV的格式。</p><p>个人感觉上其实BlockBuilder是为了DataBlock打造的,而IndexBlock只是恰好复用了一下。</p><p>所以下文将的DataBlock其实就是DataBlock的机制。</p><a id="more"></a><h2 id="DataBlock"><a href="#DataBlock" class="headerlink" title="DataBlock"></a>DataBlock</h2><p>BlockBuilder的切分是根据每个Block的大小定的。</p><p>当一个Block的大小超过4 * 1024,也就是4M的时候,就会阶段,重启一个Block。</p><p>在Block内部的数据,也不是全部堆在一起,而是分为一组一组的,叫做DataGroup(非官方定义,我定的名字)。</p><p>分组个数是固定的,根据配置的blockRestartInterval来,默认是16个KV一组。</p><p>为什么要进行分组呢,其实这里要提一下写入数据时的压缩。</p><p>比如连续的两个Key,”the car”和”the car window”,他们有共同的前缀”the car”,对于这个前缀,我们可以只写入一份来起到数据压缩的效果。</p><p>如果我们不分组,那么我们找到一个Key,想知道他原来的Key是啥,得遍历前面所有的Key,这是不现实的。</p><p>所以每隔16个KV,我们就从头开始存储,不计算与之前的Key的共同前缀了。</p><p>假设我们put三个Entry,三个InternalKey如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>. userKey=foo123, seq = <span class="number">3</span>, type=VALUE, value = <span class="string">"fvdnvfdn"</span></span><br><span class="line"><span class="number">2</span>. userKey=foo456, seq = <span class="number">4</span>, type=VALUE, value = <span class="string">"nvjkdfniq"</span></span><br><span class="line"><span class="number">3</span>. userKey=foo444, seq = <span class="number">1</span>, type=VALUE, value = <span class="string">"vfnvfdn233"</span></span><br></pre></td></tr></table></figure><p>写SSTable了,真正写入的Bytes,会对InternalKey调用encode方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Slice <span class="title">encode</span><span class="params">()</span> </span>{</span><br><span class="line"> Slice slice = Slices.allocate(userKey.length() + SIZE_OF_LONG);</span><br><span class="line"> SliceOutput sliceOutput = slice.output();</span><br><span class="line"> sliceOutput.writeBytes(userKey);</span><br><span class="line"> sliceOutput.writeLong(SequenceNumber.packSequenceAndValueType(sequenceNumber, valueType));</span><br><span class="line"> <span class="keyword">return</span> slice;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>大致就是把seq + valueType拼成一个Long类型,和userKey合在了一起。</p><p>写入一个KV的操作基本流程如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> sharedKeyBytes = calculateSharedBytes(key, lastKey); <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">int</span> nonSharedKeyBytes = key.length() - sharedKeyBytes; <span class="comment">// 2</span></span><br><span class="line">VariableLengthQuantity.writeVariableLengthInt(sharedKeyBytes, block); <span class="comment">// 3</span></span><br><span class="line">VariableLengthQuantity.writeVariableLengthInt(nonSharedKeyBytes, block); <span class="comment">// 4</span></span><br><span class="line">VariableLengthQuantity.writeVariableLengthInt(value.length(), block); <span class="comment">// 5</span></span><br><span class="line">block.writeBytes(key, sharedKeyBytes, nonSharedKeyBytes); <span class="comment">// 6</span></span><br><span class="line">block.writeBytes(value, <span class="number">0</span>, value.length()); <span class="comment">// 7</span></span><br></pre></td></tr></table></figure><p>这里我们以</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>. userKey=foo123, seq = <span class="number">3</span>, type=VALUE, value = <span class="string">"fvdnvfdn"</span></span><br><span class="line"><span class="number">2</span>. userKey=foo456, seq = <span class="number">4</span>, type=VALUE, value = <span class="string">"nvjkdfniq"</span></span><br></pre></td></tr></table></figure><p>这两个KeyValue为例,假设<code>foo123</code>已经被写入了,就是<code>lastKey</code>,当前的<code>Key=foo456</code>,其实实际上不是这样的,这里实际拿到的值已经被拼了末尾的Long类型进去,这里为了好解释。同时写入数值也会做相应的转换,这里先忽略。</p><ol><li><code>foo123</code>和<code>foo456</code>的<code>sharedKeyBytes</code>,也就是开始的<code>foo</code>,数值为3</li><li><code>nonSharedKeyBytes</code>也即是<code>456</code>,数值也是3</li><li>这一步写入<code>sharedKeyBytes</code>值到文件中,文件内容变成 <code>3</code></li><li>写入<code>nonSharedKeyBytes</code>,文件内容为<code>3 | 3</code></li><li>写入Value的长度,文件内容变成<code>3 | 3 | 8 |</code></li><li>写入非共同前缀的字符也就是456,文件内容为 <code>3 | 3 | 8 | 456 |</code></li><li>写入Value的值,文件内容为 <code>3 | 3 | 8 | 456 | fvdnvfdn</code></li></ol><p>所以整体来看,一个DataGroup中每行Record的数据如下:</p><p><img src="/images/leveldb/sstable2.png" style="zoom:50%;"></p><p>前面提到,每个DataGroup有16行,超过16行之后,就会置LastKey为空,然后下一行开始的sharedKeyBytes就是0,nonSharedKeyBytes就是当前Key的完整长度。</p><p>下面就要为每个DataGroup建立索引,记录每个DataGroup的开始位置。</p><p>变量<code>restartPositions</code>就是为这个索引准备的。</p><p><img src="/images/leveldb/sstable3.png" style="zoom:50%;"></p><p>第一个DataGroup的位置就是0。</p><p>数据写完之后,把每个DataGroup的开始Offset写入到这个Block中。</p><p>看源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Slice <span class="title">finish</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">if</span> (!finished) {</span><br><span class="line"> finished = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (entryCount > <span class="number">0</span>) {</span><br><span class="line"> restartPositions.write(block); <span class="comment">//1</span></span><br><span class="line"> block.writeInt(restartPositions.size());</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> block.writeInt(<span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> block.slice();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//1的源码</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">write</span><span class="params">(SliceOutput sliceOutput)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> index = <span class="number">0</span>; index < size; index++) {</span><br><span class="line"> sliceOutput.writeInt(values[index]);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>数据写完后,如果符合压缩条件,还要进行数据压缩。</p><p>最后补上Trailer,Trailer就一个Byte和一个Int,Byte表示压缩方式,Int表示对数据的CRC32,用来验证数据完整性。</p><p>最后整个DataBlock的数据如下:</p><p><img src="/images/leveldb/sstable4.png" style="zoom:50%;"></p><h2 id="Index-Block"><a href="#Index-Block" class="headerlink" title="Index Block"></a>Index Block</h2><p>Index Block的内容每次写入一个DataBlock都会新增。</p><p>这里的代码其实还比较绕人,主要是<code>findShortestSeparator</code>这个方法。</p><p>让我们先抛开源代码,想一想对DataBlock建立索引需要记录哪些信息。</p><ol><li>最大Key,最小Key</li><li>KV的个数</li><li>StartOffset,EndOffset</li></ol><p>先来看看最大Key和最小Key的问题。</p><p>建立这个索引,主要是为了搜索Key,和最大的比较和最小的比较一下,就能知道是否在这个DataBlock中。</p><p>那么</p><p>问题1:是否需要最小Key?看源码,是Index Block中并没有存。</p><p>问题2:是否需要最大Key?是需要的,不过这里并没有存最大Key是什么。为什么?</p><p>或者我们再反问一句,存最大Key的意义是为了记录这个DataBlock里面的数据的边界,如果我们存储比MaxKey稍大一点的Key是不是也是同样的效果呢?</p><p>举个例子:MaxKey = helloworld,下一个Block的最小Key是hellozoomer,那么我们存储hellox,是不是可以起到同样的效果呢?</p><p>这就是方法<code>findShortestSeparator</code>的功能。</p><blockquote><p> 算出来一个Key,这个Key > MaxKey && Key < nextBlockFirstKey。能够起到和MaxKey一样的作用,同时存储时能够压缩空间。</p></blockquote><p>解决了问题2,我们再来看看问题1,为啥IndexBlock中没有存MinIndex,我感觉是这样</p><blockquote><p>又不是不能用。</p><p>shortestSeparator既可以表示前一个Block的MaxKey,又可以表示后一个Block的MinKey</p><p>IndexBlock复用的DataBlock的格式,存储MinKey不太方便</p><p>按照上面一个观点,其实一个DataBlock中的KV的个数,在IndexBlock中也没有存储。</p></blockquote><p>其实如果我们看到后面那个,<code>BlockIterator::seak</code>方法,进行二分查找的时候,是直接seek到那个位置,然后读取出第一个Key的。</p><p>下面看看IndexBlock的存储。</p><p>每写完一个DataBlock,会返回这个DataBlock的两个数值</p><ol><li>offset:在文件中的offset</li><li>dataSize:block的存储长度</li></ol><p>在IndexBlock中,将shorttestSeparator作为key,(offset + dataSize)统一作为Value,构造出一个KV的结构放进去。</p><p>其实IndexBlock和DataBlock底层都是BlockBuilder的实现。</p><p>所以IndexBlock的格式和DataBlock是一样的。</p><h2 id="MetaIndex-Block"><a href="#MetaIndex-Block" class="headerlink" title="MetaIndex Block"></a>MetaIndex Block</h2><p>我看Java版本的实现中是个空Block。</p><h2 id="Footer"><a href="#Footer" class="headerlink" title="Footer"></a>Footer</h2><p>Footer主要是为了记录MetaIndexBlock和IndexBlock的位置信息和一些填充字段和MagicWord。</p><p>直接上源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">writeFooter</span><span class="params">(Footer footer, SliceOutput sliceOutput)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// remember the starting write index so we can calculate the padding</span></span><br><span class="line"> <span class="keyword">int</span> startingWriteIndex = sliceOutput.size();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// write metaindex and index handles</span></span><br><span class="line"> writeBlockHandleTo(footer.getMetaindexBlockHandle(), sliceOutput); <span class="comment">//1</span></span><br><span class="line"> writeBlockHandleTo(footer.getIndexBlockHandle(), sliceOutput); <span class="comment">//2</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// write padding</span></span><br><span class="line"> sliceOutput.writeZero(ENCODED_LENGTH - SIZE_OF_LONG - (sliceOutput.size() - startingWriteIndex));<span class="comment">//3</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// write magic number as two (little endian) integers</span></span><br><span class="line"> sliceOutput.writeInt((<span class="keyword">int</span>) TableBuilder.TABLE_MAGIC_NUMBER); <span class="comment">//4</span></span><br><span class="line"> sliceOutput.writeInt((<span class="keyword">int</span>) (TableBuilder.TABLE_MAGIC_NUMBER >>> <span class="number">32</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>写入MetaIndexBlock的位置信息(offset + size)</li><li>写入IndexBlock的位置信息(offset + size)</li><li>padding</li><li>MagicNumber</li></ol><h2 id="Table的读取和iterator"><a href="#Table的读取和iterator" class="headerlink" title="Table的读取和iterator"></a>Table的读取和iterator</h2><p>对SSTable的读取,需要传入的是SSTable的fileChannel,然后进行初始化:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">Table</span><span class="params">(String name, FileChannel fileChannel, Comparator<Slice> comparator)</span> </span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="keyword">this</span>.name = name;</span><br><span class="line"> <span class="keyword">this</span>.fileChannel = fileChannel;</span><br><span class="line"></span><br><span class="line"> Footer footer = init(); <span class="comment">// 1</span></span><br><span class="line"> indexBlock = readBlock(footer.getIndexBlockHandle()); <span class="comment">//2</span></span><br><span class="line"> metaindexBlockHandle = footer.getMetaindexBlockHandle();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>seek到文件的尾部,把MetaIndexBlock的位置信息 + IndexBlock的位置信息读取出来</li><li>把IndexBlock的信息全部读取到内存中来</li></ol><p>因为DataBlock和IndexBlock的底层都是Block,所以这里先提一下对Block的迭代方法:</p><p>实现类是<code>BlockIterator</code></p><p>再回顾下之前的Block的格式</p><blockquote><p> 数据每16个KV一组分为DataGroup,在Block的尾部记录每个DataGroup的位置信息,也就是重启点位置。</p></blockquote><p>这里的迭代分几个重要的点:</p><ol><li>重启点的信息可以全部在初始化的时候就反序列化成数组</li><li>遍历DataGroup的KV时,需要记录上一个Key的原始值,不然不好恢复出当前Key的值</li><li>二分搜索时,转化成对RestartPositions数组的二分搜索</li></ol><p>讲完了对Block的迭代,下面讲讲对SSTable的迭代</p><p>对SSTable的迭代在方法<code>Table::iterator</code>中</p><p>方法的实现如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> TableIterator <span class="title">iterator</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> TableIterator(<span class="keyword">this</span>, indexBlock.iterator());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的<code>TableIterator</code>的实现思想和Block的迭代的实现思想差不多。</p><p>IndexBlock的KV,是对前面DataBlock的【分割Key】+ 位置信息的保存。</p><p>所以这里的<code>TableIterator</code>的实现思想是:</p><ol><li>遍历时,转化成对IndexBlock和DataBlock的双层遍历。</li><li>每次读取一个DataBlock在内存中。如果当前DataBlock遍历结束,就从IndexBlock读取下一个DataBlock的位置,seek到那个位置,把下一个DataBlock全部读取到内存中,再对这个DataBlock进行遍历。</li><li>二分查找,先对IndexBlock进行二分查找,找到【分割Key】的所在的DataBlock的位置信息,然后再读取改DataBlock,在这个DataBlock中进行二分搜索。</li></ol>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>SSTable就是把一个跳表放入一个文件,但是我们不仅仅是把KeyValue写入文件就结束了,还需要做一些其他的事:</p>
<ol>
<li>写入KV</li>
<li>SSTable的一些元信息,如最大Key,最小Key,KV的个数等。</li>
<li>为KV维护一定的索引,加速查找</li>
<li>进行一定比例的数据压缩</li>
<li>数据完整性校验</li>
</ol>
<p>文件的大体格式如下:</p>
<p><img src="/images/leveldb/sstable1.png" style="zoom:50%;"></p>
<p>文件的格式大致分为一个一个的Block</p>
<ol>
<li>Data Block存储的是数据,就是KV。</li>
<li>MetaIndex Block</li>
<li>Index Block</li>
<li>Footer</li>
</ol>
<p>其中在源码中,DataBlock是大头,2和3叫Footer。</p>
<p>MetaIndex在Java版本的实现中是个空Block。</p>
<p>这三个Block的底层实现都是BlockBuilder,BlockBuilder是个存储KV的格式。</p>
<p>个人感觉上其实BlockBuilder是为了DataBlock打造的,而IndexBlock只是恰好复用了一下。</p>
<p>所以下文将的DataBlock其实就是DataBlock的机制。</p>
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(三)- MemTable解析</title>
<link href="https://blog.lovezhy.cc/2020/08/15/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%89%EF%BC%89-%20MemTable%E8%A7%A3%E6%9E%90/"/>
<id>https://blog.lovezhy.cc/2020/08/15/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%89%EF%BC%89-%20MemTable%E8%A7%A3%E6%9E%90/</id>
<published>2020-08-14T16:00:00.000Z</published>
<updated>2020-08-15T10:58:24.460Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在LevelDB中,MemTable其实就是在内存中的一个跳表。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> ConcurrentSkipListMap<InternalKey, Slice> table;</span><br></pre></td></tr></table></figure><p>稍等,为什么table的Key不是<code>String</code>,而是<code>InternalKey</code>?</p><a id="more"></a><h2 id="InternalKey"><a href="#InternalKey" class="headerlink" title="InternalKey"></a>InternalKey</h2><p>我们看看InternalKey的定义</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">InternalKey</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Slice userKey;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">long</span> sequenceNumber;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ValueType valueType;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>里面包含三个字段,分别是</p><ul><li>userKey,就是我们用户增删的key</li><li>seqNumber是每一次操作的时候,由LevelDB分配的</li><li>ValueType分为两种,分别是删除和新增。</li></ul><p>我们以这个为例</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">db.put(<span class="string">"foo"</span>, <span class="string">"v1"</span>); <span class="comment">//1</span></span><br><span class="line">db.put(<span class="string">"foo"</span>, <span class="string">"v2"</span>); <span class="comment">//2</span></span><br><span class="line">db.delete(<span class="string">"foo"</span>); <span class="comment">//3</span></span><br></pre></td></tr></table></figure><ol><li>userKey=foo, seqNum = 1, valueType = VALUE</li><li>userKey=foo, seqNum = 2, valueType = VALUE</li><li>userKey=foo, seqNum = 3, valueType = DELETE</li></ol><p>这个时候,我们抛出一个问题,第2条put执行完之后,在MemTable中还有没有internelKey1?</p><p>其实这个问题取决于compare方法。</p><h2 id="comparator"><a href="#comparator" class="headerlink" title="comparator"></a>comparator</h2><p>新建这个跳表的时候,传入的<code>comparator</code>是<code>InternalKeyComparator</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">InternalKeyComparator#compare</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">compare</span><span class="params">(InternalKey left, InternalKey right)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">int</span> result = userComparator.compare(left.getUserKey(), right.getUserKey()); <span class="comment">//1</span></span><br><span class="line"> <span class="keyword">if</span> (result != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Long.compare(right.getSequenceNumber(), left.getSequenceNumber()); <span class="comment">//2</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>这里的<code>userComparator</code>是<code>BytewiseComparator</code>,而<code>BytewiseComparator</code>就是简单的比较<code>userKey</code>了。我们三次操作的userKey都是<code>foo</code>,所以这里返回的是<code>0</code></li><li>进入第二步,就是比较seqNum,seqNum越大的,InternalKey<strong>越小</strong>,注意,是越小。</li></ol><p>所以这里问题解答出来了,执行完这三个语句之后,三个InternalKey都会在跳表中,同时seqNum越大的,排名越靠前。</p><p>所以这就导致我们对某个userKey进行search的时候,我们的InternelKey的构造,userKey=foo,seq=currentSeq,valueType=Value。</p><p>这样查找的时候,使用ceilingEntry方法查找,这个方法查找的是最近的一个和他一样或者比他大的。</p><h2 id="compact"><a href="#compact" class="headerlink" title="compact"></a>compact</h2><p>一个MemTable什么时候停止写入,变成磁盘的SSTable呢?</p><p>答案在<code>makeRoomForWrite</code>方法中:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">memTable.approximateMemoryUsage() <= options.writeBufferSize()</span><br></pre></td></tr></table></figure><p>这里的<code>options.writeBufferSize</code>,默认情况下是4 << 20,也就是4G。</p><p>具体的compact逻辑在<code>compactMemTableInternal</code>方法中,使用tableBuilder,建立SSTable后,加入到VersionSet中。</p><p>但是具体加入到哪一个Level中呢?一定就是Level0吗?</p><p>答案是不一定。看代码,这里的meta就是新生成SSTable。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> level = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">if</span> (meta != <span class="keyword">null</span> && meta.getFileSize() > <span class="number">0</span>) {</span><br><span class="line"> Slice minUserKey = meta.getSmallest().getUserKey();</span><br><span class="line"> Slice maxUserKey = meta.getLargest().getUserKey();</span><br><span class="line"> <span class="keyword">if</span> (base != <span class="keyword">null</span>) {</span><br><span class="line"> level = base.pickLevelForMemTableOutput(minUserKey, maxUserKey);</span><br><span class="line"> }</span><br><span class="line"> edit.addFile(level, meta);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>具体放入哪个Level,是由<code>pickLevelForMemTableOutput</code>这个方法决定的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">pickLevelForMemTableOutput</span><span class="params">(Slice smallestUserKey, Slice largestUserKey)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> level = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">if</span> (!overlapInLevel(<span class="number">0</span>, smallestUserKey, largestUserKey)) {</span><br><span class="line"> <span class="comment">// Push to next level if there is no overlap in next level,</span></span><br><span class="line"> <span class="comment">// and the #bytes overlapping in the level after that are limited.</span></span><br><span class="line"> InternalKey start = <span class="keyword">new</span> InternalKey(smallestUserKey, MAX_SEQUENCE_NUMBER, ValueType.VALUE);</span><br><span class="line"> InternalKey limit = <span class="keyword">new</span> InternalKey(largestUserKey, <span class="number">0</span>, ValueType.VALUE);</span><br><span class="line"> <span class="keyword">while</span> (level < MAX_MEM_COMPACT_LEVEL) {</span><br><span class="line"> <span class="keyword">if</span> (overlapInLevel(level + <span class="number">1</span>, smallestUserKey, largestUserKey)) {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">long</span> sum = Compaction.totalFileSize(versionSet.getOverlappingInputs(level + <span class="number">2</span>, start, limit));</span><br><span class="line"> <span class="keyword">if</span> (sum > MAX_GRAND_PARENT_OVERLAP_BYTES) {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> level++;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> level;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法中,overlapInLevel方法是判断新SSTable的userKey范围是否与这一层的某个文件userKey的范围重合。</p><p>如果不重合,就直接下沉到下一个Level进行查找。</p><p>图例:MemTable变成了SSTable,范围是13 ~ 18。那么他会放到哪一层呢?</p><p><img src="/images/leveldb/memtable1.png" style="zoom:50%;"></p><ol><li>首先看Level 0,没有一个SSTable的Key范围和他有重合的</li><li>再看Level 1,还是没有一个SSTable的Key范围和他有重合的。</li><li>再次下沉到Level 2,发现第一个SSTable与他有重合。</li><li>所以新的SSTable会被放到Level 1。</li></ol><p>最终结果如下图所示。<br><img src="/images/leveldb/memtable2.png" style="zoom:50%;"></p>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在LevelDB中,MemTable其实就是在内存中的一个跳表。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> ConcurrentSkipListMap&lt;InternalKey, Slice&gt; table;</span><br></pre></td></tr></table></figure>
<p>稍等,为什么table的Key不是<code>String</code>,而是<code>InternalKey</code>?</p>
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(二)- Log文件格式</title>
<link href="https://blog.lovezhy.cc/2020/08/11/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%BA%8C%EF%BC%89-%20Log%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F/"/>
<id>https://blog.lovezhy.cc/2020/08/11/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%BA%8C%EF%BC%89-%20Log%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F/</id>
<published>2020-08-10T16:00:00.000Z</published>
<updated>2020-08-15T10:58:40.255Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>题目的Log指的是文件格式,并不特使某个FileType。</p><p>因为LOG文件和Manifest文件都是使用的Log格式的文件进行记录的。</p><p>这里主要讲解的是LogWriter和LogReader两个类。</p><p>格式镇楼:</p><p><img src="/images/leveldb/log文件格式1.png" alt="log文件格式" style="zoom:50%;"></p><a id="more"></a><h2 id="LogWriter"><a href="#LogWriter" class="headerlink" title="LogWriter"></a>LogWriter</h2><p>对于LogWriter而言,其实最重要的就是一个接口,Append一个Record:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">LogWriter</span> </span>{</span><br><span class="line"> <span class="comment">// Writes a stream of chunks such that no chunk is split across a block boundary</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">addRecord</span><span class="params">(Slice record, <span class="keyword">boolean</span> force)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>并且对于LogReader而言,它也是只有一个接口,读取一个Record:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LogReader</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Slice <span class="title">readRecord</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>既然所有的操作都是顺序写,顺序读,没有其他的操作,这里的Log文件格式就可以简单粗暴一点。</p><p>文件格式有三个概念要理清:</p><ol><li><p>Block,这里的Block固定是32K。读取文件时,每次读取一个Block大小,而写入时则需要按照Block的大小进行切分。</p></li><li><p>Record:就是入参,这里的Record并没有限制大小,所有大小是不定的。</p></li><li><p>Chunk:文件的写入单位是Chunk,Chunk也没有固定大小,但是不超过一个Block。</p><p>Chunk的诞生缘由是因为Block固定是32K,而Record是不限制大小的。如果一个Record太大,超过了32K,则需要切分为多个Chunk,放入两个Block。</p></li></ol><p>由于返回给上层的单位都是Record,所以我们需要知道下一个Record被分成了几个Chunk。</p><p>于是每次写入Chunk时,存入一个ChunkType。</p><ol><li>如果Recod只被切分成一个Chunk,则ChunkType=FULL</li><li>如果Record被切分成多个Chunk,则第一个Chunk的Type=First,最后一个Chunk的Type=Last,中间的Chunk的Type=MIDDLE。</li></ol><p>所以一个Chunk的具体格式如下:</p><p><img src="/images/leveldb/log文件格式2.png" alt="log文件格式" style="zoom:50%;"></p><p>对于读取而言,其实要做的也比较简单,先不断的去读取Block,然后迭代Block中的Chunk。</p><p>同时根据ChunkType的不同,不断迭代的读取Block,拼凑出一个完整的Block。</p>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>题目的Log指的是文件格式,并不特使某个FileType。</p>
<p>因为LOG文件和Manifest文件都是使用的Log格式的文件进行记录的。</p>
<p>这里主要讲解的是LogWriter和LogReader两个类。</p>
<p>格式镇楼:</p>
<p><img src="/images/leveldb/log文件格式1.png" alt="log文件格式" style="zoom:50%;"></p>
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>LevelDB源码解析(一)- 文件类型与文件名</title>
<link href="https://blog.lovezhy.cc/2020/08/10/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89-%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B%E4%B8%8E%E6%96%87%E4%BB%B6%E5%90%8D/"/>
<id>https://blog.lovezhy.cc/2020/08/10/LevelDB%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89-%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B%E4%B8%8E%E6%96%87%E4%BB%B6%E5%90%8D/</id>
<published>2020-08-09T16:00:00.000Z</published>
<updated>2020-08-15T10:47:36.960Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在源码中,特地有个枚举是表示FileType<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> FileType</span><br><span class="line">{</span><br><span class="line"> LOG,</span><br><span class="line"> DB_LOCK,</span><br><span class="line"> TABLE,</span><br><span class="line"> DESCRIPTOR,</span><br><span class="line"> CURRENT,</span><br><span class="line"> TEMP,</span><br><span class="line"> INFO_LOG <span class="comment">// Either the current one, or an old one</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>DB创建文件时将FileNumber加上特定的后缀作为文件名,FileNumber在内部是一个uint64_t类型,并且全局递增。不同类型的文件的拓展名不同,例如sstable文件是.sst,wal日志文件是.log。LevelDB有以下文件类型:</p><h1 id="一-FileNumber"><a href="#一-FileNumber" class="headerlink" title="一. FileNumber"></a>一. FileNumber</h1><p>每个文件的文件名都是一个数字,文件类型用后缀区分,即使是不同的后缀,文件的FileNumer也不会重复。</p><p>所以FileNumber类似于数据库的主键一下,自增的进行分配。</p><p>在VersionSet的变量中:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> AtomicLong nextFileNumber = <span class="keyword">new</span> AtomicLong(<span class="number">2</span>);</span><br></pre></td></tr></table></figure><p>可以看到默认的FileNumber是从2开始的。</p><p>为什么不是从1开始,因为1默认是给第一个Manifest文件</p><p>如果是重启的应用:</p><p>FileNumber会被写到CURRENT文件中,在<code>VersionSet::recover</code>中读取CURRENT文件指向的Manifest文件时恢复出来。</p><p>对于recover方法,本质上是CURRENT和Manifest文件,下面再讲。</p><h1 id="二-FileType"><a href="#二-FileType" class="headerlink" title="二. FileType"></a>二. FileType</h1><h2 id="2-1-DB-LOCK文件"><a href="#2-1-DB-LOCK文件" class="headerlink" title="2.1 DB_LOCK文件"></a>2.1 DB_LOCK文件</h2><p>这个文件作为文件锁存在,文件名就叫<code>LOCK</code>,里面不会保存任何东西</p><h2 id="2-2-TABLE文件"><a href="#2-2-TABLE文件" class="headerlink" title="2.2 TABLE文件"></a>2.2 TABLE文件</h2><p>就是SSTable文件,以<code>.sst</code>结尾</p><h2 id="2-3-LOG文件"><a href="#2-3-LOG文件" class="headerlink" title="2.3 LOG文件"></a>2.3 LOG文件</h2><p>类似于WAL文件,以<code>.log</code>结尾</p><p>注意,这里指的是文件类型,并不是文件格式。</p><p>项目中有个LogWriter类,这个生成的文件的文件格式相同,但是既可以作为LOG文件,又可以作为Manifest文件。</p><p>数据目录下只会有一个LOG文件,每次新启时,会将旧的删除,但是文件名中仍然带有FileNumber。</p><h2 id="2-4-DESCRIPTOR"><a href="#2-4-DESCRIPTOR" class="headerlink" title="2.4 DESCRIPTOR"></a>2.4 DESCRIPTOR</h2><p>就是常说的Manifest文件,以<code>MANIFEST-</code>开头</p><p>每次Compact之后,都会产生当前Compact后SSTable文件的修改VersionEdit。</p><p>同时将VersionEdit文件的内容写入Append到Manifest文件。</p><h2 id="2-5-CURRENT"><a href="#2-5-CURRENT" class="headerlink" title="2.5 CURRENT"></a>2.5 CURRENT</h2><p>文件名就叫CURRENT,里面的内容是当前的MANIFEST文件的文件名。</p><h2 id="2-6-Temp"><a href="#2-6-Temp" class="headerlink" title="2.6 Temp"></a>2.6 Temp</h2><p>因为Log文件和Manifest文件都只有一个,在使用新的覆盖的时候,需要先创建一个Temp文件,然后再rename成真正的。</p><h2 id="2-7-INFO-LOG"><a href="#2-7-INFO-LOG" class="headerlink" title="2.7 INFO_LOG"></a>2.7 INFO_LOG</h2><p>似乎没用到</p>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在源码中,特地有个枚举是表示FileType<br><figure class="highlight java"><table><tr><t
</summary>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/categories/LevelDB/"/>
<category term="LevelDB" scheme="https://blog.lovezhy.cc/tags/LevelDB/"/>
</entry>
<entry>
<title>GuavaRateLimiter的理解</title>
<link href="https://blog.lovezhy.cc/2020/07/24/GuavaRateLimiter%E7%9A%84%E7%90%86%E8%A7%A3/"/>
<id>https://blog.lovezhy.cc/2020/07/24/GuavaRateLimiter%E7%9A%84%E7%90%86%E8%A7%A3/</id>
<published>2020-07-23T16:00:00.000Z</published>
<updated>2020-07-24T11:18:35.463Z</updated>
<content type="html"><![CDATA[<h2 id="一-介绍"><a href="#一-介绍" class="headerlink" title="一. 介绍"></a>一. 介绍</h2><p>项目中一直在使用RateLimiter进行单机的限流,但是没有去了解他的运作原理,这里就简单记录下,为以后学习Sentinal做铺垫。</p><a id="more"></a><p>这里强烈推荐酷家乐的文章<a href="https://tech.kujiale.com/ratelimiter-architecture/" target="_blank" rel="noopener">《RateLimiter解析(一) ——设计哲学与快速使用》</a></p><p>把定积分写入技术文档我也是第一次见,实打实的说,核心部分我也没看懂。</p><p>本文仅仅讨论<code>SmoothBursty</code>的情况,<code>SmoothWarmingUp</code>先放着,因为不是很理解。</p><h2 id="二-个人困惑点"><a href="#二-个人困惑点" class="headerlink" title="二. 个人困惑点"></a>二. 个人困惑点</h2><h3 id="2-1-放令牌的时间窗口"><a href="#2-1-放令牌的时间窗口" class="headerlink" title="2.1 放令牌的时间窗口"></a>2.1 放令牌的时间窗口</h3><p>其实在阅读源码前,自己对单机限流的工具进行了脑补,想单机限流工具的实现方案。</p><p>之前了解的方法,基本就两种,一种是漏桶,一种是令牌桶。</p><p>我比较疑惑的是令牌桶方式的放令牌的时间窗口,比如说,我设置的QPS是1分钟6000。</p><p>放令牌的时候,是每隔一分钟,丢6000个进去吗?</p><p>还是把6000个平均的分布在这一分钟的跨度内,每秒放100个进去呢?</p><p>个人理解比较合适的方式是每秒放100个进去。</p><p>因为如果你每分钟就立即放6000个进去,如果在1S内立马被消耗完了,那就剩余的59S就在那儿干等。</p><p>对于服务器而言,比如说Mysql,显然是不太合理的。</p><p>这种场景下,Mysql的QPS可能达到6000/S,如果抗过去了这1S,剩余的59S一个请求也就没有。</p><p>但是如果设置的QPS是1S6000。</p><p>那么这种放令牌的方式是怎么放呢?</p><p>每隔1S就放6000个令牌,还是再下降一个计时单位到毫秒进行发放?</p><p>如果每次的放置时间窗口都下降一个单位,再小的时间单位对于计算机而言也没有意义。</p><h3 id="2-2-放令牌的方式"><a href="#2-2-放令牌的方式" class="headerlink" title="2.2 放令牌的方式"></a>2.2 放令牌的方式</h3><p>往令牌桶中放令牌的方式,是单独起一个线程,每隔一段时间醒一次,往令牌桶中放令牌吗?</p><p>这种方式看起来有点太消耗内存和CPU了。</p><p>不知道RateLimiter是怎么实现的</p><h2 id="三-具体实现"><a href="#三-具体实现" class="headerlink" title="三. 具体实现"></a>三. 具体实现</h2><p>先来看看创建方法:<code>RateLimiter create(double permitsPerSecond)</code></p><p>RateLimiter的创建方法,是个double类型的值,表示1s可以产生的令牌数。</p><p>进入<code>doSetRate</code>方法,注意两行代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">double</span> stableIntervalMicros = (<span class="keyword">double</span>)TimeUnit.SECONDS.toMicros(<span class="number">1L</span>) / permitsPerSecond;</span><br><span class="line"><span class="keyword">this</span>.stableIntervalMicros = stableIntervalMicros;</span><br></pre></td></tr></table></figure><p>这里的<code>stableIntervalMicros</code>的值,就是以微秒为时间单位,计算多少微秒可以产生一个令牌。</p><p>所以这里基本上可以解答我的第一个疑惑,RateLimiter的时间窗口是微秒。</p><p>1s = 1,000millis = 1,000,000micros。</p><blockquote><p>这里为什么是微秒,能不能是毫秒的单位?我其实还不是很懂。</p><p>有时间看看其他的限流器的实现</p></blockquote><p>令牌桶与漏桶算法的区别就是在空闲时可以保存一部分的令牌,那么是多少个呢?</p><p>这个变量保存在<code>SmoothRateLimiter::maxPermits</code>中,如果看调用的话,根据是<code>SmoothBursty</code>还是<code>SmoothWarmingUp</code>是不同的,</p><p>这里我们仅仅讨论<code>SmoothBursty</code>的情况,因为<code>SmoothWarmingUp</code>实在是看不懂。</p><p>在<code>SmoothBursty.doSetRate</code>方法中,进行了这个值的设置,<code>maxBurstSeconds</code>的默认值是1s。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maxPermits = maxBurstSeconds * permitsPerSecond;</span><br></pre></td></tr></table></figure><p>所以这个空闲时最多保留的令牌数就是我们构造参数传入的值。</p><p>最后来看看最后一个问题,何时放置令牌以及取令牌的过程。</p><p>说到这儿其实已经知道了,RateLimiter并没有单独启一个线程去放令牌,而是使用了<strong>取时计算</strong>的方式。</p><p>而使用这种方式也是有弊端的,就是<code>acquire</code>方法需要加锁:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">RateLimter##acquire()</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">double</span> <span class="title">acquire</span><span class="params">(<span class="keyword">int</span> permits)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> microsToWait = reserve(permits);</span><br><span class="line">...</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">final</span> <span class="keyword">long</span> <span class="title">reserve</span><span class="params">(<span class="keyword">int</span> permits)</span> </span>{</span><br><span class="line"> checkPermits(permits);</span><br><span class="line"> <span class="keyword">synchronized</span> (mutex()) { <span class="comment">//加锁</span></span><br><span class="line"> <span class="keyword">return</span> reserveAndGetWaitLength(permits, stopwatch.readMicros());</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>取令牌时,我们要知道目前令牌桶中存有几个令牌。计算方式也比较简单,记录下上一次获取令牌的时间t,</p><blockquote><p> 公式:<code>(now - t) / windowTime</code></p></blockquote><p>就能计算出,空闲的这段时间内,产生了多少空闲的令牌。</p><p>同时记得要和<code>maxPermits</code>取个最小值。</p><p>这个方法在方法<code>resync</code>中:</p><p>方法<code>coolDownIntervalMicros()</code>在<code>SmoothBursty</code>就是上文提到的固定的时间窗口<code>stableIntervalMicros</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">resync</span><span class="params">(<span class="keyword">long</span> nowMicros)</span> </span>{</span><br><span class="line"><span class="comment">// if nextFreeTicket is in the past, resync to now</span></span><br><span class="line"> <span class="keyword">if</span> (nowMicros > nextFreeTicketMicros) {</span><br><span class="line"> storedPermits = min(maxPermits,</span><br><span class="line"> storedPermits</span><br><span class="line"> + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());</span><br><span class="line"> nextFreeTicketMicros = nowMicros;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>得到了获取时,当前令牌桶中有多少令牌,最后看整体的获取逻辑:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> <span class="keyword">long</span> <span class="title">reserveEarliestAvailable</span><span class="params">(<span class="keyword">int</span> requiredPermits, <span class="keyword">long</span> nowMicros)</span> </span>{</span><br><span class="line"> resync(nowMicros);</span><br><span class="line"> <span class="keyword">long</span> returnValue = nextFreeTicketMicros;</span><br><span class="line"> <span class="keyword">double</span> storedPermitsToSpend = min(requiredPermits, <span class="keyword">this</span>.storedPermits);</span><br><span class="line"> <span class="keyword">double</span> freshPermits = requiredPermits - storedPermitsToSpend; <span class="comment">// 1</span></span><br><span class="line"> <span class="keyword">long</span> waitMicros = storedPermitsToWaitTime(<span class="keyword">this</span>.storedPermits, storedPermitsToSpend)</span><br><span class="line"> + (<span class="keyword">long</span>) (freshPermits * stableIntervalMicros); <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">this</span>.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros); <span class="comment">// 3</span></span><br><span class="line"> } <span class="keyword">catch</span> (ArithmeticException e) {</span><br><span class="line"> <span class="keyword">this</span>.nextFreeTicketMicros = Long.MAX_VALUE;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>.storedPermits -= storedPermitsToSpend; <span class="comment">// 4</span></span><br><span class="line"> <span class="keyword">return</span> returnValue;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li><p>比较想要获取的令牌数和目前令牌桶中的令牌数量,得到除去令牌桶中的令牌数,有多少令牌还未产生,需要慢慢等待。</p><p>比如令牌桶中只有3个令牌,而需要获取10个令牌,则需要额外的等待7个令牌的获取时间</p></li><li><p><code>storedPermitsToWaitTime</code>在<code>SmoothBursty</code>是返回的0。</p><p>所以这里的<code>waitMicors</code>就是<code>freshPermits * timeWindow</code></p></li><li><p>由于下面的<code>freshPermits</code>个令牌都被这个请求拿走了,所以下一次产生令牌的时间需要加上<code>waitMicros</code></p></li></ol><p>这里假设的是令牌不够的情况,那么如果空闲时间的令牌数足够的情况是呢?</p><p>比如令牌桶中有10个令牌,请求3个。</p><ol><li><code>freshPermits</code> = 0</li><li><code>waitMicros</code> = 0</li><li><code>nextFreeTicketMicros</code> 不变</li><li>需要减去使用完的令牌数</li></ol><h2 id="四-总结"><a href="#四-总结" class="headerlink" title="四. 总结"></a>四. 总结</h2><p>看到这儿对RateLimiter有了大概的了解</p><ol><li>对于存放令牌的时间窗口是微秒。</li><li>是完全公平的限流器,来请求的线程按照顺序等待。</li></ol><p>并没有可扩展的其他复杂逻辑。比如排队多了,丢弃后续的请求,而不是一直卡在那儿等。</p>]]></content>
<summary type="html">
<h2 id="一-介绍"><a href="#一-介绍" class="headerlink" title="一. 介绍"></a>一. 介绍</h2><p>项目中一直在使用RateLimiter进行单机的限流,但是没有去了解他的运作原理,这里就简单记录下,为以后学习Sentinal做铺垫。</p>
</summary>
<category term="Guava" scheme="https://blog.lovezhy.cc/categories/Guava/"/>
<category term="Guava" scheme="https://blog.lovezhy.cc/tags/Guava/"/>
</entry>
<entry>
<title>天池比赛经历</title>
<link href="https://blog.lovezhy.cc/2020/06/29/%E5%A4%A9%E6%B1%A0%E6%AF%94%E8%B5%9B%E7%BB%8F%E5%8E%86/"/>
<id>https://blog.lovezhy.cc/2020/06/29/%E5%A4%A9%E6%B1%A0%E6%AF%94%E8%B5%9B%E7%BB%8F%E5%8E%86/</id>
<published>2020-06-28T16:00:00.000Z</published>
<updated>2020-06-29T14:05:21.472Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>第一次参加这种类似黑客马拉松的比赛,感觉还是挺新奇的。</p><p>虽然成绩不咋好,但是毕竟第一次参加,还是记录一下。</p><a id="more"></a><h1 id="成绩"><a href="#成绩" class="headerlink" title="成绩"></a>成绩</h1><p>写了大概5天吧。</p><p>Score从900 -> 3118 -> 6454 -> 49051 -> 75348 -> 100089 -> 116530</p><p>看时间消耗的话,正好卡在20s这儿,排名,6月29号看65名这儿。</p><p>前几名都是2s多的都是什么神仙啊。<br>我看我GC日志,单台机器的STW都不止2s了。</p><h1 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h1><p>对于聚合服务器而言,就是不断的请求流处理机器,获得出错的日志的traceId,取出交集作为Response响应,发送给所有服务器。</p><p>流处理机器获得响应后,再次把需要聚合的所有日志发送给聚合服务器。</p><p>而对于流处理的服务器而言:</p><ol><li>建立与日志文件的Http请求,获得InputStream对象,不断的read,一行一行的获取日志。</li><li>在内存中建立索引</li><li>配合聚合服务器做出响应</li></ol><p>这里涉及到一个问题,就是怎么判断一个traceId的所有日志能不能丢。</p><p>这里比赛放出了个限定条件:</p><blockquote><p>为了方便选手解题,相同traceId的第一条数据(span)到最后一条数据不会超过2万行。方便大家做短暂缓存的流式数据处理。真实场景中会根据时间窗口的方式来处理,超时数据额外处理。</p></blockquote><p>所以基本上,我们不断的read日志的时候,已经有2w条没有出现TraceId=X了,那么就可以认为下面不会再出现TraceId=X的日志了。</p><p>如果索引中TraceId=X的日志,没有出现Error,就可以丢掉了。</p><h1 id="优化思路"><a href="#优化思路" class="headerlink" title="优化思路"></a>优化思路</h1><ol><li>SpringMVC改成自定义协议,减少Http协议的解析时间,效果:低</li><li>传输数据使用更好的序列化工具,减少Http传输体积和反序列化时间,效果:低</li><li>HttpClient使用连接池,减少TCP每次都进行握手的时间,效果:低</li><li>调整GC,很多对象都是朝生夕死,调大年轻代内存,效果:一般</li><li>分析日志,手写方法提取出spanId和startTime和tag,不用String.split(),效果:显著</li><li>使用Http Range方法拉取trace文件,分批处理,最后合并checksum的map,效果:显著</li><li>使用生产者消费者模型,生产者线程拉取Http的Stream,解析成Model,消费者通过阻塞队列拉取,效果:待验证</li></ol><h1 id="感触"><a href="#感触" class="headerlink" title="感触"></a>感触</h1><p>其实这里有两个性能瓶颈是很容易想到的,因为这里最大的问题就是数据量非常大,单文件2500w行。</p><ol><li>Http请求数据,IO性能</li><li>parse日志,提取spanId</li><li>建立日志的索引</li></ol><p>IO性能这个基本无解,没什么好的优化方法,或者换C++是个好思路(手动狗头)</p><p>parse日志这个也基本无解,这里的parse其实还算简单的</p><p>建立日志的索引倒是考究很多。</p><p>还有个就是不能太迷信多线程,这里简单提一个例子,就是parse日志的过程中,我曾经改成了多线程的方法,但是发现CPU消耗上去了,但是性能几乎没啥变化,本地测甚至还有点下降。</p><h1 id="吐槽"><a href="#吐槽" class="headerlink" title="吐槽"></a>吐槽</h1><p>官方的题目中,明明写着是面向数据流的,然后还允许通过Range方式拉取,这不打脸么。</p>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>第一次参加这种类似黑客马拉松的比赛,感觉还是挺新奇的。</p>
<p>虽然成绩不咋好,但是毕竟第一次参加,还是记录一下。</p>
</summary>
<category term="天池" scheme="https://blog.lovezhy.cc/categories/%E5%A4%A9%E6%B1%A0/"/>
<category term="天池" scheme="https://blog.lovezhy.cc/tags/%E5%A4%A9%E6%B1%A0/"/>
</entry>
<entry>
<title>Java堆外内存理解</title>
<link href="https://blog.lovezhy.cc/2020/06/10/Java%E5%A0%86%E5%A4%96%E5%86%85%E5%AD%98%E7%90%86%E8%A7%A3/"/>
<id>https://blog.lovezhy.cc/2020/06/10/Java%E5%A0%86%E5%A4%96%E5%86%85%E5%AD%98%E7%90%86%E8%A7%A3/</id>
<published>2020-06-09T16:00:00.000Z</published>
<updated>2020-06-29T14:04:22.314Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这文章算是同事约稿(手动狗头),但是从搜集资料的过程中确实也学到了不少。</p><a id="more"></a><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><p>申请堆外内存:使用unsafe.allocate,返回一个long类型,表示起始地址</p><p>使用:unsafe.putByte,unsafe.getByte</p><p>释放堆外内存:使用unsafe.free</p><p>JDK自动释放,使用虚引用注册一个DirectByteBuffer的即将被GC的Hook,在这个Hook中调用unsafe.free</p><h1 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h1><p>使用场景:</p><ol><li>JDK IO,对fd进行read和write时,申请一个堆外内存作为中转 -> [源码IOUtil]<ol><li>为啥?</li><li>解答:R大的解答:<a href="https://www.zhihu.com/question/57374068/answer/152691891" target="_blank" rel="noopener">https://www.zhihu.com/question/57374068/answer/152691891</a></li></ol></li><li>Netty作为ByteBuffer内存池</li><li>OHC框架,Off-Heap-Cache,作为堆外缓存使用</li></ol><h1 id="优势和劣势"><a href="#优势和劣势" class="headerlink" title="优势和劣势"></a>优势和劣势</h1><p>为什么使用堆外内存:</p><ol><li>IO友好,使用场景1</li><li>自己管理内存,减少堆内存占用,减轻JVM GC压力</li></ol><p>劣势:</p><ol><li>常见内存泄漏问题,难以排查</li><li>只能是byte[]对象,存储与获取其他对象,需要自己序列化和反序列化</li><li>性能问题(相对于GC消耗需要辩证看待)<ol><li>申请和释放堆外内存消耗较大(NativeMethod,底层是malloc和free)</li><li>访问堆外内存速度不如访问JVM对象(”leave” the JVM “context”)</li></ol></li><li>内存占用:如果使用内存池管理,实际占用比实际使用的较大</li></ol><h1 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h1><p>适用于:</p><ol><li>IO的Buffer</li><li>单次使用:内存占用较大的,不影响JVM堆</li><li>频繁使用:<ol><li>占用空间较大,对象存活时间较长,配合内存池使用:减轻GC压力,减少GC毛刺与抖动,范例OHC</li><li>生命周期较短,但是量多且涉及IO,配合内存池使用:范例Netty</li></ol></li></ol><h1 id="补充阅读"><a href="#补充阅读" class="headerlink" title="补充阅读"></a>补充阅读</h1><ol><li><a href="https://github.com/snazy/ohc" target="_blank" rel="noopener">https://github.com/snazy/ohc</a></li><li>视频:Netty-A framework to rule them all,B站和Youtube均有,讲了Netty在内存使用上的优化</li><li>Java未提供munmap方法:<a href="https://www.cnblogs.com/huxi2b/p/6637425.html" target="_blank" rel="noopener">https://www.cnblogs.com/huxi2b/p/6637425.html</a></li><li>使用堆外内存优化GC,<a href="https://juejin.im/post/5cdf8df4f265da1bd260bae9" target="_blank" rel="noopener">https://juejin.im/post/5cdf8df4f265da1bd260bae9</a></li><li>堆外内存溢出问题排查,<a href="https://coldwalker.com/2018/12//troubleshooter_directbytebuffer_memory_issue/" target="_blank" rel="noopener">https://coldwalker.com/2018/12//troubleshooter_directbytebuffer_memory_issue/</a></li></ol>]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这文章算是同事约稿(手动狗头),但是从搜集资料的过程中确实也学到了不少。</p>
</summary>
<category term="Java基础" scheme="https://blog.lovezhy.cc/categories/Java%E5%9F%BA%E7%A1%80/"/>
<category term="Java" scheme="https://blog.lovezhy.cc/tags/Java/"/>
</entry>
<entry>
<title>论文翻译-What’s-Really-New-wit-NewSQL</title>
<link href="https://blog.lovezhy.cc/2020/06/08/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-What%E2%80%99s-Really-New-with-NewSQL/"/>
<id>https://blog.lovezhy.cc/2020/06/08/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91-What%E2%80%99s-Really-New-with-NewSQL/</id>
<published>2020-06-07T16:00:00.000Z</published>
<updated>2020-06-08T15:24:40.003Z</updated>
<content type="html"><![CDATA[<p><strong><a href="/files/newsql.pdf">论文PDF下载</a></strong></p><a id="more"></a><h2 id="标题"><a href="#标题" class="headerlink" title="标题"></a>标题</h2><p><strong>What’s Really New with NewSQL?</strong></p><p>Andrew Pavlo Carnegie Mellon University [email protected]</p><p>Matthew Aslett 451 Research [email protected]</p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>一类新的数据库管理系统(DBMSs)被称为NewSQL,它声称自己有能力以传统系统所不具备的方式扩展现代在线交易处理(OLTP)的工作负载。</p><p>这篇论文的作者之一(论文的两位作者分别是pavlo和matthew.aslett)在2011年的一份商业分析报告中首次使用了NewSQL这个术语,该报告讨论了新的数据库系统作为对这些老牌厂商(Oracle、IBM、微软)的挑战者的崛起。</p><p>而另一位作者则是第一批研究NewSQL DBMS的人。</p><p>从那时起,一些公司和研究项目都用这个词来描述他们的系统(无论他们是否使用正确)。</p><p><br><br>考虑到关系型DBMS已经有四十多年的历史,我们有理由问一下,NewSQL的优越性的说法是真的,还是仅仅是营销手段。</p><p>如果它们确实能够获得更好的性能,那么接下来的问题是,它们是否有什么新技术使它们能够获得这些优势,还是说硬件已经进步了很多,以至于现在早些年的瓶颈不再是问题。</p><p><br><br>为了做到这一点,我们首先讨论数据库的历史,以说明NewSQL系统是如何产生的。</p><p>然后,我们将详细解释NewSQL这个词的含义,以及属于这个定义下的不同类别的系统。</p><h2 id="1-DBMS简史"><a href="#1-DBMS简史" class="headerlink" title="1. DBMS简史"></a>1. DBMS简史</h2><p>最早的DBMS是在1960年代中期上线的。</p><p>最早的其中一个是IBM的IMS系统,用于跟踪土星五号和阿波罗太空探索项目的物资和零件库存。</p><p>它引入了应用程序的代码应该和它所操作的数据分开的理念。这使得开发人员可以编写的应用程序只关注数据的访问和操作,而不是与如何实际执行这些操作相关的复杂性和开销。</p><p>IMS之后,在20世纪70年代初,IBM公司的System R和加州大学的INGRES等第一批关系型DBMS的开创性工作也随之而来。INGRES很快被其他大学的信息系统所采用,并在70年代末实现了商业化。</p><p>大约在同一时间,甲骨文公司发布了他们的第一个DBMS版本,与System R的设计相似。<br>1980年代初,其他的公司也纷纷成立,试图重复第一批商业化DBMS的成功,包括Sybase和Informix。</p><p>尽管IBM从未向公众提供过System R,但它后来在1983年发布了一个新的关系型DBMS (DB2),其使用了System R的部分代码库。</p><p><br><br>20世纪80年代末和90年代初诞生了一类新的DBMS,这些DBMS的设计是为了克服关系模型和面向对象编程语言之间的阻抗不匹配的问题。</p><p>然而,这些面向对象的DBMS从未在市场上得到广泛的应用,因为它们缺乏像SQL那样的标准接口。</p><p>但是当十年后各大厂商增加了对对象和XML的支持时,其中的许多想法最终被纳入到了面向对象的<br>DBMS中,20多年后又在面向文档的NoSQL系统中再次被纳入。</p><p><br><br>在90年代,另一个值得注意的事件是今天的两个主要的开源DBMS项目的开始。</p><p>MySQL是1995年在瑞典开始的,它是基于早期基于ISAM的mSQL系统。</p><p>PostgreSQL开始于1994年,当时两个伯克利大学的研究生fork了20世纪80年代原始的基于QUEL的Post-gres代码,增加了对SQL的支持。</p><p><br><br>2000年代,互联网应用的出现,对硬件资源的要求比前几年的应用更具挑战性。</p><p>它们需要扩大规模以支持大量的并发用户,并且必须一直在线。<br>但这些新应用使用的数据库一直是一个瓶颈,因为资源需求远远超过了当时的DBMS和硬件所能支持的范围。 </p><p>许多人尝试了最简单的选择,即通过将数据库移动到具有更好的硬件的机器上,垂直扩展他们的DBMS。<br>然而,这样做只能提高性能,回报率却越来越低。 </p><p>此外,将数据库从一台机器移动到另一台机器是一个复杂的过程,往往需要大量的停机时间,这对于这些基于Web的应用来说是不可接受的。 </p><p>为了克服这个问题,一些公司创建了定制的中间件,将单节点的DBMS分片到成本较低的机器集群上。这样的中间件向应用程序展示了一个存储在多个物理节点上的单一逻辑数据库。</p><p>当应用程序针对这个数据库发出查询时,中间件会重定向和/或重写它们,将它们分配到集群中的一个或多个节点上执行。</p><p>节点执行这些查询,并将结果发送回中间件,中间件再将其汇总成一个单一的响应给应用。</p><p>这种中间件方法的两个显著例子是eBay的基于Oracle的集群和Google的基于MySQL的集群。</p><p>这种方法后来被Facebook采用,他们自己的MySQL集群至今仍在使用。</p><p><br><br>Sharding 中间件对于简单的操作,如读取或更新一条记录,效果很好。</p><p>但要在事务或联接表中执行更新一条以上记录的查询则比较困难。</p><p>因此,这些早期的中间件系统并不支持这些类型的操作。例如,eBay在2002年的中间件,要求其开发人员在应用级代码中实现所有的join操作。</p><p><br><br>最终,这些公司中的一些脱离了中间件,开发了自己的分布式DBMS。</p><p>这样做的动机有三个方面。</p><p>最重要的是,当时传统的DBMS注重一致性和正确性,而牺牲了可用性和性能。但这种权衡被认为不适合于需要一直在线并需要进行大量并发操作的基于Web的应用程序。</p><p>其次,有人认为使用像MySQL这样的全功能DBMS作为 “哑巴 “数据存储,会有太多的开销。</p><p>同样,也有人认为关系型模型不是表示应用程序数据的最佳方式,使用SQL对于简单的查询查询来说是一种过度的做法。</p><p><br><br>这些问题被证明是2000年代中后期NoSQL1运动的推动力的起源。</p><p>这些NoSQL系统的核心是放弃了传统DBMS的强大的事务保证和关系模型,而倾向于最终的一致性和替代性数据模型(例如,键/值、图、文档)。</p><p>这是因为有些人认为,现有的DBMS的特点抑制了它们的扩展能力和实现Web的应用所需的高可用<br>性。</p><p>最早遵循这一信条最著名的两个系统是Google的BigTable和Amazon的Dynamo。</p><p>这两个系统一开始都不在各自公司之外出现(尽管它们现在是以云服务的形式出现),因此,其他组织创建了自己的开源克隆系统。</p><p>这些包括Facebook的Cassandra(基于BigTable和Dynamo)和PowerSet的Hbase(基于BigTable)。</p><p>其他初创公司创建了自己的系统,这些系统不一定是Google或Amazon的系统的复制品,但仍然遵循NoSQL哲学的原则;其中最著名的是MongoDB。</p><p><br><br>到了2000年代末,现在已经有了多种多样的、可扩展的、更实惠的分布式DBMS。</p><p>使用NoSQL系统的好处是,开发人员可以专注于他们的应用中对他们的业务或组织更有利的方面,而不必担心如何扩展DBMS。</p><p>然而,许多应用无法使用这些NoSQL系统,因为它们不能放弃强大的交易和一致性要求。</p><p>这种情况对于处理高等级数据的企业系统(例如,财务和订单处理系统)很常见。</p><p>一些组织,最明显的是Google,发现NoSQL DBMS导致他们的开发人员花了太多时间编写代码来处理一致的数据,而使用事务使他们的工作效率更高,因为它们提供了一个有用的抽象,更容易被人类理解。</p><p>因此,这些组织唯一的选择是,要么购买更强大的单节点机器,并对DBMS进行动态扩展,要么开发自己的支持事务的自定义分片中间件。</p><p>这两种方法都非常昂贵,因此对很多人来说都不是一个选择。正是在这种环境下,NewSQL系统应运而生。</p><h2 id="2-NewSQL的崛起"><a href="#2-NewSQL的崛起" class="headerlink" title="2. NewSQL的崛起"></a>2. NewSQL的崛起</h2><p>我们对NewSQL的定义是,它们是一类现代关系型DBMS,试图为OLTP读写工作负载提供与NoSQL相同的可扩展性能,同时仍然保持事务的ACID保证。</p><p>换句话说,这些系统希望实现与2000年代的NoSQL DBMS一样的可扩展性,但仍然保持1970-80年代的传统DBMS的关系模型(带SQL)和事务支持。</p><p>这使得应用程序可以执行大量的并发事务来获取新的信息,并使用SQL(代替专有的API)修改数据库的状态。</p><p>如果一个应用程序使用了NewSQL DBMS,那么开发人员就不需要像在NoSQL系统中那样编写逻辑来处理最终一致的更新。</p><p>正如我们在下面讨论的那样,这种解释涵盖了许多学术和商业系统。</p><p><br><br>我们注意到,在2000年代中期,有一些人认为符合这个标准的数据仓库DBMS出现了(如Vertica、Greenplum、Aster Data)。</p><p>这些DBMS针对的是在线分析处理(OLAP)工作负载,不应该被认为是NewSQL系统。</p><p>OLAP DBMS专注于执行复杂的只读查询(即聚合、多路join),这些查询需要很长时间来处理大数据集(例如,几秒钟甚至几分钟)。</p><p>这些查询中的每个查询都可能与前者有明显的不同。</p><p>另一方面,NewSQL DBMS所针对的应用的特点是执行读写事务,这些事务(1)是短暂的(即没有用户停顿),(2)使用索引查找触及一小部分数据(即没有完整的表扫描或大型分布式join),(3)是重复性的(即用不同的输入执行相同的查询)。</p><p>其他的人认为,NewSQL系统的实现必须使用(1)无锁并发控制方案和(2)无共享的分布式架构。</p><p>所有我们在第3节中将其归类为NewSQL的DBMS确实具有这些属性,因此我们同意这一定义。</p><h2 id="3-NewSQL分类"><a href="#3-NewSQL分类" class="headerlink" title="3. NewSQL分类"></a>3. NewSQL分类</h2><p>鉴于上述定义,我们现在来看看今天的NewSQL DBMS的情况。</p><p>为了简化分析,我们将根据系统的优点对系统进行分类。</p><p>我们认为最能代表NewSQL系统的三类是:(1)使用新的架构从头开始构建的新型系统,(2)重新实现与Google等人在2000年代开发的分片基础架构相同的中间件,以及(3)同样基于新架构的云计算供应商提供的数据库即服务。</p><p><br><br>两位作者之前都将替换现有的单节点DBMS的存储引擎的方案纳入了我们对NewSQL系统的分类中。</p><p>其中最常见的例子是对MySQL默认的InnoDB存储引擎的替代(例如TokuDB、ScaleDB、Akiban、deepSQL)。</p><p>使用新引擎的好处是,企业可以获得更好的性能,而不需要改变他们的应用中的任何东西,并且仍然可以利用DBMS的现有生态系统(如工具、API)。</p><p>其中最有趣的是ScaleDB,因为它通过在存储引擎之间重新分配执行,在不使用中间件的情况下提供了透明的分片,而不需要使用中间件;不过,该公司后来转向了另一个领域。</p><p>除了MySQL之外,还有其他类似的系统扩展。</p><p>微软为SQL Server的内存中的Hekaton OLTP引擎几乎与传统的磁盘驻留表无缝集成。</p><p>其他的引擎则使用Postgres的外来数据封装器和API钩子来实现相同类型的集成,但以OLAP工作负载为目标(如Vitesse、CitusDB)。</p><p><br><br>我们现在断定,这样的存储引擎和单节点DBMS的扩展并不代表NewSQL系统,因此我们将其从我们的分类中省略。</p><p>MySQL的InnoDB在可靠性和性能方面已经有了很大的改进,所以切换到另一个引擎用于OLTP应用的好处并不明显。</p><p>我们承认,从面向行的InnoDB引擎切换到OLAP工作负载的列存储引擎的好处更明显(例如Infobright、InfiniDB)。</p><p>但总的来说,针对OLTP工作负载的MySQL存储引擎替换业务是失败的数据库项目的墓地。</p><h3 id="3-1-采用新型架构"><a href="#3-1-采用新型架构" class="headerlink" title="3.1 采用新型架构"></a>3.1 采用新型架构</h3><p>这一类包含了对我们来说最有趣的NewSQL系统,因为它们都是从头开始构建的。</p><p>也就是说,它们不是扩展现有的系统(例如,基于微软SQL Server的Hekaton),而是从一个新的代码库中设计出来的,没有传统系统的任何架构包袱。</p><p>这一类中的所有DBMS都是基于分布式架构的,它们在shared-nothing的架构上运行,并包含支持多节点并发控制、基于复制的错误容忍、流程控制和分布式查询处理的组件。</p><p>使用为分布式处理而构建的新DBMS的优势在于,系统的所有部分都可以针对多节点环境进行优化。</p><p>这包括查询优化器和节点之间的通信协议等。</p><p>例如,大多数NewSQL DBMS能够在节点之间直接发送节点内的查询数据,而不是像一些中间件系统那样将数据路由到中心位置。</p><p><br><br>这类DBMS中的每一个DBMS(除了Google Spanner之外)也都管理着自己的主存储系统,无论是内存中的还是磁盘上的。</p><p>这意味着他们用一个定制的存储引擎,并在其中分配数据库,而不是依赖现成的分布式文件系统(如HDFS)或存储结构(如Apache Ignite)。</p><p>这是它们的非常重要的一点,因为它允许DBMS “把查询送到数据中去”,而不是 “把数据带到查询中去”,这样做的结果是大大减少了网络流量,因为传输查询通常比必须把数据(不仅仅是图元,还包括索引和物化视图)传输到计算中去的网络流量要少得多。</p><p><br><br>管理自己的存储也使 DBMS 能够采用比 HDFS 中使用的基于块的复制方案更复杂的复制方案。</p><p>一般来说,它允许这些DBMS比其他建立在其他现有技术之上的系统获得更好的性能;这方面的例子包括像Trafodion和Splice Machine这样的 “SQL on Hadoop “系统,它们在Hbase之上提供事务处理。</p><p>因此,我们认为这样的系统不应该被认为是NewSQL。</p><p><br><br>但是,使用基于新架构的DBMS也有其弊端。</p><p>最重要的是,很多公司对采用太新的技术抱有戒心,还未被大规模使用。</p><p>这意味着与更受欢迎的DBMS厂商相比,有经验的人要少得多。</p><p>这也意味着企业将有可能无法访问现有管理和报告工具。</p><p>一些DBMS,如Clustrix和MemSQL,通过保持与MySQL线协议的兼容性来避免这个问题。</p><p><strong>比如: Clustrix, CockroachDB, Google Spanner, H-Store, HyPer, MemSQL, NuoDB, SAP HANA, VoltDB.</strong></p><h3 id="3-2-透明的分片中间件"><a href="#3-2-透明的分片中间件" class="headerlink" title="3.2 透明的分片中间件"></a>3.2 透明的分片中间件</h3><p>现在有一些产品可以提供与eBay、Google、Facebook和其他公司在2000年代开发的同类分片中间件功能。</p><p>这些产品允许一个公司将数据库分割成多个分片,这些分片存储在一个单节点的DBMS实例集群中。</p><p>Sharding技术与20世纪90年代的数据库联盟技术不同,因为每个节点(1)运行着相同的DBMS,(2)只有整体数据库的一部分,(3)不是由应用程序独立地访问和更新。</p><p><br><br>集中化的中间件组件负责路由查询、协同管理事务,以及管理节点间的数据放置、复制和分区。</p><p>通常,每个DBMS节点上都安装有一个与中间件通信的shim层。</p><p>这个组件负责在其本地DBMS实例中代表中间件执行查询并返回结果。</p><p>所有这些加在一起,使得中间件产品可以向应用程序展示一个单一的逻辑数据库,而不需要修改底层DBMS。</p><p><br><br>使用分片式中间件的关键优势在于,它们通常是对已经在使用现有单节点DBMS的应用程序的即插即用替代。</p><p>开发人员不需要对他们的应用程序做任何改动,就可以使用新的分片数据库。</p><p>中间件系统最常见的目标是MySQL。</p><p>这意味着,为了与MySQL兼容,中间件必须支持MySQL线协议。</p><p>Oracle提供了MySQL Proxy和Fabric工具包来完成这个任务,但其他的人也编写了自己的协议处理程序库,以避免GPL许可问题。</p><p><br><br>尽管中间件使企业很容易将数据库扩展到多个节点上,但这样的系统仍然必须在每个节点上使用传统的DBMS(如MySQL、Postgres、Oracle)。</p><p>这些DBMS是基于20世纪70年代开发的面向磁盘的架构,因此它们不能像一些基于新架构的NewSQL系统中那样,使用面向内存的存储管理器或并发控制方案进行优化。</p><p>之前的研究表明,面向磁盘的架构的组件是一个重要的障碍,使这些传统的DBMS无法扩展到更高的CPU内核数和更大的内存容量。</p><p>中间件的方法也会在碎片化的节点上产生冗余的查询规划和优化(即在中间件上和单个DBMS节点上分别进行一次查询),但这也允许每个节点对每个查询应用自己的本地优化。</p><p><strong>例如: AgilData Scalable Cluster 2, MariaDB MaxScale, ScaleArc, ScaleBase3.</strong></p><h3 id="3-3-Database-as-a-Service"><a href="#3-3-Database-as-a-Service" class="headerlink" title="3.3 Database-as-a-Service"></a>3.3 Database-as-a-Service</h3><p>最后,还有一些云计算供应商提供NewSQL数据库即服务(DBaaS)产品。</p><p>通过这些服务,企业无需在自己的私有硬件或云托管的虚拟机(VM)上维护DBMS。</p><p>相反,DBaaS提供商负责维护数据库的物理配置,包括系统调整(如缓冲池大小)、复制和备份。</p><p>客户将获得一个连接到DBMS的URL,以及一个仪表板或API来控制数据库系统。</p><p><br><br>DBaaS的用户根据其预期的应用程序的资源利用率支付费用。</p><p>由于数据库不同的查询语句在使用计算资源的方式上有很大的差异,因此DBaaS提供商通常不会像在面向块的存储服务(如亚马逊的S3、谷歌的云存储)中那样,以同样的方式来计量查询调用。</p><p>取而代之的是,客户订阅一个定价层,该定价层指定了提供商将保证的最大资源利用率阈值(例如,存储大小、计算能力、内存分配)。</p><p><br><br>与云计算的因素一样,由于经济规模限制,DBaaS领域的主要参与者仍然是最大的那几个公司。</p><p>但几乎所有的DBaaS都只是提供了传统的单节点DBMS(如MySQL)的托管实例:著名的例子包括Google Cloud SQL、Microsoft Azure SQL、Rackspace云数据库和Sales-force Heroku。</p><p>我们不认为这些都是NewSQL系统,因为它们使用的是基于20世纪70年代架构的面向磁盘的DBMS。</p><p>一些厂商,如微软,对他们的DBMS进行了改造,为多租户部署提供了更好的支持。</p><p><br><br>相反,我们只把那些基于新架构的DBaaS产品视为NewSQL。</p><p>最显著的例子是Amazon的Aurora为他们的MySQL RDS。</p><p>与InnoDB相比,它的显著特点是使用日志结构化存储管理器来提高I/O并行性。</p><p><br><br>还有一些公司不维护自己的数据中心,而是销售运行在这些公共云平台之上的DBaaS软件。</p><p>ClearDB提供了自己的定制DBaaS,可以部署在所有主要的云平台上。</p><p>这样做的好处是可以将数据库分布在同一地理区域的不同供应商之间,避免因服务中断而造成的停机。</p><p><br><br>截至2016年,Aurora和ClearDB是这个NewSQL类别中仅有的两个产品。</p><p>我们注意到,这个领域的几家公司已经失败了(例如,GenieDB、Xeround),迫使他们的客户争相寻找新的提供商,并在这些DBaaS被关闭之前将数据迁移出这些DBaaS。</p><p>我们将其失败的原因归结为超前于市场需求,以及被各大厂商压价。</p><p><strong> Examples: Amazon Aurora, ClearDB.</strong></p><h2 id="4-NewSQL现状"><a href="#4-NewSQL现状" class="headerlink" title="4. NewSQL现状"></a>4. NewSQL现状</h2><p>接下来,我们讨论NewSQL DBMS的特点,以说明这些系统中的新奇之处(如果有的话)。</p><p>我们的分析总结如表1所示。</p><p><img src="/images/NewSql论文/1.png" alt="image-20200525195922408" style="zoom:50%;"></p><h3 id="4-1-内存存储"><a href="#4-1-内存存储" class="headerlink" title="4.1 内存存储"></a>4.1 内存存储</h3><p>所有主要的DBMS都使用了基于70年代原始DBMS的面向磁盘的存储架构。</p><p>在这些系统中,数据库的主要存储位置被认为是在一个可块寻址的耐用存储设备上,如SSD或HDD。</p><p>由于对这些存储设备的读写速度很慢,因此DBMS使用内存来缓存从磁盘上读取的块,并缓冲事务的更新。</p><p>这是很有必要的,因为历史上,内存的价格要贵得多,而且容量有限。</p><p>然而,当下容量和价格已经到了可以完全用内存来存储所有的OLTP数据库的地步,但最大的OLTP数据库除外。</p><p>这种方法的好处是,它可以实现某些优化,因为DBMS不再需要假设一个事务可能在任何时候访问不在内存中的数据而不得不停滞不前。</p><p>因此,这些系统可以获得更好的性能,因为许多处理这些情况所需要的组件,如缓冲池管理器或重量级并发控制方案等,都不需要了。</p><p><br><br>有几种基于内存存储架构的NewSQL DBMS,包括学术型(如H-Store、HyPer)和商业型(如MemSQL、SAP HANA、VoltDB)系统。</p><p>这些系统在OLTP工作负载方面的表现明显优于基于磁盘的DBMS,因为使用内存的原因。</p><p><br><br>完全在内存中存储数据库的想法并不是一个新的想法。</p><p>20世纪80年代初,威斯康星大学麦迪逊分校的开创性研究为主内存DBMS的许多方面奠定了基础,包括索引、查询处理和恢复算法。</p><p>在这十年中,第一个分布式主内存DBMS->PRISMA/DB,也是在这十年中开发出来的。<br>第一批商业化的主内存DBMS出现在20世纪90年代,如Altibase、Oracle的TimesTen和AT&T的DataBlitz。</p><p><br><br>在内存NewSQL系统中,有一件事是具有创新的,那就是能够将数据库的子集持久化到持久化存储中,以减少其内存占用。</p><p>这使得DBMS能够支持比可用内存更大的数据库,而不必切换回面向磁盘的架构。</p><p>一般的方法是在系统内部使用一个内部跟踪机制来识别哪些数据行不再被访问,然后选择它们进行持久化。</p><p>H-Store的反缓存组件将冷数据移动到磁盘存储,然后在数据库中安装一个带有原始数据位置的 “墓碑 “记录。</p><p>当一个事务试图通过其中一个墓碑访问一行记录时,它会被中止,然后一个单独的线程异步检索该记录并将其移回内存。</p><p>另一个支持大于内存的数据库的变体是EPFL的一个学术项目,它在VoltDB中使用操作系统虚拟内存分页。</p><p>为了避免误报,所有这些DBMS都在数据库的索引中保留了被持久化的数据行的键,这抑制了那些有许多二级索引的应用程序的潜在内存节省。(就是有许多二级索引的表的也没怎么节省内存)</p><p>虽然不是NewSQL DBMS,但微软为Hekaton开发的Project Siberia在每个索引中保留了一个Bloom过滤器,以减少跟踪被持久化的数据行的内存存储开销。</p><p><br><br>另一个对内存数据库采取不同的方法的是MemSQL,管理员可以手动指示DBMS以列式格式存储一个表。</p><p>MemSQL不为这些磁盘驻留的数据行维护任何内存跟踪元数据。</p><p>它以日志结构化(log-structured)存储的方式组织这些数据,以减少更新的开销,因为在OLAP数据仓库中,更新速度传统上是很慢的。</p><h3 id="4-2-分区-分片"><a href="#4-2-分区-分片" class="headerlink" title="4.2 分区/分片"></a>4.2 分区/分片</h3><p>几乎所有的分布式NewSQL DBMS 的扩展方式都是将数据库分割成不相干的子集,称为分区或分片。</p><p><br><br>基于分区数据库上的分布式事务处理并不是一个新概念。</p><p>这些系统的许多基本原理来自于伟大的Phil Bernstein(和其他人)在1970年代末的SDD-1项目中的开创性工作。</p><p>在20世纪80年代初,两个开创性的单节点DBMS的背后团队—System R和INGRES,也都创建了各自系统的分布式版本。</p><p>IBM的R*是一个类似于SDD-1的shared-nothing、面向磁盘的分布式DBMS。</p><p>INGRES的分布式版本的动态查询优化算法将分布式查询递归分解成更小的块而被人记住。</p><p>后来,威斯康星大学麦迪逊分校的GAMMA项目探索了不同的分区策略。</p><p><br><br>但是,这些早期的分布式DBMS始终没有得到普及,原因有两个。</p><p>其中第一个原因是20世纪的计算硬件非常昂贵,以至于大多数公司无法负担得起在集群机器上部署数据库。</p><p>第二个问题是,对高性能分布式DBMS的应用需求根本不存在。</p><p>当时,DBMS的预期峰值吞吐量通常以每秒几十到几百个事务来衡量。</p><p>而当今社会,这两种假设都不存在了。</p><p>现在创建一个大规模的、数据密集型的应用比以往任何时候都要容易,部分原因在于开源的分布式系统工具、云计算平台和大量的廉价的移动设备的出现。</p><p><br><br>数据库的表被水平地分成多个分片,其边界基于表的一个(或多个)列的值(即分区属性)。</p><p>DBMS根据这些属性的值将每行数据分配到一个分片,使用范围或哈希分区法将每行记录分配到一个分片。</p><p>来自多个表的相关分片被组合在一起,形成一个由单个节点管理的分区。</p><p>该节点负责执行任何需要访问其分区中存储的数据的查询。</p><p>只有DBaaS系统(Amazon Aurora、ClearDB)不支持这种类型的分区。</p><p><br><br>理想情况下,DBMS也应该能够将一个查询的执行分配到多个分区,然后将它们的结果合并成一个结果。</p><p>除了ScaleArc之外,所有支持原生分区的NewSQL系统都提供了这种功能。</p><p><br><br>许多OLTP应用的数据库都有一个关键的属性,使其可以进行分区。</p><p>它们的数据库模式可以被移植到类似于树状的结构中,其中树的子节点与树根有外键关系。</p><p>然后根据这些关系中所涉及的属性对表进行分区,这样一来,单个实体的所有数据都被共同定位在同一个分区中。</p><p>例如,树的根可以是客户表,而数据库的分区是这样的,每个客户以及他们的订单记录和账户信息都存储在一起。</p><p>这样做的好处是,它允许大多数(如果不是全部)事务只需要在一个分区访问数据。</p><p>这反过来又降低了系统的通信开销,因为它不需要使用原子承诺协议(例如,两阶段承诺)来确保事务在不同的节点上正确完成。</p><p><br><br>偏离同源集群节点架构的NewSQL DBMS有NuoDB和MemSQL。</p><p>对于NuoDB来说,它指定一个或多个节点作为存储管理器(SM),每个节点存储数据库的一个分区。</p><p>SM的分区分为块(NuoDB中称为 “原子”)。</p><p>集群中所有其他节点都被指定为事务引擎(TE),作为原子的内存缓存。</p><p>为了处理一个查询,一个TE节点会检索它所需要的所有原子(从相应的SMs或其他TE中检索)。</p><p>TE 会在数据行上获取写锁,然后将原子的任何更改广播给其他 TE 和 SM。</p><p>为了避免原子在节点之间来回移动,NuoDB采用了负载平衡方案来确保一起使用的数据经常驻留在同一个TE上。</p><p>这意味着NuoDB最终采用了和其他分布式DBMS一样的分区方案,但不需要预先对数据库进行分区,也不需要识别表之间的关系。</p><p><br><br>MemSQL也使用了类似的异构架构,由只执行的聚合节点和存储实际数据的叶子节点组成。</p><p>这两个系统的区别在于它们如何减少从存储节点拉到执行节点的数据量。</p><p>在NuoDB中,TE缓存数据(atoms)来减少从SM读取的数据量。</p><p>MemSQL的聚合器节点不缓存任何数据,但叶子节点执行部分查询,以减少发送至聚合器节点的数据量;而在NuoDB中,这一点是不可能的,因为SM只是一个数据存储。</p><p><br><br>这两个系统能够在DBMS的集群中增加额外的执行资源(NuoDB的TE节点、MemSQL的聚合节点),而不需要重新划分数据库。</p><p>SAP HANA的一个研究原型也探索了使用这种方法。</p><p>然而,这样的异构架构在性能或操作复杂性方面是否优于同源架构(即每个节点既存储数据又执行查询),还有待观察。</p><p><br><br>NewSQL系统中分区的另一个创新的方面是,有些系统支持实时迁移。</p><p>这使得DBMS可以在物理资源之间移动数据来重新平衡和缓解热点,或者在不中断服务的情况下增加/减少DBMS的容量。</p><p>这与NoSQL系统中的再平衡类似,但难度较大,因为NewSQL DBMS在迁移过程中必须保持事务的ACID。</p><p>DBMS有两种方法来实现这个目标。</p><p>第一种是将数据库组织成许多粗粒度的 “虚拟”(即逻辑)分区,这些分区分布在物理节点之间。</p><p>然后,当DBMS需要重新平衡时,它将这些虚拟分区在节点之间移动。这是Clustrix和AgilData,以及Cassandra和DynamoDB等NoSQL系统中使用的方法。</p><p>另一种方法是DBMS通过范围分区来重新分配单个图例或图例组,以执行更精细的再平衡。这类似于MongoDB NoSQL DBMS中的自动分片功能。它在ScaleBase和H-Store等系统中得到了应用。</p><h3 id="4-3-并发控制"><a href="#4-3-并发控制" class="headerlink" title="4.3 并发控制"></a>4.3 并发控制</h3><p>并发控制方案是事务处理DBMS中最重要的实现细节,因为它几乎影响到系统的所有方面。</p><p>并发控制允许终端用户并发的访问数据库,同时给每个用户一种假象,让他们以为在只有自己在单独执行事务。</p><p>它本质上提供了系统中的原子性和隔离保证,因此它影响着整个系统的行为。</p><p><br><br>除了系统采用哪种并发控制方案外,分布式DBMS设计的另一个重要方面是系统采用中心化还是去中心化事务协调协议。</p><p>在一个采用中心化协调器的系统中,所有事务的操作都必须经过协调器,然后由协调器决定是否允许事务进行。</p><p>这与20世纪70-80年代的TP监控器(如IBM CICS、Oracle Tuxedo)采用的方法相同。</p><p>在一个去中心化的系统中,每个节点维护访问它所管理的数据的事务的状态。<br>然后,各节点之间必须相互协调,以确定并发事务是否冲突。</p><p>去中心化协调器的可扩展性更好,但要求DBMS节点中的时钟高度同步,以产生全局性的事务排序。</p><p><br><br>1970-80年代的第一批分布式DBMS使用了两阶段锁定(2PL)方案。</p><p>SDD-1是第一个专门为分布式事务处理而设计的DBMS,它是由一个中心化协调器管理的共享节点集群。</p><p>IBM的R*与SDD-1类似,但主要的区别在于R*中事务的协调是完全去中心化的;它使用分布式的2PL协议,即事务直接在节点上锁定其访问的数据项。</p><p>分布式版本的INGRES也使用了去中心化2PL,并采用了中心化死锁检测。</p><p><br><br>因为处理死锁的复杂性,几乎所有基于新架构的NewSQL系统都放弃了2PL。</p><p>相反,目前的趋势是使用时间戳顺序(TO)并发控制的各种变体方案,此方案中,DBMS假定事务不会以非线性顺序的执行。</p><p>NewSQL系统中最广泛使用的协议是去中心化的多版本并发控制(MVCC),当一行数据被事务更新时,DBMS会在数据库中创建一行新版本的数据。</p><p>维护多个版本允许某个事务在另一个事务更新相同的数据时仍能完成。</p><p>它还允许长期运行的、只读的事务不对写入者进行阻塞。</p><p>几乎所有基于新架构的NewSQL系统,如MemSQL、HyPer、HANA和CockroachDB,都使用了这个协议。虽然这些系统在其MVCC实现中使用了一些工程优化和微调来提高性能,但该方案的基本概念并不新鲜。</p><p>第一个描述MVCC的已知工作是1979年的一篇MIT博士论文[49],而最早使用MVCC的商用DBMS是Digital公司的VAX Rdb和80年代初的InterBase。</p><p>我们注意到,InterBase的架构是由Jim Starkey设计的,他也是NuoDB和失败的Falcon MySQL存储引擎项目的原设计者。</p><p><br><br>其他系统则组合了2PL和MVCC。</p><p>使用这种方案,事务仍然必须在2PL方案下获得锁来修改数据库。</p><p>当一个事务修改一条记录时,DBMS会创建一个新的记录版本,就像使用MVCC一样。</p><p>这种方案允许只读查询,从而避免了获取锁,从而不阻塞写事务。这种方法最著名的实现是MySQL的InnoDB,但它也在Google的Spanner、NuoDB和Clustrix中使用。</p><p>NuoDB在原有的MVCC的基础上进行了改进,采用了gossip协议在节点之间广播版本信息。</p><p><br><br>所有的中间件和DBaaS服务都继承了其底层DBMS体系结构的并发控制方案;</p><p>由于它们大多使用MySQL,这使得它们都是带MVCC的2PL方案。</p><p><br><br>我们认为Spanner中的并发控制实现(连同它的后代F1和SpannerSQL)是NewSQL系统中最新颖的方案之一。</p><p>实际方案本身是基于前几十年开发的2PL和MVCC组合。但Spanner的不同之处在于,它使用硬件设备(如GPS、原子钟)进行高精度时钟同步。</p><p>DBMS使用这些时钟来为事务分配时间戳,以便在广域网络上实现多版本数据库的一致视图。</p><p>CockroachDB也声称要为跨数据中心的事务提供与Spanner相同的一致性,但没有使用原子钟。<br>相反,它们依赖于一种混合时钟协议,将松散同步的硬件时钟和逻辑计数器结合在一起。</p><p><br><br>Spanner还有一点值得注意,就是它预示着Google重新转向使用事务处理最关键的服务。</p><p>Spanner的作者甚至表示,让他们的应用程序员来处理由于过度使用事务而导致的性能问题,比起像NoSQL DBMS那样编写代码来处理缺乏事务的问题要好得多。</p><p><br><br>最后,唯一没有使用MVCC变体的商用NewSQL DBMS是VoltDB。</p><p>这个系统仍然使用TO并发控制,但它没有像MVCC那样将事务交织在一起,而是安排事务在每个分区一次执行。</p><p>它还采用了混合架构,其中单分区事务以分散的方式进行调度,但多分区事务则由集中式协调器调度。</p><p>VoltDB根据逻辑时间戳对事务进行排序,然后在轮到事务时安排它们在某个分区执行。</p><p>当一个事务在一个分区执行时,它对该分区的所有数据都有独占的访问权限,因此系统不需要在其数据结构上设置细粒度的锁和锁存。</p><p>这使得只需要访问单一分区的事务能够有效地执行,因为没有来自其他事务的争夺。</p><p>基于分区的并发控制的缺点是,如果事务跨越多个分区,它的工作效果并不好,因为网络通信延迟会导致节点在等待消息的时候闲置。</p><p>这种基于分区的并发控制并不是一个新的想法。</p><p>它的一个早期变体是由Hector Garcia-Molina[34]在1992年的一篇论文中首次提出,并在20世纪90年代末的kdb系统[62]和H-Store(也就是VoltDB的学术前身)中实现。</p><p><br><br>总的来说,我们发现NewSQL系统中的核心并发控制方案除了让这些算法在现代硬件和分布式操作环境下很好地运行外,并没有什么明显的新意。</p><h3 id="4-4-二级索引"><a href="#4-4-二级索引" class="headerlink" title="4.4 二级索引"></a>4.4 二级索引</h3><p>二级索引是一个表所有属性的子集,这些属性和主键不同。</p><p>这使得DBMS能够支持超过主键或分区键查询性能的快速查询。</p><p>在非分区DBMS中支持二级索引是不值得拿出来说的,因为整个数据库位于单一节点上。</p><p>二级索引在非分区DBMS中面临的挑战是,它们不能以与数据库其他部分相同的方式进行分区。</p><p>举个例子,假设数据库的表是根据客户表的主键来分区的。</p><p>但又有一些查询想要进行从客户的电子邮件地址到账户反向查询。</p><p>由于表是根据主键分区的,所以DBMS必须将这些查询广播到每一个节点,这显然是低效的。</p><p><br></p><p>在分布式DBMS中支持二级索引的两个设计问题是:</p><p>(1)系统将在哪里存储二级索引;(2)如何在事务的上下文中维护它们。</p><p>在一个具有中心化协调器的系统中,就像sharding中间件一样,二级索引可以同时驻留在协调器节点和分片节点上。</p><p>这种方法的优点是,整个系统中只有一个版本的索引,因此更容易维护。</p><p><br></p><p>所有基于新架构的NewSQL系统都是去中心化的,并且使用分区二级索引。</p><p>这意味着每个节点存储索引的一部分,而不是每个节点都有一个完整的副本。</p><p>分区索引和复制索引之间的权衡是,对于前者,查询可能需要跨越多个节点才能找到他们要找的东西,但如果一个事务更新一个索引,它只需要修改一个节点。</p><p>在复制索引中,角色是相反的:查找查询可以只由集群中的一个节点来满足,但任何时候一个事务修改二级索引底层表中引用的属性(即键或值)时,DBMS必须执行一个分布式事务,更新索引的所有副本。</p><p><br>Clustrix是一个混合了这两个概念的去中心化二级索引的例子。</p><p>DBMS首先在每个节点上存储一个冗余的,粗粒度的(即,基于范围的)索引,它将值映射到分区。</p><p>这个映射让DBMS使用一个不是表的分区属性的属性将查询路由到适当的节点。</p><p>然后,这些查询将访问该节点的第二个分区索引,该索引将精确值映射到某一行数据。</p><p>这种两层方法重新减少了在整个集群中保持复制索引同步所需的协调量,因为它只映射范围而不是单个值。</p><p><br></p><p>当使用不支持二级索引的NewSQL DBMS时,开发人员创建二级索引最常见的方法是使用内存中的分布式缓存部署索引,例如Memcached。</p><p>但是使用外部系统需要应用程序维护缓存,因为DBMS不会自动失效外部缓存。</p><h3 id="4-5-副本"><a href="#4-5-副本" class="headerlink" title="4.5 副本"></a>4.5 副本</h3><p>一个公司能够保证其OLTP应用的高可用和数据持久化的最好方法是为他们的数据库建立副本。</p><p>所有现代DBMS,包括NewSQL系统,都支持某种副本机制。</p><p>DBaaS在这方面具有明显的优势,因为它们向客户隐藏了设置副本的所有粗暴细节。</p><p>它们使得部署一个有副本的DBMS变得很容易,管理员不必担心日志传输和确保节点同步。</p><p><br></p><p>在数据库复制方面,有两个设计方案需要决策。</p><p>第一个是DBMS如何在节点间确保数据一致性。</p><p>在一个强一致性的DBMS中,所有的写入操作必须在所有的副本中被确认和执行,这个事务才算成功执行。</p><p>这种方法的优点是,副本在执行读查询时仍然是一致的。</p><p>也就是说,如果应用程序收到了一个事务已经提交的确认,那么该事务所做的任何修改对未来的任何后续事务都是可见的,无论他们访问的是哪个DBMS节点。</p><p>这也意味着,当一个副本失败时,不会丢失更新,因为所有其他节点都是同步的。</p><p>但是维持这种同步重新要求DBMS使用原子承诺协议(例如,两阶段提交)来确保所有的副本与事务的结果一致,这有额外的开销,并且如果一个节点失败或者有网络分区延迟,可能会导致停滞。</p><p>这就是为什么NoSQL系统选择弱一致性模型(也称为最终一致性),在这种模型中,即使有副本没有确认修改成功,DBMS也可以通知应用程序已经执行成功。</p><p><br></p><p>我们所知道的所有NewSQL系统都支持强一致的复制。</p><p>但是这些系统如何保证这种一致性并没有什么创新。</p><p>DBMS的状态机复制的基本原理早在20世纪70年代就被研究出来了(引用37,42)。</p><p>NonStop SQL是20世纪80年代建立的第一批使用强一致性复制以这种同样的方式提供容错的分布式DBMS之一(引用59)。</p><p><br></p><p>除了DBMS何时向副本传播更新的策略外,对于DBMS如何执行这种传播,还有两种不同的执行模式。</p><p>第一种,称为主动-主动复制,即每个副本节点同时处理同一个请求。</p><p>例如,当一个事务执行一个查询时,DBMS会在所有的副本节点上并行执行该查询。</p><p>这与主动-被动复制不同,主动被动复制是先在单个节点处理一个请求,然后DBMS将结果状态传输到其他副本。</p><p>大多数NewSQL DBMS实现了第二种方法,因为它们使用了一个非确定性的并发控制方案。</p><p>这意味着它们不能在查询到达leader副本时向其他副本发送查询,因为它们可能会在其他副本上以不同的顺序被执行,导致数据库的状态会在每个副本上出现分歧。</p><p>这是因为它们的执行顺序取决于几个因素,包括网络延迟、缓存停顿和时钟偏移。</p><p><br></p><p>而确定性的DBMS(如H-Store、VoltDB、ClearDB)则不执行这些额外的协调步骤。</p><p>这是因为DBMS保证事务的操作在每个副本上以相同的顺序执行,从而保证数据库的状态是相同的[44]。</p><p>VoltDB和ClearDB还确保应用程序不会执行利用DBMS外部信息源的查询,而这些信息源在每个副本上可能是不同的(例如,将时间戳字段设置为本地系统时钟)。</p><p><br></p><p>NewSQL系统与以往学术界以外的工作不同的一个方面是考虑在广域网(WAN)上进行复制。</p><p>这是现代操作环境的一个副产品,现在,将系统部署在地理差异较大的多个数据中心是轻而易举的事情。</p><p>任何NewSQL DBMS都可以被配置为通过广域网提供数据的同步更新,但这将会对正常的操作造成明显的减速。</p><p>因此,它们反而提供了异步复制方法。</p><p>据我们所知,Spanner和CockroachDB是唯一的提供了一个优化的复制方案的NewSQL系统,他们可以在广域网上进行强一致的复制。</p><p>同样的,它们通过原子钟和GPS硬件时钟(在Spanner[24]的情况下)或混合时钟(在CockroachDB[41]的情况下)的组合来实现的。</p><h3 id="4-6-崩溃恢复"><a href="#4-6-崩溃恢复" class="headerlink" title="4.6 崩溃恢复"></a>4.6 崩溃恢复</h3><p>NewSQL DBMS提供容错性的另一个重要功能是其崩溃恢复机制。</p><p>但与传统的DBMS不同的是,传统DBMS容错的主要关注点是确保不丢失更新[47],新的DBMS还必须尽量减少停机时间。因为现代网络应用系统要一直在线,而网站中断的代价很高。</p><p>在没有副本的单节点系统中,传统的恢复方法是,当DBMS在崩溃后重新上线时,它从磁盘上加载最后一个检查点,然后重播它的写前日志(WAL),重新将数据库的状态转到崩溃时的状态。 </p><p>这种方法的典范方法被称为ARIES[47],由IBM研究人员在20世纪90年代发明。所有主要的DBMS都实现了ARIES的某种变体。</p><p><br></p><p>然而,在有副本的分布式DBMS中,传统的单节点方法并不直接适用。</p><p>这是因为当主节点崩溃时,系统会将其中一个从节点作为新的主节点。当上一个主节点重新上线时,它不能只加载它的最后一个检查点并重新运行它的WAL,因为DBMS还在继续处理事务,因此数据库的状态已经向前移动。</p><p>恢复中的节点需要从新的主节点(以及可能的其他副本)获取它在宕机时错过的更新。</p><p>有两种潜在的方法可以做到这一点。</p><p>第一种是让恢复节点从本地存储中加载它的最后一个检查点和WAL,然后从其他节点提取它错过的日志条目。</p><p>只要该节点处理日志的速度能快于新更新附加到它身上的速度,该节点最终会收敛到与其他复制节点相同的状态。</p><p>如果DBMS使用物理或逻辑日志,这是有可能的,因为将日志更新直接应用于数据行的时间远远小于执行原始SQL语句的时间。</p><p>为了减少恢复所需的时间,另一种选择是让恢复中的节点丢弃它的检查点,让系统取一个新的检查点,节点将从中恢复。这种方法的另外一个好处是,在DBMS中也可以使用这种相同的机制来增加一个新的复制节点。</p><p><br>中间件和DBaaS系统依赖于其底层单节点DBMS的内置机制,但增加了额外的基础设施,用于领导者选举和其他管理功能。</p><p>基于新架构的NewSQL系统使用现成的组件(如ZooKeeper、Raft)和自己对现有算法的定制实现(如Paxos)相结合。</p><p>所有这些都是20世纪90年代以来商业分布式系统中的标准程序和技术。</p><h2 id="未来趋势"><a href="#未来趋势" class="headerlink" title="未来趋势"></a>未来趋势</h2><p>我们预计,在不久的将来,数据库应用的下一个趋势是能够在新数据上执行分析查询和机器学习算法。</p><p>这种工作方式,通俗地讲就是 “实时分析 “或混合事务分析处理(HTAP),试图通过分析历史数据集与新数据的组合来推断洞察力和知识[35]。</p><p>不同于前十年的传统商业智能业务只能对历史数据进行这种分析。</p><p>在现代应用中,拥有较短的周转时间是很重要的,因为数据刚创建时具有巨大的价值,但这种价值会随着时间的推移而减少。</p><p><br></p><p>在数据库应用中支持HTAP管道的方法有三种:最常见的是部署单独的DBMS:一个用于事务,另一个用于分析查询。</p><p>在这种架构下,前端OLTP DBMS存储了所有事务中产生的新信息。</p><p>然后在后台,系统使用提取-转换-加载的方式将数据从这个OLTP DBMS迁移到第二个后端数据仓库DBMS。</p><p>应用程序在后端 DBMS 中执行所有复杂的 OLAP 查询,以避免减慢 OLTP 系统的速度。</p><p>从 OLAP 系统生成的任何新信息都会被推送到前端 DBMS 中。</p><p><br></p><p>另一种盛行的系统设计,即所谓的lambda架构[45],是使用一个独立的批处理系统(如Hadoop、Spark)来计算历史数据的综合视图,同时使用一个流处理系统(如Storm[61]、Spark Streaming[64])来提供传入数据的视图。</p><p>在这种分体式架构中,批处理系统定期重新扫描数据集,并将结果进行批量上传至流处理系统,然后流处理系统根据新的更新进行修改。</p><p><br></p><p>这两种方法的分叉环境本身就存在几个问题。</p><p>最重要的是,在不同的系统之间传播变化所需的时间通常是以分钟甚至以小时为单位的。</p><p>这种数据传输抑制了应用程序在数据库中输入数据时立即采取行动的能力。</p><p>其次,部署和维护两个不同的DBMS的管理开销是不小的,因为据估计,人员费用几乎占到了一个大型数据库系统总成本的50%[50]。</p><p>如果应用开发者要将不同数据库的数据结合起来,还需要为多个系统编写查询。</p><p>一些系统,试图通过隐藏这种拆分系统架构来实现单一平台;一个例子是Splice Machine[16],但这种方法还有其他技术问题,因为要把数据从OLTP系统(Hbase)复制到OLAP系统(Spark)中去。</p><p><br></p><p>第三种(我们认为更好的)方法是使用单一的HTAP DBMS,它支持OLTP工作负载的高吞吐量和低延迟需求,同时还允许复杂的、运行时间较长的OLAP查询对热数据(事务性)和冷数据(历史)进行操作。</p><p>这些较新的HTAP系统与传统的通用DBMS的不同之处在于,它们结合了过去十年中专门的OLTP(如内存存储、无锁执行)和OLAP(如列式存储、矢量执行)系统的进步,但却在一个DBMS中。</p><p><br>SAP HANA和MemSQL是第一个以HTAP系统自居的NewSQL DBMS。</p><p>HANA通过在内部使用多个执行引擎来实现:一个引擎用于面向行的数据,更适合交易;另一个不同的引擎用于面向列的数据,更适合分析查询。</p><p>MemSQL使用两个不同的存储管理器(一个用于行,一个用于列),但将它们混合在一个执行引擎中。</p><p>HyPer从专注于OLTP的H-Store式并发控制的面向行的系统,转而使用带有MVCC的HTAP列存储架构,使其支持更复杂的OLAP查询[48]。</p><p>甚至VoltDB也将其市场策略从单纯的OLTP性能转向提供流式语义。</p><p>同样,S-Store项目也试图在H-Store架构之上增加对流处理操作的支持[46]。</p><p>从2000年中期开始,专门的OLAP系统(如Greenplum)将开始增加对更好的OLTP的支持。</p><p><br></p><p>然而,我们注意到,HTAP DBMS的兴起确实意味着巨大的单体OLAP仓库的结束。</p><p>这种系统在短期内仍然是必要的,因为它们是一个组织所有前端OLTP孤岛的通用后端数据库。</p><p>但最终,数据库联合的复兴将使公司能够执行跨越多个OLTP数据库(甚至包括多个供应商)的分析查询,而无需移动数据。</p><h2 id="6-总结"><a href="#6-总结" class="headerlink" title="6. 总结"></a>6. 总结</h2><p>从我们的分析中得到的主要启示是,NewSQL数据基础系统并不是对现有系统架构的彻底背离,而是代表了数据库技术持续发展的下一个篇章。</p><p>这些系统所采用的大部分技术都存在于学术界和工业界以往的DBMS中。</p><p>但其中许多技术只是在单个系统中逐一实现,从未全部实现。</p><p>因此,这些NewSQL DBMS的创新之处在于,它们将这些思想融入到单一的平台中。</p><p>实现这一点绝不是一个微不足道的工程努力。</p><p>它们是一个新时代的副产品,在这个时代,分布式计算资源丰富且价格低廉,但同时对应用的要求也更高。</p><p><br></p><p>此外,考虑NewSQL DBMS在市场上的潜在影响和未来发展方向也很有意思。</p><p>鉴于传统的DBMS厂商已经根深蒂固,而且资金充裕,NewSQL系统要想获得市场份额,将面临一场艰苦的战斗。</p><p>自我们首次提出NewSQL这个术语[18]以来,在过去的五年里,有几家NewSQL公司已经倒闭(如GenieDB、Xeround、Translattice),或者转而专注于其他领域(如ScaleBase、ParElastic)。</p><p>根据我们的分析和对几家公司的访谈,我们发现NewSQL系统的被接受的速度相对较慢,特别是与开发者驱动的NoSQL吸收相比。</p><p>这是因为NewSQL DBMS的设计是为了支持事务性工作场景,而这些工作场景大多出现在企业应用中。</p><p>与新的Web应用工作场景相比,这些企业应用的数据库选择决策可能更加保守。</p><p>这一点从以下事实也可以看出,我们发现NewSQL DBMS被用来补充或替换现有的RDBMS部署,而NoSQL则被部署在新的应用工作场景中[19]。</p><p><br></p><p>与2000年代的OLAP DBMS初创公司不同,当时几乎所有的厂商都被大型技术公司收购,到目前为止,只有一家收购NewSQL公司。</p><p>2016年3月,Tableau宣布收购了为HyPer项目组建的初创公司。</p><p>另外两个可能的例外是:(1)Ap-ple在2015年3月收购了FoundationDB,但我们把它们排除在外,因为这个系统的核心是一个NoSQL键值存储,上面嫁接了一个低效的SQL层;(2)ScaleArc收购了ScaleBase,但这是一个竞争对手收购了另一个竞争对手。 </p><p>这些例子都不是那种传统厂商收购后起之秀系统的收购(比如2011年Teradata收购Aster Data Systems)。</p><p>我们反而看到,大型厂商选择创新和改进自己的系统,而不是收购NewSQL新秀。</p><p>微软在2014年在SQL Server中加入了内存Hekaton引擎,以改善OLTP工作负载。</p><p>甲骨文和IBM的创新速度稍慢;他们最近在其系统中增加了面向列的存储扩展,以与惠普Vertica和亚马逊Redshift等日益流行的OLAP DBMS竞争。它们有可能在未来为OLTP工作负载增加内存选项。</p><p><br></p><p>从更长远的角度来看,我们认为,在我们这里讨论的四类系统中,将出现功能的融合。</p><p>(1)1980-1990年代的老式DBMS,(2)2000年代的OLAP数据仓库,(3)2000年代的NoSQL DBMS,(4)2010年代的NewSQL DBMS。</p><p>我们预计,这些分类中的所有关键系统都将支持某种形式的关系模型和SQL(如果它们还没有的话),以及像HTAP DBMS那样同时支持OLTP操作和OLAP查询。当这种情况发生时,这种分类将毫无意义。</p><p><br></p><p><strong> 鸣谢 </strong></p><p>The authors would like to thank the following people for their feedback: Andy Grove (AgilData), Prakhar Verma (Amazon), Cashton Coleman (ClearDB), Dave Anselmi (Clustrix), Spencer Kimball (CockroachDB), Peter Mattis (CockroachDB), Ankur Goyal (MemSQL), Seth Proctor (NuoDB), Anil Goel (SAP HANA), Ryan Betts (VoltDB). This work was supported (in part) by the National Science Foundation (Award CCF-1438955).</p><p>For questions or comments about this paper, please call the CMU Database Hotline at <strong>+1-844-88-CMUDB</strong>.</p><h2 id="7-引用"><a href="#7-引用" class="headerlink" title="7. 引用"></a>7. 引用</h2><p>略了</p><p> </p><p> </p>]]></content>
<summary type="html">
<p><strong><a href="/files/newsql.pdf">论文PDF下载</a></strong></p>
</summary>
<category term="论文翻译" scheme="https://blog.lovezhy.cc/categories/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91/"/>
<category term="NewSQL" scheme="https://blog.lovezhy.cc/tags/NewSQL/"/>
</entry>
<entry>
<title>论文翻译 - Kafka~a Distributed Messaging System for Log Processing</title>
<link href="https://blog.lovezhy.cc/2020/05/14/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91%20-%20Kafka~a%20Distributed%20Messaging%20System%20for%20Log%20Processing/"/>
<id>https://blog.lovezhy.cc/2020/05/14/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91%20-%20Kafka~a%20Distributed%20Messaging%20System%20for%20Log%20Processing/</id>
<published>2020-05-13T16:00:00.000Z</published>
<updated>2020-05-17T07:57:24.778Z</updated>
<content type="html"><![CDATA[<p>原文地址:<a href="http://notes.stephenholiday.com/Kafka.pdf" target="_blank" rel="noopener">http://notes.stephenholiday.com/Kafka.pdf</a></p><p>太长不看:</p><p>相对于JMS等其他的消息系统,Kafka舍弃了很多功能,以达到性能上的提升。</p><p>论文讲述了Kafka设计上的取舍,以及提升性能的很多点。</p><a id="more"></a><h1 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h1><p>日志处理已经成为消费互联网公司数据管道的重要组成部分。</p><p>我们将开始介绍Kafka,这是一个我们开发出用于收集和传递大批量的日志数据,并且具有低延迟的分布式消息传递系统。</p><p>Kafka融合了现有的日志聚合器和消息传递系统的思想,适用于消费离线和在线消息。</p><p>我们在Kafka中做了不少非常规但又实用的设计,使我们的系统具有高效和扩展性。</p><p>我们的实验结果表明,与两种流行的消息传递系统相比,Kafka具有优越的性能。</p><p>我们在生产中使用Kafka已经有一段时间了,它每天要处理数百GB的新数据。</p><h1 id="1-介绍"><a href="#1-介绍" class="headerlink" title="1. 介绍"></a>1. 介绍</h1><p>任何一家大型互联网公司都会产生大量的 “日志 “数据。</p><p>这些数据通常包括:</p><ul><li><p>用户活动事件,包括登录、页面浏览、点击、”喜欢”、分享、评论和搜索查询</p></li><li><p>运营指标,如服务调用堆栈、调用延迟、错误,以及系统指标,如CPU、内存、网络或磁盘利用率等。</p></li></ul><p>长期以来,日志数据一直是分析的一个组成部分,用于跟踪用户参与度、系统利用率和其他指标。</p><p>然而最近互联网应用的趋势使得活动数据成为产品数据管道的一部分,直接用于网站功能中。</p><p>这些用途包括:</p><ul><li>搜索相关性</li><li>活动流中的受欢迎或共同出现的项目产生的推荐</li><li>广告定位和报告</li><li>防止滥用行为的安全应用,如垃圾邮件或未经授权的数据爬取</li><li>新闻联播功能,将用户的状态更新或行动汇总起来,供其 “朋友 “阅读。</li></ul><p>这种生产、实时使用的日志数据给数据系统带来了新的挑战,因为它的数据量比 “真实 “的数据要大好几个数量级。</p><p>例如,搜索、推荐和广告往往需要计算颗粒化的点击率,这不仅会产生每一个用户点击的日志记录,还会产生每个页面上几十个未点击的项目的日志记录。</p><p>中国移动每天收集5-8TB的电话通话记录,Facebook每天则收集了近6TB的各种用户活动事件。</p><p>许多早期处理这类数据的系统都是依靠从生产服务器上实际收集日志文件进行分析。</p><p>近年来,一些专门的分布式日志聚合器已经发布,包括Facebook的Scribe[6]、Yahoo的Data Highway和Cloudera的Flume。</p><p>这些系统主要是为了收集日志数据,并将日志数据加载到数据仓库或Hadoop[8]中进行离线消费。</p><p>在LinkedIn(一家社交网站),我们发现除了传统的离线分析之外,我们还需要以不超过几秒的延迟支持上述大部分实时应用。</p><p>我们构建了一种新型的日志处理的消息传递系统,称为Kafka,它结合了传统日志聚合器和消息传递系统的优点。</p><p>一方面,Kafka具有分布式和可扩展性,并提供了高吞吐量。</p><p>另一方面,Kafka提供了类似于消息传递系统的API,允许应用程序实时消耗日志事件。</p><p>Kafka已经开源,并在LinkedIn的生产中成功使用了6个多月。</p><p>它极大地简化了我们的基础设施,因为我们可以利用一个单一的软件来在线和离线消费各种类型的日志数据。</p><p>本文的其余部分安排如下。</p><ul><li>在第2节中,我们重新审视了传统的消息传递系统和日志聚合器。</li><li>在第3节中,我们描述了Kafka的架构及其关键设计原则。</li><li>在第4节中,我们描述了我们在LinkedIn上部署的Kafka</li><li>在第5节中描述了Kafka的性能结果。</li><li>我们在第6节中讨论了未来的工作</li><li>在第6节中做了总结。</li></ul><h1 id="2-相关工作"><a href="#2-相关工作" class="headerlink" title="2. 相关工作"></a>2. 相关工作</h1><p>传统的企业消息系统已经存在了很长时间,通常在处理异步数据流的事件总线中起着至关重要的作用。</p><p>然而,有几个原因导致它们往往不能很好地适应日志处理。</p><p>首先,企业级系统提供的特性与日志处理该有的不匹配。那些系统往往侧重于提供丰富的交付保证。</p><p>例如,IBM Websphere MQ具有事务式支持,允许一个应用程序将消息以原子方式插入到多个队列中。</p><p>而JMS规范允许每个消息在消费后被确认消费,消费顺序可能是无序的。(没看懂,对JMS不了解,脑补了下,乱序消费并幂等的意思?)</p><p>这样的交付保证对于收集日志数据来说往往是矫枉过正的。偶尔丢失几个页面浏览事件当然不是世界末日。</p><p>那些不需要的功能往往会增加这些系统的API和底层实现的复杂性。</p><p>其次,相比较首要设计约束功能,许多系统并不是那样强烈地关注吞吐量。例如,JMS没有API允许生产者明确地将多个消息批量化为一个请求。这意味着每个消息都需要进行一次完整的TCP/IP往返,这对于我们领域的吞吐量要求是不可行的。</p><p>第三,那些系统在分布式支持方面比较弱。没有简单的方法可以在多台机器上对消息进行分区和存储。</p><p>最后,许多消息系统假设消息会被近似实时消费掉,未被消费的消息量总是相当小。</p><p>导致如果出现消息累积,它们的性能就会大大降低。比如当数据仓库等离线消耗者对消息系统做周期性的大负载消费,而不是连续消费数据时。</p><p>在过去几年里,已经建立了一些专门的日志聚合器。</p><p>比如Facebook使用了一个叫Scribe的系统,每个前端机器可以通过网络向一组Scribe机器发送日志数据。</p><p>每台Scribe机器聚合日志条目,并定期将其转储到HDFS或NFS设备上。</p><p>雅虎的数据高速公路项目也有类似的数据传递方式,一组机器聚合来自客户端的事件,按分钟保存为文件,然后将</p><p>其添加到HDFS。</p><p>Flume是Cloudera开发的一个比较新的日志聚合器。它支持可扩展的 “管道 “和 “数据下沉”,使流式日志数据的传</p><p>输非常灵活。它也有更多的集成分布式支持。</p><p>但是,这些系统大多是为离线消耗日志数据而构建的,往往会将实现细节(如 “按分钟保存的文件”)不必要地暴露给消费者。</p><p>此外,他们中的大多数都采用了 “推送 “模式,即Broker将数据转发给消费者。</p><p>在LinkedIn,我们发现 “拉动 “模式更适合我们的应用,因为每个消费者都能以自己能承受的最大速率检索到消</p><p>息,避免被推送的消息淹没在比自己能承受的速度更快的消息中。</p><p>拉动模式还可以让消费者很容易回传,我们在</p><p>3.2节末尾讨论了这个好处的细节。</p><p>最近,雅虎研究公司开发了一种新的分布式pub/sub系统,名为HedWig。HedWig具有高度的可扩展性和可</p><p>用性,并提供了强大的持久性保证。不过,它主要是用于存储资料库(data store)的提交日志。</p><h1 id="3-Kafka架构和设计原则"><a href="#3-Kafka架构和设计原则" class="headerlink" title="3. Kafka架构和设计原则"></a>3. Kafka架构和设计原则</h1><p>由于现有的各种消息系统的局限性,我们开发了一种新的基于消息传递的日志聚合器Kafka。</p><p>我们首先介绍一下Kafka中的基本概念。</p><p>一个主题定义一个特定类型的消息流。</p><p>一个生产者可以向一个主题发布消息。然后,发布的消息被存储在一组称为Broker的服务器上。</p><p>一个消费者可以从Broker那里订阅一个或多个主题,并通过从Broker那里提取数据来消费订阅的消息。</p><p>从概念上讲,消息传递的定义是比较简单的。同样的,我们试图使Kafka API也一样简单。为了证明这一点,我们</p><p>不展示具体的API,而是介绍一些示例代码来展示API的使用方法。</p><p>下面给出了生产者的示例代码。一个消息被定义为只包含一个字节的内容。用户可以选择自己喜欢的序列化方</p><p>法对消息进行编码。为了提高效率,生产者可以在一次发布请求中发送一组消息。</p><blockquote><p><strong>Sample producer code</strong>:</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">producer = <span class="keyword">new</span> Producer(...);</span><br><span class="line">message = <span class="keyword">new</span> Message(“test message str”.getBytes()); </span><br><span class="line">set = <span class="keyword">new</span> MessageSet(message); </span><br><span class="line">producer.send(“topic1”, set);</span><br></pre></td></tr></table></figure><p>要订阅一个主题,消费者首先要为该主题创建一个或多个消息流(理解为分区)。</p><p>发布到该主题的消息将被平均分配到这些子消息流(分区)中。</p><p>关于Kafka如何分配消息的细节将在后面的3.2节中描述。</p><p>每个消息流在持续产生的消息流上提供了一个迭代器接口。</p><p>消费者对消息流中的每个消息进行迭代,并处理消息的内容。</p><p>与传统的迭代器不同,消息流迭代器永远不会终止。</p><p>如果当前没有更多的消息要消费,迭代器就会阻塞,直到新的消息被发布到主题上。</p><p>我们既支持点对点的传递模式,即多个消费者共同消费一个主题中所有消息的单一副本,也支持多个消费者各自检</p><p>索一个主题的副本的发布/订阅模式。</p><blockquote><p> <strong>Sample consumer code</strong>:</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">streams[] = Consumer.createMessageStreams(“topic1”, <span class="number">1</span>) <span class="keyword">for</span> (message : streams[<span class="number">0</span>]) {</span><br><span class="line"></span><br><span class="line">bytes = message.payload();</span><br><span class="line"> <span class="comment">// do something with the bytes</span></span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="/images/Kafka论文/1.png" alt="image-20200428203312364" style="zoom:50%;"></p><p>Kafka的整体架构如图1所示。</p><p>由于Kafka是分布式的,所以一个Kafka集群通常由多个Broker组成。</p><p>为了平衡负载,一个主题被划分成多个分区,每个Broker存储一个或多个分区。</p><p>多个生产者和消费者可以同时发布和消费消息。</p><p>在第3.1节中,我们将描述Broker上的单个分区的布局,以及我们选择的一些设计选择,以使访问分区的效率更高。</p><p>在第3.2节中,我们将描述生产者和消费者在分布式设置中如何与多个Broker交互。</p><p>在第3.3节中,我们将讨论Kafka的交付保证(delivery guarantees)。</p><h2 id="3-1-单分区的性能"><a href="#3-1-单分区的性能" class="headerlink" title="3.1 单分区的性能"></a>3.1 单分区的性能</h2><p>我们在Kafka中做了一些设计的决策,让系统更有效率。</p><p><strong>1. 简单的存储方式</strong>:Kafka有一个非常简单的存储布局。</p><p>一个主题的每个分区对应一个逻辑日志。</p><p>在物理上,一个日志被实现为一组大小大致相同的段文件(例如,1GB)。</p><p>每当生产者向分区发布消息时,Broker只需将消息附加到最后一个段文件中。</p><p>为了更好的性能,我们只有在发布了一定数量的消息后,或者在发布了一定时间后,才会将段文件刷新到磁盘上。</p><p>一个消息只有在刷新后才会暴露在消费者面前。</p><p>与典型的消息传递系统不同,Kafka中存储的消息没有明确的消息ID。</p><p>相反,每条消息都是通过其在日志中的逻辑偏移来寻址。</p><p>这避免了维护用于辅助查询的索引结构的开销,这些索引结构将消息id映射到实际的消息位置。</p><p>注意,我们提到的消息id是递增的,但不是连续的。为了计算下一条消息的id,我们必须将当前消息的长度加到它的id上。</p><p>从现在开始,我们将交替使用消息id和偏移量。</p><p>消费者总是顺序消费来自特定分区的消息。</p><p>如果消费者确认某个特定的消息偏移,就意味着消费者已经接收到了该分区中该偏移之前的所有消息。</p><p>在实际的运行中,消费者向Broker发出异步拉取消息请求,以便有一个缓冲区的数据准备好供应用程序消费。</p><p>每个拉取消息请求都包含消费开始的消息的偏移量和可接受的字节数。</p><p>每个Broker在内存中保存一个排序的偏移量列表,包括每个段文件中第一个消息的偏移量。Broker<br>通过搜索偏移量列表来定位所请求的报文所在的段文件,并将数据发回给消费者。</p><p>当消费者收到一条消息后,它计算出下一条要消费的消息的偏移量,并在下一次拉取请求中使用它。</p><p>Kafka日志和内存中索引的布局如图2所示。每个框显示了一条消息的偏移量。</p><p><img src="/images/Kafka论文/2.png" alt="image-20200506192830629" style="zoom:50%;"></p><p><strong>2. 高效的传输</strong>: 我们在Kafka中传输数据的时候非常谨慎。</p><p>早前,我们已经表明,生产者可以在一次发送请求中提交一组消息。</p><p>虽然消费者API每次迭代一条消息,但在实际运行中,每一个消费者的拉动请求也会检索到多个消</p><p>息。一次传输通常是几百个K字节的大小。</p><p>我们做出的另一个非常规的选择是避免在Kafka层面缓存消息在内存中。</p><p>相反,我们依赖底层文件系统的页面缓存。</p><p>这样做的主要好处是避免了双重缓冲,消息就只会缓存在页面缓存中。</p><p>这样做还有一个额外的好处,那就是即使在代理进程重启的时候,也能保留热缓存(warm cache)。</p><p>由于Kafka根本不在进程中缓存消息,所以它在垃圾回收内存方面的开销非常小,这使得在基于VM</p><p>的语言中高效实现是可行的。</p><p>最后,由于生产者和消费者都是按顺序访问段文件,而消费者往往会比生产者晚一点,所以正常的</p><p>操作系统缓存启发式缓存是非常有效的(缓存直写和预读)。</p><p>我们发现,生产者和消费者的性能都与数据大小呈线性关系,最大的数据量可以达到很多T字节。(没看懂)</p><p>此外,我们还对消费者的网络访问进行了优化。</p><p>Kafka是一个多消费者系统,一条消息可能被不同的消费者应用多次消耗。</p><p>从本地文件向远程socket发送字节的典型方法包括以下步骤。</p><ol><li>从存储介质中读取数据到操作系统中的页面缓存</li><li>将页面缓存中的数据复制到应用缓冲区</li><li>将应用缓冲区复制到另一个内核缓冲区</li><li>将内核缓冲区发送到Socket。</li></ol><p>其中包括4个数据复制和2个系统调用。</p><p>在Linux和其他Unix操作系统上,存在一个sendfile API,可以直接将字节从文件通道传输到socket</p><p>通道。这通常可以避免步骤(2)和(3)中介绍的2个复制和1个系统调用。</p><p>Kafka利用sendfile API来有效地将日志段文件中的字节从代理服务器向消费者传递。</p><p><strong>3. 无状态的Broker</strong>: 与大多数其他消息系统不同,在Kafka中,每个消费者消费了多少消息的信</p><p>息不是由Broker维护,而是由消费者自己维护。这样的设计减少了很多的复杂性,也减少了Broker</p><p>的开销。</p><p>但是,这使得删除消息变得很棘手,因为Broker不知道是否所有的用户都消费了这个消息。</p><p>Kafka通过使用简单的基于时间的SLA保留策略解决了这个问题。</p><p>如果一条消息在代理中保留的时间超过一定的时间,通常是7天,则会自动删除。</p><p>这个方案在实际应用中效果不错。大部分消费者包括离线的消费者,都是按日、按小时或实时完成</p><p>消费。由于Kafka的性能不会随着数据量的增大而降低,所以这种长时间保留的方案是可行的。</p><p>这种设计有一个重要的副作用。</p><p>一个消费者可以故意倒退到一个旧的偏移量,重新消费数据。</p><p>这违反了队列的通用规定,但事实证明,这对很多消费者来说是一个必不可少的功能。</p><p>例如,当消费者中的应用逻辑出现错误时,应用可以在错误修复后回放某些消息。这对我们的数据仓库或Hadoop系统中的ETL数据加载特别重要。</p><p>再比如,被消费的数据可能只是周期性地被刷新到一个持久化存储(例如,全文索引器)。</p><p>如果消费者崩溃,未冲洗的数据就会丢失。在这种情况下,消费者可以检查未冲洗的消息的最小偏移量,并在重启时从该偏移量中重新消费。</p><p>我们注意到,相比于推送模型,在拉动模型中支持消费者重新消费要容易得多。</p><h2 id="3-2-分布式协调处理"><a href="#3-2-分布式协调处理" class="headerlink" title="3.2 分布式协调处理"></a>3.2 分布式协调处理</h2><p>现在我们来解释一下生产者和消费者在分布式环境中的执行方式。</p><p>每个生产者可以向一个随机的或由分区key和分区函数语义决定的分区发布消息。我们将重点讨论消</p><p>费者是如何与Broker互动的。</p><p>Kafka有消费者组的概念。</p><p>每个消费组由一个或多个消费者组成,共同消费一组被订阅的主题,也就是说,每条消息只传递给</p><p>消费组内的一个消费者。</p><p>不同的消费者组各自独立消费全套订阅的消息,不需要跨消费者组的协调机制。</p><p>同一组内的消费者可以在不同的进程或不同的机器上。</p><p>我们的目标是在不引入过多的协调开销的情况下,将存储在Broker中的消息平均分配给组中的所有</p><p>消费者。</p><p>我们的第一个决定是将一个主题内的分区作为最小的并行单元。</p><p>这意味着,在任何时候一个分区的所有消息都只被每个消费组中的一个消费者消费。</p><p>假设我们允许多个消费者同时消费一个分区,那么他们就必须协调谁消费什么消息,这就需要加锁和维护状态,会造成一定的额外开销。</p><p>相反,在我们的设计中,消费进程只需要在消费者重新平衡负载时进行协调,正常来说这种情况不经常发生。</p><p>为了使负载真正平衡,我们需要一个主题中的分区比每个消费组中的消费者多很多。</p><p>我们可以通过对一个主题进行更多的分区来达到这个目的。</p><p>我们做的第二个决定是不设立中心化的”主控 “节点,而是让消费者以去中心化的方式相互协调。</p><p>增加一个主节点会使系统变得复杂化,因为我们不得不进一步担心主节点故障。</p><p>为了方便协调,我们采用了一个高度可用的共识服务Zookeeper。</p><p>Zookeeper有一个非常简单的、类似于文件系统的API。</p><p>人们可以创建一个路径,设置一个路径的值,读取一个路径的值,删除一个路径的值,以及列出一个路径的子路径。</p><p>它还可以做一些更有趣的事情。</p><ul><li>可以在路径上注册一个watcher,当路径的子路径或路径的值发生变化时,可以得到通知</li><li>可以将路径创建为临时的(相对于持久性的),这意味着如果创建的客户端不在了,路径会被Zookeeper服务器自动删除</li><li>zookeeper将数据复制到多个服务器上,这使得数据的可靠性和可用性很高。</li></ul><p>Kafka使用Zookeeper完成以下任务。</p><ul><li><p>检测Broker和消费者的添加和删除</p></li><li><p>当上述事件发生时,在每个消费者中触发一个再平衡过程</p></li><li><p>维护消费关系,并跟踪每个分区的消费偏移情况。</p></li></ul><p>具体来说,当每个Broker或消费者启动时,它将其信息存储在Zookeeper中的Broker或消费者注册表中。</p><p>Broker注册表包含Broker的主机名和端口,以及存储在其上的主题和分区。</p><p>消费者注册表包括消费者所属的消费组,以及它所订阅的主题集合。</p><p>每个消费组都与Zookeeper中的一个所有权注册表和一个偏移注册表相关联。</p><p>所有权注册表对每个订阅的分区都有一个路径,路径值是当前从这个分区消费的消费者id(我们使用的术语是消费者拥有这个分区)。</p><p>偏移注册表为每个订阅的分区存储了该分区中最后一个被消费的消息的偏移量。</p><p>Broker注册表、消费者注册表和所有权注册表在 Zookeeper 中创建的路径都是临时的。</p><p>偏移注册表中创建的路径是持久的。</p><p>如果一个Broker服务器发生故障,其上的所有分区都会自动从Broker注册表中删除。</p><p>消费者的故障会导致其在消费者注册表中的记录和所有权注册表中的所有分区记录丢失。</p><p>每个消费者都会在Broker注册表和消费者注册表上注册一个Zookeeper的Watcher,每当Broker集合或消费者组</p><p>发生变化时,都会收到通知。</p><p><img src="/images/Kafka论文/3.png" alt="image-20200507194132517" style="zoom:50%;"></p><p>在消费者的初始启动过程中,或者当消费者通过Watcher收到关于Broker/消费者变更的通知时,消费者会启动一</p><p>个重新平衡过程,以确定它应该消费的新分区。</p><p>在算法1中描述了这个过程。</p><p>通过从Zookeeper读取Broker和消费者注册表,消费者首先计算每个订阅主题T的可用分区集合(PT)和订阅T的消费者集合(CT)。</p><p>对于消费者选择的每个分区,它在所有权注册表中写入自己作为该分区的新所有者。</p><p>最后,消费者开始一个线程从拥有的分区中拉出数据,偏移量从存储在偏移注册表中的记录值开始。</p><p>当消息从分区中拉出时,消费者会定期更新偏移注册表中的最新消耗的偏移量。</p><p>当一个消费组内有多个消费者时,每个消费者都会收到Broker或消费者变更的通知。</p><p>但是,通知到达每个消费者的时间上略有不同。</p><p>因此,有可能是一个消费者试图夺取仍由另一个消费者拥有的分区的所有权。</p><p>当这种情况发生时,第一个消费者只需释放其当前拥有的所有分区,等待一段时间,然后重新尝试重新平衡。</p><p>在实践中,重新平衡过程通常只需重试几次就会稳定下来。</p><p>当创建一个新的消费者组时,偏移注册表中没有可用的偏移量。</p><p>在这种情况下,消费者将使用我们在Broker上提供的API,从每个订阅分区上可用的最小或最大的偏移量开始(取</p><p>决于配置)。</p><h2 id="3-3-传递保证"><a href="#3-3-传递保证" class="headerlink" title="3.3 传递保证"></a>3.3 传递保证</h2><p>一般来说,Kafka只保证至少一次交付语义。</p><p>确切一次交付语义通常需要两阶段提交,对于我们的应用来说并不是必须的。</p><p>大多数情况下,一个消息会准确地传递给每个消费组一次。</p><p>但是,当一个消费组进程崩溃而没有干净关闭的情况下,新接管的消费进程可能会得到一些重复的消息,这些消息</p><p>在最后一次偏移成功提交给zookeeper之后。</p><p>如果一个应用程序关心重复的问题,那么它必须添加自己的去重复逻辑,要么使用我们返回给消费者的偏移量,要</p><p>么使用消息中的一些唯一密钥。这通常是一种比使用两阶段提交更经济的方法。</p><p>Kafka保证来自单个分区的消息按顺序传递给消费者。</p><p>然而,对于来自不同分区的消息的顺序,Kafka并不保证。</p><p>为了避免日志损坏,Kafka在日志中为每个消息存储一个CRC。</p><p>如果Broker上有任何I/O错误,Kafka会运行一个恢复过程来删除那些具有不一致CRC的消息。</p><p>在消息级别拥有CRC也允许我们在消息产生或消费后检查网络错误。</p><p>如果一个Broker宕机,那么存储在其上的任何未被消费的信息都将不可用。</p><p>如果一个Broker上的存储系统被永久损坏,任何未被消费的消息都会永远丢失。</p><p>在未来,我们计划在Kafka中添加复制功能,以便在多个Broker上冗余存储每一条消息。</p><h1 id="4-Kafka在LinkedIn的实践"><a href="#4-Kafka在LinkedIn的实践" class="headerlink" title="4. Kafka在LinkedIn的实践"></a>4. Kafka在LinkedIn的实践</h1><p>在本节中,我们将介绍我们如何在LinkedIn使用Kafka。</p><p>图3显示了我们部署的简化版本。</p><p>在每个运行面向用户服务的数据中心,我们都会部署一个Kafka集群。</p><p>前端服务会生成各种日志数据,并分批发布到本地的Kafka的Broker中。</p><p>我们依靠硬件负载均衡器将发布请求均匀地分配给Kafka的Broker。</p><p>Kafka的在线消费者在同一数据中心内的服务中运行。</p><p><img src="/images/Kafka论文/6.png" alt="image-20200513201939870" style="zoom:50%;"></p><p>我们还在每个数据中心单独部署了一个Kafka集群,用于离线分析,该集群在地理位置上靠近我们的Hadoop集群</p><p>和其他数据仓库基础设施。</p><p>这个Kafka实例运行一组嵌入式消费者,实时从数据中心的Kafka实例中拉取数据。</p><p>然后,我们运行数据加载任务,将数据从这个Kafka的复制集群拉到Hadoop和我们的数据仓库中,在这里我们运</p><p>行各种报表作业和数据分析处理。</p><p>我们还使用这个Kafka集群进行原型开发,并有能力针对原始事件流运行简单的脚本进行实时查询。</p><p>无需过多的调整,整个管道的端到端延迟平均约为10秒,足以满足我们的要求。</p><p>目前,Kafka每天积累了数百G字节的数据和近10亿条消息。</p><p>随着我们完成对遗留系统的迁移,我们预计这个数字将大幅增长。</p><p>未来还会增加更多类型的消息。</p><p>当运营人员启动或停止Broker进行软件或硬件维护时,再平衡过程能够自动重定向消费。</p><p>我们的跟踪系统还包括一个审计系统,以验证整个管道中的数据没有丢失。</p><p>为了方便起见,每条消息都带有时间戳和服务器名称。</p><p>我们对每个生产者进行仪器化处理,使其定期生成一个监控事件,记录该生产者在固定时间窗口内为每个主题发布</p><p>的消息数量。</p><p>生产者将监控事件发布到Kafka的一个单独的主题中。</p><p>然后,消费者可以统计他们从一个给定的主题中收到的消息数量,并将这些计数与监测事件进行验证,以验证数据</p><p>的正确性。</p><p>加载到Hadoop集群中是通过实现一种特殊的Kafka输入格式来完成的,该格式允许MapReduce作业直接从Kafka</p><p>中读取数据。</p><p>MapReduce作业加载原始数据,然后将其分组和压缩,以便将来进行高效处理。</p><p>无状态的Broker和客户端存储消息偏移在这里再次发挥了作用,使得MapReduce任务管理(允许任务失败和重</p><p>启)以自然的方式处理数据负载,而不会在任务重启时重复或丢失消息。</p><p>只有在任务成功完成后,数据和偏移量才会存储在HDFS中。</p><p>我们选择使用Avro作为我们的序列化协议,因为它是高效的,并且支持模式演化。</p><p>对于每条消息,我们将其Avro模式的id和序列化的字节存储在有效payload中。</p><p>这个模式允许我们执行一个约定,以确保数据生产者和消费者之间的兼容性。</p><p>我们使用一个轻量级的模式注册服务来将模式id映射到实际的模式。</p><p>当消费者得到一个消息时,它在模式注册表中查找,以检索该模式,该模式被用来将字节解码成对象(这种查找只</p><p>需要对每个模式进行一次,因为值是不可更改的)。</p><h1 id="5-实验结果"><a href="#5-实验结果" class="headerlink" title="5. 实验结果"></a>5. 实验结果</h1><p>我们进行了一项实验性研究,将Kafka与Apache ActiveMQ v5.4(一种流行的JMS开源实现)和以性能著称的消息</p><p>系统RabbitMQ v2.4进行了比较。</p><p>我们使用了ActiveMQ的默认持久化消息存储KahaDB。</p><p>虽然这里没有介绍,但我们也测试了另一种AMQ消息存储,发现其性能与KahahaDB非常相似。</p><p>只要有可能,我们尽量在所有系统中使用可比性设置。</p><p>我们在2台Linux机器上进行了实验,每台机器都有8个2GHz核心,16GB内存,6个磁盘,带RAID 10。</p><p>这两台机器用1Gb网络链路连接。其中一台机器作为Broker,另一台机器作为生产者或消费者。</p><p><strong>Producer测试</strong>:</p><p>我们将所有系统中的Broker配置为异步刷新消息到其持久化磁盘中。</p><p>对于每个系统,我们运行了一个单一的生产者来发布总共1000万条消息,每条消息的大小为200字节。</p><p>我们将Kafka生产者配置为以1和50的大小分批发送消息。</p><p>ActiveMQ和RabbitMQ似乎没有一个简单的消息批处理方法,我们假设它使用的是1的批处理大小,结果如图4所示。</p><p>x轴代表的是随着时间的推移向Broker发送的数据量,单位为MB,y轴对应的是生产者吞吐量,单位为每秒的消息量。</p><p>平均而言,Kafka在批处理大小为1和50的情况下,Kafka可以以每秒5万条和40万条消息的速度分别发布消息。</p><p>这些数字比ActiveMQ高了好几个数量级,而且至少是比RabbitMQ高2倍。</p><p><img src="/images/Kafka论文/4.png" alt="image-20200514202746023" style="zoom:50%;"></p><p>Kafka的表现要好得多有几个原因。</p><p>首先,Kafka生产者目前不等待Broker的回执,以Broker能处理的速度发送消息。</p><p>这大大增加了发布者的吞吐量。</p><p>在批处理量为50个的情况下,单个Kafka生产者几乎打满了生产者和Broker之间的1Gb带宽。</p><p>这对于日志聚合的情况来说是一个有效的优化,因为数据必须异步发送,以避免在实时服务流量中引入任何延迟。</p><p>同时我们注意到,broker在没有回送ack的情况下,不能保证producer每一条发布的消息都能被broker实际接收到。</p><p>对于不同类型的日志数据,只要丢掉的消息数量相对较少,以持久化换取吞吐量是可取的。然而,我们确实计划在</p><p>未来解决更多关键数据的持久化问题。</p><p>其次,Kafka使用有更有效的存储格式。</p><p>正常来说,在Kafka中,每个消息的开销是9个字节,而在ActiveMQ中则是144个字节。</p><p>这意味着ActiveMQ比Kafka多用了70%的空间来存储同样的1000万条消息。</p><p>ActiveMQ的一个开销来自于JMS所要求的沉重的消息头。</p><p>另一个开销是维护各种索引结构的成本。</p><p>我们观察到,ActiveMQ中最繁忙的线程之一花了大部分时间访问B-Tree来维护消息元数据和状态。</p><p>最后,批处理通过摊销RPC开销,大大提高了吞吐量。在Kafka中,50条消息的批处理量几乎提高了一个数量级的</p><p>吞吐量。</p><p><strong>消费者测试</strong>:</p><p>在第二个实验中,我们测试了消费者的性能。</p><p>同样,对于所有系统,我们使用一个消费者来检索总共1000万条消息。</p><p>我们对所有系统进行了配置,使每个拉取请求预取的数据量大致相同–最多1000条消息或约200KB。</p><p>对于 ActiveMQ 和 RabbitMQ,我们将消费者确认模式设置为自动。</p><p>由于所有的消息都适合在内存中,所以所有的系统都是从底层文件系统的页面缓存或一些内存中的缓冲区中提供数</p><p>据。</p><p>结果如图5所示。</p><p><img src="/images/Kafka论文/5.png" alt="image-20200514203355691" style="zoom:50%;"></p><p>Kafka平均每秒消费22000条消息,是ActiveMQ和RabbitMQ的4倍多。</p><p>我们可以想到几个原因。</p><p>首先,由于Kafka有更有效的存储格式,所以消费者从Broker那里传输的字节数更少。</p><p>其次,ActiveMQ和RabbitMQ中的Broker都必须维护每一条消息的传递状态。</p><p>我们观察到ActiveMQ线程中的一个ActiveMQ线程在这个测试中忙于向磁盘写入KahaDB页面。</p><p>相比之下,Kafka代理上没有任何磁盘写入活动。</p><p>最后,通过使用sendfile API,Kafka降低了传输开销。</p><p>在这一节的最后,我们要指出,实验的目的并不是为了表明其他的消息传递系统不如Kafka。</p><p>毕竟,ActiveMQ和RabbitMQ都有比Kafka更多的功能。</p><p>主要是为了说明一个定制的系统可能带来的性能提升。</p><h1 id="6-总结与未来展望"><a href="#6-总结与未来展望" class="headerlink" title="6. 总结与未来展望"></a>6. 总结与未来展望</h1><p>我们提出了一个名为Kafka的新型系统,用于处理海量的日志数据流。</p><p>与普通消息传递系统一样,Kafka采用了一种基于拉取的消费模型,允许应用程序以自己的速度消费数据,并在需</p><p>要的时候随时倒带消费。</p><p>通过专注于日志处理应用,Kafka实现了比传统消息系统更高的吞吐量。</p><p>同时,它还提供了内置的分布式支持,并且可以进行扩展。我们已经在LinkedIn成功地将Kafka用于离线和在线应</p><p>用。</p><p>未来,我们有几个方向。</p><p>首先,我们计划在多个Broker之间添加内置的消息复制功能,即使在机器故障无法恢复的情况下,我们也可以提</p><p>供持久化和数据可用性保证。</p><p>我们希望同时支持异步和同步复制模型,以允许在生产者延迟和所提供的保证强度之间进行一些权衡。</p><p>一个应用可以根据自己对持久化、可用性和吞吐量的要求,选择合适的冗余级别。</p><p>其次,我们希望在Kafka中加入一些流处理能力。</p><p>在从Kafka中检索消息后,实时应用经常会执行类似的操作,例如基于窗口的计数,并将每条消息与二级存储中的</p><p>记录或与另一个流中的消息连接起来。</p><p>在最底层,在发布过程中,通过在join键上对消息进行语义上的分区来支持这种操作,这样,所有用特定键发送的</p><p>消息都会进入同一个分区,从而到达一个单一的消费进程。</p><p>这为在消费机集群中处理分布式流提供了基础。</p><p>在此基础上,我们觉得一个有用的信息流实用程序库,如不同的窗口化函数或连接技术将对这类应用有利。</p><h1 id="7-引用"><a href="#7-引用" class="headerlink" title="7. 引用"></a>7. 引用</h1><ol><li><a href="http://activemq.apache.org/" target="_blank" rel="noopener">http://activemq.apache.org/</a></li><li><a href="http://avro.apache.org/" target="_blank" rel="noopener">http://avro.apache.org/</a></li><li>Cloudera’s Flume, <a href="https://github.com/cloudera/flume" target="_blank" rel="noopener">https://github.com/cloudera/flume</a></li><li><a href="http://developer.yahoo.com/blogs/hadoop/posts/2010/06/ena" target="_blank" rel="noopener">http://developer.yahoo.com/blogs/hadoop/posts/2010/06/ena</a> bling_hadoop_batch_processi_1/</li><li>Efficient data transfer through zero copy: <a href="https://www.ibm.com/developerworks/linux/library/j-" target="_blank" rel="noopener">https://www.ibm.com/developerworks/linux/library/j-</a> zerocopy/</li><li>Facebook’s Scribe, <a href="http://www.facebook.com/note.php?note_id=32008268919" target="_blank" rel="noopener">http://www.facebook.com/note.php?note_id=32008268919</a></li><li>IBM Websphere MQ: <a href="http://www-" target="_blank" rel="noopener">http://www-</a> 01.ibm.com/software/integration/wmq/</li><li><a href="http://hadoop.apache.org/" target="_blank" rel="noopener">http://hadoop.apache.org/</a></li><li><a href="http://hadoop.apache.org/hdfs/" target="_blank" rel="noopener">http://hadoop.apache.org/hdfs/</a></li><li><a href="http://hadoop.apache.org/zookeeper/" target="_blank" rel="noopener">http://hadoop.apache.org/zookeeper/</a></li><li><a href="http://www.slideshare.net/cloudera/hw09-hadoop-based-" target="_blank" rel="noopener">http://www.slideshare.net/cloudera/hw09-hadoop-based-</a> data-mining-platform-for-the-telecom-industry</li><li><a href="http://www.slideshare.net/prasadc/hive-percona-2009" target="_blank" rel="noopener">http://www.slideshare.net/prasadc/hive-percona-2009</a></li><li><a href="https://issues.apache.org/jira/browse/ZOOKEEPER-775" target="_blank" rel="noopener">https://issues.apache.org/jira/browse/ZOOKEEPER-775</a></li><li>JAVA Message Service: <a href="http://download.oracle.com/javaee/1.3/jms/tutorial/1_3_1-" target="_blank" rel="noopener">http://download.oracle.com/javaee/1.3/jms/tutorial/1_3_1-</a> fcs/doc/jms_tutorialTOC.html.</li><li>Oracle Enterprise Messaging Service: <a href="http://www.oracle.com/technetwork/middleware/ias/index-" target="_blank" rel="noopener">http://www.oracle.com/technetwork/middleware/ias/index-</a> 093455.html</li><li><a href="http://www.rabbitmq.com/" target="_blank" rel="noopener">http://www.rabbitmq.com/</a></li><li>TIBCO Enterprise Message Service: <a href="http://www.tibco.com/products/soa/messaging/" target="_blank" rel="noopener">http://www.tibco.com/products/soa/messaging/</a></li><li>Kafka, <a href="http://sna-projects.com/kafka/" target="_blank" rel="noopener">http://sna-projects.com/kafka/</a></li></ol>]]></content>
<summary type="html">
<p>原文地址:<a href="http://notes.stephenholiday.com/Kafka.pdf" target="_blank" rel="noopener">http://notes.stephenholiday.com/Kafka.pdf</a></p>
<p>太长不看:</p>
<p>相对于JMS等其他的消息系统,Kafka舍弃了很多功能,以达到性能上的提升。</p>
<p>论文讲述了Kafka设计上的取舍,以及提升性能的很多点。</p>
</summary>
<category term="论文翻译" scheme="https://blog.lovezhy.cc/categories/%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91/"/>
<category term="Kafka" scheme="https://blog.lovezhy.cc/tags/Kafka/"/>
</entry>
<entry>
<title>业务思考-点赞列表怎么做</title>
<link href="https://blog.lovezhy.cc/2020/03/16/%E4%B8%9A%E5%8A%A1%E6%80%9D%E8%80%83-%E7%82%B9%E8%B5%9E%E5%88%97%E8%A1%A8%E6%80%8E%E4%B9%88%E5%81%9A/"/>
<id>https://blog.lovezhy.cc/2020/03/16/%E4%B8%9A%E5%8A%A1%E6%80%9D%E8%80%83-%E7%82%B9%E8%B5%9E%E5%88%97%E8%A1%A8%E6%80%8E%E4%B9%88%E5%81%9A/</id>
<published>2020-03-15T16:00:00.000Z</published>
<updated>2020-03-17T15:34:22.000Z</updated>
<content type="html"><![CDATA[<p>在小米有品的工作内容也算是和社交有点关系,会有类似微博的点赞,查看点赞列表的功能。<br>这个功能看起来简单,其实做起来一点都不容易。<br>为了避嫌,这里以微博为例,讲一讲自己的思考。<br>类似的,还有关注列表等。这里就简单思考点赞列表。</p><a id="more"></a><h1 id="功能"><a href="#功能" class="headerlink" title="功能"></a>功能</h1><p>微博上,我们可以给一个具体的微博点赞,然后个人中心页面可以查看自己点赞的内容的历史<br>所以基本功能概括起来如下:</p><ol><li>给微博点赞/取消点赞</li><li>查看是否给该微博点过赞</li><li>查看历史点赞记录</li></ol><p>在要应对的数据量比较大情况下,要完全实现上面这三个功能也不容易。尤其是这种很典型的具体冷热属性的数据。<br>所以会有一些产品妥协策略:</p><ol><li>时间久远的微博,默认返回未点过赞 //这种产品可能会比较同意</li><li>时间久远的微博,点赞记录中找不到 //这种一般不会同意的,放弃吧<br>为什么这么妥协会比较好做呢?下面再详细聊聊</li></ol><p>下面看看怎么实现</p><h1 id="Redis"><a href="#Redis" class="headerlink" title="Redis"></a>Redis</h1><p>这个是最简单的实现方式<br>其实还有更简单的,就是只有Mysql,但是这种一般都不会使用的,除非自己写写应用。</p><p>每个用户的点赞列表都存为一个ZSET<br><code>Key=weibo:like:${uid}</code><br><code>Value=${weiboId},Score=${Time}</code></p><ol><li>点赞时加入到ZSET,取消点赞时从ZSET中删除</li><li>查询是否点过赞使用zscore</li><li>历史点赞记录用zrange</li></ol><h2 id="注意事项一"><a href="#注意事项一" class="headerlink" title="注意事项一"></a>注意事项一</h2><p>没问题吗?<br>是的,一般来说这么搞就行了,但是其实有个不小的瑕疵。<br>查询历史点赞记录用zrange。</p><p>想象如下的例子:</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">request: {</span><br><span class="line">page: 0,</span><br><span class="line">pageSize: 10</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>好,我们用<code>zrange(key, page, pageSize)</code>返回前十条</p><p>我看到自己的前十个点赞记录,卧槽太傻比了,全部取消点赞<br>ok,我们zrem() * 10次,把zset中前10个记录删除了。</p><p>再来请求下一页:<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">request: {</span><br><span class="line">page: 1,</span><br><span class="line">pageSize: 10</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>我们用<code>zrange(key, page, pageSize)</code>返回前十条</p><p>发现问题了吗?<br>第二次zrange的10条,其实是最原始数据的20-30条。<br>中间有一页的点赞记录因为我们zrem的原因,加载不出来。</p><p>这就是用zset做分页的普遍缺点。</p><p>怎么解呢?<br>有个简单的方法,我们用<code>rangeByScore</code>方法,其实参数最大值,是上一页的最小的一个<code>Score</code>。<br>这样,前端每次的请求其实是带上上一页的最小的那个时间戳<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">request: {</span><br><span class="line">page: x,</span><br><span class="line">pageSize: 10,</span><br><span class="line">lastTime: 103232</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>这样就可以解决了。</p><h2 id="注意事项二"><a href="#注意事项二" class="headerlink" title="注意事项二"></a>注意事项二</h2><p>但是还有个问题:<br>我点赞了微博id=23。<br>然后这条微博被用户删除了。<br>那我从zset中拉到这个id,组装数据时会发现id=23查找不到。</p><p>这个时候其实有两种选择:</p><ol><li>告诉用户这个点赞内容被删除了,微博就是这么做的</li><li>返回空</li></ol><p>返回空其实又带来一个问题<br>如果我很不巧,第4页的点赞微博都是一个人的,她清空了微博<br>那请求和响应就会变成这样:<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">request: {</span><br><span class="line">page: 3,</span><br><span class="line">pageSize: 10,</span><br><span class="line">lastTime: 103232</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">response: {</span><br><span class="line">[]</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>后端返回了一个空数据。</p><p>如果这么定义的话,前端会以为已经请求空了,就会告诉用户已经没有数据了。</p><p>这个时候其实就出BUG了。</p><p>那这个怎么解呢?<br>很容易想到的就是:<br>response中带上total字段,前端判断后续有没有数据按照total来。<br>那其实和注意事项一又冲突了。不好。</p><p>还有个解法:<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">response: {</span><br><span class="line">[],</span><br><span class="line">hasNext: true</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>用<code>hasNext</code>告诉前端有没有后续数据了<br><code>hasNext</code>怎么来呢?<br>我们从zset中range获取的时候,如果拉出来的个数小于pageSize,那么就是false。<br>如果等于pageSize,那么就是true。</p><h2 id="妥协策略"><a href="#妥协策略" class="headerlink" title="妥协策略"></a>妥协策略</h2><p>全存Redis,当然会有问题,数据量太大怎么办?<br>对于妥协策略1,我们定时的扫我们的Key(或者查询时,插入时异步操作),如果发现有些点赞记录太久远,就把Value删除。<br>这样我们的Redis负担就小点,<br>但是对不起,这样其实把妥协策略2也做了,是行不通的。</p><h1 id="类Redis数据库"><a href="#类Redis数据库" class="headerlink" title="类Redis数据库"></a>类Redis数据库</h1><p>但是又不想抛弃Redis,因为Redis实现起来确实简单啊。<br>那怎么办?<br>类Redis数据库来救场了。</p><p>类Redis说白了就是兼容Redis的指令,但是存储上,不全存内存,会存到磁盘上。<br>目前市面上比较流行的类Redis数据库有Pika,SSDB这种<br>具体笔者也没使用过,就不做评价,简单介绍下<br>小公司可以自己搭建着玩玩,但是大公司可能就没这个场景了,需要懂这个的运维来支持。</p><h2 id="Pika"><a href="#Pika" class="headerlink" title="Pika"></a>Pika</h2><h2 id="SSDB"><a href="#SSDB" class="headerlink" title="SSDB"></a>SSDB</h2><h1 id="Redis-Mysql"><a href="#Redis-Mysql" class="headerlink" title="Redis + Mysql"></a>Redis + Mysql</h1><p>这种比较少见其实,但是好歹这两数据库在公司都是标配。<br>主要是Redis存热数据,Mysql存冷数据。</p><p>写的时候双写<br>查询的时候先查Redis,Redis查不到再去查Mysql<br>分页查询的时候,查Redis,过期了就去Mysql捞一部分,然后存回Redis,设置个过期时间。<br>太久的就直接查Mysql,没必要存Redis了。</p><p>但是这里得考虑几个问题:</p><ol><li>这种行为数据,实时写数据库一般不会同意的,可以先写Redis,然后搞个消息队列慢慢写数据库</li><li>查是否给该文章点赞过,先查Redis,如果空了,再查Mysql。可能会出问题,有点隐患,不过也不用太担心,因为在Mysql中的一般就是冷数据库,问题不大。Redis存的容量大一点。</li><li>分页查询点赞历史,先查Redis,到底了去查Mysql,这里切换的衔接逻辑得好好想想。问题也不是很大。</li></ol><p>看起来很不错是不是,但是这种方案,最大的问题还是Mysql。<br>你想想这个表里的数据长啥样?<br>就几个字段:</p><ol><li>id:自增主键</li><li>uid:用户id</li><li>weiboId:微博id</li><li>createTime:点赞时间</li><li>del:是否删除了(这个看公司吧,有的只允许逻辑删除)</li></ol><p>这表数据太简单了,如果真到微博那种量级,增长速度会很快很快。<br>假设用户200w,每个人点赞2篇内容,那么一天增长400w条记录,一年就146000w,14亿。<br>这谁顶得住。</p><p>这种其实硬要解还是有点方法:</p><ol><li>压缩表:把字段weiboId,改成weiboIds,一行记录多存几个点赞记录。数据行数可以缩小几个量级,但是插入,查询和Redis衔接起来就比较复杂了。<strong>同时删除几乎不好做了。</strong></li><li>分库分表。其实我感觉分库分表意义不大。</li></ol><h2 id="妥协策略-1"><a href="#妥协策略-1" class="headerlink" title="妥协策略"></a>妥协策略</h2><p>来看看这种方案,如果产品妥协了,会不会简单点:<br>妥协策略1:查是否点过赞,Redis查不到,就默认未点赞,不用去查Mysql了。<br>妥协策略2:查完Redis,去查Mysql,可以支持。</p><p>其实再拓展下,如果产品妥协了策略1,那么写入的时候,只写Redis,然后再在某个时间点,把冷数据同步到Mysql就行。<br>这样就不用双写数据库了,同时同步的时候可以批量查入。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>所以综合来看,功能上,对热点数据的点赞/取消点赞/查询是否点赞比较好<br>如果你压缩数据行:对冷数据(Mysql中的数据),取消点赞,分页查询点赞记录比较复杂。<br>如果你不压缩:数据量太大</p><h1 id="Redis-Hbase"><a href="#Redis-Hbase" class="headerlink" title="Redis + Hbase"></a>Redis + Hbase</h1><p>Redis + Hbase算是比较终极的方案了。<br>其实笔者对Hbase也不是很了解。<br>了解了再说吧。</p>]]></content>
<summary type="html">
<p>在小米有品的工作内容也算是和社交有点关系,会有类似微博的点赞,查看点赞列表的功能。<br>这个功能看起来简单,其实做起来一点都不容易。<br>为了避嫌,这里以微博为例,讲一讲自己的思考。<br>类似的,还有关注列表等。这里就简单思考点赞列表。</p>
</summary>
<category term="业务思考" scheme="https://blog.lovezhy.cc/categories/%E4%B8%9A%E5%8A%A1%E6%80%9D%E8%80%83/"/>
<category term="Redis" scheme="https://blog.lovezhy.cc/tags/Redis/"/>
<category term="业务思考" scheme="https://blog.lovezhy.cc/tags/%E4%B8%9A%E5%8A%A1%E6%80%9D%E8%80%83/"/>
<category term="Hbase" scheme="https://blog.lovezhy.cc/tags/Hbase/"/>
<category term="Pika" scheme="https://blog.lovezhy.cc/tags/Pika/"/>
<category term="分库分表" scheme="https://blog.lovezhy.cc/tags/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/"/>
</entry>
<entry>
<title>搞懂内存屏障-指令与JMM</title>
<link href="https://blog.lovezhy.cc/2020/03/14/%E6%90%9E%E6%87%82%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C-%E6%8C%87%E4%BB%A4%E4%B8%8EJMM/"/>
<id>https://blog.lovezhy.cc/2020/03/14/%E6%90%9E%E6%87%82%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C-%E6%8C%87%E4%BB%A4%E4%B8%8EJMM/</id>
<published>2020-03-13T16:00:00.000Z</published>
<updated>2020-03-14T14:55:25.770Z</updated>
<content type="html"><![CDATA[<p>前面讲了CPU的演进,提出了StoreBuffer和InvalidateQueue的设计,并且讲解了这两个设计会带来的问题。<br>解决这两个问题就是引入内存屏障:强制刷新StoreBuffer和InvalidateQueue。</p><p>这里详细讲讲x86机器上的内存屏障指令与其他隐式的含有内存屏障的指令。<br>然后再聊一聊JMM与内存屏障的对应关系。</p><a id="more"></a><h1 id="x86与内存屏障"><a href="#x86与内存屏障" class="headerlink" title="x86与内存屏障"></a>x86与内存屏障</h1><p>前面提到的StoreBuffer和InvalidateQueue并不是所有的CPU都会去实现。<br>其中x86的机器上,遵循的内存一致性协议叫TSO协议。<br>在这个协议中,有个叫WriteBuffer的东西,就是对应StoreBuffer。<br>但是并没有InvalidateQueue的存在。</p><h1 id="内存屏障指令集"><a href="#内存屏障指令集" class="headerlink" title="内存屏障指令集"></a>内存屏障指令集</h1><p>上文中,提到了三个内存屏障的指令:</p><ol><li>lfence():读屏障</li><li>sfence():写屏障</li><li>mfence():读写屏障</li></ol><p>那么在代码中是怎么定义的呢:<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> barrier() __asm__ __volatile__(<span class="meta-string">""</span>: : :<span class="meta-string">"memory"</span>) </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> mb() alternative(<span class="meta-string">"lock; addl $0,0(%%esp)"</span>, <span class="meta-string">"mfence"</span>, X86_FEATURE_XMM2) </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> rmb() alternative(<span class="meta-string">"lock; addl $0,0(%%esp)"</span>, <span class="meta-string">"lfence"</span>, X86_FEATURE_XMM2)</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> wmb() alternative(<span class="meta-string">"lock; addl $0,0(%%esp)"</span>, <span class="meta-string">"sfence"</span>, X86_FEATURE_XMM) </span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> CONFIG_SMP </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_mb() mb() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_rmb() rmb() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_wmb() wmb() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_read_barrier_depends() read_barrier_depends() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> set_mb(var, value) do { (void) xchg(&var, value); } while (0) </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">else</span> </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_mb() barrier() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_rmb() barrier() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_wmb() barrier() </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> smp_read_barrier_depends() do { } while(0) </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> set_mb(var, value) do { var = value; barrier(); } while (0) </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br></pre></td></tr></table></figure></p><p>首先来看barrirer()的定义,这个是禁止编译器进行重排序的。<br>具体的解释可以参考笔者的另外一个文章:<a href="https://blog.lovezhy.cc/2020/03/08/volatile%E5%92%8C%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C/">volatile和内存屏障</a></p><p>然后我们看CONFIG_SMP,如果定义了这个,说明该机器上不止一个Core,否则就是单核心的机器。<br>在单核心的机器上,所有的CPU的内存屏障指令都是空指令,只有禁止编译器重排序的作用。<br>这个也好理解,就不多做解释了。</p><p>而在多核心的机器上,分别定义了:</p><ol><li>smp_mb():读写屏障</li><li>smp_rmb():读屏障</li><li>smp_wmb():写屏障</li></ol><p>同时我们看具体的实现,也就是用到了我们上面提到了lfence,sfence,mfence。</p><p>但是我们再仔细看看这句话:<br><code>#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)</code></p><p>如果CPU没有lfence指令,那么就用<code>lock; addl $0,0(%%esp)</code>代替。<br>为什么?难道<code>lock; addl $0,0(%%esp)</code>也能有内存屏障的语义吗?</p><p>是的!<br>除了fence指令,还有很多的其他的指令也隐藏了内存屏障的语义。<br>下面笔者来总结一下:</p><h2 id="常见的三种"><a href="#常见的三种" class="headerlink" title="常见的三种"></a>常见的三种</h2><p>x86/64系统架构提供了三种多核的内存屏障指令:(1) sfence; (2) lfence; (3) mfence</p><ol><li>sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。</li><li>lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。</li><li>mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。</li></ol><p>其实总结起来就是读屏障,写屏障,读写屏障。</p><p>上述的是显式的会起到内存屏障作用的指令,但是还有许多指令带有异常的内存屏障的作用。</p><h2 id="MMIO写屏障"><a href="#MMIO写屏障" class="headerlink" title="MMIO写屏障"></a>MMIO写屏障</h2><p>Linux 内核有一个专门用于 MMIO 写的屏障:<br><code>mmiowb()</code><br>笔者也不熟悉这个的作用,后续再补上</p><h2 id="隐藏的内存屏障"><a href="#隐藏的内存屏障" class="headerlink" title="隐藏的内存屏障"></a>隐藏的内存屏障</h2><p>Linux 内核中一些锁或者调度函数暗含了内存屏障。</p><p>锁函数:</p><ul><li>spin locks</li><li>R/W spin locks</li><li>mutexes</li><li>semaphores</li><li>R/W semaphores</li></ul><p>中断禁止函数:<br>启动或禁止终端的函数的作用仅仅是作为编译器屏障,所以要使用内存或者 I/O 屏障 的场合,必须用别的函数。</p><p>SLEEP和WAKE-UP以及其它调度函数:<br>使用 SLEEP 和 WAKE-UP 函数时要改变 task 的状态标志,这需要使用合适的内存屏 障保证修改的顺序。</p><h1 id="JMM"><a href="#JMM" class="headerlink" title="JMM"></a>JMM</h1><p>在JMM中,定义了4中内存可见性语义:</p><ol><li>LoadLoad</li><li>LoadStore</li><li>StoreStore</li><li>StoreLoad</li></ol><p>但是这些指令对应到x86的机器上,并不是都需要实现的。<br>因为x86的核心问题是有StoreBuffer,一个值被Core0写入了StoreBuffer,另外一个Core可能读不到最新的值,除非Flush StoreBuffer。所以StoreLoad语义需要内存屏障来维持。</p><p>例如以下的例子:<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">foo</span><span class="params">(<span class="keyword">void</span>)</span> </span>{</span><br><span class="line"> x=<span class="number">1</span>; <span class="comment">//S1</span></span><br><span class="line"> r1=y; <span class="comment">//S2</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">bar</span><span class="params">(<span class="keyword">void</span>)</span> </span>{</span><br><span class="line"> y=<span class="number">1</span>; <span class="comment">//L1</span></span><br><span class="line"> r2=x;<span class="comment">//L2</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>在这个例子中,如果没有内存屏障,Core0执行foo,Core1执行bar,则r1和r2可能出现同时为0的情况。</p><p>再具体的这个文章讲的很好:<a href="https://zhuanlan.zhihu.com/p/81555436" target="_blank" rel="noopener">为什么在 x86 架构下只有 StoreLoad 屏障是有效指令?</a></p><h2 id="更具体的例子"><a href="#更具体的例子" class="headerlink" title="更具体的例子"></a>更具体的例子</h2><p>下面我们看看代码,经过JIT编译后的指令<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">int</span> a = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">int</span> b = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">10000</span>; i++) {</span><br><span class="line"> add();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">100</span>; i++) {</span><br><span class="line"> a++;</span><br><span class="line"> b += <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>如果a没有被volatile修饰:<br><img src="/images/搞定内存屏障-指令与JMM/1.png" alt="image-20200314152942448"><br>可以看到a和b的操作分别对应:<br><code>inc %r9d</code><br><code>add $0x2, %r9d</code><br>中间没有任何内存屏障的指令</p><p>如果我们加上volatile修饰呢?<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">volatile</span> <span class="keyword">int</span> a = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">volatile</span> <span class="keyword">int</span> b = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">10000</span>; i++) {</span><br><span class="line"> add();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">100</span>; i++) {</span><br><span class="line"> a++;</span><br><span class="line"> b += <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p><img src="/images/搞定内存屏障-指令与JMM/2.png" alt="image-20200314153303884"><br>可以很明显的看到两个<code>lock</code>指令。</p>]]></content>
<summary type="html">
<p>前面讲了CPU的演进,提出了StoreBuffer和InvalidateQueue的设计,并且讲解了这两个设计会带来的问题。<br>解决这两个问题就是引入内存屏障:强制刷新StoreBuffer和InvalidateQueue。</p>
<p>这里详细讲讲x86机器上的内存屏障指令与其他隐式的含有内存屏障的指令。<br>然后再聊一聊JMM与内存屏障的对应关系。</p>
</summary>
<category term="计算机基础" scheme="https://blog.lovezhy.cc/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/"/>
<category term="内存屏障" scheme="https://blog.lovezhy.cc/tags/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C/"/>
</entry>
</feed>