-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.xml
429 lines (429 loc) · 126 KB
/
index.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
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>– Envoy中文社区</title><link>/</link><description>Recent content in Envoy中文社区 on</description><generator>Hugo -- gohugo.io</generator><atom:link href="/index.xml" rel="self" type="application/rss+xml"/><item><title>Blog: Envoy源码分析之Dispatcher机制</title><link>/blog/2020/08/23/envoy%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bdispatcher%E6%9C%BA%E5%88%B6/</link><pubDate>Sun, 23 Aug 2020 00:00:00 +0000</pubDate><guid>/blog/2020/08/23/envoy%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bdispatcher%E6%9C%BA%E5%88%B6/</guid><description>
<h1 id="dispatcher机制">Dispatcher机制</h1>
<p> Envoy和Nginx一样都是基于事件驱动的架构,这种架构的核心就是事件循环(EventLoop)。业界目前典型的几种事件循环实现主要有Libevent、Libev、Libuv、Boost.Asio等,也可以完全基于Linux系统调用epoll来实现。Envoy选择在Libevent的基础上进行了封装,实现了自己的事件循环机制,在Envoy中被称为<code>Dispatcher</code>,一个<code>Dispatcher</code>对象就是一个事件分发器,就如同它的名字一样。<code>Dispatcher</code>是Envoy的核心,可以说Envoy中绝大部分的能力都是构建在<code>Dispatcher</code>的基础上。所以理解<code>Dispatcher</code>机制是掌握Envoy的一个很重要的前提。</p>
<p> 在Envoy中<code>Dispatcher</code>不仅仅提供了网络事件分发、定时器、信号处理等基本的事件循环能力,还在事件循环的基础上实现任务执行队列、<code>DeferredDelet</code>等,这两个功能为Envoy中很多组件提供了必不可少的基础能力。比如借助<code>DeferredDelet</code>实现了安全的对象析构,通过任务执行队列实现Thread Local机制等等。</p>
<h1 id="libevent事件封装">Libevent事件封装</h1>
<p> Envoy在Libevent的基础上进行了封装最为重要的一个原因就是因为Libevent本身是C开发的,很多Libevent暴露出来的结构需要自己来管理内存的分配和释放,这对于现代化的C++来说显然是无法接受的,因此Envoy借助了C++的RAII机制将这些结构封装起来,自动管理内存资源的释放。接下来我们看下Envoy是如何进行封装的。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">template</span> <span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">T</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000">deleter</span><span style="color:#000;font-weight:bold">)(</span><span style="color:#000">T</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)</span><span style="color:#ce5c00;font-weight:bold">&gt;</span>
<span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">CSmartPtr</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">unique_ptr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">T</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)(</span><span style="color:#000">T</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000">CSmartPtr</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">unique_ptr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">T</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)(</span><span style="color:#000">T</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)</span><span style="color:#ce5c00;font-weight:bold">&gt;</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">nullptr</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">deleter</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{}</span>
<span style="color:#000">CSmartPtr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">T</span><span style="color:#ce5c00;font-weight:bold">*</span> <span style="color:#000">object</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">unique_ptr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">T</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)(</span><span style="color:#000">T</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000;font-weight:bold">)</span><span style="color:#ce5c00;font-weight:bold">&gt;</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">object</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">deleter</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{}</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p> Envoy通过继承<code>unique_ptr</code>自定义了一个<code>CSmartPtr</code>,通过继承拥有了<code>unqiue_ptr</code>自动管理内存释放的能力,离开作用域后自动释放内存。借助<code>CSmartPtr</code>,Envoy将Libevent中的<code>event_base</code>包装成<code>BasePtr</code>,将<code>evconnlistener</code>包装成<code>ListenerPtr</code>。其中<code>event_base</code>就是事件循环,一个<code>event_base</code>就是一个事件循环,可以拥有多个事件循环,Envoy内部就是每一个worker线程都会有一个事件循环,也就是最常见的one loop per thread模型。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">using</span> <span style="color:#000">BasePtr</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">CSmartPtr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">event_base</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">event_base_free</span><span style="color:#ce5c00;font-weight:bold">&gt;</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#204a87;font-weight:bold">using</span> <span style="color:#000">ListenerPtr</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">CSmartPtr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">evconnlistener</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">evconnlistener_free</span><span style="color:#ce5c00;font-weight:bold">&gt;</span><span style="color:#000;font-weight:bold">;</span>
</code></pre></div><p> 在Libevent中无论是定时器到期、收到信号、还是文件可读写等都是事件,统一使用<code>event</code>类型来表示,Envoy中则将<code>event</code>作为<code>ImplBase</code>的成员,然后让所有的事件类型的对象都继承<code>ImplBase</code>,从而实现了事件的抽象。同时也借助了RAII机制自动实现了事件资源的释放。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">ImplBase</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">protected</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#ce5c00;font-weight:bold">~</span><span style="color:#000">ImplBase</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000">event</span> <span style="color:#000">raw_event_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
<span style="color:#000">ImplBase</span><span style="color:#ce5c00;font-weight:bold">::~</span><span style="color:#000">ImplBase</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">// Derived classes are assumed to have already assigned the raw event in the constructor.
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">event_del</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">raw_event_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 通过继承<code>ImplBase</code>基类可以拥有<code>event</code>事件成员,但是每一种事件表现出的具体行为是不一样的,比如说信号事件,需要有信号注册的能力,定时器事件则需要可以开启或者关闭定时的能力,文件事件则需要能够开启某些事件状态的监听。为此Envoy为每一种事件类型都抽象了对应的接口,例如文件事件接口。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">FileEvent</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#ce5c00;font-weight:bold">~</span><span style="color:#000">FileEvent</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#204a87;font-weight:bold">default</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 激活指定事件,会自动触发对应事件的callback
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">activate</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 开启指定事件状态的监听
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">setEnabled</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p> 有了事件基类和对应的接口类后,让我们来看下Envoy如何来实现一个文件事件对象。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#8f5902;font-style:italic">// 通过继承ImplBase拥有了event成员
</span><span style="color:#8f5902;font-style:italic"></span><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">FileEventImpl</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">FileEvent</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">ImplBase</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000">FileEventImpl</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">int</span> <span style="color:#000">fd</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">FileReadyCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">FileTriggerType</span> <span style="color:#000">trigger</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// Event::FileEvent
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// 实现了文件事件的接口,通过这个接口可以实现文件事件的监听
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">activate</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#204a87;font-weight:bold">override</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">setEnabled</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#204a87;font-weight:bold">override</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#204a87;font-weight:bold">private</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#8f5902;font-style:italic">// 初始化事件对象
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">assignEvents</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">event_base</span><span style="color:#ce5c00;font-weight:bold">*</span> <span style="color:#000">base</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// 事件触发时执行的callback
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">FileReadyCb</span> <span style="color:#000">cb_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 文件fd
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">int</span> <span style="color:#000">fd_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 事件触发的类型,边缘触发,还是水平触发
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">FileTriggerType</span> <span style="color:#000">trigger_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
<span style="color:#000">FileEventImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">FileEventImpl</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">int</span> <span style="color:#000">fd</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">FileReadyCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">FileTriggerType</span> <span style="color:#000">trigger</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span>
<span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#000">cb_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">cb</span><span style="color:#000;font-weight:bold">),</span> <span style="color:#000">fd_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">fd</span><span style="color:#000;font-weight:bold">),</span> <span style="color:#000">trigger_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">trigger</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">#ifdef WIN32
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">RELEASE_ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">trigger_</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#000">FileTriggerType</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Level</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#4e9a06">&#34;libevent does not support edge triggers on Windows&#34;</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">#endif
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// dispatcher.base()返回的就是上文中说到的BasePtr,事件循环对象
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// 通过assignEvents初始化事件对象,设置好要监听的事件状态,以及事件回调callback等
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// 内部调用的就是Libevent的event_assign方法。
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">assignEvents</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">events</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">base</span><span style="color:#000;font-weight:bold">());</span>
<span style="color:#8f5902;font-style:italic">// 将事件对象注册到事件循环中,内部调用的就是
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">event_add</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">raw_event_</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">nullptr</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 到此为止事件对象的封装就分析完了,接下来看下核心的<code>Dispatcher</code>对象,它提供了几个核心的方法来创建上文中分析的几个事件对象。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"> <span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">DispatcherImpl</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000;font-weight:bold">....</span>
<span style="color:#000">FileEventPtr</span> <span style="color:#000">createFileEvent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">int</span> <span style="color:#000">fd</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">FileReadyCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">FileTriggerType</span> <span style="color:#000">trigger</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#204a87;font-weight:bold">override</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">TimerPtr</span> <span style="color:#000">createTimer</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">TimerCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#204a87;font-weight:bold">override</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">SignalEventPtr</span> <span style="color:#000">listenForSignal</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">int</span> <span style="color:#000">signal_num</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">SignalCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#204a87;font-weight:bold">override</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">....</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 这就是<code>Dispatcher</code>对象的几个核心方法,在这几个方法的基础上又扩展了<code>createServerConnection</code>、<code>createClientConnection</code>等方法用于创建服务端和客户端连接对象,这两个方法内部最终都调用了<code>createFileEvent</code>方法,将socket文件的事件注册到了事件循环中。到此为止关于<code>Dispatcher</code>事件相关的几个方法都分析完了,但是<code>Dispatcher</code>对象远远还不止这些,比如说本文尚未提到的<code>Scheduler</code>,目前这个部分还尚未完成,这一块是对事件循环的抽象,目前是为了让事件循环组件可替换,目前只有<code>LibeventScheduler</code>一个实现。</p>
<h1 id="任务执行队列">任务执行队列</h1>
<pre><code>在上文中曾提到过Envoy在事件循环的基础上实现了两个比较重要的基础功能,其中一个就是任务执行队列了。可以随时通过`post`方法提交多个函数对象,然后交由`Dispatcher`来执行。所有的函数对象执行都是顺序的。是在`Dispatcher`所在的线程中执行。整个post方法的代码非常短。
</code></pre>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#8f5902;font-style:italic">// 所有的要执行的函数对象原型都一样,都是void()
</span><span style="color:#8f5902;font-style:italic"></span><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">post</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">function</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#204a87;font-weight:bold">void</span><span style="color:#000;font-weight:bold">()</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">callback</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">bool</span> <span style="color:#000">do_post</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">// 因为post方法可以跨线程执行,因此这里需要加锁来保证线程安全
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// 可以看出post方法本质上是将函数对象放到队列中,实际上并未执行
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">Thread</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">LockGuard</span> <span style="color:#000">lock</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">post_lock_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000">do_post</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">post_callbacks_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">empty</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000">post_callbacks_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">push_back</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">callback</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">do_post</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">post_timer_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">enableTimer</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">chrono</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">milliseconds</span><span style="color:#000;font-weight:bold">(</span><span style="color:#0000cf;font-weight:bold">0</span><span style="color:#000;font-weight:bold">));</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> <code>post</code>方法将传递进来的<code>callback</code>所代表的任务,添加到<code>post_callbacks_</code>所代表的类型为<code>vector&lt;callback&gt;</code>的成员变量中。如果<code>post_callbacks_</code>为空的话,说明背后的处理线程是处于非活动状态,这时通过<code>post_timer_</code>设置一个超时时间时间为0的方式来唤醒它。<code>post_timer_</code>在构造的时候就已经设置好对应的<code>callback</code>为<code>runPostCallbacks</code>,对应代码如下:</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">DispatcherImpl</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">TimeSystem</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">time_system</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">Buffer</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">WatermarkFactoryPtr</span><span style="color:#ce5c00;font-weight:bold">&amp;&amp;</span> <span style="color:#000">factory</span><span style="color:#000;font-weight:bold">)</span>
<span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#000;font-weight:bold">......</span>
<span style="color:#000">post_timer_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">createTimer</span><span style="color:#000;font-weight:bold">([</span><span style="color:#204a87;font-weight:bold">this</span><span style="color:#000;font-weight:bold">]()</span> <span style="color:#ce5c00;font-weight:bold">-&gt;</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">{</span> <span style="color:#000">runPostCallbacks</span><span style="color:#000;font-weight:bold">();</span> <span style="color:#000;font-weight:bold">})),</span>
<span style="color:#000">current_to_delete_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">to_delete_1_</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">RELEASE_ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Libevent</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Global</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">initialized</span><span style="color:#000;font-weight:bold">(),</span> <span style="color:#4e9a06">&#34;&#34;</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> <code>runPostCallbacks</code>是一个while循环,每次都从<code>post_callbacks_</code>中取出一个<code>callback</code>所代表的任务去运行,直到<code>post_callbacks_</code>为空。每次运行<code>runPostCallbacks</code>都会确保所有的任务都执行完。显然,在<code>runPostCallbacks</code>被线程执行的期间如果<code>post</code>进来了新的任务,那么新任务直接追加到<code>post_callbacks_</code>尾部即可,而无需做唤醒线程这一动作。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">runPostCallbacks</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">while</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#204a87">true</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">function</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#204a87;font-weight:bold">void</span><span style="color:#000;font-weight:bold">()</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">callback</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">Thread</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">LockGuard</span> <span style="color:#000">lock</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">post_lock_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">post_callbacks_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">empty</span><span style="color:#000;font-weight:bold">())</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">return</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000">callback</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">post_callbacks_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">front</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000">post_callbacks_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">pop_front</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000">callback</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 到此为止Envoy中的任务执行队列就分析完了,可以看出这个部分的代码实现还是很简单的,也很容易验证其正确性,在Envoy的代码中被广泛使用。这个能力和Boost::asio中的post task是类似的。</p>
<h1 id="deferreddeletable">DeferredDeletable</h1>
<p> 本小节是<code>Dispatcher</code>中最重要的一个部分<code>DeferredDeletable</code>,又被称为延迟析构,目的是用于安全的进行对象析构。C++语言本身会存在对象析构了,但还有引用它的指针存在,这个时候通过这个指针访问这个对象就会导致未定义行为了。因此写C++的同学就需要特别注意一个对象的生命周期问题,要保证引用一个对象的时候,对象还没有被析构。在C++中有不少方案可以来解决这个问题,典型的像使用shared_ptr的方式。而本文的要分析的<code>DeferredDeletable</code>则是使用另外一种方式来解决对象安全析构问题,这个方案的并不是一个通用的方案,仅能解决部分场景下的对象安全析构问题,但是对于Envoy使用到的场景已经足够了,接下来我们将分析它是如何做到对象安全析构的。</p>
<p> <code>DeferredDeletable</code>本身是一个空接口,所有要进行延迟析构的对象都要继承自这个空接口。在Envoy的代码中像下面这样继承自<code>DeferredDeletable</code>的类随处可见。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">DeferredDeletable</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#ce5c00;font-weight:bold">~</span><span style="color:#000">DeferredDeletable</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{}</span>
<span style="color:#000;font-weight:bold">};</span>
<span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">Connection</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">DeferredDeletable</span> <span style="color:#000;font-weight:bold">{</span> <span style="color:#000;font-weight:bold">....</span> <span style="color:#000;font-weight:bold">}</span>
<span style="color:#8f5902;font-style:italic">/**
</span><span style="color:#8f5902;font-style:italic"> * An instance of a generic connection pool.
</span><span style="color:#8f5902;font-style:italic"> */</span>
<span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">Instance</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">DeferredDeletable</span> <span style="color:#000;font-weight:bold">{</span> <span style="color:#000;font-weight:bold">.....</span> <span style="color:#000;font-weight:bold">}</span>
<span style="color:#8f5902;font-style:italic">/**
</span><span style="color:#8f5902;font-style:italic"> * Implementation of AsyncRequest. This implementation is capable of
</span><span style="color:#8f5902;font-style:italic"> * sending HTTP requests to a ConnectionPool asynchronously.
</span><span style="color:#8f5902;font-style:italic"> */</span>
<span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">AsyncStreamImpl</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">DeferredDeletable</span><span style="color:#000;font-weight:bold">{....}</span>
</code></pre></div><p> 这些继承<code>DeferredDeletable</code>接口的类都有一个特点,这些类基本上都是一些具有短暂生命周期的对象,比如连接对象、请求对象等。这也正是上文中提到的延迟析构并非是是一个通用方案,只是针对Envoy中的一些特定场景。<code>DeferredDeletable</code>和<code>Dispatcher</code>是密切相关,是基于<code>Dispatcher</code>来完成的。<code>Dispatcher</code>对象有一个<code>vector</code>保存了所有要延迟析构的对象。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">DispatcherImpl</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">Dispatcher</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000;font-weight:bold">......</span>
<span style="color:#204a87;font-weight:bold">private</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000;font-weight:bold">........</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">DeferredDeletablePtr</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">to_delete_1_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">DeferredDeletablePtr</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">to_delete_2_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">DeferredDeletablePtr</span><span style="color:#ce5c00;font-weight:bold">&gt;*</span> <span style="color:#000">current_to_delete_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> <code>to_delete_1_</code>和<code>to_delete_2_</code>就是用来存放所有的要延迟析构的对象,这里使用两个<code>vector</code>存放,为什么要这样做呢?或许可能有人会想这是因为要保证线程安全,不能往一个正在析构的列表中添加对象。其实并非如此,多线程操作一个队列本就是非线程安全的,所以这里使用两个队列的目的并非是为了线程安全的。带着这个疑问继续往下分析,<code>current_to_delete_</code>始终指向当前正要析构的对象列表,每次执行完析构后就交替指向另外一个对象列表,来回交替。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">clearDeferredDeleteList</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">isThreadSafe</span><span style="color:#000;font-weight:bold">());</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">DeferredDeletablePtr</span><span style="color:#ce5c00;font-weight:bold">&gt;*</span> <span style="color:#000">to_delete</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">current_to_delete_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">size_t</span> <span style="color:#000">num_to_delete</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">to_delete</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">size</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#8f5902;font-style:italic">// 如果正在删除或者没有对象可删除就返回
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">deferred_deleting_</span> <span style="color:#ce5c00;font-weight:bold">||</span> <span style="color:#ce5c00;font-weight:bold">!</span><span style="color:#000">num_to_delete</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">return</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#8f5902;font-style:italic">// 正式开始删除对象
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">ENVOY_LOG</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">trace</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#4e9a06">&#34;clearing deferred deletion list (size={})&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">num_to_delete</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// current_to_delete_指向另外一个没有进行删除的队列
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">current_to_delete_</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">to_delete_1_</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">current_to_delete_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">to_delete_2_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span> <span style="color:#204a87;font-weight:bold">else</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">current_to_delete_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#ce5c00;font-weight:bold">&amp;</span><span style="color:#000">to_delete_1_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#8f5902;font-style:italic">// 设置正在删除的标志
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">deferred_deleting_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#204a87">true</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 开始进行对象析构
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">for</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">size_t</span> <span style="color:#000">i</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#0000cf;font-weight:bold">0</span><span style="color:#000;font-weight:bold">;</span> <span style="color:#000">i</span> <span style="color:#ce5c00;font-weight:bold">&lt;</span> <span style="color:#000">num_to_delete</span><span style="color:#000;font-weight:bold">;</span> <span style="color:#000">i</span><span style="color:#ce5c00;font-weight:bold">++</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000">to_delete</span><span style="color:#000;font-weight:bold">)[</span><span style="color:#000">i</span><span style="color:#000;font-weight:bold">].</span><span style="color:#000">reset</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000">to_delete</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">clear</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#8f5902;font-style:italic">// 结束
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">deferred_deleting_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#204a87">false</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 上面的代码中我们可以看到在执行对象析构的时候先使用<code>to_delete</code>来指向当前正要析构的对象列表,然后将<code>current_to_delete_</code>指向另外一个列表,这里为什么要设置<code>deferred_deleting_</code>标志呢? 这是因为<code>clearDeferredDeleteList</code>可能会被调用多次,如果已经有对象正在析构,那么就不能再进行析构操作了,因此这里通过<code>deferred_deleting_</code>标志来保证同一时刻只能有一个对象析构的任务在执行。</p>
<blockquote>
<p>假设没有<code>deferred_deleting_</code>标志,如果此时正在执行<code>to_delete_1_</code>队列的对象析构,在析构的过程中调用了<code>clearDeferredDeleteList</code>,那么这个时候会对<code>to_delete_2_</code>队列开始析构,并且将<code>current_to_delete_</code>指向<code>to_delete_1_</code>,后续的待析构对象就都会添加到<code>to_delete_1_</code>队列中,这可能会导致对<code>to_delete_1_</code>析构的任务执行较长时间。影响其它关键任务的执行。</p>
</blockquote>
<p> 接下来我们来看下如何将对象添加到待析构的列表中。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">DispatcherImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">deferredDelete</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">DeferredDeletablePtr</span><span style="color:#ce5c00;font-weight:bold">&amp;&amp;</span> <span style="color:#000">to_delete</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">isThreadSafe</span><span style="color:#000;font-weight:bold">());</span>
<span style="color:#000">current_to_delete_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">emplace_back</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">move</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">to_delete</span><span style="color:#000;font-weight:bold">));</span>
<span style="color:#000">ENVOY_LOG</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">trace</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#4e9a06">&#34;item added to deferred deletion list (size={})&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">current_to_delete_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">size</span><span style="color:#000;font-weight:bold">());</span>
<span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#0000cf;font-weight:bold">1</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#000">current_to_delete_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">size</span><span style="color:#000;font-weight:bold">())</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">deferred_delete_timer_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">enableTimer</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">chrono</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">milliseconds</span><span style="color:#000;font-weight:bold">(</span><span style="color:#0000cf;font-weight:bold">0</span><span style="color:#000;font-weight:bold">));</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> <code>deferredDelete</code>和<code>clearDeferredDeleteList</code>这两个方法都调用了<code> ASSERT(isThreadSafe());</code>目的是断言调用这两个方法是在Dispatcher所在线程执行的,是单线程运行。可以保证线程安全。 既然如此我们便可以安全的往待析构的对象列表中追加对象了,这也验证了两个队列的设计并非是为了线程安全。那为何还要搞出<code>to_delete_1_</code>和<code>to_delete_2_</code>两个列表呢? 完全可以通过一个列表来实现,通过while循环不断的进行对象析构,直到列表为空。在处理的过程中还可以往列表中追加对象。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">while</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">!</span><span style="color:#000">current_to_delete_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">empty</span><span style="color:#000;font-weight:bold">())</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">auto</span> <span style="color:#000">obj</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">current_to_delete_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">pop_back</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#8f5902;font-style:italic">// 进行业务逻辑的处理
</span><span style="color:#8f5902;font-style:italic"></span><span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 从功能正确性的角度来看,这里使用两个列表,还是一个列表都可以正确实现,在上文中分析的任务执行队列其实就是使用一个列表来完成的。但是Envoy在这里选择了两个队列的方式,这是因为相比于任务执行队列来说延迟析构的重要性更低一些,大量对象的析构如果保存在一个队列中循环的进行析构势必会影响其他关键任务的执行,所以这里拆分成两个队列,多个任务交替的执行,避免被一个大的耗时任务长期占用,导致其他关键任务无法及时执行。</p>
<blockquote>
<p>如果用一个队列做对象析构,在对象的析构函数中可能还会再次调用deferredDelete将新的对象追加到待析构的列表中,所以可能会导致队列中的任务不断增加,造成整个对象析构耗时较长。</p>
</blockquote>
<p> 继续看<code>deferredDelete</code>的代码我们会发现另外一个问题,为何要在当前待析构对象的列表大小等于1的时候唤起定时器任务呢?</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#0000cf;font-weight:bold">1</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#000">current_to_delete_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">size</span><span style="color:#000;font-weight:bold">())</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">// deferred_delete_timer_(createTimerInternal([this]() -&gt; void {
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// clearDeferredDeleteList(); })),
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// deferred_delete_timer_定时器对应的任务就是clearDeferredDeleteList
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">deferred_delete_timer_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">enableTimer</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">chrono</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">milliseconds</span><span style="color:#000;font-weight:bold">(</span><span style="color:#0000cf;font-weight:bold">0</span><span style="color:#000;font-weight:bold">));</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 假设我们每次添加对象到当前列表中都进行唤醒,那么带来的问题就是<code>clearDeferredDeleteList</code>的任务会有多个,但是实际上只有两个队列,只需要有两个<code>clearDeferredDeleteList</code>任务就可以将两个队列中的对象都析构掉,那么剩下的任务将不会进行任何实际的工作。很显然这样会带来CPU上的浪费,因此我们应该尽可能的少唤醒,保证任何时候最多只有两个任务。因此我们只要能保证在每次队列为空的时候唤醒一次即可,因为唤醒的这次任务会负责将这个队列变为空,到时候在此唤醒一个任务即可。这也就是为什么这里通过判断当前待析构对象的列表大小等于1的原因了。</p>
<p> 到此为止<code>deferredDelete</code>的实现原理就基本分析完了,可以看出它的实现和任务队列的实现很类似,只不过一个是循环执行<code>callback</code>所代表的任务,另一个是让对象进行析构。最后让我们通过下图来看下整个<code>deferredDelete</code>的流程。</p>
<p><img src="https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/ebb781960da723965cdd9d968dc10258.png" alt="4-1.png"></p>
<ul>
<li>对象要被析构了,开始调用<code>deferredDelete</code>将对象添加到<code>to_delete_1</code>队列中,然后唤醒<code>clearDeferredDeleteList</code>任务。</li>
<li><code>clearDeferredDeleteList</code>任务开始执行,<code>current_to_delete</code>指向<code>to_delete_2</code>队列</li>
<li>对象在析构的过程中又通过<code>deferredDelete</code>添加了新的对象到to_delete_2队列中,这个队列初始是空的,因此再次唤醒一个<code>clearDeferredDeleteList</code>任务。</li>
<li><code>to_delete_1</code>队列继续进行对象的析构,在析构期间有大量对象被添加到<code>to_delete_2</code>队列中,但是没有唤醒<code>clearDeferredDeleteList</code>任务。</li>
<li>to_delete_1对象析构完毕</li>
<li>再次执行<code>clearDeferredDeleteList</code>对<code>to_delete_2</code>中对象进行析构。</li>
<li>如此反复便可以高效的在两个队列之间来回切换进行对象的析构。</li>
</ul>
<p> 虽然分析完了整个<code>deferredDelete</code>的过程,但是我们还没有回答本节一开始提到的如何安全的进行对象析构的问题。让我们先来看一下<code>deferredDelete</code>的应用场景,看看“为何要进行延迟析构?” 以及<code>deferredDelete</code>是如何解决对象安全析构的问题。在Envoy的源代码中经常会看到像下面这样的代码片段。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#000">ConnectionImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">ConnectionImpl</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">ConnectionSocketPtr</span><span style="color:#ce5c00;font-weight:bold">&amp;&amp;</span> <span style="color:#000">socket</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">TransportSocketPtr</span><span style="color:#ce5c00;font-weight:bold">&amp;&amp;</span> <span style="color:#000">transport_socket</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#204a87;font-weight:bold">bool</span> <span style="color:#000">connected</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000;font-weight:bold">......</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#8f5902;font-style:italic">// 传递裸指针到回调中
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">file_event_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">dispatcher_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">createFileEvent</span><span style="color:#000;font-weight:bold">(</span>
<span style="color:#8f5902;font-style:italic">// 这里将this裸指针传递给了内部的callback
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// callback内部通过this指针访问onFileEvent方法,如何保证
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// callback执行的时候,this指针是有效的呢?
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">fd</span><span style="color:#000;font-weight:bold">(),</span> <span style="color:#000;font-weight:bold">[</span><span style="color:#204a87;font-weight:bold">this</span><span style="color:#000;font-weight:bold">](</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">events</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">-&gt;</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">{</span> <span style="color:#000">onFileEvent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">events</span><span style="color:#000;font-weight:bold">);</span> <span style="color:#000;font-weight:bold">},</span>
<span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">FileTriggerType</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Edge</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">FileReadyType</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Read</span> <span style="color:#ce5c00;font-weight:bold">|</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">FileReadyType</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Write</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">......</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 传递给<code>Dispatcher</code>的<code>callback</code>都是通过裸指针的方式进行回调,如果进行回调的时候对象已经析构了,就会出现野指针的问题,我相信学过C++的同学都会看出这个问题,除非能在逻辑上保证<code>Dispatcher</code>的生命周期比所有对象都短,这样就能保证在回调的时候对象肯定不会析构,但是这不可能成立的,因为<code>Dispatcher</code>是<code>EventLoop</code>的核心。一个线程运行一个<code>EventLoop</code>直到线程结束,<code>Dispatcher</code>对象才会析构,这意味着<code>Dispatcher</code>对象的生命周期是最长的。所以从逻辑上没办法保证进行回调的时候对象没有析构。可能有人会有疑问,对象在析构的时候把注册的事件(<code>file_event_</code>)取消不就可以避免野指针的问题吗? 那如果事件已经触发了,<code>callback</code>正在等待运行? 又或者<code>callback</code>运行了一半呢?前者libevent是可以保证的,在调用<code>event_del</code>删除事件的时候可以把处于等待运行的事件callback取消掉,但是后者就无能为力了,这个时候如果对象析构了,那行为就是未定义了。沿着这个思路想一想,是不是只要保证对象析构的时候没有<code>callback</code>正在运行就可以解决问题了呢?是的,只要保证所有在执行中的<code>callback</code>执行完了,再做对象析构就可以了。可以利用<code>Dispatcher</code>是顺序执行所有<code>callback</code>的特点,向<code>Dispatcher</code>中插入一个任务就是用来对象析构的,那么当这个任务执行的时候是可以保证没有其他任何<code>callback</code>在运行。通过这个方法就完美解决了这里遇到的野指针问题了。或许有人又会想,这里是不是可以用<a href="https://en.cppreference.com/w/cpp/memory/shared_ptr">shared_ptr</a>和<a href="https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this">shared_from_this</a>来解这个呢? 是的,这是解决多线程环境下对象析构的秘密武器,通过延长对象的生命周期,把对象的生命周期延长到和<code>callback</code>一样,等<code>callback</code>执行完再进行析构,同样可以达到效果,但是这带来了两个问题,第一就是对象生命周期被无限拉长,虽然延迟析构也拉长了生命周期,但是时间是可预期的,一旦<code>EventLoop</code>执行了<code>clearDeferredDeleteList</code>任务就会立刻被回收,而通过<code>shared_ptr</code>的方式其生命周期取决于<code>callback</code>何时运行,而<code>callback</code>何时运行这个是没办法保证的,比如一个等待<code>socket</code>的可读事件进行回调,如果对端一直不发送数据,那么<code>callback</code>就一直不会被运行,对象就一直无法被析构,长时间累积会导致内存使用率上涨。第二就是在使用方式上侵入性较强,需要强制使用<code>shared_ptr</code>的方式创建对象。</p></description></item><item><title>Blog: Envoy源码分析之ThreadLocal机制</title><link>/blog/2020/08/23/envoy%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bthreadlocal%E6%9C%BA%E5%88%B6/</link><pubDate>Sun, 23 Aug 2020 00:00:00 +0000</pubDate><guid>/blog/2020/08/23/envoy%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bthreadlocal%E6%9C%BA%E5%88%B6/</guid><description>
<h1 id="threadlocal机制">ThreadLocal机制</h1>
<p> Envoy中的<code>ThreadLocal</code>机制其实就是我们经常说的线程本地存储简称TLS(Thread Local Storage),顾名思义通过TLS定义的变量会在每一个线程专有的存储区域存储一份,访问TLS的时候,其实访问的是当前线程占有存储区域中的副本,因此可以使得线程可以无锁的并发访问同一个变量。Linux上一般有三种方式来定义一个TLS变量。</p>
<ul>
<li>gcc对C语言的扩展<code>__thread</code></li>
<li>pthread库提供的<code>pthread_key_create</code></li>
<li>C++11的<code>std::thread_local</code>关键字</li>
</ul>
<p> Envoy的<code>ThreadLocal</code>机制就是在C++11的<code>std::thread_local</code>基础上进行了封装用于实现线程间的数据共享。Envoy因其配置的动态生效而出名,而配置动态生效的基石就是<code>ThreadLocal</code>机制,通过<code>ThreadLocal</code>机制将配置可以无锁的在多个线程之间共享,当配置发生变更的时候,通过主线程将更新后的配置Post到各个线程中,交由各个线程来更新自己的<code>ThreadLocal</code>。</p>
<h1 id="threadlocalobject">ThreadLocalObject</h1>
<p> Envoy要求所有的<code>ThreadLocal</code>数据对象都要继承<code>ThreadLocalObject</code>,比如下面这个<code>ThreadLocal</code>对象。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">struct</span> <span style="color:#000">ThreadLocalCachedDate</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">ThreadLocal</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">ThreadLocalObject</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ThreadLocalCachedDate</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">const</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">string</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">date_string</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000">date_string_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">date_string</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{}</span>
<span style="color:#204a87;font-weight:bold">const</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">string</span> <span style="color:#000">date_string_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p> 但实际上<code>ThreadLocalObject</code>只是一个空的接口类,所以并非我们继承了<code>ThreadLocalObject</code>就是一个TLS了。继承<code>ThreadLocalObject</code>目的是为了可以统一对所有要进行TLS的对象进行管理。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">ThreadLocalObject</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#ce5c00;font-weight:bold">~</span><span style="color:#000">ThreadLocalObject</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#204a87;font-weight:bold">default</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
<span style="color:#204a87;font-weight:bold">using</span> <span style="color:#000">ThreadLocalObjectSharedPtr</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">shared_ptr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">ThreadLocalObject</span><span style="color:#ce5c00;font-weight:bold">&gt;</span><span style="color:#000;font-weight:bold">;</span>
</code></pre></div><p> Envoy中需要TLS的数据有很多,最重要的当属配置,随着配置的增多,这类数据所占据的内存也会变得很大,如果每一种配置都声明为TLS会导致不少内存浪费。为此Envoy通过<code>ThreadLocalData</code>将所有要进行TLS的对象都管理起来,然后将<code>ThreadLocalData</code>本身设置为TLS,通过TLS中保存的指针来访问对应的数据。这样就可以避免直接在TLS中保存数据而带来内存上的浪费,只需要保存指向数据的指针即可,相关代码如下。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">struct</span> <span style="color:#000">ThreadLocalData</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">// 指向当前线程的Dispatcher对象
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">*</span> <span style="color:#000">dispatcher_</span><span style="color:#000;font-weight:bold">{};</span>
<span style="color:#8f5902;font-style:italic">// 保存了所有要TLS的数据对象的智能指针,通过智能指针来访问真正的数据对象
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">ThreadLocalObjectSharedPtr</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">data_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p><img src="https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/a4712346b33dc89bc09bb5c41332c5ba.jpg" alt="4-2.jpg"></p>
<pre><code>如上图所示,每一个TLS通过指针指向实际的对象,每一个数据对象只在内存中保存一份,避免内存上的浪费,但是这样带来问题就是如何做到线程安全的访问数据对象呢? 当我们要访问数据对象的时候,如果此时正在对数据对象进行更新,这个时候就会存在一个线程安全的问题了。Envoy巧妙的通过在数据对象更新的时候,先构造出一个新的数据对象,然后将TLS中的数据对象指针指向新的数据对象来实现线程安全的访问。本质上和COW(copy-on-write)很类似,但是存在两点区别。
</code></pre>
<ul>
<li>COW中是先拷贝原来的对象,然后更改对象,而Envoy在这里是重新构建一个新的数据对象</li>
<li>COW中无论是读还是写,在更改<code>shared_ptr</code>指向时,都需要加锁,因为<code>shared_ptr</code>本身的读写时非线程安全的,而Envoy不需要加锁。</li>
</ul>
<p> Envoy中指向数据对象的<code>shared_ptr</code>并非只有一个,而是每一个线程都有一个<code>shared_ptr</code>指向数据对象,更改<code>shared_ptr</code>指向新的数据对象时通过post一个任务到对应线程中,然后在同一个线程使<code>shared_ptr</code>指向新的数据对象,因此并没有多线程操作<code>shared_ptr</code>,所以没有线程安全问题,自然也不用加锁,这是Envoy实现比较巧妙的地方。</p>
<p><img src="https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/b1b456618697f8df6c2a0e81bba4a907.jpg" alt="4-3.jpg"></p>
<p> 如上图所示,T1时刻,Thread1通过TLS对象访问<code>ThreadLocalObjectOld</code>,在T2时刻在main线程发现配置发生了变化,重新构造了一个新的<code>ThreadlocalObjectNew</code>对象,然后通过Thread1的<code>Dispatcher</code>对象post了一个任务到Thread1线程,到了T3时刻这个任务开始执行,将对应的指针指向了 <code>ThreadLocalObjectNew</code>,最后在T4时刻再次访问配置的时候,就已经访问的是最新的配置了。到此为止就完成了一次配置更新,而且整个过程是线程安全的。</p>
<h1 id="threadlocal">ThreadLocal</h1>
<p> 终于到了分析真正的ThreadLocal对象的时候,它的功能其实很简单,大部分的能力都是依赖<code>Dispatcher</code>、还有上文中提到的<code>SlotImpl</code>、<code>ThreadLocalData</code>等,<code>Instance</code>是它的接口类,它继承了<code>SlotAllocator</code>接口,也包含了上文中分析的<code>allocateSlot</code>方法。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">Instance</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">SlotAllocator</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#8f5902;font-style:italic">// 每启动一个worker线程就需要通过这个方法进行注册
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">registerThread</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">bool</span> <span style="color:#000">main_thread</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 主线程在退出的时候调用,用于标记shutdown状态
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">shutdownGlobalThreading</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#8f5902;font-style:italic">// 每一个worker线程需要调用这个方法来释放自己的TLS
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">shutdownThread</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#204a87;font-weight:bold">virtual</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000">PURE</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p> 对应的实现是<code>InstanceImpl</code>对象,在<code>Instance</code> 的基础上又扩展了一些post任务到所有线程的一些方法。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp">
<span style="color:#204a87;font-weight:bold">class</span> <span style="color:#000">InstanceImpl</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">Instance</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#204a87;font-weight:bold">public</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#000;font-weight:bold">....</span>
<span style="color:#204a87;font-weight:bold">private</span><span style="color:#ce5c00;font-weight:bold">:</span>
<span style="color:#8f5902;font-style:italic">// post任务到所有注册的线程中
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">runOnAllThreads</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// post任务到所有注册的线程中,完成后通过main_callback进行通知
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">runOnAllThreads</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span> <span style="color:#000">main_callback</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// 初始化TLS指向对应的数据对象指针
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#204a87;font-weight:bold">static</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">setThreadLocal</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">uint32_t</span> <span style="color:#000">index</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">ThreadLocalObjectSharedPtr</span> <span style="color:#000">object</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">.....</span>
<span style="color:#8f5902;font-style:italic">// 保存所有注册的线程
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">list</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">reference_wrapper</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&gt;&gt;</span> <span style="color:#000">registered_threads_</span><span style="color:#000;font-weight:bold">;</span>
</code></pre></div><p> 因为所有的线程都会注册都<code>InstanceImpl</code>中,所以只需要遍历所有的线程所对应的<code>Dispatcher</code> 对象,调用其post方法将任务投递到对应线程即可,但是如何做到等所有任务执行完成后进行通知呢 ?</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">InstanceImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">runOnAllThreads</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span> <span style="color:#000">all_threads_complete_cb</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">this_thread</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">get_id</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#000">main_thread_id_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">!</span><span style="color:#000">shutdown_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#8f5902;font-style:italic">// 首先在主线程执行任务
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#8f5902;font-style:italic">// 利用了shared_ptr自定义析构函数,在析构的时候向主线程post一个完成的通知任务
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// 这个机制和Bookkeeper的实现机制是一样的。
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">shared_ptr</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span><span style="color:#ce5c00;font-weight:bold">&gt;</span> <span style="color:#000">cb_guard</span><span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">new</span> <span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">cb</span><span style="color:#000;font-weight:bold">),</span>
<span style="color:#000;font-weight:bold">[</span><span style="color:#204a87;font-weight:bold">this</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#000">all_threads_complete_cb</span><span style="color:#000;font-weight:bold">](</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">PostCb</span><span style="color:#ce5c00;font-weight:bold">*</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">main_thread_dispatcher_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">post</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">all_threads_complete_cb</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#204a87;font-weight:bold">delete</span> <span style="color:#000">cb</span><span style="color:#000;font-weight:bold">;</span> <span style="color:#000;font-weight:bold">});</span>
<span style="color:#204a87;font-weight:bold">for</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#f57900">dispatcher</span> <span style="color:#000;font-weight:bold">:</span> <span style="color:#000">registered_threads_</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">post</span><span style="color:#000;font-weight:bold">([</span><span style="color:#000">cb_guard</span><span style="color:#000;font-weight:bold">]()</span> <span style="color:#ce5c00;font-weight:bold">-&gt;</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">{</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">*</span><span style="color:#000">cb_guard</span><span style="color:#000;font-weight:bold">)();</span> <span style="color:#000;font-weight:bold">});</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 通过上面的代码可以看到,这里仍然利用到了<code>shared_ptr</code>的引用计数机制来实现的。每一个post到其他线程的任务都会导致<code>cb_guard</code>引用计数加1,post任务执行完成后<code>cb_guard</code>引用计数减1,等全部任务完成后,<code>cb_guard</code> 的引用计数就变成0了,这个时候就会执行自定义的删除器,在删除器中就会post一个任务到主线程中,从而实现了任务执行完成的通知回调机制。</p>
<p> 接下来我们来分析下<code>shutdownGlobalThreading</code>,这个函数是用于设置flag来表示正在关闭TLS,必须由主线程在其它worker线程退出之前来调用,调用完成后每一个worker线程还需要调用对应TLS的<code>shutdownThread</code>来清理TLS中的对象,到此为止才完成了全部的TLS清理工作。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c++" data-lang="c++"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">InstanceImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">shutdownGlobalThreading</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">this_thread</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">get_id</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#ce5c00;font-weight:bold">==</span> <span style="color:#000">main_thread_id_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">!</span><span style="color:#000">shutdown_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000">shutdown_</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#204a87">true</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p>上面的代码是<code>shutdownGlobalThreading</code>的实现,可以看到仅仅是设置了一个<code>shutdown_</code>的标志。</p>
<p> 最后来分析一下<code>shutdownThread</code>,每一个work线程在退出事都需要调用这个函数,这个函数会将存储的所有线程存储的对象进行清除。每一个worker线程都持有<code>InstanceImpl</code>实例的引用,在析构的时候会调用<code>shutdownThread</code>来释放自己线程的TLS内容,这个函数的实现如下:</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000">InstanceImpl</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">shutdownThread</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">ASSERT</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">shutdown_</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#204a87;font-weight:bold">for</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#204a87;font-weight:bold">auto</span> <span style="color:#000">it</span> <span style="color:#ce5c00;font-weight:bold">=</span> <span style="color:#000">thread_local_data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">rbegin</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000">it</span> <span style="color:#ce5c00;font-weight:bold">!=</span> <span style="color:#000">thread_local_data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">rend</span><span style="color:#000;font-weight:bold">();</span> <span style="color:#ce5c00;font-weight:bold">++</span><span style="color:#000">it</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">it</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">reset</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000">thread_local_data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">data_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">clear</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 比较奇怪的点在于这里是逆序遍历所有的<code>ThreadLocalObject</code>对象来进行reset的,这是因为一些&quot;持久&rdquo;(活的比较长)的对象如<code>ClusterManagerImpl</code>很早就会创建<code>ThreadLocalObject</code>对象,但是直到shutdown的时候也不析构,而在此基础上依赖<code>ClusterManagerImpl</code>的对象的如<code>GrpcClientImpl</code>等,则是后创建<code>ThreadLocalObject</code>对象,如果<code>ClusterManagerImpl</code>创建的<code>ThreadLocalObject</code>对象先析构,而<code>GrpcClientImpl</code>相关的<code>ThreadLocalObject</code>对象依赖了<code>ClusterManagerImpl</code>相关的TLS内容,那么后析构就会导致未定义的问题。为此这里选择逆序来进行<code>reset</code>,先从一个高层的对象开始,最后才开始对一些基础的对象所关联的<code>ThreadLocalObject</code>进行<code>reset</code>。例如下面这个例子:</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#204a87;font-weight:bold">struct</span> <span style="color:#000">ThreadLocalPool</span> <span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#204a87;font-weight:bold">public</span> <span style="color:#000">ThreadLocal</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">ThreadLocalObject</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000;font-weight:bold">.....</span>
<span style="color:#000">InstanceImpl</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">parent_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000">Upstream</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">ThreadLocalCluster</span><span style="color:#ce5c00;font-weight:bold">*</span> <span style="color:#000">cluster_</span><span style="color:#000;font-weight:bold">;</span>
<span style="color:#000;font-weight:bold">.....</span>
<span style="color:#000;font-weight:bold">};</span>
</code></pre></div><p> <code>redis_proxy</code>中定义了一个<code>ThreadLocalPool</code>,这个<code>ThreadLocalPool</code>又依赖较为基础的<code>ThreadLocalCluster</code>(是<code>ThreadLocalClusterManagerImpl</code>的数据成员,也就是<code>ClusterManagerImpl</code>所对应的<code>ThreadLocalObject</code>对象),如果<code>shutdownThread</code>按照顺序的方式析构的话,那么<code>ThreadLocalPool</code>中使用的<code>ThreadLocalCluster</code>会先被析构,然后才是<code>ThreadLocalPool</code>的析构,而<code>ThreadLocalPool</code>析构的时候又会使用到<code>ThreadLocalCluster</code>,但是<code>ThreadLocalCluster</code>已经析构了,这个时候就会出现野指针的问题了。</p>
<div class="highlight"><pre style="background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-cpp" data-lang="cpp"><span style="color:#000">ThreadLocalPool</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">ThreadLocalPool</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">InstanceImpl</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">parent</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#000">Event</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">Dispatcher</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">const</span>
<span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">string</span><span style="color:#ce5c00;font-weight:bold">&amp;</span> <span style="color:#000">cluster_name</span><span style="color:#000;font-weight:bold">)</span>
<span style="color:#ce5c00;font-weight:bold">:</span> <span style="color:#000">parent_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">parent</span><span style="color:#000;font-weight:bold">),</span> <span style="color:#000">dispatcher_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">dispatcher</span><span style="color:#000;font-weight:bold">),</span>
<span style="color:#000">cluster_</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">parent_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">cm_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">get</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">cluster_name</span><span style="color:#000;font-weight:bold">))</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000;font-weight:bold">.....</span>
<span style="color:#000">local_host_set_member_update_cb_handle_</span> <span style="color:#ce5c00;font-weight:bold">=</span>
<span style="color:#000">cluster_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">prioritySet</span><span style="color:#000;font-weight:bold">().</span><span style="color:#000">addMemberUpdateCb</span><span style="color:#000;font-weight:bold">(</span>
<span style="color:#000;font-weight:bold">[</span><span style="color:#204a87;font-weight:bold">this</span><span style="color:#000;font-weight:bold">](</span><span style="color:#204a87;font-weight:bold">uint32_t</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#204a87;font-weight:bold">const</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">Upstream</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">HostSharedPtr</span><span style="color:#ce5c00;font-weight:bold">&gt;&amp;</span><span style="color:#000;font-weight:bold">,</span>
<span style="color:#204a87;font-weight:bold">const</span> <span style="color:#000">std</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">vector</span><span style="color:#ce5c00;font-weight:bold">&lt;</span><span style="color:#000">Upstream</span><span style="color:#ce5c00;font-weight:bold">::</span><span style="color:#000">HostSharedPtr</span><span style="color:#ce5c00;font-weight:bold">&gt;&amp;</span> <span style="color:#000">hosts_removed</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#ce5c00;font-weight:bold">-&gt;</span> <span style="color:#204a87;font-weight:bold">void</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">onHostsRemoved</span><span style="color:#000;font-weight:bold">(</span><span style="color:#000">hosts_removed</span><span style="color:#000;font-weight:bold">);</span>
<span style="color:#000;font-weight:bold">});</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000">ThreadLocalPool</span><span style="color:#ce5c00;font-weight:bold">::~</span><span style="color:#000">ThreadLocalPool</span><span style="color:#000;font-weight:bold">()</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#8f5902;font-style:italic">// local_host_set_member_update_cb_handle_是ThreadLocalCluster的一部分
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#8f5902;font-style:italic">// ThreadLocalCluster析构会导致local_host_set_member_update_cb_handle_变成野指针
</span><span style="color:#8f5902;font-style:italic"></span> <span style="color:#000">local_host_set_member_update_cb_handle_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">remove</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#204a87;font-weight:bold">while</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#ce5c00;font-weight:bold">!</span><span style="color:#000">client_map_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">empty</span><span style="color:#000;font-weight:bold">())</span> <span style="color:#000;font-weight:bold">{</span>
<span style="color:#000">client_map_</span><span style="color:#000;font-weight:bold">.</span><span style="color:#000">begin</span><span style="color:#000;font-weight:bold">()</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">second</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">redis_client_</span><span style="color:#ce5c00;font-weight:bold">-&gt;</span><span style="color:#000">close</span><span style="color:#000;font-weight:bold">();</span>
<span style="color:#000;font-weight:bold">}</span>
<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p> 到此为止关于Envoy中的TLS实现就全部分析完毕了。</p>
<h1 id="小结">小结</h1>
<p> 通过本节的分析相信我们应该足以驾驭Envoy中的<code>ThreadLocal</code>,从其设计可以看出它的一些其巧妙之处,比如抽象出一个<code>Slot</code>和对应的线程存储进行了关联,<code>Slot</code>可以任意传递,因为不包含实际的数据,拷贝的开销很低,只包含了一个索引值,具体关联的线程存储数据是不知道的,避免直接暴露给用户背后的数据。而<code>InstanceImpl</code>对象则管理着所有<code>Slot</code>的分配和移除以及整个<code>ThreadLocal</code>对象的<code>shutdown</code>。还有引入的Bookkeeper机制也甚是巧妙,和<a href="https://envoyproxy-cn.github.io/blog/2020/08/23/envoy%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bdispatcher%E6%9C%BA%E5%88%B6/">Envoy源码分析之Dispatcher机制</a>一文中的<code>DeferredDeletable</code>机制有着异曲同工之妙,通过这个机制可以做到安全的析构<code>SlotImpl</code>对象</p></description></item><item><title>Blog: 如何为 Envoy 构建一个控制面来管理集群网络流量</title><link>/blog/2020/05/10/%E5%A6%82%E4%BD%95%E4%B8%BA-envoy-%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%8E%A7%E5%88%B6%E9%9D%A2%E6%9D%A5%E7%AE%A1%E7%90%86%E9%9B%86%E7%BE%A4%E7%BD%91%E7%BB%9C%E6%B5%81%E9%87%8F/</link><pubDate>Sun, 10 May 2020 08:45:20 +0800</pubDate><guid>/blog/2020/05/10/%E5%A6%82%E4%BD%95%E4%B8%BA-envoy-%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%8E%A7%E5%88%B6%E9%9D%A2%E6%9D%A5%E7%AE%A1%E7%90%86%E9%9B%86%E7%BE%A4%E7%BD%91%E7%BB%9C%E6%B5%81%E9%87%8F/</guid><description>
<h2 id="前言">前言</h2>
<p>这篇文章我看了之后非常想翻译,为什么呢?一方面我也在学习 Envoy,并且在公司的实际项目中使用 Envoy,另一方面,我确实也在设计一个控制管理端来统一管控多个集群的所有流量,没错我说的是所有的流量管控。目前这个管理系统在内部已经在逐步使用起来了。所以翻译这篇文章,即学习 Envoy 技术,也是想做一个参考,印证我的想法是不是 OK 的,取长补短。</p>
<h2 id="指导在服务边缘构建控制面来管理-envoy-proxy让它作为服务网关或者在服务网格中使用">指导在服务边缘构建控制面来管理 Envoy Proxy,让它作为服务网关或者在服务网格中使用</h2>
<p>Envoy 已经成为了一个非常流行的网络组件了。Matt Klein <a href="https://blog.envoyproxy.io/the-universal-data-plane-api-d15cec7a">几年前写过一篇博文</a>,就在讨论 Envoy 的动态配置 API 和它如何成为 Envoy 被采用越来越多的原因之一。他在博文中说这是“统一数据面板 API”(UDPA)。随着很多其它项目都采用 Envoy 作为其核心组件,可以毫不夸张的说 Envoy 不仅仅建立了标准 API,而且对于应用 7 层的网络解决方案来说:“Envoy 已经变成了在云原生架构下的统一数据平面”。</p>
<p><img src="imgs/envoy.png" alt=""></p>
<p>而且,由于 Envoy 的统一数据平面 API,我们可以看到业界开发了很多针对基于 Envoy 技术设施进行配置管理的管理系统。本文将会深入讨论为 Envoy 构建一个控制平面需要什么,大家可以通过这些信息来评估什么样的基础设施最适合你的组织和场景。因为这个是一个很大的话题,作者会出一个系列文章来对此进行详细说明(后面我也会挑一些我感兴趣的文章进行翻译学习)。</p>
<p>在 <a href="https://blog.envoyproxy.io/envoycon-recap-579d53576511">EnvoyCon/KubeCon 论坛有很多非常好的讨论</a>,这里好多组织都分享了他们采用 Envoy 的经验,也包括了如何构建他们自己的控制平面。下面是一些他们为什么选择自建控制平面的原因:</p>
<ol>
<li>现有的解决方案构建在不同的数据平面上,而且已经有了控制平面,需要在这里兼容 Envoy。</li>
<li>为不包含任何开源基础设施来构建,或者使用其它的 Envoy 控制平面(比如:VMs, AWS,ECS 等)。</li>
<li>不需要使用所有 Envoy 的特性,只是需要一部分。</li>
<li>为了更好适配自己的工作流和工作视图而需要为 Envoy 配置开发专属领域的 API 对象模型。</li>
<li>要线上使用,但是发现其它的控制平面并不够成熟。</li>
</ol>
<p><img src="imgs/control-plane-data-plane.png" alt=""></p>
<p>然而,仅仅因为有些早期使用者构建了他们自己的控制平面,这并不意味着你也应该做这样的事情。首先在去年中很多为 Envoy 开发的控制平面已经相当成熟了,所以你应该在决定要重新开发另外一个控制平面之前先来研究一下这些已经存在的。其次,正如 Datawire 的人们发现,并且 Daniel Bryant 最近也发文章说,为 Envoy 构建一个控制平面并不是那么容易的。</p>
<p>我参与开发几个为 Enovy 构建控制平面的开源项目。比如,Gloo 是一个功能性网关,它可以作为强大的 Kubernetes 接入服务,API 网关,或者作为从单体服务到微服务过度的功能网关。Gloo 有一个针对 Envoy 的控制平面,它可以作为我这个系列文章的例子,来说明如何在控制平面上按照需求来抽象设计,以实现插件管理和扩展性管理。其它可以参考的已经实现的控制平面如 istio 和 <a href="https://github.com/heptio/contour">Heptio Contour</a>,这些也是贯穿我这个系列文章中的好例子。如果你确定要自己开发控制平面,那么除了这些,你还可以参考其它一些已经存在的控制平面。</p>
<p><img src="imgs/envoyprojects.png" alt=""></p>
<p>在这个系列文章中,我们将会关注以下一些关键点:</p>
<ol>
<li>采用一种机制可以动态更新 Envoy 的路由,服务发现和其它配置。</li>
<li>识别使用哪些组件来构成你的控制平面,包括了后端存储,服务发现 API,安全组件等等。</li>
<li>根据场景和团队组织以最合适的方式建立任意制定区域的配置对象和 API。</li>
<li>思考如何在需要的地方以最好方式嵌入控制平面。</li>
<li>部署各种控制平面组件的方式。</li>
<li>思考如何测试控制平面。</li>
</ol>
<p>要开始这一系列的讨论,我们首先来看看如何在 Envoy 运行时,使用 Envoy 的动态配置 API 来更新 Envoy,以处理拓扑和部署中的变更。</p>
<h2 id="使用-envoy-的-xds-api-动态配置-envoy">使用 Envoy 的 xDS API 动态配置 Envoy</h2>
<p>在 Envoy 之上构建构控制平面的主要方便支持处在于它的数据平面 API。有了数据平面 API,我们可就可以动态的配置 Envoy 的大多数重要运行时设置。通过 xDS API 进行的 Envoy 配置是被设计为最终一致性的,没有一种方法可以对集群中的所有代理进行原子性的更新。当控制平面上有配置更新时,它就通过 xDS API 让数据平面代理都可以获取到,每个代理都是相互独立的来获取应用这些配置。</p>
<p>下面是我们可以通过 xDS 动态配置 Envoy 的部分运行时模型:</p>
<ol>
<li><a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/configuration/listeners/lds#config-listeners-lds">监听发现服务(LDS)API</a> - LDS 用于下发服务监听的端口。</li>
<li><a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/eds.proto#envoy-api-file-envoy-api-v2-eds-proto">终端发现服务(EDS)API</a>- EDS 用户服务发现。</li>
<li><a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/configuration/http_conn_man/rds#config-http-conn-man-rds">路由发现服务(RDS)API</a>- RDS 用于流量路由决策。</li>
<li><a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/configuration/cluster_manager/cds#config-cluster-manager-cds">集群发现服务(CDS)</a>- CDS 用于可以路由流量过去的后端服务。</li>
<li><a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/configuration/secret">密钥发现服务(SDS)</a> - SDS 用户分发密钥(证书和密钥)。</li>
</ol>
<p><img src="imgs/xds-control-plane.png" alt=""></p>
<p>这些 API 使用 proto3 的 Protocol Buffer 来定义的,并且已经有一些相关实现了,可以提供大家在构建自己的控制平面时参考:</p>
<ol>
<li><a href="https://github.com/envoyproxy/go-control-plane">go-control-plane</a></li>
<li><a href="https://github.com/envoyproxy/java-control-plane">java-control-plane</a></li>
</ol>
<p>虽然每个 xDS(LDS/EDS/RDS/CDS/SDS,这些统称xDS)都是可以动态可配置的,但是这并不意味着你必须动态配置所有内容。你可以组合适应,区分静态配置和动态配置。例如,要通过配置实现一种类型的服务发现:希望终端是动态的,但是集群在部署的时候就是已经知道路由信息了,所以你可以使用 Envoy 中的 <a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/eds.proto#envoy-api-file-envoy-api-v2-eds-proto">Endpoint Discovery Service</a> 来静态的定义集群的配置。如果在部署的时候你不确定是那个上游集群,那你可以使用<a href="https://www.envoyproxy.io/docs/envoy/v1.9.0/configuration/cluster_manager/cds#config-cluster-manager-cds">Cluster Discovery Service</a>来动态的配置发现上游。关键是你可以构建一个工作流和处理流程来静态的配置你需要的部分,而且可以使用动态 xDS 服务在运行时发现你需要的部分。为什么有不同的控制平面实现,其中一个原因就是并不是所有人都有一个完全动态和可替代的环境(这个环境下所有的配置都应该是动态的),这点几乎不可能。根据现有条件的约束和可用工作流,要为你的系统采取合适级别的动态配置,而不是全动态配置。</p>
<p>在 Gloo 的实现中,我们基于 go-control-plane 的实现来构建控制平面,实现了 xDS API 到 Envoy 的动态配置。Istio 和 Heptio Contour 也是使用这种方式。这个控制平面的 API 使用 gRPC streaming 实现,并且留了实现接口,所以我们在实现的时候只需要实现这些接口就可以了。这种方式可以以非常高效的方式把 Envoy 数据平面 API 集成到控制平面中。</p>
<p>gRPC streaming 方式并不是唯一的更新 Envoy 配置的方法。在<a href="https://www.envoyproxy.io/docs/envoy/v1.5.0/api-v1/api">Envoy 早期版本中的 xDS API</a>,轮询是唯一检测是否有新配置可用的方式。虽然这也是接受的,并且也符合配置更新最终一致性的原则,但是在网络和计算使用上还是不够高效。也比较困难去调整优化轮询配置以减少资源浪费。</p>
<p>最后,一些 Envoy 管理系统的实现采取生成<a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/v2_overview#static">静态 Envoy 配置文件</a>和给 Envoy 周期性的覆盖写入磁盘上的配置文件,再执行<a href="https://blog.envoyproxy.io/envoy-hot-restart-1d16b14555b5">Envoy 进程的热重启</a>。在高度动态环境中(像 Kubernetes,实际上任何短暂的计算平台都算),管理这种文件的生成,传递,热重启等等会显得非常笨重。Envoy 最初就是在这样操作的(Lyft公司创建这个项目是就是这样),但是它逐步发展到现在的 xDS API了。</p>
<h2 id="总结">总结</h2>
<p>Gloo 团队相信使用 gRPC streaming 和 xDS API 来实现对 Envoy 的动态配置和控制是一种比较好的方式。同样,并不是所有的 Envoy 配置都应该是动态的,尤其是你不需要动态配置的内容。但是如果你是在一个高度动态的环境(比如在 Kubernetes 中),动态配置 Envoy 就很关键了。其它的环境或许也有这样的需要。不管怎么样,动态配置使用 gRPC streaming API 是最理想的,主要有以下一些好处:</p>
<ol>
<li>事件驱动配置更新;在控制平面中配置会在可用的时候下发到 Envoy。</li>
<li>不需要轮询配置变化了。</li>
<li>不需要热重启 Envoy。</li>
<li>不会中断流量。</li>
</ol>
<h2 id="下一步">下一步</h2>
<p>这是系列文章的第一部分,我们只是建立了为 Envoy 构建控制平面的基本概念,简述了 xDS API 和对 Envoy 动态配置不同的考虑。在下一节,会在几天后发布,将会把控制面分解成为可部署的组件,确定你需要的组件,特定领域对象会是什么样子?以及对控制平面扩展插件的思考。</p>
<p>英文原文: <a href="https://blog.christianposta.com/envoy/guidance-for-building-a-control-plane-to-manage-envoy-proxy-based-infrastructure/">https://blog.christianposta.com/envoy/guidance-for-building-a-control-plane-to-manage-envoy-proxy-based-infrastructure/</a></p>
<center>
看完本文有收获?请分享给更多人
<p>关注「黑光技术」,关注大数据+微服务</p>
<p><img src="/img/qrcode_helight_tech.jpg" alt=""></p>
</center></description></item></channel></rss>