-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
732 lines (351 loc) · 478 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>vscode-golang-远程调试</title>
<link href="/2023/08/25/vscode-golang-remote-debug/"/>
<url>/2023/08/25/vscode-golang-remote-debug/</url>
<content type="html"><![CDATA[<p>开发中进行调试能够帮助我们快速定位问题,同时也可以帮助我们梳理代码逻辑。相比日志输出,调试更能够在项目早期提升开发效率。在使用 golang 开发的前两年,我主要使用 goland 进行开发,goland 的调试功能是其相比于 vscode 的一大优势。在开发过程中,我们所开发项目通常不会运行本地或者开发机,可能是运行在一个远程集群中,依赖环境配置以及其他服务,goland 能够很方便的实现在本地开发,然后将程序拷贝到服务器,并对其进行调试。</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/vscode-golang-remote-debug/2023-08-25-08-58-25.png" alt></p><p>golang 只需要将远程服务器配置位一个 target,就可实现本地开发类似的效果。</p><p>本文将介绍 vscode 借助其开发调试和任务特性,配置 launch.json 和 task.jsoan 文件实现<strong>golang 程序的一键远程调试的功能</strong>,执行调试,vscode 帮助完成所有以下步骤。</p><ol><li>编译程序,下面使用<code>uvmt-agent</code>进行演示。</li><li>将编译好的程序和调试工具 dlv 拷贝到需要调试的机器上。</li><li>在需要调试的机器上运行 dlv 通过 DAP 运行 headless server。</li><li>vscode 连接远程机器上调试服务,开始调试。</li></ol><h2 id="task-配置"><a href="#task-配置" class="headerlink" title="task 配置"></a>task 配置</h2><p>vscode 提供 task 功能,用户可以定义一些常用的 task 任务,通过命令快速执行。vscode 首先会扫描工作区,自动生成一些 task,用户的自定义 task 最终会保存在<code>.vscode/task.json</code>文件中. 我们通过以下配置实现上述 1、2、3 步骤。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"version"</span>: <span class="string">"2.0.0"</span>,</span><br><span class="line"> <span class="attr">"tasks"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 1. 编译程序,需要指定-gcflags=all="-N -l"禁用编译优化和內连</span></span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"shell"</span>, <span class="comment">// 可以使用通用shell类型,也可以使用go类型</span></span><br><span class="line"> <span class="attr">"label"</span>: <span class="string">"go: build uvmt-agent: disable Inlining and optimizations"</span>, <span class="comment">// lable是任务唯一标识</span></span><br><span class="line"> <span class="attr">"command"</span>: <span class="string">"go"</span>,</span><br><span class="line"> <span class="attr">"args"</span>: [</span><br><span class="line"> <span class="string">"build"</span>,</span><br><span class="line"> <span class="string">"-o"</span>,</span><br><span class="line"> <span class="string">"/tmp/uvmt-agent"</span>,</span><br><span class="line"> <span class="string">"-gcflags=all=\"-N -l\""</span>,</span><br><span class="line"> <span class="string">"${workspaceFolder}/v2v/cmd/uvmt-agent"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"problemMatcher"</span>: [<span class="string">"$go"</span>],</span><br><span class="line"> <span class="attr">"group"</span>: <span class="string">"build"</span>,</span><br><span class="line"> <span class="attr">"detail"</span>: <span class="string">"cd /data/dg/code/huanghews/hhkits/v2v/cmd/uvmt-agent; go build uvmt-agent"</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 2. 将编译好的uvmt-agent以及golang调试工具dlv拷贝到远程服务器</span></span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"shell"</span>,</span><br><span class="line"> <span class="attr">"label"</span>: <span class="string">"scp uvmt-agent and dlv to remoteServer"</span>,</span><br><span class="line"> <span class="attr">"command"</span>: <span class="string">"scp"</span>,</span><br><span class="line"> <span class="attr">"args"</span>: [</span><br><span class="line"> <span class="string">"/tmp/uvmt-agent"</span>,</span><br><span class="line"> <span class="string">"/data/dg/go/bin/dlv"</span>,</span><br><span class="line"> <span class="string">"root@${input:remoteServer}:/tmp"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"dependsOn"</span>: [<span class="string">"go: build uvmt-agent: disable Inlining and optimizations"</span>] <span class="comment">// 执行此任务之前,需要执行编译</span></span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 3. 在远程服务器上运行dlv dap headlease server</span></span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"shell"</span>,</span><br><span class="line"> <span class="attr">"label"</span>: <span class="string">"run remote dlv headless server"</span>,</span><br><span class="line"> <span class="attr">"command"</span>: <span class="string">"ssh"</span>,</span><br><span class="line"> <span class="attr">"args"</span>: [</span><br><span class="line"> <span class="string">"root@${input:remoteServer}"</span>,</span><br><span class="line"> <span class="string">"cd /tmp;./dlv dap --listen=:12345"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"dependsOn"</span>: [<span class="string">"scp uvmt-agent and dlv to remoteServer"</span>], <span class="comment">// 同样,需要先执行步骤2</span></span><br><span class="line"> <span class="attr">"isBackground"</span>: <span class="literal">true</span>, <span class="comment">// 因为dlv需要保持在后台运行</span></span><br><span class="line"> <span class="attr">"problemMatcher"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"pattern"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"regexp"</span>: <span class="string">"."</span>,</span><br><span class="line"> <span class="attr">"file"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"location"</span>: <span class="number">2</span>,</span><br><span class="line"> <span class="attr">"message"</span>: <span class="number">3</span></span><br><span class="line"> }</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"background"</span>: {</span><br><span class="line"> <span class="attr">"activeOnStart"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"beginsPattern"</span>: <span class="string">"."</span>,</span><br><span class="line"> <span class="attr">"endsPattern"</span>: <span class="string">"."</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ] <span class="comment">// 配置该problemMatcher十分重要,告诉vscode 前置任务已经执行完成,可以执行调试了</span></span><br><span class="line"> }</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"inputs"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 通过input可以实现remoteServer地址的配置</span></span><br><span class="line"> <span class="attr">"id"</span>: <span class="string">"remoteServer"</span>,</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"remote server ip address"</span>,</span><br><span class="line"> <span class="attr">"default"</span>: <span class="string">"192.168.100.3"</span>,</span><br><span class="line"> <span class="comment">// vscode ui会提示输入远程服务地址,并带上默认值</span></span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"promptString"</span></span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们可以直接运行此任务,验证 dlv 是否在远程服务器上运行。</p><h2 id="lanuch-配置"><a href="#lanuch-配置" class="headerlink" title="lanuch 配置"></a>lanuch 配置</h2><p>通过安装<code>Go extension</code>插件,vscode 可是对 golang 程序的调试.简单的本地调试,直接打上断点,就可以直接调试,复杂的调试需要创建<code>launch.json</code>进行配置,同样该配置文件也会保存在<code>.vscode</code>目录.通过以下配置,我们可以连接到远程运行的 dlv server,vscode 进入调试中.</p><figure class="highlight json"><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><br><span class="line"> <span class="attr">"version"</span>: <span class="string">"0.2.0"</span>,</span><br><span class="line"> <span class="attr">"configurations"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"uvmt-agent-remote-debugger"</span>,</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"go"</span>,</span><br><span class="line"> <span class="comment">// 通过launch方式,可以不需要dlv运行调试的程序</span></span><br><span class="line"> <span class="comment">// launch方式需要指定program的绝对路径</span></span><br><span class="line"> <span class="attr">"request"</span>: <span class="string">"launch"</span>,</span><br><span class="line"> <span class="attr">"mode"</span>: <span class="string">"exec"</span>,</span><br><span class="line"> <span class="attr">"port"</span>: <span class="number">12345</span>,</span><br><span class="line"> <span class="attr">"host"</span>: <span class="string">"10.76.197.214"</span>,</span><br><span class="line"> <span class="comment">// 注意dlv的版本,旧版本不支持dap模式</span></span><br><span class="line"> <span class="attr">"debugAdapter"</span>: <span class="string">"dlv-dap"</span>,</span><br><span class="line"> <span class="attr">"program"</span>: <span class="string">"/tmp/uvmt-agent"</span>,</span><br><span class="line"> <span class="comment">// 调试之前,等待该任务运行结束</span></span><br><span class="line"> <span class="attr">"preLaunchTask"</span>: <span class="string">"run remote dlv headless server"</span>,</span><br><span class="line"> <span class="attr">"showLog"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>所有配置已经完成。我们看一下效果。</p><div class="video-container"><iframe src="https://player.vimeo.com/video/857739883" frameborder="0" loading="lazy" allowfullscreen></iframe></div><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>借助 vscode 的调试和任务特性,我们可以提高开发效率,并且可自定义程度非常高,你需要根据每个项目的场景,对配置进行适当调整。出于网上相关配置用例很少,我写了这片总结,希望本文对你有帮助,如果有更优雅的方式,欢迎讨论。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://github.com/golang/vscode-go/wiki/debugging">vscode-golang 调试配置</a></li><li><a href="https://code.visualstudio.com/docs/editor/tasks">vscode task 配置</a></li></ol>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> golang </tag>
<tag> vscode </tag>
</tags>
</entry>
<entry>
<title>使用Fio测试存储性能是否满足etcd要求</title>
<link href="/2021/08/05/fio-and-etcd/"/>
<url>/2021/08/05/fio-and-etcd/</url>
<content type="html"><![CDATA[<h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>存储性能很大程度决定了<strong>etcd</strong>集群的性能表现,因为<strong>etcd</strong>必须持久化日志,一次写请求必须确保多数节点将用于保证状态机的日志写入磁盘中。在已经运行的 etcd 集群中,我们可以通过指标<code>etcd_disk_wal_fysnc_duration_seconds</code>来评估存储 I/O 性能,该指标记录了 WAL 文件系统调用 fsync 的延迟分布,当 99% 样本的同步时间小于 10 毫秒就可以认为存储性能能够满足 etcd 的性能要求。那么如何在部署实施前或没有监控系统的情况下,评估存储的性能是否满足需求呢?<strong>fio</strong>命令行工具可能是不错的选择,<strong>fio</strong>主要用于进行 I/O 性能测试,你可以使用下面的命令测试存储性能,以评估是否满足 etcd 的要求。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fio --rw=write --ioengine=sync --fdatasync=1 --directory=test-data --size=22m --bs=2300 --name=mytest</span><br></pre></td></tr></table></figure><p>观察输出,验证是否 99%的文件同步执行时间小于 10ms,下图用例的测试结果不满足要求。</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/fio-and-etcd/2021-06-29-18-59-18.png" alt></p><p>注意:</p><ol><li>参数<code>--size</code>和<code>--bs</code>的值需要根据使用场景进行调优</li><li>测试过程中,fio 产生的 IO 负载是唯一的 IO 活动,除了与 wal_sync_duration_seconds 相关的 IO 还会有其他写入存储的 IO 活动,因此实际环境下 wal_sync_duration_seconds 要更大一些。</li><li>fio 的版本需要大于 3.5,因为旧版本不会输出 fdatasycn 系统调用百分位数。</li></ol><h2 id="故事的细节"><a href="#故事的细节" class="headerlink" title="故事的细节"></a>故事的细节</h2><p>数据库系统通常都会使用预写式日志(write-ahead logging,WAL)来保证原子性和持久性,etcd 也是如此,关于 WAL 的介绍超出了本文讨论的范畴,但是我们需要知道 etcd 集群的每个成员都会持久存储上保留 WAL 日志。etcd 首先将键值存储上的某些操作(例如,更新)写入 WAL 文件中,然后才会应用这些操作。如果其中一个成员在快照期间崩溃并重启,它可以在本地通过 WAL 内容恢复自上次快照以来的事务。</p><p>因此,每当客户端添加或更新键值对数据时,etcd 会向 WAL 文件添加一条入库记录条目。再进一步处理之前,etcd 必须 100% 确保 WAL 条目已经被持久化。要在 Linux 实现这一点,仅使用<strong>write</strong>系统调用是不够的,因为对物理存储的写入操作可能会发生延迟。比如,Linux 可能会将写入的 WAL 条目在内核内存缓存中保留一段时间(例如,页缓存)。如果要确保数据被写入持久化存储,你必须在 write 系统调用之后调用fdatasync 系统调用,实际上 etcd 就是采用这种方式,下图是使用<strong>strace</strong>命令的输出,参数 8 是 WAL 文件的文件描述符。</p><figure class="highlight bash"><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">21:23:09.894875 lseek(8, 0, SEEK_CUR) = 12808 <0.000012></span><br><span class="line">21:23:09.894911 write(8, <span class="string">".\0\0\0\0\0\0\202\10\2\20\361\223\255\266\6\32$\10\0\20\10\30\26\"\34\"\r\n\3fo"</span>..., 2296) = 2296 <0.000130></span><br><span class="line">21:23:09.895041 fdatasync(8) = 0 <0.008314></span><br></pre></td></tr></table></figure><p>写入持久化存储相对是比较耗时的,如果 fdatasync 系统调用用时太长,会导致 etcd 的存储性能下降。etcd 官方文档建议存储性能,写入 WAL 文件时 fdatasync 系统耗时的 99%的百分位数应该小于 10ms,还有其他与存储相关的指标,但这是本文的重点。</p><h2 id="使用-fio-测试存储性能"><a href="#使用-fio-测试存储性能" class="headerlink" title="使用 fio 测试存储性能"></a>使用 fio 测试存储性能</h2><p>如果你有一些存储并且想知道是不是满足 etcd 的存储需求,你可以使用非常流行的 IO 测试工具 fio。磁盘 的 IO 活动可能以很多种方式发生,如同步或异步,不同的系统调用等,所以 fio 工具的使用也比较负载,它有很多参数,不同值的组合会产生完全不同的 IO 负载,要获取 etcd 相关的存储性能数据,你必要要保证 fio 产生的写负载和 etcd 写入 WAL 文件产生的 IO 负载保持一致。</p><p>这意味着,至少 fio 产生 IO 负载是顺序写入到一个文件中的,其中每次写入由 write 系统调用和 fdatasync 调用组成。要产生顺序写入,可以通过命令行参数<code>--rw=write</code>,要确保 fio 写入使用 write 系统调用而不是其他的系统调用(例如,pwrite),通过命令行参数<code>--ioengine=sync</code>实现,最后为了保证每次 write 系统调用后调用 fdatasync,指定<code>--fdatasync=1</code>。示例中剩余的两个参数<code>--size</code>和<code>--bs</code>,依赖具体的使用场景,下一节将会介绍如何设置这两个参数。</p><h2 id="为什么我们会使用-fio-以及我如何知道怎样进行配置的"><a href="#为什么我们会使用-fio-以及我如何知道怎样进行配置的" class="headerlink" title="为什么我们会使用 fio 以及我如何知道怎样进行配置的"></a>为什么我们会使用 fio 以及我如何知道怎样进行配置的</h2><p>这其实源自一个我们遇到了一个客户问题,一个客户环境中,安装了 v1.20 版本的 Kubernetes,etcd 集群使用的本地磁盘,本地磁盘是由ceph 存储集群提供的,使用过程中,集群 apiserver 会出现间断性重启,kubernetes 系统组件重启次数过高,etcd 集群也是,etcd 节点过多的重启导致 etcd 的 leader 切换次数过高,并且 etcd 的日志出现大量的 Warning 级别的日志。</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">2021-06-29 15:04:13.396304 W | etcdserver: read-only range request "key:\"/registry/cluster.flyingshark.com/clusters/\" range_end:\"/registry/cluster.flyingshark.com/clusters0\" count_only:true " with result "range_response_count:0 size:8" took too long (1.568548001s) to execute</span><br></pre></td></tr></table></figure><p>集群完全处于不可用状态,客户环境是一套私有云,k8s 集群都通过虚拟机部署,我们需要知道究竟是物理磁盘性能还是虚拟化导致的这个问题,以及给出解决方案,是需要更换存储还是能够通过参数配置解决。etcd 集群有两个关于存储性能指标,不过要在部署实施前发现问题,指标数据显然不是一个好的参考数据。</p><p>首先,我们弄清楚几个问题:etcd 写入 WAL 文件所有产生的 IO 负载是怎样的?使用了那些系统调用?写入内容大小是多少?其次,在有了这些问题的答案之后,如是使用 fio 模拟 etcd 的 IO 活动?因为 fio 功能丰富且十分灵活,我们通过<strong>lsof</strong> 和 <strong>strace</strong>这两个工具成功弄清楚了这些问题。lsof 可以用来显示进程所使用的文件描述符(FD)以及关联的文件。strace 可以测试一个处于运行状态的进程和新运行的进程,打印该进程使用的系统调用,重要的是也能测试该进程的子进程,因为 etcd 就是这样一个进程。</p><p>测试集群中 etcd 使用静态 pod 的方式运行,并且在宿主机命名空间,我们使用 lsof 来获取 etcd 进程当前 wal 文件的文件描述符(FD),这个 FD 在后续观测系统调用中将作为一些函数的参数传递。</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/fio-and-etcd/2021-08-05-14-16-26.png" alt></p><p>接下来我们使用 strace 对 etcd 集群观察系统调用,下图结果显示 WAL 文件写入大小几乎都在 100 字节这个范围,客户环境中在 2200-2400 范围,这就是为什么示例我们设置参数<code>--bs=2300</code>(bs 参数设置 fio 每次写操作的大小)的原因。这个数值可能取决于 etcd 版本,部署规模,配置等因素,这个参数影响 fdatasync 的持续时间。</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/fio-and-etcd/2021-08-05-14-19-11.png" alt></p><p>同时我们我们也确定了 etcd 使用了 fdatasync 将保证将缓冲区写入磁盘。etcd 中的代码也能进一步验证这一点,</p><figure class="highlight golang"><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="function"><span class="keyword">func</span> <span class="params">(w *WAL)</span> <span class="title">sync</span><span class="params">()</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="keyword">if</span> w.encoder != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">if</span> err := w.encoder.flush(); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> w.unsafeNoSync {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">start := time.Now()</span><br><span class="line"> <span class="comment">// 使用fdatasync同步缓冲区</span></span><br><span class="line">err := fileutil.Fdatasync(w.tail().File)</span><br><span class="line"></span><br><span class="line">took := time.Since(start)</span><br><span class="line"><span class="keyword">if</span> took > warnSyncDuration {</span><br><span class="line">w.lg.Warn(</span><br><span class="line"><span class="string">"slow fdatasync"</span>,</span><br><span class="line">zap.Duration(<span class="string">"took"</span>, took),</span><br><span class="line">zap.Duration(<span class="string">"expected-duration"</span>, warnSyncDuration),</span><br><span class="line">)</span><br><span class="line">}</span><br><span class="line">walFsyncSec.Observe(took.Seconds())</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Fdatasync is similar to fsync(), but does not flush modified metadata</span></span><br><span class="line"><span class="comment">// unless that metadata is needed in order to allow a subsequent data retrieval</span></span><br><span class="line"><span class="comment">// to be correctly handled.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Fdatasync</span><span class="params">(f *os.File)</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="keyword">return</span> syscall.Fdatasync(<span class="keyword">int</span>(f.Fd()))</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>到目前为止,我们确定了 etcd 写入日志使用的是 Append 的方式即顺序写,并且使用 fdatasync 确保数据持久化到磁盘,每次写入的日志大小大约为 2300 字节,接下来就可以使用强大的 fio 来模拟 etcd 的 I/O 活动,来测试存储是否满足需求。</p><h3 id="建议"><a href="#建议" class="headerlink" title="建议"></a>建议</h3><p>etcd 是 kubernetes 集群的核心,而 etcd 的性能主要取决于存储性能和网络性能,提升 etcd 集群性能最有效的方式是使用高性能的存储硬件,官方推荐使用 ssd 存储,如果没有办法使用高性能硬件,也可以通过以下几种方式保证 etcd 集群的稳定性:</p><ol><li>在单独节点上运行 etcd,特别注意的是不能将 etcd 运行在工作节点上,如果资源允许,将 etcd 集群与控制面单独运行。</li><li>如果 etcd 已经运行单独节点还是出现问题,你可以为 etcd 分配单独的存储卷,这样可以防止节点上的 IO 活动影响 etcd,特别是在云环境。</li><li>如果你的 etcd 运行在单独服务器上,则可以安装新的的存储设备为 etcd 专用。</li><li>为 etcd 容器设置更高的优先级<code>ionice -c2 -n0 -p "pgrep -x etcd"</code></li><li>适当增大 etcd 的 <strong>–heartbeat-interval</strong> 与 <strong>–election-timeout</strong> 启动参数来适当提高高吞吐网络下 etcd 的集群鲁棒性。</li></ol><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><ol><li><a href="https://blog.px.dev/etcd-6-tips/">etcd 架构设计</a></li><li><a href="https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md">raft 论文中文版</a></li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> etcd </tag>
<tag> fio </tag>
</tags>
</entry>
<entry>
<title>kubernetes平台开发者小技巧</title>
<link href="/2020/10/30/kubernetes-develop-tips/"/>
<url>/2020/10/30/kubernetes-develop-tips/</url>
<content type="html"><![CDATA[<p>随着<strong>Cloud Native</strong>逐渐深入人心以及<strong>kubernetes</strong>的流行,国内外出现大量的kubernetes服务提供商,如红帽,阿里,腾讯等,同时许多互联网公司也在定制kubernetes以满足自身需求,kubernetes平台开发者这个岗位的需求也逐渐增大,那作为一名合格的kubernetes平台开发者其实需要具备一些特殊技能的。</p><p>在这个系列博文中,我将结合自身的开发经历给Kubernetes平台开发者分享一些开发小技巧,帮助kubernetes平台开发者少走一些弯路。</p><h2 id="在你的项目使用依赖k8s-io-kubernetes主仓模块"><a href="#在你的项目使用依赖k8s-io-kubernetes主仓模块" class="headerlink" title="在你的项目使用依赖k8s.io/kubernetes主仓模块"></a>在你的项目使用依赖k8s.io/kubernetes主仓模块</h2><p>Kubernetes提供了<a href="https://git.k8s.io/kubernetes/staging/README.md">很多公共库</a>供开发者使用,比如<strong>client-go</strong>、<strong>apimachinery</strong>,但是官方不推荐直接依赖主仓k8s.io/kubernetes,虽然其代码完全是开源的,这样做最主要原因是直接依赖k8s.io/kubernetes会导致你的项目过大,包含太多不必要的文件。</p><p>但是除了公共库已经拆分的通用模块,主仓还有很多有意义的API接口(函数),比如包<code>pkg/core/validation</code>的<code>ValidatePodCreate</code>函数可以用来创建POD时,做<strong>前校验处理</strong>(cli-runtime库定义了ContentValidator接口,建议使用该接口和<code>dry-run</code>机制),包<code>pkg/apis/core/v1</code>中<code>Convert_v1_Pod_To_Core_Pod</code>函数可以用来将corev1.Pod转换为core.Pod等。这些包目前还没有被抽离成单独模块。</p><p>如果我们直接<code>go get k8s.io/kubernetes@v1.19.2</code>下载依赖,会出现以下错误:</p><figure class="highlight bash"><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">➜ go-get-kubernetes go get k8s.io/kubernetes</span><br><span class="line">go: k8s.io/kubernetes upgrade => v1.19.3</span><br><span class="line"> go get: k8s.io/kubernetes@v1.19.3 requires</span><br><span class="line">k8s.io/api@v0.0.0: reading k8s.io/api/go.mod at revision v0.0.0: unknown revision v0.0.0</span><br></pre></td></tr></table></figure><p>错误的原因是在kubernetes主仓中,也使用了公共库,不过<code>go.mod</code>文件中所有公共库版本都指定为了<strong>v0.0.0(显然这个版本不存在)</strong>,然后通过<strong>Go Module的replace</strong>机制,将版本替换为子目录<code>./staging/src/k8s.io</code>对应的依赖。</p><figure class="highlight text"><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">module k8s.io/kubernetes</span><br><span class="line">go 1.15</span><br><span class="line">require (</span><br><span class="line"> ...</span><br><span class="line"> k8s.io/api v0.0.0</span><br><span class="line">k8s.io/apiextensions-apiserver v0.0.0</span><br><span class="line">k8s.io/apimachinery v0.0.0</span><br><span class="line">k8s.io/apiserver v0.0.0</span><br><span class="line">k8s.io/cli-runtime v0.0.0</span><br><span class="line">k8s.io/client-go v0.0.0</span><br><span class="line"> ...</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">replace(</span><br><span class="line"> ...</span><br><span class="line"> k8s.io/client-go => ./staging/src/k8s.io/client-go</span><br><span class="line">k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider</span><br><span class="line">k8s.io/cluster-bootstrap => ./staging/src/k8s.io/cluster-bootstrap</span><br><span class="line">k8s.io/code-generator => ./staging/src/k8s.io/code-generator</span><br><span class="line">k8s.io/component-base => ./staging/src/k8s.io/component-base</span><br><span class="line"> ...</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>那么如何解决呢,只需要复制保存以下脚本(来自社区相关Issue),在项目中执行以下脚本:<code>bash hack/go-get-kubernetes.sh v1.19.2</code>,注意替换你需要的版本。这个脚本通过修改<code>go.mod</code>文件保证能够获取相关依赖。</p><figure class="highlight bash"><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">#!/bin/sh</span></span><br><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"></span><br><span class="line">VERSION=<span class="variable">${1#"v"}</span></span><br><span class="line"><span class="keyword">if</span> [ -z <span class="string">"<span class="variable">$VERSION</span>"</span> ]; <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">echo</span> <span class="string">"Must specify version!"</span></span><br><span class="line"> <span class="built_in">exit</span> 1</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line">MODS=($(</span><br><span class="line"> curl -sS https://raw.githubusercontent.com/kubernetes/kubernetes/v<span class="variable">${VERSION}</span>/go.mod |</span><br><span class="line"> sed -n <span class="string">'s|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p'</span></span><br><span class="line">))</span><br><span class="line"><span class="keyword">for</span> MOD <span class="keyword">in</span> <span class="string">"<span class="variable">${MODS[@]}</span>"</span>; <span class="keyword">do</span></span><br><span class="line"> V=$(</span><br><span class="line"> go mod download -json <span class="string">"<span class="variable">${MOD}</span>@kubernetes-<span class="variable">${VERSION}</span>"</span> |</span><br><span class="line"> sed -n <span class="string">'s|.*"Version": "\(.*\)".*|\1|p'</span></span><br><span class="line"> )</span><br><span class="line"> go mod edit <span class="string">"-replace=<span class="variable">${MOD}</span>=<span class="variable">${MOD}</span>@<span class="variable">${V}</span>"</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line">go get <span class="string">"k8s.io/kubernetes@v<span class="variable">${VERSION}</span>"</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><blockquote><p>不过建议大家不要直接依赖主仓</p></blockquote><h2 id="Goland如何调试Kubernetes相关组件"><a href="#Goland如何调试Kubernetes相关组件" class="headerlink" title="Goland如何调试Kubernetes相关组件"></a>Goland如何调试Kubernetes相关组件</h2><p>学会调试kubernetes,对于我们学习kubernetes源码及定制化kubernetes十分有帮助,其实刚开始接触kubernetes项目,我和许多开发者一样,面对kubernetes这一复杂庞大的项目,不知道从何看起,也不知道如何调试它(当然Kubectl除外),其实kubernetes的本地调试并没有想象中的复杂。</p><blockquote><p>首先假设你已经在虚拟机通过kubeadm安装了一套kubernetes集群,克隆了kubernetes代码仓到本地和虚拟机上。这里我以APIServer组件为例,其他组件类似。</p></blockquote><p>首先,查看安装Kubernetes集群的版本,并在虚机上checkout对应版本的代码仓分支,然后编译Kubernetes相关组件,编译时需要打开调试选项(参照Makefile的说明),这个编译时间可能有点长,对应的二进制产物在<code>_output/bin</code>目录中,</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 编译</span></span><br><span class="line">➜ kubernetes git:(release-1.18) ✗ make all GOGCFLAGS=”-N -l” </span><br><span class="line">➜ kubernetes git:(release-1.18) ✗ <span class="built_in">cd</span> _output/bin</span><br><span class="line">➜ bin git:(release-1.18) ✗ ll</span><br><span class="line">总用量 1.8G</span><br><span class="line">-rwxr-xr-x 1 root root 65M 9月 11 11:20 apiextensions-apiserver</span><br><span class="line">-rwxr-xr-x 1 root root 6.0M 9月 10 16:56 conversion-gen</span><br><span class="line">-rwxr-xr-x 1 root root 6.0M 9月 10 16:56 deepcopy-gen</span><br><span class="line">-rwxr-xr-x 1 root root 6.0M 9月 10 16:56 defaulter-gen</span><br><span class="line">-rwxr-xr-x 1 root root 172M 9月 11 11:21 e2e_node.test</span><br><span class="line">-rwxr-xr-x 1 root root 157M 9月 11 11:21 e2e.test</span><br><span class="line">-rwxr-xr-x 1 root root 59M 9月 11 11:21 gendocs</span><br><span class="line">-rwxr-xr-x 1 root root 192M 9月 11 11:21 genkubedocs</span><br><span class="line">-rwxr-xr-x 1 root root 200M 9月 11 11:21 genman</span><br><span class="line">-rwxr-xr-x 1 root root 9.4M 9月 11 11:21 genswaggertypedocs</span><br><span class="line">-rwxr-xr-x 1 root root 59M 9月 11 11:21 genyaml</span><br><span class="line">-rwxr-xr-x 1 root root 11M 9月 11 11:21 ginkgo</span><br><span class="line">-rwxr-xr-x 1 root root 3.6M 9月 10 16:07 go2make</span><br><span class="line">-rwxr-xr-x 1 root root 2.0M 9月 10 16:57 go-bindata</span><br><span class="line">-rwxr-xr-x 1 root root 2.8M 9月 11 11:21 go-runner</span><br><span class="line">-rwxr-xr-x 1 root root 55M 9月 11 11:20 kubeadm</span><br><span class="line">-rwxr-xr-x 1 root root 149M 9月 11 11:20 kube-apiserver</span><br><span class="line">-rwxr-xr-x 1 root root 141M 9月 11 11:20 kube-controller-manager</span><br><span class="line">-rwxr-xr-x 1 root root 60M 9月 11 11:20 kubectl</span><br><span class="line">-rwxr-xr-x 1 root root 142M 9月 11 11:21 kubelet</span><br><span class="line">-rwxr-xr-x 1 root root 139M 9月 11 11:21 kubemark</span><br><span class="line">-rwxr-xr-x 1 root root 52M 9月 11 11:20 kube-proxy</span><br><span class="line">-rwxr-xr-x 1 root root 59M 9月 11 11:20 kube-scheduler</span><br><span class="line">-rwxr-xr-x 1 root root 7.2M 9月 11 11:21 linkcheck</span><br><span class="line">-rwxr-xr-x 1 root root 2.3M 9月 11 11:20 mounter</span><br><span class="line">-rwxr-xr-x 1 root root 9.9M 9月 10 16:56 openapi-gen</span><br><span class="line">-rwxr-xr-x 1 root root 5.7M 9月 10 16:07 prerelease-lifecycle-gen</span><br></pre></td></tr></table></figure><p>调试需要使用到Go调试工具<a href="https://github.com/go-delve/delve">delve</a>,在kubernetes集群的控制节点上安装delve,这里使用<code>go get</code>的方式安装。</p><figure class="highlight bash"><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="comment"># 在Module目录外执行,并将GOBIN目录添加环境变量PATH中</span></span><br><span class="line">➜ ~ go get github.com/go-delve/delve/cmd/dlv</span><br><span class="line">➜ bin git:(release-1.18) ✗ dlv -h</span><br><span class="line">Delve is a <span class="built_in">source</span> level debugger <span class="keyword">for</span> Go programs.</span><br><span class="line"></span><br><span class="line">Delve enables you to interact with your program by controlling the execution of the process,</span><br><span class="line">evaluating variables, and providing information of thread / goroutine state, CPU register state and more.</span><br><span class="line"></span><br><span class="line">The goal of this tool is to provide a simple yet powerful interface <span class="keyword">for</span> debugging Go programs.</span><br><span class="line"></span><br><span class="line">Pass flags to the program you are debugging using `--`, <span class="keyword">for</span> example:</span><br><span class="line"></span><br><span class="line">`dlv <span class="built_in">exec</span> ./hello -- server --config conf/config.toml`</span><br></pre></td></tr></table></figure><p>通过Kubeadm安装的集群,kubernetes控制平面的组件是以<strong>Static pod</strong>形式运行的,对应的yaml文件保存在<strong>/etc/kubernetes/manifests</strong>,查看APIServer对应的配置文件如下,我们需要拷贝APIServer的启动参数。接下来,我们重命名<code>/etc/kubernetes/manifests/kube-apiserver.yaml</code>为<code>kube-apiserver.yaml.old</code>,观察容器列表,等待APIServer对应的容器停止,通过delve启动APIServer进行调试,注意相关参数配置。</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">dlv <span class="built_in">exec</span> _output/bin/kube-apiserver --headless -l 192.168.5.82:2345 --api-version=2 \\</span><br><span class="line"> --advertise-address=192.168.5.82 \\</span><br><span class="line"> --allow-privileged=<span class="literal">true</span> \\</span><br><span class="line"> --authorization-mode=Node,RBAC\\</span><br><span class="line"> --client-ca-file=/etc/kubernetes/pki/ca.crt\\</span><br><span class="line"> --enable-admission-plugins=NodeRestriction\\</span><br><span class="line"> --enable-bootstrap-token-auth=<span class="literal">true</span>\\</span><br><span class="line"> --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt\\</span><br><span class="line"> --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt\\</span><br><span class="line"> --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key\\</span><br><span class="line"> --etcd-servers=https://127.0.0.1:2379\\</span><br><span class="line"> --insecure-port=0\\</span><br><span class="line"> --kubelet-client-certificate=/etc/kubernetes/pki/apiserver- kubelet-client.crt\\</span><br><span class="line"> --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet- client.key\\</span><br><span class="line"> --kubelet-preferred-address-types=InternalIP,ExternalIP, Hostname\\</span><br><span class="line"> --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client. crt\\</span><br><span class="line"> --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client. key\\</span><br><span class="line"> --requestheader-allowed-names=front-proxy-client\\</span><br><span class="line"> --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy- ca.crt\\</span><br><span class="line"> --requestheader-extra-headers-prefix=X-Remote-Extra-\\</span><br><span class="line"> --requestheader-group-headers=X-Remote-Group\\</span><br><span class="line"> --requestheader-username-headers=X-Remote-User\\</span><br><span class="line"> --secure-port=6443\\</span><br><span class="line"> --service-account-key-file=/etc/kubernetes/pki/sa.pub\\</span><br><span class="line"> --service-cluster-ip-range=10.96.0.0/16\\</span><br><span class="line"> --tls-cert-file=/etc/kubernetes/pki/apiserver.crt\\</span><br><span class="line"> --tls-private-key-file=/etc/kubernetes/pki/apiserver.key</span><br><span class="line"> server listening at: 192.168.5.82:2345</span><br></pre></td></tr></table></figure><p>在本地开发环境也切到与虚机对应的kubernetes分支,然后添加一个远程调试的Configuration,配置对应的主机和端口,并在相关位置打上断点,开始调试。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-develop-tips/2020-10-30-16-23-53.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-develop-tips/2020-10-30-16-23-37.png" alt></p><p>到这里,就已经成功对Kubernetes的APIServer进行调试啦。</p>]]></content>
<tags>
<tag> kubernetes </tag>
<tag> kubernetes平台开发者 </tag>
</tags>
</entry>
<entry>
<title>kubeedge介绍</title>
<link href="/2020/10/27/kubeedge-day-01/"/>
<url>/2020/10/27/kubeedge-day-01/</url>
<content type="html"><![CDATA[<p><strong>KubeEdge</strong>即<strong>Kube</strong>加<strong>Edge</strong>,顾名思义就是依托k8s的容器编排能力和调度能力,实现云边协同、计算下沉、海量设备的平滑接入,旨在为边缘计算和IoT这两大物联网领域提供统一的平台方案,本次分享将给大家简单介绍目前KubeEdge项目进展,它的架构设计,以及我们基于KubeEdge做了哪些工作,并详细分析KubeEdge是如何解决云边网络通信问题的。</p><blockquote><p>本文主要内容取自一次我在公司的内部分享</p></blockquote><h2 id="KubeEdge项目介绍"><a href="#KubeEdge项目介绍" class="headerlink" title="KubeEdge项目介绍"></a>KubeEdge项目介绍</h2><p>KubeEdge是华为推出的业内首个开源边缘容器平台项目,是由华为云边缘计算产品IEF开源而来,<strong>Apache 2.0</strong>开源协议,并于2019年3月捐献给CNCF基金会,在2019年9月晋级为<strong>孵化级</strong>项目,在CNCF的社区建设上,KubeEdge积极参与<strong>kubernetes IoT Edge WG</strong>,同时也成立了Device/IoT与MEC两个SIG,可以说KubeEdge先发制人,在社区已经拥有了足够多话语权。目前参与社区贡献的企业包括:中国联通,ARM,中国移动,谐云,中国电信,时速云,JD.com,浙大SEL实验室,EMQ,InfoBlox,Inovex,Midokura等。</p><h2 id="基于kubernetes构建边缘计算的优点和痛点"><a href="#基于kubernetes构建边缘计算的优点和痛点" class="headerlink" title="基于kubernetes构建边缘计算的优点和痛点"></a>基于kubernetes构建边缘计算的优点和痛点</h2><p>为什么要基于kubernetes构建边缘计算平台呢?因为kubernetes是在太香了🤪,那我简单概括了kubernetes主要的四核心的优势:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-23-42.png" alt></p><p><em>容器化应用封装:</em> 现已成为应用交付的一个趋势,我们可以把应用打包到容器中,且只需要只打包一次,可以跑在各种地方,如果应用到IoT领域,有很多传统IoT嵌入式设备,它其实很多硬件和软件是强相关的,更换一个硬件,可能软件(固件)都需要更改,如果使用容器化封装后,设备可以支持容器runtime,我们就可以将容器跑在任何IoT设备上。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-26-27.png" alt><em>通用应用抽象定义</em> kubernetes的API包括pod的定义以及development,daemonset等控制器的定义现在其实在业内已经形成一套标准, 大家都比较了解和认可,基于这些通用应用负载模型做边缘计算平台,大家也更能接受。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-26-52.png" alt><em>松耦合架构</em> kubernetess的架构设计具有相当的先进性,可拓展性比较好,比如我们基于k8s之上可以通过CRD来定义一些API,通过设备管理CRD来定义一些IoT里的device的API,我们即可以通过k8s的一些管理方式管理这些设备;还有一些有意义的可拓展点,比如边缘节点可以对接各种runtime,有些边缘节点它的资源非常有限,我们就可以对接一些轻量化的runtime。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-27-34.png" alt><em>丰富的社区生态</em> k8s其丰富的拓展性给我们带来无限可能,同时借助其生态,能够极大地赋能边缘计算平台,减少开发工作量的同时也有强大的社区保障。</p><p>下图是KubeEdge给出的其核心的优势,不难看出,大部分都是由于其借助了kubernetes构建平台所带来的。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-30-36.png" alt></p><p>当然基于kubernetes还有很多优势,那么有哪些边缘计算的常见的关键痛点需要KubeEdge解决呢?</p><ol><li>资源有限:常见的网关设备内存很少,无法满足kubernetes对边缘节点的资源需求</li><li>网络不畅<ul><li>边缘位于私有网络,无公网IP</li><li>云边跨越公网,带宽有限,延时高</li><li>k8s的List-watch需要数据中心网络</li></ul></li><li>边缘节点如何自治<ul><li>网络不稳,随时可能离线</li><li>边缘业务离线工作</li><li>边缘离线可故障恢复</li></ul></li><li>设备接入和管理<ul><li>缺少边缘设备抽象</li><li>缺少边缘设备接入协议的支持</li></ul></li></ol><h2 id="KubeEdge的架构及核心理念"><a href="#KubeEdge的架构及核心理念" class="headerlink" title="KubeEdge的架构及核心理念"></a>KubeEdge的架构及核心理念</h2><p>KubeEdge架构主要是分了云、边、端三部分,云上边就是我们的控制面,边就是我们的边缘节点,端就是跑了我们的一些端侧设备,云上左边是一个Kubernetes的master,是没有做过改动的原生的K8s控制),后边KubeEdge的一个组件叫CloudCore,云上的组件主要是会拿一些Kubernetes控制面上的东西,通过EdgeController和DeviceController做一些处理,然后通过下边的Cloud Hub,Cloud Hub主要是跟边端通信的,边端有个EdgeHub和Cloud Hub通信,然后把数据拿下来。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-33-55.png" alt></p><p>边端是主要做了一个应用管理和设备管理的能力,应用管理左边会有一个Edged,右边有DeviceTwin、EventBus,分别是应用管理和设备管理,左边有个DataStore,就是我们说的本地自治的能力,比如说我们这应用或者设备的元素从云上分发下来,我们是先把它存到一个数据库里,然后再到它的Edged或者设备里边,这样就能保证云边网络断开或者边缘节点重启了以后我应用的Edged它可以从数据库里把应用源数据拿出来,这样就能保证在故障的情况下业务可以正常恢复。</p><h3 id="已实现的核心功能"><a href="#已实现的核心功能" class="headerlink" title="已实现的核心功能"></a>已实现的核心功能</h3><ol><li>云边可靠协同<ul><li>双向多路服用消息通道,支持边缘节点位于私有网络</li><li>Websocket + 消息封装,大幅减少通信压力,高延时下仍可正常工作</li><li>云边消息校验</li></ul></li><li>边缘离线自治<ul><li>节点云数据持久化,实现节点级离线自治</li><li>节点故障恢复,无需List-watch,降低网络压力,快速ready</li></ul></li><li>边缘极致轻量<ul><li>重组Kubelet功能模块,极致轻量化(~70MB内存占用)</li><li>支持CRI集成,Containerd、CRI-O,优化runtime资源消耗</li></ul></li><li>边缘设备管理<ul><li>云端通过kubernetes API管理设备Device</li></ul></li></ol><h2 id="KubeEdge2-0-Roadmap"><a href="#KubeEdge2-0-Roadmap" class="headerlink" title="KubeEdge2.0 Roadmap"></a>KubeEdge2.0 Roadmap</h2><p>以下是官方给的2.0计划支持的亮点功能:</p><ol><li>支持istio;</li><li>支持FaaS;</li><li>支持更多类型的设备协议到边缘节点,如AMQP,蓝牙,ZigBee等;</li><li>支持千个边缘节点和数百万个设备的超大规模边缘集群(kubernetes推荐单集群最大节点数是1000);</li><li>实现数千节点的应用智能调度;</li></ol><h2 id="KubeEdge的不足以及我们需要去做的工作"><a href="#KubeEdge的不足以及我们需要去做的工作" class="headerlink" title="KubeEdge的不足以及我们需要去做的工作"></a>KubeEdge的不足以及我们需要去做的工作</h2><ol><li>相关组件没有容器化以及缺乏高可用性方案</li><li>项目处于发展中期,很多东西处于不稳定状态</li><li>社区不是特别活跃,比如example目前无法在1.4版本上正常运行</li><li>安装工具keadm没有支持离线安装,且有一些缺陷</li><li>没有具体性能测试数据支撑</li></ol><h2 id="目前我们做的"><a href="#目前我们做的" class="headerlink" title="目前我们做的"></a>目前我们做的</h2><ol><li>基于公司PaaS产品提供完整的云端用户体验包括统一内置镜像仓,管理界面等;</li><li>自定义EdgeAppController,以满足边缘场景下应用下发的需求;</li><li>完整的监控告警支持,以及自定义指标的开发(Exporter的改造);</li><li>管理员友好的Dashboard,包括更友好的应用部署,节点接入等功能;</li><li>一些稳定性改造(云端组件cloudcore的service化);</li><li>KubeEdge的定制化,用于合并一些无法第一时间被社区合并Release的有价值的功能分支,定制化我们的功能;</li></ol><h2 id="kubeedge通信原理"><a href="#kubeedge通信原理" class="headerlink" title="kubeedge通信原理"></a>kubeedge通信原理</h2><p>我们是否能像管理普通节点一样管理边缘节点呢?比如说查看某个容器的调试日志,资源用量,<em>Exec</em>到某个容器中进行调试,通过Prometheus监控节点和应用等等,这些功能对于无论是PaaS还是边缘计算平台都非常重要💰。KubeEdge较早版本没有给出解决方案,也就是说这些功能无法从KubeEdge开箱直接获取,实现以上功能最大的障碍就是网络问题🌎。</p><p>目前针对这些功能,较新版本KubeEdge已经给出对应的解决方案,即通过<em>隧道Tunnel+自定义消息</em>实现,主要对应的模块是CloudStream和EdgeStream。当我们需要这些功能时,必须开启这两个模块,同时进行配置,具体配置可以参考官方文档,以下这两个模块的通信示意图。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-40-04.png" alt></p><p>这里我以Prometheus抓取Edge节点容器运行时指标数据为例,帮大家大致梳理流程以及主要函数。</p><blockquote><p>在普通kubernetes集群中可以通过安装cadvisor获取容器运行时的一些指标数据,KubeEdge受限于资源的限制,无法在边缘节点安装cadvisor组件,目前Edged已实现了cadvisor的指标定义。</p></blockquote><p>现在,prometheus要抓取edge1(10.31.100.4)上edged所定义的指标时,请求10.31.100.4:10350/metrics/cadvisor,由于我们配置iptables nat表的OUTPUT,所有target port为10350报文都会被转发到本机的10003的端口,由于TCP协议栈处于内核,在应用层收到请求的Source IP和Port不会改变,至此该请求将会有CloudStream内置的Http Server处理,这个Http服务默认端口为10003,它的主要功能就是处理被拦截的各种流量,目前包括Log、Metrics、节点统计等流量。</p><figure class="highlight golang"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *StreamServer)</span> <span class="title">installDebugHandler</span><span class="params">()</span></span> {</span><br><span class="line">ws := <span class="built_in">new</span>(restful.WebService)</span><br><span class="line">ws.Path(<span class="string">"/containerLogs"</span>)</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/{podNamespace}/{podID}/{containerName}"</span>).</span><br><span class="line">To(s.getContainerLogs))</span><br><span class="line">s.container.Add(ws)</span><br><span class="line">ws = <span class="built_in">new</span>(restful.WebService)</span><br><span class="line">ws.Path(<span class="string">"/exec"</span>)</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/{podNamespace}/{podID}/{containerName}"</span>).</span><br><span class="line">To(s.getExec))</span><br><span class="line">ws.Route(ws.POST(<span class="string">"/{podNamespace}/{podID}/{containerName}"</span>).</span><br><span class="line">To(s.getExec))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/{podNamespace}/{podID}/{uid}/{containerName}"</span>).</span><br><span class="line">To(s.getExec))</span><br><span class="line">ws.Route(ws.POST(<span class="string">"/{podNamespace}/{podID}/{uid}/{containerName}"</span>).</span><br><span class="line">To(s.getExec))</span><br><span class="line">s.container.Add(ws)</span><br><span class="line">ws = <span class="built_in">new</span>(restful.WebService)</span><br><span class="line">ws.Path(<span class="string">"/stats"</span>)</span><br><span class="line">ws.Route(ws.GET(<span class="string">""</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/summary"</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/container"</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/{podName}/{containerName}"</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/{namespace}/{podName}/{uid}/{containerName}"</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">s.container.Add(ws)</span><br><span class="line"><span class="comment">// metrics api is widely used for Promethus</span></span><br><span class="line">ws = <span class="built_in">new</span>(restful.WebService)</span><br><span class="line">ws.Path(<span class="string">"/metrics"</span>)</span><br><span class="line">ws.Route(ws.GET(<span class="string">""</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">ws.Route(ws.GET(<span class="string">"/cadvisor"</span>).</span><br><span class="line">To(s.getMetrics))</span><br><span class="line">s.container.Add(ws)</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>指标对应处理请求函数为<code>getMetrics</code>,主要过程是根据<code>r.Request.Host</code>找到对应Session(cloudstream维护了到每个edge节点的session池,Key即edge的Internal Ip,每个session负责向Tunnel发送和读取Message),创建新的处理的协程,并将其添加到任务池<code>Session.apiServerConn</code>中,处理协程中将真实的请求数据(原请求)<code>编码</code>为Message,并通过通道发送edgenode1的edgestream进行处理。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubeedge-day-01/2020-10-27-14-42-51.png" alt></p><p>🧐此时,请求已经被发送到edgenode1上,且被edgestream的轮询监听函数收到,edgestream会通过函数<code>ReadMessageFromTunnel</code>对消息进行解码, 判断消息的类型,这里显然是一个<em>Metrics类型</em>的消息。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReadMessageFromTunnel</span><span class="params">(r io.Reader)</span> <span class="params">(*Message, error)</span></span> {</span><br><span class="line">buf := bufio.NewReader(r)</span><br><span class="line">connectID, err := binary.ReadUvarint(buf)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">}</span><br><span class="line">messageType, err := binary.ReadUvarint(buf)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">}</span><br><span class="line">data, err := ioutil.ReadAll(buf)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">}</span><br><span class="line">klog.Infof(<span class="string">"Receive Tunnel message Connectid %d messageType %s data:%v string:[%v]"</span>,</span><br><span class="line">connectID, MessageType(messageType), data, <span class="keyword">string</span>(data))</span><br><span class="line"><span class="keyword">return</span> &Message{</span><br><span class="line">ConnectID: connectID,</span><br><span class="line">MessageType: MessageType(messageType),</span><br><span class="line">Data: data,</span><br><span class="line">}, <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果是合法的消息,edgestream创建新的协程进行处理,并把这些处理协程添加统一的任务池中,这里Metrics类型的处理过程为,将Message进行<code>解码</code>得到原始请求数据,并发起真实请求(这时已经成功穿透到内网啦),并将Metrics数据在编码成Message通过通道发送到cloudstream中。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 消息解码</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *TunnelSession)</span> <span class="title">serveMetricsConnection</span><span class="params">(m *stream.Message)</span> <span class="title">error</span></span> {</span><br><span class="line">metricsCon := &stream.EdgedMetricsConnection{</span><br><span class="line">ReadChan: <span class="built_in">make</span>(<span class="keyword">chan</span> *stream.Message, <span class="number">128</span>),</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> err := json.Unmarshal(m.Data, metricsCon); err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"unmarshal connector data error %v"</span>, err)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">s.AddLocalConnection(m.ConnectID, metricsCon)</span><br><span class="line"><span class="keyword">return</span> metricsCon.Serve(s.Tunnel)</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">func</span> <span class="params">(ms *EdgedMetricsConnection)</span> <span class="title">Serve</span><span class="params">(tunnel SafeWriteTunneler)</span> <span class="title">error</span></span> {</span><br><span class="line">client := http.Client{}</span><br><span class="line">req, err := http.NewRequest(<span class="string">"GET"</span>, ms.URL.String(), <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"create new metrics request error %v"</span>, err)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">req.Header = ms.Header</span><br><span class="line"><span class="comment">// 目前通道只支持Text message,这里解决了Prometheus抓取失败的问题</span></span><br><span class="line">req.Header.Set(<span class="string">"accept-encoding"</span>, <span class="string">"identity"</span>)</span><br><span class="line">resp, err := client.Do(req)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"request metrics error %v"</span>, err)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"><span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line">scan := bufio.NewScanner(resp.Body)</span><br><span class="line">stop := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>{})</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"><span class="keyword">for</span> mess := <span class="keyword">range</span> ms.ReadChan {</span><br><span class="line"><span class="keyword">if</span> mess.MessageType == MessageTypeRemoveConnect {</span><br><span class="line">klog.Infof(<span class="string">"receive remove client id %v"</span>, mess.ConnectID)</span><br><span class="line"><span class="built_in">close</span>(stop)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}()</span><br><span class="line"></span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"><span class="keyword">for</span> retry := <span class="number">0</span>; retry < <span class="number">3</span>; retry++ {</span><br><span class="line">msg := NewMessage(ms.MessID, MessageTypeRemoveConnect, <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">if</span> err := msg.WriteTo(tunnel); err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"%v send %s message error %v"</span>, ms, msg.MessageType, err)</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> scan.Scan() {</span><br><span class="line"><span class="keyword">select</span> {</span><br><span class="line"><span class="keyword">case</span> <-stop:</span><br><span class="line">klog.Infof(<span class="string">"receive stop single, so stop metrics scan ..."</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 10 = \n</span></span><br><span class="line">msg := NewMessage(ms.MessID, MessageTypeData, <span class="built_in">append</span>(scan.Bytes(), <span class="number">10</span>))</span><br><span class="line">err := msg.WriteTo(tunnel)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"write tunnel message %v error"</span>, msg)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">klog.Infof(<span class="string">"%v write metrics data %v"</span>, ms.String(), <span class="keyword">string</span>(scan.Bytes()))</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>最后一步,终于要结束啦😅!cloudstream会读取Message,解码得到用户需要的指标数据,并响应请求。</p><figure class="highlight golang"><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Session)</span> <span class="title">Serve</span><span class="params">()</span></span> {</span><br><span class="line"><span class="keyword">defer</span> s.Close()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> {</span><br><span class="line">t, r, err := s.tunnel.NextReader()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"get %v reader error %v"</span>, s, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> t != websocket.TextMessage {</span><br><span class="line">klog.Errorf(<span class="string">"Websocket message type must be %v type"</span>, websocket.TextMessage)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line">message, err := stream.ReadMessageFromTunnel(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"Read message from tunnel %v error %v"</span>, s.String(), err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := s.ProxyTunnelMessageToApiserver(message); err != <span class="literal">nil</span> {</span><br><span class="line">klog.Errorf(<span class="string">"Proxy tunnel message [%s] to kube-apiserver error %v"</span>, message.String(), err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>思考一下通过拦截流量实现穿透有什么弊端?</p></blockquote><h2 id="讲讲跳过的坑"><a href="#讲讲跳过的坑" class="headerlink" title="讲讲跳过的坑"></a>讲讲跳过的坑</h2><p>讲讲我在验证和学习kubeedge网络设计时的故事(坑爹过程):</p><ol><li>Kubernetes的Node的<code>Status.daemonEndpoints.KubeletEndpoint.Port</code>你知道吗?其实当我们<code>kubectl exec</code>时,最终发起向kubelet请求,kubelet的端口就是由该字段确定。</li><li>calico复杂的iptables规则,让容器的流量变得十分怪异,最终只能使用hostNetwork模式啦🤬</li><li>你知道prometheus抓取端口时,会自动加上请求头<code>Accept-Encoding: gzip</code>,并且kubeedge tunnel目前不支持Binary Message吗?</li><li>国家统计局相关统计数据表明,那两天我敲的curl命令可绕地球两圈,prometheus的重装次数高达百万次!👊</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>KubeEdge有非常良好的前景,这主要得力于<code>云边协同</code>的思想,虽然目前开源的KubeEdge整体的方案还不完善,但是,随着用户的增多和社区的发展,KubeEdge将会越来越成熟,对于想要加入KubeEdge社区的朋友,目前也是最佳时机。同时,我们基于KubeEdge所做的产品化工作目前还处于初级阶段,但是有了Kubernetes的产品化改造的经验,相关工作也在有条不紊的进行。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>除了官方仓库和文档,以下资料也十分有价值。</p><ol><li><a href="https://kccncna19.sched.com/event/Uahz">KubeEdge-Kubernetes Native Edge Computing Framework(2019-kubecon)</a></li><li><a href="https://kccncna19.sched.com/event/Uafu">KubeEdge Deep Dive(2019-kubecon)</a></li><li><a href="https://mp.weixin.qq.com/s/LS5QnDwVIMVLTuB_yvdjoA">kubeEdge架构解读</a></li><li><a href="https://www.bilibili.com/video/BV1LJ411D7t1">华为云课堂-KubeEdge实战宝典</a></li><li><a href="https://zhuanlan.zhihu.com/p/32553477">QUIC-Quick UDP Internet Connection</a>下</li></ol>]]></content>
<tags>
<tag> kubeedge </tag>
<tag> 边缘计算 </tag>
</tags>
</entry>
<entry>
<title>使用kubebuilder开发简单的Operator</title>
<link href="/2020/10/12/kubernetes-crd-day1/"/>
<url>/2020/10/12/kubernetes-crd-day1/</url>
<content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-08-14-11-12-52.png" alt></p><p>云原生技术通过方法论、工具集和最佳实践重塑着整个软件技术栈和生命周期,云原生对云计算服务方式与互联网架构进行整体性升级,深刻改变着整个 IT 领域。云原生的核心是 kubernetes,<em>围绕 kubernetes 构建满足自身需求的 PaaS 平台(应用中心)</em>是绝大数企业的诉求,但是不同企业自身场景往往存在一定的差异,Operator 是最常见 kubernetes 拓展方式。本片博文,我将会给大家理清 Operator 的来龙去脉,同时介绍如何通过 kubebuilder 快速开发一个简单的 Operator。</p><h2 id="Operator-诞生的背景"><a href="#Operator-诞生的背景" class="headerlink" title="Operator 诞生的背景"></a>Operator 诞生的背景</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-09-04-13-59-02.png" alt>kubernetes 无法做到真正意义的开箱即用的,它与传统的 PaaS 平台不同,它仅仅只提供核心的基础设施功能,但是还无法满足用户的最终需求,这里用户主要指业务开发和业务运维,比如说业务开发需要 CI/CD 工具实现 Devops 的功能,原生 kubernetes 是不提供支持的,但是我们可以通过<code>tekton</code>这一个第三方工具实现 DevOps 相关功能,这也正是 kubernetes 区别传统PaaS平台的真正强大之处,其提供完善的扩展机制以及基于此而发展出来的海量的第三方工具和丰富的生态。</p><p><code>Operator pattern</code>首先由 CoreOS 提出,通过结合 CRD 和 custom controller 将特定应用的运维知识转换为代码,实现应用运维的自动化和智能化。Operator 允许 kubernetes 来管理复杂的,有状态的分布式应用程序,并由 kubernetes 对其进行自动化管理,例如,etcd operator 能够创建并管理一组 etcd 集群,定制化的 controller 组件了解这些资源,知道如何维护这些特定的应用。</p><p>随着 kubernetes 的功能越来越复杂,其需要管理的资源在高速增长,对应的 API 和 controller 的数量也愈发无法控制,kubernetes 变得很臃肿,很多不必要的 API 和功能将出现在每次安装的集群中。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-09-04-13-53-39.png" alt></p><p>为了解决这个问题,CRD 应运而生,CRD 由 TPR(Third Part Resource v1.2 版本引入)演化而来,v1.7 进入 beta,v1.8 进入稳定,通过 CRD,kubernetes 可以动态的添加并管理资源。CRD 解决了结构化数据存储的问题,Controller 则用来跟踪这些资源,保证资源的状态满足期望值。<code>CRD+Controller=decalartive API</code>,声明式 API 设计是 kubernetes 重要的设计思想,该设计保证能够动态扩展 kubernetes API,这种模式也正是 Operator pattern。</p><p>kubernetes 本身也在通过 CRD 添加新功能,我们有什么理由不使用呢?</p><h2 id="使用场景总结及举例"><a href="#使用场景总结及举例" class="headerlink" title="使用场景总结及举例"></a>使用场景总结及举例</h2><p>CRD+custom controller 已经被广泛地使用,按使用场景可划分为以下两种:</p><ol><li>通用型 controller: 这种场景和 kubernetes 内置的<code>apps controller</code>类似,主要解决特定通用类型应用的管理</li><li>Operator: 该场景用于解决一个特定应用的自动化管理</li></ol><p>通用型 controller 往往是 kubernetes 平台侧用户,如各大云厂商和 kubernetes 服务提供商,Operator 则是各种软件服务提供商,他们设计时面向单一应用,很多开源的应用的 operator 可以在 operator hub 中获取。我列举一些示例供大家参考:</p><h3 id="通用型-Controller"><a href="#通用型-Controller" class="headerlink" title="通用型 Controller"></a>通用型 Controller</h3><ol><li>阿里的 cafedeploymentcontroller 解决金融场景下分布式应用特殊需求。</li><li>oam-kubernetes-runtime 实现了 Application Model (OAM),以系统可持续的方式拓展 kubernetes</li></ol><h3 id="Operator"><a href="#Operator" class="headerlink" title="Operator"></a>Operator</h3><ol><li>etcd operator</li><li>Prometheus operator</li></ol><p>通用型Controller与kubernetes自带的几个controller类似,旨在解决一些通用的应用模型,而Operator则更加面向单个特定应用,这两者没有本质的区别。</p><h2 id="如何开发-CRD"><a href="#如何开发-CRD" class="headerlink" title="如何开发 CRD"></a>如何开发 CRD</h2><p>作为kubernetes开发者,如何开发 CRD+Custom cntroller 呢?其实官方提供示例项目<a href>sample-controller</a>供开发者参考,开发流程大致有以下几个过程:</p><ol><li>初始化项目结构(可根据 sample controller 修改)</li><li>定义 CRD</li><li>生成代码</li><li>初始化 controller</li><li>实现 controller 具体逻辑</li></ol><p>其中步骤 2,5 是核心业务逻辑,其余步骤完全可以通过自动生成的方式省略,到目前,社区有两个成熟的脚手架工具用于简化开发,一个是有 kube-sig 维护的 kubebuilder,另一个是由 redhat 维护的 operator-sdk,这两个工具都是基于 controller-runtime 项目而实现,用户可自行选择,笔者用的是 kubebuilder。使用 kubebuilder 能够帮助我们节省以下工作:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-09-04-15-08-31.png" alt></p><p>如果你想要快速构建 CRD 和 Custom controller,脚手架工具是个不错的选择,如果是学习目的,建议结合 sample-controller 和 kubernetes controller 相关源码。</p><h2 id="kubebuilder-详解"><a href="#kubebuilder-详解" class="headerlink" title="kubebuilder 详解"></a>kubebuilder 详解</h2><p>kubebuilder 是一个帮助开发者快速开发 kubernetes API 的脚手架命令行工具,其依赖库 controller-tools 和 controller-runtime,controller-runtime 简化 kubernetes controller 的开发,并且对 kubernetes 的几个常用库进行了二次封装,以简化开发者使用。controller-tool 主要功能是代码生成。下图是使用 kubebuilder 的工作流程图:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-09-07-14-01-08.png" alt></p><p>文章后面会结合一个简单示例来介绍开发流程。</p><blockquote><p>kubebuilder 有非常良好的文档,包括一个从零开始的示例,您应该以文档为主。</p></blockquote><h2 id="使用-kubebuilder-开发一个-CRD"><a href="#使用-kubebuilder-开发一个-CRD" class="headerlink" title="使用 kubebuilder 开发一个 CRD"></a>使用 kubebuilder 开发一个 CRD</h2><p>本次示例创建一个通用的<code>Application</code>资源,Application 包含一个子资源 Deployment 以及一个 Count 资源,每当 Application 进行被重新协调<strong>Reconcil</strong>,Count 会进行自增。</p><blockquote><p>全部代码请参考代码仓</p></blockquote><h3 id="前提(你需要提前了解的)"><a href="#前提(你需要提前了解的)" class="headerlink" title="前提(你需要提前了解的)"></a>前提(你需要提前了解的)</h3><ol><li>Golang 开发者,kubernetes 大量使用<code>Code Generate</code>这一功能来自动生成重复性代码</li><li>阅读 kubernetes controller 的代码</li><li>阅读 kubebuilder 的文档</li><li>了解 kustomize</li></ol><h3 id="开发步骤及主要代码展示"><a href="#开发步骤及主要代码展示" class="headerlink" title="开发步骤及主要代码展示"></a>开发步骤及主要代码展示</h3><p>首先,根据你的开发环境安装 kubebuilder 工具,mac 下通过 homebrew 安装命令如下:</p><figure class="highlight bash"><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">➜ ~ brew install kubebuilder</span><br><span class="line">➜ ~ kubebuilder version</span><br><span class="line">Version: version.Version{KubeBuilderVersion:<span class="string">"unknown"</span>, KubernetesVendor:<span class="string">"unknown"</span>, GitCommit:<span class="string">"<span class="variable">$Format</span>:%H$"</span>, BuildDate:<span class="string">"1970-01-01T00:00:00Z"</span>, GoOs:<span class="string">"unknown"</span>, GoArch:<span class="string">"unknown"</span>}</span><br></pre></td></tr></table></figure><p>安装完毕后,首先创建项目目录<code>custom-controller</code>并使用<code>go mod</code>初始化项目</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">➜ <span class="built_in">cd</span> custom-controllers</span><br><span class="line">➜ ls</span><br><span class="line">➜ go mod init controllers.happyhack.io</span><br></pre></td></tr></table></figure><p>接着,使用 kubebuilder 初始化项目,生成相关文件和目录,并创建 CRD 资源</p><figure class="highlight bash"><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="comment"># 使用kubebuilder初始化项目</span></span><br><span class="line">➜ custom-controllers kubebuilder init --domain controller.daocloud.io --license apache2 --owner <span class="string">"Holder"</span></span><br><span class="line"><span class="comment"># 创建CRD资源</span></span><br><span class="line">➜ custom-controllers kubebuilder create api --group controller --version v1 --kind Application</span><br><span class="line">Create Resource [y/n]</span><br><span class="line">y</span><br><span class="line">Create Controller [y/n]</span><br><span class="line">y</span><br><span class="line">Writing scaffold <span class="keyword">for</span> you to edit...</span><br><span class="line">api/v1/application_types.go</span><br><span class="line">controllers/application_controller.go</span><br><span class="line">Running make:</span><br><span class="line">$ make</span><br><span class="line">/Users/donggang/Documents/Code/golang/bin/controller-gen object:headerFile=<span class="string">"hack/huilerplate.go.txt"</span> paths=<span class="string">"./..."</span></span><br><span class="line">go fmt ./...</span><br><span class="line">go vet ./...</span><br><span class="line">go build -o bin/manager main.go</span><br></pre></td></tr></table></figure><p>到目前,该项目就已经能够运行了,不过我们需要添加我们自己业务代码,主要包括修改 CRD 定义和添加 controller 逻辑两部分。首先修改 API 资源定义即 CRD 定义,Application 包含一个 Deployment,我们可以参考 kubernetes Deployment 与 Pod 这两个类型之间的关系设计 Application 和 Deployment,Deployment 通过字段<code>spec.template</code>来描述如何创建 Pod,<code>DeploymentTemplateSpec</code>描述了该如何创建 Deployment,</p><figure class="highlight golang"><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="comment">// PodTemplateSpec describes the data a deployment should have when created from a template</span></span><br><span class="line"><span class="keyword">type</span> DeploymentTemplateSpec <span class="keyword">struct</span> {</span><br><span class="line"><span class="comment">// Standard object's metadata.</span></span><br><span class="line"><span class="comment">// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata</span></span><br><span class="line"><span class="comment">// +optional</span></span><br><span class="line">metav1.ObjectMeta <span class="string">`json:"metadata,omitempty"`</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Specification of the desired behavior of the pod.</span></span><br><span class="line"><span class="comment">// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status</span></span><br><span class="line">Spec v12.DeploymentSpec <span class="string">`json:"spec,omitempty"`</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>API 描述定义完成了,接下来需要我们来进行具体业务逻辑实现了,编写具体 controller 实现,首先我们简单梳理 controller 的主要逻辑</p><ul><li>当一个 application 被创建时,需要创建对应的 deployment,当 application 被删除或更新时,对应 Deployment 也需要被删除或更新</li><li>当 application 对应的子资源 deployment 被其他客户端删除或更新时,controller 需要重建或恢复它</li><li>最后一步更新 application 的 status,这里即 count 加 1</li></ul><p>我们在方法<code>func(r *ApplicationReconciler) Reconcile(req ctrl.Request)(ctrl.Result,error)</code>实现相关逻辑,当然当业务逻辑比较复杂时,可以拆分为多个方法。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *ApplicationReconciler)</span> <span class="title">Reconcile</span><span class="params">(req ctrl.Request)</span> <span class="params">(ctrl.Result, error)</span></span> {</span><br><span class="line">ctx := context.Background()</span><br><span class="line">log := r.Log.WithValues(<span class="string">"application"</span>, req.NamespacedName)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> app controllerv1.Application</span><br><span class="line"><span class="keyword">if</span> err := r.Get(ctx, req.NamespacedName, &app); err != <span class="literal">nil</span> {</span><br><span class="line">log.Error(err, <span class="string">"unable to fetch app"</span>)</span><br><span class="line"><span class="keyword">return</span> ctrl.Result{}, client.IgnoreNotFound(err)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">selector, err := metav1.LabelSelectorAsSelector(app.Spec.Selector)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">log.Error(err, <span class="string">"unable to convert label selector"</span>)</span><br><span class="line"><span class="keyword">return</span> ctrl.Result{}, err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> deploys v12.DeploymentList</span><br><span class="line"><span class="keyword">if</span> err := r.List(ctx, &deploys, client.InNamespace(req.Namespace), client.MatchingLabelsSelector{Selector: selector}); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">if</span> errors.IsNotFound(err) {</span><br><span class="line">deploy, err := r.constructDeploymentForApplication(&app)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">log.Error(err, <span class="string">"unable to construct deployment"</span>)</span><br><span class="line"><span class="keyword">return</span> ctrl.Result{</span><br><span class="line">RequeueAfter: time.Second * <span class="number">1</span>,</span><br><span class="line">}, err</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> err = r.Create(ctx, deploy); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> ctrl.Result{RequeueAfter: time.Second * <span class="number">1</span>}, err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>完成<code>Reconcile</code>方法后,我们可以修改config目录的示例yaml,来进行本地测试了。</p><h2 id="官方开发自定义-Controller-的指导"><a href="#官方开发自定义-Controller-的指导" class="headerlink" title="官方开发自定义 Controller 的指导"></a>官方开发自定义 Controller 的指导</h2><p>kubernetes开箱自带了多个controller,这些controller在我们开发时具有非常重要的参考价值,同时社区也总结了的 controller 开发所需要遵循十一条原则,但是请大家结合实际场景灵活运用这些原则:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/kubernetes-crd-day1/2020-10-12-13-38-01.png" alt></p><h2 id="总结及展望"><a href="#总结及展望" class="headerlink" title="总结及展望"></a>总结及展望</h2><p>本文简单介绍了 CRD 以及如何使用脚手架工具 kubebuilder 帮助我们开发自定义 controller,当然这个 controller 示例的逻辑比较简单,在实际场景中,我们会遇到很多的挑战,比如controller 的逻辑会比较复杂、需要通过多个controller等。作为kubernetes开发者, Controller开发是一项必不可少的技能。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://kccna18.sched.com/event/GrUR">controller-runtime 项目将我们从 copy-and-paste 解放出来</a></li><li><a href="https://static.sched.com/hosted_files/kccncchina2018english/92/Kubebuilder_KubeCon_China.pdf">kubecon kubebuilder 简介</a></li><li><a href="https://github.com/kubernetes/community/blob/a0fdd9ccfa6d5a6b17d8d2d3eec1d2e1ee12f3c4/contributors/devel/controllers.md">kubernetes commmunity 关于开发自定义 controller 指导</a></li><li><a href="https://tiewei.github.io/posts/kubebuilder-vs-operator-sdk">operator 开发工具对比 kubebuilder vs operator sdk</a></li><li><a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">kubernetes operator 官方开发指导</a></li><li><a href="https://www.sofastack.tech/blog/sofa-channel-7-retrospect/">阿里 CAFEDeploymentcontroller 设计</a></li></ul>]]></content>
<tags>
<tag> kubernetes </tag>
<tag> operator </tag>
</tags>
</entry>
<entry>
<title>Mac安装新版本Bash</title>
<link href="/2020/07/16/upgrade-bash-on-macOS/"/>
<url>/2020/07/16/upgrade-bash-on-macOS/</url>
<content type="html"><![CDATA[<p>很多macOS的用户开发者可能不知道他们一直在使用非常老版本的<code>Bash shell</code>,部分原因是很多用户使用的是<code>zsh</code>。但是,升级Bash其实是非常有意义的,除了能够使用新特性之外,还能够避免一些意外的错误。</p><blockquote><p>写在前面:我在给一些项目如kubernetes提PR时,项目中脚本经常执行不成功。还有进行Hack工具开发completion功能时,测试completion一直无法成功。问题的根源就是Bash版本的问题。虽然在日常开发过程中,我一直使用zsh,但是安装新版本Bash对于本地开发调试十分方便。</p></blockquote><h2 id="Mac默认的Bash"><a href="#Mac默认的Bash" class="headerlink" title="Mac默认的Bash"></a>Mac默认的Bash</h2><p>执行以下命令查看macOS内置Bash版本:</p><figure class="highlight bash"><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">$ bash --version</span><br><span class="line">GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)</span><br><span class="line">Copyright (C) 2007 Free Software Foundation, Inc.</span><br></pre></td></tr></table></figure><p>可以发现,macOS使用的是<strong>3.2</strong>版本的GNU Bash,发行于2007年!到目前为止,即使是最新的MacOS,默认Bash都是这个版本。</p><p>苹果在其操作系统上使用如此旧版本的Bash和许可(licensing)有关。Bash从4.0版本就使用<a href="https://www.gnu.org/licenses/gpl.html">GNU General Public License V3</a>,但是苹果并不想支持这个许可,相关讨论可以查看<a href="https://www.reddit.com/r/apple/comments/7v97ls/why_doesnt_apple_use_any_gpl_v3_unix_packages_in/">这里</a>和<a href="http://meta.ath0.com/2012/02/05/apples-great-gpl-purge/">这里</a>,3.2版本的GNU Bash使用的GPLv2许可,这是苹果能够接受的许可,所以苹果一直使用该版本Bash。</p><p>这意味这整个世界都在使用新版本的Bash,可是macOS的用户却仍在使用10多年前的版本,到目前为止,GNU Bash的最新版本是5.0,发布于2019年1月,本文将指导大家如何升级到最新版本。</p><h2 id="为什么需要升级"><a href="#为什么需要升级" class="headerlink" title="为什么需要升级"></a>为什么需要升级</h2><p>很多人要问Bash 3.2使用正常,为什么还需要升级到最新版本呢?对我个人而言,执行一些第三方脚本会出现错误,如<a href="https://github.com/kubernetes/kubernetes/issues/81657#issuecomment-523166449">kubernetes staticcheck.sh</a>,还有一个重要的原因是<code>Programmable completion</code>补全,Bash自动补全功能大家都有使用过,您可以自动补全命令、文件名和变量,方法是先键入,然后按Tab键自动完成当前单词(按两次显示列表),这是Bash的自动补全。</p><p>但是可编程式补全功能更加强大,因为它允许依赖于上下文的特定于命令的补全。比如输入<code>cmd -[Tab][Tab]</code>,仅会显示该命令接受的选项,再比如输入<code>cmd host rm [tab][tab]</code>会显示特定配置文件中host列表,可编程式补全能够实现这些功能。</p><p>可编程式补全需要定义补全逻辑文件,通常以<strong>completion scripts</strong>存在,由命令的开发人员维护。这些补全脚本需要被<code>source</code>才能够开启。</p><p>问题是Bash的可编程完成特性从3.2版本开始就得到了扩展,而且大多数补全脚本都使用了这些新特性,这也是很多补全功能在Mac中不能正常工作的原因,如果你不升级,你将不能使用这种补全功能。</p><p>升级新版本能够解决这些问题,如果你需要详细了解macOS Bash的可编程式补全功能,你可以参考<a href="https://medium.com/@weibeld/programmable-completion-for-bash-on-macos-f81a0103080b">Programmable Completion for Bash on macOS</a>。</p><h2 id="如何升级"><a href="#如何升级" class="headerlink" title="如何升级"></a>如何升级</h2><p>macOS系统升级最新版本Bash,你需要做一下三件事:</p><ol><li>安装最新Bash</li><li>将最新Bash添加到“白名单”</li><li>设置为默认shell(可选)</li></ol><p>每一步都十分简单。</p><blockquote><p>下面的说明并不更改旧版本的Bash,而是安装新版本并将其设置为默认shell。这两个版本将同时存在于您的系统中,但是您可以从此忽略旧版本。</p></blockquote><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>当然建议通过<code>Homebrew</code>安装:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install bash</span><br></pre></td></tr></table></figure><p>等待安装完成,验证你系统的老版本的Bash和刚下载的新版本:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">which</span> -a bash</span><br><span class="line">/usr/<span class="built_in">local</span>/bin/bash</span><br><span class="line">/bin/bash</span><br></pre></td></tr></table></figure><p>第一个是新版本,第二个是内置3.2版本:</p><figure class="highlight bash"><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">$ /usr/<span class="built_in">local</span>/bin/bash --version</span><br><span class="line">GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)</span><br><span class="line">Copyright (C) 2019 Free Software Foundation, Inc.</span><br><span class="line">License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html></span><br><span class="line">This is free software; you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line">$ /bin/bash --version</span><br><span class="line">GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)</span><br><span class="line">Copyright (C) 2007 Free Software Foundation, Inc.</span><br></pre></td></tr></table></figure><p>因为新版本的所在目录(/usr/local/bin)在内置版本所在目录(/bin)之前(PATH环境变量),所以当你输入<code>bash</code>,将会使用新版本。</p><figure class="highlight bash"><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">$ bash --version</span><br><span class="line">GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>到目前为止,已经默认新版本Bash了。(很多用户到这步就ok了,没有必要进行如下配置)</p><h3 id="白名单"><a href="#白名单" class="headerlink" title="白名单"></a>白名单</h3><p>UNIX包括了一些安全特性限制哪些shell能够做为<a href="https://stackoverflow.com/questions/18186929/what-are-the-differences-between-a-login-shell-and-interactive-shell#:~:text=An%20interactive%20shell%20generally%20reads%20from%20and%20writes%20to%20a%20user's%20terminal.&text=An%20interactive%20shell%20is%20one%20which%20reads%20commands%20from%20it's,shell%20and%20an%20interactive%20one.">login shell</a>,你需要将信任的shell添加到/etc/shells中,一个shell只有在这列表才能够作为默认shell。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo vim /etc/shells</span><br></pre></td></tr></table></figure><p>将/usr/local/bin/bash shell添加到该文件中,添加完成后的内容和一下内容类似:</p><figure class="highlight text"><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">/bin/bash</span><br><span class="line">/bin/csh</span><br><span class="line">/bin/ksh</span><br><span class="line">/bin/sh</span><br><span class="line">/bin/tcsh</span><br><span class="line">/bin/zsh</span><br><span class="line">/usr/local/bin/bash</span><br></pre></td></tr></table></figure><h3 id="设置为默认shell"><a href="#设置为默认shell" class="headerlink" title="设置为默认shell"></a>设置为默认shell</h3><p>到这里,你打开一个新的terminal window仍然使用的内置Bash,因为默认shell仍然是内置版本Bash。 你已通过以下命令修改默认shell。 </p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ chsh -s /usr/<span class="built_in">local</span>/bin/bash</span><br></pre></td></tr></table></figure><p>重新打开一个终端窗口,默认shell已经修改完成。</p><h2 id="需要注意的点"><a href="#需要注意的点" class="headerlink" title="需要注意的点"></a>需要注意的点</h2><h3 id="脚本中的用法"><a href="#脚本中的用法" class="headerlink" title="脚本中的用法"></a>脚本中的用法</h3><p>之前提到过,以上操作并不是升级内置Bash版本,而是安装了一个新版本,所以系统存在两个版本的Bash。在shell脚本中,你经常有一个像下面一样的<strong>shebang</strong>行。 </p><figure class="highlight text"><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">#!/bin/bash</span><br><span class="line">echo $BASH_VERSION</span><br></pre></td></tr></table></figure><p>可以发现脚本指定的是/bin/bash,执行脚本(<strong>通过路径而不是bash</strong>)得到的输出可以发现使用的是内置Bash。</p><p>如果你需要使用新版本的Bash,你需要这样写。</p><figure class="highlight text"><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">#!/usr/local/bin/bash</span><br><span class="line">echo $BASH_VERSION</span><br></pre></td></tr></table></figure><p>很多人发现,这两个脚本具有不同的shebang行,所以没办法迁移,如果需要在其他系统执行,需要修改脚本文件。通过比较以上两个脚本,你可以这些编写shebang行。</p><figure class="highlight text"><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">#!/usr/local/bin/bash</span><br><span class="line">echo $BASH_VERSION</span><br></pre></td></tr></table></figure><p>这是推荐的shebang行的写法,它会使用PATH中第一个发现的Bash版本执行脚本文件。</p><h3 id="为什么不使用软链接"><a href="#为什么不使用软链接" class="headerlink" title="为什么不使用软链接"></a>为什么不使用软链接</h3><p>我们可不可以直接删除旧版本的Bash,把新版本的Bash放到旧版本的位置呢?比如,创建一个软链接/bin/bash指向新版本呢,就像下面一样:</p><figure class="highlight bash"><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">$ sudo rm /bin/bash</span><br><span class="line">$ sudo ln -s /usr/<span class="built_in">local</span>/bin/bash /bin/bash</span><br></pre></td></tr></table></figure><p>这样shebang行<code>#!/bin/bash</code>将会使用新版本Bash,那为什么不能做呢?</p><p>您可以执行此操作,但必须避开称为系统完整性保护(SIP)(Wikipedia)的macOS安全功能。此功能甚至禁止root用户对某些目录进行写访问(这就是为什么它也称为“无根”)的原因。这些目录在此处列出,并包含/bin。这意味着即使以root用户身份,您也不能执行上述命令,因为不允许您从/bin中删除任何内容或在/bin中创建任何文件。</p><p>解决方法是禁用SIP,在/bin中进行更改,然后再次启用SIP。可以根据<a href="https://developer.apple.com/library/archive/documentation/Security/Conceptual/System_Integrity_Protection_Guide/ConfiguringSystemIntegrityProtection/ConfiguringSystemIntegrityProtection.html">此处</a>的说明来启用和禁用SIP。它要求您将计算机引导到恢复模式,然后使用csrutil disable和csrutil enable命令。如果您想完全取代旧的Bash版本,还是要满足于同时使用两个Bash版本,则取决于您自己。</p><h2 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h2><ol><li>macOS Catalina系统,zsh将作为默认shell。</li></ol><h2 id="译自"><a href="#译自" class="headerlink" title="译自"></a>译自</h2><p><a href="https://itnext.io/upgrading-bash-on-macos-7138bd1066ba">upgrading bash on macOS</a></p>]]></content>
<categories>
<category> 译文 </category>
</categories>
<tags>
<tag> Mac </tag>
<tag> Shell </tag>
</tags>
</entry>
<entry>
<title>EFK日志平台</title>
<link href="/2020/07/14/efk/"/>
<url>/2020/07/14/efk/</url>
<content type="html"><![CDATA[<p><code>云原生技术</code>的宗旨是帮助企业组织在现代动态的环境下(公有云,私有云和混合云)高效管理应用的全生命周期,围绕<code>kubernetes</code>,用户可以灵活选择各个模块组件,搭建平台以满足多样需求。<a href="https://landscape.cncf.io/">云原生风景图</a>完整地展示了企业实践云原生所需要处理的问题。这张图很有参考意义。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-09-48-06.png" alt></p><p>图中将<code>日志,监控和链路追踪</code>都放到了<code>监控和分析</code>,这是一个趋势,背负着统一<code>OpenTrace</code>和<code>OpenCensue</code>的<code>OpenTelemetry</code>就是为了实现<code>Metrics</code>、<code>Tracing</code>和<code>Logging</code>的融合。这三者的融合才是一个完整的用户故事。基于 Metrics 的告警及时发现异常,通过 Tracing 定位问题的模块,根据模块的日志找到问题的根源,最后基于问题,调整 Metrics(增加或者调整告警的阀值等),以便下次可以更早发现或预防问题。</p><p><code>日志,监控和链路追踪</code>三中平台的基本架构是一致的:数据收集 -> 数据处理 -> 数据展示(告警),主要差异是数据源的不一致,所以搭建三套平台显然很冗余,也不便于维护管理。但是很可惜,目前很没有一套成熟的方案,不过相信随着社区的发展,不会等太久。 </p><p>结合最近的工作,本片博文,我将给大家简单介绍EFK Stack日志平台,并如何通过Helm快速部署一套EFK集群。本文内容较为浅显,适合刚接触日志平台的人阅读🤣(因为笔者也没有EFK丰富的实践经验,splunk的使用经验不知道能不能算)。EFK涉及的组件文档十分完善,还请以文档作为主要参考。</p><h2 id="主流的方案"><a href="#主流的方案" class="headerlink" title="主流的方案"></a>主流的方案</h2><ol><li>Splunk Enterprise:Splunk 公司产品,生态丰富且成熟度较高,SPEL 查询语言功能强大,维护方便,缺点是收费;</li><li>Elastic Stack(ELK Stack):由 Elasticsearch,Logstash,Kibana,Filebeats 组件组成,生态丰富,不完全开源,需要订阅才能解锁一些高级(🤣不常用)功能;</li><li>EFK Stack: 和Elastic Stack的组件类似,不过日志收集器使用的是fluented,fluented对容器应用的支持很完善,和kubernetes集群更加匹配;</li><li>PLG Stack(Grafana Loki): 由 Promtail,Loki 和 Grafana 组成,Grafana 强大的可视化加成,借鉴了 Prometheus 的设计思路;</li></ol><h2 id="EFK-方案"><a href="#EFK-方案" class="headerlink" title="EFK 方案"></a>EFK 方案</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-14-18-23.png" alt></p><p>kubernetes 官方中 Elasticsarch 插件包含 Elasticsearch,Fluented 和 kibana 三个,Elasticsearch 负责存储日志数据的存储和索引查询,Fluented 从集群收集数据并发送到 Elasticsearch,Kibana 提供数据的可视化功能。</p><h2 id="组件介绍"><a href="#组件介绍" class="headerlink" title="组件介绍"></a>组件介绍</h2><h3 id="Elasticsearch(es)"><a href="#Elasticsearch(es)" class="headerlink" title="Elasticsearch(es)"></a>Elasticsearch(es)</h3><p>一般来说,根据集群的规模和日志量来部署 es 集群,数据存储在不同节点的碎片中,集群有多个类型节点组成,以提高可用性,有以下类型的节点:</p><ul><li>主节点(Master node):集群控制器,最少需要 3 个,采用的主从模式</li><li>数据节点(Data node):保存索引数据并且执行数据查询任务</li><li>提取节点(Ingest node):通过 ingest pipeline 对文档执行预处理操作,以便在索引文档之前对文档进行转换或者增强。</li><li>协调节点(Coordinationg node):协调保存在不同节点数据的搜索请求,索引请求之类的操作,这种类型的节点需要足够的内存和 CPU 资源;</li></ul><p>一个节点默认情况下可以同时是多个类型的节点,当集群增大时,最好将其分开配置。下图显示如何将数据存储在主碎片和副本碎片中,以便在节点之间分散负载并提高数据的可用性。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-15-31-22.png" alt></p><p>每个碎片中的数据存储在一个反向索引中(inverted index),下图展示了数据如何存储在反向索引中:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-15-44-19.png" alt></p><h3 id="Fluented"><a href="#Fluented" class="headerlink" title="Fluented"></a>Fluented</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-16-06-10.png" alt>Fluented 是开源日志收集器,是 CNCF 的毕业项目,通过丰富的插件系统实现多种日志源的收集,处理和转发功能。</p><p>fluentd 既可以作为日志收集器安装到每一个结点上, 也可以作为一个服务端收集各个结点上报的日志流。你甚至也可以在各个结点上都部署 fluentd 收集日志,然后上报到一个 fluentd 集群做统一处理,然后再转发到最终的日志存储服务器。</p><p>fluented 的核心是各种命令块(directives),每种命令块完成一项功能,最终通过组合这些命令块以 pipline 的形式处理和分发日志。</p><p>主要命令有:</p><ul><li>source:收集日志源文件,如通过一下配置收集 kubernetes 所有容器日志,如果通过容器方式部署,需要将<code>/var/lib/docker/containers</code>(默认配置)目录挂在到容器中</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><source></span><br><span class="line"> @id fluentd-containers.log</span><br><span class="line"> @type tail # Fluentd 内置的输入方式,其原理是不停地从源文件中获取新的日志。</span><br><span class="line"> path /var/log/containers/*.log # 挂载的服务器Docker容器日志地址</span><br><span class="line"> pos_file /var/log/es-containers.log.pos</span><br><span class="line"> tag raw.kubernetes.* # 设置日志标签</span><br><span class="line"> read_from_head true</span><br><span class="line"> <parse> # 多行格式化成JSON</span><br><span class="line"> @type multi_format # 使用 multi-format-parser 解析器插件</span><br><span class="line"> <pattern></span><br><span class="line"> format json # JSON 解析器</span><br><span class="line"> time_key time # 指定事件时间的时间字段</span><br><span class="line"> time_format %Y-%m-%dT%H:%M:%S.%NZ # 时间格式</span><br><span class="line"> </pattern></span><br><span class="line"> <pattern></span><br><span class="line"> format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/</span><br><span class="line"> time_format %Y-%m-%dT%H:%M:%S.%N%:z</span><br><span class="line"> </pattern></span><br><span class="line"> </parse></span><br><span class="line"></source></span><br></pre></td></tr></table></figure><ul><li>match: 指定动作,通过 tag 匹配 source,然后执行指定命令来分发日志,match 是从上往下依次匹配的,一旦一个日志流被匹配上,就会停止配置,以下配置将日志数据发送到 es 集群中。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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></pre></td><td class="code"><pre><span class="line"><match **> # 唯一标识符,后面匹配一个日志源,**代表全部数据</span><br><span class="line">@id elasticsearch # 唯一标识符</span><br><span class="line">@type elasticsearch # 支持输出插件的标识符,elasticsearch是一个内置插件</span><br><span class="line">@log_level info # 指定需要捕获日志的级别</span><br><span class="line">include_tag_key true</span><br><span class="line">type_name fluentd</span><br><span class="line">host "#{ENV['OUTPUT_HOST']}" # es主机</span><br><span class="line">port "#{ENV['OUTPUT_PORT']}" # es主机端口</span><br><span class="line">logstash_format true</span><br><span class="line"><buffer> # Fluentd 允许在目标不可用时进行缓存,比如,如果网络出现故障或者 Elasticsearch 不可用的时候。缓冲区配置也有助于降低磁盘的 IO。</span><br><span class="line">@type file</span><br><span class="line">path /var/log/fluentd-buffers/kubernetes.system.buffer</span><br><span class="line">flush_mode interval</span><br><span class="line">retry_type exponential_backoff</span><br><span class="line">flush_thread_count 2</span><br><span class="line">flush_interval 5s</span><br><span class="line">retry_forever</span><br><span class="line">retry_max_interval 30</span><br><span class="line">chunk_limit_size "#{ENV['OUTPUT_BUFFER_CHUNK_LIMIT']}"</span><br><span class="line">queue_limit_length "#{ENV['OUTPUT_BUFFER_QUEUE_LIMIT']}"</span><br><span class="line">overflow_action block</span><br><span class="line"></buffer></span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>filter: 对数据进行过滤,其和 match 不同,可以通过 filter 串联成 pipeline,对数据进行窜行处理。比如我们只采集具有<code>logging=true</code>标签的 Pod 的日志,以下是示例配置:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"># 删除无用的属性</span><br><span class="line"><filter kubernetes.**></span><br><span class="line"> @type record_transformer</span><br><span class="line"> remove_keys $.docker.container_id,$.kubernetes.container_image_id,$.kubernetes.pod_id,$.kubernetes.namespace_id,$.kubernetes.master_url,$.kubernetes.labels.pod-template-hash</span><br><span class="line"></filter></span><br><span class="line"># 只保留具有logging=true标签的Pod日志</span><br><span class="line"><filter kubernetes.**></span><br><span class="line"> @id filter_log</span><br><span class="line"> @type grep</span><br><span class="line"> <regexp></span><br><span class="line"> key $.kubernetes.labels.logging</span><br><span class="line"> pattern ^true$</span><br><span class="line"> </regexp></span><br><span class="line"></filter></span><br></pre></td></tr></table></figure><p>fluented配置文件可能比较繁琐,不过通过部署<code>fluented-elastic</code>版本,来简化配置,我们只需要在默认配置文件进行少量的修改即可。</p><h2 id="kibana"><a href="#kibana" class="headerlink" title="kibana"></a>kibana</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-17-56-36.png" alt></p><p>Kibana是一个类似Grafana的数据可视化组件。</p><h2 id="使用-Helm-部署"><a href="#使用-Helm-部署" class="headerlink" title="使用 Helm 部署"></a>使用 Helm 部署</h2><p>部署前,你需要准备好 kubernetes 集群并且安装 helm,建议创建新的 namespace 部署这些组件。</p><p><strong>Step1:安装 es 集群</strong>es以Statefulset的方式部署以保证稳定的,持久的存储。选择bitnami的chart,国内能够正常拉取默认镜像。更具需求修改配置文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install elasticsearch bitami/elasticsearch -f custom.yaml</span><br></pre></td></tr></table></figure><p><strong>Step2:安装 fluented-elasticsearch</strong>通过Daemonset方式部署,收集每个节点的数据,kiwigrid仓库默认中的镜像国内能够正常拉取。</p><figure class="highlight bash"><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">helm repo add kiwigrid https://kiwigrid.github.io</span><br><span class="line">helm update</span><br><span class="line">helm install fluented kiwigrid/fluentd-elasticsearch -f custom.yaml</span><br></pre></td></tr></table></figure><p><strong>Step3:安装 kibana</strong>直接通过Deoplyment方式部署。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install kibana bitnami/kibana -f custom.yaml</span><br></pre></td></tr></table></figure><p><strong>step4:验证是否部署成功</strong></p><p>下图是部署后Pod列表:</p><figure class="highlight bash"><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">➜ Temp kubectl get pods</span><br><span class="line">NAME READY STATUS RESTARTS AGE</span><br><span class="line">elasticsearch-1594201009-elasticsearch-coordinating-only-6s55cs 1/1 Running 0 6d1h</span><br><span class="line">elasticsearch-1594201009-elasticsearch-coordinating-only-6t7k6v 1/1 Running 0 6d1h</span><br><span class="line">elasticsearch-1594201009-elasticsearch-data-0 1/1 Running 0 6d1h</span><br><span class="line">elasticsearch-1594201009-elasticsearch-data-1 1/1 Running 0 6d1h</span><br><span class="line">elasticsearch-1594201009-elasticsearch-master-0 1/1 Running 0 6d1h</span><br><span class="line">elasticsearch-1594201009-elasticsearch-master-1 1/1 Running 0 6d1h</span><br><span class="line">fluentd-elasticsearch-1594208939-8kmzz 1/1 Running 0 5d9h</span><br><span class="line">fluentd-elasticsearch-1594208939-h4znp 1/1 Running 0 5d9h</span><br><span class="line">fluentd-elasticsearch-1594208939-tcs9c 1/1 Running 0 5d9h</span><br><span class="line">kibana-1594207892-54b9cbd57-hsfss</span><br></pre></td></tr></table></figure><p>这里创建一个简单的应用,定时往标准输出打印日志:</p><figure class="highlight yaml"><figcaption><span>counter.yaml</span></figcaption><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="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">counter</span></span><br><span class="line"> <span class="attr">labels:</span></span><br><span class="line"> <span class="attr">logging:</span> <span class="string">"true"</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"> <span class="attr">containers:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">count</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">busybox</span></span><br><span class="line"> <span class="attr">args:</span> [<span class="string">/bin/sh</span>,<span class="string">-c</span>,<span class="string">'i=0;while true;do echo "$i: $(date)"; i=$((i+1)); sleep 1;done'</span>]</span><br></pre></td></tr></table></figure><p>部署测试pod,通过log可以看到日志输出。</p><figure class="highlight bash"><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">➜ Temp kubectl create -f counter.yaml </span><br><span class="line"></span><br><span class="line">➜ Temp kubectl logs counter</span><br><span class="line">0: Thu Jul 9 06:21:27 UTC 2020</span><br><span class="line">1: Thu Jul 9 06:21:28 UTC 2020</span><br><span class="line">2: Thu Jul 9 06:21:29 UTC 2020</span><br><span class="line">3: Thu Jul 9 06:21:30 UTC 2020</span><br><span class="line">4: Thu Jul 9 06:21:31 UTC 2020</span><br><span class="line">5: Thu Jul 9 06:21:32 UTC 2020</span><br><span class="line">6: Thu Jul 9 06:21:33 UTC 2020</span><br><span class="line">7: Thu Jul 9 06:21:34 UTC 2020</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>在kibana中添加对应的索引,即可看到对应的数据:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/efk/2020-07-14-19-54-40.png" alt></p><p><strong>everything-all-in</strong>这里介绍一个比较方便的 chart <code>stable/elastic-stack</code>,这个 chart 和并了以上上个 chart,同时还可以使用其他组件进行替换,如可以使用 Filebeats 替换 fluented。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ helm install efk-stack stable/elastic-stack --<span class="built_in">set</span> logstash.enabled=<span class="literal">false</span> --<span class="built_in">set</span> fluentd.enabled=<span class="literal">true</span> --<span class="built_in">set</span> fluentd-elasticsearch.enabled=<span class="literal">true</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文,给大家介绍了日志平台方案 EFK,并通过Helm部署了实验环境,不涉及到参数调优等介绍,也不涉及到es的查询和kibnama如何使用,有splunk或类似平台使用经验的读者能够很快上手。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://www.cnblogs.com/operationhome/p/10907591.html">docker容器日志管理最佳实践</a></li><li><a href="https://www.infracloud.io/logging-in-kubernetes-efk-vs-plg-stack/">EFK vs PLG Stack</a></li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> log </tag>
<tag> 日志平台 </tag>
</tags>
</entry>
<entry>
<title>构建现代化命令化工具&&使用Golang构建命令行工具</title>
<link href="/2020/07/04/build-own-cli-to-simplfy-workflow/"/>
<url>/2020/07/04/build-own-cli-to-simplfy-workflow/</url>
<content type="html"><![CDATA[<p>现代化命令行工具相比于传统命令行工具,其汲取了现代 web 的优秀的 UI 和交互设计,所以对用户更加友好,同时也保留了命令行操作的高效性,能让用户体会到<code>hack</code>的乐趣。本片博文,将给大家讨论如何构建现代化命令行工具。首先我会结合自身的经历,来谈谈为什么命令行是高效的,然后对比两组常用且核心功能相同的命令行工具,让大家知道现代命令行工具一般具备哪些特征,最后依据这些特征,给大家总结使用 go 语言构建现代命令行的过程,以及常用的库。</p><h2 id="为什么偏爱命令行"><a href="#为什么偏爱命令行" class="headerlink" title="为什么偏爱命令行"></a>为什么偏爱命令行</h2><p>作为一位 Hacker,我十分推崇<strong>键盘操作模式</strong>和<strong>以终端为中心的工作模式</strong>这两种理念哲学。很大程度上,这两种理念哲学是 Hacker 文化的重要部分。</p><p>键盘操作,与之相对应的是鼠标(触屏等)操作,这两种模式是目前的主流人机交互方式。对于使用过<code>VIM</code>和<code>Word</code>两种工具的用户,会很容易理解,两种模式的区别。<code>word</code>如今使用更加广泛,并且鼠标操作模式也一直是主流,而<code>VIM</code>的使用人群则相对固定(开发者),对于一个深度使用<code>VIM</code>的人是很难抛弃它,VIM 吸引用户的魔力正是键盘操作模式,而 VIM 正是以键盘为中心操作模式的代表,这种设计哲学几乎在很多专业软件都有体现,比如<code>Jetbrains</code>这家公司的产品,主要提供流行语言的 IDE 产品,其核心特色就是通过快捷键能够实现几乎所有功能(Jetbrains 定义为<code>Action</code>),并且这些快捷键很容易定制,同时有<code>Find Action</code>统一的入口。这也是 Jetbrains 产品的优势所在,再比如<code>VSCode</code>编辑器,能够从众多编辑器脱颖而出也正是因为其设计广泛采用了这种模式。</p><p>这两种模式在不同场景各有优势,鼠标能够让用户更容易上手,降低用户学习成本。而对于开发者,键盘操作模式能显著提升生产力,还有在一些专业工具中,大量使用快捷键代替繁琐的鼠标操作,能够使开发人员更加专注于工作,提高效率,因为键盘操作模式更容易产生肌肉记忆。</p><p>终端(终端模拟器)作为计算机发展的早期人与计算机进行的通信的工具,即使到现在,仍然无法被取代,并且相对于各种形形色色的程序,其仍然是最特殊和最具个性的一个。终端的使用主要是通过命令行工具完成具体的工作,由于早期没有鼠标,终端的工作模式以键盘为主,这里提一下,现代的终端程序也提供了对键盘的支持。<code>微软个人PC</code>早期是十分不看重终端程序的,但是现在却也大力发展终端(这里微软终端<a href="https://github.com/microsoft/terminal">开源仓库</a>),其主要原因是不想白白失去开发者这一膨大的用户群体,从中也可以看出,终端的重要地位。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-03-14-59-52.png" alt></p><p>上图是微软大力研发的命令行工具,对比传统的<code>CMD</code>程序,我们能够从中发现现代命令行工具的影子。</p><h2 id="如何设计现代的命令行工具。"><a href="#如何设计现代的命令行工具。" class="headerlink" title="如何设计现代的命令行工具。"></a>如何设计现代的命令行工具。</h2><p>良好的终端的体验通常包含以下四种工具的选择:</p><ul><li>终端模拟器的选择,优秀终端模拟器一般提供良好的 UI 设计,分屏,Session 管理等;比如 Mac 用户通常会使用 Iterm2 而不是自带终端程序,正是 Iterm2 的用户体验更加出色,功能更加齐全;</li><li>Shell 的选择,Shell 发展至今,也有很多选择,优秀的 Shell 提供自动提示,插件集成,良好的 UI 等,注意并不是每个系统都对所有类型 Shell 提供支持的,所以选择通用 Shell 会降低后续迁移的风险,Oh-My-Zsh 是现代 Shell 中的佼佼者;</li><li>包管理方式,当命令行工具变得越来越多,所带来的管理代价也越来越大,包管理工具能够帮助管理安装,卸载,升级等,包管理工具也是命令行工具;</li><li>命令行工具的选择,如何在众多质量层次不齐的工具中选择,这很大程度影响了用户体验,这是本文后续重点的讨论的内容;</li></ul><p>命令行工具通常实现一组相关的功能,<strong>功能</strong>是吸引用户使用的最重要的点,不过这里我们无法讨论功能的好坏,但是如果有一组实现相同功能的命令行工具给你选择,你该如何选择呢?毫无疑问,你会使用更简洁,方便的那一个,这就是命令行工具的用户体验的重要性,<strong>让你的用户留下来</strong>。</p><p><code>RTFM(阅读该死的手册Read The Fucking Manual)</code>文化在命令行工具的用户群体的流行,也是对许多命令行工具的吐槽,甚至,正是因为命令行工具的不易使用,才会让很多人觉得命令行很高大上。出现这一现象的原因,一是命令行工具大都是开源工具,开发质量无法得到保证,并且无法跟上现代用户的使用需求。二是大多数公司更重视应用 APP 和浏览器的设计和用户体验的优化,忽视命令行工具的优化。例如 Github 这样的企业到今年才有了官方的命令行工具<code>cli</code>。三是用户群体的忍耐性高,命令行工具的用户群体,似乎是习惯了其糟糕的体验。</p><p>说到这里,什么样的命令行工具可以称得上现代的?用户体验优秀的命令行工具和普通命令行工具有什么区别呢?事实上,也确实没有相关衡量标准的建立,不过,下面通过对比两组工具(没有使用过的读者可以试着使用),相信大家心中会有答案。</p><h3 id="mysql-VS-mycli"><a href="#mysql-VS-mycli" class="headerlink" title="mysql VS mycli"></a>mysql VS mycli</h3><script type="text/javascript" src="https://asciinema.org/a/VrpKMGCkRb3IjRfFpSJccYl2Z.js" id="asciicast-VrpKMGCkRb3IjRfFpSJccYl2Z" async></script><script type="text/javascript" src="https://asciinema.org/a/TN27rqMvGrkttncxChu9D7ZiC.js" id="asciicast-TN27rqMvGrkttncxChu9D7ZiC" async></script><p>以上两款工具都是<code>mysql</code>的客户端,但是大多数用户在使用 mycli 后会选择它,因其提供使用者良好的补全,同时界面的 UI 也更加现代。</p><h3 id="python-VS-ipython"><a href="#python-VS-ipython" class="headerlink" title="python VS ipython"></a>python VS ipython</h3><script type="text/javascript" src="https://asciinema.org/a/wDSHpCHQCDWwYL3wr2chKUvax.js" id="asciicast-wDSHpCHQCDWwYL3wr2chKUvax" async></script><script type="text/javascript" src="https://asciinema.org/a/NaRnnwi7YUY3Z4xYGmkforepp.js" id="asciicast-NaRnnwi7YUY3Z4xYGmkforepp" async></script><p>以上是两种 python 交互 Shell,ipython 提供的提示更加方便,同时也能方便查阅读文档,方便导出代码段,查询操作历史等功能,这些功能相当实用。</p><p>大家可以对比使用以上两组工具,更能够明白一个良好的用户体验为什么能让用户留下来。虽然目前没有事实的标准,但是现代的命令行工具一般具有以下特点:</p><ul><li>良好的提示和补全,特别提一点动态信息的补全,如 mycli 能够补全字段,表和数据库这些信息;</li><li>现代 UI 设计,如对于重点数据使用颜色标注,长时间的操作提供状态条,输出的内容排版等;</li><li>遵循标准统一规范(Unix/Posix,BSD,GNU风格的参数),不滥用<code>Flag,Args,Subcommand</code>这些元素。比如开关这种动作应该使用子命令而不是通过 flag 控制。统一规范有利于用户快速上手;</li><li>详细有序的输出信息,如在打印一组规则的输出时,表格会更加合理;</li><li>便携的输入,在一些情况使用交互式输入,会对用户更优化,对于需要输入如 yaml 等格式的文件,使用默认编辑器会更合理。</li></ul><h2 id="go构建现代命令行工具及相关库整理"><a href="#go构建现代命令行工具及相关库整理" class="headerlink" title="go构建现代命令行工具及相关库整理"></a>go构建现代命令行工具及相关库整理</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-02-11-51-51.png" alt></p><p>目前,我们已经可以通过很多工具构建命令行工具,最简单原始的方式就是通过 Shell 脚本,各个语言也都有命令行开发库和工具,在所有语言中最常见也是最适合开发的语言是<code>C</code>,<code>node</code>和<code>python</code>。为什么这三种语言比较合适,这里就不再赘述了。</p><p>随着<code>Golang</code>这几年的发展,相关库生态的丰富,加上<code>Golang</code>语言本身很适合构建命令行工具(最终构建物就是可执行文件),所以也出现大量通过 Go 开发的命令行工具,如<code>kubectl,hugo,cli等</code>。下面结合自己的使用经历和经验,给大家整理了go 如何选择合适工具来开发现代命令行工具。</p><blockquote><p>对于下面的工具和库,这里只做简单的介绍,详细用法参考使用文档。</p></blockquote><p>根据上述我们总结的优秀命令行工具的特点,我们来选择适合的库。首先选择框架,目前使用比较多的有<code>cobra</code>和<code>cli</code>,由于已有许多成熟的项目使用了<code>cobra</code>,所以建议使用这个库,可以参考其他的项目中优秀的设计模式,上手很简单,下面是一个简单的示例程序:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> rootCmd = &cobra.Command{</span><br><span class="line"> Use: <span class="string">"hugo"</span>,</span><br><span class="line"> Short: <span class="string">"Hugo is a very fast static site generator"</span>,</span><br><span class="line"> Long: <span class="string">`A Fast and Flexible Static Site Generator built with</span></span><br><span class="line"><span class="string"> love by spf13 and friends in Go.</span></span><br><span class="line"><span class="string"> Complete documentation is available at http://hugo.spf13.com`</span>,</span><br><span class="line"> Run: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="keyword">string</span>)</span></span> {</span><br><span class="line"> <span class="comment">// Do Stuff Here</span></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">func</span> <span class="title">Execute</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">if</span> err := rootCmd.Execute(); err != <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(err)</span><br><span class="line"> os.Exit(<span class="number">1</span>)</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">main</span><span class="params">()</span></span> {</span><br><span class="line"> cmd.Execute()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个库能够方便实现子命令(嵌套命令),同时能够自动生成帮助信息(help)和手册(man page),也能够生成补全信息,命令行参数也符合 Posix 的标准,其为我们做了很多工作,所以我们已经成功一大半了。底层搭好了,剩下的就是美化的工作。</p><h3 id="格式化输出"><a href="#格式化输出" class="headerlink" title="格式化输出"></a>格式化输出</h3><p>首先是输出的格式化,用户通过命令行获取信息,能够让用户快速定位到关键信息很重要,<a href="https://www.nngroup.com/reports/how-people-read-web-eyetracking-evidence/?lm=eyetracking-web-usability&pt=book">研究</a>表明,<code>F型,分层蛋糕型和斑点型</code>的输出是最友好的三种输出模型。不要让输出过分紧凑,适当的保留空白区域,有利于降低用户的阅读难度。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-03-20-39-19.png" alt></p><p>列表输出是最常见的一种形式,go 中通过库 Tablewriter 能够很好的处理表格的格式化。</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></pre></td><td class="code"><pre><span class="line">data := [][]<span class="keyword">string</span>{</span><br><span class="line"> []<span class="keyword">string</span>{<span class="string">"A"</span>, <span class="string">"The Good"</span>, <span class="string">"500"</span>},</span><br><span class="line"> []<span class="keyword">string</span>{<span class="string">"B"</span>, <span class="string">"The Very very Bad Man"</span>, <span class="string">"288"</span>},</span><br><span class="line"> []<span class="keyword">string</span>{<span class="string">"C"</span>, <span class="string">"The Ugly"</span>, <span class="string">"120"</span>},</span><br><span class="line"> []<span class="keyword">string</span>{<span class="string">"D"</span>, <span class="string">"The Gopher"</span>, <span class="string">"800"</span>},</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">table := tablewriter.NewWriter(os.Stdout)</span><br><span class="line">table.SetHeader([]<span class="keyword">string</span>{<span class="string">"Name"</span>, <span class="string">"Sign"</span>, <span class="string">"Rating"</span>})</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, v := <span class="keyword">range</span> data {</span><br><span class="line"> table.Append(v)</span><br><span class="line">}</span><br><span class="line">table.Render() <span class="comment">// Send output</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><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><br><span class="line">| NAME | SIGN | RATING |</span><br><span class="line">+------+-----------------------+--------+</span><br><span class="line">| A | The Good | 500 |</span><br><span class="line">| B | The Very very Bad Man | 288 |</span><br><span class="line">| C | The Ugly | 120 |</span><br><span class="line">| D | The Gopher | 800 |</span><br><span class="line">+------+-----------------------+--------+</span><br></pre></td></tr></table></figure><p>对于其他固定类型的可视化,可以使用<code>termui</code>,这个工具提供了很多小组件,如饼图,柱状图等格式化数据,有需要的小伙伴,可以自行了解学习。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-03-21-28-39.png" alt></p><h3 id="色彩"><a href="#色彩" class="headerlink" title="色彩"></a>色彩</h3><p>添加各种色彩有很多好处,一方面可以提醒用户重要的信息,另一方面一些颜色本身也具备传递信息的功能。如红色通常代表错误和危险操作,绿色通常代表成功,黄色提醒需要注意,蓝色代表着状态良好。配合终端模拟器,合理使用色彩,会让你的命令行工具更加出彩。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-03-20-58-02.png" alt></p><p>go 中通过 Color 能够打印带有色彩的文本信息,对于 Log 的打印,logrus 库也提供了彩色支持。</p><h3 id="图片-Logo"><a href="#图片-Logo" class="headerlink" title="图片 Logo"></a>图片 Logo</h3><p>在输出中添加图片和 logo 能够拉近与用户的距离,同时图片相比文字能够传递更多的信息,更容易被用户接受,有的终端模拟器可能不支持图片,所以在使用中需要考虑所面向的用户,及其使用环境。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/build-own-cli-to-simplfy-workflow/2020-07-03-21-14-09.png" alt></p><h3 id="状态条"><a href="#状态条" class="headerlink" title="状态条"></a>状态条</h3><p>一些耗时操作,如上传下载文件,如果没有任何输出,这可能会导致用户强行关闭程序,通过一个实时状态条,就能轻松过解决。Go 中有很多这样的库如 progressbar, cheggaaa/pb 等。下面是简单的 cheggaaa/pb 的 Demo:</p><figure class="highlight golang"><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">count := <span class="number">100000</span></span><br><span class="line"><span class="comment">// create and start new bar</span></span><br><span class="line">bar := pb.StartNew(count)</span><br><span class="line"></span><br><span class="line"><span class="comment">// start bar from 'default' template</span></span><br><span class="line"><span class="comment">// bar := pb.Default.Start(count)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// start bar from 'simple' template</span></span><br><span class="line"><span class="comment">// bar := pb.Simple.Start(count)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// start bar from 'full' template</span></span><br><span class="line"><span class="comment">// bar := pb.Full.Start(count)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < count; i++ {</span><br><span class="line"> bar.Increment()</span><br><span class="line"> time.Sleep(time.Millisecond)</span><br><span class="line">}</span><br><span class="line">bar.Finish()</span><br></pre></td></tr></table></figure><script type="text/javascript" src="https://asciinema.org/a/XfCiQhwLF8XmGetRxQyzNpncH.js" id="asciicast-XfCiQhwLF8XmGetRxQyzNpncH" async></script><p>构建一个用户体验优秀的命令行工具,以上几个方面是非常值得考虑的。当然除了以上几点,你还需要考虑如何发布你的工具,版本升级,文档维护等问题。</p><p>除了借鉴开源的命令行工具,大家也可以参考项目 <a href="https://github.com/donggangcj/hack">hack</a>,这是我开源的一个命令行工具,主要功能是简化Mac工作流,同时在实现功能的同时也在尝试构建现代化命令行工具,目前已经有以上相关优化的实践样例。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇博文,对于哪些深度使用命令行工具的读者,可能会更容易体会其价值。一个排斥命令行工具的人,可能不容器理解,当然,这也很正常,现在想起刚接触命令行工具的自己,同样也是排斥的。我想告诉这些读者,如果你是一位开发者,那么请先尝试拥抱命令行工具,再回头来阅读,你应该会有所收获。</p><p>如果想要快速构建命令行工具,尝试通过创建一个符合自己用户习惯的命令行工具(如将多个命令行工具的功能聚合成一个的形式),用来简化日常工作流。就像<code>hack</code>项目一样,相信你会很快上手。</p>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> golang </tag>
<tag> CLI </tag>
<tag> 自动化 </tag>
</tags>
</entry>
<entry>
<title>博客优化:Hexo支持webp格式&&迁移到香港云主机</title>
<link href="/2020/06/28/blog-day5/"/>
<url>/2020/06/28/blog-day5/</url>
<content type="html"><![CDATA[<p>最近使用<code>lighthouse</code>对博客做了一个测试,测试结果如下,以下是桌面 PC 端的测试的结果:<img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-18-13-37-08.png" alt></p><p>下图是一个优秀个人博客网站的测试结果,通过对比可以发现有很大差距。<img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-18-13-39-24.png" alt></p><p>这个测试能够一定程度反映网站的用户体验,测试主要包含<code>性能,可访问性,SEO和最佳实践</code>四个方面。通过测试,我们能发现网站存在的问题,同时<code>lighthouse</code>工具也提供关于这些问题的一些改进建议。其中很多问题网上已经有优化方案了,我们就不再搬运了,但是关于<code>Hexo</code>建站如何优雅使用<code>webp</code>图片格式的问题,还没有成熟的方案,本片博文将给大家分享我是如何实现 Hexo 兼容<code>webp</code>格式图片,且做到<strong>无感知,全自动化</strong>的管理的;同时也会简单介绍通过将博客迁移到香港云主机,优化国内读者的访问速度的过程。</p><h2 id="图片资源优化"><a href="#图片资源优化" class="headerlink" title="图片资源优化"></a>图片资源优化</h2><p>技术类文章不可避免地需要大量的配图,同时随着博客文章的图片的增多,偶尔出现几 M 大小的图片,博客页面加载就会变慢,影响读者的阅读体验。缩小页面的中配图的大小,加速博文渲染速度迫在眉睫,但是最终实现不能以牺牲图片的质量和显示效果为代价。查阅相关资料,我尝试了从以下两种思路进行优化。</p><p>第一种思路是使用压缩工具将过大的图片压缩,目前有两种类型的工具,一种是以<a href="https://tinypng.com/"><code>TinyPNG</code></a>为代表的在线工具,这类工具最大的缺点是对后续流程自动化不太友好(也可以通过<code>selenium</code>这类工具实现),不过<code>TinyPNG</code>提供了<code>API接口</code>,还提供了常用语言的客户端。另一种是以<code>OptiPNG和Jpegtopnm</code>为代表的本地命令行工具,缺点是每个工具只支持部分格式的图片,没有一个支持多种类型的工具,同时由于压缩算法需要大量的算力,本地压缩一张图片会消耗很多资源(时间和 CPU)。<code>TinyPNG</code>借助服务器相对来说速度要快一些。其优点是使用这类工具对后续自动化处理十分友好。</p><p>另一种思路是转换图片格式,本质上也是压缩,<code>lighthouse</code>提供的建议是使用<code>JPEG 2000, JPEG XR, and WebP</code>这几种格式。后续只会介绍<a href="https://developers.google.com/speed/webp"><code>webp</code></a>格式,其他两种有需要自行了解。<code>webp</code>是 Google 推出的现代图片格式,支持无损和有损压缩,旨在减小网站的图片的体积,提高网站的速度。使用方式十分简单,通过下载命令行工具<code>cwebp</code>进行转换:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 具体使用方式参考使用文档</span></span><br><span class="line">➜ bin brew install webp</span><br><span class="line">➜ bin cwebp 2020-06-04-11-33-22.png -o 2020-06-04-11-33-22-new.webp</span><br></pre></td></tr></table></figure><p>以下是使用<code>TinyPng</code>,<code>OptiPNG</code>和<code>webp</code>处理所得到的图片大小的对比(测试结果有所差异):<img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-18-16-21-22.png" alt></p><p>通过对比,<code>webp</code>格式的效果最好,而且通过对比几张图片,显示效果并没有明显的差别。毫无疑问,<code>webp</code>是更好的方案。<code>webp</code>是较新的技术,所以一些浏览器可能还没有支持。测试以下,果然主流的<code>Safari</code>不支持。这意味者使用<code>safara</code>访问时,仍然需要访问<code>PNG</code>这类格式的图片。而使用支持<code>webp</code>的浏览器,则可以访问<code>webp</code>格式的图片。</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-18-11-26-18.png" alt></p><p><code>Hexo</code>要实现浏览器的兼容,有两种方式,一种云端处理,这种方式博客无需任何变化,转化借助 CDN 或者特定服务实现,例如,<code>CloudFlare</code>的 Pro 用户可以通过开启<code>Polish</code>实现。如果你的图片部署在云主机上,同时博客中的图片保存在本地(没有通过外部引用),你可以部署<a href="https://github.com/webp-sh/webp_server_go">webp_server_go</a>服务实现转换。另一种方式相对来说比较简单,通过一段<code>Javascript脚本</code>判断浏览器是否支持<code>webp</code>格式,如果支持则获取<code>webp</code>格式图片,否则获取原格式图片,前提需要在图片服务上保存两种类型的图片。由于博主的图片都是通过<code>Jsdelivr</code>访问的,如果使用<code>webp_server_go</code>做一道中转,比较浪费流量和性能。所以就采用脚本方式实现<code>webp</code>浏览器兼容。以下是改造 Hexo Butterfly 主题实现自动转换的过程,其他主题类似:</p><p>首先在<code>_config.yaml</code>定义变量控制是否需要转换<code>webp</code>:</p><figure class="highlight yaml"><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="comment">## webp</span></span><br><span class="line"><span class="attr">use_webp:</span> <span class="string">tru</span></span><br></pre></td></tr></table></figure><p>在目录<code>themes/Butterfly/scripts</code>添加一个目录<code>webp</code>,并创建<code>index.js</code>文件:</p><figure class="highlight javascript"><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="meta">"use strict"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (hexo.config.use_webp) {</span><br><span class="line"> hexo.extend.filter.register(</span><br><span class="line"> <span class="string">"after_render:html"</span>,</span><br><span class="line"> <span class="built_in">require</span>(<span class="string">"./lib/process"</span>).webpProcess</span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>继续在<code>webp</code>目录创建子目录<code>lib</code>,并创建<code>process.js</code>文件:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">"hexo-fs"</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">webpProcess</span>(<span class="params">htmlContent</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> htmlContent.replace(</span><br><span class="line"> /<span class="xml"><span class="tag"><<span class="name">img(.*?)src="(.*?)"(.*?)</span>></span>/gi,</span></span><br><span class="line"><span class="xml"> function (str, p1, p2) {</span></span><br><span class="line"><span class="xml"> if (</span></span><br><span class="line"><span class="xml"> /https:\/\/cdn.jsdelivr.net\/gh\/donggangcj\/CDN\/image\/(.*?)\.(jpg|jpeg|png)/gi.test(</span></span><br><span class="line"><span class="xml"> p2</span></span><br><span class="line"><span class="xml"> )</span></span><br><span class="line"><span class="xml"> ) {</span></span><br><span class="line"> return `<picture><source srcset="${p2.replace(</span><br><span class="line"> /\.(jpg|jpeg|png)/gi,</span><br><span class="line"> ".webp"</span><br><span class="line"> )}" type="image/webp">${str}</picture>`;</span><br><span class="line"> }</span><br><span class="line"> return str;</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">module.exports.webpProcess = function (htmlContent) {</span><br><span class="line"> return webpProcess.call(this, htmlContent);</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>注意修改你的图片服务地址,上述脚本逻辑主要是查询页面中的图片引用,并使用<code><picture></picture></code>标记进行替换,该标记可以使用多个<code><source></code>元素和一个<code><img></code>标记。将<code>WebP</code>格式文件放入<code><source></code>元素中,并将应用更加广泛的<code>JPEG/PNG</code>等传统格式文件放入<code><img></code>标记中。对于能够理解<code>image/webp</code>源的浏览器会加载<code><source></code>标记内的<code>WebP</code>格式文件,而不理解的浏览器则回退至<code><img></code>标记内的传统格式文件。</p><p>这样就已经完成了吗?🤪 当然不是啦!思考一下到现在为止,博客图片管理流程是什么样子?</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-28-15-55-23.png" alt></p><p>熟悉我的读者应该知道我的博客已经通过命令行工具<code>hexo-iamge-sync</code>(自己通过 python 开发的命令行工具,现在已经通过 golang 重构)实现了<code>Copy-And-Paste</code>的方式,但是为了缩小图片的大小,又多了手动压缩,转换 webp 的处理,这些步骤是繁琐的且需要频繁操作,完全可以被自动化。当然<code>Let's SMTC(show me the code)</code>,来看下面的一段操作:</p><script type="text/javascript" src="https://asciinema.org/a/pLOXcFDSkqVe43urtLjmGfVg2.js" id="asciicast-pLOXcFDSkqVe43urtLjmGfVg2" async></script><p>上述操作包含两步,首先查看未同步到<code>jsdelivr</code>的图片,然后将图片发布到<code>jsdeliver</code>,当然也可以通过<code>flag</code>控制一些选项,例如是否对所有图片重新压缩和转换,这对更新操作和删除无用图片十分有帮助。该工具目前还处于开发阶段,代码仓库地址为<a href="https://github.com/donggangcj/hack">hack</a>。需要的读者可以直接使用,当然也可以自己开发工具,简化这些繁琐的工作流。</p><h2 id="博客迁移到香港云主机"><a href="#博客迁移到香港云主机" class="headerlink" title="博客迁移到香港云主机"></a>博客迁移到香港云主机</h2><p>由于我的博客目前的受众主要是国内的读者,但是<code>Github Page</code>在国内的访问速度实在有点慢,这就导致我向别人安利自己的博客时,别人要么打不开要么得加载 10s 以上,这就非常尴尬。具体国内的访问延迟测试如下:</p><p><img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-22-15-59-04.png" alt></p><p>所以优化国内的访问速度同样迫在眉睫,我尝试了以下几种优化方式:</p><ol><li>使用<code>CloudFlare做全站CDN加速</code>,如果是<code>Pro</code>用户,还可以使用<code>Polish</code>功能,在云端对图片进行 webp 的转码,但是最终我还是放弃了此方案,主要是针对国内的用户,仍然需要备案,免费的加速效果并不明显;</li><li>国内上云,需要备案就暂时不考虑了;</li><li>使用<a href="https://vercel.com/"><code>ZEIT</code></a>,大陆访问<code>ZEIT</code>是最快的!比 Netlify 和 Github Pages 以及 Heroku 都快。注意线路问题,电信走香港、台湾线路,联通移动会绕美国。同时使用其<code>Serverless</code>功能可以实现很多有意思的功能。这种方式资源有限,且不方便部署和后续迁移;</li><li>托管到国内访问速度良好的 VPS 上(VPS 服务提供商的选择很重要,通常选择香港和台湾主机)。该方式优点自主可控,同时也很灵活,如果 VPS 选的合理,速度相对来说也是最快的,但是这一切的代价就是 💰;</li></ol><p>综合考虑,最终选择通过部署到 VPS 上来优化国内访问速度。部署过程十分简单,主要是将博客的静态文件同步到云主机指定目录,再使用 Nginx 代理即 OK,当然,配置<code>Https</code> 也必不可少;<code>hexo</code>支持部署到多个托管服务上,所以笔者同时将博客部署到<code>github page</code>和云主机上。</p><p>测试迁移后效果,国内提速十分明显:<img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-22-16-21-40.png" alt></p><p>插曲:在迁移的过程中,也试了<a href="https://nova.moe/">Nova Kwok’s Awesome Blog</a>的方案,直接使用 Nova 提供的主机,这个速度简直太顺滑,不过目前自己还能折腾,就没有使用这个方案。</p><h2 id="Google-搜索引擎"><a href="#Google-搜索引擎" class="headerlink" title="Google 搜索引擎"></a>Google 搜索引擎</h2><p><code>Google</code>的搜索引擎爬虫爬取需要一定时间,而我的博客刚换了域名,所以使用<code>Google Search Console</code>工具手动推了<code>sitemap</code>。</p><ol><li>步骤一:生成<code>sitemap</code>文件,</li></ol><figure class="highlight zsh"><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="comment"># 安装sitemap生成插件</span></span><br><span class="line">➜ blog-new git:(master) npm install hexo-generator-sitemap --save</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成博客静态文件(注意_config.yaml中博客地址需要与实际部署地址一致)</span></span><br><span class="line">➜ public git:(master) ✗ hexo g && hexo d</span><br></pre></td></tr></table></figure><ol start="2"><li>步骤二:前往<code>Google SearchConsole</code>验证网站是否属于本人。这里需要在 DNS 中添加一条<code>TXT</code>记录。验证通常很快就会通过。</li></ol><p>通过在浏览器搜索<code>site:blog.happyhack.io</code>验证站点是否被 Google 引擎索引。<img src= "/img/loading.gife" data-lazy-src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day5/2020-06-28-16-51-59.png" alt></p><p>关于博客的 SEO 优化,笔者还在学习,博客也刚刚起步,后续有机会再和大家分享学习。</p><h2 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h2><ol><li>为博客添加高质量动图:通过<code>stories</code>生成动态插图并嵌入博客中。</li></ol><body onload="setTimeout(function() { document.querySelector('svg').classList.add('animated'); })"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 500 500" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="697.5" height="383"><style>svg:not(.animated) .animable {opacity: 0;}body { background: #7F68C8}.animated #freepik--Background--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) slideLeft;animation-delay: 0s;}.animated #freepik--Numbers--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) slideLeft;animation-delay: 0s;}.animated #freepik--Window--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) fadeIn;animation-delay: 0s;}.animated #freepik--Sign--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) lightSpeedLeft;animation-delay: 0s;}.animated #freepik--Card--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) lightSpeedRight;animation-delay: 0s;}.animated #freepik--Files--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) slideDown;animation-delay: 0s;}.animated #freepik--Character--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) slideLeft;animation-delay: 0s;}.animated #freepik--speech-bubble--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) slideDown;animation-delay: 0s;}.animated #freepik--Padlock--inject-112 {animation: 1s 1 forwards cubic-bezier(.36,-0.01,.5,1.38) zoomIn;animation-delay: 0s;} @keyframes slideLeft { 0% { opacity: 0; transform: translateX(-30px); } 100% { opacity: 1; transform: translateX(0); } } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes lightSpeedLeft { from { transform: translate3d(-50%, 0, 0) skewX(20deg); opacity: 0; } 60% { transform: skewX(-10deg); opacity: 1; } 80% { transform: skewX(2deg); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes lightSpeedRight { from { transform: translate3d(50%, 0, 0) skewX(-20deg); opacity: 0; } 60% { transform: skewX(10deg); opacity: 1; } 80% { transform: skewX(-2deg); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes slideDown { 0% { opacity: 0; transform: translateY(-30px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes zoomIn { 0% { opacity: 0; transform: scale(0.5); } 100% { opacity: 1; transform: scale(1); } } </style><g id="freepik--Background--inject-112" class="animable" style="transform-origin: 251.782px 252.978px;"><path id="freepik--background--inject-112" d="M446.07,138.72C351.75,12.9,216.48-13.53,99.57,68.85,3,136.88-5.26,289.61,31.94,358.81,59.7,410.44,133.8,469,230.78,480.64c115.78,13.95,167.81-13,206.23-45.46C510.36,373.13,507.58,220.78,446.07,138.72ZM145.19,465.85c-17.57-6.31-37.71-13.45-63.09-34.08a236.93,236.93,0,0,1-46.84-51.59c-7.28-11-11.3,35.9,30.59,68C116.28,486.84,162.85,472.19,145.19,465.85Z" style="fill: rgb(38, 50, 56); transform-origin: 251.782px 252.978px;" class="animable"/></g><g id="freepik--Numbers--inject-112" class="animable" style="transform-origin: 72.4298px 231.424px;"><g id="freepik--Binary--inject-112" class="animable" style="transform-origin: 72.4298px 231.424px;"><path d="M83.34,142l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; transform-origin: 84.745px 137.545px;" id="eljsujq4tk7m" class="animable"/><g id="eld849wme5wu"><path d="M86.58,150.7a8.12,8.12,0,0,1-.52,3.09,3.54,3.54,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.28.47.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 84.6147px 150.704px;" class="animable" id="ell1lb1o25efl"/></g><path d="M83.34,168.77l1-.74V160.9l-1,.59v-.81L85.13,159v8.51l1-.37V168l-2.74,1.58Z" style="fill: #7F68C8; transform-origin: 84.735px 164.29px;" id="eltc5heqy59e" class="animable"/><g id="eler4ocimtbkm"><path d="M86.58,177.48a8.12,8.12,0,0,1-.52,3.09,3.54,3.54,0,0,1-1.44,1.81q-.91.52-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.28.47.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 84.6147px 177.482px;" class="animable" id="elbkkbh3tcjx"/></g><g id="elvxk0lyolgwo"><path d="M83.34,195.55l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 84.745px 191.095px;" class="animable" id="elm7eve85f0y"/></g><path d="M83.34,208.94l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; transform-origin: 84.745px 204.485px;" id="ele95xhs3k8fc" class="animable"/><g id="eli5axcvxq6q"><path d="M83.34,222.33l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 84.745px 217.875px;" class="animable" id="elnzgxfronlp"/></g><g id="elz75mv1f15wp"><path d="M86.58,231a8.14,8.14,0,0,1-.52,3.1,3.58,3.58,0,0,1-1.44,1.81q-.91.52-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.28.47.84.15a2,2,0,0,0,.84-1.12,6.07,6.07,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 84.6147px 231.012px;" class="animable" id="el26eogd9ls27"/></g><path d="M83.34,249.11l1-.74v-7.13l-1,.59V241l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; transform-origin: 84.745px 244.635px;" id="elrf3mf1vy9pq" class="animable"/><g id="el2bnw4hyk7jg"><path d="M86.58,257.81a8.14,8.14,0,0,1-.52,3.1,3.58,3.58,0,0,1-1.44,1.81q-.91.52-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.36-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.28.46.84.15a2,2,0,0,0,.84-1.12,6.07,6.07,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 84.6147px 257.822px;" class="animable" id="elyjk7m0wy5bo"/></g><g id="el414iw94zsd5"><path d="M83.34,275.89l1-.74V268l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83l-2.74,1.58Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 84.745px 271.415px;" class="animable" id="el3e7bt08bpxl"/></g><path d="M86.58,284.59a8.14,8.14,0,0,1-.52,3.1,3.58,3.58,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.36,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.51,3.51,0,0,0-.29-1.7c-.2-.3-.48-.35-.85-.13a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.79a3.34,3.34,0,0,0,.3,1.7q.28.46.84.15a2,2,0,0,0,.84-1.12,6.07,6.07,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 84.6147px 284.602px;" id="el8vqzmkpnffr" class="animable"/><path d="M74.4,144.34a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8A1,1,0,0,1,71,149.1a4.23,4.23,0,0,1-.53-2.49v-2.25a8.16,8.16,0,0,1,.53-3.1,3.54,3.54,0,0,1,1.43-1.81c.61-.35,1.09-.3,1.45.15a4.21,4.21,0,0,1,.53,2.49Zm-.84-2.05a3.49,3.49,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.13a2,2,0,0,0-.83,1.1,5.86,5.86,0,0,0-.29,2v2.79a3.51,3.51,0,0,0,.29,1.7.55.55,0,0,0,.84.15,2,2,0,0,0,.84-1.12,5.86,5.86,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 72.4403px 144.349px;" id="elpok9z60z0c" class="animable"/><path d="M71.15,162.41l1-.73v-7.13l-1,.58v-.8l1.81-1.65v8.52l1-.37v.82l-2.74,1.59Z" style="fill: #7F68C8; transform-origin: 72.555px 157.96px;" id="el4oyep6e8rqw" class="animable"/><g id="elwx6m45492h9"><path d="M71.15,175.8l1-.73v-7.14l-1,.59v-.8l1.81-1.65v8.51l1-.36V175l-2.74,1.59Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 72.555px 171.33px;" class="animable" id="el9f9w2arj8oc"/></g><g id="el8564q3jfex"><path d="M71.15,189.19l1-.73v-7.13l-1,.58v-.8l1.81-1.65V188l1-.37v.82L71.15,190Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 72.555px 184.73px;" class="animable" id="ele5dw8exc7pn"/></g><path d="M74.4,197.9a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.23,4.23,0,0,1-.53-2.49v-2.25a8.16,8.16,0,0,1,.53-3.1A3.54,3.54,0,0,1,72.42,193c.61-.35,1.09-.3,1.45.15a4.21,4.21,0,0,1,.53,2.49Zm-.84-2.05a3.49,3.49,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.13a2,2,0,0,0-.83,1.1,5.86,5.86,0,0,0-.29,2V200a3.51,3.51,0,0,0,.29,1.7.55.55,0,0,0,.84.15,2,2,0,0,0,.84-1.12,5.86,5.86,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 72.4303px 197.907px;" id="eln5b60xm1z2" class="animable"/><g id="elpoo46axleg"><path d="M74.4,211.29a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.25,4.25,0,0,1-.53-2.49v-2.25a8.16,8.16,0,0,1,.53-3.1,3.54,3.54,0,0,1,1.43-1.81c.61-.35,1.09-.3,1.45.15A4.21,4.21,0,0,1,74.4,209Zm-.84-2a3.49,3.49,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.13a2,2,0,0,0-.83,1.1,5.86,5.86,0,0,0-.29,2v2.79a3.51,3.51,0,0,0,.29,1.7.55.55,0,0,0,.84.15,2,2,0,0,0,.84-1.12,5.88,5.88,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 72.4289px 211.302px;" class="animable" id="el2psfdskbvg3"/></g><path d="M71.15,229.36l1-.73v-7.14l-1,.59v-.8l1.81-1.65v8.51l1-.36v.82l-2.74,1.59Z" style="fill: #7F68C8; transform-origin: 72.555px 224.91px;" id="elvqurj72lc5" class="animable"/><path d="M74.4,238.07a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.25,4.25,0,0,1-.53-2.49v-2.25A8.16,8.16,0,0,1,71,235a3.54,3.54,0,0,1,1.43-1.81,1,1,0,0,1,1.45.14,4.25,4.25,0,0,1,.53,2.49Zm-.84-2a3.49,3.49,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.13a2,2,0,0,0-.83,1.1,5.86,5.86,0,0,0-.29,2v2.79a3.51,3.51,0,0,0,.29,1.7c.2.31.48.36.84.14a2,2,0,0,0,.84-1.11,5.88,5.88,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 72.435px 238.077px;" id="elflmhsdix4ve" class="animable"/><g id="el79vl534054e"><path d="M71.15,256.14l1-.73v-7.14l-1,.59v-.8l1.81-1.65v8.51l1-.36v.82L71.15,257Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 72.555px 251.705px;" class="animable" id="elji6qekkxvdp"/></g><g id="elz9ced8xjffh"><path d="M74.4,264.85a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.25,4.25,0,0,1-.53-2.49v-2.25a8.13,8.13,0,0,1,.53-3.1A3.54,3.54,0,0,1,72.42,260a1,1,0,0,1,1.45.14,4.25,4.25,0,0,1,.53,2.49Zm-.84-2.05a3.46,3.46,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.14a2.08,2.08,0,0,0-.83,1.11,5.86,5.86,0,0,0-.29,2v2.79a3.53,3.53,0,0,0,.29,1.7c.2.31.48.36.84.14a2,2,0,0,0,.84-1.11,5.88,5.88,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 72.43px 264.872px;" class="animable" id="elvzan6dsm7b"/></g><path d="M71.15,282.92l1-.73v-7.14l-1,.59v-.8l1.81-1.65v8.51l1-.36v.82l-2.74,1.58Z" style="fill: #7F68C8; transform-origin: 72.555px 278.465px;" id="elhp9lg9q29mh" class="animable"/><g id="eli2l7l9qf6g7"><path d="M74.4,291.63a8.13,8.13,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.25,4.25,0,0,1-.53-2.49v-2.25a8.13,8.13,0,0,1,.53-3.1,3.54,3.54,0,0,1,1.43-1.81,1,1,0,0,1,1.45.14,4.25,4.25,0,0,1,.53,2.49Zm-.84-2.05a3.46,3.46,0,0,0-.29-1.69c-.19-.3-.48-.35-.85-.14a2,2,0,0,0-.83,1.11,5.82,5.82,0,0,0-.29,2v2.79a3.53,3.53,0,0,0,.29,1.7c.2.31.48.35.84.14a2,2,0,0,0,.84-1.11,5.88,5.88,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 72.43px 291.632px;" class="animable" id="elzcfiv2v3h0k"/></g><g id="el65mawy68y3m"><path d="M74.4,305a8.16,8.16,0,0,1-.53,3.1,3.52,3.52,0,0,1-1.44,1.8,1,1,0,0,1-1.44-.14,4.25,4.25,0,0,1-.53-2.49V305a8.13,8.13,0,0,1,.53-3.1,3.54,3.54,0,0,1,1.43-1.81,1,1,0,0,1,1.45.14,4.25,4.25,0,0,1,.53,2.49Zm-.84-2a3.46,3.46,0,0,0-.29-1.69c-.19-.31-.48-.35-.85-.14a2,2,0,0,0-.83,1.11,5.82,5.82,0,0,0-.29,2v2.79a3.53,3.53,0,0,0,.29,1.7.55.55,0,0,0,.84.14,2,2,0,0,0,.84-1.11,5.88,5.88,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 72.43px 304.992px;" class="animable" id="elehnzmtm0ypv"/></g><path d="M59,156.06l1-.74v-7.13l-1,.59V148l1.81-1.64v8.51l1-.37v.83L59,156.88Z" style="fill: #7F68C8; transform-origin: 60.405px 151.62px;" id="elbrmnminefaf" class="animable"/><g id="elntu1vuqjem"><path d="M59,169.45l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83L59,170.27Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.405px 164.995px;" class="animable" id="eltyar1bbpsze"/></g><path d="M62.21,178.16a8.12,8.12,0,0,1-.52,3.09,3.59,3.59,0,0,1-1.44,1.81q-.91.52-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.29.47.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 60.2447px 178.162px;" id="elwjnrvep6srr" class="animable"/><g id="eluh8v6z7i6f"><path d="M59,196.23l1-.74v-7.13L59,189v-.81l1.81-1.64V195l1-.37v.83L59,197.05Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.405px 191.8px;" class="animable" id="ellmrekj5hy69"/></g><g id="elt463iz2p79b"><path d="M62.21,204.94a8.08,8.08,0,0,1-.52,3.09,3.55,3.55,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48V205a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2.05a3.46,3.46,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6.07,6.07,0,0,0-.29,2V207a3.39,3.39,0,0,0,.3,1.7c.19.3.47.35.84.14a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 60.2447px 204.969px;" class="animable" id="elsfiqewx956"/></g><path d="M62.21,218.33a8.12,8.12,0,0,1-.52,3.09,3.59,3.59,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2.05a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.29.47.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 60.2447px 218.334px;" id="elrmqyf1pv1z" class="animable"/><g id="el8djyffnjgas"><path d="M59,236.4l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83L59,237.22Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.405px 231.945px;" class="animable" id="eln3pkxfc1iqf"/></g><g id="el1nj2luuxy5v"><path d="M62.21,245.1a8.14,8.14,0,0,1-.52,3.1A3.59,3.59,0,0,1,60.25,250q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.29.47.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.2447px 245.104px;" class="animable" id="eleskj2u5tfi"/></g><path d="M62.21,258.5a8.08,8.08,0,0,1-.52,3.09,3.59,3.59,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2.05a3.46,3.46,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6.07,6.07,0,0,0-.29,2v2.79a3.39,3.39,0,0,0,.3,1.7c.19.3.47.35.84.14a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; transform-origin: 60.2447px 258.504px;" id="el56nxvb13mg9" class="animable"/><g id="elvhf2jdurgos"><path d="M59,276.57l1-.74V268.7l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83L59,277.39Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.405px 272.115px;" class="animable" id="eljxy7p5vt3go"/></g><g id="elh6qwqp2jvss"><path d="M59,290l1-.74v-7.13l-1,.59v-.81l1.81-1.64v8.51l1-.37v.83L59,290.78Z" style="fill: #7F68C8; opacity: 0.15; transform-origin: 60.405px 285.525px;" class="animable" id="eltgt7nrbg0s9"/></g><g id="elxsskw04ltep"><path d="M62.21,298.67a8.12,8.12,0,0,1-.52,3.09,3.59,3.59,0,0,1-1.44,1.81q-.91.53-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2.05a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.29.46.84.15a2,2,0,0,0,.84-1.12,6,6,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.2447px 298.674px;" class="animable" id="elwdk82w6n2i"/></g><path d="M59,316.74l1-.74v-7.13l-1,.59v-.81L60.76,307v8.51l1-.37V316L59,317.56Z" style="fill: #7F68C8; transform-origin: 60.38px 312.28px;" id="elv8l6pilm9ql" class="animable"/><g id="eltgd55xvagkk"><path d="M62.21,325.44a8.14,8.14,0,0,1-.52,3.1,3.59,3.59,0,0,1-1.44,1.81q-.91.52-1.44-.15a4.12,4.12,0,0,1-.53-2.48v-2.26a8.21,8.21,0,0,1,.52-3.1,3.52,3.52,0,0,1,1.44-1.8c.61-.35,1.09-.31,1.44.14a4.17,4.17,0,0,1,.53,2.49Zm-.83-2a3.43,3.43,0,0,0-.29-1.69c-.2-.31-.48-.35-.85-.14a2,2,0,0,0-.84,1.11,6,6,0,0,0-.29,2v2.8a3.32,3.32,0,0,0,.3,1.69q.29.46.84.15a2,2,0,0,0,.84-1.11,6.13,6.13,0,0,0,.29-2Z" style="fill: #7F68C8; opacity: 0.3; transform-origin: 60.2447px 325.452px;" class="animable" id="eleb4i8jugnau"/></g></g></g><g id="freepik--Window--inject-112" class="animable" style="transform-origin: 151.245px 174.625px;"><g id="freepik--window-code--inject-112" class="animable" style="transform-origin: 151.245px 174.625px;"><path d="M209.1,69.32V217.78a6.82,6.82,0,0,1-3.09,5.34l-105.6,60.93a2,2,0,0,1-1,.3,1.74,1.74,0,0,1-.39-.05,2.15,2.15,0,0,1-.27-.1l-.16-.09-.18-.12a1.22,1.22,0,0,1-.17-.15,2.33,2.33,0,0,1-.57-.81,3.91,3.91,0,0,1-.32-1.56V133.77a6.83,6.83,0,0,1,3.08-5.35L206,67.54a2.2,2.2,0,0,1,2.07-.26,1.17,1.17,0,0,1,.27.15l0,0a1,1,0,0,1,.2.2.48.48,0,0,1,.09.11.54.54,0,0,1,.1.16,1.59,1.59,0,0,1,.18.4.77.77,0,0,1,.08.27.07.07,0,0,1,0,.06,1.25,1.25,0,0,1,.05.29C209.09,69.06,209.1,69.19,209.1,69.32Z" style="fill: rgb(69, 90, 100); transform-origin: 153.225px 175.741px;" id="elpnunrjankxp" class="animable"/><path d="M207.38,68.12a.63.63,0,0,1,.3.07l0,0h0l.05,0,.09.11h0l.07.11,0,0,0,0,.1.29v0l0,0v0a.2.2,0,0,1,0,.07,2.83,2.83,0,0,1,0,.3V217.78a5.82,5.82,0,0,1-2.59,4.47L99.92,283.18a1,1,0,0,1-.52.17h0l-.21,0-.05,0h0l-.15-.1h0l0,0,0,0a1.5,1.5,0,0,1-.33-.48,2.91,2.91,0,0,1-.23-1.15V133.77a5.81,5.81,0,0,1,2.58-4.48l105.6-60.88a1.78,1.78,0,0,1,.87-.29m0-1a2.83,2.83,0,0,0-1.37.42l-105.6,60.88a6.83,6.83,0,0,0-3.08,5.35v147.7a3.91,3.91,0,0,0,.32,1.56,2.33,2.33,0,0,0,.57.81,1.22,1.22,0,0,0,.17.15l.18.12.16.09a2.15,2.15,0,0,0,.27.1,1.74,1.74,0,0,0,.39.05h0a1.92,1.92,0,0,0,1-.3L206,223.12a6.82,6.82,0,0,0,3.09-5.34V69.32c0-.13,0-.26,0-.38a1.25,1.25,0,0,0-.05-.29.07.07,0,0,0,0-.06.77.77,0,0,0-.08-.27,1.59,1.59,0,0,0-.18-.4.54.54,0,0,0-.1-.16.48.48,0,0,0-.09-.11,1,1,0,0,0-.2-.2l0,0a1.17,1.17,0,0,0-.27-.15,1.63,1.63,0,0,0-.7-.16Z" style="fill: rgb(69, 90, 100); transform-origin: 153.26px 175.735px;" id="el5ostfaikpb7" class="animable"/><g id="elvvv55x3lgg"><g style="opacity: 0.8; transform-origin: 153.225px 175.741px;" class="animable" id="eldgufomyvgue"><path d="M209.1,69.32V217.78a6.82,6.82,0,0,1-3.09,5.34l-105.6,60.93a2,2,0,0,1-1,.3,1.74,1.74,0,0,1-.39-.05,2.15,2.15,0,0,1-.27-.1l-.16-.09-.18-.12a1.22,1.22,0,0,1-.17-.15,2.33,2.33,0,0,1-.57-.81,3.91,3.91,0,0,1-.32-1.56V133.77a6.83,6.83,0,0,1,3.08-5.35L206,67.54a2.2,2.2,0,0,1,2.07-.26,1.17,1.17,0,0,1,.27.15l0,0a1,1,0,0,1,.2.2.48.48,0,0,1,.09.11.54.54,0,0,1,.1.16,1.59,1.59,0,0,1,.18.4.77.77,0,0,1,.08.27.07.07,0,0,1,0,.06,1.25,1.25,0,0,1,.05.29C209.09,69.06,209.1,69.19,209.1,69.32Z" style="fill: rgb(69, 90, 100); transform-origin: 153.225px 175.741px;" id="elzrpqbw2ugh" class="animable"/></g></g><path d="M208.08,67.28a2.2,2.2,0,0,0-2.07.26l-105.6,60.88a6.83,6.83,0,0,0-3.08,5.35v10.07l-3.94-2.3v-10a6.81,6.81,0,0,1,3.08-5.33l105.6-60.89a2.17,2.17,0,0,1,2.24-.18C204.87,65.47,207.43,66.93,208.08,67.28Z" style="fill: #7F68C8; transform-origin: 150.735px 104.375px;" id="elnwm3iwmlv9" class="animable"/><path d="M209.09,69.3c0-2-1.39-2.74-3.08-1.76l-105.6,60.89a6.81,6.81,0,0,0-3.08,5.33v10.09L209.09,79.42Z" style="fill: #7F68C8; transform-origin: 153.21px 105.48px;" id="elnjr9kunhr6q" class="animable"/><g id="elqupqeugpdsl"><g style="opacity: 0.4; transform-origin: 151.2px 97.8052px;" class="animable" id="el7v7h6m03icq"><path d="M206,67.54,100.42,128.42a6.41,6.41,0,0,0-2.18,2.29l-3.91-2.27a6.19,6.19,0,0,1,2.14-2.25l105.6-60.88a2.17,2.17,0,0,1,2.24-.18c.55.34,3.11,1.79,3.76,2.14A2.24,2.24,0,0,0,206,67.54Z" style="fill: rgb(255, 255, 255); transform-origin: 151.2px 97.8052px;" id="elyiwgda0049" class="animable"/></g></g><g id="el58gukpivif"><path d="M93.39,141.55v-10a6.3,6.3,0,0,1,.94-3.09l3.92,2.26a6.27,6.27,0,0,0-.92,3.07v10.08Z" style="opacity: 0.15; transform-origin: 95.82px 136.165px;" class="animable" id="elh0bvh10evq7"/></g><path d="M190.52,80.78a4.58,4.58,0,0,0-1.74,3.69c0,1.48.78,2.23,1.74,1.67a4.6,4.6,0,0,0,1.74-3.69C192.26,81,191.48,80.22,190.52,80.78Z" style="fill: rgb(69, 90, 100); transform-origin: 190.52px 83.4617px;" id="eljlx0bwcgpz" class="animable"/><path d="M196.79,77.15a4.61,4.61,0,0,0-1.74,3.69c0,1.48.78,2.23,1.74,1.67a4.58,4.58,0,0,0,1.74-3.68C198.53,77.35,197.75,76.6,196.79,77.15Z" style="fill: rgb(69, 90, 100); transform-origin: 196.79px 79.8327px;" id="el9iykatcvpyo" class="animable"/><path d="M203.06,73.52a4.61,4.61,0,0,0-1.74,3.69c0,1.48.78,2.23,1.74,1.68a4.61,4.61,0,0,0,1.74-3.69C204.8,73.72,204,73,203.06,73.52Z" style="fill: rgb(69, 90, 100); transform-origin: 203.06px 76.211px;" id="elrgdc4zji7b" class="animable"/><path d="M98.46,284.06l-.38-.22-1-.54h0L96.6,283l0,0-.81-.47-.68-.38c-.18-.11-.32-.18-.38-.21a1.68,1.68,0,0,1-.53-.43,2.47,2.47,0,0,1-.42-.63,3.82,3.82,0,0,1-.35-1.62V141.54l3.94,2.3V281.47a4.63,4.63,0,0,0,.32,1.56,2.33,2.33,0,0,0,.57.81.5.5,0,0,0,.17.15Z" style="fill: rgb(55, 71, 79); transform-origin: 95.945px 212.8px;" id="elfomsbrj8ge6" class="animable"/><path d="M104.9,152.22a1.15,1.15,0,0,1,.16-.57l2.22-4.59a.42.42,0,0,1,.1-.14l.05,0a.15.15,0,0,1,.18,0,.32.32,0,0,1,.07.23v1.54a1.6,1.6,0,0,1-.05.48,2,2,0,0,1-.15.42L106.31,152l1.17,1.07a.63.63,0,0,1,.15.24,1.16,1.16,0,0,1,.05.43v1.54a.7.7,0,0,1-.07.31.48.48,0,0,1-.18.21l-.05,0a.12.12,0,0,1-.1,0l-2.22-2a.46.46,0,0,1-.16-.4Z" style="fill: #7F68C8; transform-origin: 106.29px 151.35px;" id="elhh1fg55xfkw" class="animable"/><path d="M113,140.72a2.07,2.07,0,0,1,.17-.45.8.8,0,0,1,.32-.36l.84-.48a.12.12,0,0,1,.17,0,.28.28,0,0,1,.07.22.56.56,0,0,1,0,.17l-3.8,16.2a2,2,0,0,1-.16.44.79.79,0,0,1-.33.37l-.8.46a.15.15,0,0,1-.18,0,.32.32,0,0,1-.07-.23,1.28,1.28,0,0,1,0-.17Z" style="fill: #7F68C8; transform-origin: 111.902px 148.357px;" id="el0kbjbkrzdq5m" class="animable"/><path d="M118.89,145.29a1.18,1.18,0,0,1-.16.57l-2.21,4.59a.64.64,0,0,1-.1.14l-.06,0a.12.12,0,0,1-.17,0,.28.28,0,0,1-.08-.22v-1.54a2,2,0,0,1,0-.48,2.06,2.06,0,0,1,.16-.42l1.17-2.42-1.17-1.08a.57.57,0,0,1-.16-.23,1.43,1.43,0,0,1,0-.43v-1.54a.71.71,0,0,1,.08-.31.46.46,0,0,1,.17-.21l.06,0a.13.13,0,0,1,.1,0l2.21,2a.45.45,0,0,1,.16.39Z" style="fill: #7F68C8; transform-origin: 117.493px 146.163px;" id="elo1b4t7zprw" class="animable"/><path d="M125.05,144.22a.18.18,0,0,1,.22,0,.37.37,0,0,1,.09.27v1.7a.88.88,0,0,1-.09.38.49.49,0,0,1-.22.26l-1,.61a.17.17,0,0,1-.22,0,.37.37,0,0,1-.09-.27v-1.7a.88.88,0,0,1,.09-.38.53.53,0,0,1,.22-.26Z" style="fill: #7F68C8; transform-origin: 124.55px 145.831px;" id="el2d8r5fo1c73" class="animable"/><path d="M128.55,142.2a.18.18,0,0,1,.22,0,.38.38,0,0,1,.09.28v1.69a.85.85,0,0,1-.09.38.59.59,0,0,1-.22.26l-1,.61a.17.17,0,0,1-.22,0,.35.35,0,0,1-.09-.27v-1.7a.88.88,0,0,1,.09-.38.49.49,0,0,1,.22-.26Z" style="fill: #7F68C8; transform-origin: 128.05px 143.811px;" id="elmhvd1hdxlip" class="animable"/><path d="M132.05,140.18a.17.17,0,0,1,.21,0,.38.38,0,0,1,.09.28v1.69a.85.85,0,0,1-.09.38.61.61,0,0,1-.21.27l-1.05.6a.15.15,0,0,1-.21,0,.34.34,0,0,1-.09-.27v-1.7a.88.88,0,0,1,.09-.38.57.57,0,0,1,.21-.26Z" style="fill: #7F68C8; transform-origin: 131.525px 141.793px;" id="elw0mzj1vteqb" class="animable"/><path d="M105.84,164.59l71.55-41.31c.6-.35,1.1-.1,1.1.56a2.35,2.35,0,0,1-1.1,1.81L105.84,167c-.61.35-1.1.1-1.1-.56A2.35,2.35,0,0,1,105.84,164.59Z" style="fill: #7F68C8; transform-origin: 141.615px 145.14px;" id="elsg2sv68gnpg" class="animable"/><path d="M122.28,183.58l71.55-41.31c.61-.35,1.1-.11,1.1.55a2.34,2.34,0,0,1-1.1,1.82L122.28,186c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,122.28,183.58Z" style="fill: rgb(242, 143, 143); transform-origin: 158.055px 164.134px;" id="elpqkewvws0vm" class="animable"/><path d="M122.28,193.07l44.83-25.88c.61-.35,1.1-.1,1.1.56a2.35,2.35,0,0,1-1.1,1.82l-44.83,25.87c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,122.28,193.07Z" style="fill: rgb(242, 143, 143); transform-origin: 144.695px 181.315px;" id="elf155f7syfkw" class="animable"/><path d="M122.28,202.56l44.83-25.87c.61-.35,1.1-.1,1.1.55a2.34,2.34,0,0,1-1.1,1.82l-44.83,25.88c-.61.34-1.1.1-1.1-.56A2.35,2.35,0,0,1,122.28,202.56Z" style="fill: rgb(242, 143, 143); transform-origin: 144.695px 190.813px;" id="elte5yyloijl" class="animable"/><path d="M181.64,120.83,202.05,109c.61-.35,1.1-.1,1.1.55a2.34,2.34,0,0,1-1.1,1.82L181.64,123.2c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,181.64,120.83Z" style="fill: #7F68C8; transform-origin: 191.845px 116.1px;" id="el50591kah65g" class="animable"/><path d="M123.47,258.92l53.92-31.23c.6-.35,1.1-.1,1.1.55a2.36,2.36,0,0,1-1.1,1.82L123.47,261.3c-.6.34-1.09.1-1.09-.56A2.37,2.37,0,0,1,123.47,258.92Z" style="fill: #7F68C8; transform-origin: 150.435px 244.493px;" id="elxymurg60mn9" class="animable"/><path d="M181.64,225.24l20.41-11.83c.61-.35,1.1-.11,1.1.55a2.34,2.34,0,0,1-1.1,1.82l-20.41,11.83c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,181.64,225.24Z" style="fill: #7F68C8; transform-origin: 191.845px 220.509px;" id="elxoifesvqx9i" class="animable"/><path d="M132.68,225l69.37-40c.61-.35,1.1-.1,1.1.55a2.34,2.34,0,0,1-1.1,1.82l-69.37,40c-.6.35-1.09.1-1.09-.56A2.37,2.37,0,0,1,132.68,225Z" style="fill: #7F68C8; transform-origin: 167.37px 206.185px;" id="elk2fe3t9g8v" class="animable"/><path d="M115.28,244.85l86.77-50.38c.61-.35,1.1-.1,1.1.55a2.34,2.34,0,0,1-1.1,1.82l-86.77,50.38c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,115.28,244.85Z" style="fill: #7F68C8; transform-origin: 158.665px 220.845px;" id="elx1op3usbiq" class="animable"/><path d="M119.36,251.83l58-33.62c.6-.35,1.1-.1,1.1.55a2.36,2.36,0,0,1-1.1,1.82l-58,33.62c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,119.36,251.83Z" style="fill: rgb(255, 168, 167); transform-origin: 148.36px 236.205px;" id="elnl5ssnl9abp" class="animable"/><path d="M115.28,235l11.47-6.63c.6-.34,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-11.47,6.62c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,115.28,235Z" style="fill: #7F68C8; transform-origin: 121.01px 232.872px;" id="eldco2mephato" class="animable"/><path d="M105.84,174.11l50.2-29c.6-.35,1.09-.1,1.09.56a2.33,2.33,0,0,1-1.09,1.81l-50.2,29c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,105.84,174.11Z" style="fill: #7F68C8; transform-origin: 130.935px 160.795px;" id="el7xq4syv0ydx" class="animable"/><g id="elxs678bpnbqk"><path d="M105.84,174.11l50.2-29c.6-.35,1.09-.1,1.09.56a2.33,2.33,0,0,1-1.09,1.81l-50.2,29c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,105.84,174.11Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 130.935px 160.795px;" class="animable" id="el9pq2t5oiuh"/></g><path d="M105.84,183.58,116,177.72c.6-.35,1.09-.1,1.09.55a2.35,2.35,0,0,1-1.09,1.82L105.84,186c-.61.35-1.1.11-1.1-.55A2.35,2.35,0,0,1,105.84,183.58Z" style="fill: #7F68C8; transform-origin: 110.915px 181.861px;" id="elkguqy85q2i" class="animable"/><g id="eloc2ty5wtxsm"><path d="M105.84,183.58,116,177.72c.6-.35,1.09-.1,1.09.55a2.35,2.35,0,0,1-1.09,1.82L105.84,186c-.61.35-1.1.11-1.1-.55A2.35,2.35,0,0,1,105.84,183.58Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 110.915px 181.861px;" class="animable" id="ell7l5ahcnyub"/></g><path d="M105.84,193.07,116,187.21c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.56A2.35,2.35,0,0,1,105.84,193.07Z" style="fill: #7F68C8; transform-origin: 110.925px 191.33px;" id="elj4i1qqwl7l8" class="animable"/><g id="elwooqk2ie62"><path d="M105.84,193.07,116,187.21c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.56A2.35,2.35,0,0,1,105.84,193.07Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 110.925px 191.33px;" class="animable" id="elcl24cvao2l5"/></g><path d="M105.84,202.57,116,196.7c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.56A2.32,2.32,0,0,1,105.84,202.57Z" style="fill: #7F68C8; transform-origin: 110.925px 200.82px;" id="elnbqb0gfvq9d" class="animable"/><g id="elvh63ass0i2s"><path d="M105.84,202.57,116,196.7c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.56A2.32,2.32,0,0,1,105.84,202.57Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 110.925px 200.82px;" class="animable" id="elsfj9qgfwkm"/></g><path d="M105.84,212.06,116,206.2c.6-.35,1.09-.11,1.09.55a2.35,2.35,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,105.84,212.06Z" style="fill: #7F68C8; transform-origin: 110.925px 210.314px;" id="elmdm5rq5b6l8" class="animable"/><g id="el4g4j4uzmae8"><path d="M105.84,212.06,116,206.2c.6-.35,1.09-.11,1.09.55a2.35,2.35,0,0,1-1.09,1.82l-10.14,5.86c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,105.84,212.06Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 110.925px 210.314px;" class="animable" id="elrqz9pcnjyk"/></g><path d="M160.28,142.66l13-7.48c.6-.35,1.09-.1,1.09.55a2.35,2.35,0,0,1-1.09,1.82l-13,7.48c-.6.35-1.09.1-1.09-.55A2.35,2.35,0,0,1,160.28,142.66Z" style="fill: #7F68C8; transform-origin: 166.78px 140.105px;" id="elszu9h1006y" class="animable"/><g id="el7ihq04rqxov"><path d="M160.28,142.66l13-7.48c.6-.35,1.09-.1,1.09.55a2.35,2.35,0,0,1-1.09,1.82l-13,7.48c-.6.35-1.09.1-1.09-.55A2.35,2.35,0,0,1,160.28,142.66Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 166.78px 140.105px;" class="animable" id="elt6dxxihr3vn"/></g><path d="M121.5,212.42l63.31-36.49c.6-.35,1.09-.11,1.09.55a2.35,2.35,0,0,1-1.09,1.82L121.5,214.8c-.6.35-1.09.1-1.09-.56A2.37,2.37,0,0,1,121.5,212.42Z" style="fill: rgb(255, 168, 167); transform-origin: 153.155px 195.364px;" id="eldi2xv3d0bkl" class="animable"/><path d="M105.84,231.19l96.21-55.69c.61-.35,1.1-.1,1.1.55a2.34,2.34,0,0,1-1.1,1.82l-96.21,55.69c-.61.35-1.1.11-1.1-.55A2.35,2.35,0,0,1,105.84,231.19Z" style="fill: rgb(255, 168, 167); transform-origin: 153.945px 204.531px;" id="elp8dc6565uaq" class="animable"/><path d="M189.05,173.47l13-7.48c.61-.35,1.1-.1,1.1.56a2.35,2.35,0,0,1-1.1,1.82l-13,7.48c-.6.35-1.09.1-1.09-.56A2.37,2.37,0,0,1,189.05,173.47Z" style="fill: rgb(255, 168, 167); transform-origin: 195.555px 170.92px;" id="el6rkdekqujfd" class="animable"/><path d="M122.28,174.09l50.2-29c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-50.2,29c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,122.28,174.09Z" style="fill: #7F68C8; transform-origin: 147.375px 160.78px;" id="el3ejb1ars3vl" class="animable"/><g id="eld0z6t44sxoe"><path d="M122.28,174.09l50.2-29c.6-.35,1.09-.1,1.09.56a2.37,2.37,0,0,1-1.09,1.82l-50.2,29c-.61.35-1.1.1-1.1-.55A2.34,2.34,0,0,1,122.28,174.09Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 147.375px 160.78px;" class="animable" id="elcd2w25g7rlj"/></g><path d="M176.72,142.64l13-7.48c.6-.35,1.1-.1,1.1.56a2.38,2.38,0,0,1-1.1,1.82l-13,7.48c-.6.34-1.09.1-1.09-.56A2.37,2.37,0,0,1,176.72,142.64Z" style="fill: #7F68C8; transform-origin: 183.225px 140.088px;" id="elx621x9jjrxl" class="animable"/><g id="els3k8x166pq"><path d="M176.72,142.64l13-7.48c.6-.35,1.1-.1,1.1.56a2.38,2.38,0,0,1-1.1,1.82l-13,7.48c-.6.34-1.09.1-1.09-.56A2.37,2.37,0,0,1,176.72,142.64Z" style="fill: rgb(255, 255, 255); opacity: 0.6; transform-origin: 183.225px 140.088px;" class="animable" id="elkxtdomo42k"/></g></g></g><g id="freepik--Sign--inject-112" class="animable" style="transform-origin: 100.344px 285.463px;"><g id="freepik--Alert--inject-112" class="animable" style="transform-origin: 100.344px 285.463px;"><path d="M128.61,283.25l-18.72-32.42a10.13,10.13,0,0,0-3.61-4c-.7-.39-3.5-2-4.23-2.45-2.91-1.65-5.89.06-7.61,5L71.05,317c-1.34,3.86-1,6.41.7,7.32.72.39,3.44,2,4.13,2.39,1.36.85,3.68.51,6.71-1.23l41.07-23.71A13.55,13.55,0,0,0,128.61,283.25Z" style="fill: #7F68C8; transform-origin: 100.33px 285.463px;" id="eli9cezu6vctc" class="animable"/><g id="el2610porhcv"><path d="M128.61,283.25l-18.72-32.42a10.13,10.13,0,0,0-3.61-4c-.7-.39-3.5-2-4.23-2.45-2.91-1.65-5.89.06-7.61,5L71.05,317c-1.34,3.86-1,6.41.7,7.32.72.39,3.44,2,4.13,2.39,1.36.85,3.68.51,6.71-1.23l41.07-23.71A13.55,13.55,0,0,0,128.61,283.25Z" style="opacity: 0.2; transform-origin: 100.33px 285.463px;" class="animable" id="elsohdm6sbkw"/></g><path d="M98.69,251.9l-23.4,67.53c-2.44,7.07.82,9.77,7.3,6l41.07-23.71a13.55,13.55,0,0,0,5-18.5l-18.72-32.42C106.15,244.35,101.14,244.83,98.69,251.9Z" style="fill: #7F68C8; transform-origin: 102.471px 286.698px;" id="el92fmx47m77j" class="animable"/><path d="M105.55,292.7l-3.35,1.93A.71.71,0,0,1,101,294l-1.36-25a2.25,2.25,0,0,1,1-1.9L107,263.4c.61-.35,1.07-.07,1,.63l-1.37,26.77A2.57,2.57,0,0,1,105.55,292.7Z" style="fill: rgb(55, 71, 79); transform-origin: 103.823px 279.043px;" id="elv45yai7e0cp" class="animable"/><path d="M103.88,306.17c-2.14,1.23-3.42-.36-3.42-2.58a8.05,8.05,0,0,1,3.42-6.52c2.13-1.23,3.41.36,3.41,2.58A8.09,8.09,0,0,1,103.88,306.17Z" style="fill: rgb(55, 71, 79); transform-origin: 103.875px 301.62px;" id="elem6xjobb5q" class="animable"/></g></g><g id="freepik--Card--inject-112" class="animable" style="transform-origin: 134.891px 344.224px;"><g id="freepik--card--inject-112" class="animable" style="transform-origin: 134.891px 344.224px;"><path d="M105.35,383.47v-26.8l-2.4-2.09v27.09a2.34,2.34,0,0,0,.83,2l2.29,1.75S105.35,383.5,105.35,383.47Z" style="fill: #7F68C8; transform-origin: 104.506px 370px;" id="el87tdpbqmide" class="animable"/><g id="elkspsdq3s2p"><path d="M105.35,383.47v-26.8l-2.4-2.09v27.09a2.34,2.34,0,0,0,.84,2l2.29,1.75S105.35,383.5,105.35,383.47Z" style="opacity: 0.35; transform-origin: 104.511px 370px;" class="animable" id="ela2mtw3zypol"/></g><path d="M105.36,356.67v26.8c0,1.82,1.1,2.65,2.46,1.86l56.56-32.65.05,0a5.91,5.91,0,0,0,2.41-4.66V321.17S105.36,356.64,105.36,356.67Z" style="fill: #7F68C8; transform-origin: 136.1px 353.399px;" id="elqnx6rdye3gc" class="animable"/><g id="elpemfngixfq"><path d="M105.36,356.67v26.8c0,1.82,1.1,2.65,2.46,1.86l56.56-32.65.05,0a5.91,5.91,0,0,0,2.41-4.66V321.17S105.36,356.64,105.36,356.67Z" style="opacity: 0.15; transform-origin: 136.1px 353.399px;" class="animable" id="elqoen3c6u9l8"/></g><path d="M105.36,356.67V342.32a.31.31,0,0,1,0-.09,5.9,5.9,0,0,1,2.45-4.61L164.38,305c1.35-.78,2.46,0,2.46,1.85v14.35Z" style="fill: rgb(69, 90, 100); transform-origin: 136.098px 330.685px;" id="eldsj4vltucl" class="animable"/><path d="M163.6,303a1.51,1.51,0,0,0-1.62.12l-56.55,32.67a5.18,5.18,0,0,0-2,2.61h0a5.94,5.94,0,0,0-.42,2.08v14.36l2.38,1.8V342.31a5.94,5.94,0,0,1,.42-2.08h0a5.29,5.29,0,0,1,2-2.62L164.38,305l1.46-.2S164.18,303.44,163.6,303Z" style="fill: rgb(55, 71, 79); transform-origin: 134.425px 329.73px;" id="ell26h3t2makd" class="animable"/><path d="M111.68,363.34l-1.09.64a.15.15,0,0,1-.13,0,.14.14,0,0,1,0-.12.41.41,0,0,1,0-.18.44.44,0,0,1,.13-.14l.36-.21v-2.1c0-.12,0-.19-.1-.23a.21.21,0,0,0-.25,0h0a.12.12,0,0,1-.13,0,.13.13,0,0,1,0-.11.41.41,0,0,1,0-.18.36.36,0,0,1,.13-.14h0a.44.44,0,0,1,.73.42v2.1l.36-.21a.12.12,0,0,1,.13,0,.12.12,0,0,1,.06.12.32.32,0,0,1-.06.18A.27.27,0,0,1,111.68,363.34Z" style="fill: rgb(255, 255, 255); transform-origin: 111.164px 362.223px;" id="elbco0w3cxppp" class="animable"/><path d="M114.06,360.71l-.73.42a.85.85,0,0,0-.37.63v.32a.06.06,0,0,0,0,.06,0,0,0,0,0,.06,0l1.55-.89c.05,0,.1,0,.13,0a.11.11,0,0,1,.06.11.32.32,0,0,1-.06.18.35.35,0,0,1-.13.14l-1.82,1a.12.12,0,0,1-.13,0,.12.12,0,0,1-.05-.11V362a1.42,1.42,0,0,1,.21-.72,1.46,1.46,0,0,1,.52-.55l.73-.42a.73.73,0,0,0,.25-.27.63.63,0,0,0,.11-.36v-.42a.22.22,0,0,0-.36-.21l-.73.42a.75.75,0,0,0-.26.27.78.78,0,0,0-.11.36.41.41,0,0,1,0,.18.44.44,0,0,1-.13.14.12.12,0,0,1-.13,0,.14.14,0,0,1-.05-.12,1.48,1.48,0,0,1,.21-.72,1.5,1.5,0,0,1,.52-.54l.73-.42a.46.46,0,0,1,.51-.05.48.48,0,0,1,.22.47v.42a1.64,1.64,0,0,1-.73,1.26Z" style="fill: rgb(255, 255, 255); transform-origin: 113.692px 360.607px;" id="el2ee71u365i4" class="animable"/><path d="M117.7,358.82V359a1.48,1.48,0,0,1-.21.72,1.5,1.5,0,0,1-.52.54l-.73.42a.46.46,0,0,1-.51.05.48.48,0,0,1-.22-.47.38.38,0,0,1,.06-.18.3.3,0,0,1,.12-.13s.1,0,.13,0a.11.11,0,0,1,.06.11.22.22,0,0,0,.36.21l.73-.42a.75.75,0,0,0,.26-.27.7.7,0,0,0,.1-.36V359a.26.26,0,0,0-.1-.24.25.25,0,0,0-.26,0l-.18.1a.13.13,0,0,1-.13,0s0-.06,0-.12a.31.31,0,0,1,0-.18.35.35,0,0,1,.13-.14l.18-.1a.76.76,0,0,0,.36-.63v-.21a.25.25,0,0,0-.1-.24.25.25,0,0,0-.26,0l-.73.42a.84.84,0,0,0-.26.27.7.7,0,0,0-.1.36.32.32,0,0,1-.06.18.35.35,0,0,1-.13.14s-.09,0-.12,0a.12.12,0,0,1-.06-.12,1.39,1.39,0,0,1,.22-.72,1.34,1.34,0,0,1,.51-.54l.73-.42a.51.51,0,0,1,.52-.06.52.52,0,0,1,.21.48v.21a1.06,1.06,0,0,1-.07.38,1.38,1.38,0,0,1-.18.39.39.39,0,0,1,.18.18A.59.59,0,0,1,117.7,358.82Z" style="fill: rgb(255, 255, 255); transform-origin: 116.61px 358.745px;" id="elzgkrr8vxvlq" class="animable"/><path d="M120.56,358.16a.32.32,0,0,1-.13.13.13.13,0,0,1-.13,0,.14.14,0,0,1,0-.12V357a1.39,1.39,0,0,1-.36.33l-.73.42a.53.53,0,0,1-.52.05.5.5,0,0,1-.21-.47v-1.06a.41.41,0,0,1,0-.17.36.36,0,0,1,.13-.14.12.12,0,0,1,.13,0s.05.06.05.11v1.06a.25.25,0,0,0,.11.23.27.27,0,0,0,.26,0l.73-.42a.69.69,0,0,0,.25-.28.72.72,0,0,0,.11-.36v-1.05a.49.49,0,0,1,0-.18.4.4,0,0,1,.13-.13s.09,0,.13,0a.11.11,0,0,1,.06.11v3A.38.38,0,0,1,120.56,358.16Z" style="fill: rgb(255, 255, 255); transform-origin: 119.521px 356.624px;" id="el1w8niv8blgu" class="animable"/><path d="M124.62,353.66a.06.06,0,0,0,0,.06h.06l1-.58a.48.48,0,0,1,.51-.05.5.5,0,0,1,.22.47V354a1.49,1.49,0,0,1-.22.72,1.4,1.4,0,0,1-.51.54l-.73.42a.48.48,0,0,1-.52,0,.5.5,0,0,1-.21-.47.41.41,0,0,1,0-.18.4.4,0,0,1,.13-.13.13.13,0,0,1,.13,0s.05.06.05.12a.22.22,0,0,0,.37.21l.73-.42a.73.73,0,0,0,.25-.27.63.63,0,0,0,.11-.36v-.42a.25.25,0,0,0-.11-.24.24.24,0,0,0-.25,0l-1.1.63a.24.24,0,0,1-.25,0c-.08,0-.11-.12-.11-.24v-1.05a.41.41,0,0,1,0-.18.44.44,0,0,1,.13-.14l1.82-1.05a.12.12,0,0,1,.13,0,.11.11,0,0,1,.06.11.32.32,0,0,1-.06.18.44.44,0,0,1-.13.14l-1.55.89-.06.07a.16.16,0,0,0,0,.09Z" style="fill: rgb(255, 255, 255); transform-origin: 125.286px 353.614px;" id="elyoxot77bpp9" class="animable"/><path d="M127.9,351.88l.73-.42a.44.44,0,0,1,.73.41v.43a1.41,1.41,0,0,1-.21.71,1.46,1.46,0,0,1-.52.55l-.73.42a.45.45,0,0,1-.73-.42v-1.68a1.34,1.34,0,0,1,.22-.72,1.44,1.44,0,0,1,.51-.55l.73-.42a.53.53,0,0,1,.52-.05.5.5,0,0,1,.21.47.31.31,0,0,1-.05.18.44.44,0,0,1-.13.14.12.12,0,0,1-.13,0,.14.14,0,0,1-.05-.12.23.23,0,0,0-.37-.21l-.73.42a.69.69,0,0,0-.25.28.63.63,0,0,0-.11.36v.53A1.19,1.19,0,0,1,127.9,351.88Zm-.36,1.47a.25.25,0,0,0,.11.24.24.24,0,0,0,.25,0l.73-.42a.85.85,0,0,0,.37-.63v-.42a.22.22,0,0,0-.37-.21l-.73.42a.73.73,0,0,0-.25.27.63.63,0,0,0-.11.36Z" style="fill: rgb(255, 255, 255); transform-origin: 128.266px 352.088px;" id="elkdb66mppbpd" class="animable"/><path d="M132.19,349.51l-.82,2.26,0,0s0,0,0,0,0,0,0,0,0,0,0,.05l0,.05,0,0,0,0,0,0a.16.16,0,0,1-.13,0s-.05-.05-.05-.13a.86.86,0,0,1,.09-.26l.75-2.11a1.42,1.42,0,0,0,0-.22,1.62,1.62,0,0,0,0-.18v-.07c0-.12,0-.19-.11-.23a.21.21,0,0,0-.25,0l-.37.21-.18.11-.73.42a.12.12,0,0,1-.13,0,.14.14,0,0,1,0-.12.49.49,0,0,1,0-.18.4.4,0,0,1,.13-.13l.73-.42.18-.11.37-.21a.48.48,0,0,1,.51-.05.5.5,0,0,1,.22.47,1.25,1.25,0,0,1,0,.31C132.22,349.37,132.2,349.45,132.19,349.51Z" style="fill: rgb(255, 255, 255); transform-origin: 131.328px 350.011px;" id="elwkdzm5cg2sl" class="animable"/><path d="M135.19,348.72v.21a1.48,1.48,0,0,1-.21.72,1.36,1.36,0,0,1-.52.54l-.73.42a.46.46,0,0,1-.51.05.49.49,0,0,1-.22-.47V350a1.33,1.33,0,0,1,.07-.38,1.38,1.38,0,0,1,.18-.39.35.35,0,0,1-.18-.18.71.71,0,0,1-.07-.31v-.21a1.49,1.49,0,0,1,.22-.72,1.4,1.4,0,0,1,.51-.54l.73-.42a.48.48,0,0,1,.52-.05.5.5,0,0,1,.21.47v.21a1.15,1.15,0,0,1-.07.39,1.46,1.46,0,0,1-.18.38.39.39,0,0,1,.18.18A.59.59,0,0,1,135.19,348.72Zm-.36-1.26a.22.22,0,0,0-.37-.21l-.73.42a.73.73,0,0,0-.25.27.63.63,0,0,0-.11.36v.21a.25.25,0,0,0,.11.24.24.24,0,0,0,.25,0l.73-.42a.75.75,0,0,0,.26-.27.78.78,0,0,0,.11-.36Zm0,1.47a.27.27,0,0,0-.11-.24.25.25,0,0,0-.26,0l-.73.42a.73.73,0,0,0-.25.27.68.68,0,0,0-.11.36V350c0,.12,0,.2.11.24a.24.24,0,0,0,.25,0l.73-.42a.75.75,0,0,0,.26-.27.72.72,0,0,0,.11-.36Z" style="fill: rgb(255, 255, 255); transform-origin: 134.095px 348.73px;" id="eloqa5px2xpeq" class="animable"/><path d="M140.29,346.83l-.73.42a.51.51,0,0,1-.51.05.52.52,0,0,1-.22-.47.43.43,0,0,1,.06-.18.44.44,0,0,1,.13-.14.12.12,0,0,1,.13,0,.14.14,0,0,1,0,.12.25.25,0,0,0,.11.23.25.25,0,0,0,.25,0l.73-.42a.71.71,0,0,0,.26-.28.72.72,0,0,0,.11-.36v-.53a1.22,1.22,0,0,1-.37.32l-.73.43a.51.51,0,0,1-.51.05.52.52,0,0,1-.21-.48v-.42a1.41,1.41,0,0,1,.21-.71,1.44,1.44,0,0,1,.51-.55l.73-.42a.45.45,0,0,1,.74.42v1.68a1.46,1.46,0,0,1-.22.72A1.39,1.39,0,0,1,140.29,346.83Zm.37-2.74a.27.27,0,0,0-.11-.24.25.25,0,0,0-.26,0l-.73.42a.73.73,0,0,0-.25.27.78.78,0,0,0-.11.36v.42a.25.25,0,0,0,.11.24.24.24,0,0,0,.25,0l.73-.42a.68.68,0,0,0,.26-.27.72.72,0,0,0,.11-.36Z" style="fill: rgb(255, 255, 255); transform-origin: 139.887px 345.362px;" id="elh76ai5psth" class="animable"/><path d="M143,345.25l-1.09.63a.12.12,0,0,1-.13,0s-.05-.05-.05-.11a.41.41,0,0,1,.05-.18.36.36,0,0,1,.13-.14l.37-.21v-2.1a.22.22,0,0,0-.36-.22h0a.12.12,0,0,1-.13,0,.14.14,0,0,1-.05-.12.41.41,0,0,1,.05-.18.32.32,0,0,1,.13-.13h0a.51.51,0,0,1,.52-.06.52.52,0,0,1,.21.48V345l.36-.21a.12.12,0,0,1,.13,0,.12.12,0,0,1,.06.12.38.38,0,0,1-.06.18A.33.33,0,0,1,143,345.25Z" style="fill: rgb(255, 255, 255); transform-origin: 142.466px 344.142px;" id="el1p0ydgylwam" class="animable"/><path d="M145.4,343.88l-.73.42a.48.48,0,0,1-.52.05.5.5,0,0,1-.21-.47V342.2a1.48,1.48,0,0,1,.21-.72,1.36,1.36,0,0,1,.52-.54l.73-.43a.51.51,0,0,1,.51-.05.52.52,0,0,1,.21.48v1.68a1.48,1.48,0,0,1-.21.72A1.4,1.4,0,0,1,145.4,343.88Zm-.73,0,.73-.42a.73.73,0,0,0,.17-.15L144.3,342v1.65a.22.22,0,0,0,.37.21Zm.73-2.94-.73.42a.6.6,0,0,0-.18.15l1.27,1.28v-1.64a.25.25,0,0,0-.11-.24A.24.24,0,0,0,145.4,340.94Z" style="fill: rgb(255, 255, 255); transform-origin: 145.03px 342.409px;" id="el8gtbewirtit" class="animable"/><path d="M148.13,342.31l-1.1.63a.15.15,0,0,1-.13,0,.14.14,0,0,1-.05-.12.41.41,0,0,1,.05-.18.32.32,0,0,1,.13-.13l.37-.22v-2.1a.24.24,0,0,0-.11-.23.23.23,0,0,0-.25,0h0a.12.12,0,0,1-.13,0,.12.12,0,0,1-.05-.11.41.41,0,0,1,.05-.18.44.44,0,0,1,.13-.14h0a.44.44,0,0,1,.73.42v2.1l.37-.21a.11.11,0,0,1,.12,0,.12.12,0,0,1,.06.12.32.32,0,0,1-.06.18A.33.33,0,0,1,148.13,342.31Z" style="fill: rgb(255, 255, 255); transform-origin: 147.585px 341.183px;" id="elg8z3q6z8tgg" class="animable"/><path d="M153.23,339.36l-1.1.63a.11.11,0,0,1-.12,0,.11.11,0,0,1-.06-.11.32.32,0,0,1,.06-.18.28.28,0,0,1,.12-.14l.37-.21v-2.1a.22.22,0,0,0-.36-.22h0a.15.15,0,0,1-.13,0,.14.14,0,0,1-.05-.12.41.41,0,0,1,.05-.18.44.44,0,0,1,.13-.14h0a.53.53,0,0,1,.52-.05.51.51,0,0,1,.21.47v2.11l.37-.21c.05,0,.09,0,.13,0a.14.14,0,0,1,0,.12.41.41,0,0,1,0,.17A.36.36,0,0,1,153.23,339.36Z" style="fill: rgb(255, 255, 255); transform-origin: 152.666px 338.251px;" id="elcvsfr0ufewh" class="animable"/><path d="M155.6,336.73l-.73.42a.75.75,0,0,0-.26.27.7.7,0,0,0-.1.36v.32s0,0,0,0a.08.08,0,0,0,.07,0l1.55-.9a.12.12,0,0,1,.13,0,.14.14,0,0,1,.05.12.41.41,0,0,1-.05.18.4.4,0,0,1-.13.13l-1.83,1.06a.11.11,0,0,1-.12,0,.12.12,0,0,1-.06-.12V338a1.48,1.48,0,0,1,.21-.72,1.5,1.5,0,0,1,.52-.54l.73-.42a.71.71,0,0,0,.26-.28.72.72,0,0,0,.11-.36v-.42a.25.25,0,0,0-.11-.23.27.27,0,0,0-.26,0l-.73.43a.62.62,0,0,0-.26.27.7.7,0,0,0-.1.36.41.41,0,0,1-.19.31.14.14,0,0,1-.12,0,.12.12,0,0,1-.06-.12,1.45,1.45,0,0,1,.21-.72,1.54,1.54,0,0,1,.52-.55l.73-.42a.52.52,0,0,1,.52-.05.5.5,0,0,1,.21.47v.42a1.42,1.42,0,0,1-.21.72A1.54,1.54,0,0,1,155.6,336.73Z" style="fill: rgb(255, 255, 255); transform-origin: 155.216px 336.6px;" id="elb33k1yfqjv" class="animable"/><path d="M158.33,336.41l-1.09.64a.15.15,0,0,1-.13,0,.12.12,0,0,1-.06-.12.32.32,0,0,1,.06-.18.35.35,0,0,1,.13-.14l.36-.21v-2.1a.26.26,0,0,0-.1-.24.25.25,0,0,0-.26,0h0c-.06,0-.1,0-.13,0a.11.11,0,0,1-.06-.11.32.32,0,0,1,.06-.18.26.26,0,0,1,.13-.14h0a.48.48,0,0,1,.51-.05.5.5,0,0,1,.21.47v2.1l.37-.21a.12.12,0,0,1,.13,0,.14.14,0,0,1,0,.12.41.41,0,0,1,0,.18A.32.32,0,0,1,158.33,336.41Z" style="fill: rgb(255, 255, 255); transform-origin: 157.761px 335.297px;" id="elsg8txxtwiw" class="animable"/><path d="M161.43,333.57v.21a1.48,1.48,0,0,1-.21.72,1.43,1.43,0,0,1-.52.54l-.73.43a.51.51,0,0,1-.51,0,.5.5,0,0,1-.22-.48.32.32,0,0,1,.06-.17.28.28,0,0,1,.12-.14.12.12,0,0,1,.13,0,.11.11,0,0,1,.06.11.25.25,0,0,0,.1.24.25.25,0,0,0,.26,0l.73-.42a.62.62,0,0,0,.26-.27.72.72,0,0,0,.11-.36v-.21a.23.23,0,0,0-.37-.21l-.18.11a.12.12,0,0,1-.13,0s0-.06,0-.12a.35.35,0,0,1,0-.18.33.33,0,0,1,.13-.13l.18-.11a.75.75,0,0,0,.26-.27.72.72,0,0,0,.11-.36v-.21a.22.22,0,0,0-.37-.21l-.73.42a.75.75,0,0,0-.26.27.73.73,0,0,0-.1.36.32.32,0,0,1-.06.18.35.35,0,0,1-.13.14.11.11,0,0,1-.12,0,.12.12,0,0,1-.06-.12,1.39,1.39,0,0,1,.22-.72,1.48,1.48,0,0,1,.51-.54l.73-.42a.44.44,0,0,1,.73.42v.21a1.15,1.15,0,0,1-.07.39,1.46,1.46,0,0,1-.18.38.39.39,0,0,1,.18.18A.62.62,0,0,1,161.43,333.57Z" style="fill: rgb(255, 255, 255); transform-origin: 160.339px 333.534px;" id="elbdbk2yf94fr" class="animable"/><path d="M111.15,370l-.63.37a.08.08,0,0,1-.08,0s0,0,0-.07a.19.19,0,0,1,0-.1.15.15,0,0,1,.08-.08l.21-.13v-1.22c0-.07,0-.12-.06-.14a.16.16,0,0,0-.15,0h0a.08.08,0,0,1-.08,0s0,0,0-.07a.19.19,0,0,1,0-.1.15.15,0,0,1,.08-.08h0a.28.28,0,0,1,.3,0,.29.29,0,0,1,.12.27v1.23l.21-.12a.08.08,0,0,1,.08,0,.08.08,0,0,1,0,.07.23.23,0,0,1,0,.11A.18.18,0,0,1,111.15,370Z" style="fill: rgb(255, 255, 255); transform-origin: 110.836px 369.359px;" id="ele8okswcfclg" class="animable"/><path d="M112.54,368.51l-.43.24a.46.46,0,0,0-.15.16.42.42,0,0,0-.06.21v.19s0,0,0,0a0,0,0,0,0,0,0l.91-.53h.07a.07.07,0,0,1,0,.07.21.21,0,0,1,0,.1.19.19,0,0,1-.07.08l-1.07.62h-.07s0,0,0-.07v-.36a.84.84,0,0,1,.12-.42.93.93,0,0,1,.3-.32l.43-.25a.34.34,0,0,0,.15-.16.42.42,0,0,0,.06-.21v-.24c0-.07,0-.12-.06-.14a.14.14,0,0,0-.15,0l-.43.24a.39.39,0,0,0-.15.16.42.42,0,0,0-.06.21.19.19,0,0,1,0,.1.15.15,0,0,1-.08.08.06.06,0,0,1-.07,0s0,0,0-.07a.89.89,0,0,1,.42-.73l.43-.25a.28.28,0,0,1,.3,0,.3.3,0,0,1,.13.27v.25a.86.86,0,0,1-.13.42A.85.85,0,0,1,112.54,368.51Z" style="fill: rgb(255, 255, 255); transform-origin: 112.34px 368.413px;" id="elt9016i7xx" class="animable"/><path d="M115.09,367l-.42.25a.39.39,0,0,0-.15.16.35.35,0,0,0-.07.21v.18a.05.05,0,0,0,0,0h0l.9-.52a.08.08,0,0,1,.08,0,.09.09,0,0,1,0,.07.23.23,0,0,1,0,.11.3.3,0,0,1-.08.08l-1.06.61a.08.08,0,0,1-.08,0,.08.08,0,0,1,0-.07v-.37a.9.9,0,0,1,.12-.42,1,1,0,0,1,.31-.32l.42-.24a.46.46,0,0,0,.15-.16.44.44,0,0,0,.07-.21v-.25a.13.13,0,0,0-.07-.13s-.09,0-.15,0l-.42.25a.34.34,0,0,0-.15.16.35.35,0,0,0-.07.21.22.22,0,0,1,0,.1.19.19,0,0,1-.07.08.08.08,0,0,1-.08,0,.08.08,0,0,1,0-.07.84.84,0,0,1,.12-.42.87.87,0,0,1,.31-.32l.42-.24a.31.31,0,0,1,.3,0,.29.29,0,0,1,.13.28v.25a.82.82,0,0,1-.13.42A.75.75,0,0,1,115.09,367Z" style="fill: rgb(255, 255, 255); transform-origin: 114.877px 366.926px;" id="elw2jop3ttzao" class="animable"/><path d="M117.22,365.93v.12a.76.76,0,0,1-.13.42.85.85,0,0,1-.3.32l-.42.24a.28.28,0,0,1-.3,0,.27.27,0,0,1-.13-.27.23.23,0,0,1,0-.11.3.3,0,0,1,.08-.08.08.08,0,0,1,.07,0,.07.07,0,0,1,0,.07.14.14,0,0,0,.06.13s.09,0,.15,0l.42-.25a.34.34,0,0,0,.15-.16.35.35,0,0,0,.07-.21v-.12a.13.13,0,0,0-.22-.12l-.1.06a.08.08,0,0,1-.08,0,.08.08,0,0,1,0-.07.26.26,0,0,1,0-.11l.08-.08.1-.06a.34.34,0,0,0,.15-.16.35.35,0,0,0,.07-.21v-.12a.13.13,0,0,0-.22-.12l-.42.24a.46.46,0,0,0-.15.16.42.42,0,0,0-.06.21.25.25,0,0,1,0,.11.27.27,0,0,1-.07.08.12.12,0,0,1-.08,0,.09.09,0,0,1,0-.07.76.76,0,0,1,.13-.42.78.78,0,0,1,.3-.32l.42-.25a.28.28,0,0,1,.3,0,.29.29,0,0,1,.13.28v.12a.66.66,0,0,1,0,.23.91.91,0,0,1-.11.22.23.23,0,0,1,.11.11A.34.34,0,0,1,117.22,365.93Z" style="fill: rgb(255, 255, 255); transform-origin: 116.583px 365.905px;" id="elydw57ovpcbq" class="animable"/><path d="M113.46,369h0a.08.08,0,0,1-.06-.1l.25-2.26s0-.08.08-.07a.1.1,0,0,1,.07.1l-.26,2.27S113.49,369,113.46,369Z" style="fill: rgb(255, 255, 255); transform-origin: 113.599px 367.785px;" id="elxv7744bvkt" class="animable"/><path d="M163.15,341.92v7a1.68,1.68,0,0,1-.71,1.35L156.11,354c-.39.23-.71,0-.71-.53v-7a1.72,1.72,0,0,1,.71-1.36c1.73-1,4.61-2.71,6.33-3.71C162.84,341.16,163.15,341.39,163.15,341.92Z" style="fill: rgb(250, 250, 250); transform-origin: 159.275px 347.697px;" id="elqd9l4ap137e" class="animable"/><polygon points="163.15 343.42 163.15 343.85 160.66 345.31 160.34 345.49 158.22 346.73 157.9 346.92 155.41 348.37 155.41 347.94 163.15 343.42" style="fill: #7F68C8; transform-origin: 159.28px 345.895px;" id="el54d7s4w55yg" class="animable"/><polygon points="160.34 348.91 160.66 348.51 160.66 344.97 160.34 345.36 160.34 348.91" style="fill: #7F68C8; transform-origin: 160.5px 346.94px;" id="eloz6zm3mztgr" class="animable"/><polygon points="157.9 350.39 158.22 349.99 158.22 346.44 157.9 346.84 157.9 350.39" style="fill: #7F68C8; transform-origin: 158.06px 348.415px;" id="el149brfg05w8" class="animable"/><polygon points="163.15 346.98 163.15 347.41 155.41 351.93 155.41 351.5 157.9 350.05 158.22 349.86 160.34 348.62 160.66 348.43 163.15 346.98" style="fill: #7F68C8; transform-origin: 159.28px 349.455px;" id="elrq1zdinl89" class="animable"/><path d="M140.89,357.75l-29.55,17.06a2.24,2.24,0,0,0-.93,1.78v.17c0,.69.42,1,.93.7l29.55-17.06a2.21,2.21,0,0,0,.93-1.78v-.17C141.82,357.77,141.41,357.45,140.89,357.75Z" style="fill: rgb(69, 90, 100); transform-origin: 126.115px 367.606px;" id="elpgmz63yw0db" class="animable"/><path d="M112,345.43a.48.48,0,0,1,.06-.18.29.29,0,0,1,.11-.11l.46-.27a.11.11,0,0,1,.12,0s0,.06,0,.12a3.11,3.11,0,0,1-.12.7,3.4,3.4,0,0,1-.31.75,3.45,3.45,0,0,1-.52.72,3.37,3.37,0,0,1-.77.6,1.8,1.8,0,0,1-.7.25.83.83,0,0,1-.52-.14,1.08,1.08,0,0,1-.33-.48,2.79,2.79,0,0,1-.14-.77c0-.19,0-.41,0-.64s0-.45,0-.65a5,5,0,0,1,.14-.94,4.25,4.25,0,0,1,.33-.86,3.8,3.8,0,0,1,.52-.74,2.92,2.92,0,0,1,.7-.56,1.86,1.86,0,0,1,.77-.28.84.84,0,0,1,.52.11.79.79,0,0,1,.31.4,1.69,1.69,0,0,1,.12.55.39.39,0,0,1,0,.18.28.28,0,0,1-.12.12l-.46.27a.11.11,0,0,1-.11,0s-.06-.05-.06-.11a.68.68,0,0,0-.09-.25.37.37,0,0,0-.17-.16.39.39,0,0,0-.27,0,1.11,1.11,0,0,0-.4.15,1.52,1.52,0,0,0-.39.32,2,2,0,0,0-.28.41,2.29,2.29,0,0,0-.17.46,3,3,0,0,0-.07.5c0,.2,0,.42,0,.65s0,.45,0,.64a1.52,1.52,0,0,0,.07.41.6.6,0,0,0,.17.27.39.39,0,0,0,.28.08.78.78,0,0,0,.39-.13,2,2,0,0,0,.39-.3,1.48,1.48,0,0,0,.28-.35,1.51,1.51,0,0,0,.18-.36A1.59,1.59,0,0,0,112,345.43Z" style="fill: rgb(224, 224, 224); transform-origin: 111.05px 344.978px;" id="elcjx8tkzkqu9" class="animable"/><path d="M115.06,340c.47-.28.84-.34,1.09-.19s.37.5.37,1a2.88,2.88,0,0,1-.76,2l.8,1.43a.24.24,0,0,1,0,.08.43.43,0,0,1,0,.14.3.3,0,0,1-.09.1l-.49.28c-.08.05-.13.06-.17,0a.32.32,0,0,1-.08-.09L115,343.4l-.77.45v1.79a.3.3,0,0,1,0,.17.25.25,0,0,1-.1.12l-.47.27a.09.09,0,0,1-.11,0,.17.17,0,0,1,0-.12v-5a.41.41,0,0,1,0-.18.26.26,0,0,1,.11-.12Zm-.87,2.92.84-.48a1.79,1.79,0,0,0,.52-.46,1.16,1.16,0,0,0,.2-.7c0-.28-.07-.44-.2-.46a.82.82,0,0,0-.52.16l-.84.48Z" style="fill: rgb(224, 224, 224); transform-origin: 115.052px 342.974px;" id="eloix68r36e4" class="animable"/><path d="M120,341.56a.08.08,0,0,1,.1,0,.13.13,0,0,1,0,.12v.55a.28.28,0,0,1,0,.17.34.34,0,0,1-.1.13L117.43,344a.11.11,0,0,1-.11,0,.17.17,0,0,1,0-.12v-5a.34.34,0,0,1,0-.17.29.29,0,0,1,.11-.13l2.48-1.43a.08.08,0,0,1,.11,0,.1.1,0,0,1,0,.11v.55a.35.35,0,0,1,0,.17.36.36,0,0,1-.11.13l-1.86,1.07v1.22l1.73-1a.11.11,0,0,1,.11,0,.14.14,0,0,1,0,.12v.54a.41.41,0,0,1,0,.18.26.26,0,0,1-.11.12l-1.73,1v1.28Z" style="fill: rgb(224, 224, 224); transform-origin: 118.712px 340.571px;" id="el4z9rm0wlwcx" class="animable"/><path d="M122.4,335.73a1.86,1.86,0,0,1,.74-.28.77.77,0,0,1,.53.12,1,1,0,0,1,.32.49,2.83,2.83,0,0,1,.13.79,10.84,10.84,0,0,1,0,1.14,4.84,4.84,0,0,1-.13.95,4,4,0,0,1-.32.86,3.29,3.29,0,0,1-.52.72,2.92,2.92,0,0,1-.72.57l-1.45.83a.09.09,0,0,1-.11,0,.17.17,0,0,1-.05-.12v-5a.41.41,0,0,1,.05-.18.32.32,0,0,1,.11-.12Zm.95,1.57a1.19,1.19,0,0,0-.08-.41.6.6,0,0,0-.17-.27.42.42,0,0,0-.3-.07,1,1,0,0,0-.43.16l-.77.44v3.46l.8-.47a1.46,1.46,0,0,0,.41-.32,1.91,1.91,0,0,0,.29-.41,1.88,1.88,0,0,0,.17-.47,2.74,2.74,0,0,0,.08-.5Q123.37,337.86,123.35,337.3Z" style="fill: rgb(224, 224, 224); transform-origin: 122.477px 338.691px;" id="elnc9soje4kms" class="animable"/><path d="M125,339.58a.09.09,0,0,1-.11,0,.16.16,0,0,1,0-.12v-5a.39.39,0,0,1,0-.18.33.33,0,0,1,.11-.12l.47-.27a.11.11,0,0,1,.11,0,.14.14,0,0,1,0,.12v5a.41.41,0,0,1,0,.17.36.36,0,0,1-.11.13Z" style="fill: rgb(224, 224, 224); transform-origin: 125.236px 336.737px;" id="elntlsf8rokw" class="animable"/><path d="M129.24,331.78a.11.11,0,0,1,.11,0s0,.06,0,.12v.55a.38.38,0,0,1,0,.17.26.26,0,0,1-.11.12l-1,.6v4.21a.38.38,0,0,1,0,.17.5.5,0,0,1-.11.13l-.47.26a.09.09,0,0,1-.11,0,.17.17,0,0,1,0-.12v-4.21l-1,.6a.11.11,0,0,1-.11,0,.14.14,0,0,1,0-.12v-.55a.41.41,0,0,1,0-.17.26.26,0,0,1,.11-.12Z" style="fill: rgb(224, 224, 224); transform-origin: 127.893px 334.947px;" id="el7ggro34f9sf" class="animable"/><path d="M133.61,332.93a.47.47,0,0,1,.06-.19.38.38,0,0,1,.11-.11l.47-.27a.11.11,0,0,1,.11,0,.12.12,0,0,1,0,.13,3.87,3.87,0,0,1-.11.7,4.17,4.17,0,0,1-.31.74,3.92,3.92,0,0,1-.53.72,3.06,3.06,0,0,1-.76.6,1.69,1.69,0,0,1-.71.25.73.73,0,0,1-.51-.14.87.87,0,0,1-.33-.48,2.7,2.7,0,0,1-.15-.77c0-.19,0-.4,0-.64s0-.45,0-.65a4.75,4.75,0,0,1,.15-.93,3.26,3.26,0,0,1,.33-.87,3.41,3.41,0,0,1,.51-.73,3,3,0,0,1,.71-.57,2,2,0,0,1,.76-.28.89.89,0,0,1,.53.12.75.75,0,0,1,.3.39,1.69,1.69,0,0,1,.12.56.27.27,0,0,1,0,.17.26.26,0,0,1-.11.12l-.47.27a.11.11,0,0,1-.11,0,.15.15,0,0,1-.06-.12.67.67,0,0,0-.08-.25.35.35,0,0,0-.18-.16.39.39,0,0,0-.27,0,1.39,1.39,0,0,0-.39.15,1.76,1.76,0,0,0-.39.32,1.57,1.57,0,0,0-.28.41,1.76,1.76,0,0,0-.18.47,2.76,2.76,0,0,0-.07.49c0,.2,0,.42,0,.65s0,.45,0,.64a1.67,1.67,0,0,0,.07.42.55.55,0,0,0,.18.26.41.41,0,0,0,.28.09.93.93,0,0,0,.39-.14,1.45,1.45,0,0,0,.39-.3,2.26,2.26,0,0,0,.28-.35,2,2,0,0,0,.17-.36A1.19,1.19,0,0,0,133.61,332.93Z" style="fill: rgb(224, 224, 224); transform-origin: 132.665px 332.47px;" id="elrjcj5mrfwqe" class="animable"/><path d="M136.93,327.34c.06,0,.11,0,.14,0a.26.26,0,0,1,.08.14l1.41,4a.09.09,0,0,1,0,.05.41.41,0,0,1,0,.18.32.32,0,0,1-.11.12l-.41.24c-.08,0-.13,0-.16,0a.18.18,0,0,1-.07-.09l-.25-.71-1.82,1.05-.24,1a.77.77,0,0,1-.07.17.41.41,0,0,1-.16.16l-.41.23a.09.09,0,0,1-.11,0,.17.17,0,0,1-.05-.12.13.13,0,0,1,0-.06l1.41-5.67a1,1,0,0,1,.08-.24.32.32,0,0,1,.14-.14Zm.32,3.19-.64-1.82-.63,2.55Z" style="fill: rgb(224, 224, 224); transform-origin: 136.633px 330.619px;" id="elw6cs7q8k958" class="animable"/><path d="M140.73,325.15c.48-.28.85-.34,1.09-.19s.38.49.38,1a3,3,0,0,1-.19,1.07,2.9,2.9,0,0,1-.57.91l.79,1.44a.1.1,0,0,1,0,.07.28.28,0,0,1,0,.15.22.22,0,0,1-.09.1l-.48.28c-.08,0-.14.05-.18,0a.16.16,0,0,1-.07-.09l-.76-1.35-.76.44v1.79a.41.41,0,0,1-.05.18.32.32,0,0,1-.11.12l-.46.27a.11.11,0,0,1-.11,0,.14.14,0,0,1,0-.12v-5a.37.37,0,0,1,0-.17.28.28,0,0,1,.11-.13Zm-.86,2.91.83-.48a1.83,1.83,0,0,0,.53-.45,1.16,1.16,0,0,0,.19-.7c0-.29-.06-.44-.19-.46a.84.84,0,0,0-.53.15l-.83.48Z" style="fill: rgb(224, 224, 224); transform-origin: 140.693px 328.117px;" id="elt4wyt7gzx9g" class="animable"/><path d="M144.6,322.93a2,2,0,0,1,.74-.28.72.72,0,0,1,.53.13,1,1,0,0,1,.32.48,2.9,2.9,0,0,1,.13.8,10.65,10.65,0,0,1,0,1.13,5,5,0,0,1-.13,1,4.13,4.13,0,0,1-.32.85,3.29,3.29,0,0,1-.52.72,2.92,2.92,0,0,1-.72.57l-1.45.84a.11.11,0,0,1-.11,0,.15.15,0,0,1,0-.12v-5a.34.34,0,0,1,0-.17.36.36,0,0,1,.11-.13Zm1,1.57a1.67,1.67,0,0,0-.07-.41.54.54,0,0,0-.18-.27.42.42,0,0,0-.3-.07,1,1,0,0,0-.43.16l-.77.44v3.46l.8-.46A1.49,1.49,0,0,0,145,327a1.69,1.69,0,0,0,.29-.41,2.19,2.19,0,0,0,.18-.47,2.74,2.74,0,0,0,.07-.5Q145.57,325.06,145.55,324.5Z" style="fill: rgb(224, 224, 224); transform-origin: 144.696px 325.913px;" id="elyoxbk9i5di" class="animable"/></g></g><g id="freepik--Files--inject-112" class="animable" style="transform-origin: 359.29px 136.425px;"><g id="freepik--files--inject-112" class="animable" style="transform-origin: 359.29px 136.425px;"><path d="M363.64,94a.72.72,0,0,0-.28-.64h0l-1.89-1.1h0a.72.72,0,0,0-.71.07l-29.05,16.76-.78-2.61a1,1,0,0,0-.42-.56h0l-1.91-1.1h0a.89.89,0,0,0-.93,0L315.15,112a2.23,2.23,0,0,0-1,1.75V155.4a.72.72,0,0,0,.27.64h0l1.89,1.09h0a.73.73,0,0,0,.7-.09l45.56-26.31a2.24,2.24,0,0,0,1-1.75Z" style="fill: #7F68C8; transform-origin: 338.895px 124.689px;" id="elpj4yft103sq" class="animable"/><g id="el7sgb1bw08dc"><path d="M363.64,94a.72.72,0,0,0-.28-.64h0l-1.89-1.1h0a.72.72,0,0,0-.71.07l-29.05,16.76-.78-2.61a1,1,0,0,0-.42-.56h0l-1.91-1.1h0a.89.89,0,0,0-.93,0L315.15,112a2.23,2.23,0,0,0-1,1.75V155.4a.72.72,0,0,0,.27.64h0l1.89,1.09h0a.73.73,0,0,0,.7-.09l45.56-26.31a2.24,2.24,0,0,0,1-1.75Z" style="opacity: 0.3; transform-origin: 338.895px 124.689px;" class="animable" id="el9e92krn7b6"/></g><path d="M330.45,105.87a.9.9,0,0,0-.91,0l-12.49,7.21a2.09,2.09,0,0,0-.71.76l-1.91-1.11a2.2,2.2,0,0,1,.72-.76l12.48-7.2a.89.89,0,0,1,.93,0h0Z" style="fill: #7F68C8; transform-origin: 322.44px 109.239px;" id="elahprh2oqjc" class="animable"/><path d="M363.29,93.3a.81.81,0,0,0-.67.1l-29.37,16.95a.89.89,0,0,1-1.35-.54l-.23-.75L360.72,92.3a.94.94,0,0,1,.45-.14.53.53,0,0,1,.26.07h0Z" style="fill: #7F68C8; transform-origin: 347.48px 101.327px;" id="ellmum1nh1nj" class="animable"/><g id="el7ps7mg8kcu2"><path d="M316.18,157a.52.52,0,0,0,.1.11l-1.84-1.06h0a.48.48,0,0,1-.17-.2.37.37,0,0,1-.05-.11,1.09,1.09,0,0,1-.05-.33V113.77a.49.49,0,0,1,0-.12,2.17,2.17,0,0,1,0-.25l0-.13c.05-.13.1-.25.16-.37l.06-.13,1.91,1.11a2,2,0,0,0-.3,1V156.5a1.1,1.1,0,0,0,0,.18s0,.06,0,.1l0,.05,0,.06A.27.27,0,0,0,316.18,157Z" style="opacity: 0.35; transform-origin: 315.233px 134.94px;" class="animable" id="eli08juhe352m"/></g><path d="M384,105.76a.7.7,0,0,0-.28-.64h0l-1.89-1.1h0a.74.74,0,0,0-.72.07l-29,16.76-.79-2.61a.91.91,0,0,0-.42-.56h0L349,116.55h0a.91.91,0,0,0-.93,0l-12.49,7.21a2.23,2.23,0,0,0-1,1.76v41.62a.72.72,0,0,0,.28.64h0l0,0h0l1.89,1.09h0a.74.74,0,0,0,.69-.08L383,142.55a2.25,2.25,0,0,0,1-1.76Z" style="fill: #7F68C8; transform-origin: 359.291px 136.439px;" id="el24on7teteqa" class="animable"/><g id="elqs976oidcl"><path d="M384,105.76a.7.7,0,0,0-.28-.64h0l-1.89-1.1h0a.74.74,0,0,0-.72.07l-29,16.76-.79-2.61a.91.91,0,0,0-.42-.56h0L349,116.55h0a.91.91,0,0,0-.93,0l-12.49,7.21a2.23,2.23,0,0,0-1,1.76v41.62a.72.72,0,0,0,.28.64h0l0,0h0l1.89,1.09h0a.74.74,0,0,0,.69-.08L383,142.55a2.25,2.25,0,0,0,1-1.76Z" style="opacity: 0.15; transform-origin: 359.291px 136.439px;" class="animable" id="elk8v123qxo3"/></g><path d="M350.84,117.64a.93.93,0,0,0-.91,0l-12.48,7.21a2.11,2.11,0,0,0-.72.76l-1.9-1.1a2,2,0,0,1,.71-.76L348,116.58a.89.89,0,0,1,.92,0h0Z" style="fill: #7F68C8; transform-origin: 342.835px 121.031px;" id="eluklk234aqwk" class="animable"/><path d="M383.68,105.08a.74.74,0,0,0-.66.09l-29.37,17a.89.89,0,0,1-1.36-.54l-.22-.75,29-16.76a.94.94,0,0,1,.45-.13.6.6,0,0,1,.27.06h0Z" style="fill: #7F68C8; transform-origin: 367.875px 113.154px;" id="elogcmvv2qwbg" class="animable"/><g id="elfptxfnbaegt"><path d="M336.58,168.78a.33.33,0,0,0,.09.11l-1.84-1.06h0l0,0a.53.53,0,0,1-.17-.19.61.61,0,0,1-.05-.12,1,1,0,0,1-.05-.33V125.42c0-.08,0-.17,0-.25s0-.08,0-.13a2.87,2.87,0,0,1,.15-.37.47.47,0,0,1,.07-.12l1.9,1.1a2.06,2.06,0,0,0-.29,1v41.62a1.09,1.09,0,0,0,0,.18.36.36,0,0,0,0,.1s0,0,0,0l0,.07A.26.26,0,0,0,336.58,168.78Z" style="opacity: 0.25; transform-origin: 335.62px 146.72px;" class="animable" id="el6k7c3b0fs62"/></g><path d="M404.43,117.53a.72.72,0,0,0-.28-.64h0l-1.89-1.09h0a.72.72,0,0,0-.71.08L372.46,132.6l-.78-2.61a1,1,0,0,0-.43-.56h0l-1.91-1.1h0a.92.92,0,0,0-.93,0l-12.48,7.2a2.24,2.24,0,0,0-1,1.76v41.62a.71.71,0,0,0,.28.64h0l1.89,1.09h0a.74.74,0,0,0,.69-.08l45.57-26.31a2.24,2.24,0,0,0,1-1.76Z" style="fill: #7F68C8; transform-origin: 379.68px 148.216px;" id="elo4jfngwx9i9" class="animable"/><g id="el3ikplaqwwib"><g style="opacity: 0.5; transform-origin: 379.65px 126.565px;" class="animable" id="elg0io9ys7jgm"><path d="M371.23,129.41a.91.91,0,0,0-.91.05l-12.48,7.2a2.29,2.29,0,0,0-.72.76l-1.9-1.1a2,2,0,0,1,.72-.76l12.48-7.2a.92.92,0,0,1,.93,0h0Z" style="fill: rgb(255, 255, 255); transform-origin: 363.225px 132.827px;" id="el49xcj7kzg2o" class="animable"/><path d="M404.08,116.85a.74.74,0,0,0-.67.1L374,133.89a.88.88,0,0,1-1.35-.54l-.23-.75,29.05-16.75a.84.84,0,0,1,.45-.14.46.46,0,0,1,.26.07h0Z" style="fill: rgb(255, 255, 255); transform-origin: 388.25px 124.875px;" id="elazvcb94woqp" class="animable"/></g></g><g id="el7wj7e4ba5um"><path d="M357,180.56a.47.47,0,0,0,.1.1l-1.84-1.06h0a.7.7,0,0,1-.18-.2l0-.12a.82.82,0,0,1-.05-.33V137.32a.49.49,0,0,1,0-.12,2.34,2.34,0,0,1,0-.26l0-.12a1.8,1.8,0,0,1,.16-.38.67.67,0,0,1,.06-.12l1.9,1.1a2.09,2.09,0,0,0-.29,1v41.63a.91.91,0,0,0,0,.17s0,.07,0,.1l0,.05a.64.64,0,0,0,0,.07A.75.75,0,0,0,357,180.56Z" style="opacity: 0.2; transform-origin: 356.088px 158.49px;" class="animable" id="elhjf3zpyibdf"/></g></g></g><g id="freepik--Character--inject-112" class="animable" style="transform-origin: 319.374px 320.674px;"><g id="freepik--character--inject-112" class="animable" style="transform-origin: 319.374px 320.674px;"><g id="freepik--Laptop--inject-112" class="animable" style="transform-origin: 348.642px 417.075px;"><g id="eldqujyb1wqbi"><path d="M440.28,405.7,374.8,367.9a3.79,3.79,0,0,0-3.77,0L257.42,433.5a.84.84,0,0,0,0,1.45l66.1,38.17Z" style="opacity: 0.2; transform-origin: 348.642px 420.259px;" class="animable" id="elujm0p6lg1u"/></g><path d="M435.39,397.74v1.47a6.06,6.06,0,0,1-3,5.24l-105.83,61.1a6.09,6.09,0,0,1-6.05,0l-57.68-33.19a6.07,6.07,0,0,1-3-5.24v-2h0v0a.75.75,0,0,0,.38.66l63.35,36.59Z" style="fill: rgb(55, 71, 79); transform-origin: 347.61px 432.047px;" id="el979jfstg50u" class="animable"/><path d="M435.4,397.73l-62.75-36.22a3.62,3.62,0,0,0-3.6,0L260.19,424.37a.81.81,0,0,0,0,1.39l63.34,36.57Z" style="fill: rgb(69, 90, 100); transform-origin: 347.598px 411.68px;" id="elgh1ujp3ucsq" class="animable"/><polygon points="322.95 457 289.1 437.48 388.06 380.35 422.08 399.76 322.95 457" style="fill: rgb(55, 71, 79); transform-origin: 355.59px 418.675px;" id="elk6nnpzwbe8c" class="animable"/><polygon points="422.08 399.76 420.79 400.51 388.06 381.83 290.4 438.23 289.11 437.49 388.06 380.35 422.08 399.76" style="fill: rgb(38, 50, 56); transform-origin: 355.595px 409.29px;" id="elfl66lhaikol" class="animable"/><polygon points="306.31 402.7 306.94 403.06 323.52 412.64 353.02 395.61 353.64 395.26 336.43 385.32 306.31 402.7" style="fill: rgb(38, 50, 56); transform-origin: 329.975px 398.98px;" id="eldgesfcg2lc" class="animable"/><polygon points="306.94 403.06 323.52 412.64 353.02 395.61 336.43 386.03 306.94 403.06" style="fill: rgb(55, 71, 79); transform-origin: 329.98px 399.335px;" id="el9bg17x6l4gk" class="animable"/><path d="M323.53,462.33v4a6,6,0,0,0,3-.8l105.83-61.1a6.06,6.06,0,0,0,3-5.24v-1.48Z" style="fill: rgb(38, 50, 56); transform-origin: 379.445px 432.02px;" id="ellzegik7x6tj" class="animable"/></g><g id="freepik--Light--inject-112"><path d="M449.76,321.85,323.53,462.33,205.32,394l-.08,0-1.15-.67c-9.51-6-11.66-20-14.44-43C187,328.16,186.6,303,187,286.17s5.19-30.69,18.39-36.76v0c7-6.43,15.9-10.11,24-13.32,3.22-7.33,3.1-15.65,2.38-26.36-.85-12.3,2-28.79,19.56-37.73s36.71-.39,46.72,9.08L448.3,320.73Z" style="fill: #7F68C8; opacity: 0.1; transform-origin: 318.314px 315.279px;" class="animable" id="eldtmok0mste"/></g><g id="freepik--character--inject-112" class="animable" style="transform-origin: 282.56px 293.553px;"><g id="freepik--Arm--inject-112" class="animable" style="transform-origin: 322.371px 310.862px;"><path d="M288,223.32c10.88.17,16.28,9.18,18.55,25.48,3.12,22.44,6.48,57,6.48,57s20.17,17.35,24.16,21.36,5.67,6.27,4.3,8.92L329.1,352.87c-3.43,1.79-7.49-.8-15.12-5.57s-18.86-13-23.29-16.41c-5.23-4-8.25-6.19-10.69-19s-13.51-67.29-13.51-67.29Z" style="fill: rgb(55, 71, 79); transform-origin: 304.225px 288.38px;" id="eljaqrledakbe" class="animable"/><path d="M354.6,346.85c3.14,2.48,7.19,6.62,9.11,8.24s7.51,10.31,10.45,14.87,5,10.17,3.69,14.14-6.34,8.08-7.37,10.52-1.85,5.39-4.09,2.74-3.42-7.51-5.63-11.2-4.84-8.91-7.95-10.75-3.65.08-3.6,1.21c.06,1.32,2,4.75,2,7.88s-1.78,6.23-3.17,6.08-1.88-2.51-3.56-6.3c-1.82-4.1-2.83-4-3.27-8.73s.12-5.95-.37-10.33a17.92,17.92,0,0,0-.82-3.75,6.07,6.07,0,0,0-2.28-3l-1.54-1.07c1.61-7.57,6.21-12.38,14.12-14.14C350.29,343.24,351.45,344.38,354.6,346.85Z" style="fill: rgb(255, 189, 167); transform-origin: 357.226px 370.831px;" id="eldkmtxxnn7t5" class="animable"/><path d="M357.29,380.35h0l0-.06Z" style="fill: #7F68C8; transform-origin: 357.29px 380.32px;" id="elecy35bzhllo" class="animable"/><path d="M340.88,335.57s-4.43-.89-9.86,6-3.16,10.47-3.16,10.47L338,358.68c2.46-7.1,8.89-13.6,14.43-13.69Z" style="fill: rgb(38, 50, 56); transform-origin: 339.91px 347.106px;" id="elzzipw5nt2a" class="animable"/><path d="M363.71,355.09c-1.92-1.62-6-5.76-9.11-8.24-.94-.73-1.69-1.34-2.29-1.85h.13l-10.92-8.9c1.37-2.65-.3-4.91-4.3-8.92s-24.16-21.36-24.16-21.36-3.36-34.57-6.48-57c-2.24-16.05-7.51-25-18.06-25.45h0c7.36.76,13.72,9.62,15.72,21.27s5.87,59.71,5.87,59.71a14.73,14.73,0,0,0-6.72,4.85s5.84-3.53,7.75-2.43,16.85,14.76,21,17.8,10,7.8,7.8,11.57c0,0-.75,1.2,4.28,4.85s13.55,10.92,19.2,15.73a41.37,41.37,0,0,1,6.59,6.86C367.49,359.8,364.89,356.09,363.71,355.09Z" style="fill: #7F68C8; transform-origin: 329.265px 293.475px;" id="elk7q4ydytad" class="animable"/></g><path id="freepik--Chest--inject-112" d="M229.39,236.11c-9.88,3.91-21,8.49-28.22,17.87s-8.4,78,.13,103.2,30.1,39.22,30.1,39.22l71.34-41.19s-10-19.75-9.05-35.36,5.93-94.5-8.15-96C265.28,223.24,229.39,236.11,229.39,236.11Z" style="fill: rgb(69, 90, 100); transform-origin: 249.031px 310.114px;" class="animable"/><path d="M293.69,319.85c.54-8.83,2.37-38,1.3-61.83,0,0-.71,25.9-1.71,38.42s-3.7,25.53.86,42.34a78,78,0,0,0,6.73,17.52l1.87-1.09S292.73,335.46,293.69,319.85Z" style="fill: #7F68C8; transform-origin: 297.156px 307.16px;" id="el91zw5n81a6u" class="animable"/><g id="freepik--Skull--inject-112" class="animable" style="transform-origin: 269.292px 320.195px;"><path d="M257.44,340.25a1.16,1.16,0,0,1,.86-.37,3.14,3.14,0,0,0,2.46-1.19l16.21-17a4.73,4.73,0,0,0,1.11-2.57v0c.18-1.27.94-2.43,1.7-2.58.45-.08.78.35.86,1s.32.85.69.8.73.19.84.74a4,4,0,0,1-1,3.22,1.24,1.24,0,0,1-.77.39,4.73,4.73,0,0,0-3.14,1.64l-15.48,16.25a5.55,5.55,0,0,0-1.3,3v0c-.18,1.28-1,2.43-1.71,2.55-.45.08-.77-.37-.84-1.05s-.32-.87-.68-.82-.73-.22-.83-.77A4,4,0,0,1,257.44,340.25Z" style="fill: rgb(250, 250, 250); transform-origin: 269.295px 331.335px;" id="el5c88ywr64kt" class="animable"/><path d="M277.15,307.31c0-1.56.91-2.72.91-7.31s-2.9-7.71-8.77-4.32-8.77,9.86-8.77,14.44.92,4.7.92,6.26-1.68,3.44-.92,5.21,2.15-.08,3,.19,1,1.09,1,3.11c0,0,1.29,1.36,4.78-.65a12.13,12.13,0,0,0,4.79-4.88,6.82,6.82,0,0,1,1-4.31c.8-1.2,2.2-.95,2.95-3.58S277.15,308.87,277.15,307.31Zm-13.09,10.5c-1.35-.1-1.47-2.39-1.09-3.66a4.68,4.68,0,0,1,2.59-2.9c1.81-.74,2.77.87,2.13,2.91S265,317.88,264.06,317.81Zm6.66.84c-.39.53-1.15.62-1.43-.43-.28,1.37-1,2.16-1.42,2.07s-.3-.77.05-2,1.37-3.56,1.37-3.56a10,10,0,0,1,1.38,2C271,317.54,271.1,318.11,270.72,318.65Zm3.81-6.88c-.94,1.15-3,1.83-3.63.53s.32-4,2.13-5.36c1.16-.87,2.22-.95,2.58-.09A5.76,5.76,0,0,1,274.53,311.77Z" style="fill: rgb(250, 250, 250); transform-origin: 269.28px 309.795px;" id="elnceoef8noi" class="animable"/><path d="M281.14,326.57a1.6,1.6,0,0,0-.85.62,4,4,0,0,1-2.47,1.65l-16.21,1.7c-.57.06-1-.46-1.11-1.29h0c-.17-1.08-.94-1.36-1.69-.63a3.44,3.44,0,0,0-.86,2,2.8,2.8,0,0,1-.7,1.6,3.1,3.1,0,0,0-.83,1.71c-.21,1.27.24,2.2,1,2.05a1.46,1.46,0,0,0,.77-.5,5.42,5.42,0,0,1,3.15-2l15.48-1.63c.66-.07,1.13.52,1.29,1.47v0c.19,1.06,1,1.32,1.71.57a3.49,3.49,0,0,0,.85-2,2.72,2.72,0,0,1,.68-1.6,3.23,3.23,0,0,0,.82-1.72C282.37,327.31,281.9,326.4,281.14,326.57Z" style="fill: rgb(250, 250, 250); transform-origin: 269.292px 331.273px;" id="elpw1ffom7s7f" class="animable"/></g><g id="freepik--Head--inject-112" class="animable" style="transform-origin: 270.925px 223.853px;"><path d="M239,264.67c13.85,12.94,31.55,14.78,31.55,14.78s-5.87-1.93-17.25-12.74c-8-7.6-11.82-24.63-13.38-34l-1.38.43-.46.14-1,.32-.38.12-1.21.39-.37.13-.85.27-.33.11-1,.32-.32.11-.62.21-.27.09-.7.24-.19.06-.44.15-.18.07-.4.14-.08,0-.21.08-.05,0-.08,0c-1.08.42-2.18.86-3.28,1.31C226.51,242.66,228.62,255,239,264.67Z" style="fill: rgb(55, 71, 79); transform-origin: 248.335px 256.08px;" id="el4yrby15tsv9" class="animable"/><path d="M270.52,279.45c4.6-4,5.66-16.72,11.85-22.33s25.72-20.5,30.56-31-9.23-12.79-25-7.08-28.89,17.15-30.44,30.6S259.89,275.4,270.52,279.45Z" style="fill: rgb(55, 71, 79); transform-origin: 285.532px 247.721px;" id="elpr7x6fgn05" class="animable"/><path d="M301.24,218.05c-.7,9.36-1.93,19.13-5.54,27.87-2.72,6.62-5.73,12.15-12.13,11.37a37.46,37.46,0,0,1-10.69-3.05l-2.28,9.59a72.1,72.1,0,0,1-.08,15.62c-5.27-2-11.35-6.59-16.39-11.68-.59-.6-1.18-1.2-1.74-1.81-3.64-3.95-6.55-8-7.9-11.27,0,0,.4-5.39.75-10.31.28-3.74.93-7.36.13-11-.87-4.1-2.47-8-3.37-12.07-.19-.89-.35-1.79-.48-2.69a34.88,34.88,0,0,1,8.41-28c7.43-8.1,19.3-12,30-9.41a28.91,28.91,0,0,1,15,9.48c3.07,3.63,4.5,7.55,5.95,12C302.48,207.43,301.62,213,301.24,218.05Z" style="fill: rgb(255, 189, 167); transform-origin: 271.495px 229.933px;" id="elh3zpecg4yup" class="animable"/><path d="M298.27,233.94c-1.69,10.69-6.44,19.25-10.15,21.67s-11.41.11-11.76,0a36.12,36.12,0,0,0,7.21,1.68c6.4.78,9.41-4.75,12.13-11.37,3.61-8.74,4.84-18.51,5.54-27.87.28-3.77.84-7.84.44-11.68L298.27,205C299.8,216.63,300,223.25,298.27,233.94Z" style="fill: #7F68C8; transform-origin: 289.09px 231.182px;" id="el8o2a53w4p3e" class="animable"/><path d="M270.6,263.83a72.1,72.1,0,0,1-.08,15.62c-5.27-2-11.35-6.59-16.39-11.68-.59-.6-1.18-1.2-1.74-1.81-3.64-3.95-6.55-8-7.9-11.27,0,0,.4-5.39.75-10.31.28-3.74.93-7.36.13-11-.05-.26-.12-.52-.17-.77l1.51-3.61a47.81,47.81,0,0,0,10,15.53c3.94,4.26,8.73,6.29,16.18,9.75Z" style="fill: rgb(240, 153, 122); transform-origin: 258.69px 254.225px;" id="elsko2jno035c" class="animable"/><polygon points="291.89 226.57 291.16 227.77 289.85 244.63 298.99 243.15 291.89 226.57" style="fill: rgb(240, 153, 122); transform-origin: 294.42px 235.6px;" id="elwmjuwdk7fx" class="animable"/><path d="M293,229.18a1.19,1.19,0,0,0-2,.43c-.59,2.11-1,4.84-2.86,6.48-2.56,2.26-8.71,2.63-11.52,1.25-2.64-1.29-3-7.39-4.57-8.34-.64-.4-1.21-.27-1.45-1.73-.21-1.29.43-1.54,1.37-1.71,3.34-.61,14.57-2.53,16.51-2.21s1.42,3.09,3.25,3c1.61-.09,1.31-3,3.18-3.59a58.74,58.74,0,0,1,10.22-1.11c.87,0,1.52.09,1.41,1.36-.14,1.71-.88,1.71-1.34,1.83-.72.17-1.26,5.89-2.27,7.5-1.56,2.5-7.71,2.11-7.71,2.11Z" style="fill: rgb(38, 50, 56); transform-origin: 288.556px 229.899px;" id="elxv8xdhwqqln" class="animable"/><path d="M282.27,236.59c2.47-.25,4.4-.93,5.26-2.08,1.28-1.72,1.89-5.72,1.71-7.2s-.57-2.83-4.74-2.44-7.12.55-8.71,1.7-1.07,3.46-.55,5.18a9.12,9.12,0,0,0,2.16,4.09C278.17,236.47,280.15,236.8,282.27,236.59Z" style="fill: #7F68C8; transform-origin: 281.997px 230.728px;" id="elu439t3buv8" class="animable"/><g id="elrubqpez9u2"><path d="M282.27,236.59c2.47-.25,4.4-.93,5.26-2.08,1.28-1.72,1.89-5.72,1.71-7.2s-.57-2.83-4.74-2.44-7.12.55-8.71,1.7-1.07,3.46-.55,5.18a9.12,9.12,0,0,0,2.16,4.09C278.17,236.47,280.15,236.8,282.27,236.59Z" style="fill: rgb(255, 255, 255); opacity: 0.75; transform-origin: 281.997px 230.728px;" class="animable" id="ellkoqj5ta6hm"/></g><path d="M295.88,224.11c-1.46.55-2.07,2.64-1.65,4.72s1.15,3.78,1.67,4.08c.92.54,4.29,0,5.35-.79s2-5.46,2.12-6.66c.13-1.47-.31-2.18-2-2.08A27.55,27.55,0,0,0,295.88,224.11Z" style="fill: #7F68C8; transform-origin: 298.746px 228.249px;" id="el3b57re0z05v" class="animable"/><g id="elk2b3agv0d7f"><path d="M295.88,224.11c-1.46.55-2.07,2.64-1.65,4.72s1.15,3.78,1.67,4.08c.92.54,4.29,0,5.35-.79s2-5.46,2.12-6.66c.13-1.47-.31-2.18-2-2.08A27.55,27.55,0,0,0,295.88,224.11Z" style="fill: rgb(255, 255, 255); opacity: 0.75; transform-origin: 298.746px 228.249px;" class="animable" id="eln8ch6nu684"/></g><path d="M270.52,279.45c-8-3.09-14.47-21.68-7.67-35.79s28-22.88,40.74-23.24,9.87,4.38,9.34,5.72c0,0,4.33-3.86,2.22-15.11-2.16-11.52-7.09-20.45-17.1-29.93s-29.21-18-46.72-9.08-20.4,25.43-19.57,37.73c.73,10.7.85,19-2.37,26.36,0,0-2,9.58,8.9,23.34S270.52,279.45,270.52,279.45Z" style="fill: rgb(69, 90, 100); transform-origin: 272.488px 223.853px;" id="eltyv0nu97r4" class="animable"/></g><g id="freepik--arm--inject-112" class="animable" style="transform-origin: 268.581px 334.135px;"><path d="M290,414.33s1.34.53,3.5,1.14c1.94.55,3.74,1.88,8.34,2.88,5.45,1.19,12.09-1.3,16.69-1.92s12.6.86,17,2.24,9-5.64,11.24-5,5.79-1.43,1.53-5-10.41-8.52-15.33-9.71a28.82,28.82,0,0,0-3.46-.77,37.25,37.25,0,0,0,4.85-1.39,8.71,8.71,0,0,0,3.93-3.1c.37-.57.6-1.45,0-1.84a1.15,1.15,0,0,0-.7-.2c-3.6.19-6.48-.13-9.76-.1a13.71,13.71,0,0,0-4.39.45c-1.31.47-2.61,1-3.89,1.53-5.79,2.56-11.07,4.86-19.67,3.59l-3.58-.94c-1.11,0-3.4,2.56-5,7.11C288.75,410.44,290,414.33,290,414.33Z" style="fill: rgb(255, 189, 167); transform-origin: 319.994px 405.193px;" id="elf4dnklsioxq" class="animable"/><path d="M327.28,395.8a16.23,16.23,0,0,0-6.45,1.59,36.15,36.15,0,0,1,8.65.78,37.25,37.25,0,0,0,4.85-1.39,9.08,9.08,0,0,0,3.58-2.62C335.47,396.13,330.41,395.85,327.28,395.8Z" style="fill: rgb(240, 153, 122); transform-origin: 329.37px 396.165px;" id="elg5wlci1xfem" class="animable"/><path d="M205.36,249.42c6.1.68,16,16,18.14,32.71,2.23,17.54,5.74,77.77,5.74,77.77s25.36,12.41,35.62,17.17,18.8,8.8,19.17,13.2c-6.09,2.77-8.2,11.8-8.37,20.65-3.87,5.43-50.53-9.32-68.21-16-12.42-4.7-14.72-19.2-17.8-44.64C187,328.16,186.6,303,187,286.18S192.16,255.48,205.36,249.42Z" style="fill: rgb(55, 71, 79); transform-origin: 235.449px 330.758px;" id="elug41rqaj1as" class="animable"/><path d="M284,390.27l13.4,6.2c-4.67,1.31-7.73,10.94-6.64,18.25l-15.13-3.8s-3.16-4.22.95-12.71S284,390.27,284,390.27Z" style="fill: rgb(38, 50, 56); transform-origin: 285.935px 402.491px;" id="el2fls015ljgj" class="animable"/><path d="M315.73,395.15h0a28.53,28.53,0,0,1-15.86,2l-2.45-.64-13.4-6.2c-.37-4.4-8.92-8.43-19.17-13.2s-35.62-17.17-35.62-17.17-3.51-60.23-5.74-77.77a60.93,60.93,0,0,0-4.36-15.91l-3-6.13c4,8.67,6.08,30.17,8.55,62s2.08,38.27,2.08,38.27a58.77,58.77,0,0,0-7.93,8.24s8.39-6.83,12.59-5.25,36,16.14,41.85,19.47,8.51,5.9,8,8.77c0,0,11.52,4.57,15,6.18S307.76,400.14,315.73,395.15Z" style="fill: #7F68C8; transform-origin: 265.93px 329.57px;" id="elsh411dcub5" class="animable"/></g></g><path d="M325.23,463.22a7.19,7.19,0,0,0,6.18-.48l105.36-60.83a7.17,7.17,0,0,0,3.48-5l11.54-68.78a7.91,7.91,0,0,0,.09-1.18,7.13,7.13,0,0,0-2.12-5.09L340.27,385.07a5.31,5.31,0,0,0-2.57,3.71Z" style="fill: rgb(55, 71, 79); transform-origin: 388.555px 392.781px;" id="eluqiuu6qf2fs" class="animable"/><path d="M323.53,462.33l.72.41a8.09,8.09,0,0,0,1,.48l12.47-74.44a5.31,5.31,0,0,1,2.57-3.71l109.49-63.22a7.07,7.07,0,0,0-1.46-1.12L338.59,384.07a5.34,5.34,0,0,0-2.58,3.71Z" style="fill: rgb(69, 90, 100); transform-origin: 386.655px 391.975px;" id="elkt0pkdbbuj8" class="animable"/><path d="M338.72,386.47l-1.7-1a5.32,5.32,0,0,0-1,2.29l-12.48,74.55.72.41a8.09,8.09,0,0,0,1,.48l12.47-74.44A5.3,5.3,0,0,1,338.72,386.47Z" style="fill: rgb(38, 50, 56); transform-origin: 331.13px 424.335px;" id="elmw7pk7ishw" class="animable"/><path d="M384.57,399.72c.77-4.6,5.53-10.72,10.62-13.66s8.6-1.59,7.82,3-5.52,10.73-10.62,13.67S383.8,404.33,384.57,399.72Z" style="fill: #7F68C8; transform-origin: 393.791px 394.396px;" id="elfp69szg2p4d" class="animable"/></g></g><g id="freepik--speech-bubble--inject-112" class="animable" style="transform-origin: 262.3px 108.286px;"><g id="freepik--virus-box--inject-112" class="animable" style="transform-origin: 262.3px 108.286px;"><g id="freepik--speech-bubble--inject-112" class="animable" style="transform-origin: 262.3px 108.286px;"><path d="M267.81,126.46l-11,6.37L260.67,144a1.47,1.47,0,0,0,.69.81c.77.42,4.15,2.4,4.43,2.56a1.47,1.47,0,0,0,2.19-.91l4.31-17.45Z" style="fill: #7F68C8; transform-origin: 264.55px 137.021px;" id="elaiep1atqsqo" class="animable"/><g id="elicecnjxig5"><path d="M267.81,126.46l-11,6.37L260.67,144a1.47,1.47,0,0,0,.69.81c.77.42,4.15,2.4,4.43,2.56a1.47,1.47,0,0,0,2.19-.91l4.31-17.45Z" style="opacity: 0.2; transform-origin: 264.55px 137.021px;" class="animable" id="elp02qks8xvo"/></g><path d="M261.27,135.42l3.89,11.21a1.47,1.47,0,0,0,2.82-.13l4.31-17.45Z" style="fill: #7F68C8; transform-origin: 266.78px 138.337px;" id="elwx4bcmchuic" class="animable"/><path d="M288.46,75.45A6.94,6.94,0,0,0,285.32,70l-.53-.31a7,7,0,0,0-6.28,0L239.28,92.39a7,7,0,0,0-3.14,5.44V141a7,7,0,0,0,3.14,5.44l.52.3a7,7,0,0,0,6.29,0l39.23-22.69a7,7,0,0,0,3.14-5.44Z" style="fill: #7F68C8; transform-origin: 262.3px 108.216px;" id="el47fmuiible5" class="animable"/><g id="eltedn3cepg2"><path d="M236.14,141a7,7,0,0,0,3.14,5.44l.52.3a7.06,7.06,0,0,0,5.75.25c-1.48.56-2.6-.27-2.6-2.06V101.76a6.37,6.37,0,0,1,.92-3.09l-6.81-3.93a6.37,6.37,0,0,0-.92,3.09Z" style="opacity: 0.2; transform-origin: 240.845px 121.112px;" class="animable" id="elnna0oo1ac5h"/></g><g id="el1m9wxsiryo3"><path d="M288.35,74.55c-.36-1.33-1.58-1.75-3-.92l-39.23,22.7a6.12,6.12,0,0,0-2.22,2.34l-6.8-3.93a6.4,6.4,0,0,1,2.21-2.34L278.5,69.71a7,7,0,0,1,6.29,0l.53.3A6.91,6.91,0,0,1,288.35,74.55Z" style="fill: rgb(255, 255, 255); opacity: 0.5; transform-origin: 262.725px 83.8169px;" class="animable" id="eljw6xe7sgvl"/></g></g><path id="freepik--Virus--inject-112" d="M281.23,106.8l-1-7.5a.91.91,0,0,0-.56-.72.8.8,0,0,0-.34-.07.9.9,0,0,0-.57.2l-2.53,2a11.58,11.58,0,0,0-1.52-3.58v0h0v0L276,94l1.69-5.89a.89.89,0,0,0-.62-1.11.73.73,0,0,0-.25,0,.91.91,0,0,0-.87.66l-1.64,5.72-.95,2.27a6.61,6.61,0,0,0-.86-.64,5.71,5.71,0,0,0-2.88-.86l1.53-5.93a.91.91,0,0,0-.65-1.1.85.85,0,0,0-.23,0,.9.9,0,0,0-.87.68l-1.7,6.6a8.87,8.87,0,0,0-2,.8,10.69,10.69,0,0,0-1.41.89l-1.09-4a.91.91,0,0,0-.87-.67,1,1,0,0,0-.24,0,.91.91,0,0,0-.64,1.11l1.31,4.84a23.84,23.84,0,0,0-4.63,6.36l-.41-.64-1.29-4a.91.91,0,0,0-.86-.63.87.87,0,0,0-.28,0,.91.91,0,0,0-.59,1.13l1.37,4.26,1.14,1.76a33.29,33.29,0,0,0-2.1,6.61.64.64,0,0,0-.2,0l-3.15.83a.9.9,0,0,0-.66.72l-1.52,8.86a.92.92,0,0,0,.74,1h.16a.91.91,0,0,0,.89-.75l1.42-8.29,2-.52a30.28,30.28,0,0,0-.3,3.28,19.93,19.93,0,0,0,.81,6.61.87.87,0,0,0-.23.2l-2.92,3.53a.9.9,0,0,0-.12,1l2.82,5.7a.9.9,0,0,0,.81.51,1,1,0,0,0,.4-.1.9.9,0,0,0,.41-1.21l-2.56-5.18,2.15-2.59a7.64,7.64,0,0,0,2.69,3,5.77,5.77,0,0,0,3.09.87,7.91,7.91,0,0,0,3.73-1l.05,0h0c4-2.17,7.55-7.44,9.53-13.43l.11,0,2.21.36-2.9,7.94a.9.9,0,0,0,.54,1.16.81.81,0,0,0,.31.06.91.91,0,0,0,.85-.6l3.27-9a.9.9,0,0,0-.7-1.2l-3.17-.52a30.67,30.67,0,0,0,1.09-7.06,23.85,23.85,0,0,0-.17-3.75l.08,0,1.95-1.57.79,5.91a.91.91,0,0,0,.9.79h.12a.91.91,0,0,0,.6-.35A.94.94,0,0,0,281.23,106.8Zm-14.67-10a6.09,6.09,0,0,1,2.88-.81,4,4,0,0,1,2.13.6,5,5,0,0,1,1,.85c-1.85,3.67-4.49,6.66-7.19,8.11a6.93,6.93,0,0,1-3.23.92,4,4,0,0,1-2.81-1.09C261.22,101.44,263.79,98.25,266.56,96.76ZM256.3,117.49a30.92,30.92,0,0,1,2.28-10.41,5.74,5.74,0,0,0,3.54,1.18,7.87,7.87,0,0,0,2.74-.53l-.61,19.68a5.82,5.82,0,0,1-2.41.58,4,4,0,0,1-2.14-.6C257.43,126,256.15,122.28,256.3,117.49Zm9.8,8.79.6-19.43a20.3,20.3,0,0,0,7-7.73,16.07,16.07,0,0,1,1.25,7.33C274.73,114.24,270.87,122.67,266.1,126.28Z" style="fill: rgb(55, 71, 79); transform-origin: 265.403px 110.945px;" class="animable"/></g></g><g id="freepik--Padlock--inject-112" class="animable" style="transform-origin: 416.915px 247.594px;"><g id="freepik--padlock--inject-112" class="animable" style="transform-origin: 416.915px 247.594px;"><g id="freepik--padlock--inject-112" class="animable" style="transform-origin: 406.745px 247.594px;"><path d="M433.25,227.81a3.39,3.39,0,0,0-2-2.78l-3.91-2.26a10.65,10.65,0,0,0-9.62,0l-35.48,20.49a3.39,3.39,0,0,0-2,2.78h0v46.87h0a3.37,3.37,0,0,0,2,2.78l3.91,2.26a10.59,10.59,0,0,0,9.62,0l35.48-20.49a3.38,3.38,0,0,0,2-2.77h0V227.81Z" style="fill: #7F68C8; transform-origin: 406.745px 260.364px;" id="eleusov8sysv" class="animable"/><g id="elf6jy4d9h00m"><path d="M386.15,251.07l-3.91-2.26c-2.66-1.53-2.66-4,0-5.55l35.48-20.49a10.65,10.65,0,0,1,9.62,0l3.91,2.26c2.66,1.53,2.66,4,0,5.55l-35.48,20.49A10.65,10.65,0,0,1,386.15,251.07Z" style="fill: rgb(255, 255, 255); opacity: 0.4; transform-origin: 406.745px 236.92px;" class="animable" id="elv4obx12389o"/></g><path d="M423.66,230c0,1.22-1.72,2.2-3.84,2.2s-3.83-1-3.83-2.2,1.71-2.21,3.83-2.21S423.66,228.81,423.66,230Z" style="fill: #7F68C8; transform-origin: 419.825px 229.995px;" id="el2fnk53t6k3b" class="animable"/><g id="elbxif2d1lgxi"><path d="M423.66,230c0,1.22-1.72,2.2-3.84,2.2s-3.83-1-3.83-2.2,1.71-2.21,3.83-2.21S423.66,228.81,423.66,230Z" style="opacity: 0.2; transform-origin: 419.825px 229.995px;" class="animable" id="ela12eee1foj5"/></g><g id="el6wyhstarbhw"><path d="M419.82,230a4.83,4.83,0,0,0-3.3,1.1,5.52,5.52,0,0,0,6.61,0A4.86,4.86,0,0,0,419.82,230Z" style="opacity: 0.2; transform-origin: 419.825px 231.097px;" class="animable" id="el0qf8jq804s9"/></g><g id="elro0atiatnj"><path d="M391,252.22V299.1a9.91,9.91,0,0,1-4.81-1.14l-3.92-2.27a3.33,3.33,0,0,1-2-2.79V246a3.35,3.35,0,0,0,2,2.78l3.92,2.27A9.91,9.91,0,0,0,391,252.22Z" style="opacity: 0.2; transform-origin: 385.635px 272.551px;" class="animable" id="elxkov41mw6c"/></g><path d="M416.79,275.41l-8.48,4.89L409.63,265c-1-.53-1.66-1.85-1.66-3.74,0-3.38,2-7.3,4.58-8.76s4.58.09,4.58,3.46a11.55,11.55,0,0,1-1.65,5.67Z" style="fill: rgb(55, 71, 79); transform-origin: 412.55px 266.125px;" id="el9kehk7s4k74" class="animable"/><polygon points="416.79 275.41 409.11 270.98 408.31 280.3 416.79 275.41" style="fill: rgb(38, 50, 56); transform-origin: 412.55px 275.64px;" id="elxrs20z7nfs" class="animable"/><path d="M412.55,252.47a10,10,0,0,0-3.91,5.18l6.84,4h0a11.55,11.55,0,0,0,1.65-5.67C417.13,252.56,415.08,251,412.55,252.47Z" style="fill: rgb(38, 50, 56); transform-origin: 412.885px 256.784px;" id="elgwy7vofxo8n" class="animable"/><path d="M423.66,211.62h0V210.1c0-5.8-2.2-10.4-6-12.61-3.65-2.1-8.17-1.82-12.72.81-8.35,4.82-14.9,16.59-14.9,26.79v18.85h0a1.9,1.9,0,0,0,1.13,1.56,6,6,0,0,0,5.42,0,1.88,1.88,0,0,0,1.12-1.56h0V225.09c0-7.46,5.07-16.69,11.07-20.15,2-1.18,3.88-1.47,5-.8,1.37.79,2.19,3,2.19,6v1.51h0a1.89,1.89,0,0,0,1.12,1.55,6,6,0,0,0,5.42,0,1.93,1.93,0,0,0,1.13-1.55Z" style="fill: rgb(224, 224, 224); transform-origin: 406.85px 221.122px;" id="elzjfyoubyals" class="animable"/><g id="eldo8jao8njzb"><path d="M408.43,196.74c-.41.19-.81.41-1.22.64-8.35,4.83-14.9,16.61-14.9,26.81V243a1.88,1.88,0,0,0,1.13,1.57,5.82,5.82,0,0,0,3.58.59,2.25,2.25,0,0,1-.45.32,6,6,0,0,1-5.42,0,1.88,1.88,0,0,1-1.13-1.56V225.09c0-10.2,6.55-22,14.9-26.8A17.28,17.28,0,0,1,408.43,196.74Z" style="opacity: 0.1; transform-origin: 399.225px 221.433px;" class="animable" id="elb99lqivsdkk"/></g><path d="M423.66,210.11v1.51a1.68,1.68,0,0,1-.67,1.23,2.87,2.87,0,0,1-.46.33,6.08,6.08,0,0,1-5.42,0,1.94,1.94,0,0,1-1.13-1.56v-1.51c0-3-.81-5.19-2.17-6a3.48,3.48,0,0,0-2.57-.2,8.69,8.69,0,0,0-2.48,1c-6,3.46-11.07,12.69-11.07,20.14v18.85a1.63,1.63,0,0,1-.67,1.24,5.82,5.82,0,0,1-3.58-.59,1.88,1.88,0,0,1-1.13-1.57V224.19c0-10.2,6.55-22,14.9-26.81.41-.23.81-.45,1.22-.64a11.19,11.19,0,0,1,9.21.75C421.46,199.7,423.66,204.3,423.66,210.11Z" style="fill: rgb(224, 224, 224); transform-origin: 407.985px 220.646px;" id="elt3vl6dz5zs" class="animable"/></g><g id="freepik--Bolt--inject-112" class="animable" style="transform-origin: 441.165px 211.61px;"><polygon points="450.87 207.37 441.87 211.63 442.63 214.73 437.02 215.03 437.99 217.67 431.45 219.31 442.21 220.47 441.04 217.56 448.21 218.27 447 214 453.59 214.27 450.87 207.37" style="fill: #7F68C8; transform-origin: 442.52px 213.92px;" id="elxg82i0csy4e" class="animable"/><g id="elekbkj0n0aqu"><polygon points="450.87 207.37 441.87 211.63 442.63 214.73 437.02 215.03 437.99 217.67 431.45 219.31 442.21 220.47 441.04 217.56 448.21 218.27 447 214 453.59 214.27 450.87 207.37" style="opacity: 0.5; transform-origin: 442.52px 213.92px;" class="animable" id="ellfxaz3ryf4"/></g><polygon points="430.35 209.95 432.54 212.59 428.74 218.16 436.35 212.74 434.74 210.49 440.57 206.24 436.15 202.75 430.35 209.95" style="fill: #7F68C8; transform-origin: 434.655px 210.455px;" id="el5cupdhlhzfq" class="animable"/><g id="elap2jvo2ob9"><polygon points="430.35 209.95 432.54 212.59 428.74 218.16 436.35 212.74 434.74 210.49 440.57 206.24 436.15 202.75 430.35 209.95" style="opacity: 0.5; transform-origin: 434.655px 210.455px;" class="animable" id="elgf7ebut545"/></g></g></g></g><defs> <filter id="active" height="200%"> <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"/> <feFlood flood-color="#32DFEC" flood-opacity="1" result="PINK"/> <feComposite in="PINK" in2="DILATED" operator="in" result="OUTLINE"/> <feMerge> <feMergeNode in="OUTLINE"/> <feMergeNode in="SourceGraphic"/> </feMerge> </filter> <filter id="hover" height="200%"> <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"/> <feFlood flood-color="#ff0000" flood-opacity="0.5" result="PINK"/> <feComposite in="PINK" in2="DILATED" operator="in" result="OUTLINE"/> <feMerge> <feMergeNode in="OUTLINE"/> <feMergeNode in="SourceGraphic"/> </feMerge> <feColorMatrix type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 "/> </filter></defs></svg></body><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这是<code>博客建站系列</code>的第五篇,主要分享这段时间对博客的一些优化工作,主要契机是通过<code>lighthouse</code>测试发现博客站点有很多不足,从而进行的一系列优化,其中很多优化工作,我没在本片博文中介绍,主要是因为这些优化工作比较琐碎,同时优化方案也容易找到,对于刚建立的博客的读者应该有一定的参考意义。这段时间,认真整理博客才发现,博客的建站和优秀博文的创作不是一件容易的事,同时对那些维护了优秀博客的博主深感敬意,尤其是在这个信息快餐时代。最后,博客是一个能够让我深度思考的地方,希望我的博文能够被更多地人阅读,并给读者带来一些帮助和灵感。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://wivwiv.com/post/speed-up-website-loading/">hexo 博客建站方式对比</a></li><li><a href="https://mutebardtison.github.io/2018/07/21/How-to-Get-Your-Hexo-Blog-Listed-Indexed-on-Google/">How to Get Your Hexo Blog Listed/Indexed on Google</a></li><li><a href="https://www.itrhx.com/2019/09/17/A48-submit-search-engine-inclusion/">Hexo 博客提交百度、谷歌搜索引擎收录</a></li><li><a href="https://blog.ichr.me/post/webp-on-hexo/">hexo 实现 webp 支持</a></li></ol>]]></content>
<categories>
<category> 博客 </category>
</categories>
<tags>
<tag> hexo </tag>
</tags>
</entry>
<entry>
<title>docker最佳实践-减小镜像体积</title>
<link href="/2020/06/15/docker-day1/"/>
<url>/2020/06/15/docker-day1/</url>
<content type="html"><![CDATA[<h2 id="系列文章"><a href="#系列文章" class="headerlink" title="系列文章"></a>系列文章</h2><p><code>docker最佳实践</code>系列介绍了容器使用的一些最佳实践,内容包括如何优化减少镜像的大小,如何提升构建速度(这在<code>CICD</code>中十分重要),如何管理镜像等。如果有需要的小伙伴,可以一起讨论学习。</p><ul><li><a href>减小镜像体积</a></li><li><a href>提升构建速度</a></li></ul><h2 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h2><p>当我们刚开始接触<code>Docker</code>,并尝试使用<code>docker build</code>构建镜像时,通常会构建出体积巨大的镜像。而事实上,我们可以通过一些技巧方法减小镜像的大小。本片博文,我将介绍一些优化技巧,同时也会探讨如何在减小镜像大小和可调试性取舍。这些技巧可以分为两部分:第一部分是多阶段构建(<code>multi-stage builds</code>),正确使用多阶段构建能够极大减小构建物镜像的大小,同时还会解释静态链接(<code>static link</code>)和动态链接(<code>dynamic link</code>)之间的区别以及为什么我们需要了解它们;第二部分是使用一些常见的基础镜像,这些基础镜像仅包含我们所需要的内容,而无需引入其他文件。</p><h2 id="场景还原"><a href="#场景还原" class="headerlink" title="场景还原"></a>场景还原</h2><p>首先,我们通过<code>Golang</code>和<code>C</code>的两个<code>Hello world</code>程序去还原很多开发者第一次使用<code>docker build</code>所构建出镜像,并且看看这些的镜像的大小。</p><p>下面是<code>C</code>的<code>Hello world</code>示例程序:</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* hello.c */</span></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">puts</span>(<span class="string">"Hello, world!"</span>);</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>通过以下的<code>Dockerfile</code>文件构建镜像:</p><figure class="highlight dockerfile"><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">FROM</span> gcc</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> hello.c .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> gcc -o hello hello.c</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> [<span class="string">"./hello"</span>]</span></span><br></pre></td></tr></table></figure><p>可以发现一个简单的<code>Hello World</code>程序,最终构建出的镜像大小超过了<code>1G</code>。主要是由于我们使用了<code>gcc</code>基础镜像。如果我们使用<code>Ubuntu</code>镜像,安装<code>C</code>编译器,然后编译程序,最终构建出镜像大小只有300MB,和第一次相比,减小了不少,但这对于一个实际只有 12KB 的二进制文件来说,仍然大的难以接受。</p><figure class="highlight bash"><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">➜ c-hello-world ls -l hello</span><br><span class="line">-rwxr-xr-x 1 donggang staff 12556 6 15 11:35 hello</span><br></pre></td></tr></table></figure><p>同样,对于<code>Golang</code>程序:</p><figure class="highlight golang"><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">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</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"> fmt.Println(<span class="string">"Hello, world!"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>通过<code>golang</code>镜像构建的最终镜像大小超过了 800MB,而实际执行文件的大小只有 2MB。</p><figure class="highlight bash"><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">➜ go-hello-world ls -l go-hello-world</span><br><span class="line">-rwxr-xr-x 1 donggang staff 2174056 6 15 11:40 go-hello-world</span><br></pre></td></tr></table></figure><p>通过以上两个示例,我们急需一些方法来改善我们的构建以减小最终产物的体积。接下来,我们通过一些方法将最终产物的大小缩小 99.8%(注意:这有时会我们调试程序带来很大问题)。</p><blockquote><p>下面会通过不同的 tag 来标识优化后构建的镜像,如<code>hello:gcc</code>,<code>hello:ubuntu</code>,<code>hello:thisweirdtrick</code>,这样通过<code>docker image hello</code>,可以方便比较镜像的大小。</p></blockquote><h2 id="多阶段构建"><a href="#多阶段构建" class="headerlink" title="多阶段构建"></a>多阶段构建</h2><p>通常,我们首先是通过多阶段构建来减小镜像的大小,往往这也是最有效的方法。不过,我们需要注意,如果处理不当,可能会造成构建的镜像无法运行。</p><p>多阶段构建的核心概念很简单:“我不要包括 C 或者 Go 的编译器和整个构建辅助工具,我仅仅想要可执行文件”。我们添加构建阶段来改造之前的C演示程序,具体的<code>Dockerfile</code>文件如下:</p><figure class="highlight dockerfile"><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">FROM</span> gcc AS mybuildstage</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> hello.c .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> gcc -o hello hello.c</span></span><br><span class="line"><span class="keyword">FROM</span> ubuntu</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> --from=mybuildstage hello .</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> [<span class="string">"./hello"</span>]</span></span><br></pre></td></tr></table></figure><p>我们使用<code>gcc</code>作为基础镜像编译<code>hello.c</code>程序,这一阶段为编译阶段<code>mybuildstage</code>。然后,我们开始定义新的阶段<code>执行阶段</code>,这个阶段使用<code>ubuntu</code>镜像,这个阶段我们将上个阶段的构建产物<code>hello</code>可执行文件复制到指定目录中,最终构建出的镜像只有64MB,这减少了大约95%的大小:</p><figure class="highlight bash"><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">➜ c-hello-world docker images c-hello</span><br><span class="line">REPOSITORY TAG IMAGE ID CREATED SIZE</span><br><span class="line">c-hello gcc.ubuntu d9492a009e98 23 minutes ago 73.9MB</span><br><span class="line">c-hello gcc db0206def0e6 27 minutes ago 1.19GB</span><br></pre></td></tr></table></figure><p>通过多阶段构建,我们已经极大地优化了最终产物的大小。关于多阶段构建还有一些需要注意的点:</p><ul><li><p>在声明构建阶段时,可以不显示使用<code>As</code>关键字。后续阶段我们可以使用数字(以 0 开始)从前面的阶段复制文件。在复杂的构建中, 显示定义名称便于后续的维护。</p> <figure class="highlight dockerfile"><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">COPY</span><span class="bash"> --from=mybuildstage hello . </span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> --from=0 hello .</span></span><br></pre></td></tr></table></figure></li><li><p>使用经典镜像: 关于运行阶段的基础镜像的选择,我建议使用一些经典基础镜像,如 Centos,Debian,Fedora,Ubuntu 等, 你可能听过其他简化类型的镜像。</p></li><li><p><code>COPY --from</code>使用绝对路径:<code>golang</code>镜像默认工作目录是<code>/go</code>,所以我们需要从<code>/go</code>目录复制可执行文件。</p> <figure class="highlight dockerfile"><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">FROM</span> golang</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> hello.go .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> go build hello.go</span></span><br><span class="line"><span class="keyword">FROM</span> ubuntu</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> --from=0 /go/hello .</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> [<span class="string">"./hello"</span>]</span></span><br></pre></td></tr></table></figure></li></ul><h2 id="使用Scratch镜像"><a href="#使用Scratch镜像" class="headerlink" title="使用Scratch镜像"></a>使用<code>Scratch</code>镜像</h2><p>回到之前<code>Hello World</code>示例程序,<code>C</code>版本大小16KB,<code>Go</code>版本是2MB,那么问题来了,我们可以获取同样大小的镜像吗?可不可以构建出一个镜像,其中只包括最终的可执行文件呢?答案是肯定的,通过使用<code>scratch</code>作为运行阶段的基础镜像,注意<code>scratch</code>是一个虚拟镜像,我们不可以直接拉取或运行它,因为它完全为空。我们按照下面示例修改<code>Go</code>的<code>Dockerfile</code>文件:</p><figure class="highlight dockerfile"><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">FROM</span> golang</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> hello.go .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> go build hello.go</span></span><br><span class="line"><span class="keyword">FROM</span> scratch</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> --from=0 /go/hello .</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> [<span class="string">"./hello"</span>]</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><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">➜ go-hello-world docker images go-hello</span><br><span class="line">REPOSITORY TAG IMAGE ID CREATED SIZE</span><br><span class="line">go-hello scratch 57ae1b48d6bb 47 seconds ago 2.01MB</span><br></pre></td></tr></table></figure><p>构建完的最终镜像的大小只有2MB。同样执行成功。是不是什么时候都可以使用<code>scratch</code>作为运行阶段的基础镜像呢?当然不行,在使用<code>scratch</code>作为基础镜像时需要注意以下几点。</p><h3 id="没有shell"><a href="#没有shell" class="headerlink" title="没有shell"></a>没有<code>shell</code></h3><p><code>scratch</code>镜像没有<code>shell</code>,这意味着不能在<code>Dockerfile</code>中<code>CMD</code>使用字符串语法(<code>RUN</code>也是):</p><figure class="highlight dockerfile"><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">FROM</span> golang</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> hello.go .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> go build hello.go</span></span><br><span class="line"><span class="keyword">FROM</span> scratch</span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> --from=0 /go/hello .</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> ./hello</span></span><br></pre></td></tr></table></figure><p>如果我们执行以上构建出的镜像,会提示以下错误:</p><figure class="highlight bash"><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">➜ go-hello-world docker run go-hello:scratch.stringsyntax</span><br><span class="line">docker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused <span class="string">"exec: \"/bin/sh\": stat /bin/sh: no such file or directory"</span>: unknown.</span><br></pre></td></tr></table></figure><p>这是因为<code>RUN,CMD</code>中使用字符串语法,这些参数会传递给<code>/bin/sh</code>,<code>CMD ./hello</code>最终会执行<code>/bin/sh -c "./hello"</code>。而<code>scratch</code>中没有<code>shell</code>。解决方法就是使用JSON语法,使用JSON语法时,Docker会直接执行而不是通过<code>shell</code>执行。</p><h3 id="没有调试工具"><a href="#没有调试工具" class="headerlink" title="没有调试工具"></a>没有调试工具</h3><p>因为<code>scratch</code>是空的,所以构建出的镜像不包含任何工具,如<code>ls,ps,ping</code>等,我们也就无法进入到该容器(<code>docker exec</code>)中。</p><blockquote><p>严格意义上,我们仍然可以通过一些方法进行容器故障排错,我们可以使用docker cp从容器中获取文件,使用docker run –net container与网络堆栈进行交互,以及使用像<code>nsenter</code>这样的工具。如果使用新版本kubernetes,也可以利用<a href="https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/"><code>ephemeral container</code></a>这一特性,</p></blockquote><p>一种解决方法是使用<code>busybox</code>或者<code>alpine</code>作为基础镜像,这些镜像只有几MB大小,但提供一些方便调试的工具。</p><h3 id="没有libc"><a href="#没有libc" class="headerlink" title="没有libc"></a>没有libc</h3><p>这个问题往往很难解决,简单的<code>Go Hello World</code>能够使用<code>scratch</code>基础镜像执行,但是<code>C Hello World</code>和一些其他复杂的<code>Go</code>程序(使用<code>net</code>包,或者使用<code>sqlite</code>),往往不能成功执行,会产生如以下的报错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">standard_init_linux.go:211: <span class="built_in">exec</span> user process caused <span class="string">"no such file or directory"</span></span><br></pre></td></tr></table></figure><p>似乎是缺少了一些文件导致的,但是又没具体指出缺失了什么文件。其实这是因为缺失了必要动态库文件<code>dynamic library</code>,程序编译成功运行时,需要使用一些库,如<code>C Hello World</code>中的<code>puts</code>。在90年代,通常使用静态链接的方式<code>static linking</code>,这意味着程序使用的库将包含在最终的二进制文件中,在使用软盘分发程序和没有标准库的情况下,这种方式十分方便,但是在<code>linux分时系统</code>流行后,这种无法共享公共库的方式很快不再流行。</p><p>如今大部分情况下,使用动态链接。使用动态链接编译的程序,最终二进制文件不包含具体的库,而只包含对依赖库的引用,例如一个程序需要libtrigonometry.so中的cos和sin和tan函数。执行程序时,系统会查找该libtrigonometry.so并将其与程序一起加载,以便程序可以调用这些函数。使用动态链接往往有以下优点:</p><ol><li>节省存储资源,多个程序共享一个库;</li><li>节省内存,多个程序运行内存调用同一片内存;</li><li>维护方便,更新库时,无需重新编译程序;</li></ol><blockquote><p>有些人可能会说节省内存不是动态链接所带来的优点,而是共享库<code>shared library</code>。关于具体的细节大家可以参考具体文档。</p></blockquote><p>回到上面的示例程序,默认情况<code>C</code>使用动态链接,使用某些包的<code>Go</code>程序也是如此,上述程序使用标准C库,该库位于<code>libc.so.6</code>文件中,所以需要在镜像中包含该文件,<code>C Hello World</code>才能正常执行。而<code>scratch</code>镜像中,这个文件显然不存在,<code>buysbox</code>和<code>alpine</code>也不包含这个库,<code>busybox</code>没有包含标准C库,<code>alpine</code>使用的是另外版本。通常我们通过以下方式解决找不到库链接的问题。</p><h4 id="使用静态链接"><a href="#使用静态链接" class="headerlink" title="使用静态链接"></a>使用静态链接</h4><p>我们可以使用静态链接,这取决于我们具体使用的构建工具,如果使用<code>gcc</code>,可以通过<code>-static</code>实现静态链接:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc -o hello hello.c -static</span><br></pre></td></tr></table></figure><p>最终构建的二进制文件大小760KB而不16KB,主要是嵌入的库文件导致镜像变大,但是运行镜像时,将不再会报错。</p><h4 id="手动添加库文件"><a href="#手动添加库文件" class="headerlink" title="手动添加库文件"></a>手动添加库文件</h4><p>首先通过一些工具,可以得到程序正在使用哪些库(<code>ldd</code>,mac下使用<code>otool</code>):</p><figure class="highlight bash"><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">$ ldd hello</span><br><span class="line">linux-vdso.so.1 (0x00007ffdf8acb000)</span><br><span class="line">libc.so.6 => /usr/lib/libc.so.6 (0x00007ff897ef6000)</span><br><span class="line">/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff8980f7000)</span><br></pre></td></tr></table></figure><p>我们可以看程序使用的具体哪些库以及路径,上面的例子中,唯一有意义的是<code>libc.so.6</code>库,<code>linux-vdso.so.1</code>与虚拟动态共享对象有关,该机制主要用于加速某些系统调用,而<code>ld-linux-x86-64.so.2</code>则是动态链接器本身。 </p><p>我们可以手动将上面所有的库文件添加到镜像中,也能成功执行。这种方式对于后续维护是灾难性的,同时对于大型的程序(GUI),通过这种方式,我们往往会力不从心。所以不推荐使用这种方式。</p><h4 id="使用busybox-glibc之类的镜像"><a href="#使用busybox-glibc之类的镜像" class="headerlink" title="使用busybox:glibc之类的镜像"></a>使用<code>busybox:glibc</code>之类的镜像</h4><p>上述例子,我们可以通过<code>busybox:glibc</code>作为基础镜像,它只有5MB,这个镜像提供了<code>GNU C Libray(glibc)</code>,这样可以使用动态链接,运行这些程序。</p><p>但是如果我们的程序还使用了其他库,仍然需要手动安装。</p><h2 id="回顾"><a href="#回顾" class="headerlink" title="回顾"></a>回顾</h2><p>通过优化,我们最终将一个超过1GB的文件优化到只有几十KB:</p><ul><li>使用gcc镜像: 1.14GB</li><li>多阶段构建,使用gcc和ubuntu镜像:64.2MB</li><li>静态链接,使用alpine: 6.5MB</li><li>动态链接,使用alpine: 5.6MB</li><li>静态链接,使用scratch:940KB</li><li>静态musl二进制文件,使用scratch: 94KB</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>优化镜像的同时,能够帮助我们更好的理解容器相关核心特性。依我个人的使用的总结经验,主要会从以下几个角度思考是否可以进行优化:</p><ol><li>是否可以使用多阶段优化;</li><li>是否可以使用如<code>scratch</code>较小的镜像作为基础镜像;</li><li>是否可以移除一些没有必要的层;</li><li>是否可以合并某些层;</li></ol><p>通常,追求最小的镜像并不等同于最佳实践,我们需要综合考虑后续可调试性以及使用成本。一般情况我会偏向使用<code>scratch,alpine</code>这类的镜像。这类镜像很小的同时也提供了必要的工具和可拓展性。</p><h2 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h2><ol><li>在学习Docker以及编写Dockerfile时,我们通过工具<code>dive</code>帮助我们分析镜像的结构,方便后续优化<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/docker-day1/2020-06-15-13-56-49.png" alt></li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> docker </tag>
</tags>
</entry>
<entry>
<title>Go协程,OS线程和CPU管理</title>
<link href="/2020/06/10/go-goroutine-thread-cpu/"/>
<url>/2020/06/10/go-goroutine-thread-cpu/</url>
<content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-11-18-35.png" alt></p><blockquote><p>本片博文基于Go1.14.3版本</p></blockquote><p>创建一个系统线程并且从一个线程切换到另一个线程会带来内存和性能上的损耗。<code>Go</code>旨在更好地发挥多核的性能,其从一开始设计就考虑了对并发地优化。</p><h2 id="M-P-G模型"><a href="#M-P-G模型" class="headerlink" title="M-P-G模型"></a>M-P-G模型</h2><p>为了解决线程间切换所造成的性能损耗。<code>Go</code>有自己的调度器(<code>scheduler</code>),可以在操作系统上分配协程(<code>Goroutine</code>)。这个调度器有三个核心概念,<a href="https://golang.org/src/runtime/proc.go">源码中的描述如下</a>:</p><figure class="highlight golang"><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">The main concepts are:</span><br><span class="line">G - goroutine.</span><br><span class="line">M - worker thread, or machine.</span><br><span class="line">P - processor, a resource that is required to execute Go code.</span><br><span class="line"> M must have an associated P to execute Go code[...].</span><br></pre></td></tr></table></figure><p>下图展示了<code>M-P-G</code>模型:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-11-28-56.png" alt></p><p>每个协程<code>G</code>运行在一个系统线程<code>M</code>中,同时该线程<code>M</code>被分配一个逻辑CPU<code>P</code>上。让我们看一个简单的例子,这个例子介绍了<code>Go M-P-G</code>模型:</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"> wg.Add(<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="built_in">println</span>(<span class="string">`hello`</span>)</span><br><span class="line"> wg.Done()</span><br><span class="line"> }()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="built_in">println</span>(<span class="string">`world`</span>)</span><br><span class="line"> wg.Done()</span><br><span class="line"> }()</span><br><span class="line"></span><br><span class="line"> wg.Wait()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>Go</code>首先会创建多个<code>P</code>,这取决于执行机器上逻辑U的数量,然后会将这些<code>P</code>保存在<code>P空闲队列</code>中。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-11-48-26.png" alt></p><p>然后,新的协程或者处于待执行状态的协程将会唤醒一个逻辑U<code>P</code>去执行,<code>P</code>会创建一个具有关联OS线程的<code>M</code>:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-11-51-43.png" alt></p><p>就像<code>P</code>一样,不处于工作状态的<code>M</code>(即没有等待执行的协程),可能是从系统调用返回的,也可能被GC强行停止的,它们也会被会暂存到一个空闲列表中。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-12-39-59.png" alt></p><p>在程序启动装载时,<code>Go</code>已经创建了一些OS线程同时关联到<code>M</code>上。上面的例子中,第一个打印<code>Hello</code>的协程将会使用主线程的<code>M</code>,第二个打印<code>World</code>的协程将会从空闲队列中获取一个<code>M</code>和<code>P</code>。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-12-47-37.png" alt></p><p>目前为止,我们已经对<code>Go</code>协程和线程管理有了初步的了解。下面让我们看看哪种情况<code>Go</code>使用的<code>M</code>多于<code>P</code>,以及如何在系统调用的情况下管理协程。</p><h2 id="系统调用-System-calls"><a href="#系统调用-System-calls" class="headerlink" title="系统调用 System calls"></a>系统调用 <code>System calls</code></h2><p><code>Go</code>通过在运行中包装系统调用来优化它们(无论是否阻塞)。这个包装器(<code>wrapper</code>)将自动从线程<code>M</code>分离<code>P</code>,并运行另一个线程在其上运行。让我们通过一个读取文件的例子来解释其原理:</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> buf := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"> fd, _ := os.Open(<span class="string">"number.txt"</span>)</span><br><span class="line"> fd.Read(buf)</span><br><span class="line"> fd.Close()</span><br><span class="line"></span><br><span class="line"> <span class="built_in">println</span>(<span class="keyword">string</span>(buf)) <span class="comment">// 42</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下图是打开文件是的工作流:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-13-33-59.png" alt></p><p><code>P0</code>目前处于空闲队列,这意味这它可以分配到<code>M</code>。一旦系统调用结束返回,<code>Go</code>将按照以下规则执行直到其中一条规则满足:</p><ul><li>尝试分配之前的<code>P</code>:”P0”,然后恢复执行;</li><li>从<code>P</code>的空闲队列中获取一个新的<code>P</code>,然后恢复执行;</li><li>将协程保存在阻塞队列中,同时将<code>M</code>返回到空闲队列中;</li></ul><p>同时,在非阻塞<code>I/O</code>(例如http调用)的情况下,Go还可以处理资源尚未准备就绪的情况。在这中情况下,第一个系统如果遵循上面的工作流将不会成功,这是因为资源还未准备好,可以通过强制转到使用网络轮训器(<code>network poller</code>)并暂存协程。下面是一个例子:</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> http.Get(<span class="string">`https://httpstat.us/200`</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>一旦完成第一个系统调用并明确指出资源尚未准备好,协程将驻留(暂存),直到网络轮训器通知它资源已就绪。这种情况下线程<code>M</code>将不被阻塞。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-14-22-18.png" alt></p><p>当Go调度器调度协程继续工作时,这个被暂存的协程将会恢复执行状态。然后,调度器将询问网络轮询器在成功获得它所等待的信息后,是否有一个goroutine正在等待运行:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-14-25-22.png" alt></p><p>如果有多个goroutine准备好了,那么额外的goroutine将进入全局可运行队列,稍后进行调度。</p><h2 id="操作系统线程的限制"><a href="#操作系统线程的限制" class="headerlink" title="操作系统线程的限制"></a>操作系统线程的限制</h2><p>当使用系统调用时,<code>Go</code>不会限制阻塞OS的线程数,官方给出解释:</p><blockquote><p>环境变量<code>GOMAXPROCS</code>限制用户级别(user-level)可以执行的线程数量。这意味这<code>Go</code>在系统调用中阻塞的线程数量没有限制,这并没有和变量<code>GOMAXPROCS</code>冲突。</p></blockquote><p>下面我们通过一个例子证明:</p><figure class="highlight golang"><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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>;i < <span class="number">100</span> ;i++ {</span><br><span class="line"> wg.Add(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> http.Get(<span class="string">`https://httpstat.us/200?sleep=10000`</span>)</span><br><span class="line"></span><br><span class="line"> wg.Done()</span><br><span class="line"> }()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> wg.Wait()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>通过追踪工具<code>go tool trace</code>我们可以查看线程的数量:</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/go-goroutine-thread-cpu/2020-06-10-15-20-14.png" alt></p><p>由于<code>Go</code>优化了线程调度,当分配到该线程的协程阻塞后,该线程可以被重用,这也解释了为什么上述程序中,协程的数量不等于线程。 </p><p><strong>译自:</strong><a href="https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a">Go: Goroutine, OS Thread and CPU Management</a></p>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> go </tag>
<tag> 译文 </tag>
</tags>
</entry>
<entry>
<title>博文格式化&Hexo主题高级自定义</title>
<link href="/2020/06/02/blog-day4/"/>
<url>/2020/06/02/blog-day4/</url>
<content type="html"><![CDATA[<p>有一段时间没怎么倒腾博客了,这几天收拾了一下博客,同时将最近阅读和记录的有趣的内容整理一番,分享给大家。主要包括在博客写作时遇到的一些问题及解决方法,即我是如何构建自己的博客写作工作流的。同时也有一些在博文写作的中一些感悟。</p><h2 id="hexo-及-Butterfly-主题升级"><a href="#hexo-及-Butterfly-主题升级" class="headerlink" title="hexo 及 Butterfly 主题升级"></a>hexo 及 Butterfly 主题升级</h2><p>首先,最近升级了 hexo 版本,升级到最新版本<code>3.1.0</code>,同时也更新了主题的版本。当然,升级完就出现问题🤪,定位错误是之前复制的自定义配置文件<code>butterfly.yml</code>缺少一些必要的变量,同时该文件结构也发生了变化,导致没办法渲染成功。暗暗窃喜还好做了版本管理,直接 merge,但是由于结构改动变化太大,merge过程很不轻松,最终还是手动将之前的配置迁移到新的自定义配置文件,才解决问题。</p><p>这提醒我们<code>版本管理</code>并不能解决任何问题,只是当<code>灾难</code>降临,提供给我们多一种选择。作为软件的用户,升级前需要做好规划,特别是很长时间没有更新的情况下。而作为主题开发者,需要提供给用户良好的平滑升级功能,当然有时这需要耗费很大经历。使用开源项目,有时是需要代价和精力去维护的,而这又体现收费软件的价值所在。</p><h2 id="图片管理"><a href="#图片管理" class="headerlink" title="图片管理"></a>图片管理</h2><p><code>Hexo</code>搭建博客,其中图片的引用和管理是一件令人非常头痛的事。究其根本是<code>Markdown写作</code>方式导致的。在不涉及图片时,<code>markdown</code>的轻松写作十分方便,但是当一篇<code>.md</code>格式的博文包含了图片引用,情况就不一样了。而博客图片引用分为本地和远程两种方式,这两种方式都一些不足:</p><ul><li>引用本地文件:一旦需要导出,要么打包整个目录,所有图片引用相对路径,要么只能借助其他格式(pdf)实现。和word 文档相比,易用性很差,同时当需要将本地的写作发布到<code>wiki</code>上,其发布过程也十分繁琐,这些理应被自动化的体力活动却没有,当然会导致<code>markdown</code>在使用感受上有很大缺陷。</li><li>引用远程链接:一般通过图床实现,这种方式在写作之前需要将图片整理并发布到对应存储服务上,写作时通过工具生成下载链接,这种缺点是插入图片过程很难实现<code>Copy-And-Paste</code>,同时博文这个资源需要分为图片和内容两个资源管理,增加了复杂度。</li></ul><p>我的博文写作是通过第二种方式引入图片,这种方式优点是可以减少博客静态资源,同时通过<code>CDN</code>可以提高网站访问加速,配合<code>vscode paste-image</code>插件和自己开发的<code>hexo-image-sync</code>的工具,能够实现<code>Copy-And-Paste</code>流程。但是对于其他工作流,如工作上<code>wiki文档</code>维护,工作日报通过<code>Markdown</code>编写等,目前没有探索出很好方案。</p><p>在理想的将来,希望能<code>Markdown写作</code>够做到博文和图片资源的统一处理。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day3/2019-07-15-17-42-29.png" alt></p><h2 id="Markdown-协议-、渲染引擎和格式化"><a href="#Markdown-协议-、渲染引擎和格式化" class="headerlink" title="Markdown 协议 、渲染引擎和格式化"></a>Markdown 协议 、渲染引擎和格式化</h2><p>在使用<code>Markdown</code>工具进行写作时,通常强制换行需要通过两个空格实现,但是在通过<code>Hexo</code>渲染博文时,是不需要加上两个空格的。由于无法预知什么需要换行,博文源文件中会出现一段文字一行的情况,这十分影响写作和博客源文件阅读体验。</p><p>目前<code>Markdown</code>协议还未完全统一,主要有<code>Markdown</code>,<code>GFM</code>和<code>CommanMark</code>三种,这些协议在细节上有所区别。hexo 其渲染<code>Markdown</code>文件是通过了插件<a href="https://github.com/hexojs/hexo-renderer-marked">hexo-renderer-marked</a>实现,该插件提供一些配置项,以上关闭自动换行的功能可以在<code>hexo_config.yml</code>修改,如下:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">marked:</span></span><br><span class="line"> <span class="attr">gfm:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">breaks:</span> <span class="literal">false</span> <span class="comment">#Enable GFM line breaks. This option requires the gfm option to be true.</span></span><br></pre></td></tr></table></figure><p>这样,我们可以就通过空格控制是否换行。这个问题归根结底是博文<code>.md</code>文件的格式化问题,我们不希望在酷炫的UI 下,其博文源文件一团糟,在通过<code>vscode</code>写作时,推荐安装插件<a href="https://prettier.io/"><code>prettier</code></a>实现<code>Markdown</code>格式文件的格式化,同时开启<code>vscode</code>的自动换行功能,参考下图配置项:<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day4/2020-06-01-20-30-21.png" alt></p><p>配置完,我们写博客就有了通过IDE写代码的感觉。</p><h2 id="博文代码块"><a href="#博文代码块" class="headerlink" title="博文代码块"></a>博文代码块</h2><p>技术类的文章,尤其是源码分析类,博文中需要插入大量的代码块辅助读者阅读,有时会引用一段很长代码,如果大量使用<code>Markdown Code Block</code>,十分影响博文的页面布局,对读者不友好。<code>Github Gist</code>功能可以帮助我们更好在博文中引用并展示<code>Github</code>代码块。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day4/2020-06-01-17-43-45.png" alt></p><p>我们可以从<a href="https://gist.github.com/">网页</a>创建代码块,同时一些 IDE 开发工具集成<code>Github Gist</code>,能够从开发环境中快速创建代码块。创建完可以拿到对应内嵌 Script 链接<code><script src="https://gist.github.com/***/snippet.js"></script></code>,Markdown 是支持内联<code>HTML</code>代码的,我们直接插入链接即可。显示效果如下:</p><script src="https://gist.github.com/taka-wang/542cb5489d97967dfcad.js"></script><p>这种显示效果很好,而且方便跳转对应源码仓库,查看上下文。除了引用代码块,在引用其他资源链接(如 twitter,medium 文章)时,也可以优化其显示效果,比如<a href="https://medium.com/"><code>Medium</code></a>对常用第三方链接的显示效果如下:<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day4/2020-06-01-19-58-44.png" alt></p><p>在<code>Hexo</code>中,通过其<code>Tag Plugins</code>功能,能够做到美化一些链接的显示,以下展示链接了一篇<code>Medium</code>文章:</p><blockquote><p>Every interaction is both precious and an opportunity to delight.</p><footer><strong>Thatcher Peskens</strong><cite><a href="https://medium.com/@thatcher/why-choose-between-grpc-and-rest-bc0d351f2f84">medium.com/@thatcher/why-choose-between-grpc-and-rest-bc0d351f2f84</a></cite></footer></blockquote><p>更好的呈现博客内容也有代价,就是博客源文件中出现大量语法,也和其他第三方工具紧密耦合,不方便后续升级维护,我们在维护自己博客时,就需要在这两者之间权衡代价。</p><h2 id="主题高级配置-导入字体"><a href="#主题高级配置-导入字体" class="headerlink" title="主题高级配置-导入字体"></a>主题高级配置-导入字体</h2><p>最近在浏览文章发现时一个自建博客网站的字体让我阅读起来,十分舒心,这个字体是谷歌新推出的思源宋体,就打算将自己博客的中的字体也替换成思源宋体,查看了主题<code>Butterfly</code>的配置文件,其没有提供对该字体支持,开发者回复说导入字体需要自己实现。</p><p>对于不太熟悉前端的小伙伴这可能得需要一点时间,这里我只简单介绍一下<code>Stylus</code>以及修改的步骤,建议有一点前端知识的小伙伴了解hexo 的模版机制,以及相关技术,主要包括<code>stylus</code>,<code>pug</code>以及 hexo 对应的插件,应该不是特别困难。</p><p>目前模板<code>Butterfly</code>使用的<code>css</code>引擎是<a href="https://stylus-lang.com/"><code>stylus</code></a>,这个框架旨在提供动态、健壮、表现力(EXPRESSIVE,DYNAMIC,ROBUST)的<code>css</code>开发方式。大概提供了以下功能:</p><ol><li>可以省略 css 括号,冒号符号</li><li>提供函数,简化重复代码</li><li>提供导入功能,提高可读性</li><li>提供一些内置函数</li></ol><p>简而言之,就是更优雅的编写<code>css</code>。这样我们就可以对<code>Butterfly</code>的样式做一些自定义调整啦。<code>hexo</code>提供了<code>hexo-renderer-stylus</code>插件,我们就可以通过<code>hexo-config()</code>方法获取 yaml 文件定义的变量。</p><figure class="highlight javascript"><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="comment">// 注册hexo-config变量</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">defineConfig</span>(<span class="params">style</span>) </span>{</span><br><span class="line"> style.define(<span class="string">"hexo-config"</span>, <span class="function"><span class="keyword">function</span> (<span class="params">data</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> getProperty(self.theme.config, data.val);</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果需要通过外部变量的方式,可以通过以上方式在配置文件定义变量,目前我的做法是 check 一个自定义分支维护自己的一些更改,因为暂时没有对外暴露的需求,直接在<code>layout/includes/header/header.pug</code>的文件引入 script,然后在修改配置文件<code>font.font_family</code>变量,相关修改参考如下:</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">diff --git a/layout/includes/head.pug b/layout/includes/head.pug</span><br><span class="line">index 92a66d5..7d78cf9 100644</span><br><span class="line"><span class="comment">--- a/layout/includes/head.pug</span></span><br><span class="line"><span class="comment">+++ b/layout/includes/head.pug</span></span><br><span class="line">@@ -95,6 +95,9 @@ include ./head/comment.pug</span><br><span class="line"> if theme.blog_title_font.font_link</span><br><span class="line"> link(rel='stylesheet', href=url_for(theme.blog_title_font.font_link))</span><br><span class="line"></span><br><span class="line"><span class="addition">+link(rel='stylesheet', href=url_for('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@500&display=swap'))</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"> //- global config</span><br><span class="line"> !=partial('includes/head/config', {}, {cache:theme.fragment_cache})</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于博客中使用的第三方模板和工具,我们需要深度学习,否则就无法对博客进行美化。</p><h2 id="重拾对文字的敬畏之心"><a href="#重拾对文字的敬畏之心" class="headerlink" title="重拾对文字的敬畏之心"></a>重拾对文字的敬畏之心</h2><p>最近看了两片博文,一篇是是关于信息的半衰期<sup>[1]</sup>,<strong>信息的半衰期值指一半信息变得没有价值所需要的时间</strong>,比如软件文档,如果不维护,随着软件版本的升级,文档中很多内容会变的没有价值。博文按照内容其半衰期也有长短区分,打个比方,一篇介绍 kubernetes指定版本安装的博文,随着 kubernetes 的升级,其内容价值会越来越低,而kubernetes的升级确实很快,这就造成这类博文的半衰期很短,而如果一篇介绍 kubernetes apiserver 设计思想及模式的博文,其半衰期相对则更长。信息的半衰期能一定程度体现博文内容的价值。所以在进行博文创作的过程中,我们应努力提升博文质量,提高其半衰期。</p><p>另一篇文章是<<重拾对文字的敬畏之心-三年个人博文的词频解析与反思>><sup>[2]</sup>,作者对其博客写作做了一个词频统计分析,结果很有意思,感兴趣的同学可以阅读原文。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day4/2020-06-02-13-21-59.png" alt></p><p>在整理自己博文时,发现之前的博文读起来十分晦涩,而且有很多明显的错误,这对于读者阅读体验是灾难性的。在写作过程中不恰当的措辞,频繁使用口语化的词汇,这些问题总是存在,如何避免这些问题呢?当然没有捷径可循,<strong>只有踏踏实实多阅读一些优秀的随笔散文,沉淀这些文章,同时多进行写作练习,注意用词,写作完成后自我检查校验。最终形成自己的写作风格。</strong>恍然间才发现,小学到高中的语文课程和写作也是很有意义的。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day4/2020-06-02-11-24-24.png" alt></p><h2 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h2><ol><li>将文件中的标点符号统一成中文符号,对于技术类的文章,代码和链接中包含了很多<code>.</code>,<code>,</code>,所以只能由作者保证,没有工具满足场景。</li><li>博文中如何显示一个文件 Diff结果?之前读到过一位作者的博文,很好的展示了<code>Git diff</code>的结果,由于没有整理,已经找不到该文章。如果后续有解决方法,会在后续有关博客的相关博文中分享。</li><li>博文在引用别人的图片或文章时,需要注意协议。</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://www.jianshu.com/p/0b7721c1a623">重拾对文字的敬畏之心-三年个人博文的词频解析与反思</a></li><li><a href="https://github.com/ruanyf/weekly/blob/master/docs/issue-103.md">信息的半衰期</a></li></ol>]]></content>
<categories>
<category> 博客 </category>
</categories>
<tags>
<tag> hexo </tag>
</tags>
</entry>
<entry>
<title>the-clean-architecture</title>
<link href="/2020/05/28/the-clean-architecture/"/>
<url>/2020/05/28/the-clean-architecture/</url>
<content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/the-clean-architecture/2020-05-28-17-24-59.png" alt></p><p>过去几年,我们已经看到了一系列关于系统架构的想法,包括:</p><ul><li><a href>六边形架构(接口与适配器)</a></li><li><a href="https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/">洋葱架构(Onion Architecture)</a></li><li><a href="http://blog.cleancoders.com/2011-09-30-Screaming-Architecture">Screaming Architecture</a></li><li><a href="http://www.amazon.com/Lean-Architecture-Agile-Software-Development/dp/0470684208/">DCI</a></li><li><a href="http://www.amazon.com/Object-Oriented-Software-Engineering-Approach/dp/0201544350">BCE</a></li></ul><p>这些架构有很多共同的点(思想),尽管它们细节上有所不区别,它们都有相同的目标,那就是<strong>关注点分离(the speration of concerns)</strong>, 它们都是通过将软件分层来实现这种分离,每个组件至少有一个用于业务规则的层和另一个用于接口的层。这些架构中每一个都会产生这样的系统:</p><ol><li>独立的框架:该架构不依赖于某些特性软件库的存,</li><li>可测试的(Testable):可以在不依赖UI、数据库、Web服务器或任何其他外部依赖的情况下进行测试</li><li>独立于UI:UI可以轻松的更改,而无需更改系统的其余部分。例如用控制台UI(终端)代替Web UI,而不需要更改业务规则。</li><li>独立于数据库:你可以切换底层数据库,你的业务业务规则不应该于数据库绑定</li><li>独立于任何外部依赖:你的业务规则根本不了解外部世界</li></ol><p>本文顶部的图是尝试将所有这些体系结构集成到一个可行的想法中的尝试。</p><h2 id="依赖性规则-The-Dependency-Rule"><a href="#依赖性规则-The-Dependency-Rule" class="headerlink" title="依赖性规则(The Dependency Rule)"></a>依赖性规则(The Dependency Rule)</h2><p>同心圆表示软件的不同的领域。一般来说,随着软件的发展,软件会越来越高级(可以简单理解为分层越多)。越靠近外部的圆代表一种机制(mechanisms),越内部的圈代表一种政策(policy)。依赖性规则是软件架构很重要规则,该规则定义了源码性依赖(source code dependencies)只能指向内部,内层不需要关注任何外层的逻辑。内层的代码不能够引用外层的代码,包括函数,类,变量等。</p><h2 id="实体(Entity)"><a href="#实体(Entity)" class="headerlink" title="实体(Entity)"></a>实体(Entity)</h2><p>实体封装了企业范围的业务规则。实体可以是一个具有方法的对象,也可以是一组数据结构或函数,只要实体可以被企业多个应用程序使用,都可以成为实体。如果你只是编写一个简单的业务程序(不涉及企业),那么该程序的业务对象就是实体。实体封装了最通用和最高级的规则。当某些外部变化时,它们变化的可能性最小。例如,你不可希望当修改一个前端页面时,需要修改这些实体对象。对任何外层的修改都不会影响到实体层。</p><h2 id="用例(Use-Cases)"><a href="#用例(Use-Cases)" class="headerlink" title="用例(Use Cases)"></a>用例(Use Cases)</h2><p>这一层通常是应用程序的业务规则(Application Specific),它封装并实现了应用程序的所有用例,这些用例编排流入和流出实体层的数据,通过这实体所实现的企业业务规则实现用例业务规则。我们不希望这一层的改变影响到实体层,同时也不会希望外层如数据库的实现影响到这一层。</p><h2 id="接口适配(Interface-Adapters)"><a href="#接口适配(Interface-Adapters)" class="headerlink" title="接口适配(Interface Adapters)"></a>接口适配(Interface Adapters)</h2><p>这一层的软件(或模块)是一组适配器,用来将用例层获取的数据转换为外层依赖(数据库,HTTPServer)的数据格式.例如GUI的MVC结构通常在这一层实现。实图、演示者和控制器都属于这一层。同样,这一层也会发生数据转换,在这一层中数据会从用例层的结构装换持久性框架的结构比如数据库,这一层不需要关注任何数据库实现的逻辑</p><h2 id="框架和驱动程序(Frameworks-and-Drivers)"><a href="#框架和驱动程序(Frameworks-and-Drivers)" class="headerlink" title="框架和驱动程序(Frameworks and Drivers)"></a>框架和驱动程序(Frameworks and Drivers)</h2><p>最外层通常是由框架和工具组成,如数据库、Web框架,通常情况下这一层不需要有太多的代码,只是一些粘合下层的代码。这一层是所有细节所在,WEB是一个细节,数据库是一个细节,我们将这些放在外面,方便后续修改而不需要改动底层。</p><h2 id="是不是只有四层?"><a href="#是不是只有四层?" class="headerlink" title="是不是只有四层?"></a>是不是只有四层?</h2><p>当然不是,四层只是理想下的软件分层模型,实际情况根据需求调整。依赖性原则始终适用,源码依赖性始终是从外层指向内层的,越往内层移动,抽象级别越高。</p><h2 id="跨越边界(Crossing-boundaries)"><a href="#跨越边界(Crossing-boundaries)" class="headerlink" title="跨越边界(Crossing boundaries)"></a>跨越边界(Crossing boundaries)</h2><p>上图的右下方是我们如何越过圆边界的示例。它显示了控制器和演示者在下一层与用例进行通信。注意控制流程。它从控制器开始,遍历用例,然后在演示者中结束执行。另请注意源代码依赖性。他们每个人都指向用例。我们通常通过使用依赖倒置原则解决这种明显的矛盾。例如,在Java之类的语言中,我们将安排接口和继承关系,以使源代码依赖项在跨边界的正确点处反对控制流。例如,考虑用例需要呼叫演示者。但是,此调用不能直接进行,因为这会违反“依赖关系规则”:外层中的任何定义都不能由内层提及。 因此,我们在内层中有一个用例调用接口(在此处显示为用例输出端口),并在内层中有一个演示者来实现它。使用相同的技术来跨越架构中的所有边界。 我们利用动态多态性来创建与控制流相反的源代码依赖项,以便无论控制流的方向如何,我们都可以遵守“依赖关系规则”。</p><h2 id="什么数据需要跨越边界"><a href="#什么数据需要跨越边界" class="headerlink" title="什么数据需要跨越边界"></a>什么数据需要跨越边界</h2><p>通常,跨越边界的数据是简单的数据结构。如果愿意,可以使用基本结构或简单的数据传输对象。或者,数据可以只是函数调用中的参数。或者,您可以将其打包到一个哈希图中,或将其构造到一个对象中。重要的是,隔离的,简单的数据结构跨边界传递。我们不想欺骗并传递实体或数据库行。我们不希望数据结构具有任何违反《依赖性规则》的依赖性。例如,许多数据库框架都响应查询返回方便的数据格式。我们可以称其为RowStructure。 我们不想将该行结构向内跨边界传递。 这将违反“依赖关系规则”,因为它会迫使一个内圈知道一些关于外圈的信息。因此,当我们跨边界传递数据时,数据总是以最方便内层形式出现。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><hr><p>遵循这些简单规则并不难,并且可以避免许多麻烦。通过将软件划分为多个层,并遵循“依赖关系规则”,您将创建一个具有内在可测试性的系统,并具有其所隐含的所有优势。当系统的任何外部部分(例如数据库或Web框架)过时时,您都可以以最少的麻烦替换那些过时的元素。</p><h2 id="原文"><a href="#原文" class="headerlink" title="原文"></a>原文</h2><p><a href="http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">The Clean Architecture</a></p>]]></content>
<categories>
<category> 译文 </category>
</categories>
<tags>
<tag> 架构 </tag>
</tags>
</entry>
<entry>
<title>Golang源码分析可视化</title>
<link href="/2020/04/08/callgraph/"/>
<url>/2020/04/08/callgraph/</url>
<content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/callgraph/2020-06-08-15-14-28.png" alt></p><blockquote><p>A picture is worth a thousand words.</p></blockquote><p>我们在阅读博客和文档的时候,通常倾向于阅读配有图形的这些文章。如果一篇技术类文章从头到尾都是文字,将十分影响读者的阅读体验,也不便于读者理解。还有我们在进行一些项目源码分析时,边阅读代码,边绘制一些<code>图形</code>能够极大的提升效率,同时也能加强自己理解。这片博文我将介绍我是如何绘制<code>调用图</code>和<code>'类图'(不是面向对象语言中的类图)</code>帮助自己更好地理解<code>Golang</code>项目的。</p><p><code>Golang</code>目前还没有关于调用图和类图的官方标准,有一些工具能够静态分析能够生成项目调用图和类图,不过也不是很成熟。而且自动生成的图形美观性很差,也不便于调整。不过这些工具定义了一些如何可视化<code>Golang元素</code>的规则,我借鉴了这些规则,同时结合<code>UML类图</code>的语法定义,形成了一套规则。</p><h2 id="go-callvis"><a href="#go-callvis" class="headerlink" title="go-callvis"></a>go-callvis</h2><p><a href="https://github.com/ofabry/go-callvis">go-callvis</a>是用于可视化 Go 程序之间的调用拓扑和包关系的的开发工具。这个库定义了一些规则,用于统一的表示<code>Golang</code>项目元素。</p><p><img src="/2020/04/08/callgraph/2020-04-27-09-38-20.png" alt></p><h2 id="Golang-代码元素及规则"><a href="#Golang-代码元素及规则" class="headerlink" title="Golang 代码元素及规则"></a>Golang 代码元素及规则</h2><p>规则目前参考 go-callvis</p><h3 id="Package-Type"><a href="#Package-Type" class="headerlink" title="Package/Type"></a>Package/Type</h3><p>大型项目中结构很复杂,这时候我们只需关注核心的包和类型。</p><table><thead><tr><th>类型(Represent)</th><th>样式(Style)</th></tr></thead><tbody><tr><td>业务核心(focused)</td><td>自定义颜色</td></tr><tr><td>标准库(stdlib)</td><td>green color</td></tr><tr><td>其他(other)</td><td>yellow color</td></tr></tbody></table><h3 id="Method-Function"><a href="#Method-Function" class="headerlink" title="Method/Function"></a>Method/Function</h3><table><thead><tr><th>类型</th><th>样式</th></tr></thead><tbody><tr><td>公开(Exported)</td><td>边框加粗</td></tr><tr><td>私有(UnExported)</td><td>正常边框</td></tr><tr><td>匿名(anonymous)</td><td>虚线边框</td></tr></tbody></table><h3 id="Calls"><a href="#Calls" class="headerlink" title="Calls"></a>Calls</h3><table><thead><tr><th>类型</th><th>样式</th></tr></thead><tbody><tr><td>内部调用</td><td>黑色线</td></tr><tr><td>外部调用</td><td>棕色线</td></tr><tr><td>静态调用</td><td>实线</td></tr><tr><td>动态调用(接口)</td><td>虚线</td></tr><tr><td>正常调用</td><td>简单箭头</td></tr><tr><td>并发调用</td><td>箭头加愿</td></tr><tr><td>延迟调用</td><td>箭头加菱形</td></tr></tbody></table><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><p>下图是分析<code>kubernetes</code>项目对象校验的调用图。其中我们只关注<code>apiserver/registry/rest,pkg/registry, pkg/apis</code>这三个包,所以这些包的颜色我们自定义为白色,而<code>apimachinery</code>这个库我们不需要关注,所以标记为黄色。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/callgraph/2020-06-08-17-22-46.png" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>关于绘图工具,我是通过<code>draw.io</code>在线工具进行绘图的,后续我会开发一个<code>draw</code>模板给大家使用,当然你也可以使用自己喜欢的工具,自己定义这些规则。关键是形成统一的规则,这样可以降低不同开发者之间的成本。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://plantuml.com/">PlantUML 快速绘制语法定义</a></li></ol>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> 源码分析 </tag>
<tag> go </tag>
</tags>
</entry>
<entry>
<title>Helm:kubernetes包管理工具</title>
<link href="/2020/03/30/helm/"/>
<url>/2020/03/30/helm/</url>
<content type="html"><![CDATA[<p>最近给部门的小伙伴做了一个关于<code>helm</code>的入门介绍,收到了不错的反响,于将资料整理分享给博客的读者们。本文第一部分介绍<code>helm</code>是做什么的以及能解决什么问题。第二部分介绍<code>helm</code>的核心概念及安装使用<code>helm</code>的教程,第三部分介绍<code>helm</code>使用的核心即<code>chart</code>的开发,这一章节通过一个示例给大家演示基本的开发步骤。最后一部分,简单说明如何搭建私有模板仓。在读完后,相信你会对<code>Helm</code>及生态有一个简单的了解。</p><h2 id="什么是-helm"><a href="#什么是-helm" class="headerlink" title="什么是 helm"></a>什么是 helm</h2><p>如标题所示,官方给<code>Helm</code>的定义是 kubernetes 的包管理器。那么什么是包管理器呢?</p><h3 id="什么是包管理工具"><a href="#什么是包管理工具" class="headerlink" title="什么是包管理工具"></a>什么是包管理工具</h3><blockquote><p>包管理器一组软件工具,它们以一致的方式自动安装、升级、配置和删除计算机操作系统的计算机程序 -维基百科<br>helm 帮助用户管理 kubernetes 程序,helm chart 帮助用户定义、安装和升级最复杂的 Kubernetes 应用程序,Charts 很容易创建,版本管理,共享和发布,所以停止 copy-and-paste,开始使用 helm -helm 官方</p></blockquote><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-11-48-57.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-23-31.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-24-05.png" alt></p><h3 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h3><p><code>helm</code>的官方文档中的描述更加详细,这里只列出一些重要的概念:</p><table><thead><tr><th>Helm Concept</th><th>描述</th><th>重点</th></tr></thead><tbody><tr><td>Chart(unpackaged)</td><td>一个文件夹的文件,遵循 chart 的准则</td><td>可以直接部署到集群</td></tr><tr><td>Chart(packaged)</td><td>上述文件的压缩包 tar.gz</td><td>可以直接部署到集群</td></tr><tr><td>Chart name</td><td>Chart.yaml 定义包的名称</td><td>部分标识 chart</td></tr><tr><td>Templates</td><td>构成应用程序的一组 Kubernetes 清单</td><td>Golang 模板引擎</td></tr><tr><td>Values</td><td>可以在 Kubernetes 清单中参数化的设置</td><td>用于 template 渲染</td></tr><tr><td>Chart version</td><td>chart 版本</td><td>部分标示 chart</td></tr><tr><td>App Version</td><td>chart 中包含的应用程序版本</td><td>与 chart 版本独立</td></tr><tr><td>Release</td><td>kubernetes 集群中部署的 chart</td><td>同一个 chart 可能部署多个 release</td></tr><tr><td>Release name</td><td>release 任意名称</td><td>独立于 chart name</td></tr><tr><td>Release Revision</td><td>每次部署/升级应用程序时递增的数字</td><td>于 chart 版本无关</td></tr></tbody></table><h2 id="helm-发展"><a href="#helm-发展" class="headerlink" title="helm 发展"></a>helm 发展</h2><ul><li>每个月超过百万次下载</li><li>CNCF Incubating project</li><li>繁荣的社区</li></ul><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-25-32.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-25-58.png" alt></p><h2 id="helm-如何使用"><a href="#helm-如何使用" class="headerlink" title="helm 如何使用"></a>helm 如何使用</h2><h3 id="helm-安装"><a href="#helm-安装" class="headerlink" title="helm 安装"></a>helm 安装</h3><p><code>helm3</code>剔除了服务 Tiller,直接与<code>kubernetes apiserver</code>通信,所以安装相比<code>helm2</code>更加简单。</p><figure class="highlight bash"><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><br><span class="line"><span class="comment"># mac通过homebrew安装</span></span><br><span class="line">brew install helm</span><br><span class="line"></span><br><span class="line"><span class="comment"># 二进制文件</span></span><br><span class="line">wget https://github.com/helm/helm/releases/helm-v3.0.0-linux-amd6</span><br><span class="line">tar -zvxf helm-v3.0.0-linux-amd6</span><br><span class="line">mv linux-amd64/helm /usr/<span class="built_in">local</span>/bin/helm</span><br></pre></td></tr></table></figure><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-30-21.png" alt></p><blockquote><p>对于需要从 Helm2 升级到 helm3 的用户,官方提供了升级插件,我是使用插件进行升级的,没有遇到问题。</p></blockquote><h3 id="chart"><a href="#chart" class="headerlink" title="chart"></a>chart</h3><p><code>chart</code>:一个包含足够信息的 Helm 包,用于将一组 Kubernetes 资源安装到 Kubernetes 集群中。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-31-48.png" alt></p><p>helm 命令行工具能够自动生成一个 chart 结构:<code>helm create myapp</code></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-31-34.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-32-10.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-32-27.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-32-49.png" alt></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-33-08.png" alt></p><p><code>templates</code>包含了所有所需 kubernetes 各个资源的模板文件,<code>Value.yaml</code>文件存储<code>chart</code>的默认值,这些值可以通过<code>helm install ,helm upgrade</code>更新,<code>Chart.yaml</code>包含了<code>chart</code>的描述信息,如版本,名称等。<code>charts</code>目录存储其依赖的第三方<code>chart</code>。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-33-42.png" alt></p><h3 id="安装-Chart"><a href="#安装-Chart" class="headerlink" title="安装 Chart"></a>安装 Chart</h3><p>在根据正在运行的 Kubernetes 集群进行身份验证的环境中,使用 Helm 从本地 chart 目录或 chart repo 安装。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-34-32.png" alt></p><h3 id="使用自定义值"><a href="#使用自定义值" class="headerlink" title="使用自定义值"></a>使用自定义值</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-35-06.png" alt></p><h3 id="查看-release-状态"><a href="#查看-release-状态" class="headerlink" title="查看 release 状态"></a>查看 release 状态</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-35-39.png" alt></p><h3 id="查看所有运行的-release"><a href="#查看所有运行的-release" class="headerlink" title="查看所有运行的 release"></a>查看所有运行的 release</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-36-00.png" alt></p><h3 id="升级-release"><a href="#升级-release" class="headerlink" title="升级 release"></a>升级 release</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-36-18.png" alt></p><h3 id="回滚-release"><a href="#回滚-release" class="headerlink" title="回滚 release"></a>回滚 release</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-36-56.png" alt></p><h3 id="移除一个-release"><a href="#移除一个-release" class="headerlink" title="移除一个 release"></a>移除一个 release</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/helm/2020-06-30-13-37-28.png" alt></p><h2 id="helm-chart-如何开发"><a href="#helm-chart-如何开发" class="headerlink" title="helm chart 如何开发"></a>helm chart 如何开发</h2><p><code>chart</code>的开发是<code>Helm</code>的核心,也比较复杂。<code>Helm</code>使用<code>Go</code>模板对资源文件进行模板化,除了<code>Go</code>附带的几个内置函数,还添加了许多其他函数。</p><h3 id="chart-开发核心特性"><a href="#chart-开发核心特性" class="headerlink" title="chart 开发核心特性"></a>chart 开发核心特性</h3><p>以下列举了<code>helm chart</code>开发中几个核心知识点,这里只简单罗列,如果你需要使用,还是建议参考官方文档。</p><ol><li>内置对象:<code>Release,Values,Chart,Files</code>;</li><li>Values 文件,<code>--set</code>><code>-f customValue</code>> default file;</li><li>模板函数及流水线:对传入 template 的值进行轻度处理,helm 内置了 60 多个常见函数;</li><li>控制语句的使用;</li><li>变量;</li><li>命名模版;</li><li>访问 templates 中的文件;</li><li>帮助文件 NOTS.Txt 如何编写;</li><li>subcharts 与全局变量;</li><li>.helmignore 文件忽略不需要的文件;</li><li>chart 开发如何进行调试:<code>helm lint</code>,<code>helm install --dry-run --debug</code>,<code>helm get manifest</code>;</li></ol><h3 id="开发一个-chart-Demo"><a href="#开发一个-chart-Demo" class="headerlink" title="开发一个 chart Demo"></a>开发一个 chart Demo</h3><p>我们通过一个<code>Go</code>web 程序<code>iim</code>,来演示开发一个 chart 的完整流程。web 应用镜像已经推送到镜像仓中。</p><ol><li>首先在项目中创建 chart 目录,我们使用<code>helm create iim</code>工具生成。其中<code>templates</code>会自动生成 deployment,service,serviceaccount 资源文件,以及自定义帮助文件模板。我们可以移除一些不必要的文件,如 serviceaccount,因为这里不需要使用这些 kubernetes 资源;_ <code>NOTES.txt</code>:关于应用的帮助信息,如访问地址_ <code>_helpers.tpl</code>: 放置 chart 帮助程序的地方,便于后续重用</li><li>修改 deployment.yaml,添加入口程序参数。</li></ol><figure class="highlight yaml"><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="attr">containers:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> { { <span class="string">.Chart.Name</span> } }</span><br><span class="line"> <span class="attr">securityContext:</span> { { <span class="bullet">-</span> <span class="string">toYaml</span> <span class="string">.Values.securityContext</span> <span class="string">|</span> <span class="string">nindent</span> <span class="number">12</span> } }</span><br><span class="line"> <span class="attr">image:</span> <span class="string">"<span class="template-variable">{{ .Values.image.repository }}</span>:<span class="template-variable">{{ .Chart.AppVersion }}</span>"</span></span><br><span class="line"> <span class="attr">imagePullPolicy:</span> { { <span class="string">.Values.image.pullPolicy</span> } }</span><br><span class="line"> <span class="attr">command:</span> [<span class="string">"./iimserver"</span>]</span><br><span class="line"> <span class="attr">args:</span></span><br><span class="line"> [</span><br><span class="line"> <span class="string">"-c"</span>,</span><br><span class="line"> <span class="string">"/app/config.toml"</span>,</span><br><span class="line"> <span class="string">"-m"</span>,</span><br><span class="line"> <span class="string">"/app/model.conf"</span>,</span><br><span class="line"> <span class="string">"-menu"</span>,</span><br><span class="line"> <span class="string">"/app/menu.json"</span>,</span><br><span class="line"> ]</span><br></pre></td></tr></table></figure><ol start="3"><li>修改程序默认配置,即 Value.yaml 文件</li></ol><figure class="highlight yaml"><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="attr">replicaCount:</span> <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="attr">image:</span></span><br><span class="line"> <span class="attr">repository:</span> <span class="string">daocloud.io/daocloud/iim-backend</span></span><br><span class="line"> <span class="attr">pullPolicy:</span> <span class="string">IfNotPresent</span></span><br></pre></td></tr></table></figure><ol start="4"><li>修改程序说明文件,即<code>Chart.yaml</code>文件</li></ol><figure class="highlight yaml"><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="attr">apiVersion:</span> <span class="string">v2</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">iim</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">工业机理模型冲刺项目</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">application</span></span><br><span class="line"><span class="attr">version:</span> <span class="number">0.1</span><span class="number">.0</span></span><br><span class="line"><span class="attr">appVersion:</span> <span class="number">0.2</span><span class="number">.9</span></span><br></pre></td></tr></table></figure><ol start="5"><li>检测<code>Chart</code>是否合法</li></ol><figure class="highlight bash"><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"># 校验</span></span><br><span class="line">helm lint</span><br><span class="line"><span class="comment"># 显示模板渲染结果</span></span><br><span class="line">helm install iim-dmep --dry-run --debug .</span><br></pre></td></tr></table></figure><ol start="6"><li>运行</li></ol><figure class="highlight bash"><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">➜ iim git:(haier-shanghai) ✗ helm install iim .</span><br><span class="line">NAME: iim</span><br><span class="line">LAST DEPLOYED: Thu Feb 13 16:59:25 2020</span><br><span class="line">NAMESPACE: default</span><br><span class="line">STATUS: deployed</span><br><span class="line">REVISION: 1</span><br><span class="line">NOTES:</span><br><span class="line">1. Get the application URL by running these commands:</span><br><span class="line"> <span class="built_in">export</span> POD_NAME=$(kubectl get pods --namespace default -l <span class="string">"app.kubernetes.io/name=iim,app.kubernetes.io/instance=iim"</span> -o jsonpath=<span class="string">"{.items[0].metadata.name}"</span>)</span><br><span class="line"> <span class="built_in">echo</span> <span class="string">"Visit http://127.0.0.1:8080 to use your application"</span></span><br><span class="line"> kubectl --namespace default port-forward <span class="variable">$POD_NAME</span> 8080:80</span><br><span class="line"></span><br><span class="line">➜ iim git:(haier-shanghai) ✗ kubectl get pods</span><br><span class="line">NAME READY STATUS RESTARTS AGE</span><br><span class="line">chartmuseum-chartmuseum-795fb657bd-7h9hw 1/1 Running 0 3h51m</span><br><span class="line">iim-d4567c587-8zzpj 0/1 Running 0 40s</span><br></pre></td></tr></table></figure><h2 id="搭建私有仓库"><a href="#搭建私有仓库" class="headerlink" title="搭建私有仓库"></a>搭建私有仓库</h2><h3 id="定义与方案"><a href="#定义与方案" class="headerlink" title="定义与方案"></a>定义与方案</h3><p><code>a chart repository</code>存储和共享打包好的<code>charts</code>。<a href="https://github.com/helm/charts">官方 chart repository</a>维护了许多常用中间件的<code>chart</code>。本质上,一个<code>chart repo</code>就是一个普通的 http server,其包含一个<code>index.yaml</code>文件和一些<code>packaged chart</code>。<code>index.yaml</code>定义了<code>chart repo</code>的元数据。因此,用户可以使用<code>Google Cloud Storage bucket,Amazon S3 bucket,Github Page</code>去搭建私有仓。</p><p><code>https://example.com/charts</code>这个 chart repo 的布局如下:</p><figure class="highlight text"><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">charts/</span><br><span class="line"> |</span><br><span class="line"> |- index.yaml</span><br><span class="line"> |</span><br><span class="line"> |- alpine-0.1.2.tgz</span><br><span class="line"> |</span><br><span class="line"> |- alpine-0.1.2.tgz.prov</span><br></pre></td></tr></table></figure><p>其中的<code>index.yaml</code>内容如下:</p><figure class="highlight yaml"><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="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">entries:</span></span><br><span class="line"> <span class="attr">alpine:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">created:</span> <span class="number">2016-10-06T16:23:20.499814565-06:00</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">Deploy</span> <span class="string">a</span> <span class="string">basic</span> <span class="string">Alpine</span> <span class="string">Linux</span> <span class="string">pod</span></span><br><span class="line"> <span class="attr">digest:</span> <span class="string">99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd</span></span><br><span class="line"> <span class="attr">home:</span> <span class="string">https://helm.sh/helm</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">alpine</span></span><br><span class="line"> <span class="attr">sources:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://github.com/helm/helm</span></span><br><span class="line"> <span class="attr">urls:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://technosophos.github.io/tscharts/alpine-0.2.0.tgz</span></span><br><span class="line"> <span class="attr">version:</span> <span class="number">0.2</span><span class="number">.0</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">created:</span> <span class="number">2016-10-06T16:23:20.499543808-06:00</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">Deploy</span> <span class="string">a</span> <span class="string">basic</span> <span class="string">Alpine</span> <span class="string">Linux</span> <span class="string">pod</span></span><br><span class="line"> <span class="attr">digest:</span> <span class="string">515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727</span></span><br><span class="line"> <span class="attr">home:</span> <span class="string">https://helm.sh/helm</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">alpine</span></span><br><span class="line"> <span class="attr">sources:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://github.com/helm/helm</span></span><br><span class="line"> <span class="attr">urls:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://technosophos.github.io/tscharts/alpine-0.1.0.tgz</span></span><br><span class="line"> <span class="attr">version:</span> <span class="number">0.1</span><span class="number">.0</span></span><br><span class="line"> <span class="attr">nginx:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">created:</span> <span class="number">2016-10-06T16:23:20.499543808-06:00</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">Create</span> <span class="string">a</span> <span class="string">basic</span> <span class="string">nginx</span> <span class="string">HTTP</span> <span class="string">server</span></span><br><span class="line"> <span class="attr">digest:</span> <span class="string">aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff</span></span><br><span class="line"> <span class="attr">home:</span> <span class="string">https://helm.sh/helm</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line"> <span class="attr">sources:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://github.com/helm/charts</span></span><br><span class="line"> <span class="attr">urls:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">https://technosophos.github.io/tscharts/nginx-1.1.0.tgz</span></span><br><span class="line"> <span class="attr">version:</span> <span class="number">1.1</span><span class="number">.0</span></span><br><span class="line"><span class="attr">generated:</span> <span class="number">2016-10-06T16:23:20.499029981-06:00</span></span><br></pre></td></tr></table></figure><p><code>ChartMuseum</code>是开源的<code>helm chart repository</code>,使用<code>Golang</code>开发,并且对主流的云存储都有支持。其本身也支持容器化部署,提供了丰富的 api,重要的是其支持多租户。</p><figure class="highlight text"><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">charts</span><br><span class="line">├── org1</span><br><span class="line">│ ├── repoa</span><br><span class="line">│ │ └── nginx-ingress-0.9.3.tgz</span><br><span class="line">├── org2</span><br><span class="line">│ ├── repob</span><br><span class="line">│ │ └── chartmuseum-0.4.0.tgz</span><br></pre></td></tr></table></figure><h3 id="搭建chart-repo-demo"><a href="#搭建chart-repo-demo" class="headerlink" title="搭建chart repo demo"></a>搭建<code>chart repo</code> demo</h3><p>我们以<code>chartmusuem</code>为例,介绍如何搭建<code>chart repo</code>,以及如何将我们上面开发好的<code>chart</code>打包发布到我们的仓库中。</p><ol><li>安装并启动 chartmusuem:</li></ol><figure class="highlight bash"><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">curl -LO https://s3.amazonaws.com/chartmuseum/release/latest/bin/linux/amd64/chartmuseum</span><br><span class="line">chmod +x ./chartmuseum</span><br><span class="line">mv ./chartmuseum /usr/<span class="built_in">local</span>/bin</span><br><span class="line"></span><br><span class="line">[root@localhost ~]<span class="comment"># chartmuseum --port=9081 --storage="local" --storage-local-rootdir="/root/charts"</span></span><br><span class="line">2020-02-13T14:30:26.543+0800INFOStarting ChartMuseum{<span class="string">"port"</span>: 9081}</span><br><span class="line"></span><br></pre></td></tr></table></figure><ol start="2"><li>helm 添加 repo:</li></ol><figure class="highlight bash"><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">➜ helmdemo helm repo add chartmuseum http://192.168.5.82:9081</span><br><span class="line">➜ helmdemo helm repo update</span><br><span class="line">Hang tight <span class="keyword">while</span> we grab the latest from your chart repositories...</span><br><span class="line">...Successfully got an update from the <span class="string">"chartmuseum"</span> chart repository</span><br><span class="line">...Successfully got an update from the <span class="string">"incubator"</span> chart repository</span><br></pre></td></tr></table></figure><ol start="3"><li>将开发好的 chart 打包 tgz 文件并且发布到我们的仓库中:</li></ol><figure class="highlight bash"><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">➜ helmdemo create myappone</span><br><span class="line">➜ helmdemo <span class="built_in">cd</span> myappone/</span><br><span class="line">➜ myappone helm lint</span><br><span class="line">==> Linting .</span><br><span class="line">[INFO] Chart.yaml: icon is recommended</span><br><span class="line"></span><br><span class="line">1 chart(s) linted, 0 chart(s) failed</span><br><span class="line">➜ myappone helm package .</span><br><span class="line">Successfully packaged chart and saved it to: /Users/donggang/Documents/Temp/helmdemo/myappone/myappone-0.1.0.tgz</span><br><span class="line">➜ myappone ls</span><br><span class="line">Chart.yaml charts myappone-0.1.0.tgz templates values.yaml</span><br><span class="line">➜ myappone curl -L --data-binary <span class="string">"@myappone-0.1.0.tgz"</span> http://192.168.5.82:9081/api/charts</span><br><span class="line">{<span class="string">"saved"</span>:<span class="literal">true</span>}%</span><br></pre></td></tr></table></figure><ol start="4"><li>其他集群下载使用私有仓库的 chart</li></ol><figure class="highlight bash"><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">➜ myappone helm repo update</span><br><span class="line">➜ myappone helm search repo myappone</span><br><span class="line">NAME CHART VERSIONAPP VERSIONDESCRIPTION</span><br><span class="line">chartmuseum/myappone0.1.0 1.16.0 A Helm chart <span class="keyword">for</span> Kubernetes</span><br></pre></td></tr></table></figure><h2 id="一句话"><a href="#一句话" class="headerlink" title="一句话"></a>一句话</h2><ol><li>kubeapps: 一个<code>web ui</code>工具部署<code>helm chart</code>;</li><li>helm 支持丰富的插件,版本升级工具即通过插件实现的;</li><li>helm 提供了<code>golang sdk</code>;</li><li>目前包括<code>openshift</code>在内的厂商都支持<code>Helm</code>;</li><li><code>lens</code>:一个<code>kubernetes</code>管理看板,支持<code>Helm</code>,其交互设计非常值得学习。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>Helm</code>已经成为事实上<code>kubernetes</code>包管理器的标准,并且也成功从 CNCF 中毕业。<code>Helm</code>能够很好的解决企业使用 kubernetes 所遇到的各种痛点,且新版本的<code>helm</code>架构更加清晰,职责也更加明确。在阅读本文后,相信您也对<code>Helm</code>有了一个初步的认识。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://helm.sh/">官方文档</a></li><li><a href="https://static.sched.com/hosted_files/kccncna19/7c/Helm%20Intro.pdf">kubecon helm introduce</a></li><li><a href="https://static.sched.com/hosted_files/kccncna19/ec/helm3-deep-dive.pdf">kubecon helm deep dive </a></li><li><a href="https://codefresh.io/docs/docs/new-helm/helm-best-practices/#helm-packaging-strategies">codefresh helm 最佳实践</a></li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> Helm </tag>
</tags>
</entry>
<entry>
<title>GO111MODEDULE变量以及Go Module的使用建议</title>
<link href="/2020/03/20/GO111MODULE/"/>
<url>/2020/03/20/GO111MODULE/</url>
<content type="html"><![CDATA[<p>我们可能在很多地方如 README 文件、Makefile 文件以及 Dockerfile 文件中看到<code>GO111MODULE=on</code>,对于刚接触的<code>Golang</code>的开发者可能对此有很多疑惑。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GO111MODULE=on go get -u golang.org/x/tools/gopls@latest</span><br></pre></td></tr></table></figure><p>这片文章,我将详细介绍<code>GO111MODULE</code>变量的意义,以及什么时候需要使用到该变量,同时也总结了一些在使用 Go Modules 时需要注意的细节,帮助你在下次遇到这个变量时不再疑惑。</p><h2 id="从-GOPATH-到-GO111MODULE"><a href="#从-GOPATH-到-GO111MODULE" class="headerlink" title="从 GOPATH 到 GO111MODULE"></a>从 GOPATH 到 GO111MODULE</h2><p>首先,对于 GOPATH,无论是资深开发者和新开发者都不会陌生。GO 在 2009 年发布时,并没有提供包管理器(Package manager),<code>go get</code>会根据导入路径拉取所有的源代码,存储在<code>$GOPATH/src</code>目录中。这种方式使用<code>master</code>分支作为包的稳定版本。可想而知,这种方式缺乏灵活性同时很容易造成版本管理混乱的状态。</p><p><code>Go Modules</code>(旧版本被成为<code>vgo-versiond Go</code>)在 Go 1.11 被正式引入,与 GOPATH 方式存储<code>a signle git checkout of every package</code>的方式不同,Go Module 通过<code>go.mod</code>文件管理包的版本信息,并且能够指定版本号。</p><p>两种包管理方式<code>GOPATH</code>和<code>Go Module</code>同时存在,成为 Go 语言使用最大的坑,后续官方试图通过一个变量:<code>GO111MODULE</code>去解决这个问题,不过随之而来的是更大的灾难。</p><h2 id="GO111MODULE-环境变量"><a href="#GO111MODULE-环境变量" class="headerlink" title="GO111MODULE 环境变量"></a>GO111MODULE 环境变量</h2><p>环境变量<code>GO111MODUEL</code>的作用是控制 Go 包导入的方式,可以设置为<code>on,off,auto</code>三个值,由于该变量的语意会根据 Go 的版本变化,这在很多地方造成严重的歧义。</p><h3 id="Go1-11-和-Go-12-版本-GO111MODULE"><a href="#Go1-11-和-Go-12-版本-GO111MODULE" class="headerlink" title="Go1.11 和 Go.12 版本 GO111MODULE"></a>Go1.11 和 Go.12 版本 GO111MODULE</h3><ul><li><code>GO111MODULE=on</code>无论项目是否在 GOPATH 目录,都会强制使用 Go Modules。</li><li><code>GO111MODULE=off</code>无论项目是否在 GOPATH 目录,都会强制使用 GOPATH。</li><li><code>GO111MODULE=auto</code>是默认模式,当项目不在 GOPATH 中会启用 Go Modules;当项目在 GOPATH 目录会强制使用 GOPATH。</li></ul><p>当你的项目在 GOPATH 目录中,但是你又想使用 Go modules 下载一个指定版本的包或打包时,只能通过 GO111MODULE:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GO111MODULE=on go get github.com/golang/mock/tree/master/mockgen@v1.3.1</span><br></pre></td></tr></table></figure><h3 id="Go1-13-版本-GO111MODULE"><a href="#Go1-13-版本-GO111MODULE" class="headerlink" title="Go1.13 版本 GO111MODULE"></a>Go1.13 版本 GO111MODULE</h3><p>在 Go 1.13 版本,<code>GO111MODULE</code>默认<code>auto</code>的用法发生了该改变:</p><ul><li>如果项目包含<code>go.mod</code> 或者项目不再 GOPATH 目录中(无论是否包含<code>go.mdo</code>文件)都会使用 Go module。所以使用 Go 1.13,你可以将所有项目保存到 GOPATH 中。</li><li>只有在项目保存在<code>GOPATH</code>中同时又不包含文件<code>go.mod</code>文件时,才会使用<code>GOPATH</code>。</li></ul><h3 id="为什么很多地方使用-GO111MODULE"><a href="#为什么很多地方使用-GO111MODULE" class="headerlink" title="为什么很多地方使用 GO111MODULE"></a>为什么很多地方使用 GO111MODULE</h3><p><code>Go Module</code>可以通过<code>git tag</code>控制项目依赖包的版本,而<code>GOPATH</code>默认拉取<code>master</code>分支的最新提交。环境变量<code>GO111MODULE</code>控制了是否启用 Go module。我们则可以<code>GO111MODULE=on go get xxxx/@1.33</code>的方式拉取指定版本的包。</p><figure class="highlight bash"><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">GO111MODULE=on go get -u golang.org/x/tools/gopls@latest</span><br><span class="line">GO111MODULE=on go get -u golang.org/x/tools/gopls@master</span><br><span class="line">GO111MODULE=on go get -u golang.org/x/tools/gopls@v0.1</span><br><span class="line">GO111MODULE=on go get golang.org/x/tools/gopls@v0.1.8</span><br></pre></td></tr></table></figure><p><code>latest</code>标签使用最新版本 git tag。<code>-u</code>选项强制更新,在指定版本是这个选项没有意义。</p><h2 id="使用-Go-Modules-的建议"><a href="#使用-Go-Modules-的建议" class="headerlink" title="使用 Go Modules 的建议"></a>使用 Go Modules 的建议</h2><p>其实在新版本<code>Golang</code>中,如果不接触旧的项目,可以不用关注这个变量意义。这里总结了使用 Go Modules 的一些建议</p><h3 id="记住-go-get-将会更新-go-mod-文件"><a href="#记住-go-get-将会更新-go-mod-文件" class="headerlink" title="记住 go get 将会更新 go.mod 文件"></a>记住 go get 将会更新 go.mod 文件</h3><p><code>go get</code>主要用于下载依赖包和安装二进制文件,但是当项目中存在<code>go.mod</code>文件,使用<code>go get</code>会自动修改<code>go.mod</code>文件。这听起来很奇怪,但这也是 Go modules 的一大亮点:自动维护依赖包版本。</p><p>在一些<code>CICD</code>流程中,我们需要注意这点。比如当使用<code>Jenkins</code>时,其会在 slave 节点<code>workspace</code>目录中上拉取代码,如果需要执行一些脚本命令时,如测试,编译等,如果项目依赖有变化,则<code>go.mod</code>文件会产生修改,这就导致下次执行构建时,产生错误。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/GO111MODULE/2020-06-02-17-04-10.png" alt></p><p>这里提供有两种方式解决,一种方法是添加一个<code>clean stage</code>,丢弃暂存区的修改记录;还有一种通过容器方式进行构建。关于<code>Golang</code>项目<code>CICD</code>流程优化有机会会在后续单独写一篇博客系统介绍,这里只提一下。</p><h3 id="Go-Module-的依赖源来自哪里"><a href="#Go-Module-的依赖源来自哪里" class="headerlink" title="Go Module 的依赖源来自哪里"></a>Go Module 的依赖源来自哪里</h3><p>当使用 Go Modules,<code>go build</code>会使用存储在<code>$GOPATH/pkg/mod</code>下的包,在使用 vim、VScode 等编辑器开发 Golang 项目,可能默认会使用 GOPATH 中的包而不是使用 pkg/mod。</p><p>第二个问题是当我们在测试时需要替换一个包版本时,通常有以下几种做法:</p><p><strong>Solution 1:</strong> 使用<code>go mod vendor + go build -mod=vendor</code>,这将会强制 go 使用 vendor/目录下的包,而不是<code>$GOPATH/pkg/mod</code>中包</p><p><strong>Solution 2:</strong> 在<code>go.mod</code>文件中使用<code>replace</code>。<code>../beers</code>是相关依赖的 copy。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">use replace github.com/maelvls/beers => ../beers</span><br></pre></td></tr></table></figure><h3 id="私有-Go-modules-和-Dockfile"><a href="#私有-Go-modules-和-Dockfile" class="headerlink" title="私有 Go modules 和 Dockfile"></a>私有 Go modules 和 Dockfile</h3><p>在公司中,通常会使用很多私有仓,我们可以使用<code>GOPRIVATE</code>去设置让<code>Go</code>跳过包代理,直接从私有代码仓拉取包。但是但我们使用 docker 构建镜像时,如何拉取私有仓呢,有以下方案</p><ol><li>vendoring: 使用<code>go mod vendor</code>,这样就不要传递私有仓凭证给 docker build context。因为所有依赖都保存在 vendor 目录中,构建时需要使用选项<code>-mod=vendor</code>。</li></ol><ul><li><p>优点:加速 docker 镜像构建,(go mod download 和 docker cache 配合使用也可以优化构建速度)</p><figure class="highlight bash"><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">ENV GOPROXY https://goproxy.io</span><br><span class="line">WORKDIR /workspace</span><br><span class="line"><span class="comment">## All these steps will be cached</span></span><br><span class="line">COPY go.mod go.sum ./</span><br><span class="line"><span class="comment">## Get denpendencies will also be cached if wo don't change go.mod and go.sum</span></span><br><span class="line">RUN go mod download</span><br></pre></td></tr></table></figure></li><li><p>缺点:代码管理复杂,需要跟踪依赖包的更新</p></li></ul><ol start="2"><li>no vendoring: 如果<code>vendor/</code>目录很大(kubernetes vendor 目录大约是 30MB),这种情况使用 vendoring 的方式会很糟糕,这时候可以通过定义变量<code>GITHUB_TOKEN</code>传递给 dockerfile 文件。</li></ol><figure class="highlight bash"><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">git config --global url.<span class="string">"https://foo:<span class="variable">${GITHUB_TOKEN}</span>@github.com/company"</span>.insteadOf <span class="string">"https://github.com/company"</span></span><br><span class="line"><span class="built_in">export</span> GOPRIVATE=github.com/company/\*</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://dev.to/maelvls/why-is-go111module-everywhere-and-everything-about-go-modules-24k">Why is GO111MODULE everywhere, and everything about Go Modules</a></li></ol>]]></content>
<tags>
<tag> Golang </tag>
<tag> Go Module </tag>
</tags>
</entry>
<entry>
<title>Golang什么时候该使用指针</title>
<link href="/2020/03/18/Point-Or-Value/"/>
<url>/2020/03/18/Point-Or-Value/</url>
<content type="html"><![CDATA[<p>Golang 什么时候使用指针(<code>Pointer</code>)?什么时候使用值(<code>Value</code>)?对于<code>go</code>开发者来说是一件头疼的事情,而且这个问题似乎没有绝对的答案,那是否代表我们可以随意使用呢?答案当然是否定的。本文我将试图总结什么场景使用指针更合理。在开始阅读前,建议读者先能够清晰理解 Golang 指针、类型和值等概念。</p><blockquote><p>本文并不是标准更不是唯一答案,而是自己根据使用经验和社区的一些讨论而总结的实践</p></blockquote><p>有下几种情形,我们是否需要考虑使用指针:</p><ol><li>结构体定义的字段</li><li>方法中接受者</li><li>函数传参</li><li>函数和方法返回值</li></ol><p>这里我先给出使用一般准则,后面详细介绍不同场景的细节。</p><ul><li>方法通常使用指针作为接受者, 官方文档的建议是:<strong>如果犹豫,请使用指针</strong>;</li><li>Slices,maps,channels,strings,function value and interface value 这些类型内部通过指针实现,再定一个指针指向这些类型的变量是多余的;</li><li>当一个结构体很复杂或者需要修改结构体使用指针,其他情况使用值,因为滥用指针会出现一些不可预料的情况;</li></ul><h2 id="什么不需要使用指针"><a href="#什么不需要使用指针" class="headerlink" title="什么不需要使用指针"></a>什么不需要使用指针</h2><ol><li><a href="https://github.com/golang/go/wiki/CodeReviewComments"><code>CodeReviewComments</code></a>中建议传输(<code>pass</code>)小型结构体,如<code>type Point struct{ latitude,longtitude float64 }</code>,使用原始类型,除非需要修改它们,理由有以下几点</li></ol><ul><li>值语义可以避免歧义,因为指针类型的赋值;</li><li>牺牲干净的语义换取速度并不是<code>Golang</code>所推荐的做法,有时值传递性能更好,因为值传递能够避免缓存遗漏和队分配;</li></ul><ol start="2"><li>对于切片,不需要使用指针指向它,仍然可以改变其元素,这是因为切片内部是通过指针指向底层数组实现的,标准库中<code>io.Reader.Read(p []byte)</code>函数中即通过传递值类型实现对切片 p 的修改。其实切片也可以当作是小型结构体,其定义如下,在 64 位系统一个切片变量只占用 24 个字节。<strong>同样地,对于 map 和 channel 类型,通常也不需要使用指针</strong>。</li></ol><figure class="highlight golang"><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> SliceHeader <span class="keyword">struct</span> {</span><br><span class="line">Data <span class="keyword">uintptr</span></span><br><span class="line">Len <span class="keyword">int</span></span><br><span class="line">Cap <span class="keyword">int</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="3"><li>对于<code>slices you'll reslice</code>(更改其起始元素位置/长度/容量),如内置函数<code>func append(slice []Type, elems ...Type) []Type</code>,接受一个切片,返回新的切片。返回一个新数组会让调用者更清晰地理解函数的语义。</li></ol><h2 id="什么时候必须使用指针"><a href="#什么时候必须使用指针" class="headerlink" title="什么时候必须使用指针"></a>什么时候必须使用指针</h2><p>对于以下场景,使用指针是必须的:</p><ol><li>如果结构体中包含<code>sync.Mutex</code>获取类似其他同步字段时,由于这类字段类型是禁止拷贝的,所以无论其方法的接受者,还是其作为参数和返回值都应该使用指针:</li></ol><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> FIFO <span class="keyword">struct</span> {</span><br><span class="line">lock sync.RWMutex</span><br><span class="line">cond sync.Cond</span><br><span class="line">items <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">interface</span>{}</span><br><span class="line">queue []<span class="keyword">string</span></span><br><span class="line">populated <span class="keyword">bool</span></span><br><span class="line">initialPopulationCount <span class="keyword">int</span></span><br><span class="line">keyFunc KeyFunc</span><br><span class="line">closed <span class="keyword">bool</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Close the queue.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(f *FIFO)</span> <span class="title">Close</span><span class="params">()</span></span> {</span><br><span class="line">f.lock.Lock()</span><br><span class="line"><span class="keyword">defer</span> f.lock.Unlock()</span><br><span class="line">f.closed = <span class="literal">true</span></span><br><span class="line">f.cond.Broadcast()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewFIFO returns a Store which can be used to queue up items to</span></span><br><span class="line"><span class="comment">// process.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewFIFO</span><span class="params">(keyFunc KeyFunc)</span> *<span class="title">FIFO</span></span> {</span><br><span class="line">f := &FIFO{</span><br><span class="line">items: <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">interface</span>{}{},</span><br><span class="line">queue: []<span class="keyword">string</span>{},</span><br><span class="line">keyFunc: keyFunc,</span><br><span class="line">}</span><br><span class="line">f.cond.L = &f.lock</span><br><span class="line"><span class="keyword">return</span> f</span><br><span class="line">}</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>结构体中,除了需要考虑是否内存的占用之外,还需要考虑结构体的用途,一般主要分为<code>工具结构体</code>和<code>资源结构体</code>,<code>资源结构体</code>很容易理解,主要包括<code>VO,DAO,Entity</code>等,这类结构体一般用于模块或分层之间的通信。对于这类结构体,如用于序列化的结构体,根据序列化协议和库可能有所区别,如<code>Golang</code>默认的<code>Json</code>序列化协议对于是否显示字段,定义为指针可以可好解决。下面是<code>kubernetes IngressSpec</code>的定义。</p><figure class="highlight golang"><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="comment">// IngressSpec describes the Ingress the user wishes to exist.</span></span><br><span class="line"><span class="keyword">type</span> IngressSpec <span class="keyword">struct</span> {</span><br><span class="line">IngressClassName *<span class="keyword">string</span> <span class="string">`json:"ingressClassName,omitempty" protobuf:"bytes,4,opt,name=ingressClassName"`</span></span><br><span class="line">Backend *IngressBackend <span class="string">`json:"backend,omitempty" protobuf:"bytes,1,opt,name=backend"`</span></span><br><span class="line">TLS []IngressTLS <span class="string">`json:"tls,omitempty" protobuf:"bytes,2,rep,name=tls"`</span></span><br><span class="line">Rules []IngressRule <span class="string">`json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们可以看到字段<code>IngressClassName</code>定义为指针类型,就是为了序列化时更方便处理。<code>工具结构体</code>通常指非<code>资源结构体</code>,主要是<code>controller,config,factory</code>和自定义数据结构类型,这些结构体往往更需要考虑内存占用。</p><h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>使用指针并不是总能提升性能。使用指针可以避免值拷贝,减少栈内存的占用,由于堆内存的分配会导致<code>GC</code>频繁地执行,从而降低性能,而值传递则不会。我们通过下面的实例Demo验证。</p><p>以下两个函数分别通过值拷贝和指针共享结构体:</p><script src="https://gist.github.com/donggangcj/8e1ce4cdede2382032caf5a344010907.js"></script><p>分别进行进行基准测试:</p><script src="https://gist.github.com/donggangcj/a4760ce5a2691fc6b3c2525e71c4c452.js"></script><p>执行基准测试,<code>benchstat</code>工具需要下载,链接为<a href="https://pkg.go.dev/mod/golang.org/x/perf"><code>perf</code></a>:</p><figure class="highlight bash"><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">go <span class="built_in">test</span> ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt</span><br><span class="line">go <span class="built_in">test</span> ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt</span><br></pre></td></tr></table></figure><p>测试结果:</p><figure class="highlight text"><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">name time/op</span><br><span class="line">MemoryHeap-4 55.7ns ± 5%</span><br><span class="line"></span><br><span class="line">name alloc/op</span><br><span class="line">MemoryHeap-4 96.0B ± 0%</span><br><span class="line"></span><br><span class="line">name allocs/op</span><br><span class="line">MemoryHeap-4 1.00 ± 0%</span><br><span class="line">---</span><br><span class="line">name time/op</span><br><span class="line">MemoryStack-4 8.37ns ± 9%</span><br><span class="line"></span><br><span class="line">name alloc/op</span><br><span class="line">MemoryStack-4 0.00B</span><br><span class="line"></span><br><span class="line">name allocs/op</span><br><span class="line">MemoryStack-4 0.00</span><br></pre></td></tr></table></figure><p>可以看出,这时候通过值传递的方式执行更快,内存占用也更少。关于如何分析<code>Golang</code>程序和代码段性能,后续我会总结一篇博客单独介绍。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇博文,简单介绍怎样如何使用指针更合理,其实很多场景都没有标准答案,更多的是<code>性能</code>和<code>语义</code>两者的权衡。掌握<code>Golang</code>中类型,值,指针类型等基础概念才能优雅地使用指针。遇到疑惑的参考标准库中的实现和借鉴一些成熟的项目中的实践;</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values">stackoverflow</a></li><li><a href="https://medium.com/a-journey-with-go/go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct-44b43b104963">使用指针还是结构的 Copy</a></li><li><a href="https://medium.com/@vCabbage/go-are-pointers-a-performance-optimization-a95840d3ef85#:~:text=Pointers%20allow%20you%20to%20share,value%20and%20an%20unset%20value.">Go: Are pointers a performance optimization?</a></li></ol>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> golang </tag>
</tags>
</entry>
<entry>
<title>RESTful</title>
<link href="/2020/03/17/RESTful/"/>
<url>/2020/03/17/RESTful/</url>
<content type="html"><![CDATA[<p>接口设计</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><ol><li>资源校验,UI表单分布提交,校验,这里需要提供一些字段校验接口,比如校验template name</li></ol>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> api设计 </tag>
</tags>
</entry>
<entry>
<title>kubernetes1.17安装glusterfs</title>
<link href="/2020/03/08/kubernetes-gluster/"/>
<url>/2020/03/08/kubernetes-gluster/</url>
<content type="html"><![CDATA[<p>本文介绍了如何使用在最新kubernetes版本上安装<code>glusterfs</code>。官方源仓库<a href="https://github.com/gluster/gluster-kubernetes">master分支</a>相关脚本无法部署成功,主要是由于kubernetes的版本发生了变化,一些beta版本的资源升至稳定版本,所以一些模板yaml文件需要修改,这些修改很多在issus中能够发现,目前还未合并到主分支,仍然需要手动修改。所以我Fork了源仓库,合并了这些这些修改。代码仓库地址为:<a href="git@github.com:donggangcj/gluster-kubernetes.git">gluster-kubernetes</a>。同时还将镜像源改为国内镜像源,镜像源使用国内<code>daocloud.io/daocloud</code>下的镜像。</p><h2 id="安装前"><a href="#安装前" class="headerlink" title="安装前"></a>安装前</h2><p>以下原因可能会导致安装失败,安装前请先检查。</p><ol><li><p>glusterfs server和glusterfs client版本需要尽量保持一直,由于镜像中的glusterfs server版本较低,为<code>7.1</code>,所以客户端不能够安装最新版本,centos7 yum版本回退操作如下:</p> <figure class="highlight bash"><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="comment"># 获取事务ID</span></span><br><span class="line">yum <span class="built_in">history</span> info glusterfs-fuse </span><br><span class="line"><span class="comment"># 删除所有依赖</span></span><br><span class="line">yum <span class="built_in">history</span> undo <span class="variable">${事务ID}</span></span><br><span class="line"><span class="comment"># 安装7.1版本客户端</span></span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-libs-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-client-xlators-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-fuse-7.1-1.el7.x86_64.rpm</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li><p>国内需要注意时区问题,可能导致挂载失败;</p></li><li><p>设备不能包含任何数据;</p></li><li><p>需要加载必要的内核模块;</p></li></ol><h2 id="在每个节点上执行"><a href="#在每个节点上执行" class="headerlink" title="在每个节点上执行"></a>在每个节点上执行</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">fdisk -l</span><br><span class="line"><span class="comment"># 查看结果中是否有未被使用的磁盘</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 检查和加载内核模块</span></span><br><span class="line">lsmod | grep dm_snapshot || modprobe dm_snapshot</span><br><span class="line">lsmod | grep dm_mirror || modprobe dm_mirror</span><br><span class="line">lsmod | grep dm_thin_pool || modprobe dm_thin_pool</span><br><span class="line"><span class="comment"># 检查加载是否成功</span></span><br><span class="line">lsmod | egrep <span class="string">'^(dm_snapshot|dm_mirror|dm_thin_pool)'</span></span><br><span class="line"><span class="comment"># 输出内容</span></span><br><span class="line"><span class="comment"># dm_thin_pool 66358 0 </span></span><br><span class="line"><span class="comment"># dm_snapshot 39103 0 </span></span><br><span class="line"><span class="comment"># dm_mirror 22289 0 </span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装mount.glusterfs命令</span></span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-libs-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-client-xlators-7.1-1.el7.x86_64.rpm</span><br><span class="line">yum install -y https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-7/glusterfs-fuse-7.1-1.el7.x86_64.rpm</span><br><span class="line"><span class="comment"># 默认安装的是glusterfs 3.12,为了和下面gk-deploy脚本里面安装的版本一致,手动安装7.1版本</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看glusterfs版本</span></span><br><span class="line">glusterfs --version</span><br><span class="line">glusterfs 7.1</span><br><span class="line"></span><br><span class="line">mount.glusterfs -V</span><br><span class="line">glusterfs 7.1</span><br></pre></td></tr></table></figure><h2 id="在主节点上执行"><a href="#在主节点上执行" class="headerlink" title="在主节点上执行"></a>在主节点上执行</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># step 1. 下载安装文件</span></span><br><span class="line"><span class="comment"># 以下测试使用的,最新commit是:</span></span><br><span class="line"><span class="comment"># Latest commit</span></span><br><span class="line"><span class="comment"># ec38822</span></span><br><span class="line"><span class="comment"># 请酌情来判断是否需要执行</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/donggangcj/gluster-kubernetes.git</span><br><span class="line"><span class="built_in">cd</span> gluster-kubernetes/deploy</span><br><span class="line"></span><br><span class="line"><span class="comment"># step 2. 准备topology文件</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="comment"># - hostsnames.manage里面填写节点的hostname</span></span><br><span class="line"><span class="comment"># - hostnames.storage里面填写节点的ip</span></span><br><span class="line"><span class="comment"># - devices里面填写磁盘的名称</span></span><br><span class="line"><span class="comment"># ***************************</span></span><br><span class="line">cat << EOF > topology.json</span><br><span class="line">{</span><br><span class="line"> <span class="string">"clusters"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="string">"nodes"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="string">"node"</span>: {</span><br><span class="line"> <span class="string">"hostnames"</span>: {</span><br><span class="line"> <span class="string">"manage"</span>: [</span><br><span class="line"> <span class="string">"node01"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="string">"storage"</span>: [</span><br><span class="line"> <span class="string">"<span class="variable">${IP_LIST['node01']}</span>"</span></span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"zone"</span>: 1</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"devices"</span>: [</span><br><span class="line"> <span class="string">"/dev/sdb"</span></span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="string">"node"</span>: {</span><br><span class="line"> <span class="string">"hostnames"</span>: {</span><br><span class="line"> <span class="string">"manage"</span>: [</span><br><span class="line"> <span class="string">"node02"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="string">"storage"</span>: [</span><br><span class="line"> <span class="string">"<span class="variable">${IP_LIST['node02']}</span>"</span></span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"zone"</span>: 1</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"devices"</span>: [</span><br><span class="line"> <span class="string">"/dev/sdb"</span></span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="string">"node"</span>: {</span><br><span class="line"> <span class="string">"hostnames"</span>: {</span><br><span class="line"> <span class="string">"manage"</span>: [</span><br><span class="line"> <span class="string">"node03"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="string">"storage"</span>: [</span><br><span class="line"> <span class="string">"<span class="variable">${IP_LIST['node03']}</span>"</span></span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"zone"</span>: 1</span><br><span class="line"> },</span><br><span class="line"> <span class="string">"devices"</span>: [</span><br><span class="line"> <span class="string">"/dev/sdb"</span></span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line">}</span><br><span class="line">EOF</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装前确认节点都ready状态</span></span><br><span class="line">kubectl get nodes</span><br><span class="line"></span><br><span class="line"><span class="comment"># step 2. 部署heketi and GlusterFS</span></span><br><span class="line">ADMIN_KEY=adminkey</span><br><span class="line">USER_KEY=userkey</span><br><span class="line">./gk-deploy -g -y -v --admin-key <span class="variable">${ADMIN_KEY}</span> --user-key <span class="variable">${USER_KEY}</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="comment"># ./gk-deploy -g --abort --admin-key adminkey --user-key userkey</span></span><br><span class="line"><span class="comment"># 查看lv名称</span></span><br><span class="line"><span class="comment"># lvs</span></span><br><span class="line"><span class="comment"># 删除lv</span></span><br><span class="line"><span class="comment"># lvremove /dev/vg</span></span><br><span class="line"><span class="comment"># 清除磁盘(在节点机器上执行)</span></span><br><span class="line"><span class="comment"># wipefs -a /dev/sdc</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># step 3. 检查heketi和glusterfs运行情况</span></span><br><span class="line"><span class="built_in">export</span> HEKETI_CLI_SERVER=$(kubectl get svc/heketi --template <span class="string">'http://{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}'</span>)</span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$HEKETI_CLI_SERVER</span></span><br><span class="line">curl <span class="variable">$HEKETI_CLI_SERVER</span>/hello</span><br><span class="line"><span class="comment"># Hello from Heketi</span></span><br><span class="line"><span class="comment"># 如果timeout的话,看看是不是master没搞成node节点,没加入kube-proxy</span></span><br><span class="line"><span class="comment"># 可以获取到地址之后,到node节点上执行curl操作</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># step 4. 创建storageclass,来自动为pvc创建pv</span></span><br><span class="line">SECRET_KEY=`<span class="built_in">echo</span> -n <span class="string">"<span class="variable">${ADMIN_KEY}</span>"</span> | base64`</span><br><span class="line"></span><br><span class="line">cat << EOF | kubectl apply -f -</span><br><span class="line">apiVersion: v1</span><br><span class="line">kind: Secret</span><br><span class="line">metadata:</span><br><span class="line"> name: heketi-secret</span><br><span class="line"> namespace: default</span><br><span class="line">data:</span><br><span class="line"> <span class="comment"># base64 encoded password. E.g.: echo -n "mypassword" | base64</span></span><br><span class="line"> key: <span class="variable">${SECRET_KEY}</span></span><br><span class="line"><span class="built_in">type</span>: kubernetes.io/glusterfs</span><br><span class="line">EOF</span><br><span class="line"></span><br><span class="line">cat << EOF | kubectl apply -f -</span><br><span class="line">apiVersion: storage.k8s.io/v1</span><br><span class="line">kind: StorageClass</span><br><span class="line">metadata:</span><br><span class="line"> name: glusterfs-storage</span><br><span class="line">provisioner: kubernetes.io/glusterfs</span><br><span class="line">parameters:</span><br><span class="line"> resturl: <span class="string">"<span class="variable">${HEKETI_CLI_SERVER}</span>"</span></span><br><span class="line"> restuser: <span class="string">"admin"</span></span><br><span class="line"> secretNamespace: <span class="string">"default"</span></span><br><span class="line"> secretName: <span class="string">"heketi-secret"</span></span><br><span class="line"> volumetype: <span class="string">"replicate:3"</span></span><br><span class="line">EOF</span><br><span class="line"><span class="comment"># 注意生成pvc资源时,需要指定storageclass为上面配置的"glusterfs-storage"</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">kubectl get nodes,pods</span><br><span class="line">NAME STATUS ROLES AGE VERSION</span><br><span class="line">node/node01 Ready <none> 5d3h v1.17.0</span><br><span class="line">node/node02 Ready <none> 5d3h v1.17.0</span><br><span class="line">node/node03 Ready <none> 5d3h v1.17.0</span><br><span class="line"></span><br><span class="line">NAME READY STATUS RESTARTS AGE</span><br><span class="line">pod/glusterfs-bhprz 1/1 Running 0 45m</span><br><span class="line">pod/glusterfs-jt64n 1/1 Running 0 45m</span><br><span class="line">pod/glusterfs-vkfp5 1/1 Running 0 45m</span><br><span class="line">pod/heketi-779bc95979-272qk 1/1 Running 0 38m</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>上述仓库后续不会继续维护,相关安装参考还是以官方仓库为准。</p>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> kubernetes </tag>
<tag> kubernetes存储 </tag>
</tags>
</entry>
<entry>
<title>2019-summary</title>
<link href="/2020/01/15/2019-summary/"/>
<url>/2020/01/15/2019-summary/</url>
<content type="html"><![CDATA[<p>一年的时间,眨眼就过去了!</p><h2 id="Make-easier"><a href="#Make-easier" class="headerlink" title="Make easier"></a>Make easier</h2><p>工程师的追求是<code>让事情变得更简单!</code>。将复杂度扔给工程师。</p><p>简单的设计</p><p>简单的</p><h2 id="相信过程并坚持下去"><a href="#相信过程并坚持下去" class="headerlink" title="相信过程并坚持下去"></a>相信过程并坚持下去</h2><p>《Atomic Habits》里面,有一个很有趣的过程理论:1% worse everyday for one year:0.99³⁶⁵ = 0.031% better everyday for one year:1.01³⁶⁵ = 37.78</p>]]></content>
<categories>
<category> 生活 </category>
</categories>
<tags>
<tag> 年度总结 </tag>
</tags>
</entry>
<entry>
<title>kubernetes-api-conventions</title>
<link href="/2019/12/17/kubernetes-api-conventions/"/>
<url>/2019/12/17/kubernetes-api-conventions/</url>
<content type="html"><![CDATA[<p>本文介绍kubernetes的API设计,主要面向想要深入了解kubernetes API结构的用户。</p><p>kubernetes API采用RESTful风格(客户端create,update,delet,get对象通过标准的HTTP verbs),并且这些API优先选择JSON。同时kubernetes也暴露一些不是标准HTTP verb的接口和使用其他的content类型。服务端接收和发送的JSON数据都有对应schema( <a href="https://json-schema.org/">JSON schema</a>),kind和apiVersion这两个字段确定唯一的schema。</p><p>术语定义:</p><ol><li>kind:Object schema的名称</li><li>Resource: 获取Object的特定URL(小写),所以Resource比如<code>/api/v1/pods</code>Resource可以获取v1 Pod Object列表,这类Resource也被称为Collections;<code>/api/v1/namespace/<namespace-name>/pods/<pod-name></code> resource可以获取具体的v1 Pod Object,这类resource也被称为Elements。</li><li>API Group: 一组绑定的resource,和版本号一起通过字段<code>GROUP/VERSION</code>,例:policy.k8s.io/v1</li><li>Object: <code>group+version+kinds</code></li></ol><p>注意:</p><ol><li>一个Resource只能获取对应唯一kind的数据,而一个kind可以从多个Resource获取数据。例如kind <code>Pod</code>可以从<code>/api/v1/namespaces/<namespace-name>/pods/<pod-name></code>和<code>/api/v1/namespaces/<namespace-name>/pods/<pod-name>/status</code>。</li></ol><h2 id="Types-kinds"><a href="#Types-kinds" class="headerlink" title="Types(kinds)"></a>Types(kinds)</h2><p>Kinds有三种:</p><ol><li>Object</li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> kubernetes </tag>
<tag> kubernetes平台开发者 </tag>
</tags>
</entry>
<entry>
<title>Docker-container详解</title>
<link href="/2019/10/30/docker-container/"/>
<url>/2019/10/30/docker-container/</url>
<content type="html"><![CDATA[<p>本文介绍docker中的contanier,基于操作系统介绍什么是container。</p><h2 id="进程(Processes"><a href="#进程(Processes" class="headerlink" title="进程(Processes)"></a>进程(Processes)</h2><p>Container是具有指定配置的Linux进程(processes)。我们进行如下操作:</p><figure class="highlight bash"><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><br><span class="line"><span class="comment">## 启动一个redis进程</span></span><br><span class="line">➜ ~ docker run -d --name=db redis:alpine</span><br><span class="line"></span><br><span class="line"><span class="comment">## 显示容器运行的进程</span></span><br><span class="line">➜ ~ docker top db</span><br><span class="line">PID USER TIME COMMAND</span><br><span class="line">2288 999 0:00 redis-server</span><br><span class="line"></span><br><span class="line"><span class="comment">## 查看系统是否存在redis-server进程</span></span><br><span class="line">➜ ~ ps aux | grep redis-server</span><br><span class="line">donggang 10761 0.0 0.0 4258876 200 s007 R+ 6:57下午 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn redis-server</span><br><span class="line"></span><br><span class="line"><span class="comment">## 查看dockerd的进程树</span></span><br><span class="line">➜ ~ pstree -c -p -A $(pgrep dockerd)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在linux系统中,一个container对应一个标准的进程。所有的container进程都是dockerd进程生成的。Linux是基于文件系统的,进程的信息都存储在<code>/proc</code>目录中。每个进程对应一个目录,可以通过进程ID获得对应的目录位置。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">## 设置DBPID变量,方便后续测试</span></span><br><span class="line">➜ ~ DBPID=$(pgrep redis-server)</span><br><span class="line">➜ ~ <span class="built_in">echo</span> Redis is <span class="variable">$DBPID</span></span><br><span class="line">Redis is 6950</span><br><span class="line"><span class="comment">## 找到对应的进程目录文件,该容器相关配置都保存在该目录下</span></span><br><span class="line">➜ ~ ls /proc/<span class="variable">$DBPID</span></span><br><span class="line">attr cmdline environ io mem ns pagemap <span class="built_in">sched</span> stack task</span><br><span class="line">autogroup comm exe limits mountinfo numa_maps patch_state schedstat <span class="built_in">stat</span> timers</span><br><span class="line">auxv coredump_filter fd loginuid mounts oom_adj personality sessionid statm uid_map</span><br><span class="line">cgroup cpuset fdinfo map_files mountstats oom_score projid_map setgroups status wchan</span><br><span class="line">clear_refs cwd gid_map maps net oom_score_adj root smaps syscall</span><br><span class="line"></span><br><span class="line"><span class="comment">## 通过文件获取容器db的目录</span></span><br><span class="line">➜ ~ cat /proc/<span class="variable">$DBPID</span>/environ</span><br><span class="line">HOSTNAME=a1f8a38c84d4SHLVL=2REDIS_DOWNLOAD_SHA=6624841267e142c5d5d5be292d705f8fb6070677687c5aad1645421a936d22b3HOME=/home/redisPATH=/usr/<span class="built_in">local</span>/sbin:/usr/<span class="built_in">local</span>/bin:/usr/sbin:/usr/bin:/sbin:/binREDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.6.tar.gzREDIS_VERSION=5.0.6PWD=/data<span class="comment"># </span></span><br><span class="line"><span class="comment">## 通过docker api获取,</span></span><br><span class="line">➜ ~ docker <span class="built_in">exec</span> -it db env</span><br><span class="line">PATH=/usr/<span class="built_in">local</span>/sbin:/usr/<span class="built_in">local</span>/bin:/usr/sbin:/usr/bin:/sbin:/bin</span><br><span class="line">HOSTNAME=a1f8a38c84d4</span><br><span class="line">TERM=xterm</span><br><span class="line">REDIS_VERSION=5.0.6</span><br><span class="line">REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.6.tar.gz</span><br><span class="line">REDIS_DOWNLOAD_SHA=6624841267e142c5d5d5be292d705f8fb6070677687c5aad1645421a936d22b3</span><br><span class="line">HOME=/root</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="命名空间(namespace)"><a href="#命名空间(namespace)" class="headerlink" title="命名空间(namespace)"></a>命名空间(namespace)</h2><p>容器的一个基础部分就是namespace,namespace限制某个进程只能获取系统的某些部分,比如网络接口或这进程。当一个容器开始运行时,容器运行时(container runtime)比如docker将会创建namespace,容器对应的进程将在这个namespace中运行,</p><p>有以下种类的namespace: Mount(mnt)、Process ID(pid)、Network(net)、Interprocess Communication(ipc)、UTC(hostnames)、UserId(user)、Control group(cgroup)。有关Linux的namspace介绍移步于<a href="https://en.wikipedia.org/wiki/Linux_namespaces">https://en.wikipedia.org/wiki/Linux_namespaces</a></p><p>我们可以通过工具<code>unshare</code>创建一个隔离沙箱。使其中执行的进程不共享父进程的Pid等namespace。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">## bash运行在"沙箱中"</span></span><br><span class="line">➜ ~ sudo unshare --fork --pid --mount-proc bash</span><br><span class="line"></span><br><span class="line"><span class="comment">## 打印进程信息</span></span><br><span class="line">[root@master ~]<span class="comment"># ps</span></span><br><span class="line"> PID TTY TIME CMD</span><br><span class="line"> 1 pts/0 00:00:00 bash</span><br><span class="line"> 12 pts/0 00:00:00 ps</span><br><span class="line">[root@master ~]<span class="comment"># exit</span></span><br><span class="line"><span class="comment">## 退出沙箱</span></span><br><span class="line"><span class="built_in">exit</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 打印进程信息</span></span><br><span class="line">➜ ~ ps</span><br><span class="line"> PID TTY TIME CMD</span><br><span class="line"> 4673 pts/0 00:00:00 zsh</span><br><span class="line"> 9088 pts/0 00:00:00 ps</span><br><span class="line">16860 pts/0 00:00:00 zsh</span><br><span class="line">18119 pts/0 00:00:00 zsh</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Namespace本质上仍然是文件,它存储在磁盘中,所以Namespace是可以共享的。工具nsenter可以在指定的namespace运行一个程序。</p><figure class="highlight bash"><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="comment">## 获取容器db的namespace资源</span></span><br><span class="line">➜ ~ ls -lha /proc/<span class="variable">$DBPID</span>/ns</span><br><span class="line">总用量 0</span><br><span class="line">dr-x--x--x 2 polkitd 1000 0 10月 30 19:17 .</span><br><span class="line">dr-xr-xr-x 9 polkitd 1000 0 10月 30 19:17 ..</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:18 ipc -> ipc:[4026533033]</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:18 mnt -> mnt:[4026533031]</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:17 net -> net:[4026533036]</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:18 pid -> pid:[4026533034]</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:18 user -> user:[4026531837]</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:18 uts -> uts:[4026533032]</span><br><span class="line"></span><br><span class="line"><span class="comment">## 在container db中执行ps程序</span></span><br><span class="line">➜ ns nsenter --target <span class="variable">$DBPID</span> -m -u -i -p /bin/ps aux</span><br><span class="line">PID USER TIME COMMAND</span><br><span class="line"> 1 redis 1:03 redis-server</span><br><span class="line"> 37 root 0:00 /bin/ps aux</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>docker可以通过语法<code>container:<container-name></code>共享namespace,下面的例子中我们创建一个web容器,该容器网络共享db container的网络namespace。</p><figure class="highlight bash"><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">➜ ns docker run -d --name=web --net=container:db nginx:alpine</span><br><span class="line">Unable to find image <span class="string">'nginx:alpine'</span> locally</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">89d9c30c1d48: Already exists</span><br><span class="line">110ad692b782: Pull complete</span><br><span class="line">Digest: sha256:085e84650dbe56f27ca3ed00063a12d5b486e40c3d16d83c4e6c2aad1e4045ab</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> nginx:alpine</span><br><span class="line">4f982d745f346a7087c42c55e455448d21794641df7005c4d76299f781ba474f</span><br><span class="line">➜ ns WEBPID=$(pgrep nginx | tail -n1)</span><br><span class="line">➜ ns <span class="built_in">echo</span> nginx is <span class="variable">$WEBPID</span></span><br><span class="line">nginx is 21499</span><br><span class="line">➜ ns ls -alh /proc/<span class="variable">$WEBPID</span>/ns | grep net</span><br><span class="line">lrwxrwxrwx 1 101 101 0 10月 31 09:19 net -> net:[4026533036]</span><br><span class="line">➜ ns ls -alh /proc/<span class="variable">$DBPID</span>/ns | grep net</span><br><span class="line">lrwxrwxrwx 1 polkitd 1000 0 10月 30 19:17 net -> net:[4026533036]</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="Chroot"><a href="#Chroot" class="headerlink" title="Chroot"></a>Chroot</h2><p>Chroot能够是容器进程拥有独立的根目录(root directory<code>/</code>),<code>chroot --help</code>能够更改当前运行的进程及其子进程的根目录。 <strong>chroot不能够起到隔离的作用,仍然可以通过相对路径访问新根以外的地址</strong></p><h2 id="Cgroups-Control-Groups"><a href="#Cgroups-Control-Groups" class="headerlink" title="Cgroups(Control Groups)"></a>Cgroups(Control Groups)</h2><p>CGroups技术限制进程消耗资源,</p><h2 id="Secomp-AppArmor"><a href="#Secomp-AppArmor" class="headerlink" title="Secomp/AppArmor"></a>Secomp/AppArmor</h2><p>AppArmor描述了进程可以访问系统中哪些部分。Seccomp提供了限制可以进行哪些系统调用,阻止安装内核模块或更改文件权限等方面的功能。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://medium.com/faun/the-missing-introduction-to-containerization-de1fbb73efc5">Docker容器技术详解</a></li></ol>]]></content>
<categories>
<category> 云原生 </category>
</categories>
<tags>
<tag> docker </tag>
</tags>
</entry>
<entry>
<title>Asciinema终端录屏&Hexo文章目录管理</title>
<link href="/2019/10/28/blog-day3/"/>
<url>/2019/10/28/blog-day3/</url>
<content type="html"><![CDATA[<p>本文介绍了近期我在使用<code>Hexo</code>的两个技巧。一是使用<code>Asciinema</code>来录制终端Demo。并在Hexo博客中显示。二是如何使用目录来管理<code>Hexo</code>文章,当博客越来越多的时候,这将十分必要,当然我并不是博客太多才去分目录管理,主要是因为强迫症🤪。</p><h2 id="Asciinema"><a href="#Asciinema" class="headerlink" title="Asciinema"></a>Asciinema</h2><p>Asciinme能够解决Hexo博文中显示多行命令行代码块不太友好的显示方式,同时也能更直观的阐述命令的输入和输出。</p><script type="text/javascript" src="https://asciinema.org/a/NqBW1ccowOjMx8AK4HGKBIOyr.js" id="asciicast-NqBW1ccowOjMx8AK4HGKBIOyr" async></script><h3 id="如何使用"><a href="#如何使用" class="headerlink" title="如何使用"></a>如何使用</h3><ol><li>注册Asciinema, <a href="https://asciinema.org/docs/getting-started">官网</a>。为注册用户提供一个仓库。</li><li>下载客户端工具;<figure class="highlight bash"><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><br><span class="line"><span class="comment"># mac下homebrew安装</span></span><br><span class="line">brew install ascillnema</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li>使用<code>asciinema auth</code>登录–><code>asciinema rec</code>开始记录–><code>CTRL+D</code>或者输入<code>exit</code>退出。完成之后会输出对应的链接。具体配置参考<a href="https://asciinema.org/docs/getting-started">官方教程</a></li></ol><script type="text/javascript" src="https://asciinema.org/a/QP1uQuLTbaMBzLRj3KRAJel6y.js" id="asciicast-QP1uQuLTbaMBzLRj3KRAJel6y" async></script><ol start="4"><li><p>安装Hexo插件,支持Asciinema的渲染;</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save hexo-tag-asciinema</span><br></pre></td></tr></table></figure></li><li><p>Vscode设置Markdown的Snippet,方便编写:</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">"Asciinema":{</span><br><span class="line">"prefix": "asciinema",</span><br><span class="line">"body": "{% asciinema ${1:code}%}",</span><br><span class="line">"description": "Insert asciinema link"</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol><p>设置完,就可以酷炫的显示终端录制了。这里有一点需要注意,目前我们博文与第三方工具绑定的很紧密,会不方便博客的迁移等工作。后续迁移肯定是项大工程。</p><h2 id="使用目录管理日益增多的博文"><a href="#使用目录管理日益增多的博文" class="headerlink" title="使用目录管理日益增多的博文"></a>使用目录管理日益增多的博文</h2><p>当博文的数量很大时,一会导致目录结构混乱,同时也不方便管理和检索。</p><p>默认<code>hexo new post [title]</code>将会在<code>post</code>目录下创建,<code>hexo generate</code>生成的静态文件则是<code>:year/:month/:day/:title.html</code>。我们来看配置文件中的两个配置项:</p><figure class="highlight yml"><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="comment"># post生成的文件名</span></span><br><span class="line"><span class="attr">new_post_name:</span> <span class="string">:title.md</span> <span class="comment"># File name of new posts</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 静态文件生成规则</span></span><br><span class="line"><span class="attr">permalink:</span> <span class="string">:year/:month/:day/:title/</span></span><br></pre></td></tr></table></figure><p><code>:variable</code>是hexo中的变量表达形式,hexo内置了一些变量,参考 <a href="https://hexo.io/zh-cn/docs/variables">hexo变量</a>。修改配置文件:</p><figure class="highlight yml"><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="comment"># post生成的文件名</span></span><br><span class="line"><span class="attr">new_post_name:</span> <span class="string">:year/:title.md</span> <span class="comment"># File name of new posts</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 静态文件生成规则</span></span><br><span class="line"><span class="attr">permalink:</span> <span class="string">:year/:month/:day/:title/</span></span><br></pre></td></tr></table></figure><p>可以根据自身需求进行配置,由于笔者目前没有太多的博客,以年份归档就足够了。对于之前的文章如果需要迁移,参考以下脚本:</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入_post目录</span></span><br><span class="line"><span class="built_in">cd</span> <span class="built_in">source</span>/_post</span><br><span class="line"></span><br><span class="line"><span class="comment"># 修改文章内容,添加permalink</span></span><br><span class="line"><span class="keyword">for</span> file <span class="keyword">in</span> `ls`; <span class="keyword">do</span> link=`<span class="built_in">echo</span> <span class="variable">$file</span>|sed -n <span class="string">'s/\.md//p'</span>`; sed -i <span class="string">""</span> <span class="string">"/title.*/ a\ </span></span><br><span class="line"><span class="string">permalink: <span class="variable">$link</span></span></span><br><span class="line"><span class="string">"</span> <span class="variable">$file</span>; <span class="keyword">done</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 修改文章的创建时间为Front-matter中的时间</span></span><br><span class="line"><span class="keyword">for</span> file <span class="keyword">in</span> `find . -<span class="built_in">type</span> f`; <span class="keyword">do</span> d=`head <span class="variable">$file</span>|sed -n <span class="string">'s/^date: //p'</span> |sed -n <span class="string">'s/[- :]//pg'</span> |sed -n <span class="string">'s/\([0-9]\{2\}\)$/\.\1/p'</span>`; <span class="built_in">echo</span> <span class="variable">$d</span> ; <span class="keyword">done</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建一个2018年1月1日的文件,用于过滤出2018年的文章</span></span><br><span class="line">touch -t 201801010000 timestamp</span><br><span class="line"><span class="comment"># 创建2018文件夹</span></span><br><span class="line">mkdir 2018</span><br><span class="line"><span class="comment"># 将2018年的文章移到2018文件夹下</span></span><br><span class="line"><span class="keyword">for</span> file <span class="keyword">in</span> `find . -<span class="built_in">type</span> f -newer timestamp`; <span class="keyword">do</span> mv <span class="variable">$file</span> 2018 ; <span class="keyword">done</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 将timestamp创建时间修改为2017年1月1日,然后参考2018年文章归类方法,这里略</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 完成归类之后删除刚才创建的用于过滤的文件</span></span><br><span class="line">rm timestamp</span><br><span class="line"></span><br><span class="line"><span class="comment"># 最后如果想再恢复文章的创建时间,再执行第三步命令</span></span><br><span class="line"><span class="keyword">for</span> file <span class="keyword">in</span> `find . -<span class="built_in">type</span> f`; <span class="keyword">do</span> d=`head <span class="variable">$file</span>|sed -n <span class="string">'s/^date: //p'</span> |sed -n <span class="string">'s/[- :]//pg'</span> |sed -n <span class="string">'s/\([0-9]\{2\}\)$/\.\1/p'</span>`; <span class="built_in">echo</span> <span class="variable">$d</span> ; <span class="keyword">done</span>;</span><br></pre></td></tr></table></figure><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://blog.csdn.net/maosidiaoxian/article/details/85220394">如何在Hexo中对文章md文件分类</a></p>]]></content>
<categories>
<category> 博客 </category>
</categories>
<tags>
<tag> hexo </tag>
</tags>
</entry>
<entry>
<title>Spring事务&分布式事务&单服务处理多数据源事务</title>
<link href="/2019/09/11/transaction/"/>
<url>/2019/09/11/transaction/</url>
<content type="html"><![CDATA[<p>本文以一个实际业务问题来谈谈事务该如何处理。对接外部系统是是不可避免的,从广泛意义上来说,外部系统范围很大,中间件(数据库)也属于外部系统。当我们讨论事务时,通常我们将那些没有支持事务的系统称为外部系统,业务系统基本上都是外部系统。</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>有这样一套系统,以<code>gitlab</code>为底层系统, 在<code>gitlab project</code>的基础上封装了代码仓,系统对其中一些与gitlab关联的数据进行了落表。创建代码仓的逻辑过程比较复杂,首先通过gitlab创建代码仓(其中返回的代码仓id需要落库),系统表<code>code_repo</code>插入记录,创建gitlab<code>hook</code>,系统表<code>member</code>插入记录(维护系统用户和对应代码仓的关系),创建多个gitlab<code>branch</code>(系统业务,初始化代码仓可以初始化多个分支),系统表<code>label</code>插入记录。</p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/transaction/2019-09-10-15-11-36.png" alt></p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>以上大概说明整个业务流程, gitlab接入的方式采用<code>HTTP</code>,不采用直接对接gitlab数据库的原因是,其创建项目和分支业务逻辑复杂,梳理代价很大。对接任何系统其实本质上可以概括为两种:</p><table><thead><tr><th>对接方式</th><th>优势</th><th>缺点</th></tr></thead><tbody><tr><td>上游系统提供的接口</td><td>不需要关注上游系统的自身义务逻辑,维护只需要关注接口版本</td><td>灵活性较差</td></tr><tr><td>上游系统底层存储(数据库,文件等)</td><td>需要梳理和维护上游系统业务逻辑,维护是代码层面的</td><td>灵活性、可控性很好</td></tr></tbody></table><p>,数据库采用<code>mysql</code>。在不考虑异常的情况,整个业务流程还是比较清晰的,但是分布式的核心就是处理各种异常情况,这也是分布式复杂的地方。因为分布式的网络环境是很复杂的。我们很保证底层系统的可用性。在这里,当gitlab其中的接口出现异常,系统仍落库显然是不合理的,同样的当数据库发生异常,gitlab数据存在也是不合理的,分布式事务的核心要解决数据一致性。</p><p>解决还需要与当前的项目架构不能冲突。目前各服务还没有拆分,运行。数据库也没有拆分。其架构仍是单体架构模式,设计时考虑了后期的微服务拆分。目前主要问题是解决<code>Gitlab</code>和<code>DB</code>之间的数据一致性,其复杂度没有微服务多DB的高。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/transaction/2019-09-10-16-25-59.png" alt></p><h2 id="分布式事务(浅谈)"><a href="#分布式事务(浅谈)" class="headerlink" title="分布式事务(浅谈)"></a>分布式事务(浅谈)</h2><p>事务的概念来自数据库事务,在数据库事务定义中,事务是一个<code>执行的逻辑单元</code>,它需要提供一个一致、可靠的数据操作,它主要包括下面两个目标:</p><ol><li>当出现任何错误,包括系统宕机、部分失败,都能保证左右的数据修改都能够恢复到未修改的状态;</li><li>不通的事务并发处理相同的数据时,提供适当的隔离机制。我们常说的ACID,其实是某些数据库特有的事务实现方式,也就是实现了原子性、一致性、隔离性和持久性。dan分布式事务,一直是实现分布式系统过程中最大的挑战,在单个数据源的系统中,只要该数据源支持事务,例如大部分关系型数据库,一些MQ服务(<code>redis,mysql,activeMQ</code>等),我们实现事务相对容易。那么我们如何在分布式系统中实现事务呢?这就要从分布式系统的原则说起,目前主要有<code>BASE原理 ACP原理</code>。其中ACP是:</li><li>A:可用性(Availability)</li><li>B:一致性(Consistency)</li><li>C:分区容错性(Tolerance of network Partition)ruan zhuang t爱A和P没什么好说的,就是分布式系统的基本特性,C(一致性)指在分布式系统中,多个节点之间的数据的一致性,包括一个节点修改的数据,通过另一个节点访问的时候也能够看到;以及当一个操作需要修改多个数据源的数据,多个修改都能够同时完成或者同时不完成。</li></ol><p>这里的一致性,我们可以看作是数据库事务的ACID特性中,原子性、一致性甚至是隔离性的统一。如果以ACID标准实现分布式系统,在现实中是不可能的。其中原子性就没法实现,如果一个业务请求,要修改多个数据库中的数据,那么这多个数据库操作,就无法实现原子性,势必会有一个先后,这一点点时间就违反了原子性。</p><p>所以,我们往往无法在分布式系统中实现完全的一致性,所以就有了BASE理论,BASE是BasicallAvailable(基本可用),SOftstate(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,要求实现最终一致性即可。</p><p>其中,SoftState(软状态)是指,在一个业务操作过程中,允许出现一个中间状态,也就是软状态,而不是要求原子性那样,要么完成要么不完成。例如在下单的时候,出现一个<code>正在处理</code>状态。由于有这个软状态,那我的一致性就不是要求抢一致性,而是最终一致性,也就是说,只要最终这个请求能够处理完,所有数据状态都是处理完的状态;如果期间出错了,所有的数据也都一致,该失败的失败,该退钱的退钱,该重置的重置。 确定分布式系统的实现原则是最终一致性以后,同时也明确了我们实现分布式事务的原则,也是最终一致性。其实,不管数据库事务的ACID特性,还是分布式事务的最终一致性,都是根据事务的定义和它的两个目标,所采用的不通的实现方式。</p><p>那我们该如何实现这个最终一致性呢?y俄都是</p><h3 id="单服务的分布式事务"><a href="#单服务的分布式事务" class="headerlink" title="单服务的分布式事务"></a>单服务的分布式事务</h3><p>首先,任何一个分布式系统,总是由一个个的系统组成,也就是一个个的服务,这些服务也可以部署多个实例。同时,我们整个系统也需要一定的方式相互通信。我们采用接口的方式,也可以通过MQ之类的消息中间件通信。但是无论怎样,分布式系统会涉及多个数据源。</p><p>对于这种每个服务访问多个数据源的情况,其实就是一个最简单的分布式事务的场景。如果大家在网上搜“Spring分布式事务实现”,搜索到结果也就是在说这个场景下的分布式事务实现过程。要实现这个事务,首先需要对Spring的事务机制有一定的了解。对于这种情况,最简单的就是使用Springde JTA事务管理,但是我们知道。JTAs事务管理是通过两阶段提交实现的,在很多情况下,他的效率是很低的。因为它在多个数据源秀修改数据的时候,这些数据一直处在被锁的状态,直到多个数据源的事务都提交完成,才会释放。</p><p>如果不用JTA,Spring也给我们提供了几种方式,来近似的实现分布式事务,例如:</p><ol><li>事务同步,也就是提交一个事务的时候,通过Listener等方式通知另一个事务也提交。但是这种情况下,如果第二个事务提交出错了,第一个事务就无法回滚了,因为它已经提交完成了</li><li>链式事务,也就是将多个事务,包装在一个链式事务管理器中,当提交事务的时候,一次提交里面的事务,对于这种情况,也存在上面所说的问题</li></ol><p>所以,使用spring在单服务多数据源的情况下,实现分布式事务,实际上没办法完全实现事务的,因为出错的时候不能够保证。那么这时候,就需要通过其他机制来补充。比如重试,自己处理错误和异常。<br>大家可以试想一下,分布式系统越复杂,它出错的情况就越多,我们需要考虑的补救措施就越多。这种修修补补的实现分布式事务的最终一致性的做法,始终不是一个好做法。但是,使用Spring解决单服务的分布式系统,始终是分布式事务实现的基础。我们可以用其他的模式来方便我们解决分布式事务,但是在每个服务中,我们还是要经常使用事务同步、链式事务等来实现事务。我们用Spring来保证绝大数情况下的事务问题,而对于特殊的错误情况,就采用其他的模式来解决。</p><h3 id="分布式事务实现的模式"><a href="#分布式事务实现的模式" class="headerlink" title="分布式事务实现的模式"></a>分布式事务实现的模式</h3><p>刚才说了我们使用其他模式来处理分布式事务问题,主要有下面五种模式,参考<a href="https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/distributed-transaction.md"> 分布式事务参考 </a>:</p><ol><li>XA方案</li><li>TCC方案</li><li>本地消息表</li><li>可靠消息最终一直方案</li><li>最大努力通知方案</li></ol><h2 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h2><p>由于目前是单DB,可以尽最大限度利用单DB事务特性,<a href="https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html">Spring事务管理</a>中介绍了有两种事务管理方式(programmatically and declarative )。虽然官方文档建议使用申明式事务管理方式,但是我们在回滚数据库事务需要执行其他操作时,如API的反操作,手动方式才能够实现。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">resolvePosition</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// some business logic...</span></span><br><span class="line"> } <span class="keyword">catch</span> (NoProductInStockException ex) {</span><br><span class="line"> <span class="comment">// trigger rollback programmatically</span></span><br><span class="line"> TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>CoderepoService</code>中提供的创建代码仓接口<code>createCodeRepo</code>,主要包含①②③④操作,其中只有一个DB操作④,使用数据库事务无法满足情况。例如当①②执行成功,③执行失败,这是通过异常事务回滚无法回滚Gitlab中的数据。所以必须手动回滚,处理逻辑如下。我们Spring数据库事务,保证DB操作,当③④失败时手动捕获异常(回滚gitlab数据),并抛出系统业务异常(使事务生效)。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transaction</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> ServiceTSingleResponse<CodeRepoDTO> <span class="title">add</span><span class="params">(CodeRepoSaveRequest request)</span> throw Exception e </span>{</span><br><span class="line"> ①创建gitlab项目;</span><br><span class="line"> ②DB插入数据;</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> ③创建gitlab hook;</span><br><span class="line"> ④创建多个分支</span><br><span class="line"> }<span class="keyword">catch</span>(Exception e){</span><br><span class="line"> deleteGitlabProject(projectId);</span><br><span class="line"> TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); </span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> BaseException();</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>这里涉及的Gitlab操作对应一个回滚操作<code>deleteGitlabProject()</code>,DB通过事务回滚。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文讨论问题类似单服务多DB场景,不同的是gitlab数据与通是保证通过<code>HTTP</code>调用的,并没有实现事务。如何保证DB与第三方系统数据保持一致性的问题,尽量较小的代码改动。在微服务多数据源的情况下不能够满足需求,需要根据业务选型。</p>]]></content>
<tags>
<tag> spring </tag>
<tag> 事务 </tag>
</tags>
</entry>
<entry>
<title>Spring Boot中的并发处理</title>
<link href="/2019/08/30/springboot-concurrency/"/>
<url>/2019/08/30/springboot-concurrency/</url>
<content type="html"><![CDATA[<p>在使用<code>Spring Boot</code>开发过程中,并发<code>concurrency</code>问题不可避免。很多开发者存在这样的误区,使用<code>Servlets</code>为每个请求分配一个新的线程进行处理就不再需要并发处理了。我将在这篇文章中介绍如何在<code>Spring Boot</code>中处理并发并且如何避免一些问题。</p><h2 id="Spring-Boot-并发基础"><a href="#Spring-Boot-并发基础" class="headerlink" title="Spring Boot 并发基础"></a>Spring Boot 并发基础</h2><p>有以下几点特别值得注意: </p><ol><li>最大线程数:这是为处理应用程序的请求而分配的最大线程数。</li><li>共享资源:调用共享资源如数据库</li><li>异步方法调用:这些方法调用将会释放线程资源</li><li>内部共享资源:内部资源调用如缓存、共享应用程序状态</li></ol><p>接下来我们逐一介绍如何处理这些场景</p><h3 id="Spring-Boot应用程序的最大线程数量"><a href="#Spring-Boot应用程序的最大线程数量" class="headerlink" title="Spring Boot应用程序的最大线程数量"></a>Spring Boot应用程序的最大线程数量</h3><p>首先我们必须限制应用程序的线程数量。如果使用默认内嵌的<code>Tomcat Server</code>,我们可以通过<code>server.tomcat.max-threads</code>变量修改线程数量限制。默认为<code>200</code>。我们可以通过修改此配置以更合理利用硬件资源。</p><h3 id="共享外部资源"><a href="#共享外部资源" class="headerlink" title="共享外部资源"></a>共享外部资源</h3><p>调用数据库或者第三方<code>Restful</code>接口可能需要很长时间。</p><h3 id="异步方法调用"><a href="#异步方法调用" class="headerlink" title="异步方法调用"></a>异步方法调用</h3><p>我们可能遇到一个请求会调用多个服务。比如一次请求调用<code>Service A、B、C</code>,你肯定不想这样调用:</p><pre><code>Call service A -> Waiting response from Service A -> call service B -> Watiting ... -> Compose response from A B C</code></pre><p>每个服务调用花费三秒,整个请求处理过程将会花费9秒。如果通过下面的做法肯定会更好。</p><pre><code>Call service A Call serviec B -> Waiting response from Service A B C -> compose reponse from A B C Call serviec C</code></pre><p>这样,显然我们只需要3秒响应。异步和响应式微服务十分有趣,可以参考其他文章。这里我们只关注<code>Spring boot</code>。</p><h2 id="Spring-Boot中异步调用"><a href="#Spring-Boot中异步调用" class="headerlink" title="Spring Boot中异步调用"></a>Spring Boot中异步调用</h2><p>在<code>Spring Boot</code>中使用注解<code>@EnabelAsync</code>注解开启异步支持。使用<code>@Async</code>将返回<code>CompletableFuture<></code> 。这些异步方法将会在后台线程中执行。如果合理使用异步执行,可以避免等待时间。</p><h2 id="共享内部资源"><a href="#共享内部资源" class="headerlink" title="共享内部资源"></a>共享内部资源</h2><p>以上我们讨论了我们无法控制的外部资源,对于系统内部资源我们应该避免共享他们。<code>Spring Service and Controller</code>都是单例模式,我们需要十分小心,当状态改变时,你需要立刻处理。共享状态的其他潜在来源是高速缓存和自定义服务器范围的组件(通常是监视,安全性等)。如果你必须使用共享状态资源,下面是我的建议:</p><ol><li>处理不可变对象。如果对象是不可变的,则可以避免许多与并发相关的问题。如果你需要改变一些东西 - 只需创建一个新对象。</li><li>并非所有集合都是线程安全的。一个常见的陷阱是使用HashMap,假设它是线程安全的(它不是。如果你需要并发访问,请使用ConcurrentHashMap,HashTable或其他线程安全的解决方案。)。</li><li>不要假设第三方库是线程安全的。大多数代码都没有,并且必须控制对共享状态的访问。</li><li>如果你要依赖它 - 学习正确的并发性。我真的建议在实践中获得Java Concurrency的副本。写于2006年,但在2018年仍然非常相关。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Spring中的并发和多线程是重要的主题。在本文中,我想强调在编写Spring Boot应用程序时需要注意的关键领域。如果您想在构建高要求,高质量的服务时取得成功,您需要围绕这一主题做出有意识的决策和权衡。我希望通过这篇文章你知道如何开始</p>]]></content>
<tags>
<tag> Spring Boot </tag>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>Hexo博客访问优化</title>
<link href="/2019/07/09/blog-day2/"/>
<url>/2019/07/09/blog-day2/</url>
<content type="html"><![CDATA[<p>通过<code>Hexo&Gitpage</code>方案部署的博客系统,访问速度确实很慢。<code>一个网站如果在4秒内没有读取出来,大多数访客会选择离开</code>。</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>首先需要排查出页面加载速度慢的原因,这里我们结合<code>Chrome开发者工具</code>和开源的在线网站测试(模拟不同地区和终端)进行测试。</p><h3 id="chrome分析结果"><a href="#chrome分析结果" class="headerlink" title="chrome分析结果"></a>chrome分析结果</h3><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/2019-07-09-15-53-12.png" alt="GitPage加载分析"></p><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/2019-07-09-16-19-36.png" alt="本地加载"></p><p>通过比较<code>Gitpage</code>和<code>本地</code>访问情况,可以看出<code>DomcontentLoad</code>加载时间没有多少差距,那当然是请求资源<code>load</code>耗时比较长,其中一张4.5M的图片资源加载了3分钟。Chrome Devtool`相关介绍可以参考文档资料。</p><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><ol><li><p>图片压缩,加载一张图片速度是很慢</p></li><li><p>静态资源使用CDN加速</p></li><li><p>图片使用<code>JsDelivs CDN</code>加速,每次需要四个步骤,显然是反人类的,思考了一番,暂时通过一下方式解决,后续可以将所有操作步骤整合到命令工具中。</p><pre><code>复制博客所在目录图片到CDN仓库目录 --> push到远程分支 --> release --> 修改博客文件图片引用地址 python命令行工具 --> git命令 --> 开源release-it --> 修改paste Image配置</code></pre></li></ol><h2 id="优化后页面记载情况"><a href="#优化后页面记载情况" class="headerlink" title="优化后页面记载情况"></a>优化后页面记载情况</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/blog-day2/2019-07-11-09-11-29.png" alt></p><p>可以看出,通过上述优化配置,图片资源加载速度提升很多。</p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><ol><li>浏览器输入框输入URL,浏览器到底为我们做了什么?</li><li>python命令行工具可不可以使用<code>shell script</code>替换。</li><li>页面资源请求如何发起?(当页面滚到图片位置,开始发起请求)</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://zhuanlan.zhihu.com/p/47697445">Chrome开发者工具-知乎</a></li><li><a href="https://developers.google.com/web/tools/chrome-devtools/network/reference#timing-explanation">Chrome分析官方文档</a></li></ol>]]></content>
<categories>
<category> 博客 </category>
</categories>
<tags>
<tag> hexo </tag>
<tag> chrome devtool </tag>
</tags>
</entry>
<entry>
<title>优雅的打印日志</title>
<link href="/2019/07/05/howtolog/"/>
<url>/2019/07/05/howtolog/</url>
<content type="html"><![CDATA[<p>日志是很多开发者在开发过程中很容易忽略,开发过程中开发者可以通过IDE或其他调试工具,。在软件的生命周期中,</p><h2 id="分析丑陋的日志"><a href="#分析丑陋的日志" class="headerlink" title="分析丑陋的日志"></a>分析丑陋的日志</h2><figure class="highlight txt"><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></pre></td><td class="code"><pre><span class="line">2019-07-05 09:49:53k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:49:53,257 INFO [com.cosmo.portal.interceptor.TokenInterceptor] - AUTH:x-usc-token:5354d67c-d2b1-42b0-8160-d7040a0fd1eb clusterId:qingdao</span><br><span class="line"></span><br><span class="line">2019-07-05 09:49:53k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:49:53,258 INFO [com.cosmo.portal.service.impl.RedisTokenManager] - 用户名null</span><br><span class="line"></span><br><span class="line">2019-07-05 09:49:53k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:49:53,258 INFO [com.cosmo.portal.service.impl.RedisTokenManager] - 刷新token失败</span><br><span class="line"></span><br><span class="line">2019-07-05 09:49:53k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:49:53,258 ERROR [com.cosmo.portal.exception.GlobalExceptionHandler] - 业务逻辑异常:【Token不存在或已失效,请重新登录】</span><br><span class="line"></span><br><span class="line">2019-07-05 09:49:53k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:49:53,259 WARN [org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver] - Resolved [com.cosmo.portal.exception.BusniessException: Token不存在或已失效,请重新登录]</span><br><span class="line"></span><br><span class="line">2019-07-05 09:50:46k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:50:46,384 INFO [com.cosmo.portal.interceptor.TokenInterceptor] - AUTH:x-usc-token:5354d67c-d2b1-42b0-8160-d7040a0fd1eb clusterId:qingdao</span><br><span class="line"></span><br><span class="line">2019-07-05 09:50:46k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:50:46,385 INFO [com.cosmo.portal.service.impl.RedisTokenManager] - 用户名null</span><br><span class="line"></span><br><span class="line">2019-07-05 09:50:46k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:50:46,385 INFO [com.cosmo.portal.service.impl.RedisTokenManager] - 刷新token失败</span><br><span class="line"></span><br><span class="line">2019-07-05 09:50:46k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:50:46,385 ERROR [com.cosmo.portal.exception.GlobalExceptionHandler] - 业务逻辑异常:【Token不存在或已失效,请重新登录】</span><br><span class="line"></span><br><span class="line">2019-07-05 09:50:46k8s_cosmo-portal-console-app_cosmo-portal-console-app-6d7fc78d7b-29rw5_default_c8beb189-9e38-11e9-b1a0-0242ac120005_02019-07-05 09:50:46,386 WARN [org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver] - Resolved [com.cosmo.portal.exception.BusniessException: Token不存在或已失效,请重新登录]</span><br></pre></td></tr></table></figure><p>上面是一个<code>SpringBoot Web</code>应用输出的日志,该应用通过容器方式部署在k8s集群中。这些日志输出存在一下严重缺陷。</p><ol><li>出现<code>Error</code>级别日志,日志级别滥用。该级别日志属于系统级,当出现该级别日志时系统也已经出现一定级别的不可用;</li><li>多次请求日志并行输出,且没有明确的标识;</li><li></li></ol>]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> log </tag>
</tags>
</entry>
<entry>
<title>Hexo博客框架&Butterfly主题&Vscode&Paste Image插件</title>
<link href="/2019/07/04/blog-day1/"/>
<url>/2019/07/04/blog-day1/</url>
<content type="html"><![CDATA[<h2 id="Hexo"><a href="#Hexo" class="headerlink" title="Hexo"></a>Hexo</h2><p><a href="https://hexo.io/zh-cn/">Hexo</a>是一款快速、简洁且高校的博客框架,有以下亮点:</p><ol><li><p>Markdown编辑模式;</p></li><li><p>静态,可以使用<code>GitPage</code>部署;</p></li><li><p>基于<code>CLI</code>优雅的管理模式;</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean && hexo s</span><br></pre></td></tr></table></figure></li><li><p>主题酷炫。</p></li></ol><h2 id="为什么选择Butterlfy主题"><a href="#为什么选择Butterlfy主题" class="headerlink" title="为什么选择Butterlfy主题"></a>为什么选择<code>Butterlfy</code>主题</h2><p><strong>看图:</strong><code>Never put off till tomorrow what you can do today</code>一语胜千言,警醒自己。</p><!-- <img src="/2019/07/04/blog-day1/2019-07-04-17-39-53.png" class="" title="This is an example image"> --><!-- ![](blog-day1/2019-07-04-17-39-53.png) --><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/2019-07-04-17-39-53.png" alt></p><h2 id="博客图片管理"><a href="#博客图片管理" class="headerlink" title="博客图片管理"></a>博客图片管理</h2><p>本人使用的是<code>VSCode</code>文本编辑器,改编辑器插件体系丰富,同时对<code>VIM</code>的兼容良好。<code>Paste Image</code>这款插件能够过从系统剪切板(clipboard)复制图片到<code>Markdown</code>或其他类型文件。</p><ul><li><p><code>VsCode</code>下载插件</p></li><li><p>打开<code>VsCode</code>设置页面,设置插件,<code>${currentFileDir}/${currentFileNameWithoutExt}</code>,该变量与<code>hexo</code>生成的文件夹保持统一 <img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/2019-07-04-18-40-19.png" alt></p></li><li><p><code>hexo</code>设置新建文章时同时创建资源目录</p> <figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">post_asset_folder:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure></li><li></li></ul><h2 id="缩略图"><a href="#缩略图" class="headerlink" title="缩略图"></a>缩略图</h2><p><img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/2019-07-05-00-07-20.png" alt></p><p> 缩略图测试使用路径应用不生效,使用<code>jsdelivr+github</code>方案解决。后期优化统一管理。</p><p><strong>不足:</strong></p><ol><li><p>维护比较麻烦,也就是分布式开发中<code>一致性</code>的问题</p></li><li><p>使用<code>Paste Image</code>插件的复制命令默认使用的是<code>Markdown</code>语法。而<code>Hexo</code>设置<code>post_asset_folder</code>为<code>true</code>后,<code>Markdown</code>中不能使用原引用图片语法,必须使用标签插件引用相对路径。(安装插件<code>npm install https://github.com/CodeFalling/hexo-asset-image --save</code>)</p> <figure class="highlight text"><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">{% asset_path slug %}</span><br><span class="line">{% asset_img slug [title] %}</span><br><span class="line">{% asset_link slug [title] %}</span><br></pre></td></tr></table></figure></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="https://docs.jerryc.me/"><code>butterfly</code>作者的博客</a></li><li><a href="http://www.xinxiaoyang.com/programming/2016-11-25-hexo-image-bug/">Hexo post_asset_folder设置无法使用Markdown引用语法</a></li></ol>]]></content>
<categories>
<category> 博客 </category>
</categories>
<tags>
<tag> hexo </tag>
</tags>
</entry>
<entry>
<title>python之旅</title>
<link href="/2018/07/04/python/"/>
<url>/2018/07/04/python/</url>
<content type="html"><![CDATA[<h2 id="入坑-Python"><a href="#入坑-Python" class="headerlink" title="入坑 Python"></a>入坑 Python</h2><p>自从进入公司,到现在也有半年的时间。这半年的时间从 python 到入门到开发了几个小项目,类型涵盖了 <code>web应用 程序、爬虫程序(scrapy),python脚本工具,自动化工具</code>。对 python 语言也越来越熟悉,当然也有所感悟和总结。首先 Python 真的让语言成了一个工具,入门代价很小,上手能够开发出小工具,可以更快体验到编程的乐趣。但是做到<code>pythonic</code> 需要更多的学习和使用。</p><p>Python 可以胜任各种需求,而且对框架的依赖度不高,多熟悉 python 标准库以及常用第三方库,可以很快构建出<code>解决方案</code>。</p><h2 id="关于编辑器的选择以及工作流构建"><a href="#关于编辑器的选择以及工作流构建" class="headerlink" title="关于编辑器的选择以及工作流构建"></a>关于编辑器的选择以及工作流构建</h2><p>从踏上编程这条路,就一直纠结与编辑器(IDE)的选择,几乎所有主流的文本编辑器和编辑器都尝试了一遍,包括 Sublime Text、VS Code、SpaceEmacs(Emacs 的改进版)、终端 Vim、编辑器 MacVim、Pycharm 等可以说,一半的精力都在琢磨各种编辑器了,至于孰优孰劣就不在这里讨论了,各种博客一大堆,也可以看出不止我一个人执着于此。我也不会评判“执着与编辑器”的对错与否,不过如果有精力,那去玩一遍?只有不断实践,才能够找到自己得心应手的工具吧。</p><p>编辑器的选择本质上是对不断调整工作流,这工作流可以是纯终端,通过终端工具构建一整套命令行工作流,可能这真的很<code>Hack</code>,当然也可以直接选择如 Pycharm 之类的 IDE,用别人一整套工具集也是一种不错的选择。</p><p>目前 Sublime Text 以及 SpaceEmacs 已经基本被我抛弃了,编辑器 MacVim 也是偶尔臭美用一下。python 开发主要使用 <code>pycharm</code>,小脚本也会通过 Vim 编辑,前端或者其他文本操作都靠 VScode。不过这里要提一下的是,pycharm 我采用的 vim 的插件,主要编辑模式就是 Vim 模式。</p><p>虽然这三个工具还没完全吃透,但如果没有特殊的情况,这应该这就是我的工作流。不过我建议的话,我觉得就是自己的 <code>pycharm+VScode+vim</code>的工作流了,无论纯开发还是 DevOps 都能轻松驾驭。</p><h2 id="关于代码开发规范"><a href="#关于代码开发规范" class="headerlink" title="关于代码开发规范"></a>关于代码开发规范</h2><p>代码规范,python 是动态语言,在模块/包正确导入的前提下,它允许任意组织你的代码结构。而鄙人深受框架所害,如 spring 以及 spring boot 等分层框架,觉得那样的代码看着都神清气爽,能给他分开,就给它分开!看着必须清爽,这是我的原则。python 的 代码规范以及工程结构没办法统一,不过还是可以参照一下几个指导,并在实践中不断践行它。</p><ul><li>PEP8(Python Enhancement Proposals/python 增强建议书):官方规范,由于过于啰嗦我也没有完整读过🤣。</li><li>Google python 开发规范:大厂出品,必属精品!主要内容包括python语言开发一些建议,包括如何组织你的导入,字符串处理时如何选择使用‘+’号还是字符串格式化还是通过 john,如何注释等,很短可以反复看。</li><li>The Hitchhiker’s Guide to Python:如何让你的 python 代码更加优美,提高代码的可读性。必须读一遍。</li></ul><h2 id="行动起来"><a href="#行动起来" class="headerlink" title="行动起来"></a>行动起来</h2><p>编码能力提高还是要通过不断的编码才能提高,切勿纸上谈兵。对于刚入门的新手,由于无法从项目上手,所以可以通过一些专业的代码练习网站快速上手,当然国内如<code>leetcode</code>,<code>牛客</code>很多人都很熟悉,但是这些网站往往偏向的是算法、数据结构,而不面向单个语言,通过这类网站练习,往往效率很高。</p><p>这里推荐<code>HackerRank</code>,这个网站的交互和设计很棒,支持<code>VIM</code>编辑模式,最重要的是通过这个网站<code>python</code>专题的练习,你能更加领会到<code>python</code>这门语言特有的思想,而不需要花费太大经历在算法上。如果你需要算法的练习,那你也可以试试算法专题练习。<img src="https://cdn.jsdelivr.net/gh/donggangcj/CDN/image/python/2020-05-28-16-33-33.png" alt></p><h2 id="遇到问题如何处理"><a href="#遇到问题如何处理" class="headerlink" title="遇到问题如何处理"></a>遇到问题如何处理</h2><p>代码报错,怎么办?百度、Google、文档、笔记…这些都是解决方案,没有优劣之分,遇到国内源以及编码问题时,百度才是王道。不过需要认真考虑和总结的是,应该遵循一个怎样的流程去处理?上来直接啃官方文档,没错你的问题 99%会得以解决,不过通过文档搜索,对于现在的很 low 的我效率还是很低。所以我会不断改进自己处理问题的流程。 现在基本上是这个流程:首先回顾笔记—-Google(百度)—–文档。 可能我对笔记和文档会更加重视,虽然这并没有错,不过还是有一定的弊端,比如遇到一个问题就去系统的看它的文档,然后做记录,看着看着又遇到一个问题,于是又去看它的文档…最后场景的就是,chrome 打开一大堆页面,舍不得关闭,自己一开始的问题都忘了。这种由于缺乏目的性的学习,效率会很低。</p>]]></content>
<categories>
<category> python </category>
</categories>
<tags>
<tag> python </tag>
<tag> workflow </tag>
</tags>
</entry>
</search>