-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
512 lines (257 loc) · 233 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Qian0817</title>
<link href="http://qian0817.top/atom.xml" rel="self"/>
<link href="http://qian0817.top/"/>
<updated>2022-04-06T13:33:42.381Z</updated>
<id>http://qian0817.top/</id>
<author>
<name>qianlei</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>HTTP2协议</title>
<link href="http://qian0817.top/2021/03/15/HTTP2%E5%8D%8F%E8%AE%AE/"/>
<id>http://qian0817.top/2021/03/15/HTTP2%E5%8D%8F%E8%AE%AE/</id>
<published>2021-03-15T06:47:29.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>HTTP2是HTTP协议的一个重大的修订版本,用于解决随着Web发展HTTP1.1逐渐无法满足需求对开发者和用户造成负担的问题。</p><p>在HTTP1.1中,客户端需要多个连接才能实现并发和缩短延迟,同时HTTP1.1不会压缩请求和响应标头,导致不必要的网络流量以及等等问题。</p><h2 id="二进制分帧层"><a href="#二进制分帧层" class="headerlink" title="二进制分帧层"></a>二进制分帧层</h2><p>HTTP2大致可以分为两个部分:</p><ul><li><p>分帧层:HTTP2实现多路复用能力的核心部分</p></li><li><p>数据层:也就是HTTP及其关联数据的部分</p></li></ul><p>分帧层是HTTP2所有性能增强的核心,定义了如何封装HTTP消息并在客户端和服务端之间进行传输。</p><p><img data-src="%E5%88%86%E5%B8%A7%E5%B1%82.svg" alt="HTTP/2 二进制分帧层"></p><p>分帧层不影响HTTP的语义,不同的是传输期间的修改方式进行了变化。HTTP1.1以换行符(\r\n)作为换行符,解析速度慢且容易出错,在解析时需要不断地读入字节直到遇到分隔符为止。HTTP2将所有传输的信息分割为更小的消息和帧,并采用二进制格式进行编码,虽然肉眼识别起来比较困难,但是方便了机器解析。HTTP2中帧的结构如下所示:</p><p><img data-src="%E5%B8%A7.png" alt="image-20210315135413353"></p><p>这样一来HTTP2解析的实现和维护都可以更加简单。同时相比于HTTP1.1需要发送完上一个请求或响应才能发送下一个,HTTP2的请求和响应可以交错甚至多路复用。</p><h2 id="数据流"><a href="#数据流" class="headerlink" title="数据流"></a>数据流</h2><p>在HTTP2中所有的通信都在一个TCP连接上完成,该连接承载任意数量的双向数据流。流指的是HTTP2上独立的双向的帧序列交换,可以将流看作是连接上的一系列帧,可以承载一条或者多条的消息、每条消息都是一条逻辑的HTTP消息,包含一个或者多个帧。帧是最小的通信单位,承载特定类型的数据,例如HTTP标头和消息负载等等。</p><h3 id="流量控制"><a href="#流量控制" class="headerlink" title="流量控制"></a>流量控制</h3><p>流量控制可以阻止发送方向接收方大量的发送数据,以免超出接收方的需求和处理能力。HTTP2提供了WINDOW_UPDATE帧来指示更新后的处理字节能力,这个帧可以告诉对方更新后的处理字节能力。默认的流量控制窗口为65535。</p><h3 id="数据流优先级"><a href="#数据流优先级" class="headerlink" title="数据流优先级"></a>数据流优先级</h3><p>HTTP/2 标准允许每个数据流都有一个关联的权重和依赖关系,可以向每个数据流分配一个介于1至256之间的整数,每个数据流和其他数据流之间存在显式的依赖关系。数据流依赖关系和权重的组合客户端可以构建出优先级树,表明倾向于如何接受响应。服务端也可以控制CPU、内存以及其他资源的分配来设定数据流处理的优先级,将高优先级响应以最优方式传输至客户端。</p><h2 id="服务端推送"><a href="#服务端推送" class="headerlink" title="服务端推送"></a>服务端推送</h2><p>HTTP2中服务端可以向一个客户端发送多个响应,额外的推送资源,从而减少额外的延迟时间。</p><p><img data-src="%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8E%A8%E9%80%81.svg" alt="服务器为推送资源发起新数据流 (promise) "></p><p>服务端决定要推送一个对象时,会构造出PUSH_PROMISE帧,表明服务器向客户端推送所述资源的意图。该帧首部的流ID用来关联相应的请求。服务端推送需要保证被发送的对象是可缓存的,method首部的值必须是安全幂等的方法。</p><p>当客户端收到PUSH_PROMISE帧的时候,可以根据自身情况选择拒绝数据流(通过RST_STREAM帧),如果客户端不拒收推送,那么服务端会继续进行推送流程,用PUSH_PROMISE来指明ID对应的流来发送对象。</p><h2 id="首部压缩"><a href="#首部压缩" class="headerlink" title="首部压缩"></a>首部压缩</h2><p>在HTTP1.1中,首部信息始终以纯文本的形式进行传输,如果加上cookie信息,会增加很多无用的网络开销。为了减少此开销和提升性能,HTTP2会使用HPACK压缩请求和响应标头元数据。</p><p>HPACK通过静态哈夫曼代码对传输的首部字段进行编码,减少了各个传输的大小。HPACK要求客户端和服务端都维护和更新一个包含之前见过的标头字段的索引列表,对之前传输的值进行有效编码。</p><p><img data-src="%E9%A6%96%E9%83%A8%E5%8E%8B%E7%BC%A9.svg" alt="HPACK: HTTP/2 的标头压缩"></p>]]></content>
<summary type="html"><p>HTTP2是HTTP协议的一个重大的修订版本,用于解决随着Web发展HTTP1.1逐渐无法满足需求对开发者和用户造成负担的问题。</p>
<p>在HTTP1.1中,客户端需要多个连接才能实现并发和缩短延迟,同时HTTP1.1不会压缩请求和响应标头,导致不必要的网络流量以及等</summary>
<category term="网络" scheme="http://qian0817.top/categories/%E7%BD%91%E7%BB%9C/"/>
<category term="网络" scheme="http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="HTTP" scheme="http://qian0817.top/tags/HTTP/"/>
</entry>
<entry>
<title>kubernetes核心数据结构</title>
<link href="http://qian0817.top/2021/03/14/kubernetes%E6%A0%B8%E5%BF%83%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
<id>http://qian0817.top/2021/03/14/kubernetes%E6%A0%B8%E5%BF%83%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/</id>
<published>2021-03-14T07:44:42.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>Kubernetes是一个完全以资源为中心的系统,资源是Kubernetes中最重要的概念,Kubernetes的生态系统都围绕着资源进行运作。</p><p>Kubernetes将资源再次分组和版本化,形成Group、Version、Resource。</p><p>Group被称为资源组,在Kubernetes API Server中也称其为APIGroup。</p><p>Version被称为资源版本,在Kubernetes API Server中也可以被称为APIVersions。</p><p>Resource被称为资源,在Kubernetes API Server中也可称其为APIResource。</p><p>Kind被称为资源种类,描述Resource的种类,与Resource为同一个级别。</p><p>Kubernetes系统支持多个Group,每个Group支持多个Version,每个Version支持多个Resource,其中部分资源同时会拥有自己的子资源(SubResource),例如Deployment资源拥有Status子资源。</p><p>资源组、资源版本、资源、子资源的完整表现形式为<group>/<version>/<resource>/<subresource>。常用的Deployment资源的完整表现形式就是apps/v1/deployments/status。</p><p>每一个资源都拥有一定数量的资源操作方法,用于Etcd集群存储中对资源对象的增删改查操作。Kubernetes系统支持8种资源操作方法,分别是create、delete、deletecollection、get、list、patch、update、watch。每一个资源都至少有两个版本,分别为外部版本和内部版本。开发者也可以通过CRD实现自定义资源,允许用户将自己定义的资源添加到Kubernetes系统当中。</p><h2 id="Group"><a href="#Group" class="headerlink" title="Group"></a>Group</h2><p>Group(资源组)在Kubernetes API Server中可以称其为APIGroup。Kubernetes系统中定义了许多资源组,并按照不同的功能进行划分。资源组的数据结构代码如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> APIGroup <span class="keyword">struct</span> {</span><br><span class="line"> TypeMeta <span class="string">`json:",inline"`</span></span><br><span class="line"> <span class="comment">// 资源组名称</span></span><br><span class="line"> Name <span class="keyword">string</span> <span class="string">`json:"name" protobuf:"bytes,1,opt,name=name"`</span></span><br><span class="line"> <span class="comment">// 资源组下所支持的资源版本</span></span><br><span class="line"> Versions []GroupVersionForDiscovery <span class="string">`json:"versions" protobuf:"bytes,2,rep,name=versions"`</span></span><br><span class="line"> <span class="comment">// 首选版本</span></span><br><span class="line"> PreferredVersion GroupVersionForDiscovery <span class="string">`json:"preferredVersion,omitempty" protobuf:"bytes,3,opt,name=preferredVersion"`</span></span><br><span class="line"> ServerAddressByClientCIDRs []ServerAddressByClientCIDR <span class="string">`json:"serverAddressByClientCIDRs,omitempty" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在Kubernetes系统中,支持两类资源组,分别是拥有组名的资源组和没有组名的资源组。拥有组名的资源组其表现形式为<group>/<version>/<resource>,例如apps/v1/deployments。没有组名的资源组又被称为核心资源组,其表现形式为/<version>/<resource>。</p><h2 id="Version"><a href="#Version" class="headerlink" title="Version"></a>Version</h2><p>每当资源的新版本发布时,都需要设置版本号,目的是为了在兼容旧版本的同时不断升级新版本。Kubernetes的资源版本控制可分为3种,分别是Alpha、Beta和Stable。</p><p>Alpha版本为内部测试版本,用于Kubernetes开发者内部测试,该版本是不稳定的,可能存在很多缺陷和漏洞。Beta版本为相对稳定的版本,经过社区和官方的多次讨论。Stable版本为正式发布的版本,Stable版本基本形成了产品,不会被删除。</p><p>资源版本数据结构代码如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> APIVersions <span class="keyword">struct</span> {</span><br><span class="line"> TypeMeta <span class="string">`json:",inline"`</span></span><br><span class="line"> <span class="comment">// 所支持的资源版本列表</span></span><br><span class="line"> Versions []<span class="keyword">string</span> <span class="string">`json:"versions" protobuf:"bytes,1,rep,name=versions"`</span></span><br><span class="line"> ServerAddressByClientCIDRs []ServerAddressByClientCIDR <span class="string">`json:"serverAddressByClientCIDRs" protobuf:"bytes,2,rep,name=serverAddressByClientCIDRs"`</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Resource"><a href="#Resource" class="headerlink" title="Resource"></a>Resource</h2><p>在Kubernetes体系架构中,资源是Kubernetes最重要的概念,Kubernetes本质上是一个资源控制系统。</p><p>一个资源被实例化以后会表达为一个资源对象(Resource Object)。在Kubernetes系统中定义并运行着各式各样的资源对象,所有的资源对象都是Entity(实体),Kubernetes用这些Entity来表示当前状态。Kubernetes目前支持持久化实体和短暂性实体两种实体。</p><p>持久性实体在资源创建后会持久确保该对象存在,例如Deployment,短暂性实体在资源对象创建后,如果出现故障或者调度失败,不会重新创建该对象,例如Pod。资源数据结构如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> APIResource <span class="keyword">struct</span> {</span><br><span class="line"> Name <span class="keyword">string</span> <span class="string">`json:"name" protobuf:"bytes,1,opt,name=name"`</span></span><br><span class="line"> SingularName <span class="keyword">string</span> <span class="string">`json:"singularName" protobuf:"bytes,6,opt,name=singularName"`</span></span><br><span class="line"> Namespaced <span class="keyword">bool</span> <span class="string">`json:"namespaced" protobuf:"varint,2,opt,name=namespaced"`</span></span><br><span class="line"> Group <span class="keyword">string</span> <span class="string">`json:"group,omitempty" protobuf:"bytes,8,opt,name=group"`</span></span><br><span class="line"> Version <span class="keyword">string</span> <span class="string">`json:"version,omitempty" protobuf:"bytes,9,opt,name=version"`</span></span><br><span class="line"> Kind <span class="keyword">string</span> <span class="string">`json:"kind" protobuf:"bytes,3,opt,name=kind"`</span></span><br><span class="line"> Verbs Verbs <span class="string">`json:"verbs" protobuf:"bytes,4,opt,name=verbs"`</span></span><br><span class="line"> ShortNames []<span class="keyword">string</span> <span class="string">`json:"shortNames,omitempty" protobuf:"bytes,5,rep,name=shortNames"`</span></span><br><span class="line"> Categories []<span class="keyword">string</span> <span class="string">`json:"categories,omitempty" protobuf:"bytes,7,rep,name=categories"`</span></span><br><span class="line"> StorageVersionHash <span class="keyword">string</span> <span class="string">`json:"storageVersionHash,omitempty" protobuf:"bytes,10,opt,name=storageVersionHash"`</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在Kubernetes中,同一资源对应两个版本号,分别为内部和外部版本号。External Object(外部版本资源对象)用于对外暴露给用户请求的接口所使用的资源对象,Internal Object(内部版本使用对象)不对外暴露,仅在Kubernetes API Server内部使用,用于多资源版本的转换。</p><p>资源的内部版本和外部版本是需要相互进行转换的,用于转换的函数需要事先初始化到资源注册表中,多个外部版本之间的资源进行相互转换需要通过内部版本进行中转,这也是Kubernetes实现多资源版本转换的关键。</p><p>资源的内部版本定义了所支持的资源类型(types.go)、资源验证方法(validation.go)、资源注册至资源注册表的方法(install.go)等。资源的外部版本定义了资源的转换方法(conversion.go),资源的默认值(defaults.go)等。</p><p>以Deployment资源为例,内部版本定义在pkg/apis/apps目录下,其中register.go代码文件定义了所属的资源组和资源版本,内部版本通过runtime.APIVersionInternal标识。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// GroupName is the group name use in this package</span></span><br><span class="line"><span class="keyword">const</span> GroupName = <span class="string">"apps"</span></span><br><span class="line"><span class="comment">// SchemeGroupVersion is group version used to register these objects</span></span><br><span class="line"><span class="keyword">var</span> SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}</span><br></pre></td></tr></table></figure><p>每一个Kubernetes资源目录,都通过type.go代码文件定义当前资源组/资源版本下所支持的资源类型。</p><p>在每一个Kubernetes资源组目录中,都有一个install.go代码文件,负责将资源信息注册到资源注册表中。</p><p>一个资源组下拥有多个资源版本,例如apps资源组用于v1,v1beta1,v1beta2等资源版本。当使用apps资源组下的Deployment资源时,在一些场景下,如果不指定资源版本,则使用该资源的首选版本。以apps资源组为例,注册资源时会注册多个资源版本,</p><p>在Kubernetes系统中,针对每一个资源都有一定的操作方法,不同的资源对象拥有不同的操作方法,例如对于Pod资源,可以执行create、delete、get等操作。资源操作方法可以通过Verbs数据结构进行描述,代码如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Verbs []<span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(vs Verbs)</span> <span class="title">String</span><span class="params">()</span> <span class="title">string</span></span> {</span><br><span class="line"> <span class="keyword">return</span> fmt.Sprintf(<span class="string">"%v"</span>, []<span class="keyword">string</span>(vs))</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Kubernetes系统拥有强大的高扩展功能,其中自定义资源是一种常见的扩展方式,可以将自己定义的资源添加到Kubernetes当中。</p><p>无论内置资源还是自定义资源,都通过资源对象描述文件进行定义。一个资源对象需要用5个字段来进行描述,分别是Group/Version、Kind、MetaData、Spec、Status。这些字段定义在YAML或JSON文件中。Kubernetes系统中的所有资源对象都可以通过YAML或者JSON来进行定义。</p><h2 id="runtime-Object"><a href="#runtime-Object" class="headerlink" title="runtime.Object"></a>runtime.Object</h2><p>Runtime(运行时)一般指程序或者语言核心库的实现。runtime.Object是Kubernetes类型系统的基石,Kubernetes上所有的资源对象实际上就是一种Go语言的Struct类型,都有一个共同的结构叫做runtime.Object。其结构如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Object <span class="keyword">interface</span> {</span><br><span class="line"> GetObjectKind() schema.ObjectKind</span><br><span class="line"> DeepCopyObject() Object</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其提供了两个方法,分别是GetObjectKind用于设置并返回GroupVersionKind和DeepCopyObject用于深复制当前资源对象。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> ObjectKind <span class="keyword">interface</span> {</span><br><span class="line"> <span class="comment">// SetGroupVersionKind sets or clears the intended serialized kind of an object. Passing kind nil</span></span><br><span class="line"> <span class="comment">// should clear the current setting.</span></span><br><span class="line"> SetGroupVersionKind(kind GroupVersionKind)</span><br><span class="line"> <span class="comment">// GroupVersionKind returns the stored group, version, and kind of an object, or an empty struct</span></span><br><span class="line"> <span class="comment">// if the object does not expose or provide these fields.</span></span><br><span class="line"> GroupVersionKind() GroupVersionKind</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Kubernetes中任意资源对象都可以通过runtime.Object来存储它的类型并且进行深拷贝操作。实例代码将Pod资源对象转换为了runtime.Object之后再次转换为资源对象,代码如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">v1 <span class="string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span></span><br><span class="line"><span class="string">"k8s.io/apimachinery/pkg/runtime"</span></span><br><span class="line"><span class="string">"k8s.io/kubernetes/pkg/apis/core"</span></span><br><span class="line"><span class="string">"reflect"</span></span><br><span class="line">)</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> pod := &core.Pod{</span><br><span class="line"> TypeMeta: v1.TypeMeta{Kind: <span class="string">"Pod"</span>},</span><br><span class="line"> ObjectMeta: v1.ObjectMeta{Labels: <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>{<span class="string">"name"</span>: <span class="string">"foo"</span>}},</span><br><span class="line"> }</span><br><span class="line"> pod.DeepCopyObject()</span><br><span class="line"> pod.GroupVersionKind()</span><br><span class="line"> obj := runtime.Object(pod)</span><br><span class="line"> pod2, ok := obj.(*core.Pod)</span><br><span class="line"> <span class="keyword">if</span> !ok {</span><br><span class="line"> <span class="built_in">panic</span>(<span class="string">"unexpected"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> !reflect.DeepEqual(pod, pod2) {</span><br><span class="line"> <span class="built_in">panic</span>(<span class="string">"unexpected"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Scheme资源注册表"><a href="#Scheme资源注册表" class="headerlink" title="Scheme资源注册表"></a>Scheme资源注册表</h2><p>Kubernetes系统拥有众多的资源,每一个资源就是一个资源类型,这些资源需要一个统一的注册、存储、查询、管理机制。Kubernetes系统中所有的资源类型都需要注册到Scheme资源注册表中,其是一个内存型的资源注册表,拥有以下特点:</p><ol><li>支持注册多种资源类型,包括内部和外部版本</li><li>支持多版本的转换机制</li><li>支持不同资源的序列化反序列化机制</li></ol><p>Scheme资源注册表数据结构主要由4个map结构组成,分别是gvkToType、typeToGVK、unversionedTypes、unversionedKinds,代码如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Scheme <span class="keyword">struct</span> {</span><br><span class="line"> <span class="comment">// 存储GVK与type的映射关系</span></span><br><span class="line"> gvkToType <span class="keyword">map</span>[schema.GroupVersionKind]reflect.Type</span><br><span class="line"> <span class="comment">// 存储Type与GVK的映射关系</span></span><br><span class="line"> typeToGVK <span class="keyword">map</span>[reflect.Type][]schema.GroupVersionKind</span><br><span class="line"> <span class="comment">// 存储UnversionedType与GVK的映射关系</span></span><br><span class="line"> unversionedTypes <span class="keyword">map</span>[reflect.Type]schema.GroupVersionKind</span><br><span class="line"> <span class="comment">// 存储Kind与UnversionedType的映射关系</span></span><br><span class="line"> unversionedKinds <span class="keyword">map</span>[<span class="keyword">string</span>]reflect.Type</span><br><span class="line"> fieldLabelConversionFuncs <span class="keyword">map</span>[schema.GroupVersionKind]FieldLabelConversionFunc</span><br><span class="line"> defaulterFuncs <span class="keyword">map</span>[reflect.Type]<span class="function"><span class="keyword">func</span><span class="params">(<span class="keyword">interface</span>{})</span></span></span><br><span class="line"> converter *conversion.Converter</span><br><span class="line"> versionPriority <span class="keyword">map</span>[<span class="keyword">string</span>][]<span class="keyword">string</span></span><br><span class="line"> observedVersions []schema.GroupVersion</span><br><span class="line"> schemeName <span class="keyword">string</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在Scheme资源注册表中,不同的资源类型使用的注册方法不同</p><ul><li><p>scheme.AddUnversionedTypes:注册UnversionedType资源类型。</p></li><li><p>scheme.AddKnownTypes:注册KnownTypes资源类型</p></li><li><p>scheme.AddKnownTypeWithName:注册KnownType资源类型,须指定资源的Kind资源种类名称</p></li></ul><p>AddKnownTypes方法在注册资源类型时,无需指定Kind名称,而是通过reflect机制来获取资源类型的名称作为种类名称。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Scheme)</span> <span class="title">AddKnownTypes</span><span class="params">(gv schema.GroupVersion, types ...Object)</span></span> {</span><br><span class="line"> s.addObservedVersion(gv)</span><br><span class="line"> <span class="keyword">for</span> _, obj := <span class="keyword">range</span> types {</span><br><span class="line"> t := reflect.TypeOf(obj)</span><br><span class="line"> <span class="keyword">if</span> t.Kind() != reflect.Ptr {</span><br><span class="line"> <span class="built_in">panic</span>(<span class="string">"All types must be pointers to structs."</span>)</span><br><span class="line"> }</span><br><span class="line"> t = t.Elem()</span><br><span class="line"> s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Codec编解码器"><a href="#Codec编解码器" class="headerlink" title="Codec编解码器"></a>Codec编解码器</h2><p>Codec编解码器接口定义如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Encoder <span class="keyword">interface</span> {</span><br><span class="line"> Encode(obj Object, w io.Writer) error</span><br><span class="line"> Identifier() Identifier</span><br><span class="line">}</span><br><span class="line"><span class="keyword">type</span> Decoder <span class="keyword">interface</span> {</span><br><span class="line">Decode(data []<span class="keyword">byte</span>, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">type</span> Serializer <span class="keyword">interface</span> {</span><br><span class="line">Encoder</span><br><span class="line">Decoder</span><br><span class="line">}</span><br><span class="line"><span class="keyword">type</span> Codec Serializer</span><br></pre></td></tr></table></figure><p>kubernetes目前支持3种主要的序列化器,分别是jsonSerializer、yamlSerializer、protobufSerializer。</p><p>编解码器通过NewCodecFactory函数实例化,在实例化过程中会将上述三种序列化器全部实例化</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewCodecFactory</span><span class="params">(scheme *runtime.Scheme, mutators ...CodecFactoryOptionsMutator)</span> <span class="title">CodecFactory</span></span> {</span><br><span class="line"> options := CodecFactoryOptions{Pretty: <span class="literal">true</span>}</span><br><span class="line"> <span class="keyword">for</span> _, fn := <span class="keyword">range</span> mutators {</span><br><span class="line"> fn(&options)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> serializers := newSerializersForScheme(scheme, json.DefaultMetaFactory, options)</span><br><span class="line"> <span class="keyword">return</span> newCodecFactory(scheme, serializers)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newSerializersForScheme</span><span class="params">(scheme *runtime.Scheme, mf json.MetaFactory, options CodecFactoryOptions)</span> []<span class="title">serializerType</span></span> {</span><br><span class="line">jsonSerializer := json.NewSerializerWithOptions(</span><br><span class="line">mf, scheme, scheme,</span><br><span class="line">json.SerializerOptions{Yaml: <span class="literal">false</span>, Pretty: <span class="literal">false</span>, Strict: options.Strict},</span><br><span class="line">)</span><br><span class="line">jsonSerializerType := serializerType{</span><br><span class="line">AcceptContentTypes: []<span class="keyword">string</span>{runtime.ContentTypeJSON},</span><br><span class="line">ContentType: runtime.ContentTypeJSON,</span><br><span class="line">FileExtensions: []<span class="keyword">string</span>{<span class="string">"json"</span>},</span><br><span class="line">EncodesAsText: <span class="literal">true</span>,</span><br><span class="line">Serializer: jsonSerializer,</span><br><span class="line"></span><br><span class="line">Framer: json.Framer,</span><br><span class="line">StreamSerializer: jsonSerializer,</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> options.Pretty {</span><br><span class="line">jsonSerializerType.PrettySerializer = json.NewSerializerWithOptions(</span><br><span class="line">mf, scheme, scheme,</span><br><span class="line">json.SerializerOptions{Yaml: <span class="literal">false</span>, Pretty: <span class="literal">true</span>, Strict: options.Strict},</span><br><span class="line">)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">yamlSerializer := json.NewSerializerWithOptions(</span><br><span class="line">mf, scheme, scheme,</span><br><span class="line">json.SerializerOptions{Yaml: <span class="literal">true</span>, Pretty: <span class="literal">false</span>, Strict: options.Strict},</span><br><span class="line">)</span><br><span class="line">protoSerializer := protobuf.NewSerializer(scheme, scheme)</span><br><span class="line">protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme)</span><br><span class="line"></span><br><span class="line">serializers := []serializerType{</span><br><span class="line">jsonSerializerType,</span><br><span class="line">{</span><br><span class="line">AcceptContentTypes: []<span class="keyword">string</span>{runtime.ContentTypeYAML},</span><br><span class="line">ContentType: runtime.ContentTypeYAML,</span><br><span class="line">FileExtensions: []<span class="keyword">string</span>{<span class="string">"yaml"</span>},</span><br><span class="line">EncodesAsText: <span class="literal">true</span>,</span><br><span class="line">Serializer: yamlSerializer,</span><br><span class="line">},</span><br><span class="line">{</span><br><span class="line">AcceptContentTypes: []<span class="keyword">string</span>{runtime.ContentTypeProtobuf},</span><br><span class="line">ContentType: runtime.ContentTypeProtobuf,</span><br><span class="line">FileExtensions: []<span class="keyword">string</span>{<span class="string">"pb"</span>},</span><br><span class="line">Serializer: protoSerializer,</span><br><span class="line"></span><br><span class="line">Framer: protobuf.LengthDelimitedFramer,</span><br><span class="line">StreamSerializer: protoRawSerializer,</span><br><span class="line">},</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, fn := <span class="keyword">range</span> serializerExtensions {</span><br><span class="line"><span class="keyword">if</span> serializer, ok := fn(scheme); ok {</span><br><span class="line">serializers = <span class="built_in">append</span>(serializers, serializer)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> serializers</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Converter资源版本转换器"><a href="#Converter资源版本转换器" class="headerlink" title="Converter资源版本转换器"></a>Converter资源版本转换器</h2><p>在Kubernetes中,同一资源拥有多个资源版本,Kubernetes系统允许同一资源的不同资源版本进行转换。Converter资源版本转换器主要用于解决多资源版本的转换问题,Kubernetes系统中的一个资源支持多个资源版本,Kubernetes通过内部版本机制实现资源的版本转换。</p><p>Converter转换器数据结构主要存放转换函数,转换器数据结构代码如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Converter <span class="keyword">struct</span> {</span><br><span class="line"> <span class="comment">// 默认转换函数,一般定义在资源目录下的conversion.go代码文件中</span></span><br><span class="line"> conversionFuncs ConversionFuncs</span><br><span class="line"> <span class="comment">// 自动生成的转换函数</span></span><br><span class="line"> generatedConversionFuncs ConversionFuncs</span><br><span class="line"> <span class="comment">// 若资源对象注册到此字段,忽略此资源对象的转换操作</span></span><br><span class="line"> ignoredConversions <span class="keyword">map</span>[typePair]<span class="keyword">struct</span>{}</span><br><span class="line"> ignoredUntypedConversions <span class="keyword">map</span>[typePair]<span class="keyword">struct</span>{}</span><br><span class="line"> <span class="comment">// 在转换过程中用于获取资源对象种类的名称</span></span><br><span class="line"> nameFunc <span class="function"><span class="keyword">func</span><span class="params">(t reflect.Type)</span> <span class="title">string</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Converter转换函数需要通过注册才能在Kubernetes中进行使用,需要通过以下的注册转换函数进行注册</p><ul><li>scheme.AddIgnoredConversionType:注册忽略的资源类型</li><li>scheme.AddConversionFuncs:注册多个Conversion Func转换函数</li><li>scheme.AddConversionFunc:注册单个Conversion Func转换函数</li><li>scheme.AddGeneratedConversionFunc:注册自动生成的转换函数</li><li>scheme.AddFieldLabelConversionFunc:注册字段标签的转换函数</li></ul><p>Converter转换器在Kubernetes中使用十分广泛,例如Deployment资源对象,起初使用v1beta1资源版本,后来推出稳定的v1版本,因此会将v1beta1版本转换为v1版本,Converter转换器通过Converter函数进行资源版本的转换。</p>]]></content>
<summary type="html"><p>Kubernetes是一个完全以资源为中心的系统,资源是Kubernetes中最重要的概念,Kubernetes的生态系统都围绕着资源进行运作。</p>
<p>Kubernetes将资源再次分组和版本化,形成Group、Version、Resource。</p>
<p>Gr</summary>
<category term="kubernetes" scheme="http://qian0817.top/categories/kubernetes/"/>
<category term="kubernetes" scheme="http://qian0817.top/tags/kubernetes/"/>
</entry>
<entry>
<title>Docker实现原理</title>
<link href="http://qian0817.top/2021/02/18/Docker%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/"/>
<id>http://qian0817.top/2021/02/18/Docker%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</id>
<published>2021-02-18T06:24:35.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>对于Docker来说,Cgroups技术是用来制造约束的主要手段,Namespace技术则是用来修改进程视图的主要方法。</p><h2 id="Namespace"><a href="#Namespace" class="headerlink" title="Namespace"></a>Namespace</h2><p>在Linux系统中创建进程的系统调用是clone。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> pid = clone(main_function, stack_size, SIGCHLD, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><p>该系统调用会创建一个新的进程,并且返回它的进程号pid。如果在参数中指定CLONE_NEWPID 参数</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, <span class="literal">NULL</span>); </span><br></pre></td></tr></table></figure><p>那么新创建的进程会认为自己的PID是1,但是在宿主机中看到的PID仍然是真实的PID数值。</p><p>除了PID Namespace,Linux系统总共提供了6种Namespace</p><table><thead><tr><th>namespace</th><th>调用参数</th><th>隔离内容</th></tr></thead><tbody><tr><td>UTS</td><td>CLONE_NEWUTS</td><td>主机名和域名</td></tr><tr><td>IPC</td><td>CLONE_NEWIPC</td><td>信号量,消息队列,共享内存</td></tr><tr><td>PID</td><td>CLONE_NEWPID</td><td>进程编号</td></tr><tr><td>Network</td><td>CLONE_NEWNET</td><td>网络设备</td></tr><tr><td>Mount</td><td>CLONE_NEWNS</td><td>挂载点</td></tr><tr><td>User</td><td>CLONE_NEWUSER</td><td>用户和用户组</td></tr></tbody></table><p>通过Namespace机制,容器内部就只能看到当前Namespace所限定的文件状态等信息。所有说容器只是一种特殊的进程而已。</p><h2 id="CGroup"><a href="#CGroup" class="headerlink" title="CGroup"></a>CGroup</h2><p>虽然Namespace将进程隔离了起来,但是所能使用到的资源却可以被其他进程所占用。</p><p>Cgroup是Linux用来为进程设置资源限制的一个重要功能,用于限制一个进程组能够使用的资源上限,比如CPU、内存、磁盘、网络带宽资源等等。</p><p>在Linux中,Cgroups给用户暴露的操作接口是文件系统,以文件和目录的方式组织在操作系统的/sys/fs/cgroup路径下。可以使用以下两条命令之一展示出来:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> mount -f cgroup</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> lssubsys -m</span></span><br><span class="line">cpuset /sys/fs/cgroup/cpuset</span><br><span class="line">cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct</span><br><span class="line">blkio /sys/fs/cgroup/blkio</span><br><span class="line">memory /sys/fs/cgroup/memory</span><br><span class="line">devices /sys/fs/cgroup/devices</span><br><span class="line">freezer /sys/fs/cgroup/freezer</span><br><span class="line">net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio</span><br><span class="line">perf_event /sys/fs/cgroup/perf_event</span><br><span class="line">hugetlb /sys/fs/cgroup/hugetlb</span><br><span class="line">pids /sys/fs/cgroup/pids</span><br><span class="line">rdma /sys/fs/cgroup/rdma</span><br></pre></td></tr></table></figure><p>以CPU为例,在/sys/fs/cgroup/cpu目录下可以看到该资源具体可以被限制的方法。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> ls /sys/fs/cgroup/cpu</span></span><br><span class="line">cgroup.clone_children cpuacct.usage_all cpuacct.usage_user init.scope user.slice</span><br><span class="line">cgroup.procs cpuacct.usage_percpu cpu.cfs_period_us notify_on_release</span><br><span class="line">cgroup.sane_behavior cpuacct.usage_percpu_sys cpu.cfs_quota_us release_agent</span><br><span class="line">cpuacct.stat cpuacct.usage_percpu_user cpu.shares system.slice</span><br><span class="line">cpuacct.usage cpuacct.usage_sys cpu.stat tasks</span><br></pre></td></tr></table></figure><p>在该目录下创建一个目录container</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> mkdir container</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> ls container</span></span><br><span class="line">cgroup.clone_children cpuacct.usage_all cpuacct.usage_sys cpu.shares notify_on_release</span><br><span class="line">cgroup.procs cpuacct.usage_percpu cpuacct.usage_user cpu.stat tasks</span><br><span class="line">cpuacct.stat cpuacct.usage_percpu_sys cpu.cfs_period_us cpu.uclamp.max</span><br><span class="line">cpuacct.usage cpuacct.usage_percpu_user cpu.cfs_quota_us cpu.uclamp.min</span><br></pre></td></tr></table></figure><p>可以看出系统会在新创建的 container 目录下,自动生成该子系统对应的资源限制文件。在后台执行一个死循环脚本,消耗计算机的CPU为100%:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> <span class="keyword">while</span> : ; <span class="keyword">do</span> : ; <span class="keyword">done</span> &</span></span><br><span class="line">[1] 16594</span><br></pre></td></tr></table></figure><p>之后向其中的cfs_quota文件写入20ms(20000us),表示在每100ms的时间里,被该控制组限制的进程只能使用 20ms的CPU时间。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us</span><br></pre></td></tr></table></figure><p>然后把被限制的进程的PID写入到tasks文件当中</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">echo 16594 > /sys/fs/cgroup/cpu/container/tasks</span><br></pre></td></tr></table></figure><p>之后使用top命令就可以看到CPU的使用率降到了20%。</p><p>container目录下的文件不可以直接被删除,需要使用到工具cgdelete删除container目录</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cgdelete cpu:/container/</span><br></pre></td></tr></table></figure><h2 id="网络"><a href="#网络" class="headerlink" title="网络"></a>网络</h2><p>Docker提供了四种不同的网络模式,分别为Host、Container、None 和 Bridge 模式。</p><p>在默认的网桥模式下,会分配隔离的网络命名空间以外,Docker还会为所有的容器设置IP地址。当Docker服务器在主机上启动以后会创建新的虚拟网桥docker0,随后该主机启动的全部服务都在默认情况下与该网桥相连。</p><p>docker会为每一个容器分配一个新的IP地址并将docker0的IP地址设置为默认的网关。网桥docker0通过iptables中的配置与宿主机器上的网卡相连,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。</p><p>在Host模式下,这个容器不会获得一个独立的Network Namespace,而是和宿主机共用一个 Network Namespace。</p><p>在Container模式下,会指定新创建的容器和已经存在的一个容器共享一个Network Namespace,新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享 IP、端口范围等。</p><p>在None模式下,Docker 容器拥有自己的 Network Namespace,但是,并不为Docker 容器进行任何网络配置。因此该容器没有网卡、IP、路由等信息。</p><p>Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,从而让Docker容器能够为宿主机器和其他容器提供服务。</p><h2 id="挂载点"><a href="#挂载点" class="headerlink" title="挂载点"></a>挂载点</h2><p>如果一个容器需要启动,那么它一定需要提供一个根文件系统(rootfs),容器需要使用这个文件系统来创建一个新的进程,所有二进制的执行都必须在这个根文件系统中。</p><p>同时为了保证当前的容器进程没有办法访问宿主机上的其他目录,还需要通过pivot_root或者chroot来改变进程能够访问文件目录的根节点。通过改变当前根目录的结构,能够限制容器在新的根目录下并不能够访问旧系统根目录的结构个文件,因此也就建立了一个与原系统完全隔离的目录结构。</p><h2 id="UnionFS"><a href="#UnionFS" class="headerlink" title="UnionFS"></a>UnionFS</h2><p>Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。其原理就用到了UnionFS。</p><p>UnionFS是Linux系统设计的用于把多个文件系统联合到同一个挂载点的文件系统服务,其功能在于把多个不同位置的目录联合挂载到同一个目录下。</p><p>AUFS(Advanced UnionFS)其实就是 UnionFS的升级版,对于AUFS来说,最关键的目录结构在于/var/lib/docker/aufs路径下的diff目录,其中存储着docker的镜像层和容器层的内容。layers目录存储着镜像层的元数据,每一个文件都保存着镜像层的元数据。mnt包含镜像或者容器层的挂载点,最终会被Docker通过联合的方式进行组装。</p>]]></content>
<summary type="html"><p>对于Docker来说,Cgroups技术是用来制造约束的主要手段,Namespace技术则是用来修改进程视图的主要方法。</p>
<h2 id="Namespace"><a href="#Namespace" class="headerlink" title="Namesp</summary>
<category term="Docker" scheme="http://qian0817.top/categories/Docker/"/>
<category term="Docker" scheme="http://qian0817.top/tags/Docker/"/>
<category term="容器" scheme="http://qian0817.top/tags/%E5%AE%B9%E5%99%A8/"/>
</entry>
<entry>
<title>分布式一致性算法</title>
<link href="http://qian0817.top/2021/02/10/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E7%AE%97%E6%B3%95/"/>
<id>http://qian0817.top/2021/02/10/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E7%AE%97%E6%B3%95/</id>
<published>2021-02-10T12:05:32.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<h2 id="Paxos"><a href="#Paxos" class="headerlink" title="Paxos"></a>Paxos</h2><p>Paxos算法包含两个部分。一个是Basic Paxos算法,描述的是多节点之间如何就某个值达成共识。另一个是Multi Paxos算法,描述的是执行多个Basic Paxos实例,就一系列值达成共识。</p><h3 id="Basic-Paxos"><a href="#Basic-Paxos" class="headerlink" title="Basic Paxos"></a>Basic Paxos</h3><p>在Basic Paxos算法中,有提议者(Proposer),接受者(Acceptor),学习者(Learner)三种角色。</p><p>提议者决议一个值,用于投票表决。集群中收到客户端请求的节点,才是提议者。提议者在收到客户端请求后,发起二阶段提交,进行共识协商。</p><p>接收者对每个提议的值进行投票,并存储接受的值。一般来说,集群中的所有节点都在扮演接受者的角色。接受者参与共识协商,并接受和存储数据。</p><p>学习者是被告知投票的结果,接受达成共识的值。一般来说,学习者是数据备份节点,比如主从模型的从节点,被动地接受数据,容灾备份。</p><p>在准备阶段,客户端分别向所有的接受者发送准备请求。在准备请求中是不需要指定提议的值,只需要提案编号就可以了。如果接受者之前没有通过任何提案,那么会返回一个尚无提案的响应,并承诺以后不再通过编号小于该提案编号的提案。</p><p>第二个阶段就是接受阶段,提议者在收到大部分节点的准备响应以后,会分别发送接受请求。当接受者收到客户端的接受请求时,如果提案的提案编号小于当前能通过的提案的最小提案编号,那么会被拒绝。如果提案编号不小于节点承诺能通过的提案的最小提案编号,那么多个节点之间就能达成了共识。</p><h3 id="Multi-Paxos"><a href="#Multi-Paxos" class="headerlink" title="Multi Paxos"></a>Multi Paxos</h3><p>Basic Paxos只能就单个值达成共识,一旦遇到为一系列的值实现共识的,就需要使用到Multi Paxos算法了。</p><p>Multi Paxos引入了领导者节点,领导者作为唯一的提议者,防止出现多个提议者提示提交提案的情况。当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段,从而降低往返的消息数,提升性能并降低延时。</p><h2 id="Raft"><a href="#Raft" class="headerlink" title="Raft"></a>Raft</h2><p>Raft在Paxos算法的基础上,做了一些简化和限制。</p><p>Raft算法包含三种状态:领导者Leader,跟随者Follower以及候选人Candidate。在稳定状态下,只存在一个Leader节点和多个Follower节点,Leader节点通过心跳消息与各个Follower节点保持联系。raft算法中可能出现的状态转换关系如下图所示</p><p><a href="https://github.com/maemual/raft-zh_cn/blob/master/images/raft-%E5%9B%BE4.png"><img data-src="https://github.com/maemual/raft-zh_cn/raw/master/images/raft-%E5%9B%BE4.png" alt="图 4 "></a></p><p>在集群刚启动时,所有的节点都是follower,在经过心跳超时超时时间以后,会转变成为candidate去拉取选票,获得大多数选票以后就变成了leader。此时如果大部分其他候选人发现了新的leader已经诞生,会自动转变为follower。如果经过一个选举超时时间以后仍然没有选举出leader,那么会重新开始一次新的选举。</p><p>Raft算法在节点之间所使用的消息主要有两种</p><ul><li>RequestVote,请求其他节点给自己投票,由Candidate节点发出</li><li>AppendEntries,用于日志复制以及心跳信息,由Leader节点发出</li></ul><p>Raft在选举过程中有一个term参数,作为逻辑时钟值。在选举开始时,增加自己的term,在发送RequestVote消息以及AppendEntries带上自己的term信息,在收到RequestVote消息以及AppendEntries消息时,选择最大的term并更新自己的term。</p><h3 id="选举过程"><a href="#选举过程" class="headerlink" title="选举过程"></a>选举过程</h3><p>Raft通过心跳机制发起leader选举。节点都是从follower状态开始的,如果收到了来自leader或candidate的AppendEntries RPC,那它就保持follower状态,避免争抢称为candidate。Leader会发送空的AppendEntries RPC作为心跳信号来确立自己的地位,如果follower经过选举超时时间仍然没有收到心跳,它就会认为leader已经挂了,发起新的一轮选举。</p><h3 id="日志复制过程"><a href="#日志复制过程" class="headerlink" title="日志复制过程"></a>日志复制过程</h3><p>一旦leader被选举成功,就可以为客户端提供服务了。客户端提交每一条命令都会被按顺序记录到leader的日志中,并且加上term编号和顺序索引,然后向其他节点发送AppendEntries RPC用以复制日志。当大多数节点成功复制以后,leader就会提交命令,执行该命令并将结果返回给客户端。leader会保存当前已经提交的最高日志编号。当发送AppendEntries RPC时,会包含leader上一条刚处理过的命令,如果接收节点发现上一条命令不匹配,会拒绝执行。</p><p>如果在过程中leader崩溃了,那么所记录的日志没有完全被复制,会造成日志不一致的情况,follower相比于当前的leader可能会丢失几条日志,也可能会额外多出几条日志,例如下图所示,a和b丢失了部分命令,c和d多了几条命令,e和f既有增多也有丢失。(场景 f 可能会这样发生,该服务器在任期 2 的时候是领导人,已附加了一些日志条目到自己的日志中,但在提交之前就崩溃了;重启后在任期 3 重新被选为领导人,并且又增加了一些日志条目到自己的日志中;在任期 2 和任期 3 的日志被提交之前,这个服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。)</p><p><img data-src="https://github.com/maemual/raft-zh_cn/raw/master/images/raft-%E5%9B%BE7.png" alt="图 7"></p><p>在Raft中,leader通过强制follower复制自己的日志来解决上述日志不一致的情形,冲突的日志将会被重写。为了让日志一致,先找到最新的一致的那条日志(如f中索引为3的日志条目),然后把follower之后的日志全部删除,leader再把自己在那之后的日志全部推送给follower,这样就实现了一致。</p><h2 id="Gossip"><a href="#Gossip" class="headerlink" title="Gossip"></a>Gossip</h2><p>Gossip利用一种随机的带传染性的方式,将信息传播到整个网络当中,实现最终一致性的算法。</p><p>gossip的执行过程如下:Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。</p><p>Gossip算法包含直接邮寄、反熵和谣言传播三部分。</p><p>直接邮寄就是直接发送更新数据,当数据发送失败时,将数据缓存下来,然后进行重传。</p><p>反熵指的是集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。在反熵实现的时候主要有推、拉以及推拉结合的三种方式。因为反熵需要节点两两交换和比对自己所有的数据,执行反熵通信的成本会很高,不应该在实际场景中频繁的执行反熵。</p><p>谣言传播指的是当一个节点有一个新数据以后,这个节点变成了活跃节点,并周期性的联系其他节点发送新数据,知道所有节点都存储了该数据。</p><p>gossip协议允许网络中节点的任意增加和减少,同时网络中任何节点的宕机和重启都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性,并且不要求任何的中心节点,但是会出现消息的延迟以及消息冗余的问题。</p>]]></content>
<summary type="html"><h2 id="Paxos"><a href="#Paxos" class="headerlink" title="Paxos"></a>Paxos</h2><p>Paxos算法包含两个部分。一个是Basic Paxos算法,描述的是多节点之间如何就某个值达成共识。另一个是Mul</summary>
</entry>
<entry>
<title>Java内存区域</title>
<link href="http://qian0817.top/2021/02/10/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/"/>
<id>http://qian0817.top/2021/02/10/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/</id>
<published>2021-02-10T10:46:42.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<h2 id="程序计数器"><a href="#程序计数器" class="headerlink" title="程序计数器"></a>程序计数器</h2><p>程序计数器是一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。</p><p>如果线程执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行的是本地方法,那么计数器的值应该为空。此内存区域是唯一没有规定OutOfMemoryError情况的区域。</p><h2 id="虚拟机栈"><a href="#虚拟机栈" class="headerlink" title="虚拟机栈"></a>虚拟机栈</h2><p>Java虚拟机栈是线程私有的,其生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型,每个方法执行的时候,Java虚拟机会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应了一个栈桢在虚拟机栈中从入栈到出栈的过程。</p><p>如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError。如果Java虚拟机栈容量可以动态扩展,那么当栈无法申请到足够的内存时会抛出OutOfMemoryError异常。</p><h2 id="本地方法栈"><a href="#本地方法栈" class="headerlink" title="本地方法栈"></a>本地方法栈</h2><p>本地方法栈是为虚拟机使用到的本地方法服务,与虚拟机栈的作用十分类似。</p><h2 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h2><p>堆是所有线程共享的一块内存区域,在虚拟机启动时创建。其作用是存放对象实例,几乎所有的对象都在这里分配内存。堆是垃圾收集器管理的内存区域。</p><p>Java堆可以处于物理上不连续的内存空间中,但是在逻辑上应该被视为连续的。当Java堆中没有内存完成实例分配,并且堆无法进行扩展时,会抛出OutOfMemoryError异常。</p><h2 id="方法区"><a href="#方法区" class="headerlink" title="方法区"></a>方法区</h2><p>方法区用于存储已经被虚拟机加载的类型信息、常量、静态变量等数据。根据规范,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。</p><h2 id="直接内存"><a href="#直接内存" class="headerlink" title="直接内存"></a>直接内存</h2><p>在JDK1.4以后新加入了NIO类,引入了基于通道与缓冲区的IO方式,其可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,从而提升性能并且避免了在Java堆和Native堆中来回复制数据。直接内存虽然不是Java虚拟机规范中定义的内存区域,但是当无法分配足够的内存时,也会导致OutOfMemoryError异常。</p>]]></content>
<summary type="html"><h2 id="程序计数器"><a href="#程序计数器" class="headerlink" title="程序计数器"></a>程序计数器</h2><p>程序计数器是一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选</summary>
<category term="JVM" scheme="http://qian0817.top/categories/JVM/"/>
<category term="JVM" scheme="http://qian0817.top/tags/JVM/"/>
</entry>
<entry>
<title>Kubernetes网络实现</title>
<link href="http://qian0817.top/2021/02/09/Kubernetes%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0/"/>
<id>http://qian0817.top/2021/02/09/Kubernetes%E7%BD%91%E7%BB%9C%E5%AE%9E%E7%8E%B0/</id>
<published>2021-02-09T04:46:43.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>每个Pod都有自己唯一的IP地址,可以通过一个扁平的、非NAT网络和和其他Pod进行通信。</p><h2 id="同节点pod通信"><a href="#同节点pod通信" class="headerlink" title="同节点pod通信"></a>同节点pod通信</h2><p>基础设施容器启动之前,会为容器创建一个虚拟的Ethernet接口对(一个veth pair),其中一个对的接口保留在主机的命名空间中,其他的对被移入到容器网络命名空间当中,并重命名为eth0。</p><p>主机网络命名空间的接口会绑定到容器运行时配置使用的网络桥接上,从网桥的地址段中取IP地址赋值给容器中的eth0接口。任何运行在容器内部的程序都会发送数据到eth0网络接口,数据从主机命名空间的另一个veth接口出来,然后发送给网桥。任何连接到网桥的网络接口都可以接收该数据。</p><p>同一节点上的容器都会连接到同一个网桥,因此它们之间能够相互通信。但是要让不同节点上的容器之间能够通信,这些节点的网桥需要以某种方式连接起来。</p><h2 id="不同节点上的pod通信"><a href="#不同节点上的pod通信" class="headerlink" title="不同节点上的pod通信"></a>不同节点上的pod通信</h2><p>在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址无法做到进行互相访问,为了解决该问题,就需要使用到网络容器接口。</p><h3 id="网络容器接口"><a href="#网络容器接口" class="headerlink" title="网络容器接口"></a>网络容器接口</h3><p>为了让连接容器到网络更加方便,CNI允许Kubernetes可配置使用任何CNI插件。这些插件包含Calico、Flannel、Romana、Weave Net等等。</p><h4 id="Flannel"><a href="#Flannel" class="headerlink" title="Flannel"></a>Flannel</h4><p>Flannel支持三种后端实现</p><ul><li>VXLAN</li><li>host-gw</li><li>UDP</li></ul><h5 id="UDP模式"><a href="#UDP模式" class="headerlink" title="UDP模式"></a>UDP模式</h5><p>UDP模式是Flannel项目最早支持的模式、最直接、最好理解,也是性能最差的模式。Flannel通过在宿主机上创建出一系列的路由规则,将路由转发到一个叫做flannel0的设备当中去。</p><p>flannel0设备是一个TUN设备,是一种工作在网络层的虚拟网络设备,负责在操作系统内核与应用程序之间传递IP包。当一个IP包发送给TUN设备时,会将这个IP包交给创建这个设备的应用程序。反之创建该设备的应用程序向TUN设备发送一个IP包时,这个IP包就会出现在宿主机网络栈中,根据路由表进行下一步的处理。</p><p>在由Flannel管理的容器网络中,一台宿主机上的所有容器,都属于被宿主机分配的一个子网,这些子网与宿主机的对应关系保存在Etcd当中。因此flannel进程在处理传入的IP包时,就可以根据目的IP地址匹配到对应的子网,然后从Etcd当中找到这个子网对应的宿主机IP地址。每台宿主机上的flannel进程都监听着8235端口,在知道了对方的IP地址以后,就会把这个IP包封装到UDP包中,然后发送给对方的8235端口即可。对方宿主机的flannel进程收到该包以后,会把这个IP包发送给它所管理的TUN设备,最后通过本机的路由表发送给对应的容器。</p><p>Flannel的UDP模式提供了一个三层的Overlay网络,对发送端的IP包进行UDP封装,在接收端解封拿到原始的IP包,进而把这个包转发给目标容器。但是UDP模式存在严重的性能问题,因为相比于直接通信,基于Flannel UDP模式的容器通信多了flannelId的处理过程,在使用过程中需要经历三次用户态与内核态之间的数据拷贝。</p><p>![img](Flannel UDP.jpg)</p><h5 id="VXLAN-模式"><a href="#VXLAN-模式" class="headerlink" title="VXLAN 模式"></a>VXLAN 模式</h5><p>VXLAN(虚拟可扩展局域网)是Linux内核本身就支持的一种网络虚拟化技术,可以完全在内核态实现上述的封装与解封过程,通过与UDP相似的机制构建出Overlay网络。</p><p>VXLAN在现有的三层网络之上,覆盖一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的主机之间,可以想在同一个局域网(LAN)里面那样自由通信。</p><p>VXLAN会在宿主机上设置一个特殊的网络设备作为隧道的两端,这个设备叫做VTEP。该设备的作用是进行封装和解封装二层数据帧。其工作的执行流程全部是在内核里面完成。</p><p>为了能够将原始IP包封装并且发送到正确的宿主机当中,需要知道目的宿主机的VTEP设备。该设备的信息由每台宿主机上的flanneld进程负责维护。当一个节点Node启动并加入到Flannel网络当中后,在其他所有节点上,flanneld会添加一条路由规则,将数据包转向该节点。</p><p>为了让就要想办法把“原始 IP 包”加上一个目的 MAC 地址,封装成一个二层数据帧,在此时还需要知道目的 VTEP 设备的 MAC 地址。通过IP地址查找MAC地址是ARP表的功能,这里需要使用到的ARP表是flanneld进程在一个节点启动时自动添加在其他所有节点上的。</p><p>有了目的VTEP设备的MAC地址以后,Linux内核就可以开始二层封包工作了,封装出来的称为内部数据帧。此时还需要再把内部数据帧进一步封装成为宿主机网络里面的一个普通数据帧,这次封装出来的称为外部数据帧。</p><p>为了封装外部数据帧,Linux内核会在内部数据帧的前面加上一个特殊的VXLAN头,用来表示这个一个VXLAN要使用的数据帧。其中VXLAN头内部有个重要的标志叫做VNI,是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。在 Flannel 中,VNI 的默认值是 1。然后,Linux 内核会把这个数据帧封装进一个 UDP 包里发出去。</p><p>与UDP模式类似,在宿主机看来会以为自己的 flannel.1 设备只是在向另外一台宿主机的 flannel.1 设备,发起了一次普通的 UDP 链接。不会认为这个UDP包里面是一个完整的二层数据帧。</p><p>但是此时仍然不知道对应的宿主机地址,在这种场景下,flannel.1设备需要扮演一个网桥的角色,在二层网络上进行UDP包的转发。在Linux内核里面,网桥设备进行转发的依据,来自于一个叫作FDB的转发数据库。flannel.1网桥对应的 FDB 信息,也是 flanneld 进程负责维护的。</p><p>![img](Flannel XVLAN.jpg)</p><h5 id="host-gw模式"><a href="#host-gw模式" class="headerlink" title="host-gw模式"></a>host-gw模式</h5><p>host-gw模式的工作原理就是将每个Flannel子网的下一跳设置为该子网对应的宿主机的IP地址。Flannel子网和主机的信息都是保存在Etcd当中的,因此flanneld只需要WATCH这些数据的变化,然后实时更新路由表即可。</p><p>在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。但是Flannel的host-gw模式要求宿主机之间必须是二层连通的。</p><p>![img](Flannel host-gw.png)</p><h4 id="Calico"><a href="#Calico" class="headerlink" title="Calico"></a>Calico</h4><p>不同于Flannel通过Etcd和宿主机上的flanneld进程来维护路由信息的做法,Calico项目使用了BGP来自动的在整个集群中分发路由信息。</p><p>BGP是一种外部网关路由协议,通过在边界网关上利用TCP将各自的路由表传输给其他的边界网关。其他边界网关收到这些数据时,就会进行分析,将自己需要的信息添加到自己的路由表中。</p><p>Calico项目的架构有三个部分组成</p><ul><li>Calico的CNI插件。作为Calico与Kubernetes对接的部分。</li><li>Felix。是一个DaemonSet,负责在宿主机上插入路由规则以及维护Calico所需的网络设备。</li><li>BIRD。BGP的客户端,负责在集群中分发路由信息。</li></ul><p>Calico的CNI插件会为每个容器设置一个Veth Pair设备,然后把其中的一端放置到宿主机上。同时在每个宿主机上为每个容器的Veth Pair设备配置一条路由规则,用于接收传入的IP包。</p><p>有了这些Veth Pair设备以后,容器发出的IP包就会经过Veth Pair设备出现在宿主机上,然后宿主机网络栈就会根据路由规则的下一跳IP地址,把它们转发给正确的网关,接下来的流程就与 Flannel host-gw模式完全一致了。</p><p>其中的路由规则由Calico的Felix进程负责维护。这些路由规则信息,则是通过BIRD组件使用BGP传输而来的。</p><p><img data-src="Calico.jpg" alt="img"></p><p>Calico维护的网络在默认配置下,使用的是“Node-to-Node Mesh”模式。此时每台宿主机上的BGP client都需要跟其他所有结点的BGP client进行通信以便交换路由信息。随着节点数量的增长,连接数量会与N<sup>2</sup>的规模快速的增加给集群网络带来巨大的压力。</p><p>在更大规模的集群中,需要使用到Router Reflector的模式。在这种模式下Calico会指定一个或几个专门的节点来负责与所有的BGP建立连接,从而学习到全局的路由规则。其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由规则信息了。</p><p>同样的对于Calico来说,也要求集群宿主机之间是二层连通的。但是可以通过开启IPIP模式模式进行解决。</p><p>在IPIP模式下,会使用到一个tunl0设备将IP包发送出去。tunl0设备是一个IP隧道设备,IPIP驱动在IP包进入IP隧道设备以后,将IP包封装在一个宿主机网络的IP包中。接收方收到IP包后,使用IPIP驱动进行解包,从而拿到原始的IP包。然后原始的IP包就会经过路由规则和Veth Pair设备到达目的容器内部。</p><p>![img](Calico IPIP.jpg)</p><h2 id="Service实现原理"><a href="#Service实现原理" class="headerlink" title="Service实现原理"></a>Service实现原理</h2><p>Service是由kube-proxy组件加上iptables来共同实现的。</p><p>在开始的时候,kube-proxy确实是一个proxy,对于每个进来的连接,会连接到一个pod,这被称为userspace(用户空间)代理模式。后来性能更好的iptables模式代替了userspace模式。</p><p>当一个Service被提交给Kubernetes,kube-proxy就可以通过Service的Informer感知到这样一个Service对象的添加,之后再宿主机上创建一条iptables规则。因为这只是一条iptables规则,并没有真正的网络设备,所有无法ping通这个地址。</p><p>对于即将跳转到的规则,是一组随机模式的iptables链,随机转发的目的地是Service代理的多个Pod。这里也就是Service实现负载均衡的位置。</p><p>这样通过Service VIP的IP包1经过上述iptables处理之后,就已经变成了访问具体某一个后端Pod的IP包。这些iptables规则,是kube-proxy通过监听pod的变化事件,在宿主机上生成并维护的。</p>]]></content>
<summary type="html"><p>每个Pod都有自己唯一的IP地址,可以通过一个扁平的、非NAT网络和和其他Pod进行通信。</p>
<h2 id="同节点pod通信"><a href="#同节点pod通信" class="headerlink" title="同节点pod通信"></a>同节点pod通信<</summary>
<category term="Kubernetes" scheme="http://qian0817.top/categories/Kubernetes/"/>
<category term="Kubernetes" scheme="http://qian0817.top/tags/Kubernetes/"/>
</entry>
<entry>
<title>JVM锁优化</title>
<link href="http://qian0817.top/2021/02/05/JVM%E9%94%81%E4%BC%98%E5%8C%96/"/>
<id>http://qian0817.top/2021/02/05/JVM%E9%94%81%E4%BC%98%E5%8C%96/</id>
<published>2021-02-05T13:26:19.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<h2 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h2><p>互斥同步对性能最大的影响在于阻塞的实现,,因为挂起线程和恢复线程都需要转入到内核态进行完成。通过不放弃处理器的执行时间,让线程忙循坏,看看持有锁的线程能否很快就释放锁,就是自旋锁。</p><p>自旋锁虽然避免了线程切换的开销,但是需要占用处理器时间,所以如果锁被占用的时间很短,自旋等待的效果会比较好,反之只能白白的消耗处理器资源,带来性能的浪费。因此自选等待的时间必须要有一定的限度,如果超过自旋的限定次数仍然没有成功获得锁,那么就应当用传统的方式来挂起线程。自旋次数的默认值为10次,可以使用-XX:PreBlockSpin参数来自行修改。</p><p>在JDK6以后加入了对于自旋锁的优化,引入了自适应的自旋锁。自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者状态来决定。</p><h2 id="锁消除"><a href="#锁消除" class="headerlink" title="锁消除"></a>锁消除</h2><p>锁消除指的是虚拟机即时编译器在运行时,对一些代码有球同步但是被检测到不存在共享数据的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。</p><h2 id="锁粗化"><a href="#锁粗化" class="headerlink" title="锁粗化"></a>锁粗化</h2><p>在通常情况下,同步块的作用范围应该限制的尽量小,这样即使存在锁竞争,等待锁的线程也能尽可能地拿到锁。</p><p>但是如果一系列的操作对同一个对象反复进行加锁和解锁,那么即使没有线程竞争,频繁的进行互斥行为也会导致不必要的性能损耗。如果虚拟机探测到有这样一连串的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。</p><h2 id="轻量级锁"><a href="#轻量级锁" class="headerlink" title="轻量级锁"></a>轻量级锁</h2><p>轻量级锁的设计初衷在于在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。</p><p>虚拟机的对象头分为两个部分,其中第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄等,称为Mark Word,是实现轻量级锁和偏向锁的关键。</p><p><img data-src="%E5%AF%B9%E8%B1%A1%E5%A4%B4.jpg" alt="img"></p><p>在代码即将进入同步块的时候,如果此时同步对象没有被锁定,那么虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝。然后虚拟机使用CAS操作尝试把对象的Mark Word更新为指向Mark Record的指针。如果更新成功,即代表该线程拥有了这个对象的锁,并且对象的Mark Word的锁标志位转变为00,代表该线程处于轻量级锁定状态。如果更新失败了,那么说明至少有一条线程竞争获取该对象的锁。虚拟机会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,直接进入同步块继续执行就行了,否则膨胀为重量级锁,锁标志位变为10,此时Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。</p><p>解锁过程同样通过CAS进行完成,如果对象的Mark Word仍然指向线程的锁记录,那么就用CAS操作将当前的Mark Word和线程中复制的Mark Word替换回来。假如能够成功替换,那么整个同步过程就顺利完成了。如果替换失败,那么说明其他线程尝试获取过该锁,需要在释放锁的同时唤醒被挂起的线程。</p><h2 id="偏向锁"><a href="#偏向锁" class="headerlink" title="偏向锁"></a>偏向锁</h2><p>偏向锁的目的在于消除数据在无竞争的情况下的同步原语,进一步提升程序的运行性能。偏向锁的偏表示这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程所获取,则持有偏向锁的线程就永远不需要进行同步。</p><p>当锁对象第一次被线程获取的时候,虚拟机会将对象头的标志位设置为01,把偏向模式设置为1,表示进入偏向模式,同时使用CAS操作把获取到这个锁的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机都可以不再进行任何同步操作。</p><p>一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向,撤销后标志位恢复到未锁定或者轻量级锁定的状态,后续的同步状态就按照轻量级锁去执行。</p>]]></content>
<summary type="html"><h2 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h2><p>互斥同步对性能最大的影响在于阻塞的实现,,因为挂起线程和恢复线程都需要转入到内核态进行完成。通过不放弃处理器的执行时间,让线程忙循坏</summary>
<category term="JVM" scheme="http://qian0817.top/categories/JVM/"/>
<category term="JVM" scheme="http://qian0817.top/tags/JVM/"/>
</entry>
<entry>
<title>Kubernetes架构</title>
<link href="http://qian0817.top/2021/02/03/Kubernetes%E6%9E%B6%E6%9E%84/"/>
<id>http://qian0817.top/2021/02/03/Kubernetes%E6%9E%B6%E6%9E%84/</id>
<published>2021-02-03T02:52:28.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>Kubernetes集群中分为两个部分</p><ul><li>Kubernetes控制平面</li><li>工作节点</li></ul><p>控制平面负责存储管理集群状态、控制并使得整个集群正常运转,包含以下组件</p><ul><li>etcd分布式持久化存储</li><li>API服务器</li><li>调度器</li><li>控制器管理器</li></ul><p>工作节点上的组件包括</p><ul><li>Kubelet</li><li>Kubelet服务代理(kube-proxy)</li><li>容器运行时</li></ul><p>除此之外还包括几个附加组件,包含</p><ul><li>Kubernetes DNS 服务器</li><li>仪表盘</li><li>Ingress 仪表器</li><li>Heapster(容器集群监控)</li><li>容器网络接口插件</li></ul><p>Kubernetes系统组件间只能通过API服务器进行通信,API服务器是和etcd通信的唯一组件,其他组件不会直接和etcd进行通信,而是通过API服务器来修改集群状态。</p><p>控制平面的组件以及kube-proxy可以直接部署在系统上或者作为Pod来运行。Kubelet是唯一一个作为常规系统组件来运行的组件,被部署在master节点上,它把其他组件作为pod来运行。</p><h2 id="etcd"><a href="#etcd" class="headerlink" title="etcd"></a>etcd</h2><p>etcd是一个响应快、分布式的key-value存储,Kubernetes创建的所有对象(pod、ReplicationController等等)都持久化存储到etcd中。只有API服务器能够与etcd进行通信,所有组件都通过API服务器间接的读取和写入数据到etcd当中。etcd是Kubernetes存储集群状态和元数据唯一的地方。</p><p>为了保证高可用,常常会运行多个etcd实例,etcd使用raft算法保证一致性。</p><h2 id="API服务器"><a href="#API服务器" class="headerlink" title="API服务器"></a>API服务器</h2><p>Kubernetes API服务器作为中心组件,其他组件以及客户端(kubectl)都会调用API服务器。API服务器以restful API的形式提供了可以查询、修改集群状态的接口。API服务器负责将状态存储到etcd当中。</p><p>当接收到API请求后,API服务器内部会通过认证插件认证客户端、通过授权插件授权客户端、通过准入控制插件验证修改资源请求、验证资源以及持久化存储。</p><p>API服务器会启动控制器以及其他一些组件来监控已部署资源的变更。控制平面可以请求订阅资源被创建修改的通知,使得组件可以在集群元数据变化的时候能够执行任何需要做的任务。</p><p>客户端通过创建到API服务器的HTTP连接来监听变更,通过此连接,客户端会接收到监听对象的一系列变更通知。每当更新对象,服务器会把新版本对象发送到所有监听该对象的客户端。</p><h2 id="调度器"><a href="#调度器" class="headerlink" title="调度器"></a>调度器</h2><p>调度器利用API服务器的监听机制等待新创建的pod,然后给每个新的没有节点的pod去分配节点。</p><h2 id="控制器"><a href="#控制器" class="headerlink" title="控制器"></a>控制器</h2><p>控制器用于让系统的真实状态朝API服务器定义的期望的状态收敛,去完成具体资源的部署工作。</p><p>单个控制器、管理器进程当前组合了多个执行不同非冲突任务的控制器。这些控制器最终会被分解到不同的进程,控制器包括</p><ul><li>Replication控制器</li><li>ReplicaSet,DaemonSet和Job控制器</li><li>Deployment控制器</li><li>StatefulSet控制器</li><li>Node控制器</li><li>Service控制器</li><li>Endpoints控制器</li><li>Namespace控制器</li><li>PersistentVolume控制器</li><li>其他</li></ul><h2 id="kubelet"><a href="#kubelet" class="headerlink" title="kubelet"></a>kubelet</h2><p>Kubelet是负责所有运行在工作节点上的内容的组件。它的第一个任务就是在API服务器创建一个Node资源来注册该节点,然后持续监控API服务器是否是否把该节点分配给pod,然后启动pod容器,告知容器运行时从特定容器镜像运行容器。Kubelet随后监控运行的容器,向API服务器报告它们的状态、时间和资源消耗。</p><p>Kubelet也是运行容器存活探针的组件,当探针报错时会重启容器。当pod从API服务器删除时,Kubelet负责终止容器,并通知服务器pod已经被终止。</p><h2 id="Service-Proxy"><a href="#Service-Proxy" class="headerlink" title="Service Proxy"></a>Service Proxy</h2><p>每个工作节点也会运行kube-proxy,确保客户端可以通过Kubernetes API连接到定义的服务。kube-proxy确保对服务IP和端口的连接最终能够到达支持服务的某个pod处。如果有多个pod支撑一个服务,那么代理会发挥对pod的负载均衡作用。</p>]]></content>
<summary type="html"><p>Kubernetes集群中分为两个部分</p>
<ul>
<li>Kubernetes控制平面</li>
<li>工作节点</li>
</ul>
<p>控制平面负责存储管理集群状态、控制并使得整个集群正常运转,包含以下组件</p>
<ul>
<li>etcd分布式持久化存储</summary>
<category term="Kubernetes" scheme="http://qian0817.top/categories/Kubernetes/"/>
<category term="Kubernetes" scheme="http://qian0817.top/tags/Kubernetes/"/>
</entry>
<entry>
<title>Kubernetes调度器</title>
<link href="http://qian0817.top/2021/02/01/Kubernetes%E8%B0%83%E5%BA%A6%E5%99%A8/"/>
<id>http://qian0817.top/2021/02/01/Kubernetes%E8%B0%83%E5%BA%A6%E5%99%A8/</id>
<published>2021-02-01T13:21:23.000Z</published>
<updated>2022-04-06T13:33:42.389Z</updated>
<content type="html"><![CDATA[<p>在Kubernetes中,调度器的主要职责就是为一个新创建出来的Pod寻找一个合适的节点Node,其工作流程可以下图表示。</p><p><img data-src="%E8%B0%83%E5%BA%A6%E6%9C%BA%E5%88%B6.jpg" alt="img"></p><p>Kubernetes 的调度器由两个相互独立的控制循环组成。</p><p>第一个控制循环称为Informer Path,其主要目的在于启动一系列的Informer,用来监听Etcd中的Pod、Node、Service等对象的变化。例如当一个Pod被创建出来后,调度器会通过Pod Informer将待调度的Pod添加到调度队列之中。</p><p>第二个控制循环称为Scheduling Path。其作用是不断的从调度队列中出对一个Pod,调用Predicates算法进行过滤。通过这一步过滤得到的一组Node就是所有可以运行这个Pod的宿主机列表。接下来就可以调用Priorities算法为Node打分,分数从0到10,得分最高的Node就会作为本次调度的结果。在调度算法完成以后,调度器就会将Pod对象的nodeName字段修改为Node的名字,该步骤称为Bind。</p><h2 id="Predicates"><a href="#Predicates" class="headerlink" title="Predicates"></a>Predicates</h2><p>Predicates在调度过程中的作用就是从所有节点中去除一系列符合条件的节点。Kubernetes默认的调度策略有三种。</p><h3 id="GeneralPredicates"><a href="#GeneralPredicates" class="headerlink" title="GeneralPredicates"></a>GeneralPredicates</h3><p>GeneralPredicates负责最基础的调度策略,例如计算宿主机剩余的CPU和内存资源是否足够使用,检查宿主机的名字是否和Pod的spec.nodeName一致,申请的宿主机端口是否跟已经被使用的端口冲突,Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配。GeneralPredicates检查的是一个Pod能不能运行在Node上最基本的过滤条件。</p><h3 id="Volume-相关的过滤规则"><a href="#Volume-相关的过滤规则" class="headerlink" title="Volume 相关的过滤规则"></a>Volume 相关的过滤规则</h3><p>负责与容器持久化Volume相关的调度策略。例如多个 Pod 声明挂载的持久化 Volume 是否有冲突、一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目、持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配、Pod 对应的 PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配。</p><h3 id="宿主机相关的过滤规则"><a href="#宿主机相关的过滤规则" class="headerlink" title="宿主机相关的过滤规则"></a>宿主机相关的过滤规则</h3><p>检查Pod是否满足Node本身的一些条件,例如检查Pod 的Toleration字段与Node的Taint字段是否匹配,当前节点的内存是否足够。</p><h3 id="Pod-相关的过滤规则"><a href="#Pod-相关的过滤规则" class="headerlink" title="Pod 相关的过滤规则"></a>Pod 相关的过滤规则</h3><p>大部分与GeneralPredicates重合,特殊的在于还需检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。</p><p>上面四种类型的Predicates就构成了调度器确定一个 Node 可以运行待调度 Pod 的基本策略。</p><h2 id="Priorities"><a href="#Priorities" class="headerlink" title="Priorities"></a>Priorities</h2><p>Priorities阶段会根据不同Node的空闲资源,所有节点里各种资源分配最均衡,容器镜像是否存在以及容器镜像大小进行打分,打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。</p><h2 id="优先级和抢占机制"><a href="#优先级和抢占机制" class="headerlink" title="优先级和抢占机制"></a>优先级和抢占机制</h2><p>正常情况下,当一个Pod调度失败后,会被暂时搁置起来,直到Pod被更新或者集群状态发生变化,调度器才会对这个Pod进行重新调度。但是有时候希望一个高级别的Pod调度失败以后,该Pod并不会被搁置,而是挤走某个Node上的低优先级Pod,保证这个高优先级的Pod调度成功。</p><p>在调度其中维护着一个调度队列,当Pod拥有了优先级以后,高优先级的Pod可能会比低优先级的Pod提前出队,从而尽早地完成调度过程。</p><p>当一个高优先级的Pod调度失败的时候,调度器的抢占能力就会被出发,调度器试图从当前集群中寻找一个节点,使得当这个节点上的低优先级Pod被删除后,待调度的高优先级Pod能够被调度到该节点上,这个过程就被称为抢占。</p><p>在Kubernetes中,在调度队列的视线中有两个不同的队列,第一个队列叫做activeQ,存储在下一个调度周期需要调度的对象。第二个队列叫做unschedulableQ,专门存放调度失败的Pod。当一个 unschedulableQ 里的 Pod 被更新之后,调度器会自动把这个 Pod 移动到 activeQ 里,重新进行调度。</p><p>当发生调度失败时,该Pod就会被放到unschedulableQ中,触发调度器为抢占者寻找牺牲者的流程。首先会检查这次失败事件的原因,确认抢占是不是可以帮助抢占者找到一个新节点。如果确定抢占可以发生,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程。在模拟过程中,调度器会检查每一个节点,从节点的最低优先级的Pod开始,逐一删除这些Pod,每删除一个低优先级Pod,调度器就会检查抢占者能够运行在该Node上。调度器会在模拟的所有抢占结果中做出一个选择,判断抢占对整个系统的影响,找出最佳结果。</p><p>当得到了最佳的抢占结果后,调度器就会真正的开始抢占的操作,删除被抢占者,让抢占者在下一个调度周期重新进入调度流程,之后调度器就会通过正常的调度流程把抢占者调度成功。</p>]]></content>
<summary type="html"><p>在Kubernetes中,调度器的主要职责就是为一个新创建出来的Pod寻找一个合适的节点Node,其工作流程可以下图表示。</p>
<p><img data-src="%E8%B0%83%E5%BA%A6%E6%9C%BA%E5%88%B6.jpg" alt="img"><</summary>
<category term="Kubernetes" scheme="http://qian0817.top/categories/Kubernetes/"/>
<category term="Kubernetes" scheme="http://qian0817.top/tags/Kubernetes/"/>
</entry>
<entry>
<title>HTTP各版本特性</title>
<link href="http://qian0817.top/2021/01/30/HTTP%E5%90%84%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7/"/>
<id>http://qian0817.top/2021/01/30/HTTP%E5%90%84%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7/</id>
<published>2021-01-30T04:24:04.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>HTTP(超文本传输协议)定义了浏览器如何向服务器请求万维网文档以及服务器如何将文档传送给浏览器。</p><h2 id="HTTP-0-9"><a href="#HTTP-0-9" class="headerlink" title="HTTP 0.9"></a>HTTP 0.9</h2><p>HTTP 0.9在请求时不支持请求头,并且只支持GET方法。</p><h2 id="HTTP-1-0"><a href="#HTTP-1-0" class="headerlink" title="HTTP 1.0"></a>HTTP 1.0</h2><p>HTTP1.0 加入了以下几个内容</p><ul><li><p>在请求中加入了HTTP版本号信息</p></li><li><p>加入了请求头</p></li><li><p>增加了HTTP Status Code作为状态码</p></li></ul><p>但是HTTP1.0在性能上有很大的问题,每次请求都需要建立一个TCP连接,并且是串行执行请求。</p><h2 id="HTTP-1-1"><a href="#HTTP-1-1" class="headerlink" title="HTTP 1.1"></a>HTTP 1.1</h2><p>HTTP 1.1的变化主要有以下几个内容</p><ol><li>设置 keepalive 让 HTTP 重用 TCP 连接,省下了每次握手都要进行 TCP 3 次握手的重大开销。也就是 HTTP 长连接。</li><li>支持以流水线的形式进行请求发送,只要一个请求发出去,不必等到其回来就可以发送第二个请求出去。</li><li>增加了 cache control 机制。</li><li>增加 HOST 头,服务器可以知道你在请求哪个网站。</li><li>加入了 OPTIONS 方法,用于 CORS 应用。</li></ol><h2 id="HTTPS"><a href="#HTTPS" class="headerlink" title="HTTPS"></a>HTTPS</h2><p>HTTPS在传输过程中通过加密来保护数据安全,避免用户敏感信息被第三方获取。其过程如下</p><ol><li>客户端发起HTTPS请求,告诉客户端自己所支持的加密套件列表以及希望使用的TLS选项。</li><li>服务端选择客户端的一个加密套件,会有一套数字证书,就是一对公钥和私钥。</li><li>传输证书,将公钥信息传递给客户端。</li><li>客户端解析证书,会验证公钥是否有效。一般操作系统和浏览器都会内置一个知名证书颁发机构的名单,也可以通过手动指定证书的方式来添加证书。如果证书存在问题,那么会弹出警示框告诉用户该网站不安全。如果证书没有问题,那么生成一个随机值作为会话密钥。</li><li>将随机值通过公钥进行加密,然后将加密后的随机值传输给服务端</li><li>服务端用私钥解密,获取到的随机值就是会话密钥。双方使用该密钥进行对称加密通信。</li></ol><h2 id="HTTP-2-0"><a href="#HTTP-2-0" class="headerlink" title="HTTP 2.0"></a>HTTP 2.0</h2><p>HTTP 1.1虽然可以重用TCP连接,但是还是需要串行发送请求以保证其顺序。但是大量的网页请求中都是些资源类的东西,如果能够并行这些请求,那就会增加更大的网络吞吐和性能。同时HTTP1.1 在传输数据时使用的是文本的方式,借助zip压缩方式以减少网络带宽,但是会消耗前端和后端的CPU资源。</p><p>与HTTP2.0的新特性包括</p><ol><li>HTTP/2是一个二进制协议,增加了数据传输的效率。</li><li>可以在一个TCP连接中并发请求多个HTTP请求。</li><li>使用HPACK算法压缩头部信息,如果多个请求的头是相识的,协议会消除重复的部分。</li><li>允许服务端PUSH。例如请求X,服务端知道X依赖于Y,会同时将Y一起返回客户端。</li></ol><h2 id="HTTP-3-0"><a href="#HTTP-3-0" class="headerlink" title="HTTP 3.0"></a>HTTP 3.0</h2><p>虽然HTTP2.0能够多个HTTP请求复用一个TCP连接,但是底层的TCP协议不知道上层有多少个HTTP请求,一旦出现丢包,会造成所有的HTTP请求都必需等待丢了的包被重传回来,即使这个包不是本次HTTP请求的。</p><p>为了解决这一问题,HTTP 3.0 将HTTP的底层由TCP协议改为了UDP协议,并且使用了QUIC。因为UDP不需要管理顺序,不管丢包。QUIC的任务就是要有一个像TCP一样的丢包重传机制。在基于UDP以后,QUIC直接将TCP和TLS需要做的事情合并为了三次握手。因此QUIC可以将其视为一个在UDP之上的伪TCP +TLS +HTTP/2的多路复用的协议。</p>]]></content>
<summary type="html"><p>HTTP(超文本传输协议)定义了浏览器如何向服务器请求万维网文档以及服务器如何将文档传送给浏览器。</p>
<h2 id="HTTP-0-9"><a href="#HTTP-0-9" class="headerlink" title="HTTP 0.9"></a>HTTP </summary>
<category term="网络" scheme="http://qian0817.top/categories/%E7%BD%91%E7%BB%9C/"/>
<category term="网络" scheme="http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="HTTP" scheme="http://qian0817.top/tags/HTTP/"/>
</entry>
<entry>
<title>netty常用概念</title>
<link href="http://qian0817.top/2021/01/28/netty%E5%B8%B8%E7%94%A8%E6%A6%82%E5%BF%B5/"/>
<id>http://qian0817.top/2021/01/28/netty%E5%B8%B8%E7%94%A8%E6%A6%82%E5%BF%B5/</id>
<published>2021-01-28T06:32:27.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<h2 id="channel"><a href="#channel" class="headerlink" title="channel"></a>channel</h2><p>Channel接口是Netty对网络操作的抽象类,代表一个到实体(如硬件设备、网络套接字、文件等等)的开放连接,包含基本的读写操作。常用的Channel接口实现类包括NioServerSocketChannel和NioSocketChannel。这两个相当于BIO编程模型中的ServerSocket以及Socket两个概念。</p><h2 id="Future"><a href="#Future" class="headerlink" title="Future"></a>Future</h2><p>Netty是异步非阻塞的,所有的IO操作都是异步的,因此无法立即得到操作是否执行成功。通过Future提供了一种在操作完成时通知应用程序的方法,这个对象可以看作是一个异步操作的结果的占位符。</p><p>Netty提供了自己的实现ChannelFuture,其中addListener()方法可以注册一个ChannelFutureListener,当操作执行完成时,会自动触发返回结果。还可以通过sync()方法将异步的操作转换为同步的。</p><h2 id="ChannelHandler"><a href="#ChannelHandler" class="headerlink" title="ChannelHandler"></a>ChannelHandler</h2><p>Netty使用不同的事件来通知我们状态的改变或者是操作的状态。ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。</p><p>ChannelPipeline是ChannelHandler构成的链条,可以通过ChannelPipeline的add()方法添加ChannelHandler。</p><p>Netty也提供了预定义的ChannelHandler实现,包括实现HTTP,TLS的ChannelHandler。</p><h2 id="Eventloop"><a href="#Eventloop" class="headerlink" title="Eventloop"></a>Eventloop</h2><p>EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。</p><p>EventLoopGroup 包含多个 EventLoop。</p><p>当客户端连接服务端时,bossGroup会处理客户端连接请求。当客户端处理完成后,会将这个连接提交workerGroup来处理,然后workerGroup负责处理其 IO 相关操作。</p><h2 id="Bootstrap"><a href="#Bootstrap" class="headerlink" title="Bootstrap"></a>Bootstrap</h2><p>Bootstrap 是客户端的启动引导类,通过connect()方法连接到远程的主机和端口,作为TCP连接的客户端,也可以通过bind()方法绑定一个本地端口,作为UDP协议通信中的一端。</p><p>ServerBootstrap是服务端的启动引导类,使用bind放大绑定到本地端口上,等待客户端的连接。 </p><h2 id="线程模型"><a href="#线程模型" class="headerlink" title="线程模型"></a>线程模型</h2><p>Netty线程模型基于Reactor模式设计开发。Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的Handler处理。Netty主要靠NioEventLoopGroup线程池来实现具体的线程模型的。在实现服务端的时候,一般会初始化两个线程组,分别为bossGroup用于接受连接以及workerGroup负责具体的处理。其中bossGroup和workerGroup可以共用同一个EventLoopGroup。</p><h2 id="长连接与心跳机制"><a href="#长连接与心跳机制" class="headerlink" title="长连接与心跳机制"></a>长连接与心跳机制</h2><p>短连接指的是server与client端建立连接以后,读写完成之后就关闭掉连接,如果下次还需要继续发送消息,那么就要重新建立连接。</p><p>长连接说的就是 client 向 server 双方建立连接之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的消耗。</p><p>在保存长连接的过程中,如果遇到断网等情况,需要使用到心跳机制判断对方是否已经掉线。如果client与server之间在一定时间内没有数据交互时,会发送一个特殊的数据包给对方,当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文。当某一端收到心跳消息以后,就可以知道对方仍然在线,确保TCP连接的有效性。</p>]]></content>
<summary type="html">介绍Netty中的常用概念</summary>
<category term="Netty" scheme="http://qian0817.top/categories/Netty/"/>
<category term="网络" scheme="http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="Netty" scheme="http://qian0817.top/tags/Netty/"/>
</entry>
<entry>
<title>Kafka基本概念</title>
<link href="http://qian0817.top/2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/"/>
<id>http://qian0817.top/2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</id>
<published>2021-01-26T05:45:35.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。</p><p>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。</p><p>Producer将消息发送到Broker中去,Broker负责将收到的消息存储到磁盘当中,然后Consumer负责从Broker订阅并且消费消息。</p><p>Kafka还有两个重要的概念就是主题Topic以及分区Partition。Kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题,消费者负责订阅主题并进行消费。</p><p>主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题。同一主题下不同分区包含的消息是不同的。分区在存储层面可以看作一个可以追加的日志,消息在追加到分区日志文件的时候会分配一个特定的偏移量offset。offset是消息在分区中的唯一标识,Kafka通过offset来保证消息在分区中的顺序性。但是offset不跨越分区,因此Kafka只保证分区有序而不保证主题有序。</p><p>Kafka的分区可以分布在不同的服务器上,横跨多个Broker,以此来提供比单个Broker更强大的性能。当消息发送到Broker之前,会根据分区规则选择存储到哪个分区。</p><p>Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息,但在同一时刻副本之间的消息可能并非完全相同,副本之间是一主多从的关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本消息同步。副本处于不同的broker中,当leader副本出现故障时,从follower副本中重新选举新的leader副本继续对外提供服务。通过该机制使得Kafka集群中某个Broker失效时仍然能够对外提供服务。</p><p>分区中所有的副本称为AR(Assigned Replicas),所有与leader副本保持一定程度同步的副本组成ISR(In-Sync-Replicas)。当消费者发送消息时,会先发送到leader节点,然后follower节点才能从leader副本中拉取消息进行同步,同步期间follower副本相对于leader副本会有一定程度上的落后。与leader副本同步滞后过多的副本称为OSR(Out-of-Sync Replicas)。当leader副本发生故障时,只有ISR集合中的副本才有资格选举为新的leader。</p><p>Kafka的消费端也有容灾能力,Consumer使用拉(Pull)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。</p>]]></content>
<summary type="html"><p>一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。</p>
<p>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。</p>
<p>Producer将消息发送到Broker中去,Broker负责</summary>
<category term="Kafka" scheme="http://qian0817.top/categories/Kafka/"/>
<category term="Kafka" scheme="http://qian0817.top/tags/Kafka/"/>
</entry>
<entry>
<title>操作系统文件管理</title>
<link href="http://qian0817.top/2020/12/10/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86/"/>
<id>http://qian0817.top/2020/12/10/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86/</id>
<published>2020-12-10T06:16:05.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>文件系统是操作系统中文件的管理者。文件系统为上层用户提供文件抽象并实现文件访问所需要的接口,同时以特定的存储格式在下层存储设备中维护每个文件的数据以及元数据。操作系统将存储设备抽象为块设备,块设备上的的存储空间被划分为固定大小的存储块,每个存储块有一个地址被称为块号。</p><h2 id="基于inode的文件系统"><a href="#基于inode的文件系统" class="headerlink" title="基于inode的文件系统"></a>基于inode的文件系统</h2><h3 id="inode"><a href="#inode" class="headerlink" title="inode"></a>inode</h3><p>inode是“index node”的简写,即索引节点,记录了一个文件所对应的所有存储块号。inode中保存了三种存储指针,第一种指针为直接指针,其直接指向数据块,数据块中保存了文件数据;第二种指针为间接指针,指向了一个一级索引块,一级索引块中存放者指向数据块的指针。第三种指针为二级间接指针,指向一个二级索引块,二级索引块的每个指针都指向一个一级索引块,进而指向多个数据块。</p><p><img data-src="inode%E6%95%B0%E6%8D%AE%E7%B4%A2%E5%BC%95.png" alt="inode数据索引"></p><p>通过上述的组织方式可以看出,一个文件系统所支持的最大文件大小收到文件数据组织方式的限制,通过调整inode的设计可以改变其能够管理的最大文件大小。为了支持更大的文件,inode还可以启用三级或者四级间接指针。</p><p>inode还会记录该文件的其他元数据,例如文件模式、链接数、文件大小、文件访问时间等。</p><h3 id="存储结构"><a href="#存储结构" class="headerlink" title="存储结构"></a>存储结构</h3><p>文件系统以特定的存储布局将文件数据以及元数据保存在存储设备之中。为了高效的管理这些文件数据以及元数据,文件系统通常将存储空间划分为不同区域,分别用于不同功能。通常一个存储设备会被划分为超级块、块分配信息、inode分配信息、inode表以及文件数据块等区域。</p><p>超级块记录了整个文件系统的全局元数据,例如文件系统的版本、所管理空间大小、最后一次的挂载时间以及一些统计信息(例如最大inode数、空闲inode数、最大块数量、空闲块数量)。</p><p>块分配信息使用位图(bitmap)的格式来标记文件数据块区域中各个块的使用情况,如果为比特位为1则表示对应的数据块已经被分配和使用,若为0则代表对应的数据块空闲。inode分配信息与块分配信息类似,其对应的是每个inode的使用情况。</p><p>inode表以数组的形式存放整个文件系统的所有inode结构,文件系统使用inode在此表中的索引对不同inode进行区分。</p><p>剩余的存储空间为文件数据块区域,用于保存文件的数据。</p><h2 id="虚拟文件系统VFS"><a href="#虚拟文件系统VFS" class="headerlink" title="虚拟文件系统VFS"></a>虚拟文件系统VFS</h2><p>在一个计算机之中可能存在多个文件系统,需要使用到虚拟文件系统(VFS)对多种文件系统进行管理和协调,允许它们在同一个操作系统上共同工作。VFS定义了一系列内存数据结构,并要求底层的不同文件系统提供指定的方法,利用这些方法将不同文件系统的元数据统一转化为VFS的内存数据结构。</p>]]></content>
<summary type="html">介绍操作系统文件管理</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>操作系统同步原语</title>
<link href="http://qian0817.top/2020/12/05/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%90%8C%E6%AD%A5%E5%8E%9F%E8%AF%AD/"/>
<id>http://qian0817.top/2020/12/05/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%90%8C%E6%AD%A5%E5%8E%9F%E8%AF%AD/</id>
<published>2020-12-05T10:15:50.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>并行处理同一任务意味着对于共享资源的并发访问,为了保证共享资源状态的正确性,需要正确的在这些子任务中进行同步。为了正确的高效的解决这些同步问题,抽象出了同步原语这一概念。</p><h2 id="互斥锁"><a href="#互斥锁" class="headerlink" title="互斥锁"></a>互斥锁</h2><p>在任意时刻只允许最多一个线程访问的方式被称为互斥访问,保证互斥访问共享资源的代码区域被称为临界区。在同一时刻只能有一个线程可以执行临界区中的代码。因此在临界区中,一个线程可以安全的对共享资源进行操作。为了达到这样的目的,需要设计一个协议来保证临界区的互斥性,保证互斥访问、优先等待以及空闲让出。</p><h3 id="硬件实现:关闭中断"><a href="#硬件实现:关闭中断" class="headerlink" title="硬件实现:关闭中断"></a>硬件实现:关闭中断</h3><p>在单核条件中,关闭中断意味着当前执行的线程不能被其他线程抢占。因此如果在进入临界区之前关闭中断并在离开临界区之后在开启中断,那么就能避免当前执行临界区代码的线程被打断,保证了只有一个线程执行临界区,也就保证了互斥访问。</p><p>但是在多线程环境中,关闭中断的方式不再适用。即使关闭了所有核心的中断,也不能阻塞其他核心上正在运行的线程执行。</p><h3 id="软件实现:皮特森算法"><a href="#软件实现:皮特森算法" class="headerlink" title="软件实现:皮特森算法"></a>软件实现:皮特森算法</h3><p>皮特森算法是一种纯软件方法,可以解决两个线程的临界区问题。其代码如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> flag[<span class="number">2</span>] = {<span class="literal">false</span>,<span class="literal">false</span>};</span><br><span class="line"><span class="keyword">int</span> trun = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 线程1</span></span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> flag[<span class="number">0</span>] = <span class="literal">true</span>;</span><br><span class="line"> turn = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">while</span>(flag[<span class="number">1</span>] == <span class="literal">true</span> && turn == <span class="number">1</span>);</span><br><span class="line"> <span class="comment">// 临界区部分</span></span><br><span class="line"> flag[<span class="number">0</span>] = <span class="literal">false</span>;</span><br><span class="line"> <span class="comment">// 其他部分</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 线程2</span></span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> flag[<span class="number">1</span>] = <span class="literal">true</span>;</span><br><span class="line"> turn = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span>(flag[<span class="number">0</span>] == <span class="literal">true</span> && turn == <span class="number">0</span>);</span><br><span class="line"> <span class="comment">// 临界区部分</span></span><br><span class="line"> flag[<span class="number">1</span>] = <span class="literal">false</span>;</span><br><span class="line"> <span class="comment">// 其他部分</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中有两个重要的变量,一个是全局数组flag,其有两个布尔成员(flag[0]和flag[1]),分别代表线程1和线程2是否尝试进入临界区。第二个是全局变量turn。如果两个线程都申请进入临界区,那么turn将决定最终进入临界区的线程编号。线程在进入临界区之前,以线程1为例,必须满足flag[1]==false(表示线程1没有申请进入临界区)或者trun==0(表示turn决定线程1可以进入临界区),方能进入临界区。</p><p>皮特森算法要求访存严格按照程序顺序执行,然而现代CPU为了达到更好的性能允许访存操作乱序执行,因此皮森特算法在这些CPU上无法正常的进行工作。</p><h3 id="软硬件协同:原子操作"><a href="#软硬件协同:原子操作" class="headerlink" title="软硬件协同:原子操作"></a>软硬件协同:原子操作</h3><p>硬件提供的原子操作也可以解决临界区问题。原子操作指的是不可被打断的一个或者一系列的操作,最常见的是比较与置换(CAS),利用该特性可以实现互斥锁。</p><h4 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h4><p>自旋锁利用一个变量lock来表示锁的状态,在加锁时,线程会通过CAS判断lock是否空闲,如果空闲则上锁,否则一遍遍的进行重试。代码如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_init</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> *lock = <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> <span class="keyword">while</span> (atomic_CAS(lock,<span class="number">0</span>,<span class="number">1</span>) != <span class="number">0</span>);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> *lock = <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>自旋锁的实现简单,在竞争程度低的时候非常高效,依然广泛的运用在各种软件之中。</p><h2 id="条件变量"><a href="#条件变量" class="headerlink" title="条件变量"></a>条件变量</h2><p>条件变量可以让一个线程停止使用CPU并将自己挂起。当等待的条件满足时,其他线程会唤醒该挂起的线程让其继续执行。使用条件变量能够避免无谓的循环等待。</p><p>条件变量的实现如下,每个条件变量的结构体中都包含了一个wait_list,用于记录等待在该条件变量上的线程。线程在调用cond_wait挂起自己时,需要先将当前线程加入到等待队列之中,然后原子的挂起当前线程并且释放锁。这个操作需要利用到操作系统辅助完成,例如使用Linux系统下的futex机制。当该线程被其他线程唤醒以后,其应当再次获取锁并且进入临界区然后返回。cond_signal函数会利用操作系统提供的唤醒服务将线程唤醒。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">cond</span> {</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">thread</span> *<span class="title">wait_list</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_wait</span><span class="params">(struct cond *cond, struct lock *mutex)</span> </span>{</span><br><span class="line"> list_append(cond->wait_list,current_thread());</span><br><span class="line"> atomic_block_unlock(mutex); <span class="comment">// yuan'zi挂起</span></span><br><span class="line"> lock(mutex);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_signal</span><span class="params">(struct cond *cond, struct lock *mutex)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!list_empty(cond->wait_list)){</span><br><span class="line"> wakeup(list_remove(cond->wait_list));</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_broadcast</span><span class="params">(struct cond *cond)</span></span>{</span><br><span class="line"> <span class="keyword">while</span> (!list_empty(cond->wait_list)){</span><br><span class="line"> wakeup(list_remove(cond->wait_list));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="信号量"><a href="#信号量" class="headerlink" title="信号量"></a>信号量</h2><p>信号量在不同的线程之间充当信号灯,其根据剩余资源数量控制不同线程的执行或者等待。信号量定义了两个操作分别为P和V,因此信号量又被称为PV原语。一般使用wait和signal来表示信号量的P和V操作。信号量的语义如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait</span><span class="params">(<span class="keyword">int</span>* S)</span></span>{</span><br><span class="line"><span class="keyword">while</span>(*S <= <span class="number">0</span>); <span class="comment">// 循环忙等</span></span><br><span class="line"> *S = *S<span class="number">-1</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">signal</span><span class="params">(<span class="keyword">int</span>* S)</span></span>{</span><br><span class="line"> *S = *S + <span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面的代码仅仅是信号量的语义,并非信号量的真正实现。该代码存在两个问题。首先,可能存在多个线程同时更新共享信号量的情况导致更新丢失的问题。同时当一次资源被释放时,同时通知多个线程的效率比较低,让多余的线程挂起并且放弃CPU才是正确的选择。</p><p>实现一个正确的信号量需要组合使用互斥锁和条件变量。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sem</span> {</span></span><br><span class="line"> <span class="comment">/* </span></span><br><span class="line"><span class="comment"> 没有线程等待时,value 为整数或零,表示剩余的资源数量</span></span><br><span class="line"><span class="comment"> 有线程等待时,value 为负数,代表正在等待资源的线程数量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">int</span> value;</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 有线程等待时的可用资源数量,也代表应当唤醒的线程数量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">int</span> wakeup;</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">sem_lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">sem_cond</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait</span><span class="params">(struct sem *S)</span> </span>{</span><br><span class="line"> lock(&S->sem_lock);</span><br><span class="line"> S->value--;</span><br><span class="line"> <span class="keyword">if</span> (S->value < <span class="number">0</span>) { <span class="comment">// 代表没有空闲的资源</span></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> cond_wait(&S->sem_cond, &S->sem_lock);</span><br><span class="line"> } <span class="keyword">while</span> (S->wakeup == <span class="number">0</span>);</span><br><span class="line"> S->wakeup--;</span><br><span class="line"> }</span><br><span class="line"> unlock(&S->sem_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">signal</span><span class="params">(struct sem *S)</span> </span>{</span><br><span class="line"> lock(&S->sem_lock);</span><br><span class="line"> S->value++;</span><br><span class="line"> <span class="keyword">if</span> (S->value <= <span class="number">0</span>) {</span><br><span class="line"> S->wakeup++;</span><br><span class="line"> cond_signal(&S->sem_cond);</span><br><span class="line"> }</span><br><span class="line"> unlock(&S->sem_lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在调用wait操作时,该线程会在获取该信号量的互斥锁后,先将value数值减1。若其值大于等于0,说明有空闲的资源,wait操作成功。如果value小于0,那么需要使用到wakeup来判断是否与空闲的资源来消耗。如果wakeup也为0,需要将当前线程挂起等待。</p><p>信号量与互斥锁有着相似之处。如果信号量只允许其值在0和1之间变化时,那么可以把信号量当作互斥锁来使用。</p><h2 id="读写锁"><a href="#读写锁" class="headerlink" title="读写锁"></a>读写锁</h2><p>当一些线程仅仅需要读取而非修改共享资源时,就没有必要保证它们之间的互斥性。在这种情况下直接使用互斥锁不会带来任何正确性的问题,但是会削减读者之间的并行度,造成性能损失。读写锁就是为了解决这一问题而提出的同步原语。</p><p>读写锁允许多个读者同时进入读临界区,因此会带来倾向性的问题。假设在某一时刻,已经有一些读者在写临界区,那么此时有一个读者和一个写者同时申请进入临界区,那么是否允许该读者进入写临界区。如果允许读者进入写临界区,称为偏向读者的读写锁,否则称为偏向写者的读写锁。</p><p>下面的代码实现了偏向读写的读写锁。读者在上锁的时候需要使用到reader_lock来保证reader_cnt更新的原子性。如果是第一个读者还需要获取writer_lock保证读者和写者的互斥。同样如果读者在释放锁时发现自己是最后一个读者还需要释放write_lock取消对于写者的阻塞。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">rwlock</span> {</span></span><br><span class="line"> <span class="keyword">int</span> reader_cnt;</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">reader_lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">writer_lock</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->reader_lock);</span><br><span class="line"> lock->reader_cnt++;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">1</span>) { <span class="comment">// 第一个读者</span></span><br><span class="line"> lock(&lock->writer_lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->reader_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->reader_lock);</span><br><span class="line"> lock->reader_cnt--;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">0</span>) { <span class="comment">// 最后一个读者</span></span><br><span class="line"> unlock(&lock->writer_lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->reader_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->writer_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> unlock(&lock->writer_lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>偏向写者的读写锁实现更加复杂。在rwlock结构中需要添加一个布尔变量has_writer来表示当前是否有写者到达。当有写者获得锁时,会将has_writer设置为true。读者通过判断has_writer的值来决定是否需要等待前序的写者。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">rwlock</span> {</span></span><br><span class="line"> <span class="keyword">volatile</span> <span class="keyword">int</span> reader_cnt;</span><br><span class="line"> <span class="keyword">volatile</span> <span class="keyword">bool</span> has_writer;</span><br><span class="line"> <span class="comment">/* 读者写者需要共享锁 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">reader_cond</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">writer_cond</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> <span class="keyword">while</span> (lock->has_writer){</span><br><span class="line"> cond_wait(&lock->writer_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> lock->reader_cnt++;</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> lock->reader_cnt--;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">0</span>) { <span class="comment">// 最后一个读者</span></span><br><span class="line"> cond_signal(&lock->reader_cond);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> <span class="keyword">while</span> (lock->has_writer){</span><br><span class="line"> cond_wait(&lock->writer_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> lock->has_writer= <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">while</span> (lock->reader_cnt><span class="number">0</span>){</span><br><span class="line"> cond_wait(&lock->reader_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> lock->has_writer= <span class="literal">false</span>;</span><br><span class="line"> cond_broadcast(lock->writer_cond);</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>偏向读者的锁能够大幅的提升读者之间的并行度,但是会面临很高的写延迟,偏向写者的锁正好相反。因此在实际使用场景中需要根据具体需求选择合适的读写锁。</p><h2 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h2><p>死锁指的是一组线程中每一个线程都在等待其他线程释放资源而导致的无限等待。下列的代码就演示了死锁出现的场景。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">thread_a</span><span class="params">()</span></span>{</span><br><span class="line">lock(lock_a);</span><br><span class="line"> lock(lock_b);</span><br><span class="line"> <span class="comment">//do something</span></span><br><span class="line">unlock(lock_b);</span><br><span class="line"> unlock(lock_a);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">thread_b</span><span class="params">()</span></span>{</span><br><span class="line">lock(lock_b);</span><br><span class="line"> lock(lock_a);</span><br><span class="line"> <span class="comment">//do something</span></span><br><span class="line">unlock(lock_a);</span><br><span class="line"> unlock(lock_b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="产生原因"><a href="#产生原因" class="headerlink" title="产生原因"></a>产生原因</h3><p>导致死锁出现一共有四个必要条件</p><ol><li>互斥访问:一个共享资源在同一时刻只能被至多一个线程访问。</li><li>持有并等待:线程持有一些资源的同时在等待一些资源。</li><li>资源非抢占:一旦一个资源被持有,除非持有者主动放弃,否则其他竞争者得不到这个资源。</li><li>循环等待:在一系列线程中,例如T<sub>0</sub>,T<sub>1</sub>,T<sub>2</sub>。其中T<sub>0</sub>等待T<sub>1</sub>,T<sub>1</sub>等待T<sub>2</sub>,T<sub>2</sub>等待T<sub>0</sub>,形成了一个循环。</li></ol><h3 id="死锁检测与恢复"><a href="#死锁检测与恢复" class="headerlink" title="死锁检测与恢复"></a>死锁检测与恢复</h3><p>当死锁出现时,需要一个第三方来打破僵局,帮助进行死锁恢复。操作系统往往会扮演这个第三方的角色。</p><p>前面死锁的四个条件之中只有循环等待这一条件与实际运行状态相关,因此是检测死锁的关键。为了确认系统中是否存在循环等待,需要获取到系统中资源分配以及线程等待的相关信息,通过这两张表建立图,判断图中是否存在环,就可以进行死锁的检测。</p><p>再找到的环中的任意线程,终止该线程并释放其占有的所有资源就可以进行死锁的恢复。</p><h3 id="死锁预防"><a href="#死锁预防" class="headerlink" title="死锁预防"></a>死锁预防</h3><p>死锁预防指的是通过合理的资源分配算法,从源头上预防死锁。为了实现这个目标,需要保证死锁的四个条件不能被同时满足。</p><ol><li>避免互斥访问:</li><li>不允许持有并等待:可以让线程在真正开始操作前一次性申请所有的资源,一旦任意资源不能获取则释放掉所有资源并进行重试。该方法会导致资源利用率低,出现饥饿的情况。</li><li>允许资源被抢占:允许一个资源抢占其他线程已经占有的资源,难点在于如何保证被抢占的线程正确的进行恢复。</li><li>避免循环等待:可以要求线程按照一定的顺序进行资源获取,例如对锁进行编号,要求所有线程按照顺序获取锁,可以避免发生死锁。</li></ol><h3 id="死锁避免"><a href="#死锁避免" class="headerlink" title="死锁避免"></a>死锁避免</h3><p>死锁避免是通过系统运行时跟踪资源分配过程来避免出现死锁。银行家算法是一种具体的死锁避免算法。银行家算法通过模拟分配资源后的状态来判断系统是否处于安全状态。例如假设系统中存在M个资源,线程有N个,那么会有以下四个数据结构。</p><ol><li>全局可利用资源:Available[M]</li><li>每个线程的最大需求量:Max[N][M]</li><li>已分配资源数量:Allocation[N][M]</li><li>还需要资源数量:Need[N][M]</li></ol><p>银行家算法执行检查的流程如下:</p><ol><li>建立一个临时数组Available_temp,其初始值与数组Available数组一致,用于模拟系统可用资源数量。</li><li>找到系统当前剩余资源能够满足的线程,称为线程X,如果找不到对应的线程,说明系统处于非安全状态。</li><li>对于Available_temp中的所有成员m,都执行Available_temp[m]+=Allocation[x][m]。</li><li>遍历所有线程,查看是否还有未被标记执行结束的线程。如果有则回到第二步,否则代表系统处于安全状态。</li></ol>]]></content>
<summary type="html"><p>并行处理同一任务意味着对于共享资源的并发访问,为了保证共享资源状态的正确性,需要正确的在这些子任务中进行同步。为了正确的高效的解决这些同步问题,抽象出了同步原语这一概念。</p>
<h2 id="互斥锁"><a href="#互斥锁" class="headerlink" </summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>进程间通信方式</title>
<link href="http://qian0817.top/2020/12/03/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F/"/>
<id>http://qian0817.top/2020/12/03/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F/</id>
<published>2020-12-03T06:52:57.000Z</published>
<updated>2022-04-06T13:33:42.405Z</updated>
<content type="html"><![CDATA[<p>进程间通信是多进程协作的基础。在该过程中至少需要两个进程参与,这两方分别被称为发送者和接收者,也可以被称为调用者与被调用者、客户端和服务端。常见的进程间通信方式主要有七种。</p><h2 id="管道"><a href="#管道" class="headerlink" title="管道"></a>管道</h2><p>管道是两个进程间的一条通道,一个负责投递,一个负责接收。管道是单向的通信方式,在操作系统内核之中有缓冲区来缓冲消息,通信的数据是字节流,需要应用自己去解码。一个管道有且只有两端,一个负责输入,一个负责输出。</p><p>在UNIX中管道分为两类:命名管道和匿名管道。</p><p>匿名管道通过pipe的系统调用来创建,在创建的同时拿到两个读写的端口(文件描述符),然后只能使用这两个文件描述符来使用它。因此在这种情况下通常需要结合fork创建子进程的方式来建立父子进程间的连接。该方式适合父子进程这样的场景,不适合两个关系较远的进程。</p><p>命名管道由命令mkfifo进行创建,在创建过程中会指定一个全局的文件名来代指一个具体的管道。通过该方式任意两个进程通过一个相同的管道名就可以建立管道的通信连接。</p><h2 id="消息队列"><a href="#消息队列" class="headerlink" title="消息队列"></a>消息队列</h2><p>消息队列是以消息为数据抽象的通信方式。消息队列比较灵活,可以同时支持同时存在多个发送者和接收者。</p><p>当创建一个新的消息队列时,内核将从系统中分配一个队列数据结构,作为消息队列的内核对象。一旦一个队列被创建,那么除非内核重启或者该队列被主动删除,否则其数据会被一直保留。</p><h2 id="信号量"><a href="#信号量" class="headerlink" title="信号量"></a>信号量</h2><p>信号量一般来说仅有一个共享的整形计数器,该计数器由内核维护,对信号量的操作需要经过内核系统调用。</p><p>信号量的两个主要操作为P和V。P表示尝试一个操作,该操作的失败会将当前线程进入阻塞状态,直到其他进程执行了V操作。V操作表示增加,将该计数器加1,同时唤醒因P操作而陷入阻塞的进程。</p><p>信号量可以见到的满足进程间同步的要求,如果希望两个进程A和B,A执行完相关代码以后B再执行,那么可以使用信号量机制。</p><h2 id="共享内存"><a href="#共享内存" class="headerlink" title="共享内存"></a>共享内存</h2><p>共享内存为需要通信的进程建立共享区域,一旦共享区域完成建立,内核就不再需要参与进程间通信,通信的多方可以直接使用共享区域上的数据。</p><p>共享内存的机制就是允许多个进程在其所在的虚拟空间映射相同的物理内存页,从而进行通信。当进程不在希望共享内存时,也可以取消共享内核和虚拟内存之间的映射(之影响当前进程的映射,不影响其他仍在使用的共享内存的进程)。</p><h2 id="信号"><a href="#信号" class="headerlink" title="信号"></a>信号</h2><p>使用信号,一个进程可以随时的发送一个事件到特定的进程、线程或者进程组中。同时接受该事件的进程不需要阻塞等待该消息,内核会帮助其切换到对应的处理函数中响应信号事件,并在完成后恢复之前的上下文。</p><p>信号在Linux中应用十分广泛,例如在控制台中使用Ctrl+C终止一个程序时,其背后的逻辑就是发出一个SIGINT信号,导致默认信号处理函数结束了对应的进程。</p><h2 id="套接字"><a href="#套接字" class="headerlink" title="套接字"></a>套接字</h2><p>套接字是一种既可以用于本地也可以跨网络使用的通信机制,应用程序可以用相同的套接字接口来实现本地进程通信和跨机器的网络通信。</p><p>在套接字进程间通信的模式下,客户端进程通过一个特定的网络地址来找到想要调用的服务端进程。套接字可以使用不同的协议例如TCP和UDP来对通信进行控制。</p><p>进程间通信的对比如下</p><table><thead><tr><th>通信机制</th><th>数据抽象</th><th>参与者</th><th>方向</th><th>内核实现</th></tr></thead><tbody><tr><td>管道</td><td>字节流</td><td>两个进程</td><td>单向</td><td>FIFO的缓冲区</td></tr><tr><td>消息队列</td><td>消息</td><td>多进程</td><td>双向</td><td>队列的组织方式</td></tr><tr><td>信号量</td><td>计数器</td><td>多进程</td><td>双向</td><td>共享计数器</td></tr><tr><td>共享内存</td><td>内存区间</td><td>多进程</td><td>双向</td><td>共享的内存空间</td></tr><tr><td>信号</td><td>时间编号</td><td>多进程</td><td>单向</td><td>信号等待队列</td></tr><tr><td>套接字</td><td>数据报文</td><td>两个进程</td><td>双向</td><td>网络栈</td></tr></tbody></table>]]></content>
<summary type="html">介绍操作系统的进程间通信方式</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="进程" scheme="http://qian0817.top/tags/%E8%BF%9B%E7%A8%8B/"/>
</entry>
<entry>
<title>操作系统调度</title>
<link href="http://qian0817.top/2020/11/25/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%BA%A6/"/>
<id>http://qian0817.top/2020/11/25/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%BA%A6/</id>
<published>2020-11-25T09:34:46.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>操作系统会同时处理多个请求,但是硬件的运行资源是有限的。调度就是用来协调每个请求对于资源的使用方法。</p><p>一般调度器会通过维护运行队列来管理任务。运行队列不一定需要使用先进先出的队列,例如在Linux中使用的调度器就会使用红黑树来运行队列。当任务在执行时出发一定的条件(例如运行了指定的时间片以后或者发起了IO请求),那么会被加入到运行队列之中,等待再次被调度执行。</p><h2 id="调度指标"><a href="#调度指标" class="headerlink" title="调度指标"></a>调度指标</h2><p>在计算机处理的任务之中,有一类任务被称为批处理任务(比如复杂的科学计算等等),这些任务在执行时无需与用户进行交互,其主要的调度指标为任务处理的吞吐量,需要让吞吐量尽可能高,周转周期(即花费的时间)尽可能短。</p><p>还有一类任务为交互式任务,,需要在执行过程中对用户的操作做出响应。这类任务需要保证响应时间足够短使用户获得良好体验。</p><p>同时还有一类有截止时间要求的实时任务(例如车载操作系统需要检测汽车与外部物体的距离)。调度器必须让逝世人物在截止时间前完成,满足实时性的要求。</p><p>同时在移动设备中,调度器还需要尽可能地降低能耗。在通常情况下,调度器需要保证每个任务都有执行的机会,即满足公平性。调度器做出决策的时间也必须短,降低调度开销。</p><p>因此调度器在不同场景下有着不同的调度指标,需要在诸多方面做出权衡。</p><h2 id="调度机制"><a href="#调度机制" class="headerlink" title="调度机制"></a>调度机制</h2><p>进程可能处于不同的状态,这些状态包括新生、预备、运行、阻塞、终止。进程调度机制就负责在这些状态间进行转换。进程调度根据职责的不同,分为长期、中期、短期调度。</p><p>当用户像操作系统提交了执行某个程序的请求以后,系统可能不会立即处理该请求,这一决策由长期调度负责。长期调度用于限制系统中真正被短期调度管理的进程数量,避免短期调度的开销过大。只有当长期调度为某个程序创建了进程并且将状态设置为预备状态以后,才会由短期调度进一步管理该线程。</p><p>短期调度负责进程在预备状态、运行状态以及阻塞状态之间的转换。在短期调度决定执行某个进程以后,会将该进程从预备状态设置为运行状态。短期调度会使用适当的调度策略,尽可能地满足系统的调度指标。</p><p>中期调度实际上是换页机制的一部分。当系统中的进程已经占用了大量的内存资源以后,中期调度会挂起系统中被短期调度管理的进程,从而降低进程占用的内存总量。中期调度策略会根据策略来选择将要被挂起的进程,设置为对应的挂起状态,使其不再被调度执行。同时中期调度也会监控当前的内存使用情况,在适当的时机激活此前挂起的进程,使其可以重新被调度。</p><p>在引入了调度机制以后,进程转换的示意图变为如下所示。与之前的示意图相比,其区别在与引入了两个额外的进程状态:预备挂起状态以及挂起阻塞状态。</p><p><img data-src="%E5%9F%BA%E4%BA%8E%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6%E7%9A%84%E8%BF%9B%E7%A8%8B%E8%BD%AC%E6%8D%A2.png" alt="基于进程调度的进程转换"></p><p>在进程调度中,长期中期短期相互协作,分别以不同的目标对进程进行调度。长期调度负责决定是否将一个新的进程纳入调度管理。中期调度负责限制系统中进程的内存占用。短期调度细粒度的调度进程的执行,做出对应的调度策略。</p><h2 id="单核调度策略"><a href="#单核调度策略" class="headerlink" title="单核调度策略"></a>单核调度策略</h2><h3 id="经典调度"><a href="#经典调度" class="headerlink" title="经典调度"></a>经典调度</h3><h4 id="先到先得"><a href="#先到先得" class="headerlink" title="先到先得"></a>先到先得</h4><p>先到先得策略也叫先进先出策略。该策略在系统中维护一个运行队列,在执行任务的时候,选取队列中的第一个任务,将其移除队列并且执行,当任务阻塞时,将其放入运行队列队尾,当一个任务执行完后,再执行下一个任务。</p><p>该策略的弊端在于在长短任务的场景下对于短任务不友好,会导致短任务的周转时间与运行时间之比过大。同时其对IO密集型任务也不够友好,可能会导致I/O密集型任务长时间内无法执行。</p><h4 id="最短时间优先-最短完成时间任务优先"><a href="#最短时间优先-最短完成时间任务优先" class="headerlink" title="最短时间优先/最短完成时间任务优先"></a>最短时间优先/最短完成时间任务优先</h4><p>先到先得策略出现的问题提示我们需要让短任务立即进行执行。根据这一思想,得出的策略就是最短任务优先和最短完成时间任务优先。这两种策略的区别在于是否是抢占式的。前者必须等一个任务执行完成才能开始下一调度,后者会中断正在执行的进程转而执行其他任务。</p><p>这两种调度的弊端在于必须先预知任务的运行时间。同时前者调度策略其表现与任务到达时间点有着严重的依赖,后者调度策略会导致长任务饥饿。</p><h4 id="时间片轮转"><a href="#时间片轮转" class="headerlink" title="时间片轮转"></a>时间片轮转</h4><p>在时间片流转策略中需要为任务设置时间片,限定任务每次执行的时间。当任务执行完时间片以后,就切换到运行队列的下一个任务,这就是时间片轮转策略。由于时间片一般会设的足够小,所有任务都可以在一定时间内执行并且响应用户,从而将响应时间在可接受范围内。</p><p>该策略的弊端在于任务运行时间形似的场景下平均周转周期高,但是如果仅考虑响应时间,那么该策略会有很好的效果。</p><h3 id="优先级调度"><a href="#优先级调度" class="headerlink" title="优先级调度"></a>优先级调度</h3><p>如果操作系统可以分别交互式任务以及批处理任务,那么可以设置让交互式任务优先执行的调度策略,为此调度器引入了优先级的概念。通过优先级概念,调度器可以确定哪些任务应该优先执行。</p><h4 id="多级队列"><a href="#多级队列" class="headerlink" title="多级队列"></a>多级队列</h4><p>每个任务都会被分配预先设置好的优先级,每个优先级一个队列,任务会被存储在对应的优先级队列之中。如果优先级不同的任务同时处于预备状态,那么调度器会选择优先级较高的任务进行调度。一个任务必须等到所有优先级比它高的任务调度完才可以被调度。</p><p>该策略会导致低优先级任务饥饿的问题,很容易出现因为大量的高优先级任务不断地进入系统导致低优先级任务饥饿。</p><h4 id="多级反馈队列"><a href="#多级反馈队列" class="headerlink" title="多级反馈队列"></a>多级反馈队列</h4><p>在多级队列的基础上,多级反馈队列增加了动态设置任务优先级的策略。该策略会先对人物的运行时间进行评估,其中短任务会拥有更高的优先级。但是在真实世界中无法预知任务的完成时间,为此多级反馈队列策略会统计每个任务已经执行多长的时间,以此判断该任务是长任务还是短任务。当任务进入系统时,该策略会默认该任务为短任务,为其设置最高优先级。然后该策略会为每个任务队列设置任务的最大运行时间,如果任务在当前队列运行的总时间最终超过了队列允许的最大时间,那么会认为该任务是运行时间较长的任务,将该任务的优先级减一。</p><p>多级反馈队列策略能够动态的评估任务的运行时间,适配大致的任务优先级。同时在该策略中会定时的将所有任务的优先级提升至最高,保证不会出现任务饥饿的情况。该调度策略能够达到低平均周转周期的同时保证任务的响应时间,避免任务饥饿,在许多的操作系统中得到了应用,例如早期的Linux。</p><h2 id="多核调度策略"><a href="#多核调度策略" class="headerlink" title="多核调度策略"></a>多核调度策略</h2><p>相比于单核调度,多核调度策略还需要解决在哪个CPU上执行的问题。</p><h3 id="负载分担"><a href="#负载分担" class="headerlink" title="负载分担"></a>负载分担</h3><p>沿用单核跳读策略的思路,假设多核共享一个全局运行队列,当一个CPU核心需要调度任务时,从全局运行队列中获取一个任务作为下一个由他执行的任务。</p><p>该策略的优点在于设计实现简单,可以将多和调度策略问题规约为单核调度策略问题,同时不会出现CPU资源浪费的情况。但是也存在对应的问题,例如在多核处理器中,多核之间如何同步全局共享运行队列的信息或导致调度开销变得不可忽视。同时在多个CPU核心之间来回切换的开销很大,人物在不同CPU核心之间切换会导致诸如重新载入缓存、TLB刷新等问题。</p><h3 id="协同调度"><a href="#协同调度" class="headerlink" title="协同调度"></a>协同调度</h3><p>多线程为了充分利用多核处理器,通常会将工作量较大的任务切分为多个子任务,这些子任务中可能会存在依赖关系。协同调度的目的是尽可能让一组任务并行执行,避免调度器调度有依赖关系的两组任务。</p><p>群组调度是协同调度的一个经典策略,其基本思想是将关联任务设置为一组,并且以组为单位调度人物在多个CPU核心上执行,使他们的开始时间与结束时间尽可能保持接近。通过将任务以组为单位在多核处理器上进行调度,群组调度策略可以提升特定应用场景下的任务执行性能。</p><h3 id="两级调度"><a href="#两级调度" class="headerlink" title="两级调度"></a>两级调度</h3><p>由于全局调度器在不同CPU核心上切换执行会造成切换开销,为了减少开销,每个人物都尽可能只在一个CPU核心上进行调度。因此新的调度策略改为每个CPU核心都引入一个本地调度器,并用它管理对应核心上执行的任务。这种调度策略同时使用全局调度器和本地调度器,被称为两级调度。</p><p>当一个任务进入系统时,会根据系统的当前信息,决定该任务在哪个CPU核心上被执行。当一个任务被分配到给定的CPU核心时,将一直被该核心的本地调度器管理,不会迁移到其他CPU核心上运行。</p><h3 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h3><p>两级调度将任务绑定在特定的CPU核心上调度执行,避免了任务在多核间切换,有着良好的性能。但是两级调度没有任务在CPU核心间切换的机制,会导致多核间的负载不均匀。为了解决这一问题,引入了负载均衡策略,通过追踪每个CPU核心的负载情况,将处于高CPU负载核心管理的任务迁移到低负载CPU核心上,保证每个核心负载大致相同。</p>]]></content>
<summary type="html"><p>操作系统会同时处理多个请求,但是硬件的运行资源是有限的。调度就是用来协调每个请求对于资源的使用方法。</p>
<p>一般调度器会通过维护运行队列来管理任务。运行队列不一定需要使用先进先出的队列,例如在Linux中使用的调度器就会使用红黑树来运行队列。当任务在执行时出发一定的</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>进程线程与纤程</title>
<link href="http://qian0817.top/2020/11/21/%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%A4%E7%A8%8B/"/>
<id>http://qian0817.top/2020/11/21/%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%A4%E7%A8%8B/</id>
<published>2020-11-21T11:08:01.000Z</published>
<updated>2022-04-06T13:33:42.405Z</updated>
<content type="html"><![CDATA[<h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><h3 id="进程状态"><a href="#进程状态" class="headerlink" title="进程状态"></a>进程状态</h3><p>线程可以处于以下几种状态</p><ul><li>新生状态:表示一个线程刚被创建出来还未完成初始化,不能被调度执行。在初始化完成以后,进入预备状态。</li><li>预备状态:表示进程可以被调度运行,但还未被调度器选择。在被调度器选择以后进入运行状态。</li><li>运行状态:表示线程正在CPU上运行。当运行一段时间后,调度器可以将其重新放回调度队列之中,就会迁移至预备状态。进程结束时迁移至终止状态。当需要等待外部事件时,迁移至阻塞状态。</li><li>阻塞状态:表示线程需要等待外部事件,暂时无法被调度。当线程等待的外部事件完成以后,迁移至预备状态。</li><li>终止状态:表示进程已经完成执行,不会再被调度。</li></ul><p><img data-src="%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81.png" alt="进程状态"></p><h3 id="进程内存空间布局"><a href="#进程内存空间布局" class="headerlink" title="进程内存空间布局"></a>进程内存空间布局</h3><p>进程拥有独立的虚拟内存空间。包括以下几个部分</p><ul><li>用户栈:保存进程需要使用的各种临时数据。其扩展方向为自顶向下,栈底在高地址上。</li><li>代码库:进程执行所依赖的共享的代码库(比如libc),这些地址会映射在用户栈下方并标记为只读。</li><li>用户堆:管理进程动态分配的内存。其扩展方向为自底向上,堆顶在高地址上。</li><li>数据与代码段:保存全局变量的值以及进程执行所需要的代码,处于较低地址。</li><li>内核部分:每个进程的虚拟地址空间都映射了相同的地址空间,处于进程地址空间的最顶端。只有当程序进入内核态时才能访问内核内存。</li></ul><p><img data-src="%E8%BF%9B%E7%A8%8B%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4%E5%B8%83%E5%B1%80.png" alt="进程内存空间布局"></p><h3 id="进程控制块"><a href="#进程控制块" class="headerlink" title="进程控制块"></a>进程控制块</h3><p>在内核中,每一个进程都通过一个名为进程控制块的数据结构来保存相关状态。在Linux系统中PCB对应的数据结构为task_struct,定义在[<a href="https://github.com/torvalds/linux/blob/master/include/linux/sched.h">sched.h</a>]文件中。下面代码列举了几个重要字段。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">task_struct</span>{</span></span><br><span class="line"> <span class="comment">/* 进程状态 */</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="keyword">long</span>state;</span><br><span class="line"> <span class="comment">/* 虚拟内存状态 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">mm_struct</span> *<span class="title">mm</span>;</span></span><br><span class="line"> <span class="comment">/* 进程标识符 */</span></span><br><span class="line"> <span class="keyword">pid_t</span> pid;</span><br><span class="line"> <span class="comment">/* 进程组标识符 */</span></span><br><span class="line"> <span class="keyword">pid_t</span> tgid;</span><br><span class="line"> <span class="comment">/* 进程间关系 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">task_struct</span> __<span class="title">rcu</span> *<span class="title">parent</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">children</span>;</span></span><br><span class="line"> <span class="comment">/* 打开的文件 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">files_struct</span> *<span class="title">files</span>;</span></span><br><span class="line"> <span class="comment">/* 省略 */</span></span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="上下文切换"><a href="#上下文切换" class="headerlink" title="上下文切换"></a>上下文切换</h3><p>进程的上下文包括进程运行时的寄存器状态,能够用于保存和恢复一个进程在处理器上运行的状态。当操作系统需要切换当前执行的进程时,就会使用上下文切换机制,将起一个进程的寄存器保存到PCB之中,从而切换该线程执行。</p><p>下图展示了线程的上下文切换过程,当进程1用于中断或者系统调用进入内核以后,操作系统将进行上下文切换,保存将线程1的上下文保存到对应的PCB之中去,之后取出线程2对应的PCB的上下文,将其中的值恢复到寄存器中,最后操作系统回到用户态,继续进程2的运行。</p><p><img data-src="%E8%BF%9B%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2.png" alt="进程上下文切换"></p><h3 id="Linux进程操作"><a href="#Linux进程操作" class="headerlink" title="Linux进程操作"></a>Linux进程操作</h3><h4 id="进程创建fork"><a href="#进程创建fork" class="headerlink" title="进程创建fork"></a>进程创建fork</h4><p>在Linux中使用fork函数创建新进程,对于父进程fork函数返回当前进程的PID,子进程的返回值为0。以下是fork的一个示例。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><unistd.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">pid_t</span> rc = fork();</span><br><span class="line"> <span class="keyword">if</span> (rc < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"fork 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"当前为子进程, pid: "</span> << getpid() << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"当前为父进程, pid: "</span> << getpid() << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="进程执行exec"><a href="#进程执行exec" class="headerlink" title="进程执行exec"></a>进程执行exec</h4><p>exec由一系列接口组成,存在多个变种,功能最全面的是execve:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string"><unistd.h></span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">execve</span> <span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *__path, <span class="keyword">char</span> *<span class="keyword">const</span> __argv[], <span class="keyword">char</span> *<span class="keyword">const</span> __envp[])</span></span></span><br></pre></td></tr></table></figure><p>其共接受三个参数,第一个参数path是进程执行需要载入的可执行文件的路径,第二个参数argv是进程执行所需的参数,第三个参数envp是进程的环境变量,一般以键值对字符串的形式传入。</p><h4 id="进程管理"><a href="#进程管理" class="headerlink" title="进程管理"></a>进程管理</h4><p>在Linux中,进程都由fork创建,操作系统会以fork作为线索记录进程之间的关系,PID会记录每个进程的父进程和子进程。</p><p>处于进程树根部的是init进程,是操作系统创建的第一个进程,之后所有的进程都是由他直接或间接创建出来的。</p><p>为了方便应用程序进行管理,内核还定义了多个进程组合而成的进程组和会话。</p><p>进程组是进程的集合,由一个或多个进程组成。task_struct结构中的tgid即为进程对应的进程组标识符。如果进程想要脱离当前的进程组,可以通过调用setpgid修改自己所在的进程组。</p><p>会话是进程组的集合。由一个或多个进程组构成。会话将进程组根据执行状态分为了前台进程组、后台进程组。控制终端是会话与外界进行交互的窗口,负责接受由用户发来的输入。</p><h4 id="进程监控wait"><a href="#进程监控wait" class="headerlink" title="进程监控wait"></a>进程监控wait</h4><p>进程可以通过wait操作来对子进程进行监控。wait函数有很多变种,以waitpid为例:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string"><sts/wait.h></span></span></span><br><span class="line"><span class="function"><span class="keyword">__pid_t</span> <span class="title">waitpid</span> <span class="params">(<span class="keyword">__pid_t</span> __pid, <span class="keyword">int</span> *__stat_loc, <span class="keyword">int</span> __options)</span></span>;</span><br></pre></td></tr></table></figure><p>其中第一个参数表示需要等待的子进程,第二个参数用来表示子进程的状态,第三个参数表示选项。wait可以回收已经运行结束的子进程和释放资源。使用wait的一个示例如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><unistd.h></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><sys/wait.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">pid_t</span> rc = fork();</span><br><span class="line"> <span class="keyword">if</span> (rc < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"fork 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"子进程退出"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">int</span> status = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">if</span> (waitpid(rc, &status, <span class="number">0</span>) < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"waitpid 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (WIFEXITED(status)) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"父进程: 子进程退出"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果父进程没有调用wait操作,就算子进程已经终止,所占用的资源不会完全释放,这种进程被称为僵尸进程。内核会为僵尸进程保留其进程描述符和中止时信息,以便父进程在调用wait时监控子进程的状态。如果一个进程大量创建子进程且不调用wait,那么会导致僵尸进程占据可用的PID,使得后续的fork因为内核资源不足而失败。直到父线程退出,那么子进程将不会再被父进程使用,所有父进程创建的僵尸进程都会被内核的init进程调用wait进行回收。</p><h2 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h2><p>随着计算机技术的发展,进程显得过于笨重,主要体现在以下几点。</p><ol><li>创建进程的开销较大,需要完成创建独立的地址空间、载入数据和代码段、初始化堆等步骤。</li><li>进程用于独立的地址空间,在进程间进行数据的共享和同步比较麻烦</li></ol><p>因此提出了在进程之内添加可独立执行的单元,他们共享进程的地址空间,但又各自保存运行时的上下文状态,称为线程。</p><h3 id="线程地址空间布局"><a href="#线程地址空间布局" class="headerlink" title="线程地址空间布局"></a>线程地址空间布局</h3><p>进程为每个线程都准备了不同的栈,供它们存放临时的数据。进程除了栈只玩的所有其他区域都由该进程的所有线程共享,包括堆数据段以及代码段。</p><h3 id="线程控制块"><a href="#线程控制块" class="headerlink" title="线程控制块"></a>线程控制块</h3><p>与进程类似,线程也有自己的线程控制块(TCB),用于保存与自身相关的信息。</p><h2 id="纤程"><a href="#纤程" class="headerlink" title="纤程"></a>纤程</h2><p>随着计算机的发展,应用程序变得越来越复杂,在复杂。与操作系统调度器相比,应用程序对于线程的语义以及执行状态更加了解,因此可以做出更加的调度策略。此外用户态线程更加轻量级,创建与上下文切换的开销更小。在这样的背景下,操作系统开始提供对用户态线程(即纤程)的支持。</p><p>纤程的上下文切换的触发机制与内核态线程有较大不同。内核态线程的切换时强制的,因此称为抢占式多任务处理。而纤程在遇到阻塞时会放弃CPU,允许其他纤程的调度,这种调度方式称为合作式多任务处理。</p><p>除了操作系统以外,很多程序语言也提供了对于纤程的支持,方便应用程序创建和管理轻量级的上下文,从而支持更复杂的应用。一般将程序语言提供的纤程支持称为协程。</p><h3 id="POSIX的协程支持"><a href="#POSIX的协程支持" class="headerlink" title="POSIX的协程支持"></a>POSIX的协程支持</h3><p>POSIX中用来支持纤程的是ucontext.h中的接口。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string"><ucontext.h></span></span></span><br><span class="line"><span class="comment">/* 保存当前上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">getcontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 切换到另一个上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">setcontext</span> <span class="params">(<span class="keyword">const</span> <span class="keyword">ucontext_t</span> *__ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 切换上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">swapcontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__restrict __oucp,<span class="keyword">const</span> <span class="keyword">ucontext_t</span> *__restrict __ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 创建全新的上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">makecontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__ucp, <span class="keyword">void</span> (*__func) (<span class="keyword">void</span>),<span class="keyword">int</span> __argc, ...)</span></span>;</span><br></pre></td></tr></table></figure><p>下面的代码实现了生产者消费者模型。在主函数中使用makecontext创建了两个上下文context1和context2,分别用于调用produce和consume函数。此后程序通过setcontext不断在produce和consume函数之间跳转,不断重复的生产和消费。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><sys/wait.h></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><ucontext.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ucontext_t</span> context1, context2;</span><br><span class="line"><span class="keyword">int</span> current = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">produce</span><span class="params">()</span> </span>{</span><br><span class="line"> current++;</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"生产者当前值为"</span> << current << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> setcontext(&context2);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">consumer</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"消费者当前值为"</span> << current << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> setcontext(&context1);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">char</span> iteratorStack1[SIGSTKSZ];</span><br><span class="line"> <span class="keyword">char</span> iteratorStack2[SIGSTKSZ];</span><br><span class="line"> getcontext(&context1);</span><br><span class="line"> context1.uc_link = <span class="literal">nullptr</span>;</span><br><span class="line"> context1.uc_stack.ss_sp = iteratorStack1;</span><br><span class="line"> context1.uc_stack.ss_size = <span class="keyword">sizeof</span>(iteratorStack1);</span><br><span class="line"> makecontext(&context1, produce, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> getcontext(&context2);</span><br><span class="line"> context2.uc_link = <span class="literal">nullptr</span>;</span><br><span class="line"> context2.uc_stack.ss_sp = iteratorStack2;</span><br><span class="line"> context2.uc_stack.ss_size = <span class="keyword">sizeof</span>(iteratorStack2);</span><br><span class="line"> makecontext(&context2, consumer, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> setcontext(&context1);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过利用纤程,可以对生产者消费者这样的多个模块协作的场景进行有效的支持。在生产者完成任务以后,可以立即切换到消费者继续执行。由于该切换由用户态线程库完成,不需要操作系统调用,可以达到很好的性能。</p>]]></content>
<summary type="html">介绍进程线程与纤程</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="线程" scheme="http://qian0817.top/tags/%E7%BA%BF%E7%A8%8B/"/>
<category term="协程" scheme="http://qian0817.top/tags/%E5%8D%8F%E7%A8%8B/"/>
</entry>
<entry>
<title>操作系统内存管理</title>
<link href="http://qian0817.top/2020/11/21/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
<id>http://qian0817.top/2020/11/21/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/</id>
<published>2020-11-21T06:31:38.000Z</published>
<updated>2022-04-06T13:33:42.401Z</updated>
<content type="html"><![CDATA[<p>为了让不同的应用程序能够既高效又安全的共同使用内存,现代操作系统的普遍做法是在应用程序和物理内存之间加入一个新的抽象:虚拟内存。应用程序面向虚拟内存进行编写而不是面向物理内存进行编写,应用程序在运行时只能够使用虚拟内存,操作系统负责虚拟内存和物理内存之间的映射,CPU负责将虚拟内存翻译为物理内存。</p><p>CPU中的组件内存管理单元(MMU)负责虚拟地址到物理地址的转换。程序在CPU上运行时,它使用的虚拟地址都会由MMU进行翻译。当需要访问物理内存设备的时候,MMU翻译出的物理地址信息将会通过总线传递到相应的物理内存设备中去,从而完成相对应的物理内存读写请求。</p><h2 id="分段与分页机制"><a href="#分段与分页机制" class="headerlink" title="分段与分页机制"></a>分段与分页机制</h2><p>MMU将虚拟地址转换为物理地址的主要机制有两种,分别为分段机制和分页机制</p><h3 id="分段"><a href="#分段" class="headerlink" title="分段"></a>分段</h3><p>在分段机制下,操作系统以段为的形式管理物理内存。当CPU访问虚拟内存地址空间中的某一段时,MMU会通过查询段表得到该段对应的物理内存区域。虚拟地址由两个部分组成,第一个部分表示段号,标识着该虚拟地址属于整个虚拟地址空间的哪一段;第二部分表示段内地址,即相对于该段起始地址的偏移量。在段表中会存放着一个虚拟地址空间中每一个分段的信息,包括段起始地址和段长。在翻译虚拟地址的过程之中,MMU首先会通过段表基址寄存器来找到段表的位置,结合代翻译虚拟地址中的段号找到对应段的信息,然后取出该段的起始地址,加上段内地址就能得到最终的物理地址。段表中还存有段长信息来检查虚拟地址是否超过合法范围。</p><p><img data-src="%E5%88%86%E6%AE%B5.png" alt="分段机制"></p><p>在分段机制下,虚拟地址和物理地址都以段为单位进行分配。这种分配方式会导致物理内存的外部碎片,在段与段之间留下碎片空间,从而造成物理资源利用率的降低。例如一个3GB的内存被分为3段进行分配,第一段为0-1GB,第二段为1-2GB,第三段为2-3GB。如果第一段和第三段被释放,之后又分配一个2GB的段会出现分配失败的情况。</p><h3 id="分页"><a href="#分页" class="headerlink" title="分页"></a>分页</h3><p>分页机制时现代操作系统广泛采用的机制。其思想是将应用程序的虚拟地址空间和物理内存都划分为连续的等长的虚拟页(分段机制下位不同长度的段),由操作系统为每个应用程序构造虚拟内存到物理页的映射关系表(即页表)。虚拟地址由虚拟页号和页内偏移量两个部分组成。在翻译过程中,MMU首先会解析得到虚拟地址的虚拟页号,并通过虚拟页号去页表(存放于页表基地址寄存器)中取出对应的物理页号,然后与虚拟地址中的页内偏移量相加得到最终的物理地址。</p><h4 id="多级页表"><a href="#多级页表" class="headerlink" title="多级页表"></a>多级页表</h4><p>页表负责记录虚拟页到物理页的映射关系。对于63位的虚拟地址空间,假设页的大小为4KB,页表中每一项大小为8字节,那么一张页表的大小为2^64/4*8字节大小即32PB。为了对页表进行压缩,操作系统引入了多级页表的结构,用来满足虚拟内存在高效性上的要求。</p><p>在使用多级页表的时候,一个虚拟地址仍然包括虚拟页号以及业内偏移量,其中虚拟页号被划分为了多个部分,虚拟页号i对应着该虚拟地址在第i级页表中的索引。当任意一级的页表中的某一个条目为空时,该条目下一级的页表就不需要存在,因此多级页表的设计极大地减少了页表占用的空间大小。</p><p>以五层页表为例,通常在当MMU翻译一个虚拟地址的时候,会先根据页表基地址寄存器中的物理地址找到第0级页表页,将第0级的虚拟页号作为页表项索引,读取对应的页表项,该页表项中存储着下一级的页表项的物理地址,按照类似的方式依次读取第2级页表,第3级页表。MMU将在第3级页表项之中的页表项里面找到该虚拟地址对应的物理页号,结合虚拟地址中的业内偏移量获取最终的物理地址。</p><h4 id="TLB"><a href="#TLB" class="headerlink" title="TLB"></a>TLB</h4><p>多级页表能够压缩页表大小,但是会导致页表翻译时间的增加。为了减少地址翻译的访存次数,MMU引入了TLB机制来加速地址翻译的过程。在TLB之中缓存了虚拟页号到物理页号的映射关系,可以简单地将TLB简化成为一个键为虚拟页号值为物理页号的哈希表。</p><h4 id="换页与缺页异常"><a href="#换页与缺页异常" class="headerlink" title="换页与缺页异常"></a>换页与缺页异常</h4><p>换页机制是当物理内容容量不够的时候,操作系统将物理页的数据写入到磁盘之中,然后回收这些物理页并继续使用。当应用程序访问已分配但是未映射到物理内存的虚拟页时,就会触发缺页异常。此时操作系统会找到空闲的物理页,将之前写到磁盘上的数据重新加载到物理页之中,并在页表之中填写虚拟地址到这一物理页的映射,之后CPU回到发生缺页中断的地方继续运行。</p><h4 id="页替换策略"><a href="#页替换策略" class="headerlink" title="页替换策略"></a>页替换策略</h4><p>当分配物理页时,如果空闲的物理页已经用完,操作系统将会根据页替换策略分配一个物理页换出到磁盘中以让出空间。</p><p>OPT策略在选择被换出的页时,优先选择未来最长事件不会再访问的页。这种策略是最优的页替换策略,但是在实际中因为页访问策略取决于应用程序,操作系统无法预知应用程序的行为,所以很难进行实现。</p><p>FIFO策略优先选择最先换入的页进行换出,是最简单的页替换策略之一,因为简单其时间开销很低,但是在实际使用中常常表现不佳。</p><p>LRU策略优先选择最久未被访问的页。在实际实现中,需要精确的记录时间信息记录CPU访问了哪些物理页,其实现开销比较大。</p><p>时钟算法策略将换入物理内存的页号排成一个时钟的形状。该时钟有一个手臂只相信换入内存的后一个,同时为每一个页号维护一个访问标志位,每次需要选择换出页号时,该算法从针臂所指的页号开始检查,如果当前页号的访问位没有色湖之,就将该页替换,否则将访问位清空。如此重复直到找到一个访问位未被置上的页号。</p><h4 id="工作集模型"><a href="#工作集模型" class="headerlink" title="工作集模型"></a>工作集模型</h4><p>如果选择的页替换算法与真实的工作负载不匹配,那么会导致出现颠簸现象,造成性能损失。</p><p>工作集模型能够有效的避免颠簸现象的发生。工作集的定义是一个应用程序在时刻t的工作集W为它在时间区间[t-x,t]使用的内存页集合,也视为它在下一段x时间内会访问的内存页集合。该模型将应用程序的工作集要么全部都在物理内存之中,要么全部被换出,从而减少应用程序工作时发生换页的次数。</p><h2 id="虚拟内存"><a href="#虚拟内存" class="headerlink" title="虚拟内存"></a>虚拟内存</h2><p>虚拟内存使得应用程序能够拥有独立且连续的内存空间。其功能包括共享内存,写时拷贝,内存去重,内存压缩,内存大页等等。</p><p>共享内存允许同一个物理页在不同的应用程序之间共享,且能够同时看到对方修改的内容。共享内存的作用是让不同的应用程序相互通信和传递数据。</p><p>写时拷贝允许多个应用程序以只读的方式共享同一段物理内存。一旦某一个应用程序对该内存进行修改,就会发生缺页异常,操作系统会将对应的物理页重新拷贝一份重新映射给触发异常的应用程序,此后再恢复应用程序的执行。</p><p>基于写时拷贝技术,操作系统进一步设计了内存去重功能,在内存中扫描具有相同内容的物理页,找到映射到这些物理页的虚拟页,然后只保留其中一个物理页,将其他虚拟页以写时拷贝的方式映射到这个物理页。在Linux系统中该功能称为KSM。</p><p>当内存资源不足时,操作系统会使用一些最近不太会使用的内存页进行压缩,从而释放出更多的空闲内存。当应用程序需要进行访问的时候操作系统则将其解压,Linux中的zswap机制就使用到了内存压缩技术。</p>]]></content>
<summary type="html">介绍操作系统的内存管理机制</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="内存" scheme="http://qian0817.top/tags/%E5%86%85%E5%AD%98/"/>
</entry>
<entry>
<title>JVM中的垃圾回收算法</title>
<link href="http://qian0817.top/2020/10/03/JVM%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/"/>
<id>http://qian0817.top/2020/10/03/JVM%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/</id>
<published>2020-10-03T14:33:57.000Z</published>
<updated>2022-04-06T13:33:42.381Z</updated>
<content type="html"><![CDATA[<p>JVM内存运行时区域分为程序计数区、虚拟机栈、本地方法栈、堆区和方法区,其中前三个区域随着线程的生命周期而变化,对于这些区域不需要过多考虑内存回收的问题,当线程结束时,内存也就会被释放了。但是Java堆和方法区则需要使用到垃圾回收器去进行回收。</p><h2 id="判断对象是否存活"><a href="#判断对象是否存活" class="headerlink" title="判断对象是否存活"></a>判断对象是否存活</h2><p>在对垃圾进行回收时,需要先判断哪些对象存活,哪些对象需要进行回收。</p><h3 id="引用计数算法"><a href="#引用计数算法" class="headerlink" title="引用计数算法"></a>引用计数算法</h3><p>通过在对象中添加一个引用计数器,每有一个对象引用时计数器值加上一,当引用失效时,计数器值减去一。其原理简单且判定效率高,例如C++中的shared_ptr就是使用到了引用计数算法。但是引用计数算法无法解决循环引用的问题,例如以下的C++代码A和B最终都不会调用析构函数,因为A和B相互引用对方,即使A和B已经在最后是无法被访问到的。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><memory></span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><A> a;</span><br><span class="line"> ~B() {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"free B"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><B> b;</span><br><span class="line"> ~A() {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"free A"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><A> a = <span class="built_in">std</span>::make_shared<A>();</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><B> b = <span class="built_in">std</span>::make_shared<B>();</span><br><span class="line"> a->b = b;</span><br><span class="line"> b->a = a;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="可达性分析算法"><a href="#可达性分析算法" class="headerlink" title="可达性分析算法"></a>可达性分析算法</h3><p>Java使用的是可达性分析算法来判定对象是否存活。从GC ROOT最为起始节点,从这些节点开始根据引用关系进行搜索,如果某个对象到GC ROOT之间没有引用链相连,那么说明该对象是不可达的即需要回收的对象。</p><p>在JVM中以下对象可以作为GC ROOT</p><ul><li>虚拟机栈中引用的对象。</li><li>方法区中类静态属性引用的对象。</li><li>方法区中常量引用的对象。</li><li>本地方法栈中JNI引用的对象。</li><li>Java虚拟机内部的引用,例如基本数据类型对应的Class对象,常驻的异常对象(例如NullPointException),系统类加载器。</li><li>被同步锁持有的对象。</li><li>反应JVM内存情况的JMXBean、JVMTI中注册的回调,本地代码缓存。</li></ul><h2 id="引用类型"><a href="#引用类型" class="headerlink" title="引用类型"></a>引用类型</h2><p>Java将引用分为了强引用、软引用、弱引用和虚引用,这四种引用强度依次递减。</p><p>强引用指的是代码中普遍存在的引用赋值,例如<code> Object obj = new Object();</code>这种引用关系。只要强引用关系还存在,那么垃圾回收器就不会回收掉被引用的对象。</p><p>软引用描述那些还有用但是非必须的对象。在系统将要发生内存溢出异常之前,会把这些对象进行第二次回收。JDK提供了SoftReference类来实现软引用。</p><p>弱引用描述那些非必要对象,被弱引用关联的对象只能生存到下一次垃圾回收为止。JDK提供了WeakReference来实现弱引用。</p><p>虚引用是最弱的引用关系,虚引用在任何时候都可能会被垃圾回收器回收,主要作用是跟踪对象被垃圾回收的状态。JDK提供了PhantomReference来实现虚引用。</p><h3 id="finalize方法"><a href="#finalize方法" class="headerlink" title="finalize方法"></a>finalize方法</h3><p>在进行可达性分析以后,会判断需要进行回收的对象有无覆盖finalize()方法以及finalize()方法是否已经被调用。如果有必要执行finalize()方法,那么该对象会放置到F-Queue队列之中,由Finalizer线程去执行finalize()方法。JVM不一定会等到finalize()执行完成,同时finalize方法只会执行一次。</p><h2 id="分代收集理论"><a href="#分代收集理论" class="headerlink" title="分代收集理论"></a>分代收集理论</h2><p>大多数垃圾回收器都遵循分代收集理论进行设计,其建立在两个假说之上。</p><ol><li>绝大多数对象都是朝生夕灭的</li><li>熬过越多次垃圾收集过程的对象越难以消亡。</li></ol><p>在划分出不同的区域以后,垃圾回收器就可以每次只回收某一个部分。通常将会把Java堆分为新生代和老年代两个区域。针对不同的区域的特点,有着不同的垃圾收集算法,主要有标记-清除算法、标记-复制算法以及标记-整理算法。</p><h2 id="垃圾收集算法"><a href="#垃圾收集算法" class="headerlink" title="垃圾收集算法"></a>垃圾收集算法</h2><h3 id="标记-清除算法"><a href="#标记-清除算法" class="headerlink" title="标记-清除算法"></a>标记-清除算法</h3><p>标记-清除算法是最早出现的垃圾收集算法,其过程为:首先标记出所有需要回收的对象,在标记完成以后,统一回收掉所有未被标记的对象。标记-清除算法的缺点是执行效率不稳定和内存碎片化问题。</p><h3 id="标记-复制算法"><a href="#标记-复制算法" class="headerlink" title="标记-复制算法"></a>标记-复制算法</h3><p>标记-复制算法将内存容量分为两部分,当一部分的内存用完时,就将还存活的对象复制到另一块上去,将已使用过的内存空间一次性清理掉。其实现简单但是浪费内存空间。</p><p>通常使用这种算法来回收新生代,因为新生代中的大量对象无法熬过第一轮收集,所以不需要按照1:1的比例来划分两个区域的大小。通常将新生代区域分为一块较大的Eden区和两块较小Survivor区,每次内存分配只使用Eden区和一个Survivor区。当进行垃圾回收时,将Eden区和一块Survivor区的仍然存活对象复制到另一块Survivor区,然后清理掉Eden区和已使用的Survivor区空间。当Survivor空间不足以容纳存活的对象时,就需要依赖内存其他区域进行分配担保,通常直接将这些对象进入老年代。</p><h3 id="标记-整理算法"><a href="#标记-整理算法" class="headerlink" title="标记-整理算法"></a>标记-整理算法</h3><p>将所有存活的对象向内存空间的一端移动,直接清理掉边界以外的内存,成为标记-整理算法。其与标记-清除算法的区别在于是否需要移动对象。通常关注吞吐量的垃圾回收器(如Parallel Scavenge)使用的是标记整理算法,而关注停顿时间的收集器(如CMS)使用的是标记-清除算法。</p><h2 id="根节点枚举"><a href="#根节点枚举" class="headerlink" title="根节点枚举"></a>根节点枚举</h2><p>所有的收集器在根节点枚举这一步骤中都需要暂停用户线程,根节点的枚举必须在一个能够保障一致性的快照中进行。HotSpot使用一个OopMap的数据结构来保存哪些地方存在着对象引用,不必要一个不漏的检查完所有的执行上下文和全局的引用位置。当类加载完成时,HotSpot会把对象内什么偏移量上是什么类型的数据计算出来,这样收集器在扫描时可以知道栈里和寄存器里哪些位置是引用了。</p><h2 id="安全点和安全区域"><a href="#安全点和安全区域" class="headerlink" title="安全点和安全区域"></a>安全点和安全区域</h2><p>由于引用关系的变化,导致OopMap的内容变化的指令非常多,如果为每一个指令都生成对应的OopMap,那么会需要占用大量的额外存储中间。</p><p>因此HotSpot只在特定的位置记录这些信息,这些位置被称为安全点。因为安全点的设定,导致程序必须到达安全点之后才能够暂停,而不是在任何位置都能够停下来开始垃圾收集。虚拟机通常使用主动式中断的方式来让用户线程到达安全点,其思路为在需要中断线程的时候,设置一个标志位,各个线程在执行过程中轮询这个标志,一旦发现标志为真时主动运行到最近的安全点进行中断挂起。</p><p>如果在程序不执行的时候(例如线程处于Sleep状态或者Blocked状态),那么这时候线程无法走到安全点去中断挂起自己。因此需要引入安全区域解决这一问题。在安全区域中,引用关系不会发生变化,因此在该区域中的任意位置开始垃圾回收都是安全的。当用户线程进入安全区域中时,会先标识自己进入了安全区域,离开安全区域时,会检查虚拟机是否在垃圾收集过程中需要暂停用户线程的阶段。</p><h2 id="记忆集与卡表"><a href="#记忆集与卡表" class="headerlink" title="记忆集与卡表"></a>记忆集与卡表</h2><p>为了解决跨代引用(或者跨Region)导致的问题,垃圾收集器会建立名为记忆集的数据结构。记忆集用于记录从非收集区域指向收集区域的指针集合的数据结构。在垃圾收集的场景中,不需要记录全部的跨代引用对象,只需要判断某一块非收集区域是否存在指向收集区域的指针。</p><p>通常使用卡表来实现记忆集,其最简单的形式可以只是一个字节数组。字节数组上的每一个元素代表着其标识的内存区域中一块特定大小的内存块,成为卡页。如果一个卡页中存在跨代指针,那么将对应卡页的值标识为1,称为这个元素变脏,否则标识为0。其伪代码如下,使用的卡页为2的9次幂:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] = IS_DIRTY;</span><br></pre></td></tr></table></figure><p>在垃圾收集的过程中,只要筛选出卡页变脏的元素,就能知道哪些卡页内存中包含跨代指针,将其加入GC Roots中一并进行扫描。</p><p>在HotSpot虚拟机中使用写屏障来维护卡表状态,在引用赋值的前后产生一个环形的通知(类似于AOP),供程序执行额外的动作。虚拟机会为赋值操作生成相应的指令,在写屏障中增加了更新卡表操作。其简化逻辑如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">oop_field_store</span><span class="params">(oop* field, oop new_value)</span></span>{</span><br><span class="line"><span class="comment">// 引用字段赋值操作</span></span><br><span class="line"> *field = new_value;</span><br><span class="line"> <span class="comment">// 写后屏障,完成卡表更新操作</span></span><br><span class="line"> post_write_barrier(field, new_value);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在高并发的情况下,卡表可能存在伪共享的可能性,当多线程修改互相独立的变量时,如果这些变量同享同一个缓存行,就会彼此影响导致性能降低。为解决这一问题,可以先检查卡表标记,当该元素没有被标记过时才将其变脏,来尽量减少写卡表的操作。其伪代码如下所示:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] != DIRTY) </span><br><span class="line">CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] = DIRTY;</span><br></pre></td></tr></table></figure><h2 id="并发的可达性分析"><a href="#并发的可达性分析" class="headerlink" title="并发的可达性分析"></a>并发的可达性分析</h2><p>当用户线程与收集器并发工作时,收集器在标记对象是否为垃圾时,用户线程同时也会修改引用关系。此时会出现两种后果:</p><ol><li>将本应该消亡的对象标记为存货</li><li>将存活的对象标记为已消亡</li></ol><p>对于前一种情况,是可以容忍的,但是对于后一种情况会导致运行出错。当且仅当以下两个条件满足时,会产生对象消失的问题:</p><ol><li>赋值器插入了多条从已经被垃圾收集器扫描过的对象到未被垃圾收集器访问的对象的新引用</li><li>赋值器删除了全部从正在被访问过的对象到未被垃圾收集器访问过的对象的直接或间接引用</li></ol><p>因此要解决并发扫描时的对象消失问题,那么只需要破坏其中一个条件即可。对此有两种解决方案:增量更新和原始快照(SATB)。</p><p>增量更新当出现第一种情况时,将新插入的引用记录下来,等扫描结束以后,以这些被记录下来的对象为根,重新扫描一次。</p><p>原始快照当出现第二种情况时,就将被删除的引用记录下来。在扫描结束以后,以这些记录的引用对象为根,重新扫描一次。</p>]]></content>
<summary type="html"><p>JVM内存运行时区域分为程序计数区、虚拟机栈、本地方法栈、堆区和方法区,其中前三个区域随着线程的生命周期而变化,对于这些区域不需要过多考虑内存回收的问题,当线程结束时,内存也就会被释放了。但是Java堆和方法区则需要使用到垃圾回收器去进行回收。</p>
<h2 id="判断</summary>
<category term="JVM" scheme="http://qian0817.top/categories/JVM/"/>
<category term="JVM" scheme="http://qian0817.top/tags/JVM/"/>
</entry>
<entry>
<title>Redis中的哨兵机制</title>
<link href="http://qian0817.top/2020/09/27/Redis%E4%B8%AD%E7%9A%84%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6/"/>
<id>http://qian0817.top/2020/09/27/Redis%E4%B8%AD%E7%9A%84%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6/</id>
<published>2020-09-27T06:25:45.000Z</published>
<updated>2022-04-06T13:33:42.393Z</updated>
<content type="html"><![CDATA[<p> 哨兵是Redis高可用的解决方案,由Sentinel实例组成的哨兵系统可以监视主服务器和从服务器。当被监视的主服务器处于下线状态时,自动将某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器处理命令请求。</p><h2 id="启动Sentinel节点"><a href="#启动Sentinel节点" class="headerlink" title="启动Sentinel节点"></a>启动Sentinel节点</h2><p>使用如下的命令可以启动Sentinel节点。其中./sentinel.conf代表sentinel的配置文件位置。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">redis-sentinel ./sentinel.conf</span><br><span class="line"><span class="meta">#</span><span class="bash"> 或</span></span><br><span class="line">redis-server ./sentinel.conf --sentinel</span><br></pre></td></tr></table></figure><p>当一个Sentinel节点启动时,会执行以下的步骤:</p><h3 id="初始化服务器"><a href="#初始化服务器" class="headerlink" title="初始化服务器"></a>初始化服务器</h3><p>Redis的Sentinel节点本质就是一个Redis服务器,启动Sentinel节点的第一步就是初始化一个Redis服务器。与普通的Redis服务器不同,Sentinel节点不会载入RDB和AOF文件。初始化完成以后,将普通服务器的代码替换成为Sentinel节点的代码,替换的部分包括服务器运行的端口、可执行的命令表等等。</p><h3 id="初始化sentinel状态"><a href="#初始化sentinel状态" class="headerlink" title="初始化sentinel状态"></a>初始化sentinel状态</h3><p>接下来,服务器会初始化一个sentinelState结构,保存所有和Sentinel功能相关的状态。其结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sentinelState</span> {</span></span><br><span class="line"> <span class="comment">// 当前纪元</span></span><br><span class="line"> <span class="keyword">uint64_t</span> current_epoch;</span><br><span class="line"> <span class="comment">// 保存了所有被这个 sentinel 监视的主服务器</span></span><br><span class="line"> <span class="comment">// 字典的键是主服务器的名字</span></span><br><span class="line"> <span class="comment">// 字典的值则是一个指向 sentinelRedisInstance 结构的指针</span></span><br><span class="line"> dict *masters;</span><br><span class="line"> <span class="comment">// 是否进入了 TILT 模式?</span></span><br><span class="line"> <span class="keyword">int</span> tilt;</span><br><span class="line"> <span class="comment">// 目前正在执行的脚本的数量</span></span><br><span class="line"> <span class="keyword">int</span> running_scripts;</span><br><span class="line"> <span class="comment">// 进入 TILT 模式的时间</span></span><br><span class="line"> <span class="keyword">mstime_t</span> tilt_start_time;</span><br><span class="line"> <span class="comment">// 最后一次执行时间处理器的时间</span></span><br><span class="line"> <span class="keyword">mstime_t</span> previous_time;</span><br><span class="line"> <span class="comment">// 一个 FIFO 队列,包含了所有需要执行的用户脚本</span></span><br><span class="line"> <span class="built_in">list</span> *scripts_queue;</span><br><span class="line">} sentinel;</span><br></pre></td></tr></table></figure><p>其中masters字典保存了所有所有被Sentinel节点监视的主服务器的相关信息。其键为监视主服务器的名字,值是一个sentinelRedisInstance结构的指针,代表一个被Sentinel节点监视的Redis服务器实例,可以是主服务器、从服务器或者另外一个Sentinel节点。sentinelRedisInstance结构中会保存实例的IP地址、端口号、实例运行ID等信息。</p><h3 id="创建网络连接"><a href="#创建网络连接" class="headerlink" title="创建网络连接"></a>创建网络连接</h3><p>初始化Sentinel节点的最后一步就是创建连向被监视主服务器的网络连接。</p><p>对于每个被监视的主服务器来说,Sentinel节点会创建两个连向主服务器的异步网络连接。</p><ul><li>命令连接:用于向主服务发送命令,并接受命令回复</li><li>订阅连接:用于订阅主服务器的__sentine__:hello频道</li></ul><p>通过这两个连接,就可以来与被监视主服务器进行通信。</p><h2 id="获取服务器信息"><a href="#获取服务器信息" class="headerlink" title="获取服务器信息"></a>获取服务器信息</h2><h3 id="获取主服务器信息"><a href="#获取主服务器信息" class="headerlink" title="获取主服务器信息"></a>获取主服务器信息</h3><p>Sentinel节点会以每10秒1次的频率,向被监视的主服务器发送INFO信息,通过主服务器回复Sentinel节点可以获取主服务器本身的信息和主服务器属下所有从服务器的信息。通过主服务器传递的从服务器信息,Sentinel无需用户提供服务器的地址信息就可以自动发现从服务器。</p><p>根据主服务器传递的信息,Sentinel会对主服务器的实例结构进行更新。例如主服务器重启以后,运行ID就会和之前保存的ID不同,就需要更新运行ID信息。</p><h3 id="获取从服务器信息"><a href="#获取从服务器信息" class="headerlink" title="获取从服务器信息"></a>获取从服务器信息</h3><p>Sentinel节点在分析INFO命令中的从服务器信息时,会检查从服务器的实例结构是否已经存在于slaves字典之中。如果对应的实例结构已经存在,那么就对已保存的实例结构进行更新。否则说明这个从服务器是新发现的从服务器,会在slaves字典之中为从服务器创建一个新的实例结构。</p><p>当Sentinel发现有新的从服务器出现时,除了会创建对应的实例结构外,还会创建到从服务器的命令连接以及订阅连接。在创建连接之后,Sentinel会以每10秒1次的频率,向从服务器发送INFO命令。根据INFO命令的回复,Sentinel会获得以下的信息:</p><ul><li>从服务器运行ID</li><li>从服务器角色</li><li>主服务器IP和端口号</li><li>主从服务器连接状态</li><li>从服务器优先级</li><li>从服务器的复制偏移量 </li></ul><h2 id="发送与接受信息"><a href="#发送与接受信息" class="headerlink" title="发送与接受信息"></a>发送与接受信息</h2><p>Sentinel会以两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送一条信息,信息的内容包括:</p><ul><li>Sentinel的IP地址</li><li>Sentinel的端口号</li><li>Sentinel的运行ID</li><li>Sentinel的配置纪元</li><li>主服务器的名字</li><li>主服务器的IP地址</li><li>主服务器的端口号</li><li>主服务器当前的配置纪元</li></ul><p>当Sentinel与服务器建立起订阅连接以后,Sentinel会通过订阅连接,向服务器发送以下命令</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SUBSCRIBE __sentinel__:hello</span><br></pre></td></tr></table></figure><p>对于监视一个同服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,用于更新其他Sentinel对于发送信息Sentinel的认知和其他Senttinel对于被监视服务器的认知。</p><p>例如sentinel1、sentinel2、sentinel3都在监视同一个服务器,当sentinel1向服务器的__sentinel__:hello频道发送一条信息时,所有的sentinel都会收到这条信息。</p><p>当Sentinel从__sentinel__:hello频道接收到一条信息时,会对该信息进行分析,提取从中的Sentinel的运行ID,如果与自己的运行ID相同,说明是自己发送的,不做进一步处理。否则说明是其他的Sentinel发送的,更新信息中的各个参数。如果在此过程中发现了新的Sentinel节点,会创建连向其他Sentinel的命令连接,使得最终监视同一服务器的Sentinel形成相互连接的网络。</p><h2 id="检测下线状态"><a href="#检测下线状态" class="headerlink" title="检测下线状态"></a>检测下线状态</h2><p>Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器,从服务器,Sentinel节点)发送PING命令。通过PING命令的回复来判断实例是否在线。如果一个实例在一定时间内连续向Sentinel返回无效回复,那么会将该实例状态设置为主观下线状态。</p><p>当Sentinel将一个主服务器判断为主观下线之后,为了确认主服务是否真的下线,同样会对监视这一主服务器的Sentinel进行询问。如果从其他Sentinel中接收到足够多的下线判断后,会将服务器判定为客观下线,对主服务器进行故障转移操作。</p><h2 id="选举领头"><a href="#选举领头" class="headerlink" title="选举领头"></a>选举领头</h2><p>当一个主服务器被判断为客观下线之后,监视这个下线服务器的Sentinel会进行协商,选举出一个领头Sentinel。由该领头Sentinel对下线服务器进行故障转移操作。选举领头的方式采用的是Raft算法。</p><h2 id="故障转移"><a href="#故障转移" class="headerlink" title="故障转移"></a>故障转移</h2><p>在选举出领头Sentinel之后,领头Sentinel会对已下线的主服务器进行故障转移操作,其步骤如下:</p><h3 id="选出主服务器"><a href="#选出主服务器" class="headerlink" title="选出主服务器"></a>选出主服务器</h3><p>在主服务器下属的的所有从服务器中,选择出一个状态良好并且数据完整的从服务器,向这个从服务器发送SLAVE no one命令,将从服务器转换为主服务器。在发送完成以后,领头Sentinel会向被升级的从服务器发送INFO命令,观察其角色信息。如果返回的角色信息变为了master,则进行下一步操作。</p><h3 id="修改从服务器的复制目标"><a href="#修改从服务器的复制目标" class="headerlink" title="修改从服务器的复制目标"></a>修改从服务器的复制目标</h3><p>当新的主服务器出现以后,通过向所有的从服务器发送SLAVEOF命令,让所有的从服务器去复制新的主服务器。</p><h3 id="将旧的主服务器变为从服务器"><a href="#将旧的主服务器变为从服务器" class="headerlink" title="将旧的主服务器变为从服务器"></a>将旧的主服务器变为从服务器</h3><p>当旧的主服务器重新上线之后,Sentinel会向其发送SLAVEOF命令,让它成为新的主服务器的从服务器。</p>]]></content>
<summary type="html">介绍Redis中的哨兵机制原理</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
</feed>