-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsearch.xml
3273 lines (2740 loc) · 518 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
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[【Mini AI Agent】如何用 100 行代码构建一个最小智能体?]]></title>
<url>http://emacoo.cn/AI/mini-gpt-agent/</url>
<content type="html"><![CDATA[<blockquote>
<p>今年 3 月份,知名人工智能科学家吴恩达(Andrew Ng)在社交平台 X 上发帖提到,<strong>“我认为 AI 代理工作流程将在今年推动 AI 的大规模进步——甚至可能比下一代基础模型还要多……GPT-3.5(零样本)的正确率为 48.1%,GPT-4(零样本)为 67.0%,而在智能体循环中,GPT-3.5 的正确率高达 95.1%”</strong>。此贴发出之后,引发了业界广泛关注。有人表示,这代表着 AI 发展中的范式转变。</p>
<p><img src="andrew-ng.png" alt></p>
<p>本文首先对智能体(AI Agent)的概念做一个简单介绍,然后详细拆解一个仅用 100 行代码构建的极简智能体应用。</p>
</blockquote>
<h2 id="1-什么是智能体?"><a href="#1-什么是智能体?" class="headerlink" title="1 什么是智能体?"></a>1 什么是智能体?</h2><p><strong>智能体(AI Agent)</strong>是一种超越简单文本生成的人工智能系统,它使用大语言模型(LLM)作为其核心计算引擎,使其能够进行对话、推理、执行任务,展现一定程度的自主性。</p>
<p>在智能体架构中,核心功能可以归纳为三个步骤的循环:<strong>感知-决策-行动</strong>。智能体首先通过感知机制收集环境信息,然后基于该信息和预设目标,通过决策机制制定行动计划,最终通过动作执行机制实施这些计划。</p>
<p><img src="ai-agent.png" alt></p>
<p><em>图:智能体架构示意</em></p>
<h2 id="2-示例:Mini-AI-Agent"><a href="#2-示例:Mini-AI-Agent" class="headerlink" title="2 示例:Mini AI Agent"></a>2 示例:Mini AI Agent</h2><p>了解了智能体的概念,接下来我们一起一步步拆解一个仅用 100 行代码构建的最小智能体应用,耗时约 1 个小时。</p>
<h3 id="2-1-效果演示"><a href="#2-1-效果演示" class="headerlink" title="2.1 效果演示"></a>2.1 效果演示</h3><p>先来看一下效果演示,</p>
<p><img src="ai-agent-demo.gif" alt></p>
<p><em>图:Mini AI Agent应用演示</em></p>
<p>看似平淡无奇的两次问答,实际上已经体现了智能体的核心循环:<strong>感知-决策-行动</strong>。</p>
<ul>
<li><strong>感知</strong>:接收问题</li>
<li><strong>决策</strong>:理解问题,确定目标,然后通过推理决定使用何种工具(即制定计划)</li>
<li><strong>行动</strong>:使用工具获取信息,然后生成答案</li>
</ul>
<h3 id="2-2-环境准备"><a href="#2-2-环境准备" class="headerlink" title="2.2 环境准备"></a>2.2 环境准备</h3><h4 id="申请账号:百度千帆"><a href="#申请账号:百度千帆" class="headerlink" title="申请账号:百度千帆"></a>申请账号:百度千帆</h4><ol>
<li>访问<a href="https://cloud.baidu.com/product/wenxinworkshop" target="_blank" rel="noopener">百度智能云千帆</a>,注册账号并登录<a href="https://console.bce.baidu.com/qianfan/overview" target="_blank" rel="noopener">千帆大模型控制台</a></li>
<li>打开模型服务-应用接入页面,创建应用,记下 API Key 和 Secret Key 备用</li>
<li>打开模型服务-在线服务页面,找到 ERNIE-3.5-8K(支持函数功能),开通付费(不用担心,非常便宜,100 次调用才 2 毛钱)</li>
</ol>
<p>PS: 本文只是以百度千帆为例,大家可以根据自身经验,替换成其他任何支持函数功能的大模型,比如智谱清言、Azure、OpenAI等。</p>
<h4 id="搭建本地开发环境:Jupyter-Notebook"><a href="#搭建本地开发环境:Jupyter-Notebook" class="headerlink" title="搭建本地开发环境:Jupyter Notebook"></a>搭建本地开发环境:Jupyter Notebook</h4><ol>
<li>访问 <a href="https://www.anaconda.com/download/success" target="_blank" rel="noopener">Anaconda 官网</a>,下载安装包并安装 Anaconda</li>
<li>命令行运行 <code>conda install jupyter notebook</code>,安装 Jupyter Notebook</li>
<li>打开 GitHub 示例项目 <a href="https://github.com/emac/langchain-samples" target="_blank" rel="noopener">emac/langchain-samples</a>,git clone 到本地</li>
<li>命令行打开 langchain-samples 目录,运行 <code>jupyter notebook</code>,打开 Jupyter Notebook</li>
</ol>
<h3 id="2-3-程序拆解"><a href="#2-3-程序拆解" class="headerlink" title="2.3 程序拆解"></a>2.3 程序拆解</h3><p>准备好环境之后,就可以进入程序员最喜欢的实操环节了!</p>
<h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">!pip install langchain langchain-community langchain-core gradio</span><br></pre></td></tr></table></figure>
<p>依赖说明:</p>
<ul>
<li><a href="https://github.com/langchain-ai/langchain" target="_blank" rel="noopener">langchain</a>: 最著名的开发大语言模型应用的开源框架,没有之一</li>
<li><a href="https://www.gradio.app/" target="_blank" rel="noopener">gradio</a>: 一个用于快速构建机器学习模型的交互式 Web 应用的 Python 库</li>
</ul>
<h4 id="初始化大模型"><a href="#初始化大模型" class="headerlink" title="初始化大模型"></a>初始化大模型</h4><figure class="highlight python"><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="keyword">from</span> langchain_community.llms <span class="keyword">import</span> QianfanLLMEndpoint</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 初始化千帆"</span>)</span><br><span class="line">os.environ[<span class="string">"QIANFAN_AK"</span>]=<span class="string">'千帆应用的 API Key'</span></span><br><span class="line">os.environ[<span class="string">"QIANFAN_SK"</span>]=<span class="string">'千帆应用的 Secret Key'</span></span><br><span class="line"></span><br><span class="line">llm = QianfanLLMEndpoint(streaming=<span class="literal">True</span>,</span><br><span class="line"> model=<span class="string">"ERNIE-3.5-8K"</span>,</span><br><span class="line"> temperature=<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line">response = llm.invoke(<span class="string">"上海春天一般哪个月开始?"</span>)</span><br><span class="line">print(response)</span><br></pre></td></tr></table></figure>
<p>执行之前先替换之前记录的千帆应用的 API Key 和 Secret Key。</p>
<p>程序解读:</p>
<ol>
<li>初始化环境变量,创建一个千帆 LLM 实例</li>
<li>发起(人生)第一次大模型 API 调用,如果不成功则返回检查环境和依赖</li>
</ol>
<h4 id="定义函数"><a href="#定义函数" class="headerlink" title="定义函数"></a>定义函数</h4><figure class="highlight python"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain <span class="keyword">import</span> PromptTemplate, LLMChain</span><br><span class="line"><span class="keyword">from</span> langchain.chains <span class="keyword">import</span> LLMRequestsChain</span><br><span class="line"><span class="keyword">from</span> langchain_core.tools <span class="keyword">import</span> tool</span><br><span class="line"><span class="keyword">from</span> langchain_core.utils.function_calling <span class="keyword">import</span> convert_to_openai_tool</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 定义函数"</span>)</span><br><span class="line"><span class="meta">@tool</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">search_ip</span><span class="params">(question:str, ip:str)</span> -> str:</span></span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 首先获取输入的IP地址的位置信息,然后回答输入的问题</span></span><br><span class="line"><span class="string"> @param question: 问题</span></span><br><span class="line"><span class="string"> @param ip: IP地址</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> prompt_template = <span class="string">"""以下是IP地址'{ip}'的位置信息:</span></span><br><span class="line"><span class="string"> >>> {requests_result} <<<</span></span><br><span class="line"><span class="string"> 根据以上位置信息,回答以下这个问题:</span></span><br><span class="line"><span class="string"> >>> {question} <<<"""</span></span><br><span class="line"> prompt = PromptTemplate(</span><br><span class="line"> input_variables=[<span class="string">"question"</span>, <span class="string">"ip"</span>, <span class="string">"requests_result"</span>],</span><br><span class="line"> template=prompt_template</span><br><span class="line"> )</span><br><span class="line"> chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))</span><br><span class="line"> inputs = {</span><br><span class="line"> <span class="string">"question"</span>: question,</span><br><span class="line"> <span class="string">"ip"</span>: ip,</span><br><span class="line"> <span class="string">"url"</span>: <span class="string">"https://api.songzixian.com/api/ip?dataSource=generic_ip&ip="</span> + ip</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> chain.invoke(inputs)</span><br><span class="line"></span><br><span class="line"><span class="meta">@tool</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">search_phone</span><span class="params">(question:str, phone:str)</span> -> str:</span></span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> 首先获取输入的手机号码的归属地信息,然后回答输入的问题</span></span><br><span class="line"><span class="string"> @param question: 问题</span></span><br><span class="line"><span class="string"> @param phone: 手机号码</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> prompt_template = <span class="string">"""以下是手机号码'{phone}'的归属地信息:</span></span><br><span class="line"><span class="string"> >>> {requests_result} <<<</span></span><br><span class="line"><span class="string"> 根据以上归属地信息,回答以下这个问题:</span></span><br><span class="line"><span class="string"> >>> {question} <<<"""</span></span><br><span class="line"> prompt = PromptTemplate(</span><br><span class="line"> input_variables=[<span class="string">"question"</span>, <span class="string">"phone"</span>, <span class="string">"requests_result"</span>],</span><br><span class="line"> template=prompt_template</span><br><span class="line"> )</span><br><span class="line"> chain = LLMRequestsChain(llm_chain = LLMChain(llm=llm, prompt=prompt))</span><br><span class="line"> inputs = {</span><br><span class="line"> <span class="string">"question"</span>: question,</span><br><span class="line"> <span class="string">"phone"</span>: phone,</span><br><span class="line"> <span class="string">"url"</span>: <span class="string">"https://api.songzixian.com/api/phone-location?dataSource=phone_number_location&phoneNumber="</span> + phone</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> chain.invoke(inputs)</span><br><span class="line"></span><br><span class="line">functions=[convert_to_openai_tool(search_ip)[<span class="string">'function'</span>],convert_to_openai_tool(search_phone)[<span class="string">'function'</span>]]</span><br><span class="line">print(functions)</span><br></pre></td></tr></table></figure>
<p>程序解读:</p>
<ul>
<li><p>定义 <code>search_ip</code> 和 <code>search_phone</code> 两个工具函数,背后连接一个免费的第三方 API 接口平台,用来获取指定 IP 的位置信息和指定手机号的归属地信息,然后再结合原始问题,通过大模型生成最终回答。</p>
</li>
<li><p>tool/ convert_to_openai_tool: 用于生成函数定义的注解和工具方法</p>
</li>
<li><p><a href="https://python.langchain.com/docs/modules/model_io/prompts/quick_start/#prompttemplate" target="_blank" rel="noopener">PromptTemplate</a>: Prompt 模板,支持变量</p>
</li>
<li><p><a href="https://python.langchain.com/docs/use_cases/apis/#going-deeper" target="_blank" rel="noopener">LLMRequestsChain</a>: 一个基于 URL 请求的大模型链,先调用 URL 获取数据,然后将数据传给一个已绑定 Prompt 的大模型链获取答案</p>
</li>
</ul>
<h4 id="绑定函数"><a href="#绑定函数" class="headerlink" title="绑定函数"></a>绑定函数</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain.schema <span class="keyword">import</span> HumanMessage</span><br><span class="line"><span class="keyword">from</span> langchain_community.chat_models <span class="keyword">import</span> QianfanChatEndpoint</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"></span><br><span class="line"><span class="comment"># 此处不能设定streaming=True,否则无法激活函数回调</span></span><br><span class="line">chat = QianfanChatEndpoint(model=<span class="string">"ERNIE-3.5-8K"</span>,</span><br><span class="line"> temperature=<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_response</span><span class="params">(message)</span>:</span></span><br><span class="line"> print(<span class="string">"# 绑定函数"</span>)</span><br><span class="line"> result = chat.invoke([HumanMessage(content=message)],</span><br><span class="line"> functions=functions)</span><br><span class="line"> print(result)</span><br><span class="line"></span><br><span class="line"> function_call_info = result.additional_kwargs.get(<span class="string">"function_call"</span>, <span class="literal">None</span>)</span><br><span class="line"> print(function_call_info)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> function_call_info:</span><br><span class="line"> print(<span class="string">"# 直接返回"</span>)</span><br><span class="line"> <span class="keyword">return</span> result.content</span><br><span class="line"></span><br><span class="line"> print(<span class="string">"# 调用函数"</span>)</span><br><span class="line"> function_name = function_call_info[<span class="string">"name"</span>]</span><br><span class="line"> function_args = json.loads(function_call_info[<span class="string">"arguments"</span>])</span><br><span class="line"> function_result = eval(function_name)(function_args)</span><br><span class="line"> print(function_result)</span><br><span class="line"> <span class="keyword">return</span> function_result[<span class="string">"output"</span>]</span><br></pre></td></tr></table></figure>
<p>程序解读:</p>
<ul>
<li>创建一个千帆 Chat 实例,绑定之前定义的两个函数,传入用户输入的问题(对应智能体的<strong>感受</strong>环节),然后发起调用,根据调用结果执行不同任务(对应智能体的<strong>决策</strong>环节):<ul>
<li>如果调用结果没有提示函数调用(<code>result.additional_kwargs.get("function_call", None)</code>),则直接返回结果</li>
<li>否则,根据大模型提示的函数名和请求参数,通过反射调用相应的函数(对应智能体的<strong>执行</strong>环节),然后返回结果</li>
</ul>
</li>
</ul>
<h4 id="构建应用"><a href="#构建应用" class="headerlink" title="构建应用"></a>构建应用</h4><figure class="highlight python"><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">import</span> gradio <span class="keyword">as</span> gr</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">submit</span><span class="params">(message, chat_history)</span>:</span></span><br><span class="line"> bot_message = get_response(message)</span><br><span class="line"> <span class="comment"># 保存历史对话记录,用于显示</span></span><br><span class="line"> chat_history.append((message, bot_message))</span><br><span class="line"> <span class="keyword">return</span> <span class="string">""</span>, chat_history</span><br><span class="line"></span><br><span class="line">print(<span class="string">"# 创建交互"</span>)</span><br><span class="line"><span class="keyword">with</span> gr.Blocks() <span class="keyword">as</span> demo:</span><br><span class="line"> chatbot = gr.Chatbot(height=<span class="number">240</span>) <span class="comment"># 对话框</span></span><br><span class="line"> msg = gr.Textbox(label=<span class="string">"Prompt"</span>) <span class="comment"># 输入框</span></span><br><span class="line"> submitBtn = gr.Button(<span class="string">"Submit"</span>) <span class="comment"># 提交按钮</span></span><br><span class="line"> clearBtn = gr.ClearButton([msg, chatbot]) <span class="comment"># 清除按钮</span></span><br><span class="line"> <span class="comment"># 提交</span></span><br><span class="line"> msg.submit(submit, inputs=[msg, chatbot], outputs=[msg, chatbot]) </span><br><span class="line"> submitBtn.click(submit, inputs=[msg, chatbot], outputs=[msg, chatbot])</span><br><span class="line"> </span><br><span class="line">gr.close_all()</span><br><span class="line">demo.launch()</span><br></pre></td></tr></table></figure>
<p>程序解读:</p>
<ul>
<li>创建智能体应用,构建一个用户和智能体的对话框,将用户输入的消息传给后台创建的大模型,实时获取响应</li>
<li>此处可以看到,借助 Gradio 框架,短短几行代码,就可以构建出一个简洁的对话框应用,非常 Nice!</li>
</ul>
<p>至此,一个极简的智能体应用就构建成功了,前后仅用 100 行代码。完整代码参见 <a href="https://github.com/emac/langchain-samples/blob/main/mini-ai-agent.ipynb" target="_blank" rel="noopener">GitHub</a>。</p>
<h4 id="彩蛋:智能体如何思考?"><a href="#彩蛋:智能体如何思考?" class="headerlink" title="彩蛋:智能体如何思考?"></a>彩蛋:智能体如何思考?</h4><p>看完演示,拆解完程序,你可能对智能体里的<strong>“智能“</strong>两字的理解还是有点模模糊糊。其实要真正理解这一点,光看程序还不够,得看下大模型的响应结果(见下)。注意其中 <code>response_metadata</code> 有一个很有意思的 <code>thoughts</code> 字段(<code>用户想要知道一个特定IP地址的地理位置信息,我需要使用search_ip工具来获取这个信息</code>),其代表了大模型的思考过程,是不是和人类非常类似?</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"># 问题:120.230.93.202属于哪个城市?</span><br><span class="line"># 大模型响应结果</span><br><span class="line">additional_kwargs={'finish_reason': 'function_call', 'request_id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'search_info': [], 'function_call': {'name': 'search_ip', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}} </span><br><span class="line"></span><br><span class="line">response_metadata={'token_usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}, 'model_name': 'ERNIE-3.5-8K', 'finish_reason': 'function_call', 'id': 'as-fcd0dz2mmg', 'object': 'chat.completion', 'created': 1714900176, 'result': '', 'is_truncated': False, 'need_clear_history': False, 'function_call': {'name': 'search_ip', 'thoughts': '用户想要知道一个特定IP地址的地理位置信息,我需要使用search_ip工具来获取这个信息。', 'arguments': '{"question":"120.230.93.202属于哪个城市?","ip":"120.230.93.202"}'}, 'usage': {'prompt_tokens': 194, 'completion_tokens': 63, 'total_tokens': 257}} id='run-0706ab6f-a21f-4352-bbb1-1f1a0f13c426-0'</span><br></pre></td></tr></table></figure>
<h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>本文基于百度千帆大模型,使用 Langchain 和 Gradio 框架,用短短 100 行代码就构建出一个极简的智能体应用。该应用能够根据用户问题,选择不同的工具获取信息,并生成最终回答,体现了智能体最核心的三步循环:感知-决策-行动。基于这个演示应用,相信聪明的你可以构建出更复杂、更智能的智能体应用,欢迎<a href="https://github.com/emac/emac.github.io/issues" target="_blank" rel="noopener">留言</a>交流。</p>
<h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul>
<li><a href="https://mp.weixin.qq.com/s/WOI-owwovML5g2olnO28GQ" target="_blank" rel="noopener">吴恩达:别光盯着GPT-5,用GPT-4做个智能体可能提前达到GPT-5的效果</a></li>
</ul>
]]></content>
<categories>
<category> AI </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> LangChain </tag>
</tags>
</entry>
<entry>
<title><![CDATA[GPT四问]]></title>
<url>http://emacoo.cn/AI/gpt-forecast/</url>
<content type="html"><![CDATA[<blockquote>
<p>前两周,ZA技术社区举办了一场主题为“<a href="https://live.csdn.net/room/wl5875/fL36YEyS" target="_blank" rel="noopener">未来,程序员职业会消失吗?</a>”的Geek圆桌派,我作为业务研发的代表参加了此次直播。由于直播时间有限,未能充分表达我的观点,故写此篇博客进行详述。另一方面,也想借此篇博客,印证十年后我的这些观点是否还成立。</p>
</blockquote>
<h2 id="Q1:你对“程序员会不会被-AI-取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?"><a href="#Q1:你对“程序员会不会被-AI-取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?" class="headerlink" title="Q1:你对“程序员会不会被 AI 取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?"></a>Q1:你对“程序员会不会被 AI 取代”(或未来程序员这一职业会不会消失),持什么样看法或态度?</h2><blockquote>
<p>本文中的 AI 特指这一波以 ChatGPT 为代表的泛人工智能。</p>
</blockquote>
<p>这个问题比较大,我想从三个角度来回答。</p>
<p><strong>首先,要明确目前 AI 的边界或者说局限</strong>。在我看来,主要有三点。</p>
<p><strong>1、计算能力差</strong>。下图是去年 6 月份 UC 伯克利主导的 MT-Bench 基准测试中各个知名 LLM(Large Language Model,大语言模型) 的得分。可以看到,<strong>所有 LLM 在数学这一类别的得分都是全类别中最低的</strong>。</p>
<p><img src="mt-bench.png" alt></p>
<p>又比如去年火爆上海的数学中考 22 题,不管用 ChatGPT 还是 GPT-4,都没法给出正确的答案。</p>
<p><img src="gpt4-22.png" alt></p>
<p>关于这一点,我认为本质上的原因是 GPT 是一个语言模型,而非计算模型。</p>
<p><strong>2、无法生产新知识</strong>。<strong>GPT 的本质是知识压缩</strong>,把海量的人类社会的知识压缩到 96 层神经网络中,从而“涌现”出某种类智能。表面上看,你问 GPT 任何一个问题,GPT 都能有模有样的给出一个符合逻辑的回答,但实际上,<strong>GPT 并不真正理解它回答的内容</strong>,所以才会有鲁迅和周树人是不是同一个人的热梗。</p>
<p><strong>要生产新知识,GPT 必须拥有进化的能力</strong>。而从进化论的角度来看,任何生物要进化必然经历三个过程,交配、变异、淘汰。交配是为了交换基因,变异是产生新的更适应环境的基因,然后淘汰掉那些不适应环境的基因。从这点来看,GPT 显然都不满足。</p>
<p><strong>3、没有产生意识</strong>。何为意识?意识如何产生?虽然人类目前仍然无法给出一个准确的回答,但作为一个必要条件,感觉和欲望是意识的基本特征(引自《未来简史》)。显然,<strong>GPT 既无感觉也无欲望,因此也就没有意识</strong>。</p>
<p>理清目前 AI 的三点局限之后,接着<strong>从分工的角度看一下程序员的发展趋势</strong>。简单来说,<strong>程序员可分为三类,开发、测试和运维</strong>。其中,开发可以进一步细分为业务开发、架构开发和算法开发,测试分为功能测试和自动化测试,运维分为传统运维和云平台运维。</p>
<p>先说开发,大部分的开发都是业务开发,<strong>业务开发本质上就是将业务需求“翻译”成机器代码</strong>,而“翻译”恰恰是 GPT 的强项,加上 code-davinci 项目的加持,<strong>如果说将来某一天程序员会被 AI 取代,业务开发首当其冲</strong>。架构开发和算法开发,需要较强的抽象能力和计算能力,这两点恰恰对应目前 AI 的前两点局限,所以被 AI 取代的可能性较低。再看测试和运维,目前行业的趋势是自动化测试逐步取代功能测试,云平台运维逐步取代传统运维,AI 的出现一定程度上加速了这一过程,大浪淘沙,<strong>最终留下的只能是测试架构和运维架构</strong>。这意味着,<strong>传统意义上的或者说狭义上的程序员会越来越少</strong>。举个例子,OpenAI 官网显示,为 ChatGPT 项目做出贡献的人员只有区区 87 人。</p>
<p>另一方面,<strong>广义上的程序员会越来越多</strong>。何为程序员?简单来说就是会使用编程语言编写程序的人。我们知道,像 Java、C++ 这些编程语言属于第三代编程语言,SQL 属于第四代编程语言,那么<strong>基于自然语言交互的 GPT 是不是可以算是第五代编程语言</strong>?<strong>使用 GPT 完成特定任务的人是不是可以算是一类新的程序员</strong>?</p>
<blockquote>
<p>If you can say it, you can do it.</p>
<p>- Silvio Savarese, Salesforce执行副总裁和首席科学家</p>
</blockquote>
<p>最后,<strong>程序员会不会被 AI 取代,还要看人类社会对待 AI 的态度,以及与之配套的 AI 安全法规如何制定</strong>。由于不受化学规律限制,AI 的发展速度远超人类,我们必须让这个过程变慢,让整个社会适应这个变化,并且制定出一套道德或者说安全法规,让我们能够安全的使用 AI,否则我们的文明就有被摧毁的风险。</p>
<p><img src="prometheus.png" alt></p>
<p><em>就像《普罗米修斯》开头一幕,碳基生命会不会是硅基生命的前传?</em></p>
<p>早在 2023 年 3 月,包括图灵奖得主 Yoshua Bengio、伯克利计算机科学教授 Stuart Russell、特斯拉 CEO 埃隆·马斯克、苹果联合创始人 Steve Wozniak 等在内的数千名对人工智能领域关注的学者、企业家、教授发起了一封公开信,强烈呼吁:<strong>暂停训练比 GPT-4 更强大的系统,期限为六个月</strong>,理由是这些模型对社会和人类存在潜在风险。</p>
<p>美国有个名叫<strong>对齐研究中心(Alignment Research Center)</strong>的非营利研究机构,致力将人工智能的行为对齐人类的价值观和预期利益。OpenAI 在发布 GPT-4 之前,就曾请求对齐研究中心评估该模型对权力追求行为的能力和潜在风险。</p>
<h2 id="Q2:所以这不得不提到一个现实,AI-是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应-AI-社会的到来?"><a href="#Q2:所以这不得不提到一个现实,AI-是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应-AI-社会的到来?" class="headerlink" title="Q2:所以这不得不提到一个现实,AI 是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应 AI 社会的到来?"></a>Q2:所以这不得不提到一个现实,AI 是否会倒逼人类社会的工种变化,或者说让人学习新的职业技能,以适应 AI 社会的到来?</h2><p>先来看一则新闻,</p>
<blockquote>
<p>2023 年 5 月 2 日,代表 11,500 名编剧的美国编剧工会 WGA 因与影视制片人联盟持续存在的劳资纠纷而宣布罢工,AI 特别是 AIGC 已经成为了此次冲突的核心。自 2023 年初以来越来越受到关注的 ChatGPT,已经影响到好莱坞乃至整个影视行业。漫威最新播出的影视剧《秘密入侵》,就已经将 AI 运用于制作过程,生成了开场字幕,并饱受争议。编剧工会在谈判过程中要求不允许 AI 获得署名,并且不能要求编剧根据 AI 写好的内容进行修改,因为这样也会显著减少工作时长。与此同时,在未经允许的情况下,制作方不可以将工会成员的剧本进行 AI 训练。</p>
</blockquote>
<p>无论你承认或者不承认,AI 已经对当今社会的很多行业产生了不小的冲击。<strong>所谓大语言模型,本质上就是知识</strong>。<strong>一个好的程序员是一个好的模型,一个好的教师是一个好的模型</strong>。当 AI 发展到一定阶段,<strong>所有的知识工作者都有被取代的可能</strong>。美国普林斯顿大学教授爱德华·费尔滕(Edward Felten)甚至还提出了一个<strong>“职业AI暴露指数”(AIOE)</strong>,像客服、秘书、翻译、助教这些职业都属于高 AI 暴露率职业,程序员只能算中等 AI 暴露率职业。试想,<strong>哪个老板会抗拒能力强、又忠诚、价格还便宜的 AI 员工</strong>?</p>
<p><img src="boss.png" alt></p>
<p>虽然 AI 或早或晚、或多或少会取代或者部分取代一些职业,但<strong>同时也会创造一些新的职业,比如标注师、提示工程师、AI 研究员这类 AI 周边职业</strong>。另一方面,那些 AI 边界之外的职业我相信也会出现一个爆发式增长,特别是<strong>心理师</strong>这个职业。为什么这么说?自 18 世纪六十年代第一次工业革命以来,工作已经成为人的本能。而一旦进入 AI 时代,如《未来简史》所言,绝大部分人将沦为“无价值的群体”,大部分人将不再需要工作。在这个过程中,大量人会出现心理问题,<strong>人们被迫寻找工作之外的意义,尝试在社区、家庭、艺术、运动、精神领域和自我探索中找到目的,从追求财富自由转向追求精神自由</strong>。</p>
<blockquote>
<p>在成为自己这件事情上,没有人比得过你。</p>
<p>- 纳瓦尔</p>
</blockquote>
<h2 id="Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些-AI-的知识?培养哪些方面的技能?"><a href="#Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些-AI-的知识?培养哪些方面的技能?" class="headerlink" title="Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些 AI 的知识?培养哪些方面的技能?"></a>Q3:未来,程序员的核心竞争力会体现在哪些方面?需要具备哪些 AI 的知识?培养哪些方面的技能?</h2><blockquote>
<p>取代你的不是AI,而是使用AI的人。</p>
</blockquote>
<p>可以用 <strong>ASK (Attitude态度,Skill技能,Knowledge知识)模型</strong>来回答这个问题。</p>
<p>在态度层面,不管是作为程序员,还是普通人,我们都应该<strong>接受 AI</strong>,让 AI 成为个人工作、生活的得力助手,千万不要像好莱坞编剧那样视 AI 为敌,拒之千里之外。</p>
<p>在技能层面,作为普通人,要学会使用 AI,着重<strong>提升自身的任务分解能力、信息整合能力,特别是学习能力</strong>。作为程序员,则要具备一定程度的 AI 开发能力,比如开发 GPT 插件、基于 LangChain + Embedding 的 AI 应用、基于 Llama 的自定义模型等。</p>
<p>在知识层面,<strong>通识教育会变得越来越重要,知识的广度比深度更重要</strong>,对于程序员而言,了解机器学习、神经网络、强化学习、Transformer等这些 AI 的基本概念和原理是最低的要求。</p>
<h2 id="Q4:我们今天做一个大胆的预测,猜猜-10-年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)"><a href="#Q4:我们今天做一个大胆的预测,猜猜-10-年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)" class="headerlink" title="Q4:我们今天做一个大胆的预测,猜猜 10 年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)"></a>Q4:我们今天做一个大胆的预测,猜猜 10 年后的程序员工作是怎么样的?(如果程序员这一职业还在的话)</h2><p>虽然我不知道 AI 何时会产生真正的智能,但有一点我敢肯定,<strong>AI 一定会越来越小,越来越快</strong>。</p>
<p>关于计算机性能有个摩尔定律,当价格不变时,集成电路上可容纳的晶体管数目,约每隔 18 个月便会增加一倍,性能也将提升一倍。1946 年,第一代电子管计算机占地 150 平方米,重 30 吨,每秒 5000 次运算。到了2023年,最新款的苹果手机 15 Pro,6.1 英寸的屏幕,重 187 克,主频达到 3.78 GHz。两者相比,后者重量降低了 6 个数量级,速度却提升了 6 个数量级。</p>
<p>类似的事情大概率也会发生在 AI 身上。为了产生类比人类的“智能“,ChatGPT 每次训练要消耗 90 多万度电,相当于 1200 个中国人一年的生活用电量。而人类大脑的运行功率只有区区 20 瓦。单就能耗这一点,就有巨大的提升空间。</p>
<p>就 10 年而言,我相信程序员这一职业不会产生本质上的变化,但<strong>程序员的电脑上一定会出现各式各样的 Copilot</strong>,有协助开发的,有协助测试的,甚至有协助开会的。再往后看,当 AI 遇上元宇宙、脑机接口,<strong>程序员可能就彻底成为一个数字化职业</strong>。</p>
<p><img src="meta.png" alt></p>
<h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>面对飞速发展的 AI ,人类将何去何从?是像对待克隆人技术一样明令禁止,还是像对待核武器一样通过《不扩散核武器条约》进行限制,抑或是像《三体》中的降临派一样全面迎接 AI 时代的到来?且看十年之后。</p>
]]></content>
<categories>
<category> AI </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> GPT </tag>
</tags>
</entry>
<entry>
<title><![CDATA[故障模型哪家强?PDR 模型来帮忙]]></title>
<url>http://emacoo.cn/arch/pdr/</url>
<content type="html"><![CDATA[<blockquote>
<p>搞安全的同学都知道,有一个非常著名的网络安全模型叫 <a href="https://baike.baidu.com/item/PDR/17563449" target="_blank" rel="noopener">PDR 模型</a>,提出者是美国国际互联网安全系统公司(ISS),其核心论断是网络安全是一个时间问题,对应的公式为 <code>Et = Dt + Rt - Pt</code>,其中:</p>
<ul>
<li><strong>Et</strong> (Exposure) 暴露时间,系统暴露在攻击下的时间;</li>
<li><strong>Pt</strong> (Prevent) 防御时间,系统扛住外部攻击的时间,或者说攻击者成功渗透的整个时间;</li>
<li><strong>Dt</strong> (Detect) 检测时间,安全检测系统发现攻击所需要的时间;</li>
<li><strong>Rt</strong> (Response) 响应时间,发现攻击到攻击路径被切断,攻击被中止的整个时间。</li>
</ul>
<p>PDR 模型直观、易懂,为安全防护工作提供了比较实用的指导框架。在系统可能出现的各类故障中,安全只是其中一种,既然 PDR 模型能指导解决安全问题,那么 PDR 模型是否也能指导解决其他故障呢?我认为是肯定的。</p>
</blockquote>
<h1 id="1-什么是-PDR-故障模型?"><a href="#1-什么是-PDR-故障模型?" class="headerlink" title="1 什么是 PDR 故障模型?"></a>1 什么是 PDR 故障模型?</h1><p>对照 PDR 模型,先来看一下故障的生命周期,</p>
<p><img src="PDR.png" alt></p>
<p>从上图可以非常直观的看出,为了缩短故障时间(Failure Time),我们要想办法尽可能的缩短检测时间(Detect Time)和响应时间(Response Time),同时延长防御时间(Prevent Time)。缩短检测时间对应提升监控、告警能力,缩短响应时间对应提升故障修复、CI/CD能力,延长防御时间对应提升系统的容错能力或者说健壮性。这里比较有意思的一点是关于防御时间,只要我们能把防御时间延长到足够长(超过检测时间和响应时间之和),那么故障就没有机会造成实际影响,也就等同于“消灭”了故障。</p>
<h1 id="2-防火优于灭火"><a href="#2-防火优于灭火" class="headerlink" title="2 防火优于灭火"></a>2 防火优于灭火</h1><blockquote>
<p>魏文侯曰:‘子昆弟三人其孰最善为医?’扁鹊曰:‘长兄最善,中兄次之,扁鹊最为下。’</p>
<p>—— 《鶡冠子·卷下·世贤第十六》</p>
</blockquote>
<p>扁鹊三兄弟中,扁鹊 Rt 能力强,二哥 Dt 能力强,大哥 Pt 能力强,但此 Pt 非彼 Pt。在上述 PDR 模型中,Pt 是指故障发生之后的防御能力,而大哥的 Pt 能力是指故障(疾病)发生之前的防御能力,也即防患未然的能力。</p>
<p>受限于各方面因素,绝大多数情况下,故障防御时间是小于检测时间和响应时间之和的,因此,一旦系统出现故障,就难免会造成一些实际影响。那么有没有办法避免这类影响呢?有的,向扁鹊大哥学习,防火优于灭火。如何做到防患未然呢?以史为鉴,可以知兴替,也即故障复盘。</p>
<p>故障复盘是一件极其重要的事情,它是如此重要以至于大多数人都低估了其重要性。小到个人,大到公司,从大大小小各类故障中总结经验教训,学到很多书本上学不到的知识,从而获得最大程度的提升,这也是成长性团队的重要特征。网上关于故障复盘的资料很多,这里我只想强调三点,</p>
<p>第一,故障复盘越早做效果越好(当然是在故障被妥善处理之后)。注意,处理故障过程中,应尽可能保留故障现场,同时做好过程数据备份,以便事后进行复盘。</p>
<p>第二,在整个故障复盘过程中,应秉持对事不对人的原则,一切从事实出发,用事实说话,这样才能找到真正的根本原因,并据此提出行之有效的改进措施。</p>
<p>第三,每项改进措施应指定唯一的责任人,并区分优先级,对于高优先级的改进措施,应明确闭环时间(比如 1 个月)。</p>
<p>最后,推荐一篇陈皓老师的专栏(文末参考资料第 2 篇),同时我在附录里贴了一份故障复盘报告模板,希望大家在平时工作中能够用到。</p>
<h1 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h1><p>在今天这篇文章里,我首先提出了一个专门用于故障处理的 PDR 模型,然后给出了一些故障复盘的原则,希望能够对你有所帮助。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p>
<h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><p><em>故障复盘报告模板</em></p>
<p><img src="template.jpeg" alt></p>
<h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul>
<li><a href="https://time.geekbang.org/column/article/1059" target="_blank" rel="noopener">左耳听风-故障处理最佳实践:应对故障</a></li>
<li><a href="https://time.geekbang.org/column/article/1064" target="_blank" rel="noopener">左耳听风-故障处理最佳实践:故障改进</a></li>
<li><a href="https://www.huxiu.com/article/242040.html" target="_blank" rel="noopener">余晟以为-浅谈“黑匣子思维”</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创,故障 </tag>
</tags>
</entry>
<entry>
<title><![CDATA[不仅仅是一把瑞士军刀 —— Apifox的野望和不足]]></title>
<url>http://emacoo.cn/arch/apifox-vision/</url>
<content type="html"><![CDATA[<p>声明:本文内容不涉及任何 Apifox 的功能介绍,一来网上这方面的文章已经汗牛充栋,二来 Apifox 本身的用户体验做的非常好,对于开发者而言学习成本基本为零。</p>
<blockquote>
<p>阮一峰:不管你是前端开发还是后端开发,只要项目是服务架构,它可能会大大提升你的开发效率。</p>
<p>虫师:我们很难把它描述为一款接口管理工具或接口自动化测试工具,它增强了团队协作能力,这对一个研发团队而言很重要。</p>
<p>池建强:Apifox,这是一代更比一代强。</p>
</blockquote>
<h2 id="什么是Apifox?"><a href="#什么是Apifox?" class="headerlink" title="什么是Apifox?"></a>什么是Apifox?</h2><p>看了一众大咖们对 <a href="https://www.apifox.cn/" target="_blank" rel="noopener">Apifox</a> 赞不绝口,你可能会好奇 Apifox 究竟是何方神圣?根据<a href="https://www.apifox.cn/help/app/introduce/" target="_blank" rel="noopener">官方定义</a>,Apifox 是 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台,定位 Postman + Swagger + Mock + JMeter。如果你也曾使用过 Apifox,相信你会深表赞同。</p>
<p><img src="apifox-platform.png" alt></p>
<p>那么问题来了,在盛行小而美的 API 工具的当下,为什么会横空出世一个“瑞士军刀”般存在的 Apifox?答案就在 Apifox 的宗旨里面:节省研发团队的每一分钟。</p>
<p>在 Apifox 之前,为了达成对 API 语义的理解和实现上的一致性,前端、后端、测试使出十八般武艺,定义 API 用 Swagger,生成文档用 YAPI,前端自测用 Mock,接口测试用 Postman,性能测试用 JMeter,各类配置、数据、链接满天飞,重要的事情说三遍啊说三遍。有了 Apifox 之后,前端、后端、测试之间原本去中心化的 P2P 通讯方式变成以 Apifox 为中心的星型通讯方式,通讯对象从原本充满不确定性的人,变成稳定可靠的平台,各类配置、数据、链接也有了统一管理的地方,团队通讯成本和 API 管理成本大幅降低。</p>
<p><img src="apifox-purpose.png" alt></p>
<h2 id="Apifox的野望"><a href="#Apifox的野望" class="headerlink" title="Apifox的野望"></a>Apifox的野望</h2><p>如果你认为一体化协作平台就是 Apifox 的一切,那你可能低估了 Apifox 的野心。</p>
<p>先来看下 Apifox 的收费模式,</p>
<p><img src="apifox-price.png" alt></p>
<p>是的,你没有看错,免费版即享“无任何限制”,不限团队人数、不限功能、不限项目数、不限接口数,如此奢华的免费套餐,放眼全网也很难找到第二家(SaaS 平台)。</p>
<p>再来看下<a href="https://www.apifox.cn/help/app/changelog/" target="_blank" rel="noopener">更新日志</a>,留意以下更新:</p>
<ol>
<li>[2021-03-14] 1.2.0 新增【在线分享接口文档】功能。</li>
<li>[2021-10-29] 1.4.10 上线 API Hub功能。1)通过API Hub查找/发现他人公开的 API 项目。2)可将项目发布到API Hub(设置为公开项目即可),允许任何人通过API Hub访问、克隆该项目。</li>
<li>[2022-01-11] 1.4.17 公开项目支持通过 web 访问、运行。</li>
</ol>
<p>看懂了吗?<a href="https://www.apifox.cn/apihub/" target="_blank" rel="noopener">API Hub</a> 才是 Apifox 真正的野望,打造开放 API 共享平台,连接各类企业级 API,加速企业商业创新,成为企业之间的“交友”平台。有了 API Hub,企业之间谈合作,见面第一句话就是:PPT is cheap, show me the API!</p>
<h2 id="Apifox的不足"><a href="#Apifox的不足" class="headerlink" title="Apifox的不足"></a>Apifox的不足</h2><p>要配得上如此宏大的野心,在我看来,Apifox 无论是架构上还是产品功能上都还有很长的路要走。</p>
<p>从架构上来看,首先要做的是提升项目中模型的地位。创建完一个新项目,首先应该定义模型,然后才是接口。我们知道,模型是一个软件的骨架,是一个系统的核心。接口是系统外在能力的呈现,模型是系统内在逻辑的载体。一旦脱离了模型,接口就是无源之水,无本之木。</p>
<p>其次,作为接口的诞生地,Apifox 不妨制定或者倡导一些好的 API 设计规约,像 <a href="https://google.aip.dev/general" target="_blank" rel="noopener">Google AIP (API Improvements Proposal)</a>,<a href="https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design" target="_blank" rel="noopener">Microsoft RESTFul API Design</a>等。</p>
<p>从产品功能来看,不要局限于 Postman + Swagger + Mock + JMeter 这句 Slogan,以用户为中心,添加诸如一键生成单元测试代码、自动化测试源码编辑模式等实用功能,解决更多实际 API 开发过程中的痛点。</p>
<p>除此之外,官方资料中似乎没有看到大规模团队协作的案例,无论是SaaS版本还是私有化部署版本,平台所能支持的团队体量大小暂时未知。</p>
<h2 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h2><p>借微服务之东风,Apifox 自 2020 年 12 月 28 日推出 1.0 版本以来,以其独特的集成优势、优秀的用户体验,在国内IT界一时风光无两。不过在国外,似乎知者寥寥,stackoverflow 上甚至查无此人。随着今年 2 月份 2.0 英文版的推出,相信很快会吸引众多国外开发者的目光,祝愿 Apifox 走出国门,走向世界,早日成为国产软件之光!</p>
<h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul>
<li><a href="https://www.apifox.cn/help/#_20-%E5%88%86%E9%92%9F%E5%AD%A6%E4%BC%9A-apifox-%F0%9F%91%8D" target="_blank" rel="noopener">20 分钟学会 Apifox</a></li>
<li><a href="https://www.apifox.cn/help/app/introduce/" target="_blank" rel="noopener">Apifox 介绍</a></li>
<li><a href="https://www.infoq.com/articles/API-Design-Joshua-Bloch/" target="_blank" rel="noopener">Joshua Bloch: Bumper-Sticker API Design</a></li>
<li><a href="https://mp.weixin.qq.com/s/qWrSyzJ54YEw8sLCxAEKlA" target="_blank" rel="noopener">深度 | API 设计最佳实践的思考</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创,API </tag>
</tags>
</entry>
<entry>
<title><![CDATA[告别2021,清零2022]]></title>
<url>http://emacoo.cn/notes/2021-fin/</url>
<content type="html"><![CDATA[<blockquote>
<p>所谓清零思维,就是说职场人每过两年,都要对自己做一次清零,以对自己在市场中的“估值”,有一个清醒的认识。</p>
<p>– 老K</p>
</blockquote>
<h1 id="System-in-read"><a href="#System-in-read" class="headerlink" title="System.in.read()"></a>System.in.read()</h1><h2 id="Books"><a href="#Books" class="headerlink" title="Books"></a>Books</h2><ul>
<li><a href="http://e.dangdang.com/products/1901208652.html" target="_blank" rel="noopener">《银行数字化转型》- 付晓岩</a></li>
</ul>
<h2 id="Columns"><a href="#Columns" class="headerlink" title="Columns"></a>Columns</h2><ul>
<li><a href="https://time.geekbang.org/column/intro/313?tab=catalog" target="_blank" rel="noopener"><软件设计之美> - 郑晔</a></li>
<li><a href="https://time.geekbang.org/column/intro/113?tab=catalog" target="_blank" rel="noopener"><技术管理实战 36 讲> - 刘建国</a></li>
<li><a href="https://time.geekbang.org/column/intro/100017301?tab=catalog" target="_blank" rel="noopener"><数据结构与算法之美> - 王争</a></li>
<li><a href="https://time.geekbang.org/column/intro/100076501?tab=catalog" target="_blank" rel="noopener"><说透数字化转型> - 付晓岩</a></li>
<li><a href="https://time.geekbang.org/column/intro/100012001?tab=catalog" target="_blank" rel="noopener"><邱岳的产品实战> - 邱岳</a></li>
</ul>
<h1 id="System-out-print"><a href="#System-out-print" class="headerlink" title="System.out.print()"></a>System.out.print()</h1><h2 id="PPT"><a href="#PPT" class="headerlink" title="PPT"></a>PPT</h2><ul>
<li><a href="https://slides.com/emacooshen/confucius" target="_blank" rel="noopener">穿越时空的对话:《论语》中的管理智慧 (一)</a></li>
<li><a href="https://slides.com/emacooshen/xxl-job" target="_blank" rel="noopener">XXL-JOB 原理浅析</a></li>
<li><a href="https://slides.com/emacooshen/lifelong-learning" target="_blank" rel="noopener">穿越时空的对话:像孔子一样终身学习</a></li>
<li><a href="https://slides.com/emacooshen/x10" target="_blank" rel="noopener">10x 程序员工作法极简笔记</a></li>
<li><a href="https://docs.google.com/presentation/d/1Kgryi2z65PKQdZ7LanZHFPApUaWcFSLyx_X0v4dQmVU/edit?usp=sharing" target="_blank" rel="noopener">如何做好组织协同</a></li>
</ul>
<h2 id="Blog"><a href="#Blog" class="headerlink" title="Blog"></a>Blog</h2><p>(惭愧)</p>
<h2 id="Run"><a href="#Run" class="headerlink" title="Run"></a>Run</h2><p><img src="2021-Run.jpeg" alt></p>
]]></content>
<categories>
<category> notes </category>
</categories>
<tags>
<tag> 原创 </tag>
</tags>
</entry>
<entry>
<title><![CDATA[代码评审赋魅]]></title>
<url>http://emacoo.cn/arch/the-beauty-of-code-review/</url>
<content type="html"><![CDATA[<blockquote>
<p><img src="dead-delta.png" alt></p>
<p>先来看一个令无数技术Leader闻风丧胆的项目“死亡”三角,业务压力引发代码质量下降,代码质量下降引发开发效率下降,开发效率下降又加重了业务压力,最终导致业务压力山大,乃至项目烂尾。如何破解?方法有很多,像精简业务需求、增加开发人手、升级技术架构等,很多时候需要多管齐下,但凡打掉这个“死亡”三角中的任何一角,就能终止这个恶性循环,甚至逆转为良性循环。</p>
<p>代码评审(Code Revew,简称CR)的首要打击目标显然是“烂代码”。避免“烂代码”的最好时机是写代码的时候,其次是代码评审的时候。IBM 的 Orbit 项目有 50 万行代码,使用了 11 级的代码检查(其中包含代码评审),结果是项目提前交付,并且只有通常预期错误的 1%。一份对 AT&T 的一个 200 多人组织的研究报告显示,在引入代码评审后,生产率提高了 14%,缺陷减少了 90%。那到底什么是代码评审?如何进行代码评审?继续往下看。</p>
</blockquote>
<h2 id="1-CR-祛魅"><a href="#1-CR-祛魅" class="headerlink" title="1 CR 祛魅"></a>1 CR 祛魅</h2><blockquote>
<p>我个人认为代码有这几种级别:1)可编译,2)可运行,3)可测试,4)可读,5)可维护,6)可重用。通过自动化测试的代码只能达到第3)级,而通过CODE REVIEW的代码少会在第4)级甚至更高。</p>
<p>—— 陈皓</p>
</blockquote>
<p>下面 8 条有关 CR 的阐述,你觉得哪些是正确的?</p>
<ol>
<li>搞形式主义,存粹是浪费时间</li>
<li>CR 是保证程序正确性的手段</li>
<li>CR 是保证代码规范的手段</li>
<li>CR 是 Leader 的事,跟我没关系</li>
<li>我只看指给我的 CR,其他 CR 跟我没关系</li>
<li>没有时间 CR,直接 Merge</li>
<li>CR 必须一行不落从头看到尾</li>
<li>CR 必须一次完成</li>
</ol>
<p>———————————————————————————————— 请仔细思考 60 秒</p>
<p>3…2…1…时间到,你的答案是几条?很抱歉,在我看来,没有一条是正确的。1、4、5、6 是送分题,显然都是错误的。7 是眉毛胡子一把抓,CR 就像读书,不是所有的书都适合精度,也不是所有的代码都需要评审。8 是任务心态,为了 CR 而 CR,CR 的目的不是完成 CR,而在于提升代码质量,你写代码时也不会一次完成所有功能。比较有争议的是 2 和 3,诚然,正确性和代码规范都是 CR 要关注的方面,但这并不意味着 CR 要保证正确性和代码规范(CR 也没法保证),保证正确性的主要手段是测试(单元测试,集成测试,契约测试,功能测试,自动化测试等),而保证代码规范主要依靠代码规范检查工具(像常用的 checkstyle 和 PMD)。</p>
<p><img src="guo.png" alt></p>
<p>CR 到底是什么?依我所见,CR 本质上是一种讨论,一种严肃的、专业的、异步的以文字形式呈现的讨论,随意性和情绪化是 CR 的大忌。什么叫随意性?未经审视的评论。什么叫情绪化?因时而异,因人而异。高水平的 CR 首先要忘掉自己。</p>
<p><img src="nature-of-code-review.png" alt></p>
<h2 id="2-知:CR-的三重境界"><a href="#2-知:CR-的三重境界" class="headerlink" title="2 知:CR 的三重境界"></a>2 知:CR 的三重境界</h2><p>技术水平决定了 CR 的下限,认知高度决定了 CR 的上限。所以说 CR 水平高不高,最终还是看认知水平。认识 CR 有三重境界,分别是执行层、团队层和文化层。</p>
<h3 id="2-1-执行层:昨夜西风凋碧树,独上高楼,望尽天涯路"><a href="#2-1-执行层:昨夜西风凋碧树,独上高楼,望尽天涯路" class="headerlink" title="2.1 执行层:昨夜西风凋碧树,独上高楼,望尽天涯路"></a>2.1 执行层:昨夜西风凋碧树,独上高楼,望尽天涯路</h3><p>第一层为执行层,顾名思义就是通过如何做来认识 CR。以下列举 CR 时需重点关注的六个方面,并辅以相应的例子便于理解。</p>
<p>1)关注<strong>代码规范</strong>。命名是第一位的,一个令人费解的命名背后往往隐藏着一个设计纰漏。其他诸如空白字符、换行、注释等问题,也会影响代码的可读性和可理解性。</p>
<p><img src="example-naming.png" alt></p>
<p><img src="example-white-characters.png" alt></p>
<p>2)避免<strong>重复代码</strong>。编程法则第一条,Don’t repeat yourself. 重复代码是万恶之首,重复代码人人得而诛之!</p>
<p><img src="example-dry.png" alt></p>
<p>3)降低<strong>圈复杂度</strong>。什么是圈复杂度?简单来说就是代码中 if/case/for/while 出现的次数。圈复杂度越高,BUG 率越高。如果一个方法的圈复杂度达到 3 或者更高,那么 CR 时就要多看两眼。</p>
<p>4)关注<strong>性能问题</strong>。性能问题虽然不常见,可一旦暴雷往往就是大问题。CR 时看到循环,记得多留一个心眼。</p>
<p><img src="example-perf.png" alt></p>
<p>5)关注<strong>分布式事务</strong>。涉及远程服务调用,或者跨库更新的场景,都应考虑是否存在分布式事务问题,以及适用何种处理方式,是依赖框架保证强一致性,还是记录异常数据保证最终一致性,抑或是直接忽略?</p>
<p>6)关注<strong>架构设计</strong>。代码有代码规范,架构有架构规范。面对一个新功能的 MR(Merge Request),除了检查架构规范,还应推敲其架构设计,比如是否符合整洁架构三原则,无依赖环原则,稳定依赖原则,稳定抽象原则。</p>
<p><img src="example-arch-convention.png" alt></p>
<p><img src="example-arch-design.png" alt></p>
<p>除了线上 CR,还有一种特殊的线下 CR 方法,就是跳过 MR,直接拉取代码,进行整体 CR,将评审意见在代码中标记为 <code>TODO</code> 或者 <code>FIXME</code>,然后 @ 相关开发进行改进。这样做最大的好处,就是避免受单个 MR 的影响,掉入只见树木不见森林的陷阱。</p>
<p><img src="example-offline.png" alt></p>
<h3 id="2-2-团队层:衣带渐宽终不悔,为伊消得人憔悴"><a href="#2-2-团队层:衣带渐宽终不悔,为伊消得人憔悴" class="headerlink" title="2.2 团队层:衣带渐宽终不悔,为伊消得人憔悴"></a>2.2 团队层:衣带渐宽终不悔,为伊消得人憔悴</h3><p>接下来再看第二层,如何从团队视角认识 CR。前面说了,CR 本质上是一种讨论,培根说过「读书使人完整,讨论使人完备」,从个人到团队,CR 分别意味着什么?</p>
<ul>
<li><p>提升<strong>自我觉察</strong>能力:这是从个人视角来看,当你知道你写的代码会有另一双眼睛来审阅,那你写代码时就会保持一份警觉,放弃天知、地知、我知、你不知的幻想,认认真真写好每一行代码。</p>
</li>
<li><p>建立良好<strong>开发节奏</strong>:这是从团队视角来看,CR 是团队的同步器,每个人既是自己 MR 的作者,又是别人 MR 的评审者,从 MR 到 CR,再从 CR 到 MR,构成了每个工作日最动听的乐章。</p>
</li>
<li><p>高频次的<strong>团队活动</strong>:这也是从团队视角来看,CR 既然是讨论,那么就不仅仅是一个人的事,而是一种团队活动,一种高频次、高质量、低成本的极具性价比的团队活动。</p>
</li>
</ul>
<h3 id="2-3-文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处"><a href="#2-3-文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处" class="headerlink" title="2.3 文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处"></a>2.3 文化层:众里寻他千百度,蓦然回首,那人却在,灯火阑珊处</h3><p>最后是文化层,CR 既是传帮带文化的重要组成,又是工程师文化的日常体现。</p>
<ul>
<li><p><strong>传帮带文化</strong>的重要组成:资深工程师 CR 初级工程师的代码,可以给予高频次、高质量的指导;初级工程师 CR 资深工程师的代码,可以欣赏、学习高手如何把玩代码,取其精华去其糟粕。</p>
</li>
<li><p><strong>工程师文化</strong>的日常体现:协作、高效、进取、影响力,这些在各大互联网公司的工程师文化中频频出现的关键词,无一不与 CR 紧密相连。不夸张的说,工程师文化香不香,就看 CR 做的好不好。</p>
</li>
</ul>
<h2 id="3-行:CR-高效之法"><a href="#3-行:CR-高效之法" class="headerlink" title="3 行:CR 高效之法"></a>3 行:CR 高效之法</h2><p>认识完 CR,我们再来探讨一下如何高效的进行 CR。在我看来,高效 CR 首先有赖于以下几个客观条件和主观条件。</p>
<p>客观上来看,<strong>和谐的工程师文化</strong>和<strong>清晰的代码规范</strong>是高效 CR 的两块基石。所谓和谐的工程师文化,就是说团队对代码秉持开放的心态,不藏着掖着,以写好代码为荣,以写坏代码为耻,持续关注代码质量。而清晰的代码规范,一方面提高了代码的可读性,另一方面也统一了编码风格,极大的减少了不同代码风格对评审者注意力的干扰。</p>
<p>主观上来看,对评审者而言,第一要端正态度,<strong>保持谦卑的心态</strong>,人非圣贤孰能无过,择其善者而从之,其不善者而改之。第二要谨记<strong>评审的对象是代码,而不是人</strong>,你写下的每一条评审意见都应基于客观事实和数据,做到有理有据,不带个人情绪。</p>
<p>基于我多年 CR 的实操经验,结合<a href="https://github.com/google/eng-practices/blob/master/review/index.md" target="_blank" rel="noopener">Google Code Review Developer Guide</a>,我整理了一些高效 CR 的最佳实践,供你参考:</p>
<ul>
<li>依据个人偏好每天固定几个时间段专门用于 CR,我的习惯是出门前和下班前。CR 耗费的脑力丝毫不亚于编码,甚至更高,CR 过程中需要高度集中注意力。清醒的头脑和无干扰的环境,是提出高质量的评审意见的秘诀。</li>
<li>除了固定时间段,任务切换期间也是 CR 的好时机。</li>
<li>每次 CR 尽量控制在 15~30 分钟以内,超过 30 分钟应休息一会。</li>
<li>收到 MR 之后,先判断一下 MR 的性质,如果是 Bug Fix 类型的 MR,应尽快评审,如果是新功能 MR,则可以等待下一个 CR 窗口。</li>
<li>从收到 MR 到 CR 结束,最长不要超过 1 个工作日。</li>
<li>开始 CR 之前先要搞清楚 MR 要解决的问题背景。</li>
<li>CR 就像读书,先看目录(改动的文件列表),再精读重点章节(包含核心业务逻辑的代码),最后扫读剩余章节。</li>
<li>如果改动的文件数量较多,可以打开 IDE,切换到源分支,方便在 CR 过程中随时打开相关代码进行阅读。</li>
<li>评审核心代码时,如果发现严重问题,应立刻终止 CR,找 MR 提交者当面讨论。</li>
<li>如果 MR 提交者对评审意见提出异议,评审者应找提交者当面讨论,避免在评论区互踢皮球。</li>
<li>合并代码之前应确保所有评审意见都被妥善处理。</li>
<li>记得点赞。CR 不是只能提意见,看到优雅的代码不要吝啬你的表扬。</li>
</ul>
<h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>2019 年 Stack Overflow 的一份<a href="https://insights.stackoverflow.com/survey/2019" target="_blank" rel="noopener">调查报告</a>显示,超过 7 成的程序员会把 CR 当做日常工作的一部分,近 1/3 的程序员每周在 CR 上花费 2~3 个小时,还有 1/3 的程序员每周花费 4~5 个小时。心里默默算一下,你是在拖后腿还是领路者?如果你还没做过 CR,那么赶紧行动起来;如果你已经在 CR,很好,请继续保持。一花一世界,一叶一菩提,码中自有乾坤。CR,走起!</p>
<h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul>
<li><a href="https://slides.com/emacooshen/codereview2" target="_blank" rel="noopener">如何完成一次高质量的CODE REVIEW?(2020版)</a></li>
<li><a href="https://coolshell.cn/articles/1302.html" target="_blank" rel="noopener">陈皓:CODE REVIEW中的几个提示</a></li>
<li><a href="https://coolshell.cn/articles/11432.html" target="_blank" rel="noopener">陈皓:从CODE REVIEW 谈如何做技术</a></li>
<li><a href="https://www.techug.com/post/effective-code-reviews.html" target="_blank" rel="noopener">如何做有效的代码审查?我有这些建议</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Code Review </tag>
</tags>
</entry>
<entry>
<title><![CDATA[【JDK 11】关于 Java 模块系统,看这一篇就够了]]></title>
<url>http://emacoo.cn/coding/java-module-system/</url>
<content type="html"><![CDATA[<blockquote>
<p>继 2014 年 3 月 Java 8 发布之后,时隔 4 年,2018 年 9 月,Java 11 如期发布,其间间隔了 Java 9 和 Java 10 两个非LTS(Long Term Support)版本。作为最新的LTS版本,相比 Java 8,Java 11 包含了模块系统、改用 G1 作为默认 GC 算法、反应式流 Flow、新版 HttpClient 等诸多特性。作为 JDK 11 升级系列的第一篇,本文将介绍此次升级最重要的特性——模块系统。</p>
</blockquote>
<h2 id="1-模块系统简介"><a href="#1-模块系统简介" class="headerlink" title="1 模块系统简介"></a>1 模块系统简介</h2><p>如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。模块系统,项目代号 <a href="http://openjdk.java.net/projects/jigsaw/" target="_blank" rel="noopener">Jigsaw</a>,最早于 2008 年 8 月提出(比 Martin Fowler <a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">提出</a>微服务还早 6 年),2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。</p>
<p>那么什么是模块系统?官方的<a href="https://www.oracle.com/corporate/features/understanding-java-9-modules.html" target="_blank" rel="noopener">定义</a>是<code>A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.</code>如<em>图-1</em>所示,模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 <code>module-info.class</code> 文件,也即 <code>module descriptor</code>。 <code>module descriptor</code> 包含以下信息:</p>
<ul>
<li>模块名称</li>
<li>依赖哪些模块</li>
<li>导出模块内的哪些包(允许直接 <code>import</code> 使用)</li>
<li>开放模块内的哪些包(允许通过 Java 反射访问)</li>
<li>提供哪些服务</li>
<li>依赖哪些服务</li>
</ul>
<p><img src="jigsaw2.png" alt></p>
<p><em>图-1: Java 9 Module</em></p>
<p>也就是说,任意一个 jar 文件,只要加上一个合法的 <code>module descriptor</code>,就可以升级为一个模块。这个看似微小的改变,到底可以带来哪些好处?在我看来,至少带来四方面的好处。</p>
<p>第一,原生的依赖管理。有了模块系统,Java 可以根据 <code>module descriptor</code> 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。同时,由于模块系统不允许不同模块导出相同的包(即 <code>split package</code>,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。</p>
<p>第二,精简 JRE。引入模块系统之后,JDK 自身被划分为 94 个模块(参见<em>图-2</em>)。通过 Java 9 新增的 <code>jlink</code> 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小 JRE 大小。得益于此,JRE 11 的大小仅为 JRE 8 的 53%,从 218.4 MB缩减为 116.3 MB,JRE 中广为诟病的巨型 jar 文件 <code>rt.jar</code> 也被移除。更小的 JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。</p>
<p><img src="jigsaw3.png" alt></p>
<p><em>图-2: The Modular JDK</em></p>
<p>第三,更好的兼容性。自打 Java 出生以来,就只有 4 种包可见性,这让 Java 对面向对象的三大特征之一封装的支持大打折扣,类库维护者对此叫苦不迭,只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用,擅自使用后果自负云云。Java 9 之后,利用 <code>module descriptor</code> 中的 <code>exports</code> 关键词,模块维护者就精准控制哪些类可以对外开放使用,哪些类只能内部使用,换句话说就是不再依赖文档,而是由编译器来保证。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。</p>
<p><img src="public.png" alt></p>
<p><em>图-3: Java Accessibility</em></p>
<p>第四,提升 Java 语言开发效率。Java 9 之后,Java 像开挂了一般,一改原先一延再延的风格,严格遵循每半年一个大版本的发布策略,从 2017 年 9 月到 2020 年 3 月,从 Java 9 到 Java 14,三年时间相继发布了 6 个版本,无一延期,参见<em>图-4</em>。这无疑跟模块系统的引入有莫大关系。前文提到,Java 9 之后,JDK 被拆分为 94 个模块,每个模块有清晰的边界(<code>module descriptor</code>)和独立的单元测试,对于每个 Java 语言的开发者而言,每个人只需要关注其所负责的模块,开发效率因此大幅提升。这其中的差别,就好比单体应用架构升级到微服务架构一般,版本迭代速度不快也难。</p>
<p><img src="lifecycle.png" alt></p>
<p><em>图-4: Java SE Lifecycle</em></p>
<h2 id="2-基础篇"><a href="#2-基础篇" class="headerlink" title="2 基础篇"></a>2 基础篇</h2><h3 id="2-1-module-descriptor"><a href="#2-1-module-descriptor" class="headerlink" title="2.1 module descriptor"></a>2.1 module descriptor</h3><p>上面提到,模块的核心在于 <code>module descriptor</code>,对应根目录下的 <code>module-info.class</code> 文件,而这个 class 文件是由源代码根目录下的 <code>module-info.java</code> 编译生成。Java 为 <code>module-info.java</code> 设计了专用的语法,包含 <code>module</code>、 <code>requires</code>、<code>exports</code> 等多个关键词(参见<em>图-5</em>)。</p>
<p><img src="commands.png" alt></p>
<p><em>图-5: module-info.java 语法</em></p>
<p>语法解读:</p>
<ul>
<li><code>[open] module <module></code>: 声明一个模块,模块名称应全局唯一,不可重复。加上 <code>open</code> 关键词表示模块内的所有包都允许通过 Java 反射访问,模块声明体内不再允许使用 <code>opens</code> 语句。</li>
<li><code>requires [transitive] <module></code>: 声明模块依赖,一次只能声明一个依赖,如果依赖多个模块,需要多次声明。加上 <code>transitive</code> 关键词表示传递依赖,比如模块 A 依赖模块 B,模块 B 传递依赖模块 C,那么模块 A 就会自动依赖模块 C,类似于 Maven。</li>
<li><code>exports <package> [to <module1>[, <module2>...]]</code>: 导出模块内的包(允许直接 <code>import</code> 使用),一次导出一个包,如果需要导出多个包,需要多次声明。如果需要定向导出,可以使用 <code>to</code> 关键词,后面加上模块列表(逗号分隔)。</li>
<li><code>opens <package> [to <module>[, <module2>...]]</code>: 开放模块内的包(允许通过 Java 反射访问),一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放,可以使用 <code>to</code> 关键词,后面加上模块列表(逗号分隔)。</li>
<li><code>provides <interface | abstract class> with <class1>[, <class2> ...]</code>: 声明模块提供的 Java SPI 服务,一次可以声明多个服务实现类(逗号分隔)。</li>
<li><code>uses <interface | abstract class></code>: 声明模块依赖的 Java SPI 服务,加上之后模块内的代码就可以通过 <code>ServiceLoader.load(Class)</code> 一次性加载所声明的 SPI 服务的所有实现类。</li>
</ul>
<h3 id="2-2-p-amp-m-参数"><a href="#2-2-p-amp-m-参数" class="headerlink" title="2.2 -p & -m 参数"></a>2.2 -p & -m 参数</h3><p>Java 9 引入了一系列新的参数用于编译和运行模块,其中最重要的两个参数是 <code>-p</code> 和 <code>-m</code>。<code>-p</code> 参数指定模块路径,多个模块之间用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同时适用于 <code>javac</code> 命令和 <code>java</code> 命令,用法和Java 8 中的 <code>-cp</code> 非常类似。<code>-m</code> 参数指定待运行的模块主函数,输入格式为<code>模块名/主函数所在的类名</code>,仅适用于 <code>java</code> 命令。两个参数的基本用法如下:</p>
<ul>
<li><p><code>javac -p <module_path> <source></code></p>
</li>
<li><p><code>java -p <module_path> -m <module>/<main_class></code></p>
</li>
</ul>
<h3 id="2-3-Demo-示例"><a href="#2-3-Demo-示例" class="headerlink" title="2.3 Demo 示例"></a>2.3 Demo 示例</h3><p>为了帮助你理解 <code>module descriptor</code> 语法和新的 Java 参数,我专门设计了一个<a href="https://github.com/emac/jmods-demo" target="_blank" rel="noopener">示例工程</a>,其内包含了 5 个模块:</p>
<ul>
<li>mod1 模块: 主模块,展示了使用服务实现类的两种方式。</li>
<li>mod2a 模块: 分别导出和开放了一个包,并声明了两个服务实现类。</li>
<li>mod2b 模块: 声明了一个未公开的服务实现类。</li>
<li>mod3 模块: 定义 SPI 服务(<code>IEventListener</code>),并声明了一个未公开的服务实现类。</li>
<li>mod4 模块: 导出公共模型类。</li>
</ul>
<p><img src="demo.png" alt></p>
<p><em>图-6: 包含 5 个模块的示例工程</em></p>
<p>先来看一下主函数,方式 1 展示了直接使用 mod2 导出和开放的两个 <code>IEventListener</code> 实现类,方式 2 展示了通过 Java SPI 机制使用所有的 <code>IEventListener</code> 实现类,无视其导出/开放与否。方式 2 相比 方式 1,多了两行输出,分别来自于 mod2b 和 mod3 通过 <code>provides</code> 关键词提供的服务实现类。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EventCenter</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ReflectiveOperationException </span>{</span><br><span class="line"> <span class="comment">// 方式1:通过exports和opens</span></span><br><span class="line"> System.out.println(<span class="string">"Demo: Direct Mode"</span>);</span><br><span class="line"> <span class="keyword">var</span> listeners = <span class="keyword">new</span> ArrayList<IEventListener>();</span><br><span class="line"> <span class="comment">// 使用导出类</span></span><br><span class="line"> listeners.add(<span class="keyword">new</span> EchoListener());</span><br><span class="line"> <span class="comment">// 使用开放类</span></span><br><span class="line"> <span class="comment">// compile error: listeners.add(new ReflectEchoListener());</span></span><br><span class="line"> listeners.add((IEventListener<String>) Class.forName(<span class="string">"mod2a.opens.ReflectEchoListener"</span>).getDeclaredConstructor().newInstance());</span><br><span class="line"> <span class="keyword">var</span> event = Events.newEvent();</span><br><span class="line"> listeners.forEach(l -> l.onEvent(event));</span><br><span class="line"> System.out.println();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 方式2:通过SPI</span></span><br><span class="line"> System.out.println(<span class="string">"Demo: SPI Mode"</span>);</span><br><span class="line"> <span class="comment">// 加载所有的IEventListener实现类,无视其导出/开放与否</span></span><br><span class="line"> var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());</span><br><span class="line"> <span class="comment">// compile error: listeners.add(new InternalEchoListener());</span></span><br><span class="line"> <span class="comment">// compile error: listeners.add(new SpiEchoListener());</span></span><br><span class="line"> <span class="keyword">var</span> event2 = Events.newEvent();</span><br><span class="line"> listeners2.forEach(l -> l.onEvent(event2));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><em>代码-1: mod1.EventCenter.java</em></p>
<p>命令行下执行<code>./build_mods.sh</code>,得到输出如下,结果和预期一致。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Demo: Direct Mode</span><br><span class="line">[echo] Event received: <span class="number">68</span>eb4671-c057-<span class="number">4</span>bc2-<span class="number">9653</span>-c31f5e3f72d2</span><br><span class="line">[reflect echo] Event received: <span class="number">68</span>eb4671-c057-<span class="number">4</span>bc2-<span class="number">9653</span>-c31f5e3f72d2</span><br><span class="line"></span><br><span class="line">Demo: SPI Mode</span><br><span class="line">[spi echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[reflect echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br><span class="line">[internal echo] Event received: <span class="number">678</span>d239a-<span class="number">77</span>ef-<span class="number">4</span>b7f-b7aa-e76041fcdf47</span><br></pre></td></tr></table></figure>
<p><em>代码-2: EventCenter 结果输出</em></p>
<h2 id="3-进阶篇"><a href="#3-进阶篇" class="headerlink" title="3 进阶篇"></a>3 进阶篇</h2><p>看到这里,相信创建和运行一个新的模块应用对你而言已经不是问题了,可问题是老的 Java 8 应用怎么办?别着急,我们先来了解两个高级概念,未命名模块(unnamed module)和自动模块(automatic module)。</p>
<p><img src="cp.png" alt></p>
<p><em>图-7: 未命名模块 vs 自动模块</em></p>
<p>一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。注意,自动模块也属于命名模块的范畴,其名称是模块系统基于 jar 文件名自动推导得出的,比如 com.foo.bar-1.0.0.jar 文件推导得出的自动模块名是 com.foo.bar。<em>图-7</em>列举了未命名模块和自动模块行为上的区别,除此之外,两者还有一个关键区别,分裂包规则适用于自动模块,但对未命名模块无效,也即多个未命名模块可以导出同一个包,但自动模块不允许。</p>
<p>未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 <code>module descriptor</code>),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。进一步的解读可以参阅<a href="http://openjdk.java.net/projects/jigsaw/spec/sotms/" target="_blank" rel="noopener">官方白皮书</a>的相关章节。</p>
<p>基于未命名模块和自动模块,相应的就产生了两种老版本应用的迁移策略,或者说模块化策略。</p>
<h3 id="3-1-Bottom-up-自底向上策略"><a href="#3-1-Bottom-up-自底向上策略" class="headerlink" title="3.1 Bottom-up 自底向上策略"></a>3.1 Bottom-up 自底向上策略</h3><p>第一种策略,叫做自底向上(bottom-up)策略,即根据 jar 包依赖关系(如果依赖关系比较复杂,可以使用 <code>jdeps</code> 工具进行分析),沿着依赖树自底向上对 jar 包进行模块化改造(在 jar 包的源代码根目录下添加合法的模块描述文件 <code>module-info.java</code>)。初始时,所有 jar 包都是非模块化的,全部置于类路径下(转为未命名模块),应用以传统方式启动。然后,开始自底向上对 jar 包进行模块化改造,改造完的 jar 包就移到模块路径下,这期间应用仍以传统方式启动。最后,等所有 jar 包都完成模块化改造,应用改为 <code>-m</code> 方式启动,这也标志着应用已经迁移为真正的 Java 9 应用。以上面的示例工程为例,</p>
<p><img src="bottom-up.png" alt></p>
<p><em>图-8: Bottom-up模块化策略</em></p>
<p>1) 假设初始时,所有 jar 包都是非模块化的,此时应用运行命令为:</p>
<p><code>java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter</code></p>
<p>2) 对 mod3 和 mod4 进行模块化改造。完成之后,此时 mod1, mod2a, mod2b 还是普通的 jar 文件,新的运行命令为:</p>
<p><code>java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter</code></p>
<p>对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径,这个很好理解,因为这两个 jar 包已经改造成了真正的模块。其次,多了一个额外的参数 <code>--add-modules mod3,mod4</code>,这是为什么呢?这就要谈到模块系统的模块发现机制了。</p>
<p>不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module),然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块(observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。那么根模块是如何确定的呢?对于运行时而言,如果应用是通过 <code>-m</code> 方式启动的,那么根模块就是 <code>-m</code> 指定的主模块;如果应用是通过传统方式启动的,那么根模块就是所有的 <code>java.*</code> 模块即 JRE(参见<em>图-2</em>)。回到前面的例子,如果不加 <code>--add-modules</code> 参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块,就会报 <code>java.lang.ClassNotFoundException</code> 异常。如你所想,<code>--add-modules</code> 参数的作用就是手动指定额外的根模块,这样应用就可以正常运行了。</p>
<p>3) 接着完成 mod2a、mod2b 的模块化改造,此时运行命令为:</p>
<p><code>java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter</code></p>
<p>由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到 <code>--add-modules</code> 参数里了。</p>
<p>4) 最后完成 mod1 的模块化改造,最终运行命令就简化为:</p>
<p><code>java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter</code></p>
<p>注意此时应用是以 <code>-m</code> 方式启动,并且指定了 mod1 为主模块(也是根模块),因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。</p>
<h3 id="3-2-Top-down-自上而下策略"><a href="#3-2-Top-down-自上而下策略" class="headerlink" title="3.2 Top-down 自上而下策略"></a>3.2 Top-down 自上而下策略</h3><p>自底向上策略很容易理解,实施路径也很清晰,但它有一个隐含的假设,即所有 jar 包都是可以模块化的,那如果其中有 jar 包无法进行模块化改造(比如 jar 包是一个第三方类库),怎么办?别慌,我们再来看第二种策略,叫做自上而下(top-down)策略。</p>
<p>它的基本思路是,根据 jar 包依赖关系,从主应用开始,沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类,一类是可以改造的,一类是无法改造的。对于第一类,我们仍然采用自底向上策略进行改造,直至主应用完成改造,对于第二类,需要从一开始就放入模块路径,即转为自动模块。这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包,这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块,并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外,其它命名模块是不允许访问未命名模块的类),这样就保证自动模块自身可以照常访问其他类。等到主应用完成模块化改造,应用的启动方式就可以改为 <code>-m</code> 方式。</p>
<p>还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造,那么最终改造完之后,虽然应用运行命令和之前一样还是<code>java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter</code>,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造,借由模块系统转为自动模块。</p>
<p><img src="top-down.png" alt></p>
<p><em>图-9: Top-down模块化策略</em></p>
<p>看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况,如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块,要么自己动手丰衣足食 Hack 一个版本。当然,你也可以试试找到这些自动模块的维护者们,让他们 PK 一下决定谁才是这个分裂包的主人。</p>
<h2 id="4-番外篇"><a href="#4-番外篇" class="headerlink" title="4 番外篇"></a>4 番外篇</h2><p>有关模块系统的介绍到这就基本结束了,简单回顾一下,首先我介绍了什么是模块、模块化的好处,接着给出了定义模块的语法,和编译、运行模块的命令,并辅以一个示例工程进行说明,最后详细阐述了老版本应用模块化改造的思路。现在我们再来看一些跟模块系统比较相似的框架和工具,以进一步加深你对模块系统的理解。</p>
<h3 id="4-1-vs-OSGi"><a href="#4-1-vs-OSGi" class="headerlink" title="4.1 vs OSGi"></a>4.1 vs OSGi</h3><p>说起模块化,尤其在 Java 界,那么肯定绕不过 OSGi 这个模块系统的鼻祖。OSGi 里的 bundle 跟模块系统里的模块非常相似,都是以 jar 文件的形式存在,每个 bundle 有自己的名称,也会定义依赖的 bundle、导出的包、发布的服务等。所不同的是,OSGi bundle 可以定义版本,还有生命周期的概念,包括 installed、resolved、uninstalled、starting、active、stopping 6 种状态,所有 bundle 都由 OSGi 容器进行管理,并且在同一个 OSGi 容器里面允许同时运行同一个 bundle 的多个版本,甚至每个 bundle 有各自独立的 classloader。以上种种特性使得 OSGi 框架变得非常重,在微服务盛行的当下,越来越被边缘化。</p>
<h3 id="4-2-vs-Maven"><a href="#4-2-vs-Maven" class="headerlink" title="4.2 vs Maven"></a>4.2 vs Maven</h3><p>Maven 的依赖管理和模块系统存在一些相似之处,Maven 里的 artifact 对应模块 ,都是以 jar 文件的形式存在,有名称,可以声明传递依赖。不同之处在于,Maven artifact 支持版本,但缺少包一级的信息,也没有服务的概念。如果 Java 一出生就带有模块系统,那么 Maven 的依赖管理大概率就会直接基于模块系统来设计了。</p>
<h3 id="4-3-vs-ArchUnit"><a href="#4-3-vs-ArchUnit" class="headerlink" title="4.3 vs ArchUnit"></a>4.3 vs ArchUnit</h3><p>ArchUnit 在包可见性方面的控制能力和模块系统相比,有过之而无不及,并且可以细化到类、方法、属性这一级。但 ArchUnit 缺少模块一级的控制,模块系统的出现正好补齐了 ArchUnit 这一方面的短板,两者相辅相成、相得益彰,以后落地架构规范也省了很多口水。</p>
<h2 id="5-彩蛋"><a href="#5-彩蛋" class="headerlink" title="5 彩蛋"></a>5 彩蛋</h2><p>如果你能看到这里,恭喜你已经赢了 90% 的读者。为了表扬你的耐心,免费赠送一个小彩蛋,给你一个 jar 文件,如何用最快的速度判别它是不是一个模块?它又是如何定义的?试试看 <code>jar -d -f <jar_file></code>。</p>
<p>有关 Java 模块系统的介绍就到这里了,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>分享,和大家一起过过招。下期再见。</p>
<h2 id="6-参考"><a href="#6-参考" class="headerlink" title="6 参考"></a>6 参考</h2><ul>
<li><a href="https://www.ibm.com/developerworks/cn/java/the-new-features-of-Java-9/index.html" target="_blank" rel="noopener">Java 9 新特性概述</a></li>
<li><a href="https://openjdk.java.net/jeps/261" target="_blank" rel="noopener">JEP 261: Module System</a></li>
<li><a href="http://tutorials.jenkov.com/java/modules.html" target="_blank" rel="noopener">Java Modules</a></li>
<li><a href="https://www.oracle.com/corporate/features/understanding-java-9-modules.html" target="_blank" rel="noopener">Understanding Java 9 Modules</a></li>
<li><a href="https://www.oracle.com/java/java9-screencasts.html" target="_blank" rel="noopener">Java 9 Expert Insights</a></li>
<li><a href="https://www.cnblogs.com/IcanFixIt/p/6947763.html" target="_blank" rel="noopener">Java 9 揭秘(2. 模块化系统)</a></li>
</ul>
]]></content>
<categories>
<category> coding </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Spring Cloud】详解Feign常用配置]]></title>
<url>http://emacoo.cn/backend/spring-cloud-feign-timeout/</url>
<content type="html"><![CDATA[<h2 id="1-Feign常用配置"><a href="#1-Feign常用配置" class="headerlink" title="1 Feign常用配置"></a>1 Feign常用配置</h2><p>搭载着Spring Cloud的顺风车,Feign正以席卷之势成为使用Spring架构的大大小小互联网公司发起HTTP调用的首选框架。基于接口的声明式定义、客户端负载均衡、断路器和后备方法(fallback)是Feign相对上一代HTTP调用框架(比如Spring Template,Apache HttpClient)的四大优势。</p>
<p>类似于Retrofit和OkHttp的关系,Feign实际上是对普通HTTP客户端的一层封装,其目的是降低集成成本、提升可靠性。Feign支持三种HTTP客户端,包括JDK自带的HttpURLConnection、Apache HttpClient和Square OkHttp,默认使用Apache HttpClient。</p>
<ul>
<li>HttpURLConnection:不支持线程池,一般不会选用。</li>
<li>HttpClient:相比OkHttp,HttpClient并没有明显的优势,可能是因为使用更广泛,所以被Feign选为默认实现。从5.0版本开始才支持HTTP/2。</li>
<li>OkHttp:开发Android应用的首选HTTP客户端,支持HTTP/2,通过设置<code>feign.okhttp.enabled=true</code>启用。</li>
</ul>
<p>Feign提供了两大类配置属性来配置上述三种HTTP客户端,<code>feign.client.*</code>和<code>feign.httpclient.*</code>,前者支持按实例进行配置(注解-1),后者全局共享一套配置,包含线程池配置,但只影响HttpClient和OkHttp,不影响HttpURLConnection,具体关系见下表。</p>
<blockquote>
<p>注解-1:所谓按实例进行配置,就是指每个FeignClient实例都可以通过<code>feign.client.<feignClientName>.*</code>来单独进行配置,注意首字母小写。而<code>feign.client.default.*</code>表示默认配置。</p>
</blockquote>
<table>
<thead>
<tr>
<th>HTTP客户端</th>
<th>连接超时时间</th>
<th>请求超时时间</th>
<th>线程存活时间</th>
<th>线程池最大连接数(全局)</th>
<th>线程池最大连接数(单个HOST)</th>
</tr>
</thead>
<tbody>
<tr>
<td>HttpURLConnection</td>
<td>feign.client.<code>[default|<feignClientName>].connect-timeout</code><br>默认值:10秒</td>
<td>feign.client.<code>[default|<feignClientName>].read-timeout</code><br>默认值:60秒</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
</tr>
<tr>
<td>HttpClient</td>
<td>feign.httpclient.connection-timeout<br>默认值:2秒</td>
<td>默认值:-1(RequestConfig.Builder.socketTimeout)</td>
<td>feign.httpclient.time-to-live<br>默认值:900秒</td>
<td>feign.httpclient.max-connections<br>默认值:200</td>
<td>feign.httpclient.max-connections-per-route<br>默认值:50</td>
</tr>
<tr>
<td>OkHttp</td>
<td>feign.httpclient.connection-timeout<br>默认值:2秒</td>
<td>feign.client.<code>[default|<feignClientName>].read-timeout</code><br>默认值:10秒</td>
<td>feign.httpclient.time-to-live<br>默认值:900秒</td>
<td>feign.httpclient.max-connections<br>默认值:200</td>
<td>N/A</td>
</tr>
</tbody>
</table>
<p>从上表可以看到,Feign提供了两个连接超时配置,HttpURLConnection使用<code>feign.client.[default|<feignClientName>].connect-timeout</code>,而HttpClient和OkHttp则使用<code>feign.httpclient.connection-timeout</code>,这一点要尤其注意。</p>
<h2 id="2-启用Hystrix"><a href="#2-启用Hystrix" class="headerlink" title="2 启用Hystrix"></a>2 启用Hystrix</h2><p>通过设置<code>feign.hystrix.enabled=true</code>可以启用Feign的断路器支持(基于Hystrix)。跟Feign一样,Hystrix也支持按实例进行配置,详细配置属性参见<a href="https://github.com/Netflix/Hystrix/wiki/Configuration" target="_blank" rel="noopener">官方文档</a>。</p>
<p>由于Hystrix默认的请求超时时间为1秒,很容易触发超时异常,所以往往需要调大。调大超时时间有两种方式,</p>
<ul>
<li>第一种方式,通过<code>hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds</code>设置默认超时时间,影响所有请求。</li>
<li>第二种方式,如果你不想改变所有请求的超时时间,那么可以通过<code>hystrix.command.<HystrixCommandKey>.execution.isolation.thread.timeoutInMilliseconds</code>单独设置某个Hystrix Command的超时时间。那么问题来了,Feign下面,这个Hystrix Command Key到底是什么呢,是和Feign Client Name一样吗?答案是否定的。Feign下面,一个Hystrix Command对应的是Feign Client的一个方法,因此Hystrix Command Key的定义为<code><FeignClientName>#<methodName>(<arg1ClassName>,<arg2ClassName>...)</code>,注意首字母大写,详见<code>SetterFactory.Default#create()</code>方法。</li>
</ul>
<h2 id="3-小结"><a href="#3-小结" class="headerlink" title="3 小结"></a>3 小结</h2><p>不管是Spring还是Spring Cloud,由于整个生态过于庞大,因此即便是官方文档,也只能勉强覆盖各个组件的大体框架,一旦深入细节就只能靠开发者自己研读源码来寻找答案。就像Linus Torvalds说的,<code>Talk is cheap. Show me the code.</code></p>
<p><img src="quote-talk-is-cheap-show-me-the-code-linus-torvalds.jpg" alt></p>
<h2 id="4-参考"><a href="#4-参考" class="headerlink" title="4 参考"></a>4 参考</h2><ul>
<li><a href="https://cloud.spring.io/spring-cloud-openfeign/reference/html/" target="_blank" rel="noopener">Spring Cloud OpenFeign</a></li>
<li><a href="https://www.cnblogs.com/wlandwl/p/feign.html" target="_blank" rel="noopener">Spring cloud Feign 深度学习与应用</a></li>
<li><a href="https://www.jianshu.com/p/c836a283631e" target="_blank" rel="noopener">Spring Cloud组件那么多超时设置,如何理解和运用?</a></li>
</ul>
]]></content>
<categories>
<category> backend </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Spring Cloud </tag>
</tags>
</entry>
<entry>
<title><![CDATA[MySQL Connect/J 8.0时区陷阱]]></title>
<url>http://emacoo.cn/devops/mysql-timezone/</url>
<content type="html"><![CDATA[<p><img src="timezone-map.jpg" alt></p>
<p>最近公司正在升级Spring Boot版本(从1.5升级到2.1),其间踩到一个非常隐晦的MySQL时区陷阱,具体来说,就是数据库读出的历史数据的时间和实际时间差了14个小时,而新写入的数据又都正常。如果你之前也是使用默认的MySQL时区配置,那么大概率会碰到这个问题,深究其背后的原因又涉及到很多技术细节,故整理出来分享给大家。</p>
<p>首先来看一下原因。升级到Boot 2.1之后,MySQL Connect/J版本也随之升级到8.0,会优先使用连接参数(<code>serverTimezone</code>)中指定的时区,如果没有指定,则再使用数据库配置的时区,参考下面的<a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-other-changes.html" target="_blank" rel="noopener">官宣</a>(对应的源代码是<code>com.mysql.cj.protocol.a.NativeProtocol#configureTimezone()</code>)。由于我们之前数据库连接参数没有指定时区,并且数据库配置的是默认的<code>CST</code>时区(美国中部时区,即-6:00),所以读取出来的时间出现偏差。</p>
<blockquote>
<p>Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true:</p>
<ul>
<li>The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.)</li>
<li>The server’s time zone is overridden by setting the Connector/J connection property <code>serverTimezone</code> (for example, <code>serverTimezone=Europe/Paris</code>).</li>
</ul>
</blockquote>
<p>找到原因之后,解决办法就比较直白了,</p>
<p>方法一:数据库的连接参数添加<code>serverTimezone=Asia/Shanghai</code>或者<code>serverTimezone=GMT%2B8</code>。Boot 1.5下不需要添加此参数,但添加了也无妨。</p>
<p>方法二:修改MySQL数据库的time_zone配置,改为<code>+8:00</code>(默认是<code>SYSTEM</code>)。采用此方法,则不需要修改数据库连接参数。</p>
<p>方法二显然更优,一次修改,终生受益。但要注意,对于升级到Boot 2.1之后新生成的那批数据,如果包含时间类型的字段并且该字段值是应用指定的而不是数据库生成的(例如<code>DEFAULT CURRENT_TIMESTAMP</code>),那么需要手动修复(加上偏差的小时数)。</p>
<p>两个解决办法都很简单,有同学马上会问,为什么Boot 1.5下没有这个问题?为什么Boot 2.0下读取历史数据存在14个小时的偏差,而新生成的数据又是好的?要回答这两个问题,看官宣就不够了,需要读一下MySQL Connect/J的源代码。</p>
<p>谜题一,为什么Boot 1.5下没有这个问题?答案隐藏在<code>com.mysql.jdbc.ResultSetImpl</code>和<code>com.mysql.jdbc.ConnectionImpl</code>两个类的源代码中。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源代码:com.mysql.jdbc.ResultSetImpl</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> TimeZone <span class="title">getDefaultTimeZone</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// useLegacyDatetimeCode默认为true,因此使用connection的默认时区</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.useLegacyDatetimeCode ? <span class="keyword">this</span>.connection.getDefaultTimeZone() : <span class="keyword">this</span>.serverTimeZoneTz;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源代码:com.mysql.jdbc.ConnectionImpl</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ConnectionImpl</span><span class="params">(String hostToConnectTo, <span class="keyword">int</span> portToConnectTo, Properties info, String databaseToConnectTo, String url)</span> <span class="keyword">throws</span> SQLException </span>{</span><br><span class="line"> <span class="comment">// connection的默认时区使用的是JVM的默认时区,一般为操作系统的时区</span></span><br><span class="line"> <span class="comment">// We store this per-connection, due to static synchronization issues in Java's built-in TimeZone class...</span></span><br><span class="line"> <span class="keyword">this</span>.defaultTimeZone = TimeUtil.getDefaultTimeZone(getCacheDefaultTimezone());</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Boot 1.5下,MySQL Connect/J默认使用操作系统的时区(Asia/Shanghai,即+8:00),而忽略连接参数或者数据库指定的时区,因此不管是读数据还是写数据都是使用统一的时区,因此不存在时间偏差。</p>
<p>谜题二,为什么Boot 2.0下读取历史数据存在14个小时的偏差,而新生成的数据又是好的?升级到Boot 2.0之后,MySQL Connect/J改为使用数据库配置的CST时区,而历史数据是在Boot 1.5下的Asia/Shanghai时区生成的,因此读出来存在14(-6:00和+8:00之间)个小时的偏差。对于新生成的数据,由于同处在CST时区下,因此没有偏差。</p>
<p>解完这两个谜题,你可能还有些疑惑。那么接下来,结合数据流转的顺序,我们再来分析一下数据流转过程中时区的变化。</p>
<p><img src="mysql-timezone.png" alt></p>
<p>设定Application-1为数据生产方,Application-2为数据消费方,TZ-IN1为Application-1所处的时区,TZ-IN2为Application-1写入数据库的时区,TZ-OUT1为Application-2读出数据库的时区,TZ-OUT2为Application-2所处的时区。如前所述,TZ-IN2和TZ-OUT1由连接参数或者数据库配置决定。</p>
<p>整个数据流转过程,会涉及3次显式的时区转换和1次隐式的时区转换。</p>
<ul>
<li>转换①(显式):TZ-IN1转TZ-IN2,这个转换由MySQL Connect/J完成(参考<code>com.mysql.cj.ClientPreparedQueryBindings#setTimestamp()</code>,限于篇幅,此处不再展开分析)。</li>
<li>转换②(隐式):TZ-IN2转无时区,MySQL内部存储时间类型的字段时或者忽略时区(DateTime类型)或者使用UTC(Timestamp类型),参考MySQL官宣的时间类型部分。</li>
<li>转换③(显式):无时区转TZ-OUT1,将MySQL读出的无时区时间置为TZ-OUT1时区(参考<code>com.mysql.cj.result.SqlTimestampValueFactory#localCreateFromTimestamp()</code>)。</li>
<li>转换④(显式):TZ-OUT1转TZ-OUT2,这个转换由Application-2负责,一般在DAO层完成。</li>
</ul>
<p>仔细分析这4次时区转换,其中①、②、③都是由MySQL完成,正确性不用怀疑,但由于TZ-IN2和TZ-OUT1都是由应用指定,如果两者值不相同,那么最后结果就会出现偏差(我们踩到的就是这个坑)。至于④,那么就得靠应用来保证正确性了,一般也不会出错。说句题外话,不管是时区转换,还是其他类型的数据转换(比如字符集转换),我们可以发现,正确转换的关键在于数据接收方必须使用和数据发送方相同的格式。这看上去像是一句废话,却是解决此类问题的底层心法。</p>
<p>至此,这个MySQL Connect/J 8.0的时区陷阱就算被填平了,希望你从中有所收获。</p>
]]></content>
<categories>
<category> devops </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Java </tag>
<tag> MySQL </tag>
</tags>
</entry>
<entry>
<title><![CDATA[从零搭建一个基于Istio的服务网格]]></title>
<url>http://emacoo.cn/devops/istio-tutorial/</url>
<content type="html"><![CDATA[<blockquote>
<p><a href="http://emacoo.cn/arch/service-mesh-overview/">上篇</a>文章从微服务1.0时代的三大痛点(技术门槛高,多语言支持不足和代码侵入性强)说起,由此引出服务网格的起源和演化历史。但古语有云<code>纸上得来终觉浅,绝知此事要躬行</code>,不亲自撸一遍命令,怎敢跟人提服务网格?本篇我将教大家如何在本地从零搭建一个基于<a href="https://istio.io/" target="_blank" rel="noopener">Istio</a>的服务网格,从而对服务网格有一个更直观的认识。</p>
</blockquote>
<h2 id="1-通关密码:上上下下左左右右ABAB"><a href="#1-通关密码:上上下下左左右右ABAB" class="headerlink" title="1 通关密码:上上下下左左右右ABAB"></a>1 通关密码:上上下下左左右右ABAB</h2><ul>
<li>原料:Mac一台,VPN账号一枚</li>
<li>做法:依序安装和运行<a href="https://kubernetes.io/" target="_blank" rel="noopener">Kubernetes</a>,<a href="https://github.com/kubernetes/minikube" target="_blank" rel="noopener">Minikube</a>,Istio</li>
</ul>
<p><img src="mario.png" alt></p>
<h2 id="2-穿墙大法:Shadowsocks"><a href="#2-穿墙大法:Shadowsocks" class="headerlink" title="2 穿墙大法:Shadowsocks"></a>2 穿墙大法:Shadowsocks</h2><p>无论是Kubernetes、Minikube还是Istio,官方提供的安装文档都非常详尽,只要英文过关,依葫芦画瓢基本上都能跑通。但如果你在国内,还得加一个必要条件,学会如何<a href="https://zh.wikipedia.org/zh-hans/%E7%AA%81%E7%A0%B4%E7%BD%91%E7%BB%9C%E5%AE%A1%E6%9F%A5" target="_blank" rel="noopener">突破网络审查</a>,俗称翻墙。</p>
<p>Mac下的翻墙软件我首推<a href="https://shadowsocks.org/en/index.html" target="_blank" rel="noopener">Shadowsocks</a>,同时支持Socks5代理和HTTP代理,最新版本可以从<a href="https://github.com/shadowsocks/ShadowsocksX-NG/releases" target="_blank" rel="noopener">GitHub</a>下载。</p>
<h2 id="3-小Boss-kubectl"><a href="#3-小Boss-kubectl" class="headerlink" title="3 小Boss: kubectl!"></a>3 小Boss: kubectl!</h2><h3 id="3-1-安装"><a href="#3-1-安装" class="headerlink" title="3.1 安装"></a>3.1 安装</h3><p>Kubernetes是Istio首推的运行平台,因此作为第一步,我们首先来安装kubectl,Kubernetes的命令行工具,用来控制Kubernetes集群。根据<a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/" target="_blank" rel="noopener">官方文档</a>,Mac下安装kubectl只需要一行命令,<code>brew install kubectl</code>,这简单、极致的用户体验让你感动的想哭。But wait…</p>
<h3 id="3-2-穿墙1-Brew"><a href="#3-2-穿墙1-Brew" class="headerlink" title="3.2 穿墙1: Brew"></a>3.2 穿墙1: Brew</h3><p>你敲完命令,踌躇满志的按下回车之后,可能会发现,屏幕迟迟没有输出,10秒,30秒,1分钟,3分钟,10分钟。。。恭喜你,你被墙了!</p>
<p><a href="https://brew.sh/" target="_blank" rel="noopener">Brew</a>默认的镜像源是GitHub,而GitHub时不时会被墙,即使不被墙访问速度有时也慢的令人发指,导致Brew命令也常常超时甚至失败。解决办法要么<a href="https://segmentfault.com/a/1190000008274997" target="_blank" rel="noopener">换源</a>,要么给GitHub配上Socks5代理。对码农而言,我更推荐后一种,方法如下:</p>
<p>1) 打开~/.gitconfig文件,如果不存在则新建</p>
<p>2) 在文件末尾添加如下配置并保存:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[http "https://github.com"]</span><br><span class="line"> proxy = socks5://127.0.0.1:1086</span><br><span class="line">[https "https://github.com"]</span><br><span class="line"> proxy = socks5://127.0.0.1:1086</span><br></pre></td></tr></table></figure>
<p><em>注:<code>socks5://127.0.0.1:1086</code>是Shadowsocks默认开启的Socks5代理地址。</em></p>
<p>配上Socks5代理之后,一般就可以妥妥的运行Brew命令了。</p>
<h3 id="3-3-验证"><a href="#3-3-验证" class="headerlink" title="3.3 验证"></a>3.3 验证</h3><p>安装好kubectl之后,直接运行<code>kubectl version</code>查看版本号。完整的kubectl命令列表在<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands" target="_blank" rel="noopener">这里</a>可以找到。如果想进一步学习常见的kubectl命令,可以访问<a href="https://www.katacoda.com/courses/kubernetes/playground" target="_blank" rel="noopener">Kubernetes Playground</a>完成在线练习。</p>
<h2 id="4-中Boss-Minikube"><a href="#4-中Boss-Minikube" class="headerlink" title="4 中Boss: Minikube!"></a>4 中Boss: Minikube!</h2><h3 id="4-1-安装"><a href="#4-1-安装" class="headerlink" title="4.1 安装"></a>4.1 安装</h3><p>安装完kubectl,接下来就是在本地搭建Kubernetes集群,Minikube是最简单的一种搭建方式,它通过VM模拟了一个单节点的Kubernetes集群。<a href="https://kubernetes.io/docs/tutorials/stateless-application/hello-minikube/" target="_blank" rel="noopener">官方文档</a>给出了Mac下的安装命令。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 && \</span><br><span class="line">chmod +x minikube && \</span><br><span class="line">sudo mv minikube /usr/local/bin/</span><br></pre></td></tr></table></figure>
<p>Minikube默认使用的VM Driver是<a href="https://www.virtualbox.org/" target="_blank" rel="noopener">VirutalBox</a>,因此启动Minikube之前,还要安装VirtualBox。</p>
<h3 id="4-2-启动"><a href="#4-2-启动" class="headerlink" title="4.2 启动"></a>4.2 启动</h3><p>安装好Minikube和VirutalBox之后,可运行如下命令第一次启动Minikube:</p>
<p><code>minikube start --docker-env HTTP_PROXY=http://<本机IP>:1087 --docker-env HTTPS_PROXY=http://<本机IP>:1087</code></p>
<p><em>注:官方文档给出的启动命令带有<code>--vm-driver=xhyve</code>,而事实上最新版本的Minikube已经废弃了xhyve driver,应去除。</em></p>
<h3 id="4-3-穿墙2-Docker"><a href="#4-3-穿墙2-Docker" class="headerlink" title="4.3 穿墙2: Docker"></a>4.3 穿墙2: Docker</h3><p>你可能已经注意到,上面的启动命令中带了两个<code>--docker-env</code>参数,都指向了Shadowsocks开启的HTTP代理,为啥呢?还是因为墙。Minikube默认使用Docker作为容器运行时,并在VM中内置了一个Docker守护进程,使用的镜像源是<a href="https://hub.docker.com/" target="_blank" rel="noopener">DockerHub</a>。如果你经常使用Docker,那你一定知道在国内使用Docker一般都要修改镜像源(比如阿里云的<a href="https://cr.console.aliyun.com/#/accelerator" target="_blank" rel="noopener">容器镜像服务</a>)或者使用代理,否则拉取速度也是慢的令人发指。由于Minikube使用的是内置的Docker守护进程,使用代理更为方便,但要注意,开启Shadowsocks HTTP代理时,需要修改代理的侦听地址为本机IP,而不是默认的<code>127.0.0.1</code>,否则在VM中的Docker守护进程是无法访问到这个代理的。</p>
<p><em>注:<code>--docker-env</code>参数只有在第一次启动Minikube时需要,之后启动直接运行<code>minikube start</code>即可。如果需要修改代理地址,可编辑<code>~/.minikube/machines/minikube/config.json</code>文件。</em></p>
<h3 id="4-4-验证"><a href="#4-4-验证" class="headerlink" title="4.4 验证"></a>4.4 验证</h3><p>安装完Minikube之后,就可以试着创建第一个Kubernetes服务了,具体步骤参考<a href="https://kubernetes.io/docs/getting-started-guides/minikube/#quickstart" target="_blank" rel="noopener">官方文档</a>。</p>
<h2 id="5-大Boss-Istio"><a href="#5-大Boss-Istio" class="headerlink" title="5 大Boss: Istio!"></a>5 大Boss: Istio!</h2><h3 id="5-1-安装"><a href="#5-1-安装" class="headerlink" title="5.1 安装"></a>5.1 安装</h3><p>拿到了kubectl和Minikube两大神器,搭建Istio可以说是水到渠成了。基本步骤如下,</p>
<p>1) 启动Minikube</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></pre></td><td class="code"><pre><span class="line">minikube start \</span><br><span class="line"> --extra-config=controller-manager.ClusterSigningCertFile=<span class="string">"/var/lib/localkube/certs/ca.crt"</span> \</span><br><span class="line"> --extra-config=controller-manager.ClusterSigningKeyFile=<span class="string">"/var/lib/localkube/certs/ca.key"</span> \</span><br><span class="line"> --extra-config=apiserver.Admission.PluginNames=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota \</span><br><span class="line"> --kubernetes-version=v1.9.0</span><br></pre></td></tr></table></figure>
<p>2) 下载并解压Istio安装包</p>
<p><code>curl -L https://git.io/getLatestIstio | sh -</code></p>
<p>3) 进入安装目录(假设为<code>istio-0.7</code>),将<code>bin/</code>目录添加到<code>PATH</code>环境变量</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"><span class="built_in">cd</span> istio-0.7</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PWD</span>/bin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure>
<p>4) 部署Istio的核心组件(包括外部流量网关<a href="https://istio.io/docs/tasks/traffic-management/ingress.html" target="_blank" rel="noopener">Ingress</a>, 管理Envoy实例生命周期的<a href="https://istio.io/docs/concepts/traffic-management/pilot.html" target="_blank" rel="noopener">Pilot</a>以及执行访问控制和使用策略的<a href="https://istio.io/docs/concepts/policy-and-control/mixer.html" target="_blank" rel="noopener">Mixer</a>)到Kubernetes</p>
<p><code>kubectl apply -f install/kubernetes/istio.yaml</code></p>
<p><em>注:如果你需要启用Istio的<a href="https://istio.io/docs/concepts/security/mutual-tls.html" target="_blank" rel="noopener">Mutual TLS Authentication</a>(服务身份验证)功能,可以改为运行<code>kubectl apply -f install/kubernetes/istio-auth.yaml</code>。</em></p>
<p>至此,一个基于Istio的服务网格就算安装完成了。One more thing,还记得上篇文章提到的服务网格所独有的边车模式吗?为了将一个具体的服务接入Istio,需要为每一个服务实例创建一个配套的边车进程。根据<a href="https://istio.io/docs/setup/kubernetes/sidecar-injection.html" target="_blank" rel="noopener">官方文档</a>,Istio提供手动和自动两种方式来创建边车进程,前者发生于部署阶段,而后者发生于Pod创建阶段,推荐使用后者,具体步骤参考官方文档,限于篇幅,这里就不再赘述。</p>
<h3 id="5-2-验证"><a href="#5-2-验证" class="headerlink" title="5.2 验证"></a>5.2 验证</h3><p>安装完Istio之后,可运行<code>kubectl get pods -n istio-system</code>查看所有Istio相关的Pods,确保这些Pods都处于<code>Running</code>状态。然后,你就可以开始Istio的探索之旅了,建议从官方提供的<a href="https://istio.io/docs/guides/bookinfo.html" target="_blank" rel="noopener">Bookinginfo</a>示例应用起步,这里就不再展开。</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">NAME READY STATUS RESTARTS AGE</span><br><span class="line">istio-ca-59f6dcb7d9-5mll5 1/1 Running 18 42d</span><br><span class="line">istio-ingress-779649ff5b-x2qmn 1/1 Running 26 42d</span><br><span class="line">istio-mixer-7f4fd7dff-6l5g5 3/3 Running 54 42d</span><br><span class="line">istio-pilot-5f5f76ddc8-6867m 2/2 Running 36 42d</span><br><span class="line">istio-sidecar-injector-7947777478-gzcfz 1/1 Running 9 41d</span><br></pre></td></tr></table></figure>
<h2 id="6-小结"><a href="#6-小结" class="headerlink" title="6 小结"></a>6 小结</h2><p>以上就是搭建一个基于Istio的服务网格的基本教程,希望能够帮助你对服务网格有一个更直观的认识。有关服务网格的进一步介绍,以后有机会我再跟你分享。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p>
<h2 id="7-参考"><a href="#7-参考" class="headerlink" title="7 参考"></a>7 参考</h2><ul>
<li><a href="https://istio.io/docs/concepts/what-is-istio/overview.html" target="_blank" rel="noopener">Istio - Overview</a></li>
<li><a href="http://istio.doczh.cn/" target="_blank" rel="noopener">Istio官方文档中文版</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/29586032" target="_blank" rel="noopener">数人云|万字解读:Service Mesh服务网格新生代–Istio</a></li>
</ul>
]]></content>
<categories>
<category> devops </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Service Mesh </tag>
<tag> Istio </tag>
</tags>
</entry>
<entry>
<title><![CDATA[服务网格:微服务进入2.0时代]]></title>
<url>http://emacoo.cn/arch/service-mesh-overview/</url>
<content type="html"><![CDATA[<blockquote>
<p>微服务自<a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="noopener">2014年3月</a>由Martin Fowler首次提出以来,在<a href="http://projects.spring.io/spring-cloud/" target="_blank" rel="noopener">Spring Cloud</a>、<a href="http://dubbo.apache.org/" target="_blank" rel="noopener">Dubbo</a>等各类微服务框架的帮助下,以燎原之势席卷了整个IT技术界,成为了最主流的分布式应用解决方案。但仍然还有很多问题没有得到根本性的解决,比如技术门槛高、多语言支持不足、代码侵入性强等。如何应对这些挑战成为了下一代微服务首要回答的问题。直到服务网格(Service Mesh)被提出,这一切都有了答案。</p>
</blockquote>
<h2 id="1-微服务之殇"><a href="#1-微服务之殇" class="headerlink" title="1 微服务之殇"></a>1 微服务之殇</h2><p>时光回到2017年初,那时所有主流的微服务框架,不管是类库性质的<a href="https://twitter.github.io/finagle/" target="_blank" rel="noopener">Finagle</a>、<a href="https://github.com/Netflix/Hystrix" target="_blank" rel="noopener">Hystrix</a>,还是框架性质的Spring Cloud、Dubbo,本质上都归于应用内解决方案,都存在以下三个问题:</p>
<ul>
<li><strong>技术门槛高</strong>:随着微服务实施水平的不断深化,除了基础的<a href="http://emacoo.cn/arch/microservice-registry-center/">服务发现</a>、<a href="http://emacoo.cn/arch/microservice-config/">配置中心</a>和<a href="http://emacoo.cn/arch/microservice-oauth2/">授权管理</a>之外,团队将不可避免的在服务治理层面面临各类新的挑战,包括但不限于分布式跟踪、熔断降级、灰度发布、故障切换等,这对团队提出了非常高的技术要求。</li>
</ul>
<p><img src="service-governance.jpg" alt></p>
<p><em>图片出处:<a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></em></p>
<ul>
<li><strong>多语言支持不足</strong>:对于稍具规模的团队,尤其在高速成长的互联网创业公司,多语言的技术栈是常态,跨语言的服务调用也是常态,但目前开源社区上并没有一套统一的、跨语言的微服务技术栈。</li>
<li><strong>代码侵入性强</strong>:主流的微服务框架(比如Spring Cloud、Dubbo)或多或少都对业务代码有一定的侵入性,框架替换成本高,导致业务团队配合意愿低,微服务落地困难。</li>
</ul>
<p>这些问题加起来导致的结果就是,在实施微服务的过程中,小团队Hold不住,大公司推不动。</p>
<h2 id="2-另辟蹊径"><a href="#2-另辟蹊径" class="headerlink" title="2 另辟蹊径"></a>2 另辟蹊径</h2><p>如何解决上述三个问题呢?最容易想到的是代理模式,在LB层(比如<a href="http://nginx.org/" target="_blank" rel="noopener">Nginx</a>、<a href="https://httpd.apache.org/" target="_blank" rel="noopener">Apache HTTP Server</a>)处理所有的服务调用,以及部分服务治理问题(比如分布式跟踪、熔断降级)。但这个方案有两个显著的缺点,第一,中心化架构,代理端自身的性能和可用性将是整个系统的瓶颈;第二,运维复杂度高,业务团队笑了,运维团队哭了。</p>
<blockquote>
<p>难道这就是桃园吗?</p>
</blockquote>
<p>服务网格(Service Mesh)应运而生!自2016年9月Linkerd第一次公开使用之后,伴随着<a href="https://linkerd.io/" target="_blank" rel="noopener">Linkerd</a>、<a href="https://www.envoyproxy.io/" target="_blank" rel="noopener">Envoy</a>、<a href="https://istio.io/" target="_blank" rel="noopener">Istio</a>、<a href="https://www.nginx.com/products/" target="_blank" rel="noopener">NGINX Application Platform</a>、<a href="https://conduit.io/" target="_blank" rel="noopener">Conduit</a>等新框架如雨后春笋般不断涌现,在微服务之后,服务网格和它的边车(Sidecar)模式引领了IT技术界2017一整年的走向。</p>
<h2 id="3-服务网格"><a href="#3-服务网格" class="headerlink" title="3 服务网格"></a>3 服务网格</h2><h3 id="3-1-元定义"><a href="#3-1-元定义" class="headerlink" title="3.1 元定义"></a>3.1 元定义</h3><p>首先,我们来看一下服务网格的提出者William Morgan是如何描述它的。</p>
<blockquote>
<p>A service mesh is a dedicated infrastructure layer for handling service-to-service communication. Consists of a control plane and data plane (service proxies act as “mesh”). - William Morgan, <a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></p>
</blockquote>
<p>上面这段话非常清晰的指明了服务网格的职责,即处理服务间通讯,这正是服务治理的核心所在。而<code>a dedicated infrastructure layer</code>这几个单词将服务网格和之前所有的微服务框架(framework)划清了界限,也即服务网格独立于具体的服务而存在,这从根本上解决了前文提到的老的微服务框架在多语言支持和代码侵入方面存在的问题。并且,由于服务网格的独立性,业务团队不再需要操心服务治理相关的复杂度,全权交给服务网格处理即可。</p>
<p>那你可能会问,这不跟之前提到的代理模式差不多吗?区别在于服务网格独创的边车模式。针对每一个服务实例,服务网格都会在同一主机上一对一并行部署一个边车进程,接管该服务实例所有对外的网络通讯(参见下图)。这样就去除了代理模式下中心化架构的瓶颈。同时,借助于良好的框架封装,运维成本也可以得到有效的控制。</p>
<p><img src="linkerd-service-mesh-diagram.png" alt></p>
<p><em>图片出处:<a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></em></p>
<h3 id="3-2-演化史"><a href="#3-2-演化史" class="headerlink" title="3.2 演化史"></a>3.2 演化史</h3><p>追本溯源,服务网格从无到有可分为三个演化阶段(参见下图)。第一个阶段,每个服务各显神通,自行处理对外通讯。第二个阶段,所有服务使用统一的类库进行通讯。第三个阶段,服务不再关心通讯细节,统统交给边车进程,就像在TCP/IP协议中,应用层只需把要传输的内容告诉TCP层,由TCP层负责将所有内容原封不动的送达目的端,整个过程中应用层并不需要关心实际传输过程中的任何细节。</p>
<p><img src="pattern-network.png" alt></p>
<p><img src="pattern-library.png" alt></p>
<p><img src="pattern-sidecar.png" alt></p>
<p><em>图片出处:<a href="http://philcalcado.com/2017/08/03/pattern_service_mesh.html" target="_blank" rel="noopener">Pattern: Service Mesh</a></em></p>
<h3 id="3-3-时间线"><a href="#3-3-时间线" class="headerlink" title="3.3 时间线"></a>3.3 时间线</h3><p>最后,再来回看一下服务网格年轻的历史。虽然服务网格的正式提出是在2016年9月,但其实早在2013年,Airbnb就提出了类似的想法——<a href="https://medium.com/airbnb-engineering/smartstack-service-discovery-in-the-cloud-4b8a080de619" target="_blank" rel="noopener">SmartStack</a>,只不过SmartStack局限于服务发现,并没有引起太多关注,类似的还有Netflix的Prana和唯品会的OSP Local Proxy。2016年服务网格提出之后,以Linkerd和Envoy为代表的框架开始崭露头角,并于2017年先后加入CNCF基金(Cloud Native Computing Foundation),最终促使了一代新贵Istio的诞生。2018年,Istio将发布1.0版本,这也许意味着微服务开始进入2.0时代。</p>
<p><img src="history.jpg" alt></p>
<p><em>图片出处:<a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></em></p>
<h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上就是我对服务网格的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。下一篇我会教大家如何在本地从零搭建一个基于Istio的服务网格,敬请期待。</p>
<h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul>
<li><a href="https://dzone.com/articles/whats-a-service-mesh-and-why-do-i-need-one" target="_blank" rel="noopener">What’s a Service Mesh? And Why Do I Need One?</a></li>
<li><a href="http://philcalcado.com/2017/08/03/pattern_service_mesh.html" target="_blank" rel="noopener">Pattern: Service Mesh</a></li>
<li><a href="https://servicemesh.gitbooks.io/awesome-servicemesh/" target="_blank" rel="noopener">Awesome Service Mesh</a></li>
<li><a href="https://servicemesh.gitbooks.io/awesome-servicemesh/mesh/2017/service-mesh-next-generation-of-microservice/" target="_blank" rel="noopener">Service Mesh:下一代微服务</a></li>
<li><a href="http://www.infoq.com/cn/articles/2017-service-mesh?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=%E6%9E%B6%E6%9E%84%20&%20%E8%AE%BE%E8%AE%A1-articles" target="_blank" rel="noopener">解读2017之Service Mesh:群雄逐鹿烽烟起</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Service Mesh </tag>
</tags>
</entry>
<entry>
<title><![CDATA[零基础玩转Serverless]]></title>
<url>http://emacoo.cn/arch/serverless-tutorial/</url>
<content type="html"><![CDATA[<blockquote>
<p><a href="http://emacoo.cn/arch/serverless-overview/">上篇</a>文章首先指出了Serverless=No Server这一常见误区,然后明确定义了<strong>函数</strong>这个Serverless中的核心概念,接着介绍了Serverless的4个关键特性:运行成本更低、自动扩缩容、事件驱动、无状态性,最后分析了Serverless和微服务、DevOps之间的关联关系。为了帮助大家更直观的理解Serverless,本文将介绍三种在<a href="https://aws.amazon.com/cn/lambda/" target="_blank" rel="noopener">AWS Lambda</a>上创建函数的方式。</p>
</blockquote>
<h2 id="1-Hello-AWS-Lambda"><a href="#1-Hello-AWS-Lambda" class="headerlink" title="1 Hello, AWS Lambda!"></a>1 Hello, AWS Lambda!</h2><p><img src="aws-lambda-123.png" alt></p>
<h3 id="1-1-注册AWS账户"><a href="#1-1-注册AWS账户" class="headerlink" title="1.1 注册AWS账户"></a>1.1 注册AWS账户</h3><p>首先,打开Amazon AWS<a href="https://amazonaws-china.com/cn/" target="_blank" rel="noopener">官网</a>,点击右上角<strong>注册</strong>按钮开始注册流程。</p>
<p>注册AWS除了邮箱、地址、手机号(用于接受语音验证码)等基本信息之外,还需要绑定一张信用卡(银联、MasterCard、VISA),绑卡过程中会发生一笔1美元的信用卡预授权扣费。</p>
<p>注册成功之后,即可获赠<a href="https://amazonaws-china.com/cn/free/" target="_blank" rel="noopener">AWS免费套餐</a>大礼包,包括12个月免费的基础IaaS & PaaS服务(比如EC2, S3, RDS等),以及永久免费的AWS Lambda<a href="https://amazonaws-china.com/cn/lambda/pricing/" target="_blank" rel="noopener">免费套餐</a>(包括每月100万个免费请求以及每月400000GB-秒的计算时间,对于个人使用而言完全是足够了)。</p>
<h3 id="1-2-创建函数"><a href="#1-2-创建函数" class="headerlink" title="1.2 创建函数"></a>1.2 创建函数</h3><p>接下来,就来创建第一个AWS Lambda函数吧。</p>
<p>1) 登录AWS,点击最上方的菜单栏<strong>服务->计算:Lambda</strong>,进入Lambda控制台。<br>2) 在页面上找到并点击<strong>创建函数</strong>按钮。<br>3) 作为第一个函数,选择<strong>从头开始创作</strong>,输入函数名称<code>hello-lambda</code>,运行语言选择<code>Node.js 6.10</code>,角色选择系统默认创建的<code>service-role/admin</code>,点击<strong>创建函数</strong>完成创建。</p>
<p><img src="hello-lambda.png" alt></p>
<h3 id="1-3-简单测试"><a href="#1-3-简单测试" class="headerlink" title="1.3 简单测试"></a>1.3 简单测试</h3><p>新函数创建好之后,就可以开始测试了。在函数详情页的右上角找到并点击<strong>测试</strong>按钮,第一次会提示你先创建一个测试事件,输入名称,使用默认模板完成创建。回到详情页,再次点击<strong>测试</strong>按钮,就会触发测试。测试完成之后,展开详细信息,就可以看到具体的响应结果,以及本次测试产生的计费时间。</p>
<p><img src="test-result.png" alt></p>
<h3 id="1-4-公网测试"><a href="#1-4-公网测试" class="headerlink" title="1.4 公网测试"></a>1.4 公网测试</h3><p>函数详情页的测试按钮是最简单的一种测试Lambda函数的方式,但这种方式仅限于AWS内网,如果想在公网环境下进行测试,该如何操作呢?最自然的方式是绑定API Gateway,将函数转化为可公开调用的API。</p>
<h4 id="1-4-1-绑定API-Gateway"><a href="#1-4-1-绑定API-Gateway" class="headerlink" title="1.4.1 绑定API Gateway"></a>1.4.1 绑定API Gateway</h4><p>1) 同样是函数详情页,在左侧找到<strong>添加触发器</strong>,点击<strong>API Gateway</strong>,保持默认设置完成添加。<br>2) 修改函数代码,返回符合API Gateway格式要求的响应结果,参考<a href="https://amazonaws-china.com/cn/premiumsupport/knowledge-center/malformed-502-api-gateway/" target="_blank" rel="noopener">这里</a>。<br>3) 保存上述改动。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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">exports.handler = (event, context, callback) => {</span><br><span class="line"> var responseBody = {</span><br><span class="line"> "key3": "value3",</span><br><span class="line"> "key2": "value2",</span><br><span class="line"> "key1": "value1"</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> var response = {</span><br><span class="line"> "statusCode": 200,</span><br><span class="line"> "headers": {</span><br><span class="line"> "my_header": "my_value"</span><br><span class="line"> },</span><br><span class="line"> "body": JSON.stringify(responseBody),</span><br><span class="line"> "isBase64Encoded": false</span><br><span class="line"> };</span><br><span class="line"> callback(null, response);</span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p><em>示例函数代码</em></p>
<p>点击最上方的菜单栏<strong>服务->网络和内容分发:API Gateway</strong>,进入API Gateway控制台,在左侧导航栏应该能够看到<strong>API->LambdaMicroservice</strong>,说明函数已经成功绑定。依次点击<strong>API->LambdaMicroservice->阶段->prod->/->hello-lambda->GET</strong>,记下调用URL。</p>
<h4 id="1-4-2-创建用户"><a href="#1-4-2-创建用户" class="headerlink" title="1.4.2 创建用户"></a>1.4.2 创建用户</h4><p>API Gateway默认使用的鉴权方式是AWS_IAM,即调用方必须拥有特定的IAM Permssions才能调用API,参考<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html" target="_blank" rel="noopener">这里</a>。具体来说,需要一个拥有<code>execute-api:Invoke</code>权限的用户。</p>
<p>1) 点击最上方的菜单栏<strong>服务->安全、身份与合规:IAM</strong>,进入IAM控制台。<br>2) 点击左侧导航栏<strong>用户</strong>,进入用户面板。<br>3) 点击<strong>添加用户</strong>按钮,输入用户名,访问类型选择<strong>编程访问</strong>,点击<strong>下一步:权限</strong>。<br>4) 选择<strong>直接附加现有策略</strong>,搜索并选中<code>AmazonAPIGatewayInvokeFullAccess</code>,完成创建。<br>5) 返回用户列表页,点击刚刚创建的用户进入用户详情页,点击<strong>安全证书->创建访问密钥</strong>,记下<strong>访问密钥 ID</strong>和<strong>私有访问密钥</strong>。</p>
<h4 id="1-4-3-使用Postman测试API"><a href="#1-4-3-使用Postman测试API" class="headerlink" title="1.4.3 使用Postman测试API"></a>1.4.3 使用Postman测试API</h4><p>做完前两步的准备工作,就可以使用Postman进行测试了。</p>
<p>1) 下载并启动<a href="https://www.getpostman.com/" target="_blank" rel="noopener">Postman</a>。<br>2) 创建一个新的请求,<strong>Authorization</strong>选择<code>AWS Signature</code>,输入之前记下的URL、AccessKey(访问密钥 ID)和SecretKey(私有访问密钥),AWS Region填入URL中紧邻<strong>amazonaws.com</strong>的一个子域名,Service Name填入<code>execute-api</code>。<br>3) 点击<strong>Send</strong>,稍等一会,应该就能看到正常的响应结果。</p>
<p><img src="postman-request.png" alt></p>
<p>进一步信息可参考<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-use-postman-to-call-api.html" target="_blank" rel="noopener">这里</a>。</p>
<h2 id="2-加餐一:Spring-Cloud-Function"><a href="#2-加餐一:Spring-Cloud-Function" class="headerlink" title="2 加餐一:Spring Cloud Function"></a>2 加餐一:Spring Cloud Function</h2><p>除了Node.js,AWS Lambda还支持Java 8、C#、Go、Python等多种运行语言。接下来,就以一个<a href="http://cloud.spring.io/spring-cloud-function/spring-cloud-function.html" target="_blank" rel="noopener">Spring Cloud Function</a>(简称SCF)应用为例,展示如何创建一个Java 8的函数。</p>
<blockquote>
<p>SCF是Spring社区提供的一个以函数为核心的开发框架。除了本地运行,SCF应用还可以部署到AWS、Azure、OpenWhisk等多种Serverless平台。最新的发布版本是1.0.0.M3。</p>
</blockquote>
<p><strong>打包应用:</strong></p>
<p>1) <code>git clone</code>SCF<a href="https://github.com/spring-cloud/spring-cloud-function" target="_blank" rel="noopener">官方仓库</a>。<br>2) 进入<strong>spring-cloud-function-samples/function-sample-aws</strong>目录,运行<code>mvn clean package</code>。<br>3) 运行成功后在<strong>target</strong>目录下可以找到名为<strong>function-sample-aws-1.0.0.BUILD-SNAPSHOT-aws.jar</strong>的应用包。</p>
<p><strong>创建函数:</strong></p>
<p>1) 和之前一样,进入Lambda控制台,点击<strong>创建函数</strong>按钮,运行语言选择<code>Java 8</code>,完成创建。<br>2) 进入函数详情页,点击<strong>函数代码->上传</strong>按钮,选择之前打好的应用包,处理程序改为<code>org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler</code>。<br>3) 保存修改。</p>
<p><strong>测试函数:</strong></p>
<p>1) 进入函数详情页,点击右上角的<strong>测试</strong>按钮,填入<code>{"value": "hello, lambda!"}</code>创建新的测试事件。<br>2) 再次点击<strong>测试</strong>按钮,触发第一次测试。不出意外,第一次测试会提示失败,错误消息类似于<code>errorMessage": "2018-02-04T13:09:59.745Z b1c9b0a1-09ac-11e8-9fdf-858e20f0ff70 Task timed out after 3.00 seconds"</code>。出错的直接原因是函数设置的超时时间太短(默认3秒),根本原因是函数的无状态性,每次函数调用都要经历一次冷启动,这对于Node应用没有太大问题,但对于Java 8应用,即便是一个最简单的Hello World应用,完成一次冷启动至少需要5到10秒。<br>3) 修改<strong>基本设置->内存</strong>为<code>512MB</code>,<strong>基本设置->超时</strong>为<code>5分钟</code>,保存然后重新测试。这一次测试应该可以成功,返回结果为<code>{"value": "HELLO, LAMBDA!"}</code>。</p>
<h2 id="3-加餐二:serverless-toolkit"><a href="#3-加餐二:serverless-toolkit" class="headerlink" title="3 加餐二:serverless toolkit"></a>3 加餐二:serverless toolkit</h2><p>除了直接在AWS后台创建函数,还有一种更为简便的方式,使用<a href="https://serverless.com/" target="_blank" rel="noopener">serverless.com</a>平台提供的serverless toolkit。</p>
<p><img src="serverless-toolkit.png" alt></p>
<p>操作非常简单,这里就不展开了,不过有两点需要注意:</p>
<ul>
<li>在将应用部署到AWS之前,先要创建一个拥有<code>AdministratorAccess</code>权限的用户,参考<a href="https://serverless.com/framework/docs/providers/aws/guide/credentials/" target="_blank" rel="noopener">这里</a>。</li>
<li>默认创建的应用鉴权为空,即可以在公网直接访问。</li>
</ul>
<h2 id="4-小结"><a href="#4-小结" class="headerlink" title="4 小结"></a>4 小结</h2><p>以上简单介绍了三种在AWS Lambda上创建函数的方式,希望对你理解Serverless有所帮助。有关Serverless其他特性的研究,以后有机会我再跟你分享。欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。</p>
<h2 id="5-参考"><a href="#5-参考" class="headerlink" title="5 参考"></a>5 参考</h2><ul>
<li><a href="http://serverless.ink/" target="_blank" rel="noopener">Serverless 应用开发指南</a></li>
<li><a href="https://spring.io/blog/2017/07/05/introducing-spring-cloud-function" target="_blank" rel="noopener">Introducing Spring Cloud Function</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Serverless </tag>
</tags>
</entry>
<entry>
<title><![CDATA[我的2018书单]]></title>
<url>http://emacoo.cn/notes/2018-booklist/</url>
<content type="html"><![CDATA[<blockquote>
<p>2018,愚者暗于成事,智者见于未萌。</p>
</blockquote>
<h2 id="在读"><a href="#在读" class="headerlink" title="在读"></a>在读</h2><p><img src="23737589-1_w_2.jpg" alt></p>
<h2 id="已读"><a href="#已读" class="headerlink" title="已读"></a>已读</h2>]]></content>
<categories>
<category> notes </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> 悦读 </tag>
</tags>
</entry>
<entry>
<title><![CDATA[所谓Serverless,你理解对了吗?]]></title>
<url>http://emacoo.cn/arch/serverless-overview/</url>
<content type="html"><![CDATA[<blockquote>
<p>随着DevOps和微服务的理念日渐被IT业界所接受,另一个新名词Serverless也<a href="https://trends.google.com/trends/explore?date=today%205-y&q=serverless" target="_blank" rel="noopener">开始</a>进入人们的视野。尤其在今年4月份国内两大云服务厂商阿里云、腾讯云先后推出各自的Serverless产品之后,Serverless一时洛阳纸贵。那到底什么是Serverless,它跟DevOps和微服务又有什么样的联系呢?本文将尝试揭开Serverless的神秘面纱,让你一睹为快。</p>
</blockquote>
<h2 id="1-Serverless-No-Server"><a href="#1-Serverless-No-Server" class="headerlink" title="1 Serverless != No Server"></a>1 Serverless != No Server</h2><p>首先,必须澄清的是Serverless并不能按字面上理解为无服务器,而是说对应用开发者而言,不再需要<strong>操心</strong>大部分跟服务器相关的事务,比如服务器选购、应用运行环境配置、负载均衡、日志搜集、系统监控等,这些事情统统交给Serverless平台即可,应用开发者唯一需要做的就是编写应用代码,实现业务逻辑。为了避免歧义,本文将保留使用Serverless,而不是其通常的中文翻译无服务器。</p>
<p>Serverless最早由Amazon提出,第一个Serverless平台是2014年年底推出的<a href="https://aws.amazon.com/cn/lambda/" target="_blank" rel="noopener">AWS Lambda</a>,应用开发者只需要上传代码或者应用包,即可发布一个应用。之后全球各大云服务厂商都纷纷推出各自的Serverless平台,比如<a href="https://cloud.google.com/functions/" target="_blank" rel="noopener">Google Cloud Functions</a>,<a href="https://azure.microsoft.com/en-us/services/functions/" target="_blank" rel="noopener">Azure Functions</a>,<a href="https://www.ibm.com/cloud/functions" target="_blank" rel="noopener">IBM Cloud Functions</a>,以及前面提到的<a href="https://www.aliyun.com/product/fc" target="_blank" rel="noopener">阿里云函数计算</a>和<a href="https://www.aliyun.com/product/fc" target="_blank" rel="noopener">腾讯云无服务器云函数</a>等。在云服务厂商之外,开源社区也涌现出很多优秀的Serverless框架,比如<a href="https://openwhisk.apache.org/" target="_blank" rel="noopener">Apache OpenWhisk</a>,<a href="http://cloud.spring.io/spring-cloud-function/" target="_blank" rel="noopener">Spring Cloud Function</a>,<a href="https://github.com/lambadaframework/lambadaframework" target="_blank" rel="noopener">Lambada Framework</a>,<a href="https://webtask.io/" target="_blank" rel="noopener">webtask</a>等。</p>
<p>根据<a href="https://martinfowler.com/articles/serverless.html" target="_blank" rel="noopener">Serverless Architectures</a>一文,Serverless应用可以细分为BaaS和FaaS两类,</p>
<ul>
<li>BaaS: Backend as a Service,这里的Backend可以指代任何第三方提供的应用和服务,比如提供云数据库服务的<a href="https://firebase.google.com/" target="_blank" rel="noopener">Firebase</a>和<a href="http://parseplatform.org/" target="_blank" rel="noopener">Parse</a>,提供统一用户身份验证服务的<a href="https://auth0.com/" target="_blank" rel="noopener">Auth0</a>和<a href="https://aws.amazon.com/cn/cognito/" target="_blank" rel="noopener">Amazon Cognito</a>等。</li>
<li>FaaS: Functions as a Service,应用以函数的形式存在,并由第三方云平台托管运行,比如之前提到的AWS Lambda,Google Cloud Functions等。</li>
</ul>
<p>本文主要讨论的是FaaS,这也是目前各类Serverless平台和框架主要支持的类型。</p>
<h2 id="2-函数即应用"><a href="#2-函数即应用" class="headerlink" title="2 函数即应用"></a>2 函数即应用</h2><blockquote>
<p>当我们讨论函数时,我们到底在讨论什么?</p>
</blockquote>
<p>函数,往大了说可以是一个应用的main函数,往小了说也可以是一个简单的加法函数,那到底该如何理解FaaS中的函数呢?先来看张图。</p>
<p><img src="faas.png" alt></p>
<p>左侧的Monolith即我们常说的单体应用,中间是微服务,右侧就是FaaS中的函数(为了避免歧义,如不特殊指明,下文提到的函数都是指代FaaS中的函数)。如同一个单体应用可以按业务模块拆分成多个微服务,一个微服务也可以按使用场景拆分成多个函数。比如一个广告微服务,至少可以拆分出实时竞价、展示计数、报表查询等多个函数。也就是说,FaaS中的函数和微服务中的API是同一粒度的。但不同于API,在Serverless架构下,每个函数都是独立部署,按需执行。那这样的拆分有意义吗?接着往下看。</p>
<h2 id="3-搞懂Serverless的4把钥匙"><a href="#3-搞懂Serverless的4把钥匙" class="headerlink" title="3 搞懂Serverless的4把钥匙"></a>3 搞懂Serverless的4把钥匙</h2><p>和其他架构相比,Serverless有以下4个特点。</p>
<h3 id="3-1-运行成本更低"><a href="#3-1-运行成本更低" class="headerlink" title="3.1 运行成本更低"></a>3.1 运行成本更低</h3><p>无论是过去的IDC,还是如今的云主机,本质上都是一种包月计费模式,也就是说,不管有没有用户访问你的应用,也不管你有没有部署应用,你都要付相同的钱。但对于Serverless应用,你只需要根据实际使用的资源量(比如AWS Lambda是按<code>内存大小*计算时间</code>计算资源量)进行付费,也即用多少,付多少,相当于移动网络的按流量计费模式。那为什么说使用这种模式就能降低运行成本呢?</p>
<p><img src="inconsistent-traffic-pattern.png" alt></p>
<p>红线以下的长方形面积代表了传统包月计费模式下你所需要支付的成本,而蓝色区域的面积则代表了按流量计费模式下的成本,显然后者要远低于前者。根据福布斯2015年发布的一份<a href="https://www.forbes.com/forbes/welcome/?toURL=https://www.forbes.com/sites/benkepes/2015/06/03/30-of-servers-are-sitting-comatose-according-to-research/&refURL=&referrer=#2f4944612c2" target="_blank" rel="noopener">研究报告</a>,从全年来看,一个典型的数据中心里的服务器平均资源使用率只有可怜的5%到15%,也就是说如果全部使用Serverless,理论上至少可以节省80%的运行成本。</p>
<p>按流量计费的另一个隐藏的好处是任何的性能提升都可以直接的反应到运行成本上,这让技术人员的价值也有了更充分的体现。</p>
<h3 id="3-2-自动扩缩容"><a href="#3-2-自动扩缩容" class="headerlink" title="3.2 自动扩缩容"></a>3.2 自动扩缩容</h3><p>Serverless第二个常被提及的特点是自动扩缩容。前面说了函数即应用,一个函数只做一件事,可以独立的进行扩缩容,而不用担心影响其他函数,并且由于粒度更小,扩缩容速度也更快。而对于单体应用和微服务,借助于各种容器编排技术,虽然也能实现自动扩缩容,但由于粒度关系,相比函数,始终会存在一定的资源浪费。比如一个微服务提供两个API,其中一个API需要进行扩容,而另一个并不需要,那么这时候扩容,对于不需要的API就是一种浪费。</p>
<h3 id="3-3-事件驱动"><a href="#3-3-事件驱动" class="headerlink" title="3.3 事件驱动"></a>3.3 事件驱动</h3><p>函数本质上实现的是一种<a href="https://en.wikipedia.org/wiki/IPO_model" target="_blank" rel="noopener">IPO</a>(Input-Process-Output)模型,它是短暂的,是即用即走的。这点是函数区别于单体应用和微服务的另一个特征。不管是单体应用,还是微服务,都是系统中的常驻进程,套用一句流行语,就是你来或不来,我都在这里,不舍不弃。而函数不一样,既不发布任何服务,没有请求时也不消耗任何资源,只有当请求来了,才会消耗资源进行响应,服务完立刻释放资源。正是由于这一点,函数天然的适用于任何事件驱动的业务场景,比如广告竞价,身份验证,定时任务,以及一些新兴的IoT应用。</p>
<p><img src="event-driven-iot.png" alt></p>
<p><em>OpenWhisk给出的一个IoT电冰箱的<a href="https://www.slideshare.net/DanielKrook/openwhisk-a-platform-for-cloud-native-serverless-event-driven-apps?ref=https://developer.ibm.com/opentech/2016/09/06/what-makes-serverless-attractive/" target="_blank" rel="noopener">案例</a></em></p>
<h3 id="3-4-无状态性"><a href="#3-4-无状态性" class="headerlink" title="3.4 无状态性"></a>3.4 无状态性</h3><p>函数的IPO本质决定了函数的另一个特征,无状态性。无状态一方面有助于提高函数的可重用性和可迁移性,但另一方面也带来了一些性能上的损失。第一,函数不是常驻进程,这就意味着每来一个请求,函数都要经历一次冷启动,这对编译型语言编写的应用不啻为一场噩梦(以Spring Boot为例,即便是一个最简单的Hello World应用,至少也需要5秒钟才能启动完毕)。第二,每服务完一个请求,函数所在的进程就会被杀掉,也就是说使用内存进行缓存对函数而言不再有意义。第三,由于每次启动都可能被调度到新的服务器上,任何基于本地磁盘的缓存技术也就不再适用。从第二点和第三点可知,函数只能使用外存(比如Redis,数据库)进行缓存,而操作外存都需要通过网络,性能跟内存、本地硬盘相比差了一到两个数量级。</p>
<h2 id="4-DevOps-gt-NoOps"><a href="#4-DevOps-gt-NoOps" class="headerlink" title="4 DevOps => NoOps"></a>4 DevOps => NoOps</h2><blockquote>
<p>如果说Agile+IaaS促成了DevOps,那么Agile+PaaS就孕育了Serverless。</p>
</blockquote>
<p>理解了什么是Serverless,再来看看它和DevOps的关系。DevOps虽然做了很多Dev的事,但底牌还是Ops(好比猫熊虽然长得像猫,但实际上还是熊)。但Serverless不同,从本质上说,它是把Ops外包给第三方平台,让Dev专注于业务逻辑的实现而不用操心Ops相关的工作,最终的结果就是绝大多数企业不再需要Ops这个岗位。它和DevOps最大的共同点就是帮助企业缩短产品上市的时间。</p>
<h2 id="5-小结"><a href="#5-小结" class="headerlink" title="5 小结"></a>5 小结</h2><p>以上就是我对Serverless的一些简单介绍,欢迎你到我的<a href="https://github.com/emac/emac.github.io/issues/2" target="_blank" rel="noopener">留言板</a>留言交流,和大家一起过过招。下一篇我会手把手教大家如何在AWS Lambda部署一个基于Spring Cloud Function的Serverless应用,敬请期待。</p>
<h2 id="6-参考"><a href="#6-参考" class="headerlink" title="6 参考"></a>6 参考</h2><ul>
<li><a href="https://martinfowler.com/articles/serverless.html" target="_blank" rel="noopener">Serverless Architectures</a></li>
<li><a href="https://developer.ibm.com/opentech/2016/09/06/what-makes-serverless-attractive/" target="_blank" rel="noopener">What makes serverless architectures so attractive?</a></li>
<li><a href="http://www.infoq.com/cn/articles/practical-serverless-computing?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=%E6%9E%B6%E6%9E%84%20&%20%E8%AE%BE%E8%AE%A1-articles" target="_blank" rel="noopener">InfoQ虚拟研讨会:无服务器计算的实践方法</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA%3D%3D&chksm=88931c6cbfe4957a702e66221e1bf997c4ba5a66de279294b08cccadd3ff5d6cabf103657484&idx=1&mid=2649694991&mpshare=1&scene=23&sn=818dea0cb058a08ac6b66ee865204630&srcid=0907dIsFi2ho3ez9orBMGatf" target="_blank" rel="noopener">Serverless云函数架构精解</a></li>
<li><a href="http://www.infoq.com/cn/news/2017/06/tengxun-cloud-serverless?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=DevOps" target="_blank" rel="noopener">姗姗来迟的Serverless如何助力微服务和DevOps</a></li>
</ul>
]]></content>
<categories>
<category> arch </category>
</categories>
<tags>
<tag> 原创 </tag>
<tag> Serverless </tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Spring 5】响应式Web框架实战(下)]]></title>
<url>http://emacoo.cn/backend/spring5-reactive-tutorial2/</url>
<content type="html"><![CDATA[<blockquote>
<p>引子:被誉为“中国大数据第一人”的涂子沛先生在其成名作《数据之巅》里提到,摩尔定律、社交媒体、数据挖掘是大数据的三大成因。IBM的研究称,整个人类文明所获得的全部数据中,有90%是过去两年内产生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在内的大批新技术应运而生。其中以<a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a>和<a href="http://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>为代表的响应式(Reactive)编程技术针对的就是经典的大数据4V定义(Volume,Variety,Velocity,Value)中的Velocity,即高并发问题,而在即将发布的Spring 5中,也引入了响应式编程的支持。在接下来的几周,我会围绕响应式编程分三期与你分享我的一些学习心得。本篇是第三篇(下),通过一个简单的Spring 5示例应用,探一探即将于下月底发布的Spring 5的究竟。</p>
<p>前情概要:</p>
<ul>
<li><a href="http://emacoo.cn/backend/spring5-overview/">【Spring 5】响应式Web框架前瞻</a></li>
<li><a href="http://emacoo.cn/backend/reactive-overview/">响应式编程总览</a></li>
<li><a href="http://emacoo.cn/backend/spring5-reactive-tutorial/">【Spring 5】响应式Web框架实战(上)</a></li>
</ul>
</blockquote>