-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsearch.xml
1490 lines (1490 loc) · 631 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[SpringBoot 使用 Redis 分布式锁解决并发问题]]></title>
<url>%2F2021%2F07%2F24%2FSpringBoot-%E4%BD%BF%E7%94%A8-Redis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E8%A7%A3%E5%86%B3%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[问题背景现在的应用程序架构中,很多服务都是多副本运行,从而保证服务的稳定性。一个服务实例挂了,其他服务依旧可以接收请求。但是服务的多副本运行随之也会引来一些分布式问题,比如某个接口的处理逻辑是这样的:接收到请求后,先查询 DB 看是否有相关的数据,如果没有则插入数据,如果有则更新数据。在这种场景下如果相同的 N 个请求并发发到后端服务实例,就会出现重复插入数据的情况: 解决方案针对上面问题,一般的解决方案是使用分布式锁来解决。同一个进程内的话用本进程内的锁即可解决,但是服务多实例部署的话是分布式的,各自进程独立,这种情况下可以设置一个全局获取锁的地方,各个进程都可以通过某种方式获取这个全局锁,获得到锁后就可以执行相关业务逻辑代码,没有拿到锁则跳过不执行,这个全局锁就是我们所说的分布式锁。分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。 我们这里介绍如何基于 Redis 的分布式锁来解决分布式并发问题:Redis 充当获取全局锁的地方,每个实例在接收到请求的时候首先从 Redis 获取锁,获取到锁后执行业务逻辑代码,没争抢到锁则放弃执行。 主要实现原理:Redis 锁主要利用 Redis 的 setnx 命令: 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。Value 一般用 UUID 标识,确保锁不被误解。 解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。 锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。 可靠性:为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性。在任意时刻,保证只有一台机器的一个线程可以持有锁; 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁; 具备非阻塞性。一旦获取不到锁就立刻返回加锁失败; 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了; SpringBoot 集成使用 Redis 分布式锁写了一个 RedisLock 工具类,用于业务逻辑执行前加锁和业务逻辑执行完解锁操作。这里的加锁操作可能实现的不是很完善,有加锁和锁过期两个操作原子性问题,如果 SpringBoot 版本是2.x的话是可以用注释中的代码在加锁的时候同时设置锁过期时间,如果 SpringBoot 版本是2.x以下的话建议使用 Lua 脚本来确保操作的原子性,这里为了简单就先这样写:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;/** * @description: Redis分布式锁实现工具类 * @author: qianghaohao * @time: 2021/7/19 */@Componentpublic class RedisLock { @Autowired StringRedisTemplate redisTemplate; /** * 获取锁 * * @param lockKey 锁 * @param identity 身份标识(保证锁不会被其他人释放) * @param expireTime 锁的过期时间(单位:秒) * @return */ public boolean lock(String lockKey, String identity, long expireTime) { // 由于我们目前 springboot 版本比较低,1.5.9,因此还不支持下面这种写法 // return redisTemplate.opsForValue().setIfAbsent(lockKey, identity, expireTime, TimeUnit.SECONDS); if (redisTemplate.opsForValue().setIfAbsent(lockKey, identity)) { redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS); return true; } return false; } /** * 释放锁 * * @param lockKey 锁 * @param identity 身份标识(保证锁不会被其他人释放) * @return */ public boolean releaseLock(String lockKey, String identity) { String luaScript = "if " + " redis.call('get', KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); Object result = redisTemplate.execute(redisScript, keys, identity); return (boolean) result; }} 使用示例这里只贴出关键的使用代码,注意:锁的 key 根据自己的业务逻辑命名,能唯一标示同一个请求即可。value 这里设置为 UUID,为了确保释放锁的时候能正确释放(只释放自己加的锁)。12@Autowiredprivate RedisLock redisLock; // redis 分布式锁 123456789101112131415161718 String redisLockKey = String.format("%s:docker-image:%s", REDIS_LOCK_PREFIX, imageVo.getImageRepository()); String redisLockValue = UUID.randomUUID().toString(); try { if (!redisLock.lock(redisLockKey, redisLockValue, REDIS_LOCK_TIMEOUT)) { logger.info("redisLockKey [" + redisLockKey + "] 已存在,不执行镜像插入和更新"); result.setMessage("新建镜像频繁,稍后重试,锁占用"); return result; } ... // 执行业务逻辑catch (Execpion e) { ... // 异常处理} finally { // 释放锁 if (!redisLock.releaseLock(redisLockKey, redisLockValue)) { logger.error("释放redis锁 [" + redisLockKey + "] 失败); } else { logger.error("释放redis锁 [" + redisLockKey + "] 成功"); } } 参考文档https://www.jianshu.com/p/6c2f85e2c586https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpirngBoot 整合使用 Redis]]></title>
<url>%2F2021%2F07%2F17%2FSpirngBoot-%E6%95%B4%E5%90%88%E4%BD%BF%E7%94%A8-Redis%2F</url>
<content type="text"><![CDATA[本文主要介绍如何在 springboot 项目中集成使用 Redis。springboot 将很多基础的工具组件都封装成了一个个的 starter,比如基础的 web 框架相关的 pring-boot-starter-web,操作数据库相关的 spring-boot-starter-data-jpa 等等。如果要操作 Redis,同理需要引入 redis 相关的 starter:spring-boot-starter-data-redis。下面介绍 springboot 集成使用 Redis 的详细过程。 1、引入 redis starter POM 依赖1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 2、Redis 数据源配置springboot 配置文件(application.properties)添加如下 redis 配置:123456# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password= 3、使用示例写一个测试用例测试使用下,添加并查询 K-V: 123456789101112@RunWith(SpringRunner.class)@SpringBootTestpublic class RedisUsageTest extends TestCase { @Autowired private StringRedisTemplate redisTemplate; @Test public void redisUsageTest() { redisTemplate.opsForValue().set("name", "jim"); System.out.println(redisTemplate.opsForValue().get("name")); }} 登录 Redis 控制台可以看到数据已经写入了 Redis123127.0.0.1:6379> get name"jim"127.0.0.1:6379> 参考文档https://cloud.tencent.com/developer/article/1457454https://blog.csdn.net/tuzongxun/article/details/107794207]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 解压 gzip 和 tar.gz 文件]]></title>
<url>%2F2021%2F05%2F23%2FJava-%E8%A7%A3%E5%8E%8B-gzip-%E5%92%8C-tar-gz-%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[在开发过程中有时候会需要解压 gzip 或者 tar.gz 文件,下面封装了一个工具类,可以解压 gzip 和 tar.gz 文件。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778package com.example.demo.common.utils;/** * Created by qianghaohao on 2021/5/23 */import org.apache.commons.compress.archivers.tar.TarArchiveEntry;import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.util.zip.GZIPInputStream;/** * @description: * @author: qianghaohao * @time: 2021/5/23 */public class FileUtils { private static final int BUFFER_SIZE = 1024; /** * 解压 gzip 文件 * * @param input * @param output * */ public static void decompressGzip(File input, File output) throws IOException { try (GZIPInputStream in = new GZIPInputStream(new FileInputStream(input))) { try (FileOutputStream out = new FileOutputStream(output)) { byte[] buffer = new byte[BUFFER_SIZE]; int len; while((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } } } /** * 解压 tar.gz 文件到指定目录 * * @param tarGzFile tar.gz 文件路径 * @param destDir 解压到 destDir 目录,如果没有则自动创建 * */ public static void extractTarGZ(File tarGzFile, String destDir) throws IOException { GzipCompressorInputStream gzipIn = new GzipCompressorInputStream(new FileInputStream(tarGzFile)); try (TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) { TarArchiveEntry entry; while ((entry = (TarArchiveEntry) tarIn.getNextEntry()) != null) { if (entry.isDirectory()) { File f = new File(destDir + "/" + entry.getName()); boolean created = f.mkdirs(); if (!created) { System.out.printf("Unable to create directory '%s', during extraction of archive contents.\n", f.getAbsolutePath()); } } else { int count; byte [] data = new byte[BUFFER_SIZE]; FileOutputStream fos = new FileOutputStream(destDir + "/" + entry.getName(), false); try (BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER_SIZE)) { while ((count = tarIn.read(data, 0, BUFFER_SIZE)) != -1) { dest.write(data, 0, count); } } } } } }} 使用示例:123456789101112@Testpublic void decompressGizpTest() throws IOException { File input = new File("/xxx/output.gz"); File output = new File("/xxx/output"); FileUtils.decompressGzip(input, output);}@Testpublic void decompressTarGizpTest() throws IOException { File input = new File("/xxx/output.tar.gz"); FileUtils.extractTarGZ(input, "/tmp/");}]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 发送 HTTP 请求的两种常用方法]]></title>
<url>%2F2021%2F05%2F16%2FJava-%E5%8F%91%E9%80%81-HTTP-%E8%AF%B7%E6%B1%82%E7%9A%84%E4%B8%A4%E7%A7%8D%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%2F</url>
<content type="text"><![CDATA[本文主要介绍在 Java 编程中发送 HTTP 请求的两种常用方法: JDK 原生的 HttpURLConnection 发送 HTTP 请求 Apache HhttpClient 发送 HTTP 请求 两种方法都可以发送 HTTP 请求,第一种是 Java 原生的,因此使用起来相对麻烦一些,第二种是通过第三方的包来实现,这个包是 Apache 旗下的专门用来发送 HTTP 请求的 HttpClient 包,是对 Java 原生的 HttpURLConnection 扩展,因此功能也更加强大,使用起来也相对简单一些,目前这种方式发送 HTTP 请求应用比较广泛,因此主要学习这种方式。 Apache HttpClient 官方文档见这里:http://hc.apache.org/httpcomponents-client-5.1.x/ Talk is cheap. Show me the code. 下面看具体使用代码示例。 1、JDK 原生的 HttpURLConnection 发送 HTTP 请求 GET/POST基于 JDK 原生的 HttpURLConnection 类,简单封装了下 HTTP GET 和 POST 请求方法,重在学习使用。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161package com.example.demo.common.utils;/** * Created by qianghaohao on 2021/5/16 */import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;/** * @description: 使用 java 原生 HttpURLConnection 类发送 http 请求 * @author: qianghaohao * @time: 2021/5/16 */public class HttpURLConnectionUtils { public static String doGet(String httpUrl) { HttpURLConnection connection = null; InputStream inputStream = null; BufferedReader bufferedReader = null; String result = null; try { // 创建远程url连接对象 URL url = new URL(httpUrl); // 通过远程url连接对象打开一个连接,强转成httpURLConnection类 connection = (HttpURLConnection) url.openConnection(); // 设置连接方式:get connection.setRequestMethod("GET"); // 设置连接主机服务器的超时时间:15000毫秒 connection.setConnectTimeout(15000); // 设置读取远程返回的数据时间:60000毫秒 connection.setReadTimeout(60000); // 通过connection连接,获取输入流 if (connection.getResponseCode() == 200) { inputStream = connection.getInputStream(); // 封装输入流is,并指定字符集 bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); // 存放数据 StringBuilder sb = new StringBuilder(); String temp; while ((temp = bufferedReader.readLine()) != null) { sb.append(temp); sb.append(System.lineSeparator()); // 这里需要追加换行符,默认读取的流没有换行符,需要加上才能符合预期 } result = sb.toString(); } } catch (IOException e) { e.printStackTrace(); } finally { // 关闭资源 if (null != bufferedReader) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != inputStream) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != connection) { connection.disconnect();// 关闭远程连接 } } return result; } public static String doPost(String httpUrl, String param) { HttpURLConnection connection = null; InputStream is = null; OutputStream os = null; BufferedReader br = null; String result = null; try { URL url = new URL(httpUrl); // 通过远程url连接对象打开连接 connection = (HttpURLConnection) url.openConnection(); // 设置连接请求方式 connection.setRequestMethod("POST"); // 设置连接主机服务器超时时间:15000毫秒 connection.setConnectTimeout(15000); // 设置读取主机服务器返回数据超时时间:60000毫秒 connection.setReadTimeout(60000); // 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true connection.setDoOutput(true); // 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 通过连接对象获取一个输出流 os = connection.getOutputStream(); // 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的 os.write(param.getBytes()); // 通过连接对象获取一个输入流,向远程读取 if (connection.getResponseCode() == 200) { is = connection.getInputStream(); // 对输入流对象进行包装:charset根据工作项目组的要求来设置 br = new BufferedReader(new InputStreamReader(is, "UTF-8")); StringBuilder sb = new StringBuilder(); String temp; // 循环遍历一行一行读取数据 while ((temp = br.readLine()) != null) { sb.append(temp); sb.append(System.lineSeparator()); } result = sb.toString(); } } catch (IOException e) { e.printStackTrace(); } finally { // 关闭资源 if (null != br) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != os) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != is) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != connection) { // 断开与远程地址url的连接 connection.disconnect(); } } return result; }} 2、Apache HhttpClient 发送 HTTP 请求 GET/POST基于 Apache HhttpClient,简单封装了下 HTTP GET 和 POST 请求方法,重在学习使用。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125package com.example.demo.common.utils;/** * Created by qianghaohao on 2021/5/16 */import org.apache.http.HttpEntity;import org.apache.http.NameValuePair;import org.apache.http.client.entity.UrlEncodedFormEntity;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.client.methods.HttpPost;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.message.BasicNameValuePair;import org.apache.http.util.EntityUtils;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.Set;/** * @description: 使用 Apache HttpClient 发送 http 请求 * @author: qianghaohao * @time: 2021/5/16 */public class HttpClientUtils { public static String doGet(String url) { String content = null; // 创建 HttpClient 对象 CloseableHttpClient httpclient = HttpClients.createDefault(); // 创建 Http GET 请求 HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = null; try { // 执行请求 response = httpclient.execute(httpGet); // 判断返回状态是否为200 if (response.getStatusLine().getStatusCode() == 200) { //响应体内容 content = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (IOException e) { e.printStackTrace(); } finally { if (response != null) { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } try { httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return content; } public static String doPost(String url, Map<String, Object> paramMap) { CloseableHttpClient httpClient = null; CloseableHttpResponse httpResponse = null; String result = null; // 创建httpClient实例 httpClient = HttpClients.createDefault(); // 创建httpPost远程连接实例 HttpPost httpPost = new HttpPost(url); // 设置请求头 httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); // 封装post请求参数 if (null != paramMap && paramMap.size() > 0) { List<NameValuePair> nvps = new ArrayList<NameValuePair>(); // 通过map集成entrySet方法获取entity Set<Map.Entry<String, Object>> entrySet = paramMap.entrySet(); // 循环遍历,获取迭代器 for (Map.Entry<String, Object> mapEntry : entrySet) { nvps.add(new BasicNameValuePair(mapEntry.getKey(), mapEntry.getValue().toString())); } // 为httpPost设置封装好的请求参数 try { httpPost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } try { // httpClient对象执行post请求,并返回响应参数对象 httpResponse = httpClient.execute(httpPost); // 从响应对象中获取响应内容 HttpEntity entity = httpResponse.getEntity(); result = EntityUtils.toString(entity); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭资源 if (null != httpResponse) { try { httpResponse.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != httpClient) { try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; }} 3、上面封装类使用示例1234567891011121314151617181920212223242526272829@Testpublic void httpDoGetTest() { // Java 原生 HttpURLConnection 发送 HTTP 请求测试 String url = "https://httpbin.org/get"; String result = HttpURLConnectionUtils.doGet(url); System.out.println(result); // Apache HttpClient 发送 http 请求测试 System.out.println("=============="); result = HttpClientUtils.doGet(url); System.out.println(result);}@Testpublic void httpDoPostTest() { // Java 原生 HttpURLConnection 发送 HTTP 请求测试 String url = "https://httpbin.org/post"; String urlParameters = "name=Jack&occupation=programmer"; String result = HttpURLConnectionUtils.doPost(url, urlParameters); System.out.println(result); // Apache HttpClient 发送 http 请求测试 System.out.println("=============="); Map<String, Object> params = new HashMap<>(); params.put("name", "Jack"); params.put("occupation", "programmer"); result = HttpClientUtils.doPost(url, params); System.out.println(result);}]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mac 下多版本 JDK 安装及切换]]></title>
<url>%2F2021%2F04%2F10%2FMac-%E4%B8%8B%E5%A4%9A%E7%89%88%E6%9C%AC-JDK-%E5%AE%89%E8%A3%85%E5%8F%8A%E5%88%87%E6%8D%A2%2F</url>
<content type="text"><![CDATA[背景由于公司项目基于 JDK 1.8,我本地默认安装的是 JDK 10,这样在 idea 中通过 maven 编译的时候各种报错,有点不兼容。为了解决这个问题最好的办法就是再安装一个 1.8 的 JDK 版本了,和公司项目代码版本兼容。本文主要介绍 Mac 下如何安装 JDK 并且多版本如何切换。 Mac 下 JDK 安装配置Mac 下安装 JDK 比较简单,只需要访问 Oracle 官网,找到对应环境和版本的 JDK 下载安装即可,下载 mac 下的 dmg 安装文件,一路点击下一步即可。JDK 下载页面见这里: https://www.oracle.com/cn/java/technologies/javase-downloads.html mac 下安装 jdk 1.8 的话只需要下载这个安装包即可:jdk-8u281-macosx-x64.dmg 同时安装其他版本,只需要下载其他版本的 dmg 安装包安装即可。 Mac 下多版本 JDK 切换1、使用如下命令查看本地安装了哪些 JDK 版本及对应的安装路径:123456haohao@haohaodeMacBook-Pro ~ /usr/libexec/java_home -V ✔ 11:03:12Matching Java Virtual Machines (2): 10.0.2, x86_64: "Java SE 10.0.2" /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home 1.8.0_281, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home 可以看出当前系统有两个 JDK 版本和对应的安装路径。 2、切换到对应的 JDK 版本 设置 JAVA_HOME 环境变量为 JDK 安装路径,vim ~/.zshrc1export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home source 一下让配置生效: source ~/.zshrc 3、检查版本是否切换成功1234haohao@haohaodeMacBook-Pro ~ java -version ✔ 11:11:27java version "1.8.0_281"Java(TM) SE Runtime Environment (build 1.8.0_281-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java JSch 远程执行 Shell 命令]]></title>
<url>%2F2021%2F04%2F03%2FJava-JSch-%E8%BF%9C%E7%A8%8B%E6%89%A7%E8%A1%8C-Shell-%E5%91%BD%E4%BB%A4%2F</url>
<content type="text"><![CDATA[背景项目需求,需要远程 ssh 登录到某个节点执行 shell 命令来完成任务。对于这种需求,如果不用 java 程序,直接 linux 的 ssh 命令就可以完成,但是在编码到程序中时需要相关的程序包来完成,本文主要介绍在 java 中如何使用 JSch 包实现 ssh 远程连接并执行命令。 JSch 简介JSch 是Java Secure Channel的缩写。JSch是一个SSH2的纯Java实现。它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等,当然你也可以集成它的功能到你自己的应用程序。框架jsch很老的框架,更新到2016年,现在也不更新了。 JSch 使用 shell 执行命令,有两种方法 ChannelExec: 一次执行一条命令,一般我们用这个就够了。 ChannelShell: 可执行多条命令,平时开发用的不多,根据需要来吧;12ChannelExec channelExec = (ChannelExec) session.openChannel("exec");//只能执行一条指令(也可执行符合指令)ChannelShell channelShell = (ChannelShell) session.openChannel("shell");//可执行多条指令 不过需要输入输出流 1. ChannelExec 每个命令之间用 ; 隔开。说明:各命令的执行给果,不会影响其它命令的执行。换句话说,各个命令都会执行,但不保证每个命令都执行成功。 每个命令之间用 && 隔开。说明:若前面的命令执行成功,才会去执行后面的命令。这样可以保证所有的命令执行完毕后,执行过程都是成功的。 每个命令之间用 || 隔开。说明:|| 是或的意思,只有前面的命令执行失败后才去执行下一条命令,直到执行成功一条命令为止。 2. ChannelShell对于ChannelShell,以输入流的形式,可执行多条指令,这就像在本地计算机上使用交互式shell(它通常用于:交互式使用)。如要要想停止,有两种方式: 发送一个exit命令,告诉程序本次交互结束; 使用字节流中的available方法,来获取数据的总大小,然后循环去读。 使用示例1. 引入 pom 依赖12345<dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.53</version></dependency> 2. jsch 使用示例在此封装了一个 Shell 工具类,用来执行 shell 命令,具体使用细节在代码注释中有说明,可以直接拷贝并使用,代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110package org.example.shell;/** * Created by qianghaohao on 2021/3/28 */import com.jcraft.jsch.ChannelExec;import com.jcraft.jsch.JSch;import com.jcraft.jsch.Session;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;/** * @description: * @author: qianghaohao * @time: 2021/3/28 */public class Shell { private String host; private String username; private String password; private int port = 22; private int timeout = 60 * 60 * 1000; public Shell(String host, String username, String password, int port, int timeout) { this.host = host; this.username = username; this.password = password; this.port = port; this.timeout = timeout; } public Shell(String host, String username, String password) { this.host = host; this.username = username; this.password = password; } public String execCommand(String cmd) { JSch jSch = new JSch(); Session session = null; ChannelExec channelExec = null; BufferedReader inputStreamReader = null; BufferedReader errInputStreamReader = null; StringBuilder runLog = new StringBuilder(""); StringBuilder errLog = new StringBuilder(""); try { // 1. 获取 ssh session session = jSch.getSession(username, host, port); session.setPassword(password); session.setTimeout(timeout); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); // 获取到 ssh session // 2. 通过 exec 方式执行 shell 命令 channelExec = (ChannelExec) session.openChannel("exec"); channelExec.setCommand(cmd); channelExec.connect(); // 执行命令 // 3. 获取标准输入流 inputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getInputStream())); // 4. 获取标准错误输入流 errInputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getErrStream())); // 5. 记录命令执行 log String line = null; while ((line = inputStreamReader.readLine()) != null) { runLog.append(line).append("\n"); } // 6. 记录命令执行错误 log String errLine = null; while ((errLine = errInputStreamReader.readLine()) != null) { errLog.append(errLine).append("\n"); } // 7. 输出 shell 命令执行日志 System.out.println("exitStatus=" + channelExec.getExitStatus() + ", openChannel.isClosed=" + channelExec.isClosed()); System.out.println("命令执行完成,执行日志如下:"); System.out.println(runLog.toString()); System.out.println("命令执行完成,执行错误日志如下:"); System.out.println(errLog.toString()); } catch (Exception e) { e.printStackTrace(); } finally { try { if (inputStreamReader != null) { inputStreamReader.close(); } if (errInputStreamReader != null) { errInputStreamReader.close(); } if (channelExec != null) { channelExec.disconnect(); } if (session != null) { session.disconnect(); } } catch (IOException e) { e.printStackTrace(); } } return runLog.toString(); }} 上述工具类使用:12345678910111213141516package org.example;import org.example.shell.Shell;/** * Hello world! * */public class App { public static void main( String[] args ) { String cmd = "ls -1"; Shell shell = new Shell("192.168.10.10", "ubuntu", "11111"); String execLog = shell.execCommand(cmd); System.out.println(execLog); }} 需要注意的点如果需要后台执行某个命令,不能直接 <命令> + & 的方式执行,这样在 JSch 中不生效,需要写成这样的格式:<命令> > /dev/null 2>&1 &。比如要后台执行 sleep 60,需要写成 sleep 60 > /dev/null 2>&1 具体 issue 见这里:https://stackoverflow.com/questions/37833683/running-programs-using-jsch-in-the-background 参考文档https://www.cnblogs.com/slankka/p/11988477.html https://blog.csdn.net/sinat_24928447/article/details/83022818]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 中 final 关键字详解]]></title>
<url>%2F2021%2F04%2F03%2FJava-%E4%B8%AD-final-%E5%85%B3%E9%94%AE%E5%AD%97%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[在 Java 中,final 关键字可以修饰的东西比较多,很容易混淆,在这里记录一下。主要从功能上说明一下,不做过多的代码演示。 final 关键字用途1. final 变量凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值。 2. final方法参数如果 final 关键字修饰方法参数时,方法中是不能改变该参数的,举例如下: 1234public void testFunc(final Integer i) { i = 20; // 报错: Cannot assign a value to final variable 'i' System.out.println(i);} 3. final 方法final也可以声明方法。方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。 4. final 类使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java 中有许多类是final的,譬如String, Interger以及其他包装类。 5. final 关键字好处 final关键字提高了性能。JVM和Java应用都会缓存final变量。 final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。 使用final关键字,JVM会对方法、变量及类进行优化。 final 和 static两者得分开解释,不能混为一谈,这样更容易理解,static 作用于成员变量用来表示只保存一份副本,是类变量而已,而final的作用是用来保证变量不可变。两者用在一起表示类的不可以修改值的类变量。 1234public final class Employee { public static final String SERVER = "example.com:9090"; ...} 总结 参考文档https://www.jb51.net/article/157603.htm]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[GO 语言程序设计读书笔记-接口值]]></title>
<url>%2F2020%2F09%2F06%2FGO-%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-%E6%8E%A5%E5%8F%A3%E5%80%BC%2F</url>
<content type="text"><![CDATA[从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。比如下面声明一个接口变量 w 并赋值,那么 w 接口值可以用如下图表示: 接口的零值接口的零值就是把它的动态类型和动态值都设为 nil,如下图所示:var w io.Writer // 接口的零值 在这种情况下就是一个 nil 接口值,可以用 w == nil 或者 w != nil 来检测一个接口值是否时 nil。调用一个 nil 接口的任何方法都会导致崩溃:w.Write([]byte("hello")) // panic:对空指针引用值 接口值的比较接口值可以用 == 和 != 来做比较。如果两个接口值都是 nil 或者二者的动态类型完全一致且二者动态值相等(使用动态类型的==操作符来比较),那么两个接口值相等。因为接口值是可以比较的,所以它们可以作为 map 的键,也可以作为 switch 语句的操作数。 注意:在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可以比较的(比如 slice),那么这个比较会导致代码 panic:12345678910package mainimport ( "fmt")func main() { var x interface{} = []int{1, 2, 3} fmt.Println(x == x)} panic 信息:12345panic: runtime error: comparing uncomparable type []intgoroutine 1 [running]:main.main() /Users/qianghaohao/go/src/examplecode/main.go:9 +0x82 获取接口值的动态类型使用 fmt 包的 %T 可以打印某个接口值的动态类型,有助于问题调试:1234567func main() { var w io.Writer fmt.Printf("%T\n", w) // <nil> w = os.Stdout fmt.Printf("%T\n", w) // os.File} 输出:12<nil>*os.File nil 接口值和动态值为 nil 接口值的区别nil 接口值:接口值的动态类型和动态值都为 nil。1var w io.Writer 动态值为 nil 的接口:接口值的动态类型有具体类型,动态值为 nil。12var buf *bytes.Buffervar w io.Writer = buf 示例代码分析:123456789101112131415const debug = falsefunc main() { var buf *bytes.Buffer if debug { buf = new(bytes.Buffer) } f(buf)}func f(out io.Writer) { if out != nil { out.Write([]byte("done!\n")) }} 当 debug 为 true 时,buf 为执行 bytes.Buffer 对象的一个指针,当调用函数 f 时,将 buf 赋给 out,此时 out 的动态值不是 nil,而是指向 byte.Buffer 对象的指针。out 的动态类型是 *byte.Buffer。随后调用 out.Write 函数,代码正常运行,不会引发 panic。 当 debug 为 false 时,调用函数 f 时,将 buf 赋值给 out,此时 out 的动态值虽然时 nil,但其动态类型不是 nil,所以 out != nil 表达式为 true,随后调用 out.Write 函数,但是 Write 函数的接收者为 nil,所以在底层拷贝 buffer 时会导致 panic(空指针引用),具体可以看看 Write 函数的底层实现就知道了。]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 kind 1 分钟启动一个本地 k8s 开发集群]]></title>
<url>%2F2020%2F08%2F15%2F%E4%BD%BF%E7%94%A8-kind-1-%E5%88%86%E9%92%9F%E5%90%AF%E5%8A%A8%E4%B8%80%E4%B8%AA%E6%9C%AC%E5%9C%B0-k8s-%E5%BC%80%E5%8F%91%E9%9B%86%E7%BE%A4%2F</url>
<content type="text"><![CDATA[kind 简介Github 地址:https://github.com/kubernetes-sigs/kindkind 是一个快速启动 kubernetes 集群的工具,适合本地 k8s 开发环境搭建,能在 1 分钟之内就启动一个非常轻量级的 k8s 集群。之所以如此之快,得益于其基于其把整个 k8s 集群用到的组件都封装在了 Docker 容器里,构建一个 k8s 集群就是启动一个 Docker 容器,如此简单,正如下面图片描述一样: 说说我为什么使用 kind 吧:我之前本地 k8s 开发环境是基于 vagrant + virtualbox 来搭建,但是和 kind 比起来太重量级了,主要有如下痛点: 资源消耗严重:vagrant + virtualbox + kubernetes 这一套其实本质上还是 k8s 运行在 virtualbox 虚拟机上,这个资源消耗可想而知,我的电脑配置低,经常由于资源消耗太多导致电脑发热、风扇狂转、死机。。。 使用复杂:vagrant + virtualbox + kubernetes 虽然比直接二进制搭建简化了不少,但是还是有一定的技术门槛的,需要对 vagrant 有一定的了解,而且编写 vagrant 需要有一定的经验才能把集群配置好; kind 使用kind 的使用非常简单,其实就是一个命令行工具,通过这个工具创建、删除 k8s 集群,下面简单说下使用。 1.准备工作 Kind 的主要功能目前需要有 Docker 环境的支持,可参考 Docker 官方文档进行安装; 安装操作 k8s 的 kubectl 命令行,安装方法可参考官方文档; 2.安装到 github release 页下载对应操作系统的二进制文件到本地,并放到 PATH 环境变量:https://github.com/kubernetes-sigs/kind/releases 3.创建集群1kind create cluster 完成后就可以直接 kubectl 操作集群了,kubeconfig 已经自动生效了,在 ~/.kube/config 路径。123$ kubectl get nodeNAME STATUS ROLES AGE VERSIONkind-control-plane Ready master 53m v1.18.2 4.获取集群 kubeconfig 文件1kind get kubeconfig 5.销毁集群1kind delete cluster 执行 kind --help 获取帮助和其他支持的命令参数。 kind 配置我们可以对要创建的集群进行一些定制化配置,kind 支持的配置见这里:https://kind.sigs.k8s.io/docs/user/configuration/配置方法:参照文档,编写配置 kind 配置文件 config.yaml,然后在 kind 创建集群的时候指定配置文件:1kind create cluster --config=/foo/bar/config.yaml 举例:默认 kind 创建出来的集群 apiserver 监听的地址是:127.0.0.1:[随机端口],我要改成默认监听的地址是:0.0.0.0:6443,编写如下 config.yaml12345678910kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4networking: # WARNING: It is _strongly_ recommended that you keep this the default # (127.0.0.1) for security reasons. However it is possible to change this. apiServerAddress: "0.0.0.0" # By default the API server listens on a random open port. # You may choose a specific port but probably don't need to in most cases. # Using a random port makes it easier to spin up multiple clusters. apiServerPort: 6443 创建集群时指定上面配置文件:1kind create cluster --config=config.yaml 总结总而言之,kind 是一个非常轻量级的工具,能以最轻量的方式构建 k8s 集群,其设计理念是是一个 k8s 就是一个 Docker 容器,构建复杂的 k8s 集群变成了启动一个 Docker 容器,如此简单。 我对 kind 的评价是:快!快!快! 轻量!轻量!轻量! 效率!效率!效率!]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang 项目配置文件读取之 viper 实践]]></title>
<url>%2F2020%2F07%2F11%2FGolang-%E9%A1%B9%E7%9B%AE%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E4%B9%8B-viper-%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[在我们做一个工程化项目的时候,经常涉及到配置文件的读取,viper 包很好地满足这一需求,而且在 Golang 生态中是流行度最高的。导入方式:1import "github.com/spf13/viper" 这里分享下我对 viper 包的使用关键实践: 首先,在代码工程中单独定义一个包(我一般起名为 config 或者 configloader),这个包专门用来读取加载配置文件,并做相关校验,包里面我定义 3 个函数和 1 个全局变量: var viperConfig *viper.Viper: 全局配置变量; func Init(configDir string) error: 初始化加载配置文件; func GetConfig() *viper.Viper: 获取配置文件,供其他包调用,拿到配置文件实例; func validateConfig(v *viper.Viper) error: 校验配置文件; 接下来在工程入口处引用上面这个配置包的 Init 函数做配置文件的初始化和加载,加载的结果就是实例化一个 viper.Viper 全局变量,在其他包中用的时候调用这个配置包的 func GetConfig() viper.Vipe 函数即可拿到这个全局变量,即配置文件内容。 示例代码(代码仅供参考,截取字自前做的爬虫程序部分代码):12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576package configloaderimport ( "fmt" "github.com/spf13/viper" "example.com/pkg/util/fs")// viperConfig 全局配置变量var viperConfig *viper.Viper// 初始化配置文件相关设置,在 main 包中调用进行初始化加载func Init(configDir string) error { if configDir == "" { return fmt.Errorf("config directory is empty") } if !fs.PathExists(configDir) { return fmt.Errorf("no such config directory: %s", configDir) } viper.SetConfigName("spider") // name of config file (without extension) viper.AddConfigPath(configDir) // path to look for the config file in err := viper.ReadInConfig() // Find and read the config file if err != nil { // Handle errors reading the config file return fmt.Errorf("Fatal error config file: %s \n", err) } viperConfig = viper.GetViper() if err = validateConfig(viperConfig); err != nil { return fmt.Errorf("invalid configuration, error msg: %s", err) } return nil}// GetConfig 获取全局配置func GetConfig() *viper.Viper { return viperConfig}func validateConfig(v *viper.Viper) error { var ( urlListFile = v.GetString("urlListFile") outputDirectory = v.GetString("outputDirectory") maxDepth = v.GetInt("maxDepth") crawlInterval = v.GetInt("crawlInterval") crawlTimeout = v.GetInt("crawlTimeout") targetUrl = v.GetString("targetUrl") threadCount = v.GetInt("threadCount") ) if urlListFile == "" { return fmt.Errorf("invalid targetUrl: %s, please check configuration", urlListFile) } if outputDirectory == "" { return fmt.Errorf("invalid targetUrl: %s, please check configuration", outputDirectory) } if maxDepth <= 0 { return fmt.Errorf("invalid maxDepth: %d, please check configuration", maxDepth) } if crawlInterval <= 0 { return fmt.Errorf("invalid crawlInterval: %d, please check configuration", crawlInterval) } if crawlTimeout <= 0 { return fmt.Errorf("invalid crawlTimeout: %d, please check configuration", crawlTimeout) } if targetUrl == "" { return fmt.Errorf("invalid targetUrl: %s, please check configuration", targetUrl) } if threadCount <= 0 { return fmt.Errorf("invalid threadCount: %d, please check configuration", threadCount) } return nil}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[通过 swagger-ui 查看 kube-apiserver apis]]></title>
<url>%2F2020%2F07%2F11%2F%E9%80%9A%E8%BF%87-swagger-ui-%E6%9F%A5%E7%9C%8B-kube-apiserver-apis%2F</url>
<content type="text"><![CDATA[从 k8s v1.14 开始,官方已经废弃了 swagger 接口,使用 openapi 规范,暴露出来的接口是: /openapi/v2,我们要想通过swagger-ui 来查看 apiserver 接口,可以自己本地跑个 swagger-ui 服务,然后访问 kube-apiserver 地址的 openapi 接口地址即可,swagger-ui 来源支持 openapi 数据格式。 1.本地启动 swagger-ui 服务1docker run --name swagger-ui --restart always -p 7070:8080 -d swaggerapi/swagger-ui:latest 2.访问 swagger-ui 地址 http://127.0.0.1:7070,输入 kube-apiserver 地址的 openapi 接口地址:http://kube-apiserver-ip:8080/openapi/v2 这里 openapi 地址是 kube-apiserver 的 http 地址,为了方便,如果是 https 比较麻烦,因为涉及到了 apiserver 的认证授权相关东西。所以为了方便本地调试,还是用 http 地址。 注意:直接访问存在跨域问题,需要安装 Chrome 的 Allow CORS 插件实现跨域访问,插件传送门:https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf?hl=en 参考文档:https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/https://blog.schwarzeni.com/2019/09/16/Minikube%E4%BD%BF%E7%94%A8Swagger%E6%9F%A5%E7%9C%8BAPI/]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang 项目中集成日志功能]]></title>
<url>%2F2020%2F07%2F11%2FGolang-%E9%A1%B9%E7%9B%AE%E4%B8%AD%E9%9B%86%E6%88%90%E6%97%A5%E5%BF%97%E5%8A%9F%E8%83%BD%2F</url>
<content type="text"><![CDATA[在一个 web 项目中,日志打印功能是必须的,有了详细的日志能为问题排查带来很大的便利。Golang 有很多开源的日志包可供使用,这里我还是使用非常流行的 logrus 包,结合 file-rotatelogs 包实现日志的自动切割轮转。 集成方法: 1.单独定义一个 package 名为 logger,里面只有一个 init.go 文件,初始化日志配置,这个配置是全局的: 日志输出格式为 json; 日志自动轮转,保留最近 7 天日志,一天产生一个日志文件,防止服务长时间运行占用系统过多的磁盘资源; 日志在输出到文件的同时并打到 stdout; 12345678910111213141516171819202122232425262728293031323334353637383940414243package loggerimport ( "io" "os" "path" "time" log "github.com/sirupsen/logrus" rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/demo/pkg/common/config" "github.com/demo/pkg/utils/fs")// 全局日志配置初始化// 1.日志同时输出到到文件和标准输出;// 2.保留最近 7 天日志,一天一个日志文件;//func init() { logDir := config.GetConfig().GetString("log_dir") if !fs.PathExists(logDir) { if err := os.MkdirAll(logDir, 0755); err != nil { panic(err) } } logfile := path.Join(logDir, "app") fsWriter, err := rotatelogs.New( logfile+"_%Y-%m-%d.log", rotatelogs.WithMaxAge(time.Duration(168)*time.Hour), rotatelogs.WithRotationTime(time.Duration(24)*time.Hour), ) if err != nil { panic(err) } multiWriter := io.MultiWriter(fsWriter, os.Stdout) log.SetReportCaller(true) log.SetFormatter(&log.JSONFormatter{}) log.SetOutput(multiWriter) log.SetLevel(log.InfoLevel)} 2.在项目入口 main 包引入该包,初始化全局日志配置 1234package mainimport ( _ "github.com/demo/pkg/logger" // 引入包时自动执行了 logger 包的 init 函数完成了日志初始化配置) 3.至此,在项目其他地方如果要打印日志,直接引用 logrus 包即可,在 main 包中已经完成了日志配置,无需在其他使用的地方再次初始化配置; 123456789package helloimport ( log "github.com/sirupsen/logrus")func HelloLog() { log.Info("HelloLog func called")}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang 从 Json 串中快速取出需要的字段]]></title>
<url>%2F2020%2F07%2F11%2FGolang-%E4%BB%8E-Json-%E4%B8%B2%E4%B8%AD%E5%BF%AB%E9%80%9F%E5%8F%96%E5%87%BA%E9%9C%80%E8%A6%81%E7%9A%84%E5%AD%97%E6%AE%B5%2F</url>
<content type="text"><![CDATA[在 web 编程中很多情况下接口的数据是 json 格式,在我们拿到接口的 json 数据后如何方便地从中提取出需要的字段呢?我们可以自定义一个结构体,然后通过 Golang 的标准库 json 解析到我们定义的结构体中。但是当 json 格式比较复杂,嵌套层级比较深的时候,还是用这种方法就比较麻烦了。在这里推荐一个包: gojsonq,可以很简便地从 json 串中提取出需要的字段,无需定义额外的结构体,然后解析,直接链式地从 json 串中提取需要的字段即可,有点类似动态语言:1go get github.com/thedevsaddam/gojsonq/v2 Github 地址:https://github.com/thedevsaddam/gojsonq Package 简介:A simple Go package to Query over JSON Data. It provides simple, elegant and fast ODM like API to access, query JSON document. 示例一:从如下 json 串中提取 name.first 字段数据1234567{ "name":{ "first":"Tom", "last":"Hanks" }, "age":61} Find 方法返回的结果为 interface{} 类型。123456789package mainimport gojsonq "github.com/thedevsaddam/gojsonq/v2"func main() { const json = `{"name":{"first":"Tom","last":"Hanks"},"age":61}` name := gojsonq.New().FromString(json).Find("name.first") println(name.(string)) // Tom} 示例二:从如下 json 串中提取 temperatures 字段数据,并计算平均值12345678910111213{ "city":"dhaka", "type":"weekly", "temperatures":[ 30, 39.9, 35.4, 33.5, 31.6, 33.2, 30.7 ]} 支持列表计算:12345678910111213package mainimport ( "fmt" gojsonq "github.com/thedevsaddam/gojsonq/v2")func main() { const json = `{"city":"dhaka","type":"weekly","temperatures":[30,39.9,35.4,33.5,31.6,33.2,30.7]}` avg := gojsonq.New().FromString(json).From("temperatures").Avg() fmt.Printf("Average temperature: %.2f", avg) // 33.471428571428575} 示例三:从如下 json 串中提取 data.pageNumber 数组,赋值给 Golang 整型数组 上面的例子提取简单的数值类型,返回的是 interface{} 类型,可以通过类型断言得到需要的数据。如果提取的是数组类型,是无法通过类型断言来获取 interface{} 类型的值的,可以使用 FindR 拿到结果,然后通过结果的 As 方法将数据解析到我们定义的变量类型中。123456789101112131415161718192021222324{ "code":"0", "message":"success", "data":{ "thumb_up":0, "system":0, "im":0, "avatarUrl":"https://profile.csdnimg.cn/F/4/B/helloworld", "invitation":0, "comment":0, "follow":0, "totalCount":0, "coupon_order":0, "pageNumbers":[ 1, 2, 3, 4, 5, 6 ] }, "status":true} 123456789101112131415161718192021222324252627282930313233343536373839func main() { jsonStr := `{ "code":"0", "message":"success", "data":{ "thumb_up":0, "system":0, "im":0, "avatarUrl":"https://profile.csdnimg.cn/F/4/B/helloworld", "invitation":0, "comment":0, "follow":0, "totalCount":0, "coupon_order":0, "pageNumbers":[ 1, 2, 3, 4, 5, 6 ] }, "status":true }` // 保存 json 字符串中 pageNumbers 字段列表数据 pageNumbers := make([]int, 0) result, err := gojsonq.New().FromString(jsonStr).FindR("data.pageNumbers") if err != nil { fmt.Println(err) } // 将提前的数据解析为自定义变量类型 err = result.As(&pageNumbers) if err != nil { fmt.Println(err) } fmt.Println(pageNumbers)} 更多功能及示例见官方文档:https://github.com/thedevsaddam/gojsonq/wiki/Queries]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubernetes 源码编译]]></title>
<url>%2F2020%2F03%2F01%2Fkubernetes-%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%2F</url>
<content type="text"><![CDATA[kubernetes 源码编译分为本地编译和镜像编译,本地编译是指最终编译出来的是二进制可执行文件,镜像编译是最终编译出来的产出物为 docker 镜像 tar 包。本文主要介绍本地编译的方法,以编译 kube-apiserver 组件为例说明。 环境要求 Go 环境: go1.12.xx gcc 我的环境说明:Mac Os + go1.12.10 + gcc,如果读者本地的 Go 版本不是 go1.12.xx,可以使用 gvm 工具安装一个,gvm 是 Go 多版本管理工具,具体使用方法可以看之前的文章:Golang 多版本管理神器 gvm 本地编译也分为两种,一种是 make 编译,另一种是 Go 命令行编译,下面一一介绍: 一、make 编译1.下载 k8s 源码1go get k8s.io/kubernetes 2.编译指定版本源码(以1.16.3为例)12cd $GOPATH/src/k8s.io/kubernetesgit checkout tags/v1.16.3 3.设置要编译的组件(以编译 kube-apiserver 组件为例说明) mac 下编译要安装 GNU tar: sudo brew install gnu-tar 1make clean && make WHAT=cmd/kube-apiserver 编译产出物会在 _output/bin 目录生成:12345678$ ls -1 _output/binconversion-gendeepcopy-gendefaulter-gengo-bindatago2makekube-apiserveropenapi-gen 二、Go 命令编译1.cd $GOPATH/src/k8s.io/kubernetes && make generated_files2.进入对应组件目录编译(以 kube-apiserver组件编译为例)1cd cmd/kube-apiserver && go build -v 源码编译可能遇到的问题编译可能报类似下面错误:12345678go/build: importGo k8s.io/kubernetes: exit status 1can't load package: package k8s.io/kubernetes: cannot find module providing package k8s.io/kubernetes+++ [0301 18:04:57] Building go targets for darwin/amd64: ./vendor/k8s.io/code-generator/cmd/deepcopy-gencan't load package: package k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/deepcopy-gen: cannot find module providing package k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/deepcopy-gen!!! [0301 18:05:04] Call tree:!!! [0301 18:05:04] 1: 解决办法:golang 版本换成 go1.12.xx 即可。具体 issue 见这里:https://github.com/kubernetes/kubernetes/issues/84224 参考资料https://www.kubernetes.org.cn/5033.htmlhttps://blog.csdn.net/boling_cavalry/article/details/88591982https://github.com/MicrosoftDocs/Virtualization-Documentation/blob/master/virtualization/windowscontainers/kubernetes/compiling-kubernetes-binaries.md]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang 多版本管理神器 gvm]]></title>
<url>%2F2020%2F03%2F01%2FGolang-%E5%A4%9A%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E7%A5%9E%E5%99%A8-gvm%2F</url>
<content type="text"><![CDATA[缘起最近编译 kubernetes 遇到了点坑,编译各种报错,经搜索调研发现 k8s 的编译对 go 的版本有很严格的要求。比如我的 go1.13.4 就无法编译 kubernetes v1.16.3,必须得 go1.12.xx 版本才能编译。为了解决这种尴尬的场景只能再在主机安装个 go1.12.xx 版本,那么有没有什么优雅的方式来实现本机多版本 Golang 版本的管理呢,能很方便的进行不同版本的切换,这也是本文的目的,推荐一款 Go多版本管理神器 gvm,用法类似 Python 的多版本管理工具 pyenv。 简介gvm,即 Go Version Manager,Go 版本管理器,使用 shell 脚本开发,它可以非常轻量的切换 Go 版本。对比其他语言,通常也有类似的工具,如 NodeJS 的 NVM,Python 的 pyenv 等。在使用方法上和 Python 的多版本管理工具 pyenv 非常类似。 其实不借助类似的版本管理工具安装多个版本 Go 也是可以自己手动实现的,做法很简单,就是下载不同的 Golang 安装包,然后放置到独立的目录,使用时将 GOROOT 和 GOPATH 指向对应版本的目录即可完成版本切换。其实 gvm 原理上就是这么做的,只不过通过工具的形式将这些繁杂的手工操作封装起来,使得开发起来更加优雅,不必再为 Go 的安装、版本管理花费更多的心思。下面为 gvm 的工作原理: 安装1bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) 安装可能遇到的坑:123$ gvm install go1.12.10Downloading Go source...ERROR: Couldn't download Go source. Check the logs /Users/jim/.gvm/logs/go-download.log 根据提示看 log 报错12345Cloning into '/Users/jim/.gvm/archive/go'...error: RPC failed; curl 18 transfer closed with outstanding read data remainingfatal: the remote end hung up unexpectedlyfatal: early EOFfatal: index-pack failed 解决问题:vim ~/.gvm/scripts/install修改 GO_SOURCE_URL 变量地址为: GO_SOURCE_URL=git://github.com/golang/go 使用速记1.列出当前已安装的 Go 版本1gvm list 2.列出当前可以安装的 Go 版本1gvm listall 3.安装指定版本的 Go1gvm install go1.12.10 4.切换到指定的 Go 版本临时切换1gvm use go1.12 永久切换1gvm use go1.12 --default 参考资料https://juejin.im/post/5d848b66f265da03a7160e89]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang logrus 日志包及日志切割]]></title>
<url>%2F2020%2F03%2F01%2FGolang-logrus-%E6%97%A5%E5%BF%97%E5%8C%85%E5%8F%8A%E6%97%A5%E5%BF%97%E5%88%87%E5%89%B2%2F</url>
<content type="text"><![CDATA[本文主要介绍 Golang 中最佳日志解决方案,包括常用日志包 logrus 的基本使用,如何结合 file-rotatelogs 包实现日志文件的轮转切割两大话题。 Golang 关于日志处理有很多包可以使用,标准库提供的 log 包功能比较少,不支持日志级别的精确控制,自定义添加日志字段等。在众多的日志包中,更推荐使用第三方的 logrus 包,完全兼容自带的 log 包。logrus 是目前 Github 上 star 数量最多的日志库,logrus 功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能。 很多开源项目,如 docker,prometheus,dejavuzhou/ginbro 等,都是用了 logrus 来记录其日志。 logrus 特性 完全兼容 golang 标准库日志模块:logrus 拥有六种日志级别:debug、info、warn、error、fatal 和 panic,这是 golang 标准库日志模块的 API 的超集。 logrus.Debug(“Useful debugging information.”) logrus.Info(“Something noteworthy happened!”) logrus.Warn(“You should probably take a look at this.”) logrus.Error(“Something failed but I’m not quitting.”) logrus.Fatal(“Bye.”) //log之后会调用os.Exit(1) logrus.Panic(“I’m bailing.”) //log之后会panic() 可扩展的 Hook 机制:允许使用者通过 hook 的方式将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch 或者 mq 等,或者通过 hook 定义日志内容和格式等。 可选的日志输出格式:logrus 内置了两种日志格式,JSONFormatter 和 TextFormatter,如果这两个格式不满足需求,可以自己动手实现接口 Formatter 接口来定义自己的日志格式。 Field 机制:logrus 鼓励通过 Field 机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志。 logrus 是一个可插拔的、结构化的日志框架。 Entry: logrus.WithFields 会自动返回一个 *Entry,Entry里面的有些变量会被自动加上 time:entry被创建时的时间戳 msg:在调用.Info()等方法时被添加 level,当前日志级别 logrus 基本使用12345678910111213141516171819202122232425262728293031323334353637package mainimport ( "os" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus")var logger *logrus.Entryfunc init() { // 设置日志格式为json格式 log.SetFormatter(&log.JSONFormatter{}) log.SetOutput(os.Stdout) log.SetLevel(log.InfoLevel) logger = log.WithFields(log.Fields{"request_id": "123444", "user_ip": "127.0.0.1"})}func main() { logger.Info("hello, logrus....") logger.Info("hello, logrus1....") // log.WithFields(log.Fields{ // "animal": "walrus", // "size": 10, // }).Info("A group of walrus emerges from the ocean") // log.WithFields(log.Fields{ // "omg": true, // "number": 122, // }).Warn("The group's number increased tremendously!") // log.WithFields(log.Fields{ // "omg": true, // "number": 100, // }).Fatal("The ice breaks!")} 基于 logrus 和 file-rotatelogs 包实现日志切割很多时候应用会将日志输出到文件系统,对于访问量大的应用来说日志的自动轮转切割管理是个很重要的问题,如果应用不能妥善处理日志管理,那么会带来很多不必要的维护开销:外部工具切割日志、人工清理日志等手段确保不会将磁盘打满。 file-rotatelogs: When you integrate this to to you app, it automatically write to logs that are rotated from within the app: No more disk-full alerts because you forgot to setup logrotate! logrus 本身不支持日志轮转切割功能,需要配合 file-rotatelogs 包来实现,防止日志打满磁盘。file-rotatelogs 实现了 io.Writer 接口,并且提供了文件的切割功能,其实例可以作为 logrus 的目标输出,两者能无缝集成,这也是 file-rotatelogs 的设计初衷: It’s normally expected that this library is used with some other logging service, such as the built-in log library, or loggers such as github.com/lestrrat-go/apache-logformat. 示例代码:应用日志文件 /Users/opensource/test/go.log,每隔 1 分钟轮转一个新文件,保留最近 3 分钟的日志文件,多余的自动清理掉。1234567891011121314151617181920212223242526272829303132333435package mainimport ( "time" rotatelogs "github.com/lestrrat-go/file-rotatelogs" log "github.com/sirupsen/logrus")func init() { path := "/Users/opensource/test/go.log" /* 日志轮转相关函数 `WithLinkName` 为最新的日志建立软连接 `WithRotationTime` 设置日志分割的时间,隔多久分割一次 WithMaxAge 和 WithRotationCount二者只能设置一个 `WithMaxAge` 设置文件清理前的最长保存时间 `WithRotationCount` 设置文件清理前最多保存的个数 */ // 下面配置日志每隔 1 分钟轮转一个新文件,保留最近 3 分钟的日志文件,多余的自动清理掉。 writer, _ := rotatelogs.New( path+".%Y%m%d%H%M", rotatelogs.WithLinkName(path), rotatelogs.WithMaxAge(time.Duration(180)*time.Second), rotatelogs.WithRotationTime(time.Duration(60)*time.Second), ) log.SetOutput(writer) //log.SetFormatter(&log.JSONFormatter{})}func main() { for { log.Info("hello, world!") time.Sleep(time.Duration(2) * time.Second) }} Golang 标准日志库 log 使用虽然 Golang 标准日志库功能少,但是可以选择性的了解下,下面为基本使用的代码示例,比较简单:1234567891011121314151617package mainimport ( "fmt" "log")func init() { log.SetPrefix("【UserCenter】") // 设置每行日志的前缀 log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC) // 设置日志的抬头字段}func main() { log.Println("log...") log.Fatalln("Fatal Error...") fmt.Println("Not print!")} 自定义日志输出123456789101112131415161718192021222324252627282930package mainimport ( "io" "log" "os")var ( Info *log.Logger Warning *log.Logger Error *log.Logger)func init() { errFile, err := os.OpenFile("errors.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Fatalln("打开日志文件失败:", err) } Info = log.New(os.Stdout, "Info:", log.Ldate|log.Ltime|log.Lshortfile) Warning = log.New(os.Stdout, "Warning:", log.Ldate|log.Ltime|log.Lshortfile) Error = log.New(io.MultiWriter(os.Stderr, errFile), "Error:", log.Ldate|log.Ltime|log.Lshortfile)}func main() { Info.Println("Info log...") Warning.Printf("Warning log...") Error.Println("Error log...")} 相关文档https://mojotv.cn/2018/12/27/golang-logrus-tutorialhttps://github.com/lestrrat-go/file-rotatelogshttps://www.flysnow.org/2017/05/06/go-in-action-go-log.html]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Prometheus Operator 构建 Kubernetes 监控系统]]></title>
<url>%2F2019%2F12%2F15%2F%E4%BD%BF%E7%94%A8-Prometheus-Operator-%E6%9E%84%E5%BB%BA-Kubernetes-%E7%9B%91%E6%8E%A7%E7%B3%BB%E7%BB%9F%2F</url>
<content type="text"><![CDATA[本文主要介绍使用 Prometheus 监控 Kubernetes 的最佳实践,借助 Prometheus Operator 和 Helm 快速完成 Kubernetes 集群的监控。 Prometheus Operator 是 SRE 的一种实践,一种新的软件类型,大大简化了在 Kubernetes 上部署、管理和运行 Prometheus 和 Alertmanager 集群,同时还保持 Kubernetes 原生配置项,和 Kubernetes 无缝贴合。 什么是 Prometheus?Prometheus 是由 SoundCloud 开源的监控告警解决方案,从 2012 年开始编写代码,再到 2015 年 github 上开源以来,已经吸引了 9k+ 关注,以及很多大公司的使用;2016 年 Prometheus 成为继 k8s 后,第二名 CNCF(Cloud Native Computing Foundation) 成员。 作为新一代开源解决方案,很多理念与 Google SRE 运维之道不谋而合。比如 Operator 的概念:将复杂运维工作固化到 Operator 软件实体中。 Prometheus 主要功能 多维 数据模型(时序由 metric 名字和 k/v 的 labels 构成)。 灵活的查询语句(PromQL)。 无依赖存储,支持 local 和 remote 不同模型。 采用 http 协议,使用 pull 模式,拉取数据,简单易懂。 监控目标,可以采用服务发现或静态配置的方式。 支持多种统计数据模型,图形化友好。 Prometheus 核心组件 the main Prometheus server which scrapes and stores time series data client libraries for instrumenting application code a push gateway for supporting short-lived jobs special-purpose exporters for services like HAProxy, StatsD, Graphite, etc. an alertmanager to handle alerts various support tools Prometheus 架构 从架构图也可以看出 Prometheus 的主要模块包含,Server, Exporters, Pushgateway, PromQL, Alertmanager, WebUI 等。各组件之间的协同逻辑: Prometheus server 定期从静态配置的 targets 或者服务发现的 targets 拉取数据。 当新拉取的数据大于配置内存缓存区的时候,Prometheus 会将数据持久化到磁盘(如果使用 remote storage 将持久化到云端)。 Prometheus 可以配置 rules,然后定时查询数据,当条件触发的时候,会将 alert 推送到配置的 Alertmanager。 Alertmanager 收到警告的时候,可以根据配置,聚合,去重,降噪,最后发送警告。 可以使用 API,Prometheus Console 或者 Grafana 查询和聚合数据。 什么是 Operator?Operator 是 CoreOs 提出的一个概念,也是 SRE 的一种实践,通过开发软件的方式将复杂运维工作固化到 Operator 中。Operator 是一种新的基于 Kubernetes 平台的软件类型,主要解决 Kubernetes 中某些复杂应用的运维部书工作,利用 Kubernetes 的 Resources 和 Controllers 两种资源对象实现 Operator,Resources 描述期望的目标状态,Controllers 负责达成期望目标的具体实现逻辑。 Prometheus Operator 介绍Prometheus 是一套开源的系统监控、报警、时间序列数据库的组合,而 Prometheus Operator 是 CoreOS 开源的一套用于管理在 Kubernetes 集群上的 Prometheus 控制器,它是为了简化在 Kubernetes 上部署、管理和运行 Prometheus 和 Alertmanager 集群,同时还保持 Kubernetes 原生配置项,和 Kubernetes 无缝贴合。 Operator: Operator 资源会根据自定义资源(Custom Resource Definition / CRDs)来部署和管理 Prometheus Server,同时监控这些自定义资源事件的变化来做相应的处理,是整个系统的控制中心。 Prometheus: Prometheus 资源是声明性地描述 Prometheus 部署的期望状态。 Prometheus Server: Operator 根据自定义资源 Prometheus 类型中定义的内容而部署的 Prometheus Server 集群,这些自定义资源可以看作是用来管理 Prometheus Server 集群的 StatefulSets 资源。 ServiceMonitor: ServiceMonitor 也是一个自定义资源,它描述了一组被 Prometheus 监控的 targets 列表。该资源通过 Labels 来选取对应的 Service Endpoint,让 Prometheus Server 通过选取的 Service 来获取 Metrics 信息。 Service: kubernetes 的 service 资源,提供 metrics 接口,供 Prometheus 抓取。 Alertmanager: Alertmanager 也是一个自定义资源类型,由 Operator 根据资源描述内容来部署 Alertmanager 集群。 Helm 部署 Prometheus Operator使用 GitHub 提供的 prometheus-operator Chart 部署,该 Chart 提供了一套完整的 Prometheus 监控栈,不需要其他任何安装部署操作,一个 Chart 3 分钟搞定一切工作。1helm install --name kube-prometheus stable/prometheus-operator --namespace monitoring 测试访问:Grafana UI1kubectl port-forward <grafna pod> --address 0.0.0.0 -n monitoring 3000:3000 Prometheus UI1kubectl port-forward <prometheus pod> --address 0.0.0.0 -n monitoring 9090:9090 参考文档https://coreos.com/blog/the-prometheus-operator.htmlhttps://github.com/coreos/kube-prometheushttps://github.com/coreos/prometheus-operatorhttps://blog.csdn.net/aixiaoyang168/article/details/81661459https://blog.51cto.com/3241766/2450776https://www.chenshaowen.com/blog/quickly-deploy-prometheus-using-helm-and-operator.html]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MySQL 慢查询日志导入 Elasticsearch 可视化查询分析]]></title>
<url>%2F2019%2F10%2F24%2FMySQL-%E6%85%A2%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97%E5%AF%BC%E5%85%A5-Elasticsearch-%E5%8F%AF%E8%A7%86%E5%8C%96%E6%9F%A5%E8%AF%A2%E5%88%86%E6%9E%90%2F</url>
<content type="text"><![CDATA[当应用程序后台 SQL 查询慢的时候我们一般第一时间会查看数据库慢查询记录,但是慢查询记录是原始文本,直接查询搜索分析比较费时费力,虽然业界有针对 MySQL 慢查询分析的命令行工具(比如:pt-query-digest),但是使用起来还是不够方便,而且分析结果也是针对整个实例的大概统计,不能及时定位到某个应用(库.表)的慢查询。出于这个目的我们可以将 MySQL 原始慢查询日志结构化导入 Elasticsearch,然后通过 Kibana 可视化查询分析,由于日志结构化解析出来了,所以可以快速查询分析。本文主要介绍如何运用业界主流的开源工具链实现这一需求,整体架构如下: 工具链集合 Filebeat:日志收集端,使用 Filebeat 的 MySQL 模块结构化解析慢查询日志并写入到 Elasticsearch。 Elasticsearch:存储 Filebeat 发送过来的日志消息; Kibana:可视化查询分析 Elasticsearch 存储的日志数据; docker-compose:容器化快速启动 Elasticsearch + Kibana 组件; 具体实现docker-compose 启动 Elasticsearch + Kibana 组件,然后使用 Filebeat 自带的 MySQL 模块结构化慢查询日志并传输到 Elasticsearch。 docker-compose 启动 Elasticsearch + Kibana 组件 Elasticsearch 7.4.0 Kibana 7.4.0 docker-compose.yml12345678910111213141516171819202122232425262728293031version: '3'networks: esnet: driver: bridgeservices: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0 environment: - node.name=es01 - discovery.seed_hosts=es01 - cluster.initial_master_nodes=es01 - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms4096m -Xmx4096m" ulimits: memlock: soft: -1 hard: -1 ports: - "9500:9200" networks: - esnet kibana: image: docker.elastic.co/kibana/kibana:7.4.0 environment: - "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" ports: - "5601:5601" networks: - esnet 启动:1docker-compose up -d 安装配置 Filebeat安装12curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.4.0-x86_64.rpmsudo rpm -vi filebeat-7.4.0-x86_64.rpm 配置 Filebeat 配置 Filebeat 输出到 Elasticsearch:vim /etc/filebeat/filebeat.yml 填入如下内容 123output.elasticsearch: # Array of hosts to connect to. hosts: ["localhost:9500"] 激活 filebeat mysql 模块 1filebeat modules enable mysql 关于 Filebeat mysql 模块介绍见这里:https://www.elastic.co/guide/en/beats/filebeat/master/filebeat-module-mysql.html 设置初始化环境 1filebeat setup -e 慢查询日志抓取目录路径这里设置为:/data1/web/slow-query/original/*.log 路径vim /etc/filebeat/modules.d/mysql.yml 123456- module: mysql error: enabled: false slowlog: enabled: true var.paths: ["/data1/web/slow-query/original/*.log"] Filebeat MySQL 慢查询日志解析配置由于我们 MySQL 使用云提供商的 SQL 服务,但是云提供商的 MySQL 实例慢查询日志格式和自搭的有略微的区别,不太是很标准,所以需要自定义日志格式解析表达式,正则表达式符合 logstash Grok 语法,可以在这里调试正则表达式:http://grokdebug.herokuapp.com/。关于 Grok 正则语法的学习资料可以看看这两篇文章,这里不做介绍:ELK实战 - Grok简易入门,Logstash 官网:Grok 过滤器插件。 我们目前慢查询日志格式样例: 123456# Time: 2019-10-23T00:00:22.964315Z# User@Host: db[db] @ [cloudsqlproxy~192.168.1.11] thread_id: 87983 server_id: 2945557302# Query_time: 1.649439 Lock_time: 0.000116 Rows_sent: 1 Rows_examined: 1634use report;SET timestamp=1571788822;select * from table where team_id = 71206683786887168 and definition_id = 142 and definition_md5 = 'acd2e0a2fecb08ceb13c6ae' and UNIX_TIMESTAMP(create_time) * 1000 <= 1568851199999 order by create_time desc limit 1; 对应的 Grok 正则:说明:在 Grok 中转义一个字符使用一个 \ 而不是两个 \,比如要转义 [ 需要写成 \[。 1^# Time: %{TIMESTAMP_ISO8601:mysql.slowlog.time}\n# User@Host: %{USER:mysql.slowlog.user}\[%{USER:mysql.slowlog.current_user}\] @ %{HOSTNAME:mysql.slowlog.host}? \[([a-zA-Z~]*)?%{IP:mysql.slowlog.ip}?\]%{SPACE}(Id:%{SPACE}%{NUMBER:mysql.slowlog.id:int})?(%{SPACE}thread_id:%{SPACE}%{NUMBER:mysql.slowlog.thread_id:int})?(%{SPACE}server_id:%{SPACE}%{NUMBER:mysql.slowlog.server_id})?\n# Query_time: %{NUMBER:mysql.slowlog.query_time.sec:float}%{SPACE}Lock_time: %{NUMBER:mysql.slowlog.lock_time.sec:float}%{SPACE}Rows_sent: %{NUMBER:mysql.slowlog.rows_sent:int}%{SPACE}Rows_examined: %{NUMBER:mysql.slowlog.rows_examined:float}\n((use|USE) .*;\n)?SET timestamp=%{NUMBER:mysql.slowlog.timestamp};\n%{GREEDYDATA:mysql.slowlog.query} 将调试好的 Grok 正则写入下面文件中: 说明: 写入下面 pipeline.json 文件中的正则特殊字符需要两个 \ 转义,比如要转义 [ 需要写成 \\[。 /usr/share/filebeat/module/mysql/slowlog/ingest/pipeline.json 1234567891011121314151617181920212223242526272829303132{ "description": "Pipeline for parsing MySQL slow logs.", "processors": [{ "grok": { "field": "message", "patterns":[ "^# Time: %{TIMESTAMP_ISO8601:mysql.slowlog.time}\n# User@Host: %{USER:mysql.slowlog.user}\\[%{USER:mysql.slowlog.current_user}\\] @ %{HOSTNAME:mysql.slowlog.host}? \\[([a-zA-Z~]*)?%{IP:mysql.slowlog.ip}?\\]%{SPACE}(Id:%{SPACE}%{NUMBER:mysql.slowlog.id:int})?(%{SPACE}thread_id:%{SPACE}%{NUMBER:mysql.slowlog.thread_id:int})?(%{SPACE}server_id:%{SPACE}%{NUMBER:mysql.slowlog.server_id})?\n# Query_time: %{NUMBER:mysql.slowlog.query_time.sec:float}%{SPACE}Lock_time: %{NUMBER:mysql.slowlog.lock_time.sec:float}%{SPACE}Rows_sent: %{NUMBER:mysql.slowlog.rows_sent:int}%{SPACE}Rows_examined: %{NUMBER:mysql.slowlog.rows_examined:float}\n((use|USE) .*;\n)?SET timestamp=%{NUMBER:mysql.slowlog.timestamp};\n%{GREEDYDATA:mysql.slowlog.query}" ], "pattern_definitions" : { "GREEDYMULTILINE" : "(.|\n)*" }, "ignore_missing": false } }, { "remove":{ "field": "message" } }, { "date": { "field": "mysql.slowlog.time", "target_field": "@timestamp", "formats": ["ISO8601"], "ignore_failure": true } }], "on_failure" : [{ "set" : { "field" : "error.message", "value" : "{{ _ingest.on_failure_message }}" } }]} /usr/share/filebeat/module/mysql/slowlog/config/slowlog.yml 文件调整如下: 1234567891011type: logpaths:{{ range $i, $path := .paths }} - {{$path}}{{ end }}exclude_files: ['.gz$']multiline: pattern: '^# Time:' negate: true match: afterexclude_lines: ['^[\/\w\.]+, Version: .* started with:.*'] # Exclude the header 启动 filebeat 开始日志收集: 1systemctl start filebeat Kibana 可视化查询在 Kibana 界面创建 filebeat 索引即可实时查看慢查询日志,举例:查看最近 7 天 10s ~ 20s 的慢查询记录: 遇到的问题及解决方法Kibana 首次查看上面建立的索引数据可能会报类似下面的错误,主要原因是单条日志事件过长 Kibana 有限制:1Request to Elasticsearch failed: {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"The length of [error.message] field of [JHzl020BgPENHIlNYRoc] doc of [filebeat-7.4.0-2019.10.16-000001] index has exceeded [1000000] - maximum allowed to be analyzed for highlighting. This maximum can be set by changing the [index.highlight.max_analyzed_offset] index level setting. For large texts, indexing with offsets or term vectors is recommended!"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"filebeat-7.4.0-2019.10.16-000001","node":"H_Zq22spSJKz0RWR_alDsA","reason":{"type":"illegal_argument_exception","reason":"The length of [error.message] field of [JHzl020BgPENHIlNYRoc] doc of [filebeat-7.4.0-2019.10.16-000001] index has exceeded [1000000] - maximum allowed to be analyzed for highlighting. This maximum can be set by changing the [index.highlight.max_analyzed_offset] index level setting. For large texts, indexing with offsets or term vectors is recommended!"}}],"caused_by":{"type":"illegal_argument_exception","reason":"The length of [error.message] field of [JHzl020BgPENHIlNYRoc] doc of [filebeat-7.4.0-2019.10.16-000001] index has exceeded [1000000] - maximum allowed to be analyzed for highlighting. This maximum can be set by changing the [index.highlight.max_analyzed_offset] index level setting. For large texts, indexing with offsets or term vectors is recommended!","caused_by":{"type":"illegal_argument_exception","reason":"The length of [error.message] field of [JHzl020BgPENHIlNYRoc] doc of [filebeat-7.4.0-2019.10.16-000001] index has exceeded [1000000] - maximum allowed to be analyzed for highlighting. This maximum can be set by changing the [index.highlight.max_analyzed_offset] index level setting. For large texts, indexing with offsets or term vectors is recommended!"}}},"status":400} 解决方法:打开 Kibana –> Management –> Advanced Settings –> Highlight results 开关关闭。 关于 Filebeat 的一些使用心得1. yum 方式安装的 Filebeat 其日志如何查看? journalctl -u filebeat -f tail -f /var/log/filebeat/filebeat 2. Filebeat 状态清理:清理 registry 有时候我们需要清理下 Filebeat 状态,从头开始读取日志,yum 方式安装的 filebeat 直接清理 /var/lib/filebeat/registry 文件夹即可。 参考文档https://xiezefan.me/2017/04/09/elk_in_action_grok_start/ | Grok 简易入门https://github.com/elastic/logstash/blob/v1.4.2/patterns/grok-patterns | Grok 本身支持的模式列表https://churrops.io/2018/06/18/elastic-modulo-mysql-do-filebeat-para-capturar-slowlogs-slow-queries/https://discuss.elastic.co/t/filebeat-mysql-module-slowlog-error-message-provided-grok-expressions-do-not-match-field-value/135945]]></content>
<categories>
<category>MySQL</category>
</categories>
<tags>
<tag>MySQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 Kubernetes 的 7 大 DevOps 关键实践]]></title>
<url>%2F2019%2F09%2F07%2F%E5%9F%BA%E4%BA%8E-Kubernetes-%E7%9A%84-7-%E5%A4%A7-DevOps-%E5%85%B3%E9%94%AE%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[本文主要介绍 DevOps 的 7 大关键实践在 Kubernetes 平台下如何落地,结合我们目前基于 Kubernetes 平台的 DevOps 实践谈谈是如何贯彻相关理念的,这里不会对其具体实现细节进行深入讲解,只做一个大致的概括的描述,分享下已有的实践,具体实践细节有时间了会单独整理一篇文章分享。 DevOps 简介DevOps 集文化理念、实践和工具于一身,可以提高企业高速交付应用程序和服务的能力,与使用传统软件开发和基础设施管理流程相比,能够帮助企业更快地发展和改进产品。 GoalsThe goals of DevOps span the entire delivery pipeline. They include: Improved deployment frequency; Faster time to market; Lower failure rate of new releases; Shortened lead time between fixes; Faster mean time to recovery (in the event of a new release crashing or otherwise disabling the current system). DevOps 的 7 大关键实践在Kubernetes 平台下的落地下面为基于 Kubernetes 的 CI/CD 流水线整体实践,使用 Kubernetes + Docker + Jenkins + Helm 等云原生工具链来构建 CI/CD 管道,基于 Google Cloud Platform 云平台构建: 1. Configuration Management关于配置文件的管理我们统一使用 Kubernetes 提供的 configmap 和 secret 资源对象来实现,对于一般的应用配置使用 configmap 管理,对于敏感数据使用 secret 存储。 2. Release Management关于发布管理,使用 Helm 工具统一以 Chart 的方式打包并发布应用,维护每个微服务的历史版本,可以按需回滚。应用的镜像版本统一存储到一个 Docker 私服。 3. Continuous Integration关于持续集成整合了一套完全开源的工具链:Gitlab + Maven + Jenkins + TestNg + SonarQuebe + Allure。Jenkins 运行在 Kubernetes 集群中,所有的工具链都以容器的方式运行,按需定义并使用,无需单独安装维护。 4. Continuous Deployment关于持续部署方面做的不是很好,没有形成完整的 Pipeline,整个自动化 Pipeline 进行到预发布环境,对于生产环境发布,研发人员自助式点击另一个单独的部署 Pipeline Job 进行部署,选择镜像版本进行发布。 5. Infrastructure as Code基础架构即代码的指的是所有软件基础设施的管理维护都以代码的方式管理起来,对于基础设施资源的创建、销毁、变更,不再是人工通过界面化点触式管理,所有这些更改都基于代码的提交变更和一套自动化框架。比如知名的 Terraform 就是一个业界比较流行的 IaC 框架,专门用于代码化管理云基础设施。在 Infrastructure as Code 方面,其实 Kubernetes 完全契合这一点,所有的基础架构及应用服务都可以被抽象成 API 资源对象,我们只需要按需定义 yaml 资源文件即可快速生成相应的资源。比如我们需要一个 Redis 中间件,那么只需要编写一个声明式 yaml 文件,定义需要的配置、版本等信息即可以容器的方式在 k8s 集群中运行起来。最终只需要在 git 代码仓库维护这些 yaml 文件即可。 6. Test Automation关于测试自动化,目前主要有三种类型的测试:单元测试、后端接口测试、UI 自动化测试。所有的这些测试都集成在 Jenkins pipeline 中。后端接口测试和 UI 测试除了在自动化 Pipeline 中运行,还会每天定时跑测试,最终的测试报告统一收集到 Reportportal 报表平台。 7. Application Performance Monitoring关于应用监控采用云原生架构下的最佳实践: Prometheus 监控栈 关于日志采集基于 EFK 技术栈 相关文档https://www.wikiwand.com/en/DevOps#/Definitions_and_history]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>DevOps</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kubernetes 基于 EFK 技术栈的日志收集实践]]></title>
<url>%2F2019%2F09%2F05%2FKubernetes-%E5%9F%BA%E4%BA%8E-EFK-%E6%8A%80%E6%9C%AF%E6%A0%88%E7%9A%84%E6%97%A5%E5%BF%97%E6%94%B6%E9%9B%86%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[之前写过一篇文章介绍了容器环境下日志管理的原理机制:从 Docker 到 Kubernetes 日志管理机制详解,文章内容偏理论,本文在该理论的支撑下具体实践 Kubernetes 下基于 EFK 技术栈的日志收集,本文偏实践,要想全面了解 Kubernetes 下日志收集管理机制,最好还是两篇文章顺序阅读。 本文不仅限于介绍怎么在 Kubernetes 集群部署 EFK 组件,还涉及到其他相关话题:EFK 组件简介、Kubernetes 环境下为什么选用 EFK 优于 ELK 组件、Fluentd 简介、Fluentd 工作机制等等。本文结构如下: EFK 组件简介 Kubernetes 环境下为什么选用 EFK 优于 ELK 组件 Fluentd 简介 Fluentd 工作机制 Fluentd 配置的核心指令 Kubernetes EFK 日志收集架构 Kubernetes EFK 日志收集架构实践 EFK 组件简介顾名思义,EFK 是 Elasticsearch、Fluentd、Kibana 三个开源软件组件的首字母缩写,EFK 是业界主流的容器时代日志收集处理的最佳解决方案。Fluentd 是一个日志收集器,负责从各个服务器节点抓取日志。Elasticsearch 是一个搜索存储引擎,可以存储 Fluentd 收集的日志,并提供查询服务。Kibana 是 Elasticsearch 的一个界面化组件,提供 UI 方式查询 Elasticsearch 存储的数据。三个组件组合起来工作就是 EFK,组件之间数据流是这样:Log 源(比如 Log 文件、k8s Pod 日志、Docker 日志驱动等等)—> Fluentd 收集 —> Elasticsearch 存储 —> Kibana UI 查看。 Kubernetes 环境下为什么选用 EFK 优于 ELK 组件ELK 是 Elasticsearch、Logstash、Kibana 三个开源组件的缩写,可以看出和 EFK 的区别在于第二个字母,一个是 Logstash,一个是 Fluentd。 我们很早之前使用 ELK 技术栈收集云主机上部署的应用日志,每台虚拟主机部署一个 Logstash 实例收集应用程序目录日志,然后统一传输到一个 Logshtash 中央处理节点,处理后的数据存储到 Elasticsearch。这种方案虽然可以实现日志的统一收集处理,但是 Logstash 比较吃系统资源,基本上资源占用堪比一个 Java 微服务,一个简单的抓取节点就大概需要 1 GB 的内存,我们知道云主机的机器资源是非常昂贵的,所以这种方案对资源浪费比较大,不是很推荐。之前我们基于该 ELK 方案的日志收集大致架构图如下:收集大概 13 个微服务日志,其中 Logstash Server 端平均内存使用能达到 8GB +,显然 Logstash 比较重量级,对系统资源的消耗可见一斑。从上面的实践案例可以知道 ELK 技术栈的缺陷在于 Log 收集器 Logstash 比较重量级,对系统资源消耗比较大。那么有没有更加轻量级的替代方案呢?答案是有的,比如 Elastic 的 beats 家族的 Filebeat 就可以取代 Logstash 作为日志抓取器,还有 Fluentd 也可以作为 Logstash 替代品,Fluentd 也是本文要讲的重头戏。为什么我们选用 Fluentd 而不用 Filebeat 呢,个人认为 Fluentd 兼具日志抓取、收集、处理、轻量级的特性,拥有丰富的插件生态,是日志解决方案的神器。而 Filebeat 功能重点在于日志的抓取、轻量级,但是对于日志的处理功能不是很强大。 Fluentd 的轻量级体现在它本身核心代码是用 C 语言编写,更接近操作系统底层语言,所以一般性能是比较高的,据官方介绍 30~40 MB 内存就能处理 13000 日志事件/秒/核,由此可见其性能是多么强大,所以对比 Logstash 而言,用 30 MB 内存就能解决的问题当然选择 Fluentd 了。 Fluentd 简介Fluentd 是一个开源的日志收集器,它统一了日志的收集和处理逻辑,多种不同来源的日志都可以通过 Fluentd 这个单一的工具统一收集,然后统一存储到单个或多个存储后端。 像很早之前没有类似 Fluentd 之类的工具的话,为了收集日志可能会有各种五花八门的方法(工具),比如:Shell 脚本分析日志文件、服务器 syslog 收集、rsync 定时同步,scp 拷贝日志文件到统一的存储服务器等等,这么做显然带来的问题是我们要维护各种日志收集端工具,由于各种工具使用上不统一,对整个日志系统的维护人员来说不亚于一场灾难。我们可以用一张图来描述这种复杂、错乱的场景:针对上述存在的问题,Fluentd 插件式架构很好地解决了该问题,所有数据收集端和存储端都可以通过 Fluentd 使用不同的插件统一起来,这么做带来的最大好处就是大大简化了日志收集的架构,整个架构都以 Fuentd 为核心,不再需要维护人员掌握、维护各种乱七八糟的小工具。上面复杂、错乱的数据收集架构就可以简化为以 Fluentd 为核心的简单架构了:关于 Fluentd 更多的介绍见官方文档:https://docs.fluentd.org/ 。 Fluentd 工作机制Fluentd 和其他日志收集器的工作原理类似,对数据的处理流程也分为三大阶段:收集—>处理、过滤—>输出。这三个处理阶段都有不同的插件支持,可以灵活组合。在 Fluentd 中事件是 整个数据流处理的基本单位,fluentd 的 input 插件每接收到一条日志都会将其封装成一个 fluentd 事件,然后发送给 fluentd 引擎处理,fluentd 引擎根据事件中包含的 tab 匹配不同的 filter 插件进行事件的处理,处理完后发送到 output 插件,output 插件根据事件中的 tag 匹配事件,将匹配到的事件发送到对应的后端。 Fluentd 事件由三部分组成: tag: . 分隔的字符串,供 fluentd 路由引擎路由使用; time: 由输入插件指定的事件发生的时间戳,必须符合 Unix 时间格式; record: 日志内容; Fluentd 配置的核心指令上面简单介绍了下 Fluentd 对日志数据的处理流程,其是在这个流程中 Fluentd 的行为是通过其配置文件定义的,配置文件由一条条指令组成。下面是 Fluentd 配置文件的 6 大指令,是 Fluentd 配置的核心指令: source directives determine the input sources. match directives determine the output destinations. filter directives determine the event processing pipelines. system directives set system wide configuration. label directives group the output and filter for internalrouting @include directives include other files. 关于每条指令在具体配置文件中如何使用这里不再赘述,详情见:https://docs.fluentd.org/configuration/config-file Kubernetes EFK 日志收集架构之前介绍过 Kubernetes 日志管理机制:再 k8s 每个节点上,kubelet 会为每个容器的日志创建一个软链接,软连接存储路径为:/var/log/containers/,软连接会链接到 /var/log/pods/ 目录下相应 pod 目录的容器日志,被链接的日志文件也是软链接,最终链接到 Docker 容器引擎的日志存储目录:/var/lib/docker/container 下相应容器的日志。所以 /var/log 和 /var/lib/docker/container 目录是整个节点所有容器日志的统一存储地方,这就为 Fluentd 日志收集提供了很大的方便。 针对上述 k8s 日志管理机制,Kubernetes 官方给出了推荐的日志收集方案:以 DaemonsSet 的方式在 k8s 集群每个节点部署一个节点级的 Fluentd 日志收集器,Fluentd Pod 在启动时挂载了宿主机的 /var/log 和 /var/lib/docker/container 目录,因此可以直接对宿主机目录中的容器日志读取并传输到存储后端:Elasticsearch。然后 Kibana 和 Elasticsearch 对接即可实时查看收集到的日志数据。 Kubernetes EFK 日志收集架构实践上面介绍了 Kubernetes EFK 日志收集架构,本节基于该架构进行实践。我们使用 Kubernetes 官方提供的 EFK 组件 mainfest 文件进行部署,Github 仓库目录为:https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-elasticsearch 该目录下存储了 EFK 三大组件的部署 yaml 资源文件,有两点需要说明下: Elasticsearch 数据持久化:默认 EmptyDir 的方式,这种方式在 Pod 重新调度后数据会丢失,不过为了实验,所以在这里就不进行修改了,使用默认的即可; Kibana deployment 文件需要修改下:去掉 SERVER_BASEPATH 环境变量,要不然部署后访问会 404; Kubernetes EFK 部署进入 fluentd-elasticsearch 目录,执行:1kubectl create -f . 查看 Pod 状态,确保每个组件启动成功,一般都会启动成功的,这里基本没什么坑。 Kibana 对外访问接下来将 Kibana 服务从 Kubernetes 集群对外暴露出来,实现对外访问,我们使用 Kong 网关对外暴露服务。具体 Kong 网关在 Kubernetes 的使用在这里不再赘述,如想了解见这里:Kong 微服务网关在 Kubernetes 的实践。 Konga 配置 Kibana 服务对外访问: 创建 kibana-logging Kong Service 创建 Kong 路由到 kibana-logging Kong Service kibnana.kong.com 绑定 host 到集群节点 IP 即可访问查看收集的日志: 相关文档https://kubernetes.io/docs/concepts/cluster-administration/logging/ | Kubernetes 日志架构https://www.fluentd.org/architecture | What is fluentd?https://docs.fluentd.org/quickstart/life-of-a-fluentd-event | Life of a Fluentd event]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kubernetes CronJob 的一个应用案例]]></title>
<url>%2F2019%2F08%2F30%2FKubernetes-CronJob-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%BA%94%E7%94%A8%E6%A1%88%E4%BE%8B%2F</url>
<content type="text"><![CDATA[最近 Kubernetes 集群中出现过几次 Redis 故障,具体表现是每次集群重启(云资源按需启动), Redis Pod 都要老半天才能启动起来,后来逐渐排查定位才发现原来是由于 Redis 开启了 aof 持久化机制。 我们知道在 AOF 持久化机制下,Resdis 的每一条写命令都会被同步、并且追加的方式持久化的磁盘文件,当 Redis 由于意外故障时,下次重启就会原封不动地执行 AOF 文件中的命令来恢复 Redis。那么显然这种方式会带来一个问题是随着时间的推移 aof 文件体积会越来越大,每次 Redis 重启都要执行一遍 aof 持久化的命令,速度也会越来越慢,从而导致 Redis 启动变慢。 显然解决方法是怎么把 AOF 文件变小,丢弃没用的记录。Redis 有一条 BGREWRITE 命令就是解决这问题的,这条命令的工作原理是将当前 Redis 中的数据都导出成 Redis 写语句,然后生成新的 aof 文件,替换掉旧的。显然新的 aof 文件体积会原因小于长时间运行的旧的 aof 文件,因为新的 aof 只是当前 Redis 的数据恢复语句,只记录当前的状态。 由于我们 Redis 是运行在 Kubernetes 集群中,所以借助于 Kubernetes 的 CronJob 机制定期执行 Redis BGREWRITE 命令来重写 aof 文件,从而缩小文件体积。 Kubernetes CronJob 简介Kubernetes CronJob 资源用来定义一些需要定时执行的任务,类似于 Linux/Unix 的 Crontab。CronJob 资源创建后会按照写的定时任务规则启动 Pod 执行定义的任务。关于 CronJob 更详细的信息见这里:https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs 基于 k8s CronJob 定期 AOF 重写定义 CronJob 资源文件,每两小时进行一次 Redis AOF 重写,资源文件如下:redis-cron.yaml123456789101112131415161718192021222324252627apiVersion: batch/v1beta1 #for API server versions >= 1.8.0 use batch/v1beta1kind: CronJobmetadata: name: redis-bgrewriteaof-cron labels: app: redis-bgrewriteaof-cronspec: schedule: "0 */2 * * *" successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 3 concurrencyPolicy: Forbid startingDeadlineSeconds: 120 jobTemplate: spec: template: spec: containers: - name: redis image: docker.io/bitnami/redis:4.0.12 env: - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-test key: redis-password args: [redis-cli, -h, redis-stage-master, -a, $(REDIS_PASSWORD), BGREWRITEAOF] restartPolicy: OnFailure 关于上面资源里面的一些配置项含义在此不做具体介绍,很多都能看懂,具体每个配置项含义看官方文档,都有详细的解释。这里有一点需要「特别注意」的是 redis容器的 args 字段引用环境变量的方法:比如这里引用了 REDIS_PASSWORD 环境变量,需要写成:$(REDIS_PASSWORD) 这样的方式引用,而不能写成:$REDIS_PASSWORD 或者 ${REDIS_PASSWORD}。 执行 kubectl create 创建定义的 CronJob 资源1kubectl create -f redis-cron.yaml -n test 查看 CronJob 执行情况123$ kubectl get cronjob -n testNAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGEredis-bgrewriteaof-cron 0 */2 * * * False 0 1h 14h 可以看出最近一小时前已经调度过一次,如果要看调度是否成功可以看对应 Pod 的 log,也可以 kubectl get jobs -n test 查看启动的 Job 的状态。 相关文档https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs | Running Automated Tasks with a CronJobhttps://redis.io/commands/bgrewriteaof | Redis之AOF重写及其实现原理]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库升级 DevOps 落地实践]]></title>
<url>%2F2019%2F08%2F29%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8D%87%E7%BA%A7-DevOps-%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[在我们做持续集成/交付的过程中,应用的发布已经通过 DevOps 流水线基本能满足快速迭代的需求,但是很多企业在落地实践 DevOps 的过程中很容易忽略的一点是关于应用数据库版本、升级的管理,每次上线发布数据库的更新依然通过运维或者 DBA 手工更新,在微服务、容器盛行的背景下,服务多,服务发布速度快,显然靠人工该 DB 是跟不上迭代速度的,从而导致 DB 的更新成了整个软件交付周期的瓶颈。这一点我是深有体会,尤其是每次上线时,多个微服务同时上线,同时还需要进行 DB 升级,这个时候研发人员会给我们 SQL 执行,当然研发人员的数量是远远多于开发人员,所以每次上线运维人员经常陷入被”围堵“的尴尬境地。当然还存在其他种种痛点,大概总结下有如下痛点: DB 的升级人工执行跟不上版本发布速度,成为软件交付的瓶颈; 人工执行 DB 升级错误率比自动化执行更容易出错; 各个环境 DB 没有统一的版本管理,经常会出现这个 SQL 有没有在某个环境执行的疑问; 环境之间数据脚本同步经常出现遗漏的情况,由于开发或测试环境操作 DB 的人多,在应用从一个环境升级到另一个环境中经常忘记执行某条 SQL,然后导致各种问题故障。这种问题甚至在应用上线时也会频频出现,然后通知运维或 DBA 执行遗漏 SQL; 那么在 DevOps 落地实践中如何很好地处理好数据库升级这一环呢,从而解决上述存在的种种痛点,不要让数据库升级成为软件交付的瓶颈,使得数据库的升级流程融入自动化流水线。我调研了下业界关于应用 DB 升级的方案,不少文章或者圈内人士推崇专门的数据库管理工具版本化管理,自动化执行,比如 Flyway,Liquibase 等著名的工具,都是专业的数据库版本管理和自动化工具。 在本文中主要介绍如何将 Flyway 和其他 DevOps 工具链整合,实现 DB 升级的自动化和管理的版本化,从而解决之前存在的一系列痛点。本文用到的工具链有:Flyway + Jenkins 2.0(Pipeline 脚本)+ Gitlab + MySQL。需要说明的一点是本文并不是一步步讲解各种工具链如何使用和相关介绍,重点在于工具链的整合实践,以及如何恰当地应用。在文末附有完整的 Pipeline 实现脚本,仅供参考! 数据库升级 DevOps 实践带来了什么收益其实在文章开言已经说清楚了,总结起来就两点: 所有环境数据库版本统一管理; 数据库升级变更自动化; 实践方案概要数据库升级脚本统一按微服务模块以独立 git 仓库的形式管理起来,每次版本迭代,规划好 SQL 模型定义(DDL),将 db 脚本签入独立的 git 仓库,然后使用专门的数据库版本管理工具自动扫描仓库目录的 db 升级脚本,由于 db 升级脚本文件名称符合一定的命名规范,所以工具可以自动按版本号顺序执行脚本,并且已经执行过的脚步文件再次执行会忽略。关于 DB 升级工具的选择,我们选用 Flyway,功能单一、容易上手,以规约优于配置的思想规范 DB 的版本化管理,我们写的 SQL 脚本文件都必须符合 Flyway 的文件名命名规范,这样才能在升级过程中生效。 具体实践借助的工具链:Flyway + Jenkins 2.0(Pipeline 脚本)+ Gitlab + MySQL(Google Cloud SQL) 以微服务应用 git 工程名称在 gitlab 一个单独的组创建 db 代码工程; 在 db 代码工程中创建以数据库命名的目录,存放对应数据库升级的脚本文件,脚本文件名称需要符合 FlywayDB 的命名规范: db 代码工程分支管理:dev 环境对应 dev 分支,test 环境对应 test 分支,stage 环境对应 stage 分支,生产环境对应 master 分支; Jenkins 脚本注册相应代码工程名称和对应 db 名称; 点击 Jenkins 执行数据库升级; 强制规约 gitlab 代码工程名称和 db 工程名称一致,db 工程目录下文件夹以数据库名称命名; db 脚本名称符合 FlywayDB 命名规范; db 脚本文件版本名统一大于 1.0,比如: 可以是 V1.0.1,但不能是 V0.2.3; db 脚本内容为 DDL 语句,不能包含 DML 或者 DCL 语句,这个要严格审核,因为 DML 和 DCL 版本追踪没意义,而且各个环境可能还不兼容,FlywayDB 的本质是数据库 Sechma 版本管理,只关心表结构,表里面的数据不关心。关于数据库 DDL、DML、DCL 相关概念及区别见这里; 已经执行过的 db 脚本不能修改后重复执行,并且执行过的 db 脚本文件需要原封不动保留,不能丢失和修改,否则升级会失败,这个一定要注意。如果对已经执行的 db 脚本不满意,有改动需要变更,则新加 db 脚本文件,可以小版本号比原先增 1,相当于临时 fix,但是我们尽量减少这种情况的发生; 具体 Workflow开发人员 Workflow开发、测试、预发布环境开发人员点击 Jenkins job 执行数据库升级: 将 SQL 脚本按照 FlywayDB 规范提交到对应的 db 仓库,提交 MR 到对应分支; 小组 db 脚本审核人审核没问题后合并 db 代码; 小组成员点击 Jenkins Job,执行数据库升级3.1 选择环境+服务名称+要升级的数据库名称3.2 运行 Job 运维人员 Workflow运维人员只负责线上 SQL 的升级: 开发人员告知运维人员本次上线 db 脚本已提交到代码仓库并 merge 到 master 分支; 运维人员点击 Jenkins Job 执行相应服务的数据库升级:2.1 选择服务名称+要升级的数据库名称2.2 运行 Job,Pipeline 会阻塞在确认节点,做最后的审查2.3 Job 执行完成 Workflow 举例 新建一个 gitlab 工程,专门存放 db 脚本:服务名称假设为 db-migration-demo,db 名称为 demo,仓库里面存放的 SQL 脚本如下: git 提交代码,然后点击 Jenkins Job,执行数据库升级 关于 Pipeline 设计的两个功能点1. 数据库整库备份策略数据库 DDL 变更前整库备份一下是有必要的,但是每次变更都整库备份也不合理,因为可能某天上线,数据库升级比较集中,一天内会触发很多次备份,造成了资源的浪费。解决方案是给备份一个时间窗口(比如 2 小时),每次执行前判断下最近两小时是否有备份,如果没有则触发整库备份,这样就能避免每次执行 Job 都会触发整库备份。具体解决方法:获取当前时间减去两小时的时间,然后和上次整库备份的时间戳比较,如果前者大,说明最近两小时内没备份,然后自动触发整库备份,时间戳比较用 Shell 脚本实现:12345678910# !/bin/basht1=`date -d "$1" +%s`t2=`date -d "$2" +%s`if [ $t1 -ge $t2 ]; then echo "true"else echo "false"fi 2. 每次变更前备份库下的所有表结构,同时记录下 FlywayDB 更改前后状态表结构备份和 FlywayDB 更改前后状态信息都以制品的方式归到 Jenkins,这样可以随时在 Jenkins 界面查看相关信息,比如查看 Flyway 前后执行状态如何,点开制品页即可看到: 附:Pipeline 脚本实现为例减小文章的篇幅,这里只贴下运维人员 Workflow 的 Jenkins pipeline 脚本,研发人员的和这个类似,只是一些小的改动。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171pipeline { parameters { //服务名称 choice(name:'serviceName', choices: [ 'db-migration-demo' ] , description: '服务名称') //数据库 choice(name:'dbName', choices: [ 'demo' ] , description: '数据库名称') } agent { kubernetes { label "sql-${UUID.randomUUID().toString()}" defaultContainer 'jnlp' yaml """apiVersion: v1kind: Podmetadata: labels: some-label: db-imgrationspec: containers: - name: flyway image: boxfuse/flyway command: - cat tty: true - name: mysql-client image: arey/mysql-client command: - cat tty: true - name: gcloud image: google/cloud-sdk:alpine command: - cat tty: true""" } } post { failure { echo "Database migration failed!" } success { echo "Database migration success!" } } options { gitLabConnection('gitlab-connection') //保持构建的最大个数 buildDiscarder(logRotator(numToKeepStr: '20')) } stages { stage('初始化') { steps { script { currentBuild.description = "production环境${params.serviceName}服务${params.dbName}库升级..." } container('gcloud') { withCredentials([file(credentialsId: 'cloudInfrastructureAccess', variable: 'cloudSQLCredentials')]) { sh "gcloud auth activate-service-account ${env.cloudInfrastructureAccessSA} --key-file=${cloudSQLCredentials} --project=${env.gcpProject}" } } // 判断是否要进行数据库备份,如果两小时内没有备份则自动触发全量备份 script { isBackup = 'false' // 默认 jenkins 跑在 busybox 容器,获取时间和普通 Linux 发行版有点区别 date2HoursAgo = sh(returnStdout: true, script: "date -u +'%Y-%m-%d %H' -d@\"\$((`date +%s`-7200))\"").trim() container('gcloud') { latestDBBackupTime = sh(returnStdout: true, script: "gcloud sql backups list --instance=${env.prodMySqlInstance} --limit=1 | grep -v 'WINDOW_START_TIME' | awk '{print \$2}' | awk -F ':' '{print \$1}'|sed 's/T/ /g'").trim() } withCredentials([file(credentialsId: 'time-compare.sh', variable: 'timeCompare')]) { isBackup = sh(returnStdout: true, script: "sh ${timeCompare} \'$date2HoursAgo\' \'$latestDBBackupTime\'").trim() echo "$date2HoursAgo" echo "$latestDBBackupTime" echo "$isBackup" } } } } // 如果两小时内没有备份则自动触发全量备份 stage('整库智能备份') { when { expression { isBackup == 'true' } } steps { script { container('gcloud') { // 列出最近 10 个备份,便于观察 sh "gcloud sql backups list --instance=${env.prodMysqlInstance} --limit=10" backupTimestamp = sh(returnStdout: true, script: "date -u +'%Y-%m-%d %H%M%S'").trim() backupDescription="Flyway backuped at $backupTimestamp (UTC)" // gcloud 创建 db 备份 sh "gcloud sql backups create --async --instance=${env.prodMysqlInstance} --description=\'$backupDescription\'" sh "gcloud sql backups list --instance=${env.prodMysqlInstance} --limit=10" } } } } stage('表结构备份') { steps { withCredentials([usernamePassword(credentialsId: "sql-secret-production", passwordVariable: 'sqlPass', usernameVariable: 'sqlUser')]) { container('mysql-client') { sh "mysqldump -h ${env.prodMySqlHost} -u$sqlUser -p$sqlPass -d ${params.dbName} --single-transaction > ${params.serviceName}-${params.dbName}-`TZ=UTC-8 date +%Y%m%d-%H%M%S`-dump.sql" } } // 表结构备份同步到 gcs 存储桶 container('gcloud') { sh "gsutil cp *-dump.sql ${env.gcsBackupBucket}/db/production/${params.serviceName}/" } // jenkins 归档数据库备份,可在 BlueOcean 页面制品页查看 archiveArtifacts "*-dump.sql" } } stage('拉取 db 脚本') { steps { script { checkout([$class: 'GitSCM', branches: [[name: "master"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'jenkins-deploy', url: "${env.dbMigrationGitRepoGroup}/${params.serviceName}"]]]) } } } stage('flyway migrate') { steps{ script { host = "${env.prodMySqlHost}" timestamp = sh(returnStdout: true, script: "TZ=UTC-8 date +%Y%m%d-%H%M%S").trim() flywayStateFile = "flyway-state-production-${params.serviceName}-${params.dbName}_${timestamp}.txt" container('flyway') { withCredentials([usernamePassword(credentialsId: "sql-secret-production", passwordVariable: 'sqlPass', usernameVariable: 'sqlUser')]){ sh "flyway -url=jdbc:mysql://${host}/${params.dbName}?useSSL=false -user=${sqlUser} -password=${sqlPass} -locations=filesystem:${params.dbName} -baselineOnMigrate=true repair" sh "echo \"[ flyway 升级前 db 状态 ]\" > $flywayStateFile" sh "flyway -url=jdbc:mysql://${host}/${params.dbName}?useSSL=false -user=${sqlUser} -password=${sqlPass} -locations=filesystem:${params.dbName} -baselineOnMigrate=true info \ | tee -a $flywayStateFile" try { timeout(time: 8, unit: 'HOURS') { env.isMigrateDB = input message: '确认升级 DB?', parameters: [choice(name: "isMigrateDB", choices: 'Yes\nNo', description: "您当前选择要升级的是${params.serviceName}服务${params.dbName}库,确认升级?")] } } catch (err) { sh "echo 'Exception!' && exit 1" } if (env.isMigrateDB == 'No') { sh "echo '已取消升级!' && exit 1" } sh "flyway -url=jdbc:mysql://${host}/${params.dbName}?useSSL=false -user=${sqlUser} -password=${sqlPass} -locations=filesystem:${params.dbName} -baselineOnMigrate=true migrate" sh "echo \"\n\n[ flyway 升级后 db 状态 ]\" >> $flywayStateFile" sh "flyway -url=jdbc:mysql://${host}/${params.dbName}?useSSL=false -user=${sqlUser} -password=${sqlPass} -locations=filesystem:${params.dbName} -baselineOnMigrate=true info \ | tee -a $flywayStateFile" archiveArtifacts "$flywayStateFile" } } } } } }}]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>DevOps</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从 Docker 到 Kubernetes 日志管理机制详解]]></title>
<url>%2F2019%2F08%2F26%2F%E4%BB%8E-Docker-%E5%88%B0-Kubernetes-%E6%97%A5%E5%BF%97%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[在容器化时代,容器应用的日志管理和传统应用存在很大的区别,为了顺应容器化应用,Docker 和 Kubernetes 提供了一套完美的日志解决方案。本文从 Docker 到 Kubernetes 逐步介绍在容器化时代日志的管理机制,以及在 Kubernetes 平台下有哪些最佳的日志收集方案。涉及到的话题有 Docker 日志管理机制、Kubernetes 日志管理机制、Kubernetes 集群日志收集方案。本文结构如下: Docker 日志管理机制 Docker 的日志种类 基于日志驱动(loging driver)的日志管理机制 Docker 日志驱动(loging driver)配置 Docker 默认的日志驱动 json-file Kubernetes 日志管理机制 应用 Pod 日志 Kuberntes 集群组件日志 Kubernetes 集群日志收集方案 节点级日志代理方案 sidecar 容器方案 应用程序直接将日志传输到日志平台 Docker 日志管理机制Docker 的日志种类在 Docker 中日志分为两大类: Docker 引擎日志; 容器日志; Docker 引擎日志Docker 引擎日志就是 docker 服务的日志,即 dockerd 守护进程的日志,在支持 Systemd 的系统中可以通过 journal -u docker 查看日志。 容器日志容器日志指的是每个容器打到 stdout 和 stderr 的日志,而不是容器内部的日志文件。docker 管理所有容器打到 stdout 和 stderr 的日志,其他来源的日志不归 docker 管理。我们通过 docker logs 命令查看容器日志都是读取容器打到 stdout 和 stderr 的日志。 基于日志驱动(loging driver)的日志管理机制Docker 提供了一套通用、灵活的日志管理机制,Docker 将所有容器打到 stdout 和 stderr 的日志都统一通过日志驱动重定向到某个地方。 Docker 支持的日志驱动有很多,比如 local、json-file、syslog、journald 等等,类似插件一样,不同的日志驱动可以将日志重定向到不同的地方,这体现了 Docker 日志管理的灵活性,以热插拔的方式实现日志不同目的地的输出。 Dokcer 默认的日志日志驱动是 json-file,该驱动将将来自容器的 stdout 和 stderr 日志都统一以 json 的形式存储到本地磁盘。日志存储路径格式为:/var/lib/docker/containers/<容器 id>/<容器 id>-json.log。所以可以看出在 json-file 日志驱动下,Docker 将所有容器日志都统一重定向到了 /var/lib/docker/containers/ 目录下,这为日志收集提供了很大的便利。 注意:只有日志驱动为:local、json-file 或者 journald 时,docker logs 命令才能查看到容器打到 stdout/stderr 的日志。 下面为官方支持的日志驱动列表: 驱动 描述 none 运行的容器没有日志,docker logs 也不返回任何输出。 local 日志以自定义格式存储,旨在实现最小开销。 json-file 日志格式为JSON。Docker的默认日志记录驱动程序。 syslog 将日志消息写入syslog。该 syslog 守护程序必须在主机上运行。 journald 将日志消息写入journald。该journald守护程序必须在主机上运行。 gelf 将日志消息写入Graylog扩展日志格式(GELF)端点,例如Graylog或Logstash。 fluentd 将日志消息写入fluentd(转发输入)。该fluentd守护程序必须在主机上运行。 awslogs 将日志消息写入Amazon CloudWatch Logs。 splunk 使 用HTTP 事件收集器将日志消息写入 splunk。 etwlogs 将日志消息写为 Windows 事件跟踪(ETW)事件。仅适用于Windows平台。 gcplogs 将日志消息写入 Google Cloud Platform(GCP)Logging。 logentries 将日志消息写入 Rapid7 Logentries。 Docker 日志驱动(loging driver)配置上面我们已经知道 Docker 支持多种日志驱动类型,我们可以修改默认的日志驱动配置。日志驱动可以全局配置,也可以给特定容器配置。 查看 Docker 当前的日志驱动配置 1docker info |grep "Logging Driver" 查看单个容器的设置的日志驱动 1docker inspect -f '{{.HostConfig.LogConfig.Type}}' 容器id Docker 日志驱动全局配置全局配置意味所有容器都生效,编辑 /etc/docker/daemon.json 文件(如果文件不存在新建一个),添加日志驱动配置。示例:配置 Docker 引擎日志驱动为 syslog 123{ "log-driver": "syslog"} 给特定容器配置日志驱动在启动容器时指定日志驱动 --log-driver 参数。示例:启动 nginx 容器,日志驱动指定为 journald 1docker run --name nginx -d --log-driver journald nginx Docker 默认的日志驱动 json-filejson-file 日志驱动记录所有容器的 STOUT/STDERR 的输出 ,用 JSON 的格式写到文件中,每一条 json 日志中默认包含 log, stream, time 三个字段,示例日志如下:文件路径为:/var/lib/docker/containers/40f1851f5eb9e684f0b0db216ea19542529e0a2a2e7d4d8e1d69f3591a573c39/40f1851f5eb9e684f0b0db216ea19542529e0a2a2e7d4d8e1d69f3591a573c39-json.log1{"log":"14:C 25 Jul 2019 12:27:04.072 * DB saved on disk\n","stream":"stdout","time":"2019-07-25T12:27:04.072712524Z"} 那么打到磁盘的 json 文件该如何配置轮转,防止撑满磁盘呢?每种 Docker 日志驱动都有相应的配置项日志轮转,比如根据单个文件大小和日志文件数量配置轮转。json-file 日志驱动支持的配置选项如下: 选项 描述 示例值 max-size 切割之前日志的最大大小。可取值单位为(k,m,g), 默认为-1(表示无限制)。 --log-opt max-size=10m max-file 可以存在的最大日志文件数。如果切割日志会创建超过阈值的文件数,则会删除最旧的文件。仅在max-size设置时有效。正整数。默认为1。 --log-opt max-file=3 labels 适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的标签列表。 --log-opt labels=production_status,geo env 适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的环境变量列表。 --log-opt env=os,customer compress 切割的日志是否进行压缩。默认是disabled。 --log-opt compress=true Kubernetes 日志管理机制在 Kubernetes 中日志也主要有两大类: 应用 Pod 日志; Kuberntes 集群组件日志; 应用 Pod 日志 Kubernetes Pod 的日志管理是基于 Docker 引擎的,Kubernetes 并不管理日志的轮转策略,日志的存储都是基于 Docker 的日志管理策略。k8s 集群调度的基本单位就是 Pod,而 Pod 是一组容器,所以 k8s 日志管理基于 Docker 引擎这一说法也就不难理解了,最终日志还是要落到一个个容器上面。 假设 Docker 日志驱动为 json-file,那么在 k8s 每个节点上,kubelet 会为每个容器的日志创建一个软链接,软连接存储路径为:/var/log/containers/,软连接会链接到 /var/log/pods/ 目录下相应 pod 目录的容器日志,被链接的日志文件也是软链接,最终链接到 Docker 容器引擎的日志存储目录:/var/lib/docker/container 下相应容器的日志。另外这些软链接文件名称含有 k8s 相关信息,比如:Pod id,名字空间,容器 ID 等信息,这就为日志收集提供了很大的便利。 举例:我们跟踪一个容器日志文件,证明上述的说明,跟踪一个 kong Pod 日志,Pod 副本数为 1 /var/log/containers/kong-kong-d889cf995-2ntwz_kong_kong-432e47df36d0992a3a8d20ef6912112615ffeb30e6a95c484d15614302f8db03.log------->/var/log/pods/kong_kong-kong-d889cf995-2ntwz_a6377053-9ca3-48f9-9f73-49856908b94a/kong/0.log------->/var/lib/docker/containers/432e47df36d0992a3a8d20ef6912112615ffeb30e6a95c484d15614302f8db03/432e47df36d0992a3a8d20ef6912112615ffeb30e6a95c484d15614302f8db03-json.log Kuberntes 集群组件日志Kuberntes 集群组件日志分为两类: 运行在容器中的 Kubernetes scheduler 和 kube-proxy。 未运行在容器中的 kubelet 和容器 runtime,比如 Docker。 在使用 systemd 机制的服务器上,kubelet 和容器 runtime 写入日志到 journald。如果没有 systemd,他们写入日志到 /var/log 目录的 .log 文件。容器中的系统组件通常将日志写到 /var/log 目录,在 kubeadm 安装的集群中它们以静态 Pod 的形式运行在集群中,因此日志一般在 /var/log/pods 目录下。 Kubernetes 集群日志收集方案 Kubernetes 本身并未提供集群日志收集方案,k8s 官方文档给了三种日志收集的建议方案: 使用运行在每个节点上的节点级的日志代理 在应用程序的 pod 中包含专门记录日志 sidecar 容器 应用程序直接将日志传输到日志平台 节点级日志代理方案从前面的介绍我们已经了解到,k8s 每个节点都将容器日志统一存储到了 /var/log/containers/ 目录下,因此可以在每个节点安装一个日志代理,将该目录下的日志实时传输到日志存储平台。 由于需要每个节点运行一个日志代理,因此日志代理推荐以 DaemonSet 的方式运行在每个节点。官方推荐的日志代理是 fluentd,当然也可以使用其他日志代理,比如 filebeat,logstash 等。 sidecar 容器方案有两种使用 sidecar 容器的方式: sidecar 容器重定向日志流 sidecar 容器作为日志代理 sidecar 容器重定向日志流这种方式基于节点级日志代理方案,sidecar 容器和应用容器在同一个 Pod 运行,这个容器的作用就是读取应用容器的日志文件,然后将读取的日志内容重定向到 stdout 和 stderr,然后通过节点级日志代理统一收集。这种方式不推荐使用,缺点就是日志重复存储了,导致磁盘使用会成倍增加。比如应用容器的日志本身打到文件存储了一份,sidecar 容器重定向又存储了一份(存储到了 /var/lib/docker/containers/ 目录下)。这种方式的应用场景是应用本身不支持将日志打到 stdout 和 stderr,所以才需要 sidecar 容器重定向下。 sidecar 容器作为日志代理这种方式不需要节点级日志代理,和应用容器在一起的 sidecar 容器直接作为日志代理方式运行在 Pod 中,sidecar 容器读取应用容器的日志,然后直接实时传输到日志存储平台。很显然这种方式也存在一个缺点,就是每个应用 Pod 都需要有个 sidecar 容器作为日志代理,而日志代理对系统 CPU、和内存都有一定的消耗,在节点 Pod 数很多的时候这个资源消耗其实是不小的。另外还有个问题就是在这种方式下由于应用容器日志不直接打到 stdout 和 stderr,所以是无法使用 kubectl logs 命令查看 Pod 中容器日志。 应用程序直接将日志传输到日志平台这种方式就是应用程序本身直接将日志打到统一的日志收集平台,比如 Java 应用可以配置日志的 appender,打到不同的地方,很显然这种方式对应用程序有一定的侵入性,而且还要保证日志系统的健壮性,从这个角度看应用和日志系统还有一定的耦合性,所以个人不是很推荐这种方式。 总结:综合对比上述三种日志收集方案优缺点,更推荐使用节点级日志代理方案,这种方式对应用没有侵入性,而且对系统资源没有额外的消耗,也不影响 kubelet 工具查看 Pod 容器日志。 相关文档https://juejin.im/entry/5c03f8bb5188251ba905741d | Docker 日志驱动配置https://www.cnblogs.com/operationhome/p/10907591.html | Docker容器日志管理最佳实践https://www.cnblogs.com/cocowool/p/Docker_Kubernetes_Log_Location.html | 谈一下Docker与Kubernetes集群的日志和日志管理https://kubernetes.io/docs/concepts/cluster-administration/logging/ | Kubernetes 日志架构]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kubernetes 集群安全机制详解]]></title>
<url>%2F2019%2F08%2F22%2FKubernetes-%E9%9B%86%E7%BE%A4%E5%AE%89%E5%85%A8%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[本文主要介绍 Kubernetes 的安全机制,如何使用一系列概念、技术点、机制确保集群的访问是安全的,涉及到的关键词有:api-server,认证,授权,准入控制,RBAC,Service Account,客户端证书认证,Kubernetes 用户,Token 认证等等。虽然涉及到的技术点比较琐碎,比较多,但是了解整个机制后就很容易将其串起来,从而能很好地理解 Kubernetes 集群安全机制。本文结构如下: Kubernetes api-server 安全访问机制; Kubernetes 认证方式之客户端证书(TLS); Kubernetes 授权方式之 RBAC 介绍; Kubernetes 中两种账号类型介绍; 实践:基于客户端证书认证方式新建 Kubeconfig 访问集群; 实践:Kubeconfig 或 token 方式登陆 Kubernetes dashboard; Kubernetes api-server 安全访问机制kube-apiserver 是 k8s 整个集群的入口,是一个 REST API 服务,提供的 API 实现了 Kubernetes 各类资源对象(如 Pod,RC,Service 等)的增、删、改、查,API Server 也是集群内各个功能模块之间交互和通信的枢纽,是整个集群的总线和数据中心。 由此可见 API Server 的重要性了,我们用 kubectl、各种语言提供的客户端库或者发送 REST 请求和集群交互,其实底层都是以 HTTP REST 请求的方式同 API Server 交互,那么访问的安全机制是如何保证的呢,总不能随便来一个请求都能接受并响应吧。API Server 为此提供了一套特有的、灵活的安全机制,每个请求到达 API Server 后都会经过:认证(Authentication)–>授权(Authorization)–>准入控制(Admission Control) 三道安全关卡,通过这三道安全关卡的请求才给予响应: 认证(Authentication)认证阶段的工作是识别用户身份,支持的认证方式有很多,比如:HTTP Base,HTTP token,TLS,Service Account,OpenID Connect 等,API Server 启动时可以同时指定多种认证方式,会逐个使用这些方法对客户请求认证,只要通过任意一种认证方式,API Server 就会认为 Authentication 成功。高版本的 Kubernetes 默认认证方式是 TLS。在 TLS 认证方案中,每个用户都拥有自己的 X.509 客户端证书,API 服务器通过配置的证书颁发机构(CA)验证客户端证书。 授权(Authorization)授权阶段判断请求是否有相应的权限,授权方式有多种:AlwaysDeny,AlwaysAllow,ABAC,RBAC,Node 等。API Server 启动时如果多种授权模式同时被启用,Kubernetes 将检查所有模块,如果其中一种通过授权,则请求授权通过。 如果所有的模块全部拒绝,则请求被拒绝(HTTP状态码403)。高版本 Kubernetes 默认开启的授权方式是 RBAC 和 Node。 准入控制(Admission Control)准入控制判断操作是否符合集群要求,准入控制配备有一个“准入控制器”的列表,发送给 API Server 的每个请求都需要通过每个准入控制器的检查,检查不通过,则 API Server 拒绝调用请求,有点像 Web 编程的拦截器的意思。具体细节在这里不进行展开了,如想进一步了解见这里:Using Admission Controllers。 Kubernetes 认证方式之客户端证书(TLS)通过上一节介绍我们知道 Kubernetes 认证方式有多种,这里我们简单介绍下客户端证书(TLS)认证方式,也叫 HTTPS 双向认证。一般我们访问一个 https 网站,认证是单向的,只有客户端会验证服务端的身份,服务端不会管客户端身份如何。我们来大概看下 HTTPS 握手过程(单向认证): 客户端发送 Client Hello 消息给服务端; 服务端回复 Server Hello 消息和自身证书给客户端;3.客户端检查服务端证书的合法性,证书检查通过后根据双方发送的消息生成 Premaster Key,然后用服务端的证书里面的公钥加密 Premaster Key 并发送给服务端 ; 服务端通过自己的私钥解密得到 Premaster Key,然后通过双方协商的算法和交换的消息生成 Session Key(后续双方数据加密用的对称密钥,客户端也能通过同样的方法生成同样的 Key),然后回复客户端一个消息表明握手结束,后续发送的消息会以协商的对称密钥加密。 关于 HTTPS 握手详细过程见之前文章:「Wireshark 抓包理解 HTTPS 协议」 HTTPS 双向认证的过程就是在上述第 3 步的时候同时回复自己的证书给服务端,然后第 4 步服务端验证收到客户端证书的合法性,从而达到了验证客户端的目的。在 Kubernetes 中就是用了这样的机制,只不过相关的 CA 证书是自签名的: Kubernetes 授权方式之 RBAC 介绍基于角色的访问控制(Role-Based Access Control, 即 RBAC),是 k8s 提供的一种授权策略,也是新版集群默认启用的方式。RBAC 将角色和角色绑定分开,角色指的是一组定义好的操作集群资源的权限,而角色绑定是将角色和用户、组或者服务账号实体绑定,从而赋予这些实体权限。可以看出 RBAC 这种授权方式很灵活,要赋予某个实体权限只需要绑定相应的角色即可。针对 RBAC 机制,k8s 提供了四种 API 资源:Role、ClusterRole、RoleBinding、ClusterRoleBinding。 Role: 只能用于授予对某一单一命名空间中资源的访问权限,因此在定义时必须指定 namespace;以下示例描述了 default 命名空间中的一个 Role 对象的定义,用于授予对 pod 的读访问权限: 123456789kind: RoleapiVersion: rbac.authorization.k8s.io/v1beta1metadata: namespace: default name: pod-readerrules:- apiGroups: [""] # 空字符串""表明使用core API group resources: ["pods"] verbs: ["get", "watch", "list"] ClusterRole:针对集群范围的角色,能访问整个集群的资源;下面示例中的 ClusterRole 定义可用于授予用户对某一特定命名空间,或者所有命名空间中的 secret(取决于其绑定方式)的读访问权限: 123456789kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1beta1metadata: # 鉴于ClusterRole是集群范围对象,所以这里不需要定义"namespace"字段 name: secret-readerrules:- apiGroups: [""] resources: ["secrets"] verbs: ["get", "watch", "list"] RoleBinding:将 Role 和用户实体绑定,从而赋予用户实体命名空间内的权限,同时也支持 ClusterRole 和用户实体的绑定;下面示例中定义的 RoleBinding 对象在 default 命名空间中将 pod-reader 角色授予用户 jane。 这一授权将允许用户 jane 从 default 命名空间中读取pod: 1234567891011121314# 以下角色绑定定义将允许用户"jane"从"default"命名空间中读取pod。kind: RoleBindingapiVersion: rbac.authorization.k8s.io/v1beta1metadata: name: read-pods namespace: defaultsubjects:- kind: User name: jane apiGroup: rbac.authorization.k8s.ioroleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io ClusterRoleBinding:将 ClusterRole 和用户实体绑定,从而赋予用户实体集群范围的权限;下面示例中所定义的 ClusterRoleBinding 允许在用户组 manager 中的任何用户都可以读取集群中任何命名空间中的 secret: 12345678910111213# 以下`ClusterRoleBinding`对象允许在用户组"manager"中的任何用户都可以读取集群中任何命名空间中的secret。kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1beta1metadata: name: read-secrets-globalsubjects:- kind: Group name: manager apiGroup: rbac.authorization.k8s.ioroleRef: kind: ClusterRole name: secret-reader apiGroup: rbac.authorization.k8s.io 关于 RBAC 更详细的讲解见这里:https://jimmysong.io/kubernetes-handbook/concepts/rbac.html Kubernetes 中两种账号类型介绍K8S中有两种用户(User):服务账号(ServiceAccount)和普通的用户(User)。 ServiceAccount 是由 k8s 管理的,而 User 账号是在外部管理,k8s 不存储用户列表,也就是说针对用户的增、删、该、查都是在集群外部进行,k8s 本身不提供普通用户的管理。 两种账号的区别: ServiceAccount 是 k8s 内部资源,而普通用户是存在于 k8s 之外的; ServiceAccount 是属于某个命名空间的,不是全局的,而普通用户是全局的,不归某个 namespace 特有; ServiceAccount 一般用于集群内部 Pod 进程使用,和 api-server 交互,而普通用户一般用于 kubectl 或者 REST 请求使用; ServiceAccount 的实际应用ServiceAccount 可以用于 Pod 访问 api-server,其对应的 Token 可以用于 kubectl 访问集群,或者登陆 kubernetes dashboard。 普通用户的实际应用 X509 客户端证书客户端证书验证通过为API Server 指定 –client-ca-file=xxx 选项启用,API Server通过此 ca 文件来验证 API 请求携带的客户端证书的有效性,一旦验证成功,API Server 就会将客户端证书 Subject 里的 CN 属性作为此次请求的用户名。关于客户端证书方式的用户后面会有专门的实践介绍。 静态token文件通过指定–token-auth-file=SOMEFILE 选项来启用 bearer token 验证方式,引用的文件是一个包含了 token,用户名,用户ID 的csv文件,请求时,带上 Authorization: Bearer xxx 头信息即可通过 bearer token 验证; 静态密码文件通过指定 --basic-auth-file=SOMEFILE 选项启用密码验证,引用的文件是一个包含 密码,用户名,用户ID 的csv文件,请求时需要将 Authorization 头设置为 Basic BASE64ENCODED(USER:PASSWORD); 实践:基于客户端证书认证方式新建 Kubeconfig 访问集群Kubeconfig 文件详解我们知道在安装完 k8s 集群后会生成 $HOME/.kube/config 文件,这个文件就是 kubectl 命令行工具访问集群时使用的认证文件,也叫 Kubeconfig 文件。这个 Kubeconfig 文件中有很多重要的信息,文件大概结构是这样,这里说明下每个字段的含义:12345678910111213141516171819apiVersion: v1clusters:- cluster: certificate-authority-data: ... server: https://192.168.26.10:6443 name: kubernetescontexts:- context: cluster: kubernetes user: kubernetes-admin name: kubernetes-admin@kubernetescurrent-context: kubernetes-admin@kuberneteskind: Configpreferences: {}users:- name: kubernetes-admin user: client-certificate-data: ... client-key-data: ... 可以看出文件分为三大部分:clusters、contexts、usersclusters 部分定义集群信息,包括 api-server 地址、certificate-authority-data: 用于服务端证书认证的自签名 CA 根证书(master 节点 /etc/kubernetes/pki/ca.crt 文件内容 )。 contexts 部分集群信息和用户的绑定,kubectl 通过上下文提供的信息连接集群。 users 部分多种用户类型,默认是客户端证书(x.509 标准的证书)和证书私钥,也可以是 ServiceAccount Token。这里重点说下前者: client-certificate-data: base64 加密后的客户端证书; client-key-data: base64 加密后的证书私钥; 一个请求在通过 api-server 的认证关卡后,api-server 会从收到客户端证书中取用户信息,然后用于后面的授权关卡,这里所说的用户并不是服务账号,而是客户端证书里面的 Subject 信息:O 代表用户组,CN 代表用户名。为了证明,可以使用 openssl 手动获取证书中的这个信息:首先,将 Kubeconfig 证书的 user 部分 client-certificate-data 字段内容进行 base64 解密,保存文件为 client.crt,然后使用 openssl 解析证书信息即可看到 Subject 信息:1openssl x509 -in client.crt -text 解析集群默认的 Kubeconfig 客户端证书得到的 Subject 信息是:1Subject: O=system:masters, CN=kubernetes-admin 可以看出该证书绑定的用户组是 system:masters,用户名是 kubernetes-admin,而集群中默认有个 ClusterRoleBinding 叫 cluster-admin,它将名为 cluster-admin 的 ClusterRole 和用户组 system:masters 进行了绑定,而名为 cluster-admin 的 ClusterRole 有集群范围的 Superadmin 权限,这也就理解了为什么默认的 Kubeconfig 能拥有极高的权限来操作 k8s 集群了。 新建具有只读权限的 Kubeconfig 文件上面我们已经解释了为什么默认的 Kubeconfig 文件具有 Superadmin 权限,这个权限比较高,有点类型 Linux 系统的 Root 权限。有时我们会将集群访问权限开放给其他人员,比如供研发人员查看 Pod 状态、日志等信息,这个时候直接用系统默认的 Kubeconfig 就不太合理了,权限太大,集群的安全性没有了保障。更合理的做法是给研发人员一个只读权限的账号,避免对集群进行一些误操作导致故障。 我们以客户端证书认证方式创建 Kubeconfig 文件,所以需要向集群自签名 CA 机构(master 节点)申请证书,然后通过 RBAC 授权方式给证书用户授予集群只读权限,具体方法如下: 假设我们设置证书的用户名为:developer – 证书申请时 -subj 选项 CN 参数。 生成客户端 TLS 证书 创建证书私钥 1openssl genrsa -out developer.key 2048 用上面私钥创建一个 csr(证书签名请求)文件,其中我们需要在 subject 里带上用户信息(CN为用户名,O为用户组) 1openssl req -new -key developer.key -out developer.csr -subj "/CN=developer" 其中/O参数可以出现多次,即可以有多个用户组 找到 k8s 集群(API Server)的 CA 根证书文件,其位置取决于安装集群的方式,通常会在 master 节点的 /etc/kubernetes/pki/ 路径下,会有两个文件,一个是 CA 根证书(ca.crt),一个是 CA 私钥(ca.key) 。 通过集群的 CA 根证书和第 2 步创建的 csr 文件,来为用户颁发证书1openssl x509 -req -in developer.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out developer.crt -days 365 至此,客户端证书颁发完成,我们后面会用到文件是 developer.key 和 developer.crt 基于 RBAC 授权方式授予用户只读权限在 k8s 集群中已经有个默认的名为 view 只读 ClusterRole 了,我们只需要将该 ClusterRole 绑定到 developer 用户即可:1kubectl create clusterrolebinding kubernetes-viewer --clusterrole=view --user=developer 基于客户端证书生成 Kubeconfig 文件前面已经生成了客户端证书,并给证书里的用户赋予了集群只读权限,接下来基于客户端证书生成 Kubeconfig 文件:拷贝一份 $HOME/.kube.config,假设名为 developer-config,在其基础上做修改: contexts 部分 user 字段改为 developer,name 字段改为 developer@kubernetes。(这些改动随意命名,只要前后统一即可); users 部分 name 字段改为 developer,client-certificate-data 字段改为 developer.crt base64 加密后的内容,client-key-data 改为 developer.key base64 加密后的内容; 注意:证书 base64 加密时指定 –wrap=0 参数cat developer.crt | base64 –wrap=0cat developer.key | base64 –wrap=0 接下来测试使用新建的 Kubeconfig 文件: [root@master ~]# kubectl –kubeconfig developer-config –context=developer@kubernetes get podNAME READY STATUS RESTARTS AGEnginx-deployment-5754944d6c-dqsdj 1/1 Running 0 5d9hnginx-deployment-5754944d6c-q675s 1/1 Running 0 5d9h[root@master ~]# kubectl –kubeconfig developer-config –context=developer@kubernetes delete pod nginx-deployment-5754944d6c-dqsdjError from server (Forbidden): pods “nginx-deployment-5754944d6c-dqsdj” is forbidden: User “developer” cannot delete resource “pods” in API group “” in the namespace “default” 可以看出新建的 Kubeconfig 文件可以使用,写权限是被 forbidden 的,说明前面配的 RBAC 权限机制是起作用的。 实践:Kubeconfig 或 token 方式登陆 Kubernetes dashboard我们打开 kubernetes dashboard 访问地址首先看到的是登陆认证页面,有两种登陆认证方式可供选择:Kubeconfig 和 Token 方式 其实两种方式都需要服务账号的 Token,对于 Kubeconfig 方式直接使用集群默认的 Kubeconfig: $HOME/.kube/config 文件不能登陆,因为文件中缺少 Token 字段,所以直接选择本地的 Kubeconfig 文件登陆会报错。正确的使用方法是获取某个服务账号的 Token,然后将 Token 加入到 $HOME/.kube/config 文件。下面具体实践下两种登陆 dashboard 方式: 准备工作首先,两种方式都需要服务账号,所以我们先创建一个服务账号,然后为了测试,给这个服务账号一个查看权限(RBAC 授权),到时候登陆 dashboard 后只能查看,不能对集群中的资源做修改。 创建一个服务账号(在 default 命名空间下); 1kubectl create serviceaccount kube-dashboard-reader 将系统自带的 ClusterRole:view 角色绑定到上一步创建的服务账号,授予集群范围的资源只读权限; 1kubectl create clusterrolebinding kube-dashboard-reader --clusterrole=view --serviceaccount=default:kube-dashboard-reader 获取服务账号的 token; 1kubectl get secret `kubectl get secret -n default | grep kube-dashboard-reader | awk '{print $1}'` -o jsonpath={.data.token} -n default | base64 -d Kubeconfig 方式登陆 dashboard拷贝一份 $HOME/.kube/config,修改内容,将准备工作中获取的 Token 添加入到文件中:在 Kubeconfig 的 Users 下 User 部分添加,类型下面这样:1234567...users:- name: kubernetes-admin user: client-certificate-data: ... client-key-data: ... token: <这里为上面获取的 Token...> 然后登陆界面选择 Kubeconfig 单选框,选择该文件即可成功登陆 dashboard。 Token 方式登陆 dashboard登陆界面选择 Token 单选框,将准备工作中获取的 Token 粘贴进去即可成功登陆。 相关文档https://kubernetes.io/zh/docs/reference/access-authn-authz/controlling-access/ | Kubernetes API 访问控制https://mp.weixin.qq.com/s/u4bGemn1cxbiZBoMX54sPA | 小白都能看懂的 Kubernetes安全之 API-server 安全https://zhuanlan.zhihu.com/p/43237959 | 为 Kubernetes 集群添加用户https://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/ | Kubernetes 集群的安全配置https://support.qacafe.com/knowledge-base/how-do-i-display-the-contents-of-a-ssl-certificate/ | x.509 证书内容查看]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kong 微服务网关在 Kubernetes 的实践]]></title>
<url>%2F2019%2F08%2F17%2FKong-%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%BD%91%E5%85%B3%E5%9C%A8-Kubernetes-%E7%9A%84%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[本文主要介绍将 Kong 微服务网关作为 Kubernetes 集群统一入口的最佳实践,之前写过一篇文章使用 Nginx Ingress Controller 作为集群统一的流量入口:使用 Kubernetes Ingress 对外暴露服务,但是相比于 Kong Ingress Controller 来说,Kong 支持的功能更加强大,更适合微服务架构: 拥有庞大的插件生态,能轻易扩展 Kong 支持的功能,比如 API 认证,流控,访问限制等; Kong 服务本身和 Admin 管理 API 都集成在一个进程,通过端口区分两者,简化了部署的复杂度; Kong 节点的配置统一持久化到数据库,所有节点通过数据库共享数据,在 Ingress 更新后能实时同步到各个节点,而 Nginx Ingress Controller 是通过重新加载机制响应 Ingress 更新,这种方式代价比较大,可能会导致服务的短暂中断; Kong 有成熟的第三方管理 UI 和 Admin 管理 API 对接,从而能可视化管理 Kong 配置; 本文先介绍 Kong 微服务网关在 Kubernetes 的架构,然后进行架构实践,涉及到的话题有: Kong 微服务网关在 Kubernetes 的架构; helm 部署 Kong(包含 Kong Ingress Controller); 部署 Konga; 示例:通过 Konga 配置对外访问 Kubernetes Dashboard; Kong 微服务网关在 Kubernetes 的架构Kubernetes 简化了微服务架构,以 Service 为单位,代表一个个微服务,但是 k8s 集群整个网络对外是隔离的,在微服务架构下一般需要一个网关作为所有 API 的入口,在 k8s 中架构微服务同样也需要一个网关,作为集群统一的入口,作为服务消费方和提供方的交互中间件。Kong 可以充当这一网关角色,为集群提供统一的外部流量入口,集群内部 Service 之间通信通过 Service 名称调用:那么 Kong 是如何在 k8s 集群上跑起来?具体机制是什么样的?Kong 作为服务接入层,不仅提供了外部流量的接收转发,而且其本身还提供了 Admin 管理 API,通过 Admin 管理 API 实现 Kong 的路由转发等相关配置,这两项功能都是在一个进程中实现。 在 k8s 中 Kong 以 Pod 形式作为节点运行,Pod 通过 Deployment 或者 DaemenSet 管理。所有 Kong 节点共享一个数据库,因此通过 Admin API 配置,所有节点都能同步感知到变化。既然 Kong 以 Pod 的形式运行在 k8s 集群中,那么其本身需要对外暴露,这样外部流量才能进来,在本地可以 nodePort 或者 hostNetwork 对外提供服务,在云平台一般通过 LoadBalancer 实现。一般的部署最佳实践是将 Kong 的 Admin 管理功能独立出来一个 Pod,专门作为所有其他节点的统一配置管理,不对外提供流量转发服务,只提供配置功能,而其他 Kong 节点专门提供流量转发功能。 说一说 Kong Ingress Controller:其实没有 Kong Ingress Controller 也完全够用,其存在的意义是为了实现 k8s Ingress 资源对象。我们知道 Ingress 只不过是定义了一些流量路由规则,但是光有这个路由规则没用,需要 Ingress Controller 来将这些路由规则转化成相应代理的实际配置,比如 Kong Ingress Controller 就可以将 Ingress 转化成 Kong 的配置。与 Nginx Ingress Controller 不同,Kong Ingress Controller 不对外提供服务,只作为 k8s Ingress 资源的解析转化服务,将解析转化的结果(Kong 的配置:比如 Service、Route 实体等)通过与 Kong Admin API 写入 Kong 数据库,所以 Kong Ingress Controller 需要和 Kong Admin API 打通。所以当我们需要配置 Kong 的路由时,既可以通过创建 k8s Ingress 实现,也可以通过 Kong Admin API 直接配置。 helm 部署 Kong(包含 Kong Ingress Controller)说明:本地集群部署,为了方便 Kong Proxy 和 Kong Admin 没有独立开,共用一个进程,同时提供流量转发和 Admin 管理 API。使用 helm 官方 Chart: stable/kong,由于我是在本地裸机集群部署,很多云的功能不支持,比如:LoadBalancer、PV、PVC 等,所以需要对 Chart 的 values 文件做一些定制化以符合本地需求: 由于本地裸机集群不支持 LoadBalancer,所以采用 nodePort 方式对外暴露 Kong proxy 和 Kong admin 服务,Chart 默认是 nodePort 方式,在这里自定义下端口:Kong proxy 指定 nodePort 80 和 443 端口,Kong Admin 指定 8001 端口:Values.proxy.http.nodePort: 80 Values.proxy.tls.nodePort: 443, Values.admin.nodePort: 8001; 注意:默认 k8s nodePort 端口范围是 30000~32767,手动分配该范围之外的端口会报错!该限制可以调整,具体见之前文章:Kubernetes 调整 nodePort 端口范围 启用 Kong admin 和 Kong proxy Ingress,部署时会创建相应的 Ingress 资源,实现服务对外访问:Values.admin.ingress.enabled: true, Values.proxy.ingress.enabled: true,另外还得设置对外访问的域名(没有域名的话可以随便起个域名,然后绑 /etc/hosts 访问):Values.admin.ingress.hosts: [admin.kong.com], Values.proxy.ingress.hosts: [proxy.kong.com]; 作为练习,为了方便,Kong admin 改用监听 HTTP 8001 端口:Values.admin.useTLS: false, .Values.admin.servicePort: 8001, .Values.admin.containerPort: 8001。另外也需要将 Pod 探针协议也改为 HTTP:Values.livenessProbe.httpGet.scheme: HTTP, Values.readinessProbe.httpGet.scheme: HTTP; Kong proxy Ingress 启用 HTTPS,这样后续 kong 就可以同时支持 HTTP 和 HTTP 代理了,这里展开下具体过程: 创建 TLS 证书:域名为 proxy.kong.com 1openssl req -x509 -nodes -days 65536 -newkey rsa:2048 -keyout proxy-kong.key -out proxy-kong.crt -subj "/CN=proxy.kong.com/O=proxy.kong.com" 使用生成的证书创建 k8s Secret 资源: 1kubectl create secret tls proxy-kong-ssl --key proxy-kong.key --cert proxy-kong.crt -n kong 编辑 values 文件启用 Kong Proxy Ingress tls,引用上面创建的 Secret:Values.proxy.ingress.tls: 123- hosts: - proxy.kong.com secretName: proxy-kong-ssl 启用 Kong Ingress Controller,默认是不会部署 Kong Ingress Controller:ingressController.enabled: true; 由于本地裸机环境不支持 PV 存储,所以在部署时禁用 Postgres 数据持久化:helm 安装时指定 --set postgresql.persistence.enabled=false,这样 Postgres 存储会使用 emptyDir 方式挂载卷,在 Pod 重启后数据会丢失,本地自己玩的话可以先这么搞。当然要复杂点的话,可以自己再搭个 nfs 支持 PV 资源对象。 定制后的 values 文件在这里:https://raw.githubusercontent.com/qhh0205/helm-charts/master/kong-values.yml helm 部署1helm install stable/kong --name kong --set postgresql.persistence.enabled=false -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/kong-values.yml --namespace kong 验证部署1234567891011[root@master kong]# kubectl get pod -n kong NAME READY STATUS RESTARTS AGEkong-kong-controller-76d657b78-r6cj7 2/2 Running 1 58skong-kong-d889cf995-dw7kj 1/1 Running 0 58skong-kong-init-migrations-c6fml 0/1 Completed 0 58skong-postgresql-0 1/1 Running 0 58s[root@master kong]# kubectl get ingress -n kongNAME HOSTS ADDRESS PORTS AGEkong-kong-admin admin.kong.com 80 84skong-kong-proxy prox.kong.com 80, 443 84s curl 测试1234567[root@master kong]# curl -I http://admin.kong.com HTTP/1.1 200 OKContent-Type: application/json...[root@master kong]# curl http://proxy.kong.com{"message":"no Route matched with those values"} 部署 Konga上面已经将整个 Kong 平台运行在了 Kubernetes 集群,并启用了 Kong Ingress Controller,但是目前做 Kong 相关的路由配置只能通过 curl 调 Kong Admin API,配置起来不是很方便。所以需要将针对 Kong 的 UI 管理服务 Konga 部署到集群,并和 Kong 打通,这样就可以可视化做 Kong 的配置了。由于 Konga 的部署很简单,官方也没有 Chart,所以我们通过 yaml 文件创建相关资源。 为了节省资源,Konga 和 Kong 共用一个 Postgresql,Konga 和 Kong 本身对数据库资源占用很少,所以两个同类服务共用一个数据库是完全合理的。下面为 k8s 资源文件,服务对外暴露方式为 Kong Ingress,域名设为(名字随便起的,绑 host 访问):konga.kong.com: 数据库密码在前面安装 Kong 时 Chart 创建的 Secret 中,获取方法:kubectl get secret kong-postgresql -n kong -o yaml | grep password | awk -F ':' '{print $2}' | tr -d ' ' | base64 -d konga.yml123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263apiVersion: extensions/v1beta1kind: Deploymentmetadata: labels: app: konga name: kongaspec: replicas: 1 selector: matchLabels: app: konga strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: konga spec: containers: - env: - name: DB_ADAPTER value: postgres - name: DB_URI value: "postgresql://kong:K9IV9pHTdS@kong-postgresql:5432/konga_database" image: pantsel/konga imagePullPolicy: Always name: konga ports: - containerPort: 1337 protocol: TCP restartPolicy: Always---apiVersion: v1kind: Servicemetadata: name: kongaspec: ports: - name: http port: 1337 targetPort: 1337 protocol: TCP selector: app: konga---apiVersion: extensions/v1beta1kind: Ingressmetadata: name: konga-ingressspec: rules: - host: konga.kong.com http: paths: - path: / backend: serviceName: konga servicePort: 1337 kubectl 部署 Konga1kubectl create -f konga.yml -n kong 部署完成后绑定 host 将 konga.kong.com 指向集群节点 IP 即可访问: 接下来随便注册个账号,然后配置连接到 Kong Admin 地址,由于都在集群内部,所以直接用 Kong Admin 的 ServiceName + 端口号连就可以: 连接没问题后,主页面会显示 Kong 相关的全局信息: 示例:通过 Konga 配置对外访问 Kubernetes Dashboard之前我们基于 Nginx Ingress Controller 对外暴露 Kubernetes Dashboard,现在我们基于集群中 Kong 平台配置对外访问,通过 Konga 可视化操作。通过 Konga 配置服务对外访问只需要两步: 创建一个对应服务的 Service(不是 k8s 的 Servide,是 Kong 里面 Service 的概念:反向代理上游服务的抽象); 创建对应 Service 的路由; 下面以配置 Kubernetes dashboard 服务对外访问为例,对外域名设为 dashboard.kube.com (名字随便起的,绑 host 访问) 创建 Kong Service: 创建服务路由: 配置完成,浏览器测试访问:https://dashboard.kube.com 相关文档https://konghq.com/solutions/kubernetes-ingress/ | Kong on Kuberneteshttps://konghq.com/blog/kubernetes-ingress-controller-for-kong/ | Announcing the Kubernetes Ingress Controller for Konghttps://docs.konghq.com/install/kubernetes/ | Kong and Kong Enterprise on Kuberneteshttps://github.com/Kong/kubernetes-ingress-controller | GitHub Kong Ingress Controller]]></content>
<categories>
<category>微服务网关</category>
</categories>
<tags>
<tag>微服务网关</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kubernetes 调整 nodePort 端口范围]]></title>
<url>%2F2019%2F08%2F15%2FKubernetes-%E8%B0%83%E6%95%B4-nodePort-%E7%AB%AF%E5%8F%A3%E8%8C%83%E5%9B%B4%2F</url>
<content type="text"><![CDATA[默认情况下,k8s 集群 nodePort 分配的端口范围为:30000-32767,如果我们指定的端口不在这个范围就会报类似下面这样的错误: Error: release kong failed: Service “kong-kong-admin” is invalid: spec.ports[0].nodePort: Invalid value: 8444: provided port is not in the valid range. The range of valid ports is 30000-32767 解决方法就是调整 kube-apiserver 组件启动参数,指定 nodePort 范围。如果是用 kubeadm 安装的集群,那么 apiserver 是以静态 pod 的形式运行,pod 文件定义在 /etc/kubernetes/manifests/kube-apiserver.yaml。/etc/kubernetes/manifests 目录下是所有静态 pod 文件的定义,kubelet 会监控该目录下文件的变动,只要发生变化,pod 就会重建,响应相应的改动。所以我们修改 /etc/kubernetes/manifests/kube-apiserver.yaml 文件,添加 nodePort 范围参数后会自动生效,无需进行其他操作:vim /etc/kubernetes/manifests/kube-apiserver.yaml在 command 下添加 --service-node-port-range=1-65535 参数,修改后会自动生效,无需其他操作:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081apiVersion: v1kind: Podmetadata: creationTimestamp: null labels: component: kube-apiserver tier: control-plane name: kube-apiserver namespace: kube-systemspec: containers: - command: - kube-apiserver - --service-node-port-range=1-65535 - --advertise-address=192.168.26.10 - --allow-privileged=true - --authorization-mode=Node,RBAC - --client-ca-file=/etc/kubernetes/pki/ca.crt - --enable-admission-plugins=NodeRestriction - --enable-bootstrap-token-auth=true - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key - --etcd-servers=https://127.0.0.1:2379 - --insecure-port=0 - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key - --requestheader-allowed-names=front-proxy-client - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt - --requestheader-extra-headers-prefix=X-Remote-Extra- - --requestheader-group-headers=X-Remote-Group - --requestheader-username-headers=X-Remote-User - --secure-port=6443 - --service-account-key-file=/etc/kubernetes/pki/sa.pub - --service-cluster-ip-range=10.96.0.0/12 - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key image: registry.aliyuncs.com/google_containers/kube-apiserver:v1.15.2 imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 8 httpGet: host: 192.168.26.10 path: /healthz port: 6443 scheme: HTTPS initialDelaySeconds: 15 timeoutSeconds: 15 name: kube-apiserver resources: requests: cpu: 250m volumeMounts: - mountPath: /etc/ssl/certs name: ca-certs readOnly: true - mountPath: /etc/pki name: etc-pki readOnly: true - mountPath: /etc/kubernetes/pki name: k8s-certs readOnly: true hostNetwork: true priorityClassName: system-cluster-critical volumes: - hostPath: path: /etc/ssl/certs type: DirectoryOrCreate name: ca-certs - hostPath: path: /etc/pki type: DirectoryOrCreate name: etc-pki - hostPath: path: /etc/kubernetes/pki type: DirectoryOrCreate name: k8s-certsstatus: {} 相关文档:http://www.thinkcode.se/blog/2019/02/20/kubernetes-service-node-port-range]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 docker-compose 容器化构建 Kong 微服务网关平台]]></title>
<url>%2F2019%2F08%2F14%2F%E5%9F%BA%E4%BA%8E-docker-compose-%E5%AE%B9%E5%99%A8%E5%8C%96%E6%9E%84%E5%BB%BA-Kong-%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%BD%91%E5%85%B3%E5%B9%B3%E5%8F%B0%2F</url>
<content type="text"><![CDATA[本文主要介绍如何使用 docker-compose 快速体验 Kong 微服务网关,先简单介绍基本概念,然后做了一个 Demo 测试使用,涉及到的相关话题有: Kong 简介; Konga 简介; 基于 docker-compose 容器化构建 Kong 微服务网关平台; 使用 Konga 可视化创建一个 Service 及路由; Kong 简介Kong 是微服务网关模式架构中连接服务消费方和服务提供方的中间件系统,即我们经常所说的微服务网关,网关将各自的业务系统的演进和发展做了天然的隔离,使业务系统更加专注于业务服务本身,同时微服务网关还可以为服务提供和沉淀更多附加功能。微服务网关的主要作用如下: 请求接入:管理所有请求接入,作为所有 API 接口的请求入口; 业务聚合:所有微服务后端可以注册在 API 网关,通过 API 网关统一暴露服务; 拦截策略:可以通过统一的安全、路由、流控等公共服务组件; 统一管理:提供统一的监控工具,配置管理工具等基础设施; Kong 作为完全开源的微服务网关,基于 Nginx 实现,所以其性能表现是毋庸置疑的,另外和其他网关系统类似,也支持插件化扩展,提供了丰富的插件可供使用。Kong 进程启动后会启动多个端口,每个端口功能也不一样: 8001 端口:http 管理 API; 8444 端口:https 管理 API; 8000 端口:接收处理 http 流量; 8443 端口:接收处理 https 流量; Kong 的使用特别简单,需要搞懂几个概念就可以快速使用了: Service:Service 是要对外暴露的上游服务,类似于 Nginx 反向代理配置的 upstream; Route:Route 定义了路由规则,外部流量如何路由到相应的 Service; Consumer:类似账号的概念,可以设置不同的 Consumer 对 API 的访问限制; 关于 Kong 数据持久化Kong 有两种运行模式,以 db-less 模式运行时所有路由、Service 等信息都存储在内存中,这些信息都是通过 declarative 配置文件动态生成,然后加在到内存。另一种是以 db 模式运行,需要额外的数据库支持,用于存储路由、Service 等信息,这种方式是生产环境推荐的方式。Kong 支持两种数据库持久化:Postgres 或者 Cassandra。 Konga 简介Konga 是一个第三方开源的针对 Kong 网关的 UI 管理界面,与 Kong 没有关系。通过 Konga 我们可以可视化配置 Kong 相关的配置,在没有可视化界面的情况下只能通过 curl 调用 Kong 提供的 Admin API 来管理 Kong 配置,相对于可视化配置来说复杂度是显而易见的。Konga 支持的主要特性如下: 管理所有 Kong Admin API 对象; 多个 Kong 节点管理; 使用快照备份、恢复 Kong 节点; 通过健康检查健康节点和 API 状态; 支持邮寄和 Slack 告警通知; 多用户管理; 支持数据库集成(MySQL,postgresSQL,MongoDB,SQL Server); Konga 默认将用户信息和配置信息存在在本地磁盘文件,我们可以选择和数据库集成,将相关信息存储到数据库,这也是生产环境推荐的做法。 使用 docker-compose 容器化构建 Kong 微服务网关平台使用 docker-compose 将 Kong 网关、Konga UI 管理页面、数据库三个服务组合起来,组成一个完整、可用的网关系统,Kong 网关和 Konga 服务共用一个 postgres 数据库。下面为启动整个系统完整的 docker-compose 文件,来自:https://gist.github.com/pantsel/73d949774bd8e917bfd3d9745d71febf 在其基础上对存在的问题进行了修复:docker-compose.yml123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102version: "3"networks: kong-net: driver: bridgeservices: ####################################### # Postgres: The database used by Kong ####################################### kong-database: image: postgres:9.6 restart: always networks: - kong-net environment: POSTGRES_USER: kong POSTGRES_DB: kong ports: - "5432:5432" healthcheck: test: ["CMD", "pg_isready", "-U", "kong"] interval: 5s timeout: 5s retries: 5 ####################################### # Kong database migration ####################################### kong-migration: image: kong:latest command: "kong migrations bootstrap" networks: - kong-net restart: on-failure environment: KONG_PG_HOST: kong-database links: - kong-database depends_on: - kong-database ####################################### # Kong: The API Gateway ####################################### kong: image: kong:latest restart: always networks: - kong-net environment: KONG_PG_HOST: kong-database KONG_PROXY_LISTEN: 0.0.0.0:8000 KONG_PROXY_LISTEN_SSL: 0.0.0.0:8443 KONG_ADMIN_LISTEN: 0.0.0.0:8001 depends_on: - kong-migration - kong-database healthcheck: test: ["CMD", "curl", "-f", "http://kong:8001"] interval: 5s timeout: 2s retries: 15 ports: - "8001:8001" - "8000:8000" - "8443:8443" ####################################### # Konga database prepare ####################################### konga-prepare: image: pantsel/konga:next command: "-c prepare -a postgres -u postgresql://kong@kong-database:5432/konga_db" networks: - kong-net restart: on-failure links: - kong-database depends_on: - kong-database ####################################### # Konga: Kong GUI ####################################### konga: image: pantsel/konga:latest restart: always networks: - kong-net environment: DB_ADAPTER: postgres DB_HOST: kong-database DB_USER: kong TOKEN_SECRET: km1GUr4RkcQD7DewhJPNXrCuZwcKmqjb DB_DATABASE: konga_db NODE_ENV: production depends_on: - kong-database ports: - "1337:1337" docker-compose 一键启动相关服务:1docker-compose up -d 启动后访问 http://nodeIP:1337 即可看到 Konga 登陆注册页面,首次访问需要注册用户,随便填写用户名称、邮箱等相关信息即可: 在登陆后页面比较简单,因为还没有连接到 Kong,下面配置连接到要管理的 Kong 节点:Name:随便填写;Kong Admin URL:填写 Kong Admin API 地址; 连接到 Kong 节点后即可看到节点更详细的信息: 使用 Konga 可视化创建一个 Service 及路由Kong 官方文档给了一个 Kong 的使用 例子:通过 curl 调用 Kong Admin API 创建一个 Service 和路由,然后通过 curl 测试访问。这里我们演示如何通过 Konga 可视化做同样的配置: 首先创建一个 Service,指向 http://mockbin.org 服务: 创建一个对外访问的路由,路由到上一步创建的 Service: 上面配置中 Hosts 即为对外访问的 host 名称,只要将 api.example.com 绑定到网关 IP 地址即可进行访问,会路由到绑定的 Service:1curl http://api.example.com:8000 相关文档https://docs.konghq.com/ | Kong 官方文档https://github.com/pantsel/konga/ | Konga GitHub 仓库https://gist.github.com/pantsel/73d949774bd8e917bfd3d9745d71febf | kong docker-compose]]></content>
<categories>
<category>微服务网关</category>
</categories>
<tags>
<tag>微服务网关</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Kubernetes Ingress 对外暴露服务]]></title>
<url>%2F2019%2F08%2F12%2F%E4%BD%BF%E7%94%A8-Kubernetes-Ingress-%E5%AF%B9%E5%A4%96%E6%9A%B4%E9%9C%B2%E6%9C%8D%E5%8A%A1%2F</url>
<content type="text"><![CDATA[本文主要介绍如何通过 Kubernetes Ingress 资源对象实现从外部对 k8s 集群中服务的访问,介绍了 k8s 对外暴露服务的多种方法、Ingress 及 Ingress Controller 的概念。涉及到的话题有: k8s 对外暴露服务的方法; Ingress 及 Ingress Controller 简介; helm 裸机部署 Nginx Ingress Controller; 使用 Ingress 对外暴露服务; 通过 Ingress 访问 kubernetes dashboard(支持 HTTPS 访问); k8s 对外暴露服务的方法向 k8s 集群外部暴露服务的方式有三种: nodePort,LoadBalancer 和本文要介绍的 Ingress。每种方式都有各自的优缺点,nodePort 方式在服务变多的情况下会导致节点要开的端口越来越多,不好管理。而 LoadBalancer 更适合结合云提供商的 LB 来使用,但是在 LB 越来越多的情况下对成本的花费也是不可小觑。Ingress 是 k8s 官方提供的用于对外暴露服务的方式,也是在生产环境用的比较多的方式,一般在云环境下是 LB + Ingress Ctroller 方式对外提供服务,这样就可以在一个 LB 的情况下根据域名路由到对应后端的 Service,有点类似于 Nginx 反向代理,只不过在 k8s 集群中,这个反向代理是集群外部流量的统一入口。 Ingress 及 Ingress Controller 简介Ingress 是 k8s 资源对象,用于对外暴露服务,该资源对象定义了不同主机名(域名)及 URL 和对应后端 Service(k8s Service)的绑定,根据不同的路径路由 http 和 https 流量。而 Ingress Contoller 是一个 pod 服务,封装了一个 web 前端负载均衡器,同时在其基础上实现了动态感知 Ingress 并根据 Ingress 的定义动态生成 前端 web 负载均衡器的配置文件,比如 Nginx Ingress Controller 本质上就是一个 Nginx,只不过它能根据 Ingress 资源的定义动态生成 Nginx 的配置文件,然后动态 Reload。个人觉得 Ingress Controller 的重大作用是将前端负载均衡器和 Kubernetes 完美地结合了起来,一方面在云、容器平台下方便配置的管理,另一方面实现了集群统一的流量入口,而不是像 nodePort 那样给集群打多个孔。 所以,总的来说要使用 Ingress,得先部署 Ingress Controller 实体(相当于前端 Nginx),然后再创建 Ingress (相当于 Nginx 配置的 k8s 资源体现),Ingress Controller 部署好后会动态检测 Ingress 的创建情况生成相应配置。Ingress Controller 的实现有很多种:有基于 Nginx 的,也有基于 HAProxy的,还有基于 OpenResty 的 Kong Ingress Controller 等,更多 Controller 见:https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/,本文使用基于 Nginx 的 Ingress Controller:ingress-nginx。 helm 裸机部署 Nginx Ingress Controller基于 Nginx 的 Ingress Controller 有两种,一种是 k8s 社区提供的 ingress-nginx,另一种是 Nginx 社区提供的 kubernetes-igress,关于两者的区别见 这里。 在这里我们部署 k8s 社区提供的 ingress-nginx,Ingress Controller 对外暴露方式采用 hostNetwork,在裸机环境下更多其他暴露方式见:https://kubernetes.github.io/ingress-nginx/deploy/baremetal/ 使用 Helm 官方提供的 Chart stable/nginx-ingress,修改 values 文件: 使用 DaemonSet 控制器,默认是 Deployment:controller.kind 设为 DaemonSet; pod 使用主机网络:controller.hostNetwork 设为 true; 在hostNetwork 下 pod 使用集群提供 dns 服务:controller.dnsPolicy 设为 ClusterFirstWithHostNet; Service 类型设为 ClusterIP,默认是 LoadBalancer:controller.service.type 设为 ClusterIP; 默认后端镜像使用 docker hub 提供的镜像,Google 国内无法访问; 修改后的 values 文件:https://raw.githubusercontent.com/qhh0205/helm-charts/master/nginx-ingress-values.ymlhelm 部署1helm install stable/nginx-ingress --name nginx-ingress -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/nginx-ingress-values.yml 验证部署是否成功1234[root@master ~]# kubectl get podNAME READY STATUS RESTARTS AGEnginx-ingress-controller-mg8df 1/1 Running 2 2m14snginx-ingress-default-backend-577857cd9c-gfsnd 1/1 Running 0 2m14s 浏览器访问节点 ip 出现:default backend - 404 页面,部署成功。 至此 Nginx Ingress Controller 已部署完成,接下来讲解如何通过 Ingress 结合 Ingress Controller 实现集群服务对外访问。 使用 Ingress 对外暴露服务为了快速体验 Ingress,下面部署一个 nginx 服务,然后通过 Ingress 对外暴露 nginx service 进行访问。首先部署 nginx 服务:Deployment + Service:nginx.yml123456789101112131415161718192021222324252627apiVersion: extensions/v1beta1kind: Deploymentmetadata: name: nginx-deploymentspec: replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80---kind: ServiceapiVersion: v1metadata: name: nginxspec: selector: app: nginx ports: - port: 80 targetPort: 80 kubectl create -f nginx.yml 接下来创建 Ingress 对外暴露 nginx service 80 端口:ingress.yml:12345678910111213141516apiVersion: extensions/v1beta1kind: Ingressmetadata: name: ingress-nginx annotations: # use the shared ingress-nginx kubernetes.io/ingress.class: "nginx"spec: rules: - host: nginx.kube.com http: paths: - path: / backend: serviceName: nginx servicePort: 80 说明: kubernetes.io/ingress.class: "nginx":Nginx Ingress Controller 根据该注解自动发现 Ingress; host: nginx.kube.com:对外访问的域名; serviceName: nginx:对外暴露的 Service 名称; servicePort: 80:nginx service 监听的端口; 注意:创建的 Ingress 必须要和对外暴露的 Service 在同一命名空间下! 将域名 nginx.kube.com 绑定到 k8s 任意节点 ip 即可访问:http://nginx.kube.com 上面的示例不支持 https 访问,下面举一个支持 https 的 Ingress 例子:通过 Ingress 访问 kubernetes dashboard 服务。 通过 Ingress 访问 kubernetes dashboard(支持 HTTPS 访问)之前我们使用 helm 以 nodePort 的方式部署了 kubernetes dashboard:「helm 部署 kubernetes-dashboard」,从集群外部只能通过 nodeIP:nodePort 端口号 访问,接下来基于之前部署的 kubernetes-dashboard 配置如何通过 Ingress 访问,并且支持 HTTPS 访问,HTTP 自动跳转到 HTTPS。 :首先,练习使用,先用自签名证书来代替吧:1openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout kube-dashboard.key -out kube-dashboard.crt -subj "/CN=dashboard.kube.com/O=dashboard.kube.com" 使用生成的证书创建 k8s Secret 资源,下一步创建的 Ingress 会引用这个 Secret:1kubectl create secret tls kube-dasboard-ssl --key kube-dashboard.key --cert kube-dashboard.crt -n kube-system 创建 Ingress 资源对象(支持 HTTPS 访问):kube-dashboard-ingress.yml123456789101112131415161718192021apiVersion: extensions/v1beta1kind: Ingressmetadata: name: ingress-kube-dashboard annotations: # use the shared ingress-nginx kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"spec: tls: - hosts: - dashboard.kube.com secretName: kube-dasboard-ssl rules: - host: dashboard.kube.com http: paths: - path: / backend: serviceName: kubernetes-dashboard servicePort: 443 kubectl create -f kube-dashboard-ingress.yml -n kube-system说明: kubernetes.io/ingress.class: "nginx":Inginx Ingress Controller 根据该注解自动发现 Ingress; nginx.ingress.kubernetes.io/backend-protocol: Controller 向后端 Service 转发时使用 HTTPS 协议,这个注解必须添加,否则访问会报错,可以看到 Ingress Controller 报错日志:kubectl logs -f nginx-ingress-controller-mg8df 2019/08/12 06:40:00 [error] 557#557: *56049 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: 192.168.26.10, server: dashboard.kube.com, request: “GET / HTTP/1.1”, upstream: “http://10.244.1.8:8443/“, host: “dashboard.kube.com” 报错原因主要是 dashboard 服务后端只支持 https,但是 Ingress Controller 接到客户端的请求时往后端 dashboard 服务转发时使用的是 http 协议,解决办法就是给 创建的 Ingress 设置:nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" 注解。解决方法参考自 StackOverflow:https://stackoverflow.com/questions/48324760/ingress-configuration-for-dashboard secretName: kube-dasboard-ssl:https 证书 Secret; host: dashboard.kube.com:对外访问的域名; serviceName: kubernetes-dashboard:集群对外暴露的 Service 名称; servicePort: 443:service 监听的端口; 注意:创建的 Ingress 必须要和对外暴露的 Service 在同一命名空间下! 将域名 dashboard.kube.com 绑定到 k8s 任意节点 ip 即可访问:https://dashboard.kube.com 相关文档https://kubernetes.io/docs/concepts/services-networking/ingress/ | 官方文档https://mritd.me/2017/03/04/how-to-use-nginx-ingress/ | Kubernetes Nginx Ingress 教程https://github.com/nginxinc/kubernetes-ingress | Inginx Ingress Controller:nginx 社区提供https://github.com/kubernetes/ingress-nginx | Inginx Ingress Controller:k8s 社区提供https://github.com/nginxinc/kubernetes-ingress/blob/master/docs/nginx-ingress-controllers.md | 两种基于 nginx 的 Ingress Controller 区别https://kubernetes.github.io/ingress-nginx/deploy/ | Inginx Ingress Controller k8s 社区版安装文档https://kubernetes.github.io/ingress-nginx/deploy/baremetal/ | 在 裸机环境下 Inginx Ingress Controller 对外暴露方案]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubectl 多集群访问配置]]></title>
<url>%2F2019%2F08%2F09%2Fkubectl-%E5%A4%9A%E9%9B%86%E7%BE%A4%E8%AE%BF%E9%97%AE%E9%85%8D%E7%BD%AE%2F</url>
<content type="text"><![CDATA[配置 KUBECONFIG 环境变量,是 kubectl 工具支持的变量,变量内容是冒号分隔的 kubernetes config 认证文件路径。假如我们有两个集群:A 和 B,A 集群的 config 文件为:$HOME/.kube/config,B 集群的 config 文件为:$HOME/.kube/config-local。要配置 kubectl 随时在两个集群间切换,只需要设置 KUBECONFIG 环境变量为:$HOME/.kube/config:$HOME/.kube/config-local1export KUBECONFIG=$HOME/.kube/config:$HOME/.kube/config-local 当进行上面配置后,使用 kubectl config view 查看 kubectl 配置时,结果为两个文件的合并。当需要切换集群时,使用 kubectl config use-context <context 名称> 参考文档:https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mac OS 启用 ssh 远程登陆]]></title>
<url>%2F2019%2F08%2F08%2FMac-OS-%E5%90%AF%E7%94%A8-ssh-%E8%BF%9C%E7%A8%8B%E7%99%BB%E9%99%86%2F</url>
<content type="text"><![CDATA[检查 ssh 远程登陆是否启用1sudo systemsetup -getremotelogin 启用 ssh 远程登陆1sudo systemsetup -setremotelogin on 启用后就可以用 ssh 来登陆 mac 系统了,账号和密码为系统的账号密码。 关闭 ssh 远程登陆1sudo systemsetup -f -setremotelogin off]]></content>
<categories>
<category>Mac</category>
</categories>
<tags>
<tag>Mac</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vagrant 多网卡环境下 flannel 网络插件导致 DNS 无法解析]]></title>
<url>%2F2019%2F08%2F08%2FVagrant-%E5%A4%9A%E7%BD%91%E5%8D%A1%E7%8E%AF%E5%A2%83%E4%B8%8B-flannel-%E7%BD%91%E7%BB%9C%E6%8F%92%E4%BB%B6%E5%AF%BC%E8%87%B4-DNS-%E6%97%A0%E6%B3%95%E8%A7%A3%E6%9E%90%2F</url>
<content type="text"><![CDATA[之前写过一篇 k8s 集群自动化部署的文章:「Kubeadm 结合 Vagrant 自动化部署最新版 Kubernetes 集群」,发现集群启动后 DNS 无法解析,公网和集群内部都无法解析,具体问题表现是:进入 pod 执行 ping service 名称或者公网域名都是无法解析 Unknow host。 经过网上搜索一番找到了问题并得以解决,主要原因是 Vagrant 在多主机模式下有多个网卡,eth0 网卡用于 nat 转发访问公网,而 eth1 网卡才是主机真正的 IP,在这种情况下直接部署 k8s flannel 插件会导致 CoreDNS 无法工作。解决方法很简单,调整下 flannel 的启动参数,加上 - --iface=eth1 网卡参数:vim https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml- --kube-subnet-mgr 后面加一行参数:- --iface=eth11234567891011......containers: - name: kube-flannel image: quay.io/coreos/flannel:v0.11.0-amd64 command: - /opt/bin/flanneld args: - --ip-masq - --kube-subnet-mgr - --iface=eth1...... 参考文档:https://blog.frognew.com/2019/07/kubeadm-install-kubernetes-1.15.html#2-3-%E5%AE%89%E8%A3%85pod-network | 使用kubeadm安装Kubernetes 1.1https://github.com/kubernetes/kubeadm/issues/1056 | github issue 讨论]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[helm 部署 kubernetes-dashboard]]></title>
<url>%2F2019%2F08%2F08%2Fhelm-%E9%83%A8%E7%BD%B2-kubernetes-dashboard%2F</url>
<content type="text"><![CDATA[kubernetes-dashboard 是 k8s 官方提供的集群 Web UI,可以查看集群详细的信息,比如集群的 api 资源,pod 日志,工作负载,节点资源利用率等等。在部署 kubernetes-dashboard 前需要先安装 heapster ,heapster 用于收集数据,而 dashboard 是展示数据的界面。关于 heapster 的安装见之前文章「helm 部署 heapster 组件」。如果没有部署 heapster 组件,kubernetes-dashboard 的 pod 会报错: 2019/08/06 10:30:06 Metric client health check failed: the server could not find the requested resource (get services heapster). Retrying in 30 seconds. 使用官方提供的 Chart:https://github.com/helm/charts/tree/master/stable/kubernetes-dashboard对 values 文件进行一些定制: docker 镜像地址改为阿里云镜像地址,国内访问不了默认的镜像地址; service 类型改为 NodePort,并指定 nodePort 端口为 30000; 定制后的 values 文件:https://raw.githubusercontent.com/qhh0205/helm-charts/master/kube-component-values/kube-dashboard.yml helm 部署:1helm install stable/kubernetes-dashboard --name kubernetes-dashboard -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/kube-component-values/kube-dashboard.yml --namespace kube-system 创建集群服务账号:admin1kubectl create -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/some-apiserver-rs/admin-sa.yml 获取 dashboard 访问 token:1kubectl get secret `kubectl get secret -n kube-system | grep admin-token | awk '{print $1}'` -o jsonpath={.data.token} -n kube-system | base64 -d]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[helm 部署 heapster 组件]]></title>
<url>%2F2019%2F08%2F08%2Fhelm-%E9%83%A8%E7%BD%B2-heapster-%E7%BB%84%E4%BB%B6%2F</url>
<content type="text"><![CDATA[之前工作用的 k8s 集群(GKE)都是支持 kubectl top node 查看节点资源使用情况的,最近自己本地新搭的集群发现用不了该命令。网上搜索了下发现是由于缺少集群指标收集组件导致,目前常用的集群指标收集组件是 heapster 和 metrics-server,看官方介绍 heapster 要逐渐被淘汰了,更推荐 metrics-server。但是为了适配后续要安装的 kubernetes-dashboard,先使用 heapster 组件。 heapster 组件是 Kubernetes 官方支持的容器集群监控组件,主要用于收集集群指标数据并存储,收集的数据可以对接可视化图表展示分析,比如 grafana、kubernetes-dashboard。 除了 kubectl top node 依赖于 heapster 收集的指标,kubernetes-dashboard 也需要 heapster,本文使用 helm 来一键部署 heapster 组件。 heapster Chart 使用 helm 官方提供的:https://github.com/helm/charts/tree/master/stable/heapster对 values 文件做一些定制: 使用阿里云镜像仓库,Chart 默认使用 k8s.gcr.io,国内是拉不下来镜像的; rbac 服务账号使用 admin,使用默认的 default 账号权限太小,收集指标时报错。 heapster 启动参数 –source 调整成:kubernetes:https://kubernetes.default:443?useServiceAccount=true&kubeletHttps=true&kubeletPort=10250&insecure=true,否则 heapster 会报错: E0806 22:11:05.017143 1 manager.go:101] Error in scraping containers from kubelet_summary:192.168.26.10:10255: Get http://192.168.26.10:10255/stats/summary/: dial tcp 192.168.26.10:10255: getsockopt: connection refused 报错主要原因是 k8s 1.12.0 以后已经取消了 kubelet 10255 端口: 12345 --read-only-port int32 The read-only port for the Kubelet to serve on with no authentication/authorization (set to 0 to disable) (default 10255) (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.) 详情见:https://sealyun.com/post/heapster-error/ 定制后的 values 文件在这里:https://raw.githubusercontent.com/qhh0205/helm-charts/master/kube-component-values/heapster-values.yml 创建集群服务账号:admin,heapster 使用 admin 服务账号,确保有足够权限1kubectl create -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/some-apiserver-rs/admin-sa.yml 接下来使用 helm 安装部署 heapster:1helm install stable/heapster --name heapster -f https://raw.githubusercontent.com/qhh0205/helm-charts/master/kube-component-values/heapster-values.yml --namespace kube-system heapster 安装完后就可以正常使用 kubectl top node 了:1234[root@master ~]# kubectl top nodeNAME CPU(cores) CPU% MEMORY(bytes) MEMORY%master 141m 7% 1096Mi 63%node1 45m 2% 821Mi 47% 如果 heapster pod 有问题一般会报下面错误:12[root@master ~]# kubectl top nodeerror: metrics not available yet]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Helm 安装使用]]></title>
<url>%2F2019%2F08%2F08%2FHelm-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[其实 Helm 的安装很简单,之所以单独写这篇文章主要是因为国内网络原因导致 helm 使用存在障碍(防火墙对 google 不友好),本文重点说如何解决这一问题。 helm 安装官方提供了一件安装脚本,安装最新版:https://helm.sh/docs/using_helm/#installing-helm1curl -L https://git.io/get_helm.sh | bash 创建服务账号和角色绑定rbac-config.yaml:123456789101112131415161718apiVersion: v1kind: ServiceAccountmetadata: name: tiller namespace: kube-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: tillerroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-adminsubjects: - kind: ServiceAccount name: tiller namespace: kube-system 1kubectl create -f rbac-config.yaml helm init 初始化国内无法访问 gcr.io 仓库,指定阿里云镜像仓库,同时指定前面创建的服务账号:1helm init --service-account tiller -i registry.aliyuncs.com/google_containers/tiller:v2.14.3 国内也无法访问 helm 默认的 Chart 仓库,所以也改成阿里云 Chart 镜像仓库:12helm repo remove stablehelm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Kubeadm 结合 Vagrant 自动化部署最新版 Kubernetes 集群]]></title>
<url>%2F2019%2F08%2F06%2FKubeadm-%E7%BB%93%E5%90%88-Vagrant-%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E6%9C%80%E6%96%B0%E7%89%88-Kubernetes-%E9%9B%86%E7%BE%A4%2F</url>
<content type="text"><![CDATA[之前写过一篇搭建 k8s 集群的教程:「使用 kubeadm 搭建 kubernetes 集群」,教程中用到了 kubeadm 和 vagrant,但是整个过程还是手动一步一步完成:创建节点--> 节点配置、相关软件安装 --> 初始化 master 节点 --> node 节点加入 master 节点。其实这个过程完全可以通过 Vagrant 的配置器自动化来实现,达到的目的是启动一个 k8s 只需在 Vagrant 工程目录执行:vagrant up 即可一键完成集群的创建。 本文主要介绍如何使用 Kubeadm 结合 Vagrant 自动化 k8s 集群的创建,在了解了 kubeadm 手动搭建 kubernetes 集群的过程后,自动化就简单了,如果不了解请参见之前的文章:「使用 kubeadm 搭建 kubernetes 集群」,梳理下整个过程,在此不做过多介绍。下面大概介绍下自动化流程: 首先抽象出来每个节点需要执行的通用脚本,完成一些常用软件的安装、docker 安装、kubectl 和 kubeadm 安装,还有一些节点的系统级配置:具体实现脚本见:https://github.com/qhh0205/kubeadm-vagrant/blob/master/install-centos.sh 编写 Vagrantfile,完成主节点的初始化安装和 node 节点加入主节点。但是有个地方和之前手动安装不太一样,为了自动化,我们必须在 kubeadm 初始化 master 节点之前生成 TOKEN(使用其他任意主机的 kubeadm 工具生成 TOKEN 即可),然后自动化脚本统一用这个 TOKEN 初始化主节点和从节点加入。Vagrantfile 具体实现见:https://github.com/qhh0205/kubeadm-vagrant/blob/master/Vagrantfile 完整的 Vagrant 工程在这里:https://github.com/qhh0205/kubeadm-vagrant使用 kubeadm + vagrant 自动化部署 k8s 集群,基于 Centos7 操作系统。该工程 fork 自 kubeadm-vagrant, 对已知问题进行了修复:节点设置正确的 IP 地址「set-k8s-node-ip.sh」。否则使用过程中会出现问题,具体问题见这里:「kubeadm + vagrant 部署多节点 k8s 的一个坑」。其他一些调整:节点初始化脚本更改、Vagrantfile 添加 Shell 脚本配置器,运行初始化脚本。 默认:1 个 master 节点,1 个 node 节点,可以根据需要修改 Vagrantfile 文件,具体见工程 README.md 说明。]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubeadm + vagrant 部署多节点 k8s 的一个坑]]></title>
<url>%2F2019%2F08%2F06%2Fkubeadm-vagrant-%E9%83%A8%E7%BD%B2%E5%A4%9A%E8%8A%82%E7%82%B9-k8s-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%9D%91%2F</url>
<content type="text"><![CDATA[之前写过一篇「使用 kubeadm 搭建 kubernetes 集群」教程,教程里面使用 Vagrant 启动 3 个节点,1 个 master,2 个 node 节点,后来使用过程中才慢慢发现还是存在问题的。具体问题表现是: kubectl get node -o wide 查看到节点 IP 都是:10.0.2.15; 1234[root@master ~]# kubectl get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIMEmaster Ready master 5m11s v1.15.1 10.0.2.15 <none> CentOS Linux 7 (Core) 3.10.0-862.2.3.el7.x86_64 docker://19.3.1node1 Ready <none> 2m31s v1.15.1 10.0.2.15 <none> CentOS Linux 7 (Core) 3.10.0-862.2.3.el7.x86_64 docker://19.3.1 kubect get pod 可以查看 pod,pod 也运行正常,但是无法查看 pod 日志,也无法 kubectl exec -it 进入 pod。具体报错如下: 12345678910[root@master ~]# kubectl get podNAME READY STATUS RESTARTS AGEnginx-deployment-5754944d6c-gnqjg 1/1 Running 0 66snginx-deployment-5754944d6c-mxgn7 1/1 Running 0 66s[root@master ~]# kubectl logs nginx-deployment-5754944d6c-gnqjgError from server (NotFound): the server could not find the requested resource ( pods/log nginx-deployment-5754944d6c-gnqjg)[root@master ~]# kubectl exec -it nginx-deployment-5754944d6c-gnqjg sherror: unable to upgrade connection: pod does not exist 带着上面两个问题,于是网上搜索一番,找到了根因并得以解决:https://github.com/kubernetes/kubernetes/issues/60835 主要原因Vagrant 在多主机模式时每个主机的 eth0 网口 ip 都是 10.0.2.15,这个网口是所有主机访问公网的出口,用于 nat 转发。而 eth1才是主机真正的 IP。kubelet 在启动时默认读取的是 eth0 网卡的 IP,因此在集群部署完后 kubect get node -o wide 查看到节点的 IP 都是 10.0.2.15。123456789101112131415[vagrant@master ~]$ ip addr...2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:54:00:c9:c7:04 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0 valid_lft 85708sec preferred_lft 85708sec inet6 fe80::5054:ff:fec9:c704/64 scope link valid_lft forever preferred_lft forever3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:25:1b:45 brd ff:ff:ff:ff:ff:ff inet 192.168.26.10/24 brd 192.168.26.255 scope global noprefixroute eth1 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fe25:1b45/64 scope link valid_lft forever preferred_lft forever... 解决方法上面知道问题的根本原因是 k8s 节点 IP 获取不对导致访问节点出现问题,那么解决方法就是调整 kubelet 参数设置正确的IP 地址:编辑 /etc/sysconfig/kubelet 文件,KUBELET_EXTRA_ARGS 环境变量添加 --node-ip 参数:1KUBELET_EXTRA_ARGS="--node-ip=<eth1 网口 IP>" 然后重启 kubelet:systemctl restart kubelet执行 kubectl get node -o wide 发现节点 IP 已经改变成了KUBELET_EXTRA_ARGS 变量指定的 IP。1234[root@master ~]# kubectl get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIMEmaster Ready master 24m v1.15.1 192.168.26.10 <none> CentOS Linux 7 (Core) 3.10.0-862.2.3.el7.x86_64 docker://19.3.1node1 Ready <none> 21m v1.15.1 10.0.2.15 <none> CentOS Linux 7 (Core) 3.10.0-862.2.3.el7.x86_64 docker://19.3.1 用同样的方法修改其他节点 IP 即可。为了方便,这里提供了一个命令,自动化上面步骤:12echo KUBELET_EXTRA_ARGS=\"--node-ip=`ip addr show eth1 | grep inet | grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}/" | tr -d '/'`\" > /etc/sysconfig/kubeletsystemctl restart kubelet]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Jenkins 集成 allure 测试报告工具]]></title>
<url>%2F2019%2F07%2F25%2FJenkins-%E9%9B%86%E6%88%90-allure-%E6%B5%8B%E8%AF%95%E6%8A%A5%E5%91%8A%E5%B7%A5%E5%85%B7%2F</url>
<content type="text"><![CDATA[allure 基于已有的测试报告数据进行进一步的加工,美化等操作,相当于做了一次数据格式转换。allure 支持多种语言的多种测试框架,比如 Java 的 jUnit4、jUnit5、TestNg 等等。 本文主要介绍如何在 Jenkins 中集成 allure 测试报表工具,在每次项目自动化测试完成后,用 allure 生成经过加工后的测试报告。我们以 java 工程的 TestNg 测试为例,处理 TestNg 生成的测试报告。 Jenkins 安装 allure 插件全局工具配置: Jenkinsfile 添加 allure 代码 123script { allure jdk: '', report: "target/allure-report-unit", results: [[path: "target/surefire-reports"]]} target/allure-report-unit 参数:allure 报告生成路径; target/surefire-reports 参数:测试报告原始路径; Jenkins 平台查看 allure 报告在 allure 成功集成到 Jenkins 后,allure 每次处理完成,在 Jenkins job 页面都可以看到 allure 的图标,点击图标即可查看报告详细信息: 遇到问题及解决方法问题:allure 在 Jenkins pipline 中生成报表时报目录权限问题:java.nio.file.AccessDeniedException 原因:jenkins k8s pod 执行 job 时默认用户为 jenkins,但是 pipeline 中调用的容器生成的文件的属主是 root。解决方法:配置 jenkins k8s 插件模版,添加安全配置,运行用户设置为 roothttps://groups.google.com/forum/#!topic/jenkinsci-users/GR0n8ZkCJ-E pod 配置(spec 下):123securityContext: runAsUser: 0 fsGroup: 0 参考文档https://docs.qameta.io/allure/]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>DevOps</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Go 开发命令行应用]]></title>
<url>%2F2019%2F07%2F24%2F%E4%BD%BF%E7%94%A8-Go-%E5%BC%80%E5%8F%91%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%BA%94%E7%94%A8%2F</url>
<content type="text"><![CDATA[作为一个程序员,命令行工具是我们再熟悉不过的了,我们每天或多或少都会用到命令行工具。比如项目构建、打包、启动等等。那么如何用 Go 语言编写类似的工具呢?调研了下,大概有下面三种方法: os.Args 函数os.Args 功能类似于 Shell 脚本的 $@ 功能,获取到命令行输入,然后进行人工解析处理,这种方式对于编写简单的工具还行,对于复杂点的工具,光解析输入参数就是一场”灾难”了。 使用 Go 标准 flag 包flag 包是 Golang 官方提供的命令行参数解析包,省去人工解析工作,对于构建一个一般功能的命令行应用足够了,但是对于复杂应用还是显得比较麻烦。 使用第三方包 cli 或者 cobra使用第三方包就比较专业了,提供的功能更加丰富,使用起来也很顺手。目前对于 Golang 命令行应用开发业界比较主流的就是 cli 和 cobra 了。其实两者的流行度差不多,cobra 的学习成本稍微高点,而且更加专业,看文档介绍,Kubernetes、Moby、rkt、etcd 等都是基于 cobra 构建。我个人更推荐使用 cli,因为 cli 比较轻量级、容易学习,使用起来也更加得心应手,对于开发日常应用足够了。 本文主要介绍后面两种方式,即 flag 包和第三方包 cli 的使用,对于 cobra 的使用这里不做具体介绍。 使用 flag 包构建命令行应用 flag 包的使用很简单,能很方便地解析命令行输入,支持的命令行参数类型有 bool, int, uint, string, time.Duration, float 类型,另外还可以自定义类型。这里介绍下一般的使用方法,假如要开发一个命令行工具,使用方式:1./go-curl -v -X "GET" https://example.com 具体实现代码:123456789101112131415161718func main() { // 定义一个 bool 类型的参数,默认值为 false,第三个参数为 Usage 说明 // 函数返回值为对应类型的指针 v := flag.Bool("v", false, "Makes curl verbose during the operation.") // 定义一个 String 类型的参数,默认值为 GET,第三个参数为 Usage 说明 // 与上面那种不同的是函数第一个参数为变量的指针 var X string flag.StringVar(&X, "X", "GET", "(HTTP) Specifies a custom request method to use when communicating with the HTTP server.") // 在参数定义完后必须调用 flag.Parse() 完成命令行参数的解析 flag.Parse() // 返回其余参数的列表 args := flag.Args() fmt.Printf("v: %t, X: %s, args: %v\n", *v, X, args) os.Exit(0)} 可以看出定义参数有两种方式: 指针类型参数:调用函数为 flag.Type 形式,返回值为对应类型的指针; 值类型参数:调用函数为 flag.TypeVar 形式,调用时传递变量的指针; 两种方法效果都是一样的,只不过一种解析返回的是指针,另一种是直接使用变量。 使用 cli 包构建命令行应用使用 cli 包开发命令行工具能省很多事,而且写出来的代码结构非常清晰,很容易理解。具体使用见代码仓库 README.md 。在这里举一个例子,我最近写的服务部署命令行工具:https://github.com/qhh0205/deploy-kit 通过看官方文档结合这个例子能很容易掌握 cli 包的是使用技巧。下面为该部署工具的 --help 输出:1234567891011121314151617181920NAME: deploy - deploy applicationUSAGE: deploy [global options] command [command options] [arguments...]VERSION: v1.0COMMANDS: list, ls list all of services app deploy microservice application web deploy web application lsbranch, lsb list the code branches of service upload-cdn, upcdn upload file or directory to gcs bucket help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --help, -h show help --version, -v print the version 该工具基于 cli 包构建,cli 包的使用核心是通过 app.Commands = []cli.Command{} 定义一些列命令、选项,并且通过 Action 绑定对应选项参数的处理函数。 参考文档https://github.com/urfave/cli | cli GitHubhttps://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter13/13.1.html | flag 包介绍https://blog.yumaojun.net/2016/12/30/go-cobra/ | cobra 使用,如何使用golang编写漂亮的命令行工具https://blog.rapid7.com/2016/08/04/build-a-simple-cli-tool-with-golang/ | Building a Simple CLI Tool with Golanghttps://medium.com/what-i-talk-about-when-i-talk-about-technology/dealing-with-command-line-options-in-golang-flag-package-e5fb6ef1a79e | Dealing with Command Line Options in Golang: flag package]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang 协程顺序打印]]></title>
<url>%2F2019%2F07%2F23%2FGolang-%E5%8D%8F%E7%A8%8B%E9%A1%BA%E5%BA%8F%E6%89%93%E5%8D%B0%2F</url>
<content type="text"><![CDATA[A、B 两个协程分别打印 1、2、3、4 和 A,B,C,D实现:定义 A、B 两个 channal,开 A、B 两个协程,A 协程输出[1, 2, 3, 4]、B 协程输出[A, B, C, D],通过两个独立的 channal 控制顺序,交替输出。12345678910111213141516171819202122232425262728func main() { A := make(chan bool, 1) B := make(chan bool) Exit := make(chan bool) go func() { s := []int{1, 2, 3, 4} for i := 0; i < len(s); i++ { if ok := <-A; ok { fmt.Println("A: ", s[i]) B <- true } } }() go func() { defer func() { close(Exit) }() s := []byte{'A', 'B', 'C', 'D'} for i := 0; i < len(s); i++ { if ok := <-B; ok { fmt.Printf("B: %c\n", s[i]) A <- true } } }() A <- true <-Exit} A、B 两个协程顺序打印 1~20实现:与上面基本一样,定义 A、B 两个 channal,开 A、B 两个协程,A 协程输出奇数、B 协程输出偶数,通过两个独立的 channal 控制顺序,交替输出。1234567891011121314151617181920212223242526272829303132package mainimport "fmt"func main() { A := make(chan bool, 1) B := make(chan bool) Exit := make(chan bool) go func() { for i := 1; i <= 10; i++ { if ok := <-A; ok { fmt.Println("A = ", 2*i-1) B <- true } } }() go func() { defer func() { close(Exit) }() for i := 1; i <= 10; i++ { if ok := <-B; ok { fmt.Println("B : ", 2*i) A <- true } } }() A <- true <-Exit}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go 语言 exec 实时获取外部命令的执行输出]]></title>
<url>%2F2019%2F07%2F20%2FGo-%E8%AF%AD%E8%A8%80-exec-%E5%AE%9E%E6%97%B6%E8%8E%B7%E5%8F%96%E5%A4%96%E9%83%A8%E5%91%BD%E4%BB%A4%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BE%93%E5%87%BA%2F</url>
<content type="text"><![CDATA[在 Go 语言中调用外部 Linux 命令可以通过标准的 os/exec 包实现,我们一般的使用方式如下:123456789101112package mainimport ( "fmt" "os/exec")func main() { cmd := exec.Command("ls", "-al") output, _ := cmd.CombinedOutput() fmt.Println(string(output))} 上面这种使用方式虽然能获取到外部命令的执行结果输出 output,但是必须得命令执行完成后才将获取到的结果一次性返回。很多时候我们是需要实时知道命令执行的输出,比如我们调用一个外部的服务构建命令: mvn build,这种情况下实时输出命令执行的结果对我们来说很重要。再比如我们 ping 远程 IP,需要知道实时输出,如果直接 ping 完在输出,在使用上来说体验不好。 要实现外部命令执行结果的实时输出,需要使用 Cmd 结构的 StdoutPipe() 方法创建一个管道连接到命令执行的输出,然后用 for 循环从管道中实时读取命令执行的输出并打印到终端。具体代码如下:12345678910111213141516171819202122232425262728func RunCommand(name string, arg ...string) error { cmd := exec.Command(name, arg...) // 命令的错误输出和标准输出都连接到同一个管道 stdout, err := cmd.StdoutPipe() cmd.Stderr = cmd.Stdout if err != nil { return err } if err = cmd.Start(); err != nil { return err } // 从管道中实时获取输出并打印到终端 for { tmp := make([]byte, 1024) _, err := stdout.Read(tmp) fmt.Print(string(tmp)) if err != nil { break } } if err = cmd.Wait(); err != nil { return err } return nil}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go 语言读写文件]]></title>
<url>%2F2019%2F07%2F15%2FGo-%E8%AF%AD%E8%A8%80%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[在这里演示下如何通过 Go 读写文件,Go 读写文件有很 IO 多函数可以使用,在这里使用 os 包的 OpenFile 和 Open 函数打开文件,然后用 bufio 包带缓冲的读写器读写文件。查看 OpenFile 源码,其实 Open 函数底层还是调用了 OpenFile。123456789101112131415161718192021222324252627282930313233343536373839package mainimport ( "bufio" "fmt" "io" "os")func main() { // 写文件 outputFile, outputError := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if outputError != nil { fmt.Println(outputError) return } defer outputFile.Close() outputWriter := bufio.NewWriter(outputFile) for i := 0; i < 10; i++ { outputWriter.WriteString("hello, world\n") } // 一定得记得将缓冲区内容刷新到磁盘文件 outputWriter.Flush() // 读文件 inputFile, inputError := os.Open("file.txt") if inputError != nil { fmt.Println(inputError) return } defer inputFile.Close() inputReader := bufio.NewReader(inputFile) for { inputString, readerError := inputReader.ReadString('\n') fmt.Printf(inputString) if readerError == io.EOF { return } }}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自动化测试报表统一平台 ReportPortal 集成 TestNG]]></title>
<url>%2F2019%2F07%2F10%2F%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95%E6%8A%A5%E8%A1%A8%E7%BB%9F%E4%B8%80%E5%B9%B3%E5%8F%B0-ReportPortal-%E9%9B%86%E6%88%90-TestNG%2F</url>
<content type="text"><![CDATA[本文主要介绍 ReportPortal 如何集成 TestNG 测试框架,用到的工具链有:ReportPortal + TestNG + log4j。ReportPortal 集成 TestNG 的主要原理是通过给 TestNG 配置 ReportPortal 的 listener,在测试开始时该监听器将测试信息实时上报给 ReportPortal 平台。另外我们通过给 log4j 配置 ReportPortal appender,将测试过程中代码日志也上报到 ReportPortal 平台。然后 ReportPortal 将收到的数据进行整合、分析,形成平台数据统一展示。 ReportPortal 简介ReportPortal 是一个统一的自动化测试报告收集、分析、可视化平台,可以集成多种测试框架,比如 TestNG、Selenium 等等。ReportPortal 的主要特性有: 能轻易和多种测试框架集成; 实时展示测试情况; 所有的自动化测试结果在一个地方统一查看; 保留历史测试信息; 能和 bug 跟踪系统集成,比如 Jira; ReportPortal 解决了什么问题个人认为 ReportPortal 最大的价值在于报表的统一收集、查看、分析。假如没有 ReportPortal 工具,我们可能需要自己写脚本,或者 Jenkins 插件针对不同的测试框架装不同的插件,然后展示测试报告,但是 Jenkins 收集的测试报告只能在 Jenkins 平台查看。微服务拆分细、导致 Jenkins job 数量比较多,要看每次测试的报告要逐个点开进去查看,没有一个全局的地方查看。另外 Jenkins 本身的插件生态提供的测试报告收集不支持对历史测试报告的统一查询,如果有这种需求,基本不能满足。 ReportPortal 基本是全测试框架支持的统一报表收集、分析、可视化平台,能轻松解决上述存在的痛点。 ReportPortal 在 CI/CD 中扮演了什么角色CI/CD 我们已经很熟悉了,但是如何将 CI/CD 与 CT 无缝整合,也许 ReportPortal 在 CI/CD 与 CT 的整合中扮演了重要角色。DevOps 的关键在于自动化统一标准、流程,根据 ReportPortal 的特性及本人的试用,发现 ReportPortal 真是对 CI/CD 完美的补充,整个交付流水线更加统一、规范、简洁、无缝衔接。 ReportPortal + TestNG + log4j 集成详细步骤以一个基于 TestNG 测试框架的 java 工程为例说明,配置前 java 工程目录结构:12345678910111213141516171819202122.├── pom.xml├── README.md├── run.sh└── src ├── main │ ├── java │ │ └── com │ └── resources │ ├── config.properties │ ├── dev.yml │ ├── log4j.properties │ ├── log4testng.properties │ ├── production.yml │ ├── stage.yml │ ├── test.yml │ └── web.yml └── test ├── java │ └── com └── resources └── testng.xml 1. 配置工程 pom 文件1.1 配置 ReportPortal 相关依赖远程仓库12345678910<repositories> <repository> <id>bintray</id> <url>http://dl.bintray.com/epam/reportportal</url> </repository> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository></repositories> 1.2 添加一些依赖配置testng 依赖12345<dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.11</version></dependency> ReportPortal agent 的 testng 实现12345<dependency> <groupId>com.epam.reportportal</groupId> <artifactId>agent-java-testng</artifactId> <version>4.2.1</version></dependency> 添加 Rport Portal 的 log 包装以及 log4j 本身的配置123456789101112131415<dependency> <groupId>com.epam.reportportal</groupId> <artifactId>logger-java-log4j</artifactId> <version>4.0.1</version></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.26</version></dependency><dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version></dependency> 1.3 maven-surefire-plugin 插件配置说明:src/test/resources/testng.xml:tesng 执行测试用例时读取的文件,该文件指定执行哪些测试用例等信息;1234567891011121314151617<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.22.0</version><configuration> <testFailureIgnore>true</testFailureIgnore> <suiteXmlFiles> <suiteXmlFilexmlFile>src/test/resources/testng.xml</suiteXmlFilexmlFile> </suiteXmlFiles> <properties> <property> <name>usedefaultlisteners</name> <!-- disabling default listeners is optional --> <value>false</value> </property> </properties></configuration></plugin> 1.4 maven-compiler-plugin 插件配置123456789<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration></plugin> 2. TestNG 配置 RportPortal listenertestng testng.xml 文件添加 RportPortal 的 listener,文件:src/test/resources/testng.xml123<listeners> <listener class-name="com.epam.reportportal.testng.ReportPortalTestNGListener"/></listeners> 3. 工程添加 ReportPortal resourcesrc/test/resources/ 目录添加 ReportPortal 配置文件:reportportal.properties获取 ReportPortal 配置:访问 ReportPortal UI—>点击右上角图标—>点击 Profile—>拷贝右下角框框中 REQUERED 配置。 将上面获取到的 ReportPortal 配置放到 src/test/resources/ 目录下reportportal.properties 文件:示例文件内容:1234rp.endpoint = http://reportIp:8080rp.uuid = xxxxxrp.launch = superadmin_TEST_EXAMPLErp.project = superadmin_personal 4. 配置 log4j 的 ReportPortal appendersrc/test/resources/ 目录添加 log4j.xml 文件,主要是配置 log4j 的 ReportPortal appender,内容如下:123456789101112131415161718<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"><log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="ReportPortalAppender" class="com.epam.ta.reportportal.log4j.appender.ReportPortalAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%d{HH:mm:ss}] %-5p (%F:%L) - %m%n"/> </layout> </appender> <logger name="com.epam.reportportal.apache"> <level value="OFF"/> </logger> <root> <level value="info"/> <appender-ref ref="ReportPortalAppender"/> </root></log4j:configuration> 至此相关配置已完成,工程目录结构此时为:123456789101112131415161718192021222324.├── pom.xml├── README.md├── run.sh└── src ├── main │ ├── java │ │ └── com │ └── resources │ ├── config.properties │ ├── dev.yml │ ├── log4j.properties │ ├── log4testng.properties │ ├── production.yml │ ├── stage.yml │ ├── test.yml │ └── web.yml └── test ├── java │ └── com └── resources ├── log4j.xml ├── reportportal.properties └── testng.xml 5. 执行 mvn clean test 测试6. 到 ReportPortal 控制台观察新的 Launches 是否启动每次测试会在 ReportPortal 平台对应触发一个 Launche,包含本次构建相关信息。 7. 创建 ReportPortal Dashboard,可视化测试报告创建 ReportPortal 的 Dashboard 很简单,也很灵活,主要思想是 RP 提供了多种图表,然后每个图表配置条件,筛选出想要的 Launches 展示。 相关文档https://reportportal.io/docshttps://github.com/reportportal/example-java-TestNG]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>DevOps</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang json 编解码]]></title>
<url>%2F2019%2F07%2F09%2FGolang-json-%E7%BC%96%E8%A7%A3%E7%A0%81%2F</url>
<content type="text"><![CDATA[Json 编码Json 编码的过程即为将程序的数据结构转化为 json 串的过程,比如 Golang 里面的结构体、Python 中的字典,这些有结构的数据转化为 json 串。在 Golang 中编码 Json 使用 encoding/json 包的 Marshal() 函数,函数原型为:1func Marshal(v interface{}) ([]byte, error) 举例:将 Book 结构体对象编码为 json 串,然后输出到控制台123456789101112131415161718192021222324import ( "encoding/json" "fmt")type Book struct { Title string Authors []string Publisher string IsPublished bool Price float64}func main() { var gobook Book = Book{ "Go语言编程", []string{"XuShiwei", "HughLv"}, "ituring.com.cn", true, 9.99, } b, _ := json.Marshal(gobook) fmt.Println(string(b))} Json 解码Json 解码的过程和编码刚好相反,将普通的 json 字符串转化为有结构的程序数据。比如将 json 串转化为 Golang 的结构体。在Golang 中解码 json 的函数为 encoding/json 包的 Unmarshal() 函数,函数原型为:1func Unmarshal(data []byte, v interface{}) error 举例:将 json 字符串解码成 Golang Book 结构体对象,并打印每个字段的值12345678910111213141516171819202122package mainimport ( //"encoding/json" "encoding/json" "fmt")type Book struct { Title string Authors []string Publisher string IsPublished bool Price float64}func main() { jsonStr := `{"Title":"Go语言编程","Authors":["XuShiwei","HughLv"],"Publisher":"ituring.com.cn","IsPublished":true,"Price":9.99}` var book Book json.Unmarshal([]byte(jsonStr), &book) fmt.Println(book.Title, book.Authors, book.Publisher, book.IsPublished, book.Price)}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 iperf 测试 Linux 服务器带宽]]></title>
<url>%2F2019%2F07%2F02%2F%E4%BD%BF%E7%94%A8-iperf-%E6%B5%8B%E8%AF%95-Linux-%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%B8%A6%E5%AE%BD%2F</url>
<content type="text"><![CDATA[iperf 简介iperf 是一个用于测试网络带宽的命令行工具,可以测试服务器的网络吞吐量。目前发现两个很实用的功能: 测试服务器网络吞吐量:如果我们需要知道某台服务器的「最大」网络带宽,那么最好在同区域找两台同等配置的机器测试,因为带宽测试结果和两节点的距离有关、也和运营商的限制有关、也和服务器 CPU 核数有关。 测试到服务端节点网速:如果我们想知道目前客户端到服务器的实际网速是多少,在服务器启动 iperf,客户端连接 iperf 服务端,测试结果就是当前客户端到服务器的真实网速。 工具安装1yum install -y iperf iperf 选项参数通用选项12345678-f <kmKM> 报告输出格式。 [kmKM] format to report: Kbits, Mbits, KBytes, MBytes-i <sec> 在周期性报告带宽之间暂停n秒。如周期是10s,则-i指定为2,则每隔2秒报告一次带宽测试情况,则共计报告5次-p 设置服务端监听的端口,默认是5001-u 使用UDP协议测试-w n<K/M> 指定TCP窗口大小-m 输出MTU大小-M 设置MTU大小-o <filename> 结果输出至文件 服务端选项123-s iperf服务器模式-d 以后台模式运行服务端-U 运行一个单一线程的UDP模式 客户端选项123456-b , --bandwidth n[KM] 指定客户端通过UDP协议发送数据的带宽(bit/s)该参数只对 udp 测试有效。默认是1Mbit/s-c <ServerIP> 以客户端模式运行iperf,并且连接至服务端主机ServerIP。 eg: iperf -c <server_ip>-d 双向测试-t 指定iperf带宽测试时间,默认是10s。 eg: iperf -c <server_ip> -t 20-P 指定客户端并发线程数,默认只运行一个线程。 eg,指定3个线程 : iperf -c <server_ip> -P 3-T 指定TTL值 使用方法示例准备两台服务器 A 和 B,并分别安装 iperf 命令行工具。 测试 A 服务器的出站带宽:在 B 服务器启动 iperf 服务端,A 服务器使用 iperf 连接 B 服务器 iperf 服务端,这样测试的就是 A 服务器的出口带宽: 12B: iperf -s -i 2 # 启动服务端A: iperf -c <B_server_ip> -i 2 -t 60 # 客户端链接 测试 A 服务器的入站带宽:在 A 服务器启动 iperf 服务的,B 服务器使用 iperf 连接 A 服务器 iperf 服务端,这样测试的就是 A 服务器的入口带宽。 12A: iperf -s -i 2 # 启动服务端B: iperf -c <A_server_ip> -i 2 -t 60 # 客户端链接 测试结果示例12345678910111213141516[root@com26-83 ~]# iperf -c x.x.x.x -i 2 -t 60------------------------------------------------------------Client connecting to x.x.x.x, TCP port 5001TCP window size: 22.1 KByte (default)------------------------------------------------------------[ 3] local 10.2.26.83 port 48234 connected with x.x.x.x port 5001[ ID] Interval Transfer Bandwidth[ 3] 0.0- 2.0 sec 147 KBytes 603 Kbits/sec[ 3] 2.0- 4.0 sec 369 KBytes 1.51 Mbits/sec[ 3] 4.0- 6.0 sec 512 KBytes 2.10 Mbits/sec[ 3] 6.0- 8.0 sec 896 KBytes 3.67 Mbits/sec[ 3] 8.0-10.0 sec 1.62 MBytes 6.82 Mbits/sec[ 3] 10.0-12.0 sec 2.12 MBytes 8.91 Mbits/sec[ 3] 12.0-14.0 sec 3.38 MBytes 14.2 Mbits/sec[ 3] 14.0-16.0 sec 6.00 MBytes 25.2 Mbits/sec[ 3] 16.0-18.0 sec 8.00 MBytes 33.6 Mbits/sec 相关文档https://www.cnblogs.com/zdz8207/p/linux-iperf.html]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Stunnel 隐藏 OpenVPN 流量]]></title>
<url>%2F2019%2F06%2F23%2F%E4%BD%BF%E7%94%A8-Stunnel-%E9%9A%90%E8%97%8F-OpenVPN-%E6%B5%81%E9%87%8F%2F</url>
<content type="text"><![CDATA[简介众所周知的原因,在海外直接搭建 OpenVPN 根本无法使用(TCP 模式),或者用段时间就被墙了(UDP 模式)。本文主要介绍如何通过 Stunnel 隐藏 OpenVPN 流量,使其看起来像普通的 SSL 协议传输,从而绕过 gfw。 Stunnel 分为客户端和服务端,客户端负责接收用户 OpenVPN 客户端流量并转化成 SSL 协议加密数据包,然后转发给 Stunnel 服务端,实现 SSL 协议数据传输,服务端然后将流量转化成 OpenVPN 流量传输给 OpenVPN 服务端。因此我们可以在国内搭 Stunnel 客户端,国外搭 Stunnel 服务端。OpenVPN + Stunnel 整体架构如下: Stunnel 隐藏 OpenVPN 流量具体过程1. 首先需要有个 OpenVPN 服务端关于 OpenVPN 的搭建及使用在这里不多说了,之前写过文章,详情见这里。这里要说明的是,Stunnel 不支持 udp 流量转换,所以 OpenVPN 需要以 TCP 模式运行。下面为 OpenVPN TCP 模式的配置示例:12345678910111213141516171819202122232425port 4001 # 监听的端口号proto tcp-serverdev tunca /etc/openvpn/server/certs/ca.crt # CA 根证书路径cert /etc/openvpn/server/certs/server.crt # open VPN 服务器证书路径key /etc/openvpn/server/certs/server.key # open VPN 服务器密钥路径,This file should be kept secretdh /etc/openvpn/server/certs/dh.pem # Diffie-Hellman 算法密钥文件路径tls-auth /etc/openvpn/server/certs/ta.key 0 # tls-auth key,参数 0 可以省略,如果不省略,那么客户端# 配置相应的参数该配成 1。如果省略,那么客户端不需要 tls-auth 配置server 10.8.0.0 255.255.255.0 # 该网段为 open VPN 虚拟网卡网段,不要和内网网段冲突即可。open VPN 默认为 10.8.0.0/24push "dhcp-option DNS 8.8.8.8" # DNS 服务器配置,可以根据需要指定其他 nspush "dhcp-option DNS 8.8.4.4"push "redirect-gateway def1" # 客户端所有流量都通过 open VPN 转发,类似于代理开全局compress lzoduplicate-cn # 允许一个用户多个终端连接keepalive 10 120comp-lzopersist-keypersist-tunuser openvpn # open VPN 进程启动用户,openvpn 用户在安装完 openvpn 后就自动生成了group openvpnlog /var/log/openvpn/server.log # 指定 log 文件位置log-append /var/log/openvpn/server.logstatus /var/log/openvpn/status.logverb 3 2. Stunnel 服务端安装配置安装配置 Stunnel 服务端(海外节点):123456789101112yum -y install stunnelcd /etc/stunnelopenssl req -new -x509 -days 3650 -nodes -out stunnel.pem -keyout stunnel.pemchmod 600 /etc/stunnel/stunnel.pemvim stunnel.conf 填入如下内容:pid = /var/run/stunnel.pidoutput = /var/log/stunnel.logclient = no[openvpn]accept = 443 connect = 127.0.0.1:4001cert = /etc/stunnel/stunnel.pem 说明: accept = 443 # Stunnel 服务端监听端口connect = 127.0.0.1:4001 # OpenVPN 服务端地址 使用 systemd 启动 Stunnel 服务端:为了管理方便,我们使用 systemd 管理 Stunnel 服务,编辑一个 systemd unit 文件,vim /lib/systemd/system/stunnel.service:12345678910111213141516171819[Unit]Description=SSL tunnel for network daemonsAfter=network.targetAfter=syslog.target[Install]WantedBy=multi-user.targetAlias=stunnel.target[Service]Type=forkingExecStart=/usr/bin/stunnel /etc/stunnel/stunnel.confExecStop=/usr/bin/killall -9 stunnel# Give up if ping don't get an answerTimeoutSec=600Restart=alwaysPrivateTmp=false 启动 Stunnel 服务端:12systemctl start stunnel.servicesystemctl enable stunnel.service 3. Stunnel 客户端安装配置Stunnel 的客户端安装和服务器一样,同样的软件,既可以作为客户端,也可以作为服务端,只是配置不同而已。 安装配置 Stunnel 客户端(国内节点):12345678910111213yum -y install stunnelcd /etc/stunnelscp .... # 将服务端的证书 stunnel.pem 拷贝到这里chmod 600 /etc/stunnel/stunnel.pemvim stunnel.conf 填入如下内容:pid=/var/run/stunnel.pidoutput=/var/log/stunnel.logclient = yes[openvpn]accept=8443connect=stunnel_server_ip:443cert = /etc/stunnel/stunnel.pem 说明: accept=8443 # Stunnel 客户端监听端口stunnel_server_ip:443 # stunnel 服务端 ip 及端口 使用 systemd 启动 Stunnel 客户端:这里前面同服务端的操作过程,不再赘述。启动 Stunnel 客户端:12systemctl start stunnel.servicesystemctl enable stunnel.service 4. 使用 OpenVPN 连接 StunnelStunnel + OpenVPN 都配好后,就可以使用 OpenVPN 客户端实现自由上网了,需要注意的是 OpenVPN 客户端现在需要连接的是 Stunnel 客户端,不再是直接连接 OpenVPN 服务端。 相关文档https://github.com/Xaqron/stunnel]]></content>
<categories>
<category>OpenVPN</category>
</categories>
<tags>
<tag>OpenVPN</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Haproxy 从入门到掌握]]></title>
<url>%2F2019%2F06%2F17%2FHaproxy-%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%8E%8C%E6%8F%A1%2F</url>
<content type="text"><![CDATA[简介HAProxy 是一款开源且免费的反向代理软件,为基于 TCP 和 HTTP 的应用提供高可用、负载均衡和代理功能。它特别适用于流量非常大的网站,为世界上访问量最大的网站提供了强大的支持。多年来,HAProxy 已经成为事实上的标准开源负载均衡器,大多数主流的 Linux 发行版已经自带了该安装包,并且在云平台也经常被使用。 HAProxy 是一个纯粹的反向代理软件,与 nginx 不同的是 haproxy 没有 web 服务功能,而且同时支持 4 层和 7 层代理。Nginx 从 1.9.0 才开始支持 4 层代理,通过 stream 模块支持,该模块默认不会自带安装,需要编译安装的时候手动添加上这个模块。 HAProxy的核心功能: 负载均衡:L4 和 L7 两种模式,支持 RR/静态RR/LC/IP Hash/URI Hash/URL_PARAM Hash/HTTP_HEADER Hash等丰富的负载均衡算法; 健康检查:支持 TCP 和 HTTP 两种健康检查模式; 会话保持:对于未实现会话共享的应用集群,可通过 Insert Cookie/Rewrite Cookie/Prefix Cookie,以及上述的多种 Hash 方式实现会话保持; SSL:HAProxy 可以解析 HTTPS 协议,并能够将请求解密为 HTTP 后向后端传输; HTTP请求重写与重定向; 监控与统计:HAProxy提供了基于 Web 的统计信息页面,展现健康状态和流量数据。基于此功能,使用者可以开发监控程序来监控 HAProxy 的状态; Centos7 下安装 HAProxy1234567wget http://www.haproxy.org/download/1.8/src/haproxy-1.8.20.tar.gztar zxvf haproxy-1.8.20.tar.gzyum groupinstall -y 'Development Tools' # 安装 gcc 相关软件cd haproxy-1.8.20make TARGET=linux2628 # 编译:TARGET 和内核版本有关,不同的内核版 # 本对应不同值,对应关系在 READMEmake install # 安装到系统路径 使用 HAProxy 搭建一个 L4 层代理这里使用 HAProxy 转发流量到后台 3 个 Shadowsocks 节点的 1443/tcp 端口,并配有 TCP 健康检查机制: HAProxy 的 L4 层代理配置很简单,定义一对 frontend 和 backend,frontend 为 haproxy 前端监听的端口,backend 为后端服务器节点,我们访问 haproxy 不同的端口即可访问到对应的后端服务。frontend 和 backend 通过 default_backend 后面的名称关联。其他的配置项说明见下面配置文件 haproxy.cfg 中注释说明: 新建一个 HAProxy 配置文件:mkdir /etc/haproxyvim /etc/haproxy/haproxy.cfg 填入如下内容:haproxy.cfg:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849## demo config for Proxy mode## global 为全局配置项,主要和 haproxy 进程本身有关。defaults 为默认配置,当后面的 listen、frontend、banckend 等块没有再次指明相关配置时,会继承 defaults 的配置。global maxconn 20000 #最大连接数 ulimit-n 204800 #ulimit的数量限制 log 127.0.0.1 local3 user haproxy group haproxy chroot /var/empty nbproc 4 #启动后运行的进程数量 daemon #以后台形式运行haproxy pidfile /var/run/haproxy.piddefaults log global mode tcp retries 3 #3次连接失败认为服务不可用,也可以在后面设置 timeout connect 5s #连接超时 timeout client 30s #客户端超时 timeout server 30s #服务器端超时 option redispatch option nolinger no option dontlognull option tcplog option log-separate-errorslisten admin_stats #监控页面设置 bind 0.0.0.0:26000 # 监控页面监听端口号 bind-process 1 mode http log 127.0.0.1 local3 err stats refresh 30s ##每隔30秒自动刷新监控页面 stats uri /admin stats realm welcome login\ Haproxy stats auth admin:123456 stats hide-version stats admin if TRUEfrontend shadowsocks bind *:1443 default_backend shadowsocksbackend shadowsocks # balance roundrobin #负载均衡的方式,roundrobin是轮询 # check inter 1500 心跳检测频率, rise 3 是3次正确认为服务器可用,fall 3是3次失败认为服务器不可用 server ss-node-1 192.168.0.1:1443 check inter 1500 rise 3 fall 3 server ss-node-1 192.168.0.2:1443 check inter 1500 rise 3 fall 3 server ss-node-1 192.168.0.3:1443 check inter 1500 rise 3 fall 3 测试配置文件是否有效1haproxy -f /etc/haproxy/haproxy.cfg -c 启动 HAProy 服务:1haproxy -f /etc/haproxy/haproxy.cfg 我们还可以访问 HAProxy 自带的监控页面:上面我们配置的访问地址为 haproxy_ip:26000/admin,账号:admin,密码:123456。HAProxy 自带的监控页面特别好用,可以看到每个后端节点的流量使用情况、在线状态、可以随时将节点从后端集群中剔除或者改变状态。 相关文档http://www.haproxy.org/ | HAProxy 官网https://www.jianshu.com/p/c9f6d55288c0 | HAProxy从零开始到掌握https://www.jianshu.com/p/17c2f87bb27f | 简述Haproxy常见的负载均衡调度算法及应用场景详解https://www.serverlab.ca/tutorials/linux/network-services/how-to-configure-haproxy-health-checks/ | HAProxy 健康检查配置]]></content>
<categories>
<category>HAProxy</category>
</categories>
<tags>
<tag>HAProxy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Cenos7 下搭建 OpenVPN 过程记录]]></title>
<url>%2F2019%2F06%2F16%2FCenos7-%E4%B8%8B%E6%90%AD%E5%BB%BA-OpenVPN-%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95%2F</url>
<content type="text"><![CDATA[OpenVPN 服务端安装配置由于不同环境及软件版本命令使用略有差异,特别是 easy-rsa 的使用在 2.0 和 3.0 的差别有点大,所以在此先说明下安装环境及相关软件版本: 系统平台:Centos7 OpenVPN 版本:2.4.7 easy-rsa 版本:3.0.3 尽管不同环境及软件版本命令使用略有所差异,但是整个搭建过程都是一致的: 安装相关软件—>生成相关证书:CA 根证书、服务器证书—>配置 open VPN 服务端—>添加防火墙规则:snat—>启动 open VPN 服务端—>创建一个用户测试连接:创建客户端 CA 证书、生成 .ovpn 配置文件、打包相关文件供客户端使用。 1.安装 openvpn、easy-rsa、iptables-services12yum -y install epel-releaseyum -y install openvpn easy-rsa iptables-services 2.使用 easy-rsa 生成需要的证书及相关文件,在这个阶段会产生一些 key 和证书: CA 根证书 OpenVPN 服务器 ssl 证书 Diffie-Hellman 算法用到的 key 2.1 将 easy-rsa 脚本复制到 /etc/openvpn/,该脚本主要用来方便地生成 CA 证书和各种 key1cp -r /usr/share/easy-rsa/ /etc/openvpn/ 2.2 跳到 easy-rsa 目录并编辑 vars 文件,添加一些生成证书时用到的变量12345678cd /etc/openvpn/easy-rsa/<easy-rsa 版本号>/ # 查看 easy-rsa 版本号:yum info easy-rsavim vars # 没这个文件的话新建,填写如下内容(变量值根据实际情况随便填写):export KEY_COUNTRY="***"export KEY_PROVINCE="***"export KEY_CITY="***"export KEY_ORG="***"export KEY_EMAIL="***"source ./vars # 使变量生效 2.3 生成 CA 根证书12./easyrsa init-pki #初始化 pki 相关目录./easyrsa build-ca nopass #生成 CA 根证书, 输入 Common Name,名字随便起。 2.4 生成 OpenVPN 服务器证书和密钥第一个参数 server 为证书名称,可以随便起,比如 ./easyrsa build-server-full openvpn nopass1./easyrsa build-server-full server nopass 2.5 生成 Diffie-Hellman 算法需要的密钥文件1./easyrsa gen-dh #创建Diffie-Hellman,这可能得等一小会儿 2.6 生成 tls-auth key,这个 key 主要用于防止 DoS 和 TLS 攻击,这一步其实是可选的,但为了安全还是生成一下,该文件在后面配置 open VPN 时会用到。1openvpn --genkey --secret ta.key 2.7 将上面生成的相关证书文件整理到 /etc/openvpn/server/certs (这一步完全是为了维护方便)123456mkdir /etc/openvpn/server/certs && cd /etc/openvpn/server/certs/cp /etc/openvpn/easy-rsa/3/pki/dh.pem ./ # SSL 协商时 Diffie-Hellman 算法需要的 keycp /etc/openvpn/easy-rsa/3/pki/ca.crt ./ # CA 根证书cp /etc/openvpn/easy-rsa/3/pki/issued/server.crt ./ # open VPN 服务器证书cp /etc/openvpn/easy-rsa/3/pki/private/server.key ./ # open VPN 服务器证书 keycp /etc/openvpn/easy-rsa/3/ta.key ./ # tls-auth key 2.8 创建 open VPN 日志目录12mkdir -p /var/log/openvpn/chown openvpn:openvpn /var/log/openvpn 3.配置 OpenVPN可以从 /usr/share/doc/openvpn-/sample/sample-config-files 复制一份 demo 到 /etc/openvpn/(openvpn 版本号查看:yum info openvpn。)然后改改,或者从头开始创建一个新的配置文件。我选择新建配置:cd /etc/openvpn/vim server.conf 填入如下内容(很多配置项不需要特别了解,重要的配置这里注释出来了,其他相关配置项想了解的话见 这里):server.conf:1234567891011121314151617181920212223242526port 1194 # 监听的端口号proto udp # 服务端用的协议,udp 能快点,所以我选择 udpdev tunca /etc/openvpn/server/certs/ca.crt # CA 根证书路径cert /etc/openvpn/server/certs/server.crt # open VPN 服务器证书路径key /etc/openvpn/server/certs/server.key # open VPN 服务器密钥路径,This file should be kept secretdh /etc/openvpn/server/certs/dh.pem # Diffie-Hellman 算法密钥文件路径tls-auth /etc/openvpn/server/certs/ta.key 0 # tls-auth key,参数 0 可以省略,如果不省略,那么客户端# 配置相应的参数该配成 1。如果省略,那么客户端不需要 tls-auth 配置server 10.8.0.0 255.255.255.0 # 该网段为 open VPN 虚拟网卡网段,不要和内网网段冲突即可。open VPN 默认为 10.8.0.0/24push "dhcp-option DNS 8.8.8.8" # DNS 服务器配置,可以根据需要指定其他 nspush "dhcp-option DNS 8.8.4.4"push "redirect-gateway def1" # 客户端所有流量都通过 open VPN 转发,类似于代理开全局compress lzoduplicate-cn # 允许一个用户多个终端连接keepalive 10 120comp-lzopersist-keypersist-tunuser openvpn # open VPN 进程启动用户,openvpn 用户在安装完 openvpn 后就自动生成了group openvpnlog /var/log/openvpn/server.log # 指定 log 文件位置log-append /var/log/openvpn/server.logstatus /var/log/openvpn/status.logverb 3explicit-exit-notify 1 4.防火墙相关配置(使用 iptables 添加 snat 规则)4.1 禁用 Centos7 默认的 firewalld,使用经典的 iptables 防火墙管理软件:12systemctl stop firewalldsystemctl mask firewalld 4.2 禁用 SELinux马上关闭:setenforce 0 | 马上生效永久关闭:sed -i ‘s/SELINUX=enforcing/SELINUX=disabled/g’ /etc/selinux/config | 需要重启服务器生效 4.3 启用iptables123systemctl enable iptablessystemctl start iptablesiptables -F # 清理所有防火墙规则 4.4 添加防火墙规则,将 openvpn 的网络流量转发到公网:snat 规则12iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -j MASQUERADEiptables-save > /etc/sysconfig/iptables # iptables 规则持久化保存 4.5 Linux 服务器启用地址转发12echo net.ipv4.ip_forward = 1 >> /etc/sysctl.confsysctl -p # 这一步一定得执行,否则不会立即生效。 5.启动 open VPN123systemctl start openvpn@server # 启动systemctl enable openvpn@server # 开机自启动systemctl status openvpn@server # 查看服务状态 添加一个 OpenVPN 用户OpenVPN 服务端搭建完了,但是我们该如何使用呢?下面以 Mac 平台下使用为例: 要连接到 open VPN 服务端首先得需要一个客户端软件,在 Mac 下推荐使用 Tunnelblick,下载地址:https://tunnelblick.net/。Tunnelblick 是一个开源、免费的 Mac 版 open VPN 客户端软件。 接下来在服务端创建一个 open VPN 用户:其实创建用户的过程就是生成客户端 SSL 证书的过程,然后将其他相关的证书文件、key、.ovpn 文件(客户端配置文件)打包到一起供客户端使用。由于创建一个用户的过程比较繁琐,所以在此将整个过程写成了一个脚本 ovpn_user.sh,脚本内容比较简单,一看就懂: 首先创建一个客户端配置模板文件 sample.ovpn,该文件在脚本中会用到,放到 /etc/openvpn/client/ 目录,内容如下:sample.ovpn:1234567891011121314clientproto udpdev tunremote [open VPN服务端公网 ip,根据实际情况填写] 1194ca ca.crtcert admin.crtkey admin.keytls-auth ta.key 1remote-cert-tls serverpersist-tunpersist-keycomp-lzoverb 3mute-replay-warnings 下面为创建 open VPN 用户脚本:./ovpn_user.sh:12345678910111213141516171819202122232425262728293031# ! /bin/bashset -eOVPN_USER_KEYS_DIR=/etc/openvpn/client/keysEASY_RSA_VERSION=3EASY_RSA_DIR=/etc/openvpn/easy-rsa/PKI_DIR=$EASY_RSA_DIR/$EASY_RSA_VERSION/pkifor user in "$@"do if [ -d "$OVPN_USER_KEYS_DIR/$user" ]; then rm -rf $OVPN_USER_KEYS_DIR/$user rm -rf $PKI_DIR/reqs/$user.req sed -i '/'"$user"'/d' $PKI_DIR/index.txt fi cd $EASY_RSA_DIR/$EASY_RSA_VERSION # 生成客户端 ssl 证书文件 ./easyrsa build-client-full $user nopass # 整理下生成的文件 mkdir -p $OVPN_USER_KEYS_DIR/$user cp $PKI_DIR/ca.crt $OVPN_USER_KEYS_DIR/$user/ # CA 根证书 cp $PKI_DIR/issued/$user.crt $OVPN_USER_KEYS_DIR/$user/ # 客户端证书 cp $PKI_DIR/private/$user.key $OVPN_USER_KEYS_DIR/$user/ # 客户端证书密钥 cp /etc/openvpn/client/sample.ovpn $OVPN_USER_KEYS_DIR/$user/$user.ovpn # 客户端配置文件 sed -i 's/admin/'"$user"'/g' $OVPN_USER_KEYS_DIR/$user/$user.ovpn cp /etc/openvpn/server/certs/ta.key $OVPN_USER_KEYS_DIR/$user/ta.key # auth-tls 文件 cd $OVPN_USER_KEYS_DIR zip -r $user.zip $userdoneexit 0 执行上面脚本创建一个用户:sh ovpn_user.sh <username>,会在 /etc/openvpn/client/keys 目录下生成以用户名命名的 zip 打包文件,将该压缩包下载到本地解压,然后将里面的 .ovpn 文件拖拽到 Tunnelblick 客户端软件即可使用。压缩包里面文件有如下,示例:123456.├── ca.crt├── username.crt├── username.key├── username.ovpn└── ta.key 删除一个 OpenVPN 用户上面我们知道了如何添加一个用户,那么如果公司员工离职了或者其他原因,想删除对应用户 OpenVPN 的使用权,该如何操作呢?其实很简单,OpenVPN 的客户端和服务端的认证主要通过 SSL 证书进行双向认证,所以只要吊销对应用户的 SSL 证书即可。 编辑 OpenVPN 服务端配置 server.conf 添加如下配置: 1crl-verify /etc/openvpn/easy-rsa/3/pki/crl.pem 吊销用户证书,假设要吊销的用户名为 username 123cd /etc/openvpn/easy-rsa/3/./easyrsa revoke username./easyrsa gen-crl 重启 OpenVPN 服务端使其生效 1systemctl start openvpn@server 为了方便,也将上面步骤整理成了一个脚本,可以一键删除用户:del_ovpn_user.sh:123456789101112131415161718# ! /bin/bashset -eOVPN_USER_KEYS_DIR=/etc/openvpn/client/keysEASY_RSA_VERSION=3EASY_RSA_DIR=/etc/openvpn/easy-rsa/for user in "$@"do cd $EASY_RSA_DIR/$EASY_RSA_VERSION echo -e 'yes\n' | ./easyrsa revoke $user ./easyrsa gen-crl # 吊销掉证书后清理客户端相关文件 if [ -d "$OVPN_USER_KEYS_DIR/$user" ]; then rm -rf $OVPN_USER_KEYS_DIR/${user}* fi systemctl restart openvpn@serverdoneexit 0 安装过程中遇到的问题及解决方法问题 1:open VPN 客户端可以正常连接到服务端,但是无法上网,ping 任何地址都不通,只有服务端公网 ip 可以 ping 通。问题原因及解决方法:主要原因是服务的地址转发功能没打开,其实我前面配置了 echo net.ipv4.ip_forward = 1 >> /etc/sysctl.conf,但是没有执行 sysctl -p 使其立即生效,所以才导致出现问题。因此一定要记得两条命令都要执行。 问题 2: open VPN 可以正常使用,但是看客户端日志却有如下错误:122019-06-15 02:39:03.957926 AEAD Decrypt error: bad packet ID (may be a replay): [ #6361 ] -- see the man page entry for --no-replay and --replay-window for more info or silence this warning with --mute-replay-warnings2019-06-15 02:39:23.413750 AEAD Decrypt error: bad packet ID (may be a replay): [ #6508 ] -- see the man page entry for --no-replay and --replay-window for more info or silence this warning with --mute-replay-warnings 问题原因及解决方法:其实这个问题一般在 open VPN 是 UDP 服务的情况下出现,主要原因是 UDP 数据包重复发送导致,在 Wi-Fi 网络下经常出现,这并不影响使用,但是我们可以选择禁止掉该错误:根据错误提示可知使用 –mute-replay-warnings 参数可以消除该警告,我们使用的 open VPN 是 GUI 的,所以修改客户端 .ovpn 配置文件,末尾添加:mute-replay-warnings 即可解决。 该问题在这里有讨论:https://sourceforge.net/p/openvpn/mailman/message/10655695/ 相关文档关于 open VPN 客户端和服务端配置文件配置项说明:很全面,可以随时查看不懂的配置项https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage https://openvpn.net/ | OpenVPN 官网https://www.fandenggui.com/post/centos7-install-openvpn.html | Centos7 安装 OpenVPNhttps://www.howtoing.com/how-to-install-openvpn-on-centos-7 | Centos7 安装 OpenVPN https://www.xiaohui.com/dev/server/20070904-revoke-openvpn-client.htm | 吊销客户端证书https://scott.stevensononthe.net/2015/02/how-to-addremove-additional-users-to-openvpn/ | 吊销客户端证书https://tunnelblick.net/cConnectedBut.html | open VPN 一些常见问题https://tunnelblick.net/ipinfo | 本地公网 ip 查看]]></content>
<categories>
<category>OpenVPN</category>
</categories>
<tags>
<tag>OpenVPN</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 ansible 模板化 haproxy 配置文件]]></title>
<url>%2F2019%2F06%2F11%2F%E4%BD%BF%E7%94%A8-ansible-%E6%A8%A1%E6%9D%BF%E5%8C%96-haproxy-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[今天使用 ansible 自动化一些日常工作,其中包括 haproxy 的配置变更,我们 haproxy 里面定义了很多 frontend 和 backend,猛一看还不好模版化,其实仔细研究一下发现完全可以通过模板的循环语法动态生成配置文件,在此分享下。首先看一下未模板化时的原始配置:haproxy.cfg:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051global maxconn 20000 ulimit-n 204800 log 127.0.0.1 local3 user haproxy group haproxy chroot /var/empty nbproc 4 #启动后运行的进程数量 daemon #以后台形式运行haproxy pidfile /var/run/haproxy.piddefaults log global mode tcp retries 3 #3次连接失败认为服务不可用,也可以在后面设置 timeout connect 5s #连接超时 timeout client 30s #客户端超时 timeout server 30s #服务器端超时 option redispatch option nolinger no option dontlognull option tcplog option log-separate-errorslisten admin_stats #监控页面设置 bind 0.0.0.0:26000 bind-process 1 mode http log 127.0.0.1 local3 err stats refresh 30s #每隔30秒自动刷新监控页面 stats uri /admin stats realm welcome login\ Haproxy stats auth admin:123456 stats hide-version stats admin if TRUE# 通过配置文件动态生成 hproxy 配置frontend redis bind *:6379 default_backend redisbackend redis server 10.1.1.1:6379 10.1.1.1:6379 check inter 1500 rise 3 fall 3frontend es bind *:9300 default_backend esbackend es server 10.1.1.2:9300 10.1.1.2:9300 check inter 1500 rise 3 fall 3frontend mysql bind *:3306 default_backend mysqlbackend mysql server 10.1.1.3:3306 10.1.1.3:3306 check inter 1500 rise 3 fall 3 观察可以发现 frontend 和 backend 是成对出现的,一对为一个完整的配置,所以可以将 frontend 和 backend 对抽象为一个变量列表的元素,我们通过定义一个 ansible 变量列表循环生成同样的配置即可。变量具体定义如下(haproxy_servers 变量),下面为一个完整的测试 playbook。playbook.yml:1234567891011121314151617181920212223242526---- name: Test Playbook... hosts: all become: yes gather_facts: no vars: haproxy_servers: - frontend: 'redis' bind_port: 6379 backend: - address: 10.1.1.1:6379 - frontend: 'es' bind_port: 9300 backend: - address: 10.1.1.2:9300 - frontend: 'mysql' bind_port: 3306 backend: - address: 10.1.1.3:3306 tasks: - name: Generate haproxy config template: src: haproxy.j2 dest: /tmp/haproxy.cfg mode: 0644 force: yes 接下来我们看看模板文件 haproxy.j2 的定义:主要通过 jinja2 模板的循环语法遍历 haproxy_servers 变量生成 haproxy 配置。haproxy.j2:123456789101112131415161718192021222324252627282930313233343536373839404142434445global maxconn 20000 ulimit-n 204800 log 127.0.0.1 local3 user haproxy group haproxy chroot /var/empty nbproc 4 #启动后运行的进程数量 daemon #以后台形式运行haproxy pidfile /var/run/haproxy.piddefaults log global mode tcp retries 3 #3次连接失败认为服务不可用,也可以在后面设置 timeout connect 5s #连接超时 timeout client 30s #客户端超时 timeout server 30s #服务器端超时 option redispatch option nolinger no option dontlognull option tcplog option log-separate-errorslisten admin_stats #监控页面设置 bind 0.0.0.0:26000 bind-process 1 mode http log 127.0.0.1 local3 err stats refresh 30s #每隔30秒自动刷新监控页面 stats uri /admin stats realm welcome login\ Haproxy stats auth admin:123456 stats hide-version stats admin if TRUE# 通过配置文件动态生成 hproxy 配置{% for frontend in haproxy_servers %}frontend {{ frontend.frontend }} bind *:{{ frontend.bind_port }} default_backend {{ frontend.frontend }}backend {{ frontend.frontend }}{% for backend in frontend.backend %} server {{ backend.address }} {{ backend.address }} check inter 1500 rise 3 fall 3{% endfor %}{% endfor %} 测试下模板化结果?:我一般用 vagrant + ansible 测试 ansible 脚本,所以直接执行 vagrant rsync && vagrant provision 即可看到效果。关于 vagrant + ansible 的最佳实践请戳之前的这篇文章:使用 Vagrant 调试 Ansible Playbook。]]></content>
<categories>
<category>Ansible</category>
</categories>
<tags>
<tag>Ansible</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何在 Centos7 使用 iptables]]></title>
<url>%2F2019%2F06%2F09%2F%E5%A6%82%E4%BD%95%E5%9C%A8-Centos7-%E4%BD%BF%E7%94%A8-iptables%2F</url>
<content type="text"><![CDATA[从 Red Hat Enterprise Linux (RHEL) 7 和 CentOS 7 开始,firewalld 便作为系统默认的防火墙软件,替代之前的 iptables。firewalld 使用 firewall-cmd 命令管理防火墙规则,但是对于习惯了 iptables 的用户来说更倾向于使用传统的 iptables 方式,因为没有学习成本,能马上使用。即便 iptables 不再是 RHEL 7 和 CentOS 7 默认的防火墙管理软件,但是它们并没有完全屏蔽 iptables,通过安装依然可以使用。 简单说下 iptables 和 firewalld 区别 firewalld 是 Red Hat Enterprise Linux (RHEL) 7 和 CentOS 7 开始开始引进的防火墙管理软件; firewalld 可以动态修改单条规则,而不需要像iptables那样,在修改了规则后必须得全部刷新才可以生效; firewalld 在使用上要比 iptables 人性化很多,即使不明白“五张表五条链”而且对 TCP/IP 协议不理解也可以实现大部分功能; firewalld 需要每个服务都去设置才能放行,因为默认是拒绝。而 iptables 里默认是每个服务是允许,需要拒绝的才去限制; firewalld 自身并不具备防火墙的功能,而是和 iptables 一样需要通过内核的 netfilter 来实现,也就是说 firewalld 和 iptables一样,他们的作用都是用于维护规则,而真正使用规则干活的是内核的 netfilter,只不过 firewalld 和 iptables 的结构以及使用方法不一样罢了; firewalld 底层调用的命令仍然是 iptables;下图是 iptables 和 firewalld 的关系: 下面我们介绍下如何在 Centos7 系统下继续使用传统的 iptables 来管理防火墙规则。 关闭并注销 systemd 管理的 firewalld 服务1234$ systemctl stop firewalld# 注销 systmed 管理服务的过程相当于将原先指向相应 service 的软件链接# 重新指向了 /dev/null$ systemctl mask firewalld 安装并配置 iptables 安装 iptables 命令行工具 1$ yum install -y iptables 安装 iptables 服务(安装后默认归 systemd 管理) 1$ yum install -y iptables-services 这一步iptables-services安装完后会自动生成 iptables 的规则文件: /etc/sysconfig/iptables 设置开机自启动 1$ systemctl enable iptables 启动 iptables 防火墙服务启动防火墙后即可用 iptables -L -n 命令查看当前防火墙规则了。 12345678910111213$ systemctl start iptables$ iptables -L -nChain INPUT (policy ACCEPT)target prot opt source destinationACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHEDACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0ACCEPT all -- 0.0.0.0/0 0.0.0.0/0ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibitedChain FORWARD (policy ACCEPT)target prot opt source destinationREJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited iptables 添加防火墙规则,有两种方法: 通过 iptables 命令行工具添加:iptables -I INPUT ...;这种方式添加的规则不需要重启 iptables 即可立即生效,但是配置在重启服务器后会丢失,想持久保存下来执行如下命令(该命令会自动把命令行配置的规则写入 /etc/sysconfig/iptables,从而实现 iptables 规则的持久化保存): 1$ service iptables save ⚠️注意 service iptables save 和 iptables-save 命令的区别: service iptables save 作用是将 iptables 命令编辑的防火墙规则持久化保存下来,保存到 /etc/sysconfig/iptables; iptables-save 的作用是将内核中当前存在的防火墙规则导出来,和直接 cat /etc/sysconfig/iptables 的效果是一样的; 通过编辑 /etc/sysconfig/iptables 文件添加防火墙规则;通过这种方式添加的防火墙规则,需要重启 iptables 服务才能生效,并且服务器重启后配置的规则依然保留: 1$ systemctl restart iptables 举例:添加/删除 禁止 ping 响应的规则 1234添加禁止 ICMP 回显响应规则:iptables -A INPUT -i eth1 -p icmp -m icmp --icmp-type 8 -j DROP删除上面规则:iptables -D INPUT -i eth1 -p icmp -m icmp --icmp-type 8 -j DROP 查看防火墙状态 1$ systemctl status iptables 相关资料http://shaozhuqing.com/?p=4787 | iptables 和 firewalld 关系https://blog.51cto.com/xjsunjie/1902993 | 细说firewalld和iptableshttps://support.rackspace.com/how-to/use-iptables-with-centos-7/ | Use iptables with CentOS 7https://o-my-chenjian.com/2017/02/28/Using-Iptables-On-Centos7/ | 在Centos7上使用Iptables]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Vagrant 调试 Ansible Playbook]]></title>
<url>%2F2019%2F06%2F07%2F%E4%BD%BF%E7%94%A8-Vagrant-%E8%B0%83%E8%AF%95-Ansible-Playbook%2F</url>
<content type="text"><![CDATA[简介本文主要介绍使用 Vagrant 本地调试 Ansible Playbook 的最佳实践。 我平时用 ansible 做一些自动化任务,难免要写很多 playbook,如果直接将写的 playbook 在线上或者真实的服务器运行难免会担心出错,而且很可能会导致严重的错误。最好的方法就是先在本地虚拟机测试好,然后跑到真实的环境。我们可以将 Vagrant 和 ansible 结合使用来轻松地在本地调试 playbook。为什么使用这种方式呢?我觉得有如下好处(当然用了之后就知道有多爽了): 虚拟机用 Vagrant 管理,随时可以方便地删除、重建,这些操作都是简单的命令行; ansible 脚本在本地虚拟机可以随便折腾,哪怕 VM 折腾坏了,可以马上重建 VM; 所有操作都是基于配置文件,没有界面点触式操作,可以很好地将其工程化,放到 git 仓库统一管理; Vagrant 结合 Ansible 的 workflowVagrant 结合 Ansible 的主要工作原理是使用 Vagrant 的 ansible_local 或者 ansible 配置器(Provisioner),这两个的唯一区别是前者会在 provision 时自动在 VM 安装 ansible,后者不会自动安装,需要自行安装。我选择用 ansible_local 配置器,懒得装一遍 ansible… 而且在 vagrant destroy 销毁虚拟机后重建时还能得到与之前一致的配置。 下面介绍下我本地调试 ansible playbook 脚本的 workflow。 使用 Vagrantfile 定义虚拟机 123456789101112131415161718Vagrant.configure("2") do |config| config.vm.box = "Centos7" config.vm.hostname= "ansible" config.vm.network "private_network", ip: "192.168.10.100" config.vm.provider :virtualbox do |vbox| vbox.name = "ansible_vagrant" vbox.memory = "512" vbox.cpus = 1 end # ansible 相关配置 config.vm.provision "ansible_local" do |ansible| # ansible 运行时输出详细信息,作用同 ansible-playbook 的 -v 参数 # ansible.verbose = "v" # 指定运行哪个 playbook ansible.playbook = "playbook.yml" endend 在 Vagrant 工程目录下编写 playbook,示例: 12345678910111213141516171819---- name: Test Playbook... hosts: all become: yes gather_facts: no vars: - ip_list: - 127.0.0.1 - 127.0.0.1 - 127.0.0.1 - 127.0.0.2 tasks: - name: remove ip list file file: state: absent path: /tmp/ip.txt - name: test gen file shell: echo "{{ item }}" >> /tmp/ip.txt with_items: "{{ ip_list }}" playbook 写完后 执行 vagrant up 启动虚拟机,启动过程中会自动执行 Vagrantfile 中配置的 playbook 文件(在 Vagrant 工程目录下); 如果 playbook 运行有问题,则继续修改; 执行 vagrant rsync && vagrant provision 重新运行 playbook; 注意:在执行 vagrant provision 之前,一定要先 vagrant rsync 同步下本地主机和 VM 的共享目录,否则本地修改后不会生效。ansible 脚本的运行最终是在 VM 上面,读取的文件都是在 VM 的 /vagrant 目录。 如果测试 playbook 还是有问题则返回到第 4 步继续修改并测试; 如果一切都测试顺利的话,为了保险,最后模拟一下真实的环境:vagrant destroy 销毁 VM,然后 vagrant up 重新创建 VM 并自动运行 ansible playbook。 相关资料https://linux.cn/article-9502-1.html | 使用 Vagrant 测试 Ansible 剧本https://www.vagrantup.com/docs/provisioning/ansible_intro.html | Vagrant 和 Ansiblehttps://www.vagrantup.com/docs/provisioning/ansible_local.html | Vagrant ansible_local 配置器https://www.vagrantup.com/docs/provisioning/ansible_common.html | Vagrant ansible 和 ansible_local 配置器通用配置]]></content>
<categories>
<category>Ansible</category>
</categories>
<tags>
<tag>Ansible</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo next 主题加载自定义 js 文件]]></title>
<url>%2F2019%2F06%2F02%2FHexo-next-%E4%B8%BB%E9%A2%98%E5%8A%A0%E8%BD%BD%E8%87%AA%E5%AE%9A%E4%B9%89-js-%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[为什么要配置 hexo next 主题自定义 js 文件呢?主要原因有两点: 不可靠:加载第三方站点的 js 依赖其站点的稳定性,如果第三方站点给挂了或者不维护了,那么加载的地址就失效了,访问直接 404… 比如最近就遇到 next 主题”不蒜子”文章 PV 统计功能用不了了,Chrome 抓包发现 https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js 这个地址 404 了,看了 “不蒜子”官方 blog 通知 才发现原来换成了 https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js。 加载速度慢:比如之前将 gitalk 功能用到的 https://rawgit.com/qhh0205/78e9e0b1f3114db6737f3ed8cdd51d3a/raw/3894c5be5aa2378336b1f5ee0f296fa0b22d06e9/md5.min.js 文件嵌入到主题,发现每次打开 blog 网站都加载很慢,Chrome 抓包发现是该文件加载缓慢,一直 pending 很久… 那么解决上面两个问题的办法就是可以将远程加载的 js 文件下载下来,放到本地 netx 主题 source/js/src/ 目录下,让 hexo 生成静态网站时,加载生成静态站点本身的 js。下面举两个例子。 next 主题 gitalk 评论功能加载自定义 js 将 https://github.com/blueimp/JavaScript-MD5/blob/master/js/md5.min.js 文件下载下来放到 themes/next/source/js/src/ 路径下。 修改 themes/next/layout/_third-party/comments/gitalk.swig,加载 md5.min.js 改为 <script src="/js/src/md5.min.js"></script>:1234567891011121314151617{% if page.comments && theme.gitalk.enable %} <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css"> <script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script> <script src="/js/src/md5.min.js"></script> <script type="text/javascript"> var gitalk = new Gitalk({ clientID: '3840ba8c8d80c18be7e3', clientSecret: '1b00f2efe5285973c24da9ed9ac895775eacc8ea', repo: '{{ theme.gitalk.repo }}', owner: '{{ theme.gitalk.githubID }}', admin: ['{{ theme.gitalk.adminUser }}'], id: md5(location.pathname), distractionFreeMode: '{{ theme.gitalk.distractionFreeMode }}' }) gitalk.render('gitalk-container') </script>{% endif %} next 主题 “不蒜子” PV 统计功能加载自定义 js 将 https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js 文件下载下来放到 themes/next/source/js/src/ 路径下。 修改 themes/next/layout/_third-party/analytics/busuanzi-counter.swig,将原先 <script async src="https://dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js"></script> 改为 <script async src="/js/src/busuanzi.pure.mini.js"></script>:123456789101112131415161718192021{% if theme.busuanzi_count.enable %}<div class="busuanzi-count"> <script async src="/js/src/busuanzi.pure.mini.js"></script> {% if theme.busuanzi_count.site_uv %} <span class="site-uv"> {{ theme.busuanzi_count.site_uv_header }} <span class="busuanzi-value" id="busuanzi_value_site_uv"></span> {{ theme.busuanzi_count.site_uv_footer }} </span> {% endif %} {% if theme.busuanzi_count.site_pv %} <span class="site-pv"> {{ theme.busuanzi_count.site_pv_header }} <span class="busuanzi-value" id="busuanzi_value_site_pv"></span> {{ theme.busuanzi_count.site_pv_footer }} </span> {% endif %}</div>{% endif %} 配置完成后需要 hexo clean && hexo g 生效… 附件怕以下两个远程站点 js 丢了,在此备份一下吧…https://github.com/blueimp/JavaScript-MD5/blob/master/js/md5.min.js:12!function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function r(n,t){return n<<t|n>>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<<r%32,n[14+(r+64>>>9<<4)]=r;var e,i,a,d,h,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e<n.length;e+=16)i=l,a=g,d=v,h=m,g=f(g=f(g=f(g=f(g=c(g=c(g=c(g=c(g=u(g=u(g=u(g=u(g=o(g=o(g=o(g=o(g,v=o(v,m=o(m,l=o(l,g,v,m,n[e],7,-680876936),g,v,n[e+1],12,-389564586),l,g,n[e+2],17,606105819),m,l,n[e+3],22,-1044525330),v=o(v,m=o(m,l=o(l,g,v,m,n[e+4],7,-176418897),g,v,n[e+5],12,1200080426),l,g,n[e+6],17,-1473231341),m,l,n[e+7],22,-45705983),v=o(v,m=o(m,l=o(l,g,v,m,n[e+8],7,1770035416),g,v,n[e+9],12,-1958414417),l,g,n[e+10],17,-42063),m,l,n[e+11],22,-1990404162),v=o(v,m=o(m,l=o(l,g,v,m,n[e+12],7,1804603682),g,v,n[e+13],12,-40341101),l,g,n[e+14],17,-1502002290),m,l,n[e+15],22,1236535329),v=u(v,m=u(m,l=u(l,g,v,m,n[e+1],5,-165796510),g,v,n[e+6],9,-1069501632),l,g,n[e+11],14,643717713),m,l,n[e],20,-373897302),v=u(v,m=u(m,l=u(l,g,v,m,n[e+5],5,-701558691),g,v,n[e+10],9,38016083),l,g,n[e+15],14,-660478335),m,l,n[e+4],20,-405537848),v=u(v,m=u(m,l=u(l,g,v,m,n[e+9],5,568446438),g,v,n[e+14],9,-1019803690),l,g,n[e+3],14,-187363961),m,l,n[e+8],20,1163531501),v=u(v,m=u(m,l=u(l,g,v,m,n[e+13],5,-1444681467),g,v,n[e+2],9,-51403784),l,g,n[e+7],14,1735328473),m,l,n[e+12],20,-1926607734),v=c(v,m=c(m,l=c(l,g,v,m,n[e+5],4,-378558),g,v,n[e+8],11,-2022574463),l,g,n[e+11],16,1839030562),m,l,n[e+14],23,-35309556),v=c(v,m=c(m,l=c(l,g,v,m,n[e+1],4,-1530992060),g,v,n[e+4],11,1272893353),l,g,n[e+7],16,-155497632),m,l,n[e+10],23,-1094730640),v=c(v,m=c(m,l=c(l,g,v,m,n[e+13],4,681279174),g,v,n[e],11,-358537222),l,g,n[e+3],16,-722521979),m,l,n[e+6],23,76029189),v=c(v,m=c(m,l=c(l,g,v,m,n[e+9],4,-640364487),g,v,n[e+12],11,-421815835),l,g,n[e+15],16,530742520),m,l,n[e+2],23,-995338651),v=f(v,m=f(m,l=f(l,g,v,m,n[e],6,-198630844),g,v,n[e+7],10,1126891415),l,g,n[e+14],15,-1416354905),m,l,n[e+5],21,-57434055),v=f(v,m=f(m,l=f(l,g,v,m,n[e+12],6,1700485571),g,v,n[e+3],10,-1894986606),l,g,n[e+10],15,-1051523),m,l,n[e+1],21,-2054922799),v=f(v,m=f(m,l=f(l,g,v,m,n[e+8],6,1873313359),g,v,n[e+15],10,-30611744),l,g,n[e+6],15,-1560198380),m,l,n[e+13],21,1309151649),v=f(v,m=f(m,l=f(l,g,v,m,n[e+4],6,-145523070),g,v,n[e+11],10,-1120210379),l,g,n[e+2],15,718787259),m,l,n[e+9],21,-343485551),l=t(l,i),g=t(g,a),v=t(v,d),m=t(m,h);return[l,g,v,m]}function a(n){var t,r="",e=32*n.length;for(t=0;t<e;t+=8)r+=String.fromCharCode(n[t>>5]>>>t%32&255);return r}function d(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t<r.length;t+=1)r[t]=0;var e=8*n.length;for(t=0;t<e;t+=8)r[t>>5]|=(255&n.charCodeAt(t/8))<<t%32;return r}function h(n){return a(i(d(n),8*n.length))}function l(n,t){var r,e,o=d(n),u=[],c=[];for(u[15]=c[15]=void 0,o.length>16&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(d(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="";for(r=0;r<n.length;r+=1)t=n.charCodeAt(r),e+="0123456789abcdef".charAt(t>>>4&15)+"0123456789abcdef".charAt(15&t);return e}function v(n){return unescape(encodeURIComponent(n))}function m(n){return h(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);//# sourceMappingURL=md5.min.js.map https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js:1var bszCaller,bszTag;!function(){var c,d,e,a=!1,b=[];ready=function(c){return a||"interactive"===document.readyState||"complete"===document.readyState?c.call(document):b.push(function(){return c.call(this)}),this},d=function(){for(var a=0,c=b.length;c>a;a++)b[a].apply(document);b=[]},e=function(){a||(a=!0,d.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.detachEvent("onreadystatechange",e),window==window.top&&(clearInterval(c),c=null)))},document.addEventListener?document.addEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.attachEvent("onreadystatechange",function(){/loaded|complete/.test(document.readyState)&&e()}),window==window.top&&(c=setInterval(function(){try{a||document.documentElement.doScroll("left")}catch(b){return}e()},5)))}(),bszCaller={fetch:function(a,b){var c="BusuanziCallback_"+Math.floor(1099511627776*Math.random());window[c]=this.evalCall(b),a=a.replace("=BusuanziCallback","="+c),scriptTag=document.createElement("SCRIPT"),scriptTag.type="text/javascript",scriptTag.defer=!0,scriptTag.src=a,document.getElementsByTagName("HEAD")[0].appendChild(scriptTag)},evalCall:function(a){return function(b){ready(function(){try{a(b),scriptTag.parentElement.removeChild(scriptTag)}catch(c){bszTag.hides()}})}}},bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback",function(a){bszTag.texts(a),bszTag.shows()}),bszTag={bszs:["site_pv","page_pv","site_uv"],texts:function(a){this.bszs.map(function(b){var c=document.getElementById("busuanzi_value_"+b);c&&(c.innerHTML=a[b])})},hides:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="none")})},shows:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="inline")})}};]]></content>
<categories>
<category>Hexo</category>
</categories>
<tags>
<tag>Hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[docker-compose 启动 Redis 服务]]></title>
<url>%2F2019%2F05%2F31%2Fdocker-compose-%E5%90%AF%E5%8A%A8-Redis-%E6%9C%8D%E5%8A%A1%2F</url>
<content type="text"><![CDATA[使用 docker-compose 以 aof 持久化方式启动单节点 Redis。docker-compose.yml:123456789101112---version: '3'services: redis: image: redis:4.0.13 container_name: redis restart: always command: --appendonly yes ports: - 6379:6379 volumes: - ./redis_data:/data]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Nginx root 和 alias 指令的区别]]></title>
<url>%2F2019%2F05%2F31%2FNginx-root-%E5%92%8C-alias-%E6%8C%87%E4%BB%A4%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[nginx 的 root 和 alias 指令都是用于访问服务器本地文件的,两者区别如下: 配置语法及适用范围 12345678[root]语法:root path默认值:root html配置段:http、server、location、if[alias]语法:alias path配置段:location root 的处理结果是:root 路径+location 路径; alias 的处理结果是:使用 alias 路径替换 location 路径; alias 后面的路径结尾必须是 ‘/‘,而 root 可有可无; 举例当 location 配置为如下时:123location ^~ /documents/ { root /var/www-html/documents/;} 请求:GET /documents/a.js —> 相当于请求本地路径:/var/www-html/documents/documents/a.js 请求:GET /documents/html/index.html —> 相当于请求本地路径:/var/www-html/documents/documents/html/index.html 当 location 配置为如下时:123location ^~ /documents/ { alias /var/www-html/documents/;} 请求:GET /documents/a.js —> 相当于请求本地路径:/var/www-html/documents/a.js 请求:GET /documents/html/index.html —> 相当于请求本地路径:/var/www-html/documents/html/index.html]]></content>
<categories>
<category>Nginx</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Wireshark 抓包理解 HTTPS 协议]]></title>
<url>%2F2019%2F05%2F26%2FWireshark-%E6%8A%93%E5%8C%85%E7%90%86%E8%A7%A3-HTTPS-%E5%8D%8F%E8%AE%AE%2F</url>
<content type="text"><![CDATA[HTTPS 简介HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer)协议是 HTTP 协议的安全版,在 HTTP 应用层和传输层加入了 SSL/TLS 层,确保数据传输的安全性,所以 HTTPS 协议并不是什么新的协议,仅仅是 HTTP 协议和安全协议的组合。 HTTPS 协议主要解决如下三个通信安全问题: 窃听风险(eavesdropping):第三方可以获知通信内容。 篡改风险(tampering):第三方可以修改通信内容。 冒充风险(pretending):第三方可以冒充他人身份参与通信。 HTTPS 通过 SSL/TLS 协议解决了上述三个问题,可以达到: 加密数据以防止数据中途被窃取; 维护数据的完整性,确保数据在传输过程中不被改变; 认证用户和服务器,确保数据发送到正确的客户机和服务器; 既然安全问题是 SSL/TLS 保证的,那么就有必要仔细探索下 SSL/TLS 协议的机制,如下为 HTTPS 通信的整个网络协议栈,其中 SSL/TLS 协议又分为两层: 握手协议(SSL Handshake Protocol):它建立在记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。 记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 关于更多 SSL 和 TLS 知识见之前的文章: 基于 OpenSSL 生成自签名证书。 SSL/TLS 通信过程开始加密通信之前,客户端和服务器首先必须建立连接和交换参数,这个过程叫做握手(handshake)。SSL/TLS 握手其实就是通过非对称加密,生成对称加密的 session key 的过程。 假定客户端叫做爱丽丝服务器叫做鲍勃,整个握手过程可以用下图说明: 整个握手过程通俗地说分为如下五步(真实的过程涉及的细节比这个多): 第一步,爱丽丝给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。 第二步,鲍勃确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。 第三步,爱丽丝确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使 用数字证书中的公钥,加密这个随机数,发给鲍勃。 第四步,鲍勃使用自己的私钥,获取爱丽丝发来的随机数(即Premaster secret)。 第五步,爱丽丝和鲍勃根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。 SSL 握手的过程为双方发送消息的过程,这里所说的消息并不是一个独立的 TCP 数据包,而是 SSL 协议的术语。根据服务端实现的不同,可能一个 TCP 包中包含多条消息,而不是每条消息单独发送(每条单独发送效率太低),这个我们后面通过 Wireshark 抓包可以看到。 下图为双方握手过程中互相发送的 SSL 消息: 客户端发送的初始消息Client Hello 消息客户端发送 Client Hello 消息给服务端来初始化会话消息,该消息包含如下信息: Version Number: 客户端发送它所支持的最高 SSL/TLS 版本。版本 2 代表 SSL 2.0,版本 3 代表 SSL 3.0,版本 3.1 代表 TLS。 Randomly Generated Data:一个 32 字节的客户端随机数,该随机数被服务端生成通信用的对称密钥(master secret); Session Identification :session ID 被客户端用于恢复之前的会话(只有恢复 session 时该字段才有值),这样可以简化 SSL 握手过程,避免每次请求都建立新的连接而握手,握手过程是需要消耗很多计算资源的。已建立的连接信息存储在客户端和服务端各自的 session 缓存中,用 session ID 标识; Cipher Suite: 客户端发送它所支持的加密套件列表; Compression Algorithm: 压缩算法,目前该字段几乎不在使用; 服务端响应Server Hello 消息服务端回复 Server Hello 消息给客户端: Version Number:服务端发送双发所支持的最高的 SSL/TLS 版本; Randomly Generated Data:一个 32 字节的服务端随机数,被客户端用于生成通信用的对称密钥(master secret); Session Identification:该字段有如下三中情况: New session ID:客户端和服务端初次建立连接时生成的 session ID。或者客户端尝试恢复 session,但是服务端无法恢复,因此也会生成新的 session ID; Resumed Session ID:和客户端发送的恢复会话 ID 一致,用于恢复会话; Null:该字段为 Null,表明这是一个新的 Session,但是服务端不打算用于后续的会话恢复,因此不会产生 session ID,该字段为空; Cipher Suite: 服务端发送双发支持的最安全的加密套件; Compression Algorithm:指定双方使用的压缩算法,目前该字段几乎不在使用; Server Certificate 消息服务端发送自己的 SSL 证书给客户端,证书中包含服务端的公钥,客户端用该证书验证服务端的身份。 Server Key Exchange 消息这个消息是可选的,该消息主要用来传递双方协商密钥的参数,比如双方使用 Diffie-Hellman (迪菲) 算法生成 premaster secret 时,会用该字段传递双方的公共参数。所以具体该字段是什么内容取决于双方协商密钥的加密套件。 Client Certificate Request 消息这个消息也是可选的,只有当服务端也需要验证客户端会用到。有的安全度高的网站会要求验证客户端,确认客户的真实身份,比如银行之类的网站。 Server Hello Done 消息服务器发送 ServerHelloDone 消息,告知客户端服务端这边握手相关的消息发送完毕,等待客户端响应。 客户端回复Client Certificate 消息如果服务端发送了 Client Certificate Request 消息,那么客户端会发送该消息给服务端,包含自己的证书信息,供服务端进行客户端身份认证。 Client Key Exchange 消息根据协商的密钥算法不同,该消息的内容会不同,该消息主要包含密钥协商的参数。比如双方使用 Diffie-Hellman (迪菲) 算法生成 premaster secret 时,会用该字段传递双方的公共参数。 Certificate Verify 消息该消息只有在 Client Certificate message 消息发送时才发送。客户端通过自己的私钥签名从开始到现在的所有发送过的消息,然后服务端会用客户端的公钥验证这个签名。 Change Cipher Spec 消息通知服务器此消息以后客户端会以之前协商的密钥加密发送数据。 Client Finished 消息客户端计算生成对称密钥,然后使用该对称密钥加密之前所有收发握手消息的 Hash 值,发送给服务器,服务器将用相同的会话密钥(使用相同方法生成)解密此消息,校验其中的Hash 值。该消息是 SSL 握手协议记录层加密的第一条消息。 服务端最后对客户端响应Change Cipher Spec 消息通知客户端此消息以后服务端将会以之前协商的密钥加密发送数据。 Server Finished 消息服务器使用对称密钥加密(生成方式与客户端相同)之前所发送的所有握手消息的hash值,发送给客户端去校验。 至此 SSL 握手过程结束,双发之后的通信数据都会用双方协商的对称密钥 Session Key 加密传输。 下图为 SSL/TLS 通信的整个过程:TCP 三次握手 + SSL/TLS 握手: Wireshark 抓包分析 SSL/TLS 握手过程本节使用 wireshark 抓包工具分析一个完整的 HTTPS 通信过程,看看通信过程中双方消息是如何传送的。前面我们说过,根据服务端实现的不同,可能一个 TCP 包中包含多条 SSL/TLS 消息,而不是每条消息单独发送(每条单独发送效率太低)。 使用如下 wireshark https 包过滤器:1tcp.port==443 and (ip.dst==104.18.40.252 or ip.src==104.18.40.252) 下面为 Wireshark 抓取的 https 流量包,展示了整个通信过程:建立 TCP 连接 –> SSL/TLS 握手 –> 应用数据加密传输: 上面是一个实际的 SSL/TLS 握手过程,分为如下 5 步: 客户端发送 Client Hello 消息给服务端; 服务端回应 Server Hello 消息; 服务端同时回应 Server Certificate、Server Key Exchange 和 Server Hello Done 消息; 客户端发送 Client Key Exchange、Change Cipher Spec 和 Client Finished 消息; 服务端最后发送 Change Cipher Spec 和 Server Finished 消息; 下面我们分步分析每个阶段的包的内容,看是否和前面的理论一致。 客户端发送 Client Hello 消息给服务端 可以看出 TLS 协议确实分为两层:TLS 记录层、TLS 握手层,其中 TLS 握手层基于 TLS 记录层。 另外客户端发送的 Client Hello 消息当中包含的信息也可以看到: Version:客户端支持的 TLS 版本号; Random:客户端生成的 32 字节随机数; Session ID:会话 ID; Cipher Suites:客户端支持的加密套件列表; Compression Methods:客户端支持的压缩算法; 服务端回应 Server Hello 消息 Server Hello 包含如下信息: Version:双方支持的 TLS 版本号; Random:服务端生成的 32 字节随机数; Session ID:会话 ID; Cipher Suites:双方协商的加密套件; Compression Methods:压缩算法; 服务端同时回应 Server Certificate、Server Key Exchange 和 Server Hello Done 消息 可以看出每个 TLS 记录层是一个消息,服务端同时回复了有 3 个消息:Server Certificate、Server Key Exchange、Server Hello Done。 从 Server Key Exchange 消息可以看出双方密钥协商使用的是 Diffie-Hellman (迪菲) 算法,该消息用于传递 Diffie-Hellman (迪菲) 算法的参数。 客户端发送 Client Key Exchange、Change Cipher Spec 和 Client Finished 消息 可以看出客户端同时回复了 3 个消息:Client Key Exchange、Change Cipher Spec 和 Client Finished 消息。Client Key Exchange 的内容为 Diffie-Hellman (迪菲) 算法的参数,用于生成 premaster key,然后和双方之前的随机数结合生成对称密钥。 服务端最后发送 Change Cipher Spec 和 Server Finished 消息 服务端最后发送 Change Cipher Spec 和 Server Finished 消息,至此 SSL/TLS 握手完毕,接下来双方会用对称加密的方式加密传输数据。 相关资料https://segmentfault.com/a/1190000002554673 | SSL/TLS 原理详解http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html | SSL/TLS 协议运行机制概述http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html | 图解 SSL/TLS 协议https://www.jianshu.com/p/a3a25c6627ee | Https详解+wireshark抓包演示https://segmentfault.com/a/1190000007283514 | TLS/SSL 高级进阶https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc785811(v=ws.10) | 微软 Windows 文档]]></content>
<categories>
<category>HTTPS</category>
</categories>
<tags>
<tag>HTTPS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 OpenSSL 生成自签名证书]]></title>
<url>%2F2019%2F05%2F18%2F%E5%9F%BA%E4%BA%8E-OpenSSL-%E7%94%9F%E6%88%90%E8%87%AA%E7%AD%BE%E5%90%8D%E8%AF%81%E4%B9%A6%2F</url>
<content type="text"><![CDATA[PKI、CA、SSL、TLS、OpenSSL几个概念 PKI 和 CAPKI 就是 Public Key Infrastructure 的缩写,翻译过来就是公开密钥基础设施。它是利用公开密钥技术所构建的,解决网络安全问题的,普遍适用的一种基础设施。 PKI 是目前唯一的能够基本全面解决安全问题的可能的方案。 PKI 通过电子证书以及管理这些电子证书的一整套设施,维持网络世界的秩序;通过提供一系列的安全服务,为网络电子商务、电子政务提供有力的安全保障。 通俗点说 PKI 就是一整套安全相关标准,然后基于这套标准体系衍生一系列安全相关的产品,主要目的是保证数据在网络上安全、可靠地传输。 PKI 主要由以下组件组成: 认证中心 CA(证书签发) ; X.500目录服务器(证书保存) ; 具有高强度密码算法(SSL)的安全WWW服务器(即配置了 HTTPS 的 apache) ; Web(安全通信平台): Web 有 Web Client 端和 Web Server 端两部分 自开发安全应用系统 自开发安全应用系统是指各行业自开发的各种具体应用系统,例如银行、证券的应用系统等。 CA 是 PKI 的”核心”,即数字证书的申请及签发机关,CA 必须具备权威性的特征,它负责管理 PKI 结构下的所有用户(包括各种应用程序)的证书,把用户的公钥和用户的其他信息捆绑在一起,在网上验证用户的身份,CA 还要负责用户证书的黑名单登记和黑名单发布 。 CA 实现了 PKI 中一些很重要的功能: 接收验证最终用户数字证书的申请; 确定是否接受最终用户数字证书的申请-证书的审批; 向申请者颁发、拒绝颁发数字证书-证书的发放; 接收、处理最终用户的数字证书更新请求-证书的更新; 接收最终用户数字证书的查询、撤销; 产生和发布证书废止列表(CRL); 数字证书的归档; 密钥归档; 历史数据归档; 在这么多功能中,CA 的核心功能就是”发放”和”管理”数字证书: SSL 和 TLSSSL 和 TLS 协议是介于 HTTP 协议与 TCP 之间的一个可选层,主要用于 Web 客户端和服务器之间进行数据的安全传输: SSL: Secure Socket Layer,安全套接字层),为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取。当前版本为3.0。它已被广泛地用于Web浏览器与服务器之间的身份认证和加密数据传输。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。 TLS: (Transport Layer Security,传输层安全协议),用于两个应用程序之间提供保密性和数据完整性。TLS 1.0是IETF(Internet Engineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本,可以理解为SSL 3.1,它是写入了 RFC 的。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。 SSL/TLS协议提供的服务主要有: 认证用户和服务器,确保数据发送到正确的客户机和服务器; 加密数据以防止数据中途被窃取; 维护数据的完整性,确保数据在传输过程中不被改变; OpenSSLOpenSSL 是一个开源的加密工具包,主要包括如下三部分: libssl (with platform specific naming): Provides the client and server-side implementations for SSLv3 and TLS. libcrypto (with platform specific naming): Provides general cryptographic and X.509 support needed by SSL/TLS but not logically part of it. openssl: A command line tool that can be used for:Creation of key parameters Creation of X.509 certificates, CSRs and CRLs Calculation of message digests Encryption and decryption SSL/TLS client and server tests Handling of S/MIME signed or encrypted mail And more... 使用 OpenSSL 生产自签名 SSL 证书过程 以下为 Centos7 环境下生成自签名 SSL 证书的具体过程: 修改 openssl 配置文件 123456789vi /etc/pki/tls/openssl.cnf# match 表示后续生成的子证书的对应项必须和创建根证书时填的值一样,否则报错。以下配置只规定子证书的 countryName 必须和根证书一致。[ policy_match ] 段配置改成如下:countryName = matchstateOrProvinceName = optionalorganizationName = optionalorganizationalUnitName = optionalcommonName = suppliedemailAddress = optional 在服务器 pki 的 CA 目录下新建两个文件 1cd /etc/pki/CA && touch index.txt serial && echo 01 > serial 生成 CA 根证书密钥 1cd /etc/pki/CA/ && openssl genrsa -out private/cakey.pem 2048 && chmod 400 private/cakey.pem 生成根证书(根据提示输入信息,除了 Country Name 选项需要记住的,后面的随便填) 1openssl req -new -x509 -key private/cakey.pem -out cacert.pem 生成密钥文件 1openssl genrsa -out nginx.key 2048 生成证书请求文件(CSR):A. 根据提示输入信息,除了 Country Name 与前面根证书一致外,其他随便填写B. Common Name 填写要保护的域名,比如:*.qhh.me 1openssl req -new -key nginx.key -out nginx.csr 使用 openssl 签署 CSR 请求,生成证书 12345678openssl ca -in nginx.csr -cert /etc/pki/CA/cacert.pem -keyfile /etc/pki/CA/private/cakey.pem -days 365 -out nginx.crt参数项说明:-in: CSR 请求文件-cert: 用于签发的根 CA 证书-keyfile: 根 CA 的私钥文件-days: 生成的证书的有效天数-out: 生成证书的文件名 至此自签名证书生成完成,最终需要:nginx.key 和 nginx.crt 配置 Nginx 使用自签名证书123456789101112131415server { listen 80; server_name domain; return 301 https://$host$request_uri;}server { listen 443 ssl; ssl_certificate ssl/nginx.crt; # 前面生成的 crt 证书文件 ssl_certificate_key ssl/nginx.key; # 前面生成的证书私钥 server_name domain; location / { root /var/www-html; index index.html; }} 相关资料http://seanlook.com/2015/01/18/openssl-self-sign-ca/ | 基于 OpenSSL 自签署证书http://www.cnblogs.com/littlehann/p/3738141.html | openSSL命令、PKI、CA、SSL证书原理https://cnzhx.net/blog/ssl-on-lamp-on-vps/http://seanlook.com/2015/01/15/openssl-certificate-encryption/ | OpenSSL 与 SSL 数字证书概念贴https://kb.cnblogs.com/page/194742/ | 数字证书及 CA 的扫盲http://netsecurity.51cto.com/art/200602/21066.htm | PKI/CA 技术的介绍http://cnzhx.net/blog/self-signed-certificate-as-trusted-root-ca-in-windows/ | 浏览器添加自签名证书https://aotu.io/notes/2016/08/16/nginx-https/index.html | Nginx 配置 HTTPS 服务器]]></content>
<categories>
<category>HTTPS</category>
</categories>
<tags>
<tag>HTTPS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[tcpdump使用技巧]]></title>
<url>%2F2019%2F05%2F07%2Ftcpdump%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%2F</url>
<content type="text"><![CDATA[tcpdump 是一个开源的命令行网络封包分析工具,一般用于服务器端网络流量、协议分析。今天有幸看到了有人很好地总结了关于 tcpdump 的使用技巧,在此转载下。本文转载自这里:https://github.com/linuxwiki/SourceWiki/blob/master/%E7%BD%91%E7%BB%9C/tcpdump.md 作者: 潜水大叔 一般情况下,非HTTP协议的网络分析,在服务器端用tcpdump比较多,在客户端用wireshark比较多,两个抓包软件的语法是一样的。 一、基本语法1.1、过滤主机 抓取所有经过eth1,目的或源地址是192.168.1.1的网络数据 1tcpdump -i eth1 host 192.168.1.1 指定源地址 1tcpdump -i eth1 src host 192.168.1.1 指定目的地址 1tcpdump -i eth1 dst host 192.168.1.1 1.2、过滤端口 抓取所有经过eth1,目的或源端口是25的网络数据 1tcpdump -i eth1 port 25 指定源端口 1tcpdump -i eth1 src port 25 指定目的端口 1tcpdump -i eth1 dst port 25 1.3、网络过滤123tcpdump -i eth1 net 192.168tcpdump -i eth1 src net 192.168tcpdump -i eth1 dst net 192.168 1.4、协议过滤12345tcpdump -i eth1 arptcpdump -i eth1 iptcpdump -i eth1 tcptcpdump -i eth1 udptcpdump -i eth1 icmp 1.5、常用表达式非 : ! or "not" (去掉双引号) 且 : && or "and" 或 : || or "or" 抓取所有经过eth1,目的地址是192.168.1.254或192.168.1.200端口是80的TCP数据 1tcpdump -i eth1 '((tcp) and (port 80) and ((dst host 192.168.1.254) or (dst host 192.168.1.200)))' 抓取所有经过eth1,目标MAC地址是00:01:02:03:04:05的ICMP数据 1tcpdump -i eth1 '((icmp) and ((ether dst host 00:01:02:03:04:05)))' 抓取所有经过eth1,目的网络是192.168,但目的主机不是192.168.1.200的TCP数据 1tcpdump -i eth1 '((tcp) and ((dst net 192.168) and (not dst host 192.168.1.200)))' 二、高级包头过滤首先了解如何从包头过滤信息 12345proto[x:y] : 过滤从x字节开始的y字节数。比如ip[2:2]过滤出3、4字节(第一字节从0开始排)proto[x:y] & z = 0 : proto[x:y]和z的与操作为0proto[x:y] & z !=0 : proto[x:y]和z的与操作不为0proto[x:y] & z = z : proto[x:y]和z的与操作为zproto[x:y] = z : proto[x:y]等于z 操作符 : >, <, >=, <=, =, != 2.1、IP头12345678910111213141516170 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Version| IHL |Type of Service| Total Length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Identification |Flags| Fragment Offset |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Time to Live | Protocol | Header Checksum |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Source Address |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Destination Address |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Options | Padding | <-- optional+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| DATA ... |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 本文只针对IPv4。 2.2、IP选项设置了吗?“一般”的IP头是20字节,但IP头有选项设置,不能直接从偏移21字节处读取数据。IP头有个长度字段可以知道头长度是否大于20字节。 123+-+-+-+-+-+-+-+-+|Version| IHL |+-+-+-+-+-+-+-+-+ 通常第一个字节的二进制值是:01000101,分成两个部分: 0100 = 4 表示IP版本0101 = 5 表示IP头32 bit的块数,5 x 32 bits = 160 bits or 20 bytes 如果第一字节第二部分的值大于5,那么表示头有IP选项。 下面介绍两种过滤方法(第一种方法比较操蛋,可忽略): a. 比较第一字节的值是否大于01000101,这可以判断IPv4带IP选项的数据和IPv6的数据。 01000101十进制等于69,计算方法如下(小提示:用计算器更方便) 1234567890 : 0 \1 : 2^6 = 64 \ 第一部分 (IP版本)0 : 0 /0 : 0 /-0 : 0 \1 : 2^2 = 4 \ 第二部分 (头长度)0 : 0 /1 : 2^0 = 1 / 64 + 4 + 1 = 69 如果设置了IP选项,那么第一自己是01000110(十进制70),过滤规则:1tcpdump -i eth1 'ip[0] > 69' IPv6的数据也会匹配,看看第二种方法。 b. 位操作 0100 0101 : 第一字节的二进制0000 1111 : 与操作<=========0000 0101 : 结果 正确的过滤方法 1tcpdump -i eth1 'ip[0] & 15 > 5' 或者1tcpdump -i eth1 'ip[0] & 0x0f > 5' 2.3、分片标记当发送端的MTU大于到目的路径链路上的MTU时就会被分片,这段话有点拗口,权威的请参考《TCP/IP详解》。唉,32借我的书没还,只能凑合写,大家记得看书啊。 分片信息在IP头的第七和第八字节: 123+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|Flags| Fragment Offset |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Bit 0: 保留,必须是0Bit 1: (DF) 0 = 可能分片, 1 = 不分片Bit 2: (MF) 0 = 最后的分片, 1 = 还有分片 Fragment Offset字段只有在分片的时候才使用。 要抓带DF位标记的不分片的包,第七字节的值应该是: 01000000 = 64 1tcpdump -i eth1 'ip[6] = 64' 2.4、抓分片包 匹配MF,分片包 1tcpdump -i eth1 'ip[6] = 32' 最后分片包的开始3位是0,但是有Fragment Offset字段。 匹配分片和最后分片 1tcpdump -i eth1 '((ip[6:2] > 0) and (not ip[6] = 64))' 测试分片可以用下面的命令: 1ping -M want -s 3000 192.168.1.1 2.5、匹配小TTLTTL字段在第九字节,并且正好是完整的一个字节,TTL最大值是255,二进制为11111111。 可以用下面的命令验证一下: 12$ ping -M want -s 3000 -t 256 192.168.1.200ping: ttl 256 out of range 123+-+-+-+-+-+-+-+-+| Time to Live |+-+-+-+-+-+-+-+-+ 在网关可以用下面的命令看看网络中谁在使用traceroute 1tcpdump -i eth1 'ip[8] < 5' 2.6、抓大于X字节的包 大于600字节 1tcpdump -i eth1 'ip[2:2] > 600' 2.7、更多的IP过滤首先还是需要知道TCP基本结构,再次推荐《TCP/IP详解》,卷一就够看的了,避免走火入魔。 TCP头 123456789101112131415161718190 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Source Port | Destination Port |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Sequence Number |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Acknowledgment Number |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Data | |C|E|U|A|P|R|S|F| || Offset| Res. |W|C|R|C|S|S|Y|I| Window || | |R|E|G|K|H|T|N|N| |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Checksum | Urgent Pointer |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| Options | Padding |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| data |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 抓取源端口大于1024的TCP数据包 1tcpdump -i eth1 'tcp[0:2] > 1024' 匹配TCP数据包的特殊标记 TCP标记定义在TCP头的第十四个字节 12345+-+-+-+-+-+-+-+-+|C|E|U|A|P|R|S|F||W|C|R|C|S|S|Y|I||R|E|G|K|H|T|N|N|+-+-+-+-+-+-+-+-+ 重复一下TCP三次握手,两个主机是如何勾搭的: 源发送SYN 目标回答SYN, ACK 源发送ACK 没女朋友的童鞋要学习一下: MM,你的手有空吗?-_- 有空,你呢?~_~ 我也有空 *_* 失败的loser是酱紫的: MM,这是你掉的板砖吗?(SYN)  ̄▽ ̄ 不是,找拍啊?(RST-ACK) ˋ﹏ˊ 只抓SYN包,第十四字节是二进制的00000010,也就是十进制的2 1tcpdump -i eth1 'tcp[13] = 2' 抓SYN, ACK (00010010 or 18) 1tcpdump -i eth1 'tcp[13] = 18' 抓SYN或者SYN-ACK 1tcpdump -i eth1 'tcp[13] & 2 = 2' 用到了位操作,就是不管ACK位是啥。 抓PSH-ACK 1tcpdump -i eth1 'tcp[13] = 24' 抓所有包含FIN标记的包(FIN通常和ACK一起,表示幽会完了,回见) 1tcpdump -i eth1 'tcp[13] & 1 = 1' 抓RST(勾搭没成功,伟大的greatwall对她认为有敏感信息的连接发RST包,典型的棒打鸳鸯) 1tcpdump -i eth1 'tcp[13] & 4 = 4' 下图详细描述了TCP各种状态的标记,方便分析。 2.8、大叔注tcpdump考虑了一些数字恐惧症者的需求,提供了部分常用的字段偏移名字: icmptype (ICMP类型字段)icmpcode (ICMP符号字段)tcpflags (TCP标记字段) ICMP类型值有: icmp-echoreply, icmp-unreach, icmp-sourcequench, icmp-redirect, icmp-echo, icmp-routeradvert, icmp-routersolicit, icmp-timxceed, icmp-paramprob, icmp-tstamp, icmp-tstampreply, icmp-ireq, icmp-ireqreply, icmp-maskreq, icmp-maskreply TCP标记值: tcp-fin, tcp-syn, tcp-rst, tcp-push, tcp-push, tcp-ack, tcp-urg 这样上面按照TCP标记位抓包的就可以写直观的表达式了: 只抓SYN包 1tcpdump -i eth1 'tcp[tcpflags] = tcp-syn' 抓SYN, ACK 1tcpdump -i eth1 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack != 0' 2.9、抓SMTP数据1tcpdump -i eth1 '((port 25) and (tcp[(tcp[12]>>2):4] = 0x4d41494c))' 抓取数据区开始为”MAIL”的包,”MAIL”的十六进制为0x4d41494c。 2.10、抓HTTP GET数据1tcpdump -i eth1 'tcp[(tcp[12]>>2):4] = 0x47455420' “GET “的十六进制是47455420 2.11、抓SSH返回1tcpdump -i eth1 'tcp[(tcp[12]>>2):4] = 0x5353482D' “SSH-“的十六进制是0x5353482D 1tcpdump -i eth1 '(tcp[(tcp[12]>>2):4] = 0x5353482D) and (tcp[((tcp[12]>>2)+4):2] = 0x312E)' 抓老版本的SSH返回信息,如”SSH-1.99..” 三、大叔注如果是为了查看数据内容,建议用tcpdump -s 0 -w filename把数据包都保存下来,然后用wireshark的Follow TCP Stream/Follow UDP Stream来查看整个会话的内容。 -s 0是抓取完整数据包,否则默认只抓68字节。 另外,用tcpflow也可以方便的获取TCP会话内容,支持tcpdump的各种表达式。 3.1、UDP头1234567891011 0 7 8 15 16 23 24 31+--------+--------+--------+--------+| Source | Destination || Port | Port |+--------+--------+--------+--------+| | || Length | Checksum |+--------+--------+--------+--------+| || DATA ... |+-----------------------------------+ 抓DNS请求数据 1tcpdump -i eth1 udp dst port 53 3.2、其他-c参数对于运维人员来说也比较常用,因为流量比较大的服务器,靠人工CTRL+C还是抓的太多,甚至导致服务器宕机,于是可以用-c参数指定抓多少个包。 1time tcpdump -nn -i eth0 'tcp[tcpflags] = tcp-syn' -c 10000 > /dev/null 上面的命令计算抓10000个SYN包花费多少时间,可以判断访问量大概是多少。 四、参考资料 tcpdump advanced filters]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>tcpdump</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Amazon CloudFront CDN + s3 源站跨域配置]]></title>
<url>%2F2019%2F05%2F06%2FAmazon-CloudFront-CDN-s3-%E6%BA%90%E7%AB%99%E8%B7%A8%E5%9F%9F%E9%85%8D%E7%BD%AE%2F</url>
<content type="text"><![CDATA[问题描述使用 Amazon CloudFront CDN + s3 源站托管前端静态页面,前端跨域请求时报错:1...blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 解决方法配置 Amazon CloudFront CDN 和 s3 支持跨域请求 1. s3 存储桶添加 CORS 配置存储桶—>权限—>CORS配置,添加类似下面 xml 格式的 CORS 配置:1234567891011121314<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>HEAD</AllowedMethod> <AllowedMethod>PUT</AllowedMethod> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <ExposeHeader>x-amz-server-side-encryption</ExposeHeader> <ExposeHeader>x-amz-request-id</ExposeHeader> <ExposeHeader>x-amz-id-2</ExposeHeader> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration> s3 CORS 相关配置项说明: <AllowedOrigin>*</AllowedOrigin>: 允许访问来源,* 表示允许所有来源访问,具体根据实际情况配置; <AllowedMethod>HEAD</AllowedMethod>: 允许的请求方法:GET、PUT、POST、DELETE、HEAD,不包含 OPTIONS 请求; <MaxAgeSeconds>3000</MaxAgeSeconds>: 指定在 Amazon S3 针对特定资源的预检 OPTIONS 请求做出响应后,浏览器缓存该响应的时间(以秒为单位,在本示例中为 3000 秒)。通过缓存响应,在需要重复原始请求时,浏览器无需向 Amazon S3 发送预检请求。 其他配置项解释见这里:https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/cors.html 使用 curl 测试存储桶 CORS 配置是否正确:1curl -I -v -L -H 'origin: <跨域请求的来源域名>' <s3资源地址> 如果响应头中有如下请求头,则表示配置正确:12Access-Control-Allow-Origin: <curl 请求时 -H 参数指定的值>Access-Control-Allow-Methods: <s3 存储桶 CORS 配置指定的请求方法> 2. CloudFront 分发行为中配置正确的”白名单标头”:打开 Amazon CloudFront 控制台—>点击要配置的分发—>选中”行为”列—>选中某条行为配置行,点击”编辑”—>”白名单标头”添加如下标头(CORS 相关配置,必须得添加,否则跨域请求时会出问题):1234Access-Control-Request-HeadersAccess-Control-Request-MethodCloudFront-Forwarded-ProtoOrigin 3. CloudFront 缓存行为允许 OPTIONS 请求:打开 Amazon CloudFront 控制台—>点击要配置的分发—>选中”行为”列—>选中某条行为配置行,点击”编辑”—>”缓存的 HTTP 方法” 下面勾选 OPTIONS 复选框 相关资料 https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/cors.html https://aws.amazon.com/cn/premiumsupport/knowledge-center/no-access-control-allow-origin-error/?nc1=h_ls]]></content>
<categories>
<category>Aws</category>
</categories>
<tags>
<tag>Aws</tag>
</tags>
</entry>
<entry>
<title><![CDATA[计算机网络协议学习总结]]></title>
<url>%2F2019%2F05%2F01%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[计算机网络协议分层目前关于网络通信的标准有两套协议,分别是 OSI 参考模型和 TCP/IP 模型。OSI 参考模型只用于理论研究,而 TCP/IP 模型更注重于实际应用,他们的关系如下:12345678910111213141516171819 +-------------------------+ +----------------------+ | | | | | OSI Model | | TCP/IP Model | | | | | +-------------------------+ +----------------------+ | Application Layer | | | +-------------------------+ | | | Presentation Layer | | Application Layer | +-------------------------+ | | | Session Layer | | |------------------------------------------------------------------------------- | Transport Layer | | Transport Layer | +-------------------------+ +----------------------+ | Network Layer | | Internet Layer |------------------------------------------------------------------------------- | Data Link Layer | | | +-------------------------+ | Network Access Layer | | Physical Layer | | | +-------------------------+ +----------------------+ TCP/IP 模型各层常见协议123456789101112131415161718192021222324 +--------+ +--------+ +-----------+ +--------+ | Application Layer | ping | | telnet | | OSPF | | DNS | User Space | +---+----+ +----+---+ +----+------+ +---+----+ | | | | | |--------------------------------------------------------------------------------------socket------------ | | | | | | +----+---+ | +---+----+ | Transport Layer | | TCP | | | UDP | | | +--------+----+ | +-------+--------+ | | | | | |---------------------------------------------------------------------------------- | | | | | | +---+----+ +--+--+--+-+ | Internet Layer | ICMP +-------------+ IP | Kernel Space | +--------+ +-----+----+ | | |---------------------------------------------------------------------------------- | | | +--------+ +-----+----+ +-------+ | Network Access Layer | ARP +------------+ DataLink +------+ RARP | | +--------+ +-----+----+ +-------+ | | v-------------------------------------------------------+-------------------------- Physical media TCP/IP 数据包的封装与解封在 TCP/IP 网络中,通信双方的交互过程就是对协议的封装与解封装的过程,发送方数据处理的方式是从高层到底层,逐层进行数据封装。接收方数据处理的方式是从底层到高层,逐层进行数据解封装。接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。 数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和尾中的过程。该过程是在协议数据单元(PDU)中实现的,其中每层的 PDU 一般由本层的协议头、协议尾和数据封装构成。 数据解封装是指对端的同等层对数据包解析的过程,通过解拆获取需要的数据,然后将数据传递到上层处理。 不同的协议层对数据包有不同的称谓: TCP 传给 IP 的数据单元称作 TCP 报文段或简称为 TCP 段(TCP segment); UDP 数据与 TCP 数据基本一致,唯一的不同是 UDP 传给 IP 的信息单元称作 UDP 数据报(UDP datagram),而且 UDP 的首部长为 8 字节; IP 传给网络接口层的数据单元称作 IP 数据报(IP datagram); 在数据链路层传输的数据单元称作帧(Frame ); 下面为应用层数据进入 TCP/IP 协议栈时的封装过程(解封装的过程与该过程相反):123456789101112131415161718192021222324 +-----------------+ | Application data| +-----------------+ | | v v +--------------------------------+ |TCP/UDP header| Application data| +--------------------------------+ | | +<-- TCP segment/UDP datagram -->+ v v +------------------------------------------+ |IP header|TCP/UDP header| Application data| +------------------------------------------+ | | +<-------------- IP datagram ------------->+ v v+--------------------------------------------------------------+|Eth header|IP header|TCP/UDP header| Application data|Eth tail|+--------------------------------------------------------------+ | |<-------------------- Ethernet frame --------------->+ | | | <----------- 40~1500 Bytes ------------> | 以太网数据帧封装以太网数据帧(也叫 MAC 帧)是网络硬件上面传送数据的最小单位,它承载了上层(网络层)的通信数据,是网络接口层的封包格式。我们一般接触比较多的是网络接口,比如 eht0、eht1…,这些网络接口上处理的主要数据包就是以太网数据帧。以太网数据帧封装格式在 RFC 894 中定义,具体格式如下:123456789101112131415161718192021222324+----------+---------+------+-----------------------------------+------+| MAC dst | Mac src | Type | Data | CRC |+----------+---------+-------------------------------------------------+ 6 6 2 | 46~1500 Bytes | 4 | Type | +------------------------------------------+ |0x800 | IP datagram | +------+-----------------------------------+ 2 46~1500 Bytes | Type | +-------------------------+ |0x806 | ARP |PAD| +------+------------------+ 2 28 18 | Type | +-------------------------+ |0x8035| RARP |PAD| +-------------------------+ 2 28 18 IP 数据报封装IP 协议处于网络层,几乎所有上层协议都会使用到 IP 协议。 IP 有两种版本,一种是目前使用最广泛的 IPv4 (Internet Protocol version 4, 因特网协定第四版), 一种则是预期未来会热门的 IPv6 。IPv4 记录的地址长度为 32 bit(4 Bytes),IPv6 的地址可以达到 128 bit(16 Bytes)。下面为 IP 数据报的封装格式: 123456789101112131415161718| 4 bits | 4 bits | 8 bitys |3bits| 13 bits |+-------------------------------------------------------------------------+| Version | IHL |Type of Service | Total Length |+---------+---------------------------------------------------------------+| Identification |Flags| Fragmentation Offset |+-------------------+-----------------------------------------------------+| Time To Live | Protocol | Header Checksum |+-------------------+----------------+------------------------------------+| Source Address |+-------------------------------------------------------------------------+| Destination Address |+-------------------------------------------------------------------------+| Options(Up to 40 bytes) |+-------------------------------------------------------------------------+| || Data || |+-------------------------------------------------------------------------+ Version(版本): 版本号指定 IP 协议的版本,长度为 4 bits。对于 IPv4 来说,其值为 4; IHL(Internet Header Length, IP 数据报头部长度):IP 数据报的头部长度,不包括数据部分,单位为 4 字节。由于该字段为 4 bits,所以 IP 封包头部最长为 (2^4-1)x4=60 字节; Type of Service(服务类型):服务类型包括一个 3 位的优先权字段(现已被弃用),4 位的 TOS 字段和 1 位保留字段(必须置0)。4 位 TOS 字段分别表示最小延时、最大吞吐量、最高可靠性和最小费用; Total Length(总长度):总长度表示整个 IP 数据报的长度,以字节为单位,可以看出最大值为 2^16-1=65535 字节; Identification(标识码):标示字段唯一的标示主机发送的每一个 IP 数据报,初始值由系统随机生成; Flags: 标志的第一位保留,第二位表示「禁止分片」。如果设置了这个位,系统不对 IP 报文分片。在这种情况下,如果 IP 数据报的长度超过 MTU(Max Transfer Unit,最大传输单元),IP 模块将丢弃该数据报并返回一个 ICMP 差错报文。第三位表示「更多分片」,如果为 1,表示后续还有该 IP 报文的分片; Fragment Offset(分片偏移): 分片偏移是分片相对原始 IP 数据报开始处的偏移,在接收端组合分片时,根据这个字段决定各分片的先后顺序; Time To Live(TTL, 存活时间): 表示这个IP封包的存活时间,范围为0-255。当这个IP封包通过一个路由器时, TTL就会减一,当TTL为0时,这个封包将会被直接丢弃; Protocol(协议代码): 协议字段用来区分上层的协议,其中 ICMP 是 1,TCP 是6,UDP是 17,更多可查看 /etc/protocols 文件; Header Checksum(头部校验码): 头部校验码由发送端填充,接收端使用 CRC 循环冗余校验算法检查 IP 数据报是否损坏; Source Address: 发送端 IP 地址 Destination Address: 接收端 IP 地址 Options: 选项部分长度最大为 40 字节,最小为 0 字节。因为头部长度字段最大可表示 15,也就是说 IP 数据报的报头最大可以有 60 字节,而前面这些已经占了 20 字节,故选项部分最多只能有 40 字节。 IP 分片当要发送的数据大于 MTU 的时候,通常需要进行 IP 分片,将数据分成多个 IP 数据报发送。MTU 一般为 1500 字节。 由上文可知,在 3 位的标志字段中,如果允许分片,则相同的 16 位的标识字段标识这些分片属于同一个数据块,片偏移标识这些分片的先后顺序。 IP 相关的概念IP 地址的组成IP 地址是一个 32 bits 的数值,为了记忆方便,一般将其写成 4 段以 . 号分隔的十进制形式:123IP的表示式: 00000000.00000000.00000000.00000000 ==> 0.0.0.0 11111111.11111111.11111111.11111111 ==> 255.255.255.255 一个 IP 地址分为 Net_ID(网络号)和 Host_ID(主机号)两部分:1234192.168.0.0~192.168.0.255 这个Class C 的说明:11000000.10101000.00000000.0000000011000000.10101000.00000000.11111111|----------Net_ID---------|HOST_ID| 同一网段在同一个物理网段内,主机的 IP 具有相同的 Net_ID(网络号) ,并且具有唯一的 Host_ID(主机号),那么这些 IP 群就是在同一个网段。 在同一个网段内,Net_ID 是不变的,而 Host_ID 则是不可重复,此外,Host_ID 在二进位的表示法当中,不可同时为 0 也不可同时为 1 ,因为全为 0 表示整个网段的地址(Network IP),而全为 1 则表示为广播的地址(Broadcast IP)。 IP 地址的分类InterNIC 将整个 IP 网段分为五种等级, 每种等级的范围主要与 IP 那 32 bits 数值的前面几个位有关,基本定义如下:12345678910111213141516以二进位说明Network第一个数字的定义: Class A : 0 xxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D的开头是0 |--net--|---------host------------| Class B : 10 xxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D的开头是10 |------net-------|------host------| Class C : 110 xxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D的开头是110 |-----------net-----------|-host--| Class D : 1110 xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D的开头是1110 Class E : 1111 xxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx ==> NetI_D的开头是1111五种分级在十进位的表示: Class A : 0.xx.xx.xx ~ 127.xx.xx.xx Class B : 128.xx.xx.xx ~ 191.xx.xx.xx Class C : 192. xx.xx.xx ~ 223.xx.xx.xx Class D : 224.xx.xx.xx ~ 239.xx.xx.xx Class E : 240.xx.xx.xx ~ 255.xx.xx.xx 子网划分前面我们知道了标准的 5 种 IP 等级划分,在一个 A 类网络中最多能有 2^24-2=16777214 台主机。其实在实际应用中一个网络不可能有这么多主机,有这么多主机意味着时时刻刻都会发生广播(比如 ARP 广播)风暴,导致一个网络的主机通信阻塞,根本不能正常工作。 为了解决上述问题,我们可以将一个大网络切分的更细点,让同一个网络中主机的数据量控制在合理的范围。具体的做法就是通过向 IP 的主机位借位,使其成为网络位,这样主机位就缩短了,从而减少了一个网络中主机的数量。 举例:一个 C 类 IP 地址网络位有 24 位,主机位有 8 位,那么这种情况下一个 C 类网络中就最多有 2^8-2=254 台主机。如果我们向主机位借一位,使其成为网络位,那么此时网络位就有 25 bit,主机位有 7 bit,所以此时一个网络中最多有 2^7-2=126 台主机。 子网掩码子网掩码(Netmask)也是 32 位数值,在数值上,位于 Net_ID(网络位)的为 1,而 Host_ID(主机位)为 0123456192.168.0.0~192.168.0.255 这个C Class 的Netmask 说明第一个IP: 11000000.10101000.00000000.00000000最后一个: 11000000.10101000.00000000.11111111 |----------Net_ID---------|--host--|Netmask : 11111111.11111111.11111111.00000000 <== Netmask 二进位 : 255 . 255 . 255 . 0 <== Netmask十进位 网络相关的几个参数 Network:一个网络中的第一个 IP, Host_ID(网络位) 全为 0 时的 IP 地址,此时表示整个网段; Broadcast:一个网络中的最后一个 IP,Host_ID(网络位) 全为 1 时的 IP 地址,即广播地址; Netmask:位于 Net_ID(网络位)的为 1,而 Host_ID(主机位)为 0,此时的 IP 即为子网掩码;例题:12345678910111213试着计算出 172.16.0.0,但 Net_ID 占用 23 位时,这个网络的 Netmask, Network, Broadcast 等参数?---------------------------------------------------------------------解答:由于 172.16.xxx.xxx 是在 Class B 的等级当中,亦即 Net_ID 是16 位才对。不过题目给的Net_ID 占用了23 个位!等于是向 Host_ID 借了(23-16) 7 个位用在 Net_ID 当中。所以整个 IP 的位址会变成这样:预设: 172 . 16 .0000000 0.00000000 |----Net_ID-------|--Host--| Network: 172.16.0000000 0.00000000 172.16.0.0Broadcast: 172.16.0000000 1.11111111 172.16.1.255Netmask: 11111111.11111111.1111111 0.00000000 255.255.254.0 CIDR(Classless Interdomain Routing)一般来说,如果我们知道了 Network 以及 Netmask 之后,就可以定义出该网段的所有 IP 了,因此,我们常常会以 Network 以及 Netmask 来表示一个网段,例如这样的写法:123Network/Netmask192.168.0.0/255.255.255.0192.168.0.0/24 <==因为 Net_ID 共有 24 bits 一般我们将一个网段 Network/网络位数量 这种写法称作 CIDR,比如:192.168.0.0/24。 路由概念在同一个网段中可以通过局域网广播的方式传递数据报,但是在不同的网段中通信就需要借助于路由器的帮忙了。 我们以下面图示的例子来做说明。下列图示当中共有两个不同的网段,分别是Network A 与Network B,这两个网段是经由一部路由器(Server A) 来进行资料转递的,那么当 PC01 这部主机想要传送资料到 PC11 时, 它的IP 封包该如何传输呢?1234567891011121314151617181920212223242526272829303132+--------------------------------------------------+| Network A || || +-------+ +-------+ || | | | | || | PC 01 | | PC 02 | || | | | | || +-------+--------+---------+-------+ || IP: 192.168.0.1 | IP: 192.168.0.2 || GW:192.168.0.254 | GW:192.168.0.254 |+--------------------------------------------------+ | 192.168.0.254 +--------+ | | |Server A| | | +--------+ 192.168.1.254 |+--------------------------------------------------+| | || +-------+--------+-----------+-------+ || | | | | || | PC 11 | | PC 12 | || | | | | || +-------+ +-------+ || IP: 192.168.1.1 IP: 192.168.1.2 || GW:192.168.1.254 GW:192.168.1.254 || || Network B |+--------------------------------------------------+ 查询 IP 封包的目标 IP 地址:当 PC01 有 IP 封包需要传送时,主机会查阅 IP 封包头部的目标 IP 地址; 查询本机的路由配置:PC01 主机会分析自己的路由表,当发现目标 IP 与本机 IP 的 Net_ID 相同时(同一网段),则PC01 会直接透过局域网功能,将数据包直接传送给目的地主机。 查询默认网关(default gateway):但在本案例中, PC01 与 PC11 并非在同一网段,因此 PC01 会分析路由表当中是否有其他相符合的路由设定,如果没有的话,就直接将该 IP 封包送到默认网关(default gateway)上头去,在本案例当中 default gateway 则是Server A。 送出封包至 gateway 后,不理会封包流向:当 IP 由 PC01 送给 Server A 之后, PC01 就不理会接下来的工作。而 Server A 接收到这个封包后,会依据上述的流程,也分析自己的路由表,然后向后继续传输到正确的目的地主机上。 TCP 协议TCP 协议处于四层参考模型的第三层,即传输层。同一层的还有 UDP 协议。TCP 和 UDP 的区别是:TCP 是可靠的,面向连接的,基于字节流的服务;UDP 是不可靠的,无连接的,面向数据块的服务。 解释如下: 每次使用 TCP 传输数据时,都要先建立一对一的连接,即面向连接的。而使用 UDP 时不必先建立连接,而是直接将数据广播出去就可以,即无连接; 因为 TCP 必须先建立连接,所以 TCP 传输的速度要比 UDP 慢; TCP 使用一系列机制来保证数据的可靠传输,包括数据确认机制,超时重传机制等。发送 TCP 数据时,应用层需将数据写入到 OS 提供的 buffer 里面,操作系统将其看作一连串的,没有边界的数据流,通过相对序号进行定位;而发送 UDP 数据时,应用层交给 OS 多大的数据包,操作系统就直接发送出去。根本不考虑效率,如果数据太大,可能会在 IP 层进行分片,如果数据太小,则每个数据包的有效载荷会比较低,浪费带宽; TCP 报头格式123456789101112131415|4 bits| 6 bits | 6 bits | 16 bits |+----------------+--------------------------------------+| Source Port | Destination Port |+--------------------------+----------------------------+| Sequence Number |+-------------------------------------------------------+| Acknowledge Number |+----------------+---------+----------------------------+|Data |Reserved | Code | Window ||Offset| | | |+----------------+--------------------------------------+| Checksum | Urgent Pointer |+--------------------------+----------------------------+| Options(0~40 bytes) |+-------------------------------------------------------+ Source Port & Destination Port(来源和目的端口):指明该 TCP 报文是由哪一个应用程序发送给哪一个应用程序的。因为端口号标示这应用层的一个服务进程; Sequence Number(序号):序号表明该报文段在整个数据流中相对于开始位置的偏移量; Acknowledge Number(确认号):确认号表明该报文是对对端的哪一个报文的确认,特别声明的是,只有当 ACK 标志为 1 时,确认号才有效。TCP 的数据确认机制就是通过这两个字段来实现的; Data Offset:标识该 TCP 头部有多少个 4 字节,共表示最长 15x4=60 字节。同IP头部; Reserved:保留不用,以便于将来扩展; Code(Control Flag,控制标志码):共 6 位:S、A、F、U、R、P,含义分别为 S –> SYN,若为 1,表明这是一个请求报文。A –> ACK,若为 1,表明确认号有效,这是一个确认报文。F –> FIN,若为 1,表明这是一个断开连接的请求报文。U –> URG,若为 1,表明紧急指针有效。R –> RST,若为 1,表明这是一个复位报文段,接收端会清空自己的发送缓冲区。P –> PSH,若为 1,提示接收端应用程序应立即从缓冲区中读走数据; Window(滑动窗口):用于流量控制,告诉对端自己的缓冲区大小,用于 TCP 的滑动窗口机制; Checksum(确认检查码):由发送端对 TCP 头部和数据部分进行 CRC 循环冗余校验后填充,接收端以此确定该数据报是否损坏; Urgent Pointer(紧急指针):若 URG 标志为 1,则紧急指针有效,指明 TCP 带外数据的相对位置; Options(选项):目前此字段仅应用于表示接收端可以接收的最大数据区段容量,若此字段不使用, 表示可以使用任意数据区段的大小,这个字段较少使用。选项最大长度为 40 字节,计算方法和 IP 头部选项的计算方法一致;]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[公有云服务器磁盘分区扩展]]></title>
<url>%2F2019%2F04%2F29%2F%E5%85%AC%E6%9C%89%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%A3%81%E7%9B%98%E5%88%86%E5%8C%BA%E6%89%A9%E5%B1%95%2F</url>
<content type="text"><![CDATA[问题描述在 web 控制台增加了磁盘空间大小,但是登陆到服务器 df -h 查看还是原先大小,并没有变化。 解决方法123456# 查看设备名称lsblk# /dev/xvdb : 磁盘设备名称,扩展该磁盘的第一个分区growpart /dev/xvdb 1# /dev/xvdb1: 分区名称,扩展分区文件系统大小resize2fs /dev/xvdb1]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[用 Dnsmasq 搭建一个简单的 DNS 服务器]]></title>
<url>%2F2019%2F04%2F27%2F%E7%94%A8-Dnsmasq-%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-DNS-%E6%9C%8D%E5%8A%A1%E5%99%A8%2F</url>
<content type="text"><![CDATA[本文主要介绍如何通过 Dnsmasq 工具搭建一个简单的 DNS 服务器,搭建完成后就可以马上测试使用了。 Dnsmasq 简介Dnsmasq 是一个轻量级的 DNS 缓存、DHCP、TFTP、PXE 服务器。 作为域名解析服务器,dnsmasq 可以通过缓存 DNS 请求来提高对访问过域名的解析速度。 作为 DHCP 服务器,Dnsmasq 可以用于为局域网电脑分配内网 IP 地址和提供路由。DNS 和 DHCP 两个功能可以同时或分别单独实现。 Dnsmasq 的应用场景我们一般使用 Dnsmasq 的 DNS 功能,总结了下基于该功能有如下使用场景: 作为内部局域网的一个 DNS 缓存服务器。通过 DNS 缓存的功能,可以提高应用程序域名解析的速度。比如 Kubernetes 的 kube-dns 组件中就用 dnsmasq 容器作为 DNS 服务器,用 kube-dns 容器作为 dnsmasq 的上游服务器。dnsmasq 本身具有缓存功能,所以可以大大提高集群中服务名的解析速度,而不需要每次解析请求都访问 kube-dns 容器。 实现 DNS 劫持功能。在局域网中,我们有时候可能希望暂时将某个公网域名解析到一个临时的地址,不走公网 DNS。 Dnsmasq 的工作原理Dnsmasq 在接受到用户的一个 DNS 请求时,首先会查找 /etc/hosts 这个文件,如果 /etc/hosts 文件没有请求的记录,然后查找 /etc/resolv.conf 中定义的外部 DNS(也叫上游 DNS 服务器,nameserver 配置),外部 DNS 通过递归查询查找到请求后响应给客户端,然后 dnsmasq 将请求结果缓存下来(缓存到内存)供后续的解析请求。 配置 Dnsmasq 为 DNS 缓存服务器,同时在 /etc/hosts 文件中加入本地内网解析,这样一来每当内网机器查询时就会优先查询 hosts 文件,这就等于将 /etc/hosts 共享给全内网机器使用,从而解决内网机器互相识别的问题。相比逐台机器编辑 hosts 文件或者添加 Bind DNS 记录,仅编辑一个 hosts 文件,这简直太容易了。 Dnsmasq 安装Dnsmasq 的安装特别简单,以 Centos7 下安装为例:1sudo yum install -y dnsmasq Dnsmasq 配置及启动配置Dnsmasq 的所有的配置都在 /etc/dnsmasq.conf 这一个文件中完成 。官方在配置文件 /etc/dnsmasq.conf 中针对选项和参数等做了比较好的注释说明,我们可以将配置做一次备份,以便以后查阅。默认情况下 dnsmasq.conf 中只开启了最后 include 项,因此可以在 /etc/dnsmasq.conf 的前提下,将自定义的配置放到 /etc/dnsmasq.d 目录下的一个任意名字的配置文件当中。 注意: /etc/dnsmasq.d/*.conf 的优先级大于 /etc/dnsmasq.conf 关于 dnsmasq 的配置项非常多,具体配置项含义在 /etc/dnsmasq.conf 中有详细的说明,本文如下配置实现一个简单的 DNS 服务器(配置文件放到了 /etc/dnsmasq.d/ 目录下,命名为 dnsmasq.conf):1234567891011121314151617181920#dnsmasq 启动监听的端口号port=53#从不转发格式错误的域名domain-needed#默认情况下Dnsmasq会发送查询到它的任何上游DNS服务器上,如果取消注释,#则Dnsmasq则会严格按照/etc/resolv.conf中的 DNS Server 顺序进#行查询,直到第一个成功解析成功为止。strict-order# dnsmasq 缓存大小,默认 150cache-size=8192#address 可以将指定的域解析为一个IP地址,即泛域名解析。# 将 *.taobao.com 解析到 10.10.10.10address=/taobao.com/10.10.10.10#把所有.cn的域名全部通过 114.114.114.114 这台国内DNS服务器来解析server=/cn/114.114.114.114 为了验证 /etc/hosts 文件解析是否起作用,我们也向 hosts 文件添加几条记录:1210.4.29.106 ansible10.4.24.116 www.baidu.com 注意:/etc/hosts 文件修改后需要重启 dnsmasq,否则修改不会生效。重启方法:systemctl restart dnsmasq 启动1234# 设置为开机自启动systemctl enable dnsmasq# 启动 dnsmasq 服务systemctl start dnsmasq 测试使用 Dnsmasq我们搭建的 DNS 服务器地址为:192.168.10.200 使用 dig 命令指定 DNS 服务器地址来查看解析是否生效:123dig @192.168.10.200 ansibledig @192.168.10.200 www.taobao.comdig @192.168.10.200 ip.cn 验证 Dnsmasq 缓存功能是否生效首先使用 dig 查询一个之前未查询过的域名,然后看响应时间是多少:第一次 dig:123456dig @192.168.10.200 qhh.me...... ;; Query time: 478 msec;; SERVER: 192.168.10.200#53(192.168.10.200);; WHEN: Sat Apr 27 21:45:24 CST 2019;; MSG SIZE rcvd: 56 第二次 dig:123456dig @192.168.10.200 qhh.me ......;; Query time: 0 msec;; SERVER: 192.168.10.200#53(192.168.10.200);; WHEN: Sat Apr 27 21:45:32 CST 2019;; MSG SIZE rcvd: 67 可以看到两次同样的 dig 查询的时间不一样,第一次 478 ms,第二次 0 ms,说明第二次直接是从缓存中取的数据,没有向上游服务器发起请求。 Dnsmasq 的缓存在哪里?如何查看?dnsmasq 的缓存并不是保存在本地磁盘的某个文件,而是存储在内存中,因此是无法直接查看的。当然作为一个 Geek,想要查看缓存的内容也是有办法的: dnsmasq 启动参数添加 –log-queries 12vi /usr/lib/systemd/system/dnsmasq.serviceExecStart=/usr/sbin/dnsmasq -k 改为:ExecStart=/usr/sbin/dnsmasq -k --log-queries 重新加载 Systemd Unit 配置文件 1systemctl daemon-reload 重启 dnsmasq 1systemctl restart dnsmasq 执行如下命令 dump 出来缓存内容到 journal 日志 1kill -SIGUSR1 <PID> 查看 dump 出来的 dns 记录(dnsmasq 当前缓存的内容) 1journalctl -u dnsmasq 参考资料http://www.thekelleys.org.uk/dnsmasq/doc.html | dnsmasq 官方文档https://www.hi-linux.com/posts/30947.html | 一篇比较全面的博客https://yq.aliyun.com/articles/582537 | 一篇比较精简的博客http://flux242.blogspot.com/2012/06/dnsmasq-cache-size-tuning.html | 介绍了 dnsmasq 的基本概念、缓存淘汰机制等相关内容]]></content>
<categories>
<category>DNS</category>
</categories>
<tags>
<tag>DNS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 kubeadm 搭建 kubernetes 集群]]></title>
<url>%2F2019%2F03%2F19%2F%E4%BD%BF%E7%94%A8-kubeadm-%E6%90%AD%E5%BB%BA-kubernetes-%E9%9B%86%E7%BE%A4%2F</url>
<content type="text"><![CDATA[kubeadm 简介kubeadm 是 k8s 官方提供的用于快速部署 k8s 集群的命令行工具,也是官方推荐的最小化部署 k8s 集群的最佳实践,比起直接用二进制部署能省去很多工作,因为该方式部署的集群的各个组件以 docker 容器的方式启动,而各个容器的启动都是通过该工具配自动化启动起来的。 kubeadm 不仅仅能部署 k8s 集群,还能很方便的管理集群,比如集群的升级、降级、集群初始化配置等管理操作。 kubeadm 的设计初衷是为新用户提供一种便捷的方式来首次试用 Kubernetes, 同时也方便老用户搭建集群测试他们的应用。 kubeadm 的使用案例: 新用户可以从 kubeadm 开始来试用 Kubernetes。 熟悉 Kubernetes 的用户可以使用 kubeadm 快速搭建集群并测试他们的应用。 大型的项目可以将 kubeadm 和其他的安装工具一起形成一个比较复杂的系统。 安装环境要求 一台或多台运行着下列系统的机器: Ubuntu 16.04+ Debian 9 CentOS 7 RHEL 7 Fedora 25/26 HypriotOS v1.0.1+ Container Linux (针对1800.6.0 版本测试) 每台机器 2 GB 或更多的 RAM (如果少于这个数字将会影响您应用的运行内存) 2 CPU 核心或更多(节点少于 2 核的话新版本 kubeadm 会报错) 集群中的所有机器的网络彼此均能相互连接(公网和内网都可以) 禁用 Swap 交换分区。(Swap 分区必须要禁掉,否则安装会报错) 准备环境本文使用 kubeadm 部署一个 3 节点的 k8s 集群:1 个 master 节点,2 个 node 节点。各节点详细信息如下: Hostname IP OS 发行版 内存(GB) CPU(核) k8s-master 192.168.10.100 Centos7 2 2 k8s-node-1 192.168.10.101 Centos7 2 2 k8s-node-2 192.168.10.102 Centos7 2 2 kubeadm 安装 k8s 集群完整流程 使用 Vagrant 启动 3 台符合上述要求的虚拟机 调整每台虚拟机的服务器参数 各节点安装 docker、kubeadm、kubelet、kubectl 工具 使用 kubeadm 部署 master 节点 安装 Pod 网络插件(CNI) 使用 kubeadm 部署 node 节点 接下来我们依次介绍每步的具体细节: 使用 Vagrant 启动 3 台符合上述要求的虚拟机Vagrant 的使用在这里不具体介绍了,如需了解请点击这里。本文用到的 Vagrantfile:123456789101112131415161718192021222324252627282930313233# -*- mode: ruby -*-# vi: set ft=ruby :# author: qhh0205$num_nodes = 2Vagrant.configure("2") do |config| # k8s 主节点定义及初始化配置 config.vm.define "k8s-master" do | k8s_master | k8s_master.vm.box = "Centos7" k8s_master.vm.hostname = "k8s-master" k8s_master.vm.network "private_network", ip: "192.168.10.100" k8s_master.vm.provider "virtualbox" do | v | v.name = "k8s-master" v.memory = "2048" v.cpus = 2 end end # k8s node 节点定义及初始化配置 (1..$num_nodes).each do |i| config.vm.define "k8s-node-#{i}" do |node| node.vm.box = "Centos7" node.vm.hostname = "k8s-node-#{i}" node.vm.network "private_network", ip: "192.168.10.#{i+100}" node.vm.provider "virtualbox" do |v| v.name = "k8s-node-#{i}" v.memory = "2048" v.cpus = 2 end end endend 进入 Vagrantfile 文件所在目录,执行如下命令启动上述定义的 3 台虚拟机:1vagrant up 调整每台虚拟机的服务器参数 禁用 swap 分区:临时禁用:swapoff -a永久禁用:sed -i '/swap/s/^/#/g' /etc/fstabswap 分区必须禁止掉,否则 kubadm init 自检时会报如下错误: 1[ERROR Swap]: running with swap on is not supported. Please disable swap 将桥接的 IPv4 流量传递到 iptables 的链: 12345$ cat > /etc/sysctl.d/k8s.conf << EOFnet.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1EOF$ sysctl --system 如果不进行这一步的设置,kubadm init 自检时会报如下错误: 1[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables contents are not set to 1 关闭网络防火墙: 12systemctl stop firewalldsystemctl disable firewalld 禁用 SELinux:临时关闭 selinux(不需要重启主机): setenforce 0永久关闭 selinux(需要重启主机才能生效):sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config 各节点安装 docker、kubeadm、kubelet、kubectl 工具安装 Docker配置 Docker yum 源(阿里 yum 源):1curl -sS -o /etc/yum.repos.d/docker-ce.repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 安装 docker:123yum install --nogpgcheck -y yum-utils device-mapper-persistent-data lvm2yum install --nogpgcheck -y docker-cesystemctl enable docker && systemctl start docker 安装 kubeadm、kubelet、kubectl 工具配置相关工具 yum 源(阿里 yum 源):123456789cat > /etc/yum.repos.d/kubernetes.repo << EOF[kubernetes]name=Kubernetesbaseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64enabled=1gpgcheck=1repo_gpgcheck=1gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpgEOF 安装 kubeadm、kubelet、kubectl:其实 kubeadm、kubelet、kubectl 这三个工具的版本命名是一致的(和 k8s 版本命名一致),我们可以指定安装特定的版本,即安装指定版本的 k8s 集群。 查看哪些版本可以安装:yum --showduplicates list kubeadm|kubelet|kubectl 在这里我们安装 1.13.2 版本:12345yum install -y kubeadm-1.13.2 kubelet-1.13.2 kubectl-1.13.2# 设置 kubelet 开机自启动: kubelet 特别重要,如果服务器重启后 kubelet# 没有启动,那么 k8s 相关组件的容器就无法启动。在这里不需要把 kubelet 启动# 起来,因为现在还启动不起来,后续执行的 kubeadm 命令会自动把 kubelet 拉起来。systemctl enable kubelet 使用 kubeadm 部署 master 节点登陆 master 节点执行如下命令:1kubeadm init --kubernetes-version v1.13.2 --image-repository registry.aliyuncs.com/google_containers --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=192.168.10.100 参数说明:--kubernetes-version: 安装指定版本的 k8s 版本,该参数和 kubeadm 的版本有关,特定版本的 kubeadm 并不能安装所有版本的 k8s,最好还是 kubeadm 的版本和该参数指定的版本一致。 --image-repository: 该参数仅在高版本(具体哪个版本没仔细查,反正在 1.13.x 中是支持的)的 kubeadm 中支持,用来设置 kubeadm 拉取 k8s 各组件镜像的地址,默认拉取的地址是:k8s.gcr.io。众所周知 k8s.gcr.io 国内是无法访问的,所以在这里改为阿里云镜像仓库。 --pod-network-cidr: 设置 pod ip 的网段 ,网段之所以是 10.244.0.0/16,是因为后面安装 flannel 网络插件时,yaml 文件里面的 ip 段也是这个,两个保持一致,不然可能会使得 Node 间 Cluster IP 不通。这个参数必须得指定,如果这里不设置的话后面安装 flannel 网络插件时会报如下错误:1E0317 17:02:15.077598 1 main.go:289] Error registering network: failed to acquire lease: node "k8s-master" pod cidr not assigned --apiserver-advertise-address: API server 用来告知集群中其它成员的地址,这个参数也必须得设置,否则 api-server 容器启动不起来,该参数的值为 master 节点所在的本地 ip 地址。 题外话:像之前没有 --image-repository 这个参数时,大家为了通过 kubeadm 安装 k8s 都是采用”曲线救国”的方式:先从别的地方把同样的镜像拉到本地(当然镜像的 tag 肯定不是 k8s.gcr.io/xxxx),然后将拉下来的镜像重新打个 tag,tag 命名成和执行 kubeadm init 时真正拉取镜像的名称一致(比如:k8s.gcr.io/kube-controller-manager-amd64:v1.13.2)。这么做显然做了很多不必要的工作,幸好现在有了 --image-repository 这个参数能自定义 kubeadm 拉取 k8s 相关组件的镜像地址了。 执行上面命令后,如果出现如下输出(截取了部分),则表示 master 节点安装成功了:123456789101112131415161718192021...[bootstraptoken] creating the "cluster-info" ConfigMap in the "kube-public" namespace[addons] Applied essential addon: CoreDNS[addons] Applied essential addon: kube-proxyYour Kubernetes master has initialized successfully!To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/configYou should now deploy a pod network to the cluster.Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/You can now join any number of machines by running the following on each nodeas root: kubeadm join 192.168.10.100:6443 --token jm6o42.9ystvjarc6u09pjp --discovery-token-ca-cert-hash sha256:64405f3a90597e0ebf1f33134649196047ce74df575cb1a7b38c4ed1e2f94421 根据上面输出知道:要开始使用集群普通用户执行下面命令:123mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config 现在就可以使用 kubectl 访问集群了:123[vagrant@k8s-master ~]$ kubectl get nodeNAME STATUS ROLES AGE VERSIONk8s-master NotReady master 13m v1.13.2 可以看出现在 master 节点还是 NotReady 状态,这是因为默认情况下,为了保证 master 的安全,master 是不会被分配工作负载的。你可以取消这个限制通过输入(不建议这样做,我们后面会向集群中添加两 node 工作节点):1$ kubectl taint nodes --all node-role.kubernetes.io/master- 安装 Pod 网络插件(CNI)Pod 网络插件有很多种,具体见这里:https://kubernetes.io/docs/concepts/cluster-administration/addons/,我们选择部署 Flannel 网络插件:1kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 使用 kubeadm 部署 node 节点前面已经将 master 节点部署完了,接下来部署 node 节点就很简单了,在 node节点执行如下命令将自己加到 k8s 集群中(复制 master 节点安装完后的输出):1kubeadm join 192.168.10.100:6443 --token jm6o42.9ystvjarc6u09pjp --discovery-token-ca-cert-hash sha256:64405f3a90597e0ebf1f33134649196047ce74df575cb1a7b38c4ed1e2f94421 出现如下输出(截取了部分)表示成功将 node 添加到了集群:123456...This node has joined the cluster:* Certificate signing request was sent to apiserver and a response was received.* The Kubelet was informed of the new secure connection details.Run 'kubectl get nodes' on the master to see this node join the cluster. 在 master 节点查看 node 状态,如果都为 Ready,则表示集群搭建完成:1234[vagrant@k8s-master ~]$ kubectl get nodesNAME STATUS ROLES AGE VERSIONk8s-master Ready master 14m v1.13.2k8s-node-1 Ready <none> 3m v1.13.2 用同样的方法把另一个节点也加入到集群中。 相关资料https://purewhite.io/2017/12/17/use-kubeadm-setup-k8s/https://kubernetes.io/zh/docs/setup/independent/install-kubeadm/https://k8smeetup.github.io/docs/admin/kubeadm/]]></content>
<categories>
<category>Kubenetes</category>
</categories>
<tags>
<tag>Kubernetes</tag>
</tags>
</entry>
<entry>
<title><![CDATA[.dockerignore 文件从入门到实践]]></title>
<url>%2F2019%2F02%2F24%2Fdockerignore-%E6%96%87%E4%BB%B6%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[简介.dockerignore 文件的作用类似于 git 工程中的 .gitignore 。不同的是 .dockerignore 应用于 docker 镜像的构建,它存在于 docker 构建上下文的根目录,用来排除不需要上传到 docker 服务端的文件或目录。 docker 在构建镜像时首先从构建上下文找有没有 .dockerignore 文件,如果有的话则在上传上下文到 docker 服务端时忽略掉 .dockerignore 里面的文件列表。这么做显然带来的好处是: 构建镜像时能避免不需要的大文件上传到服务端,从而拖慢构建的速度、网络带宽的消耗; 可以避免构建镜像时将一些敏感文件及其他不需要的文件打包到镜像中,从而提高镜像的安全性; .dockerignore 文件编写方法.dockerignore 文件的写法和 .gitignore 类似,支持正则和通配符,具体规则如下: 每行为一个条目; 以 # 开头的行为注释; 空行被忽略; 构建上下文路径为所有文件的根路径; 文件匹配规则具体语法如下: 规则 行为 */temp* 匹配根路径下一级目录下所有以 temp 开头的文件或目录 */*/temp* 匹配根路径下两级目录下所有以 temp 开头的文件或目录 temp? 匹配根路径下以 temp 开头,任意一个字符结尾的文件或目录 **/*.go 匹配所有路径下以 .go 结尾的文件或目录,即递归搜索所有路径 *.md!README.md 匹配根路径下所有以 .md 结尾的文件或目录,但 README.md 除外 ⚠️注意事项:如果两个匹配语法规则有包含或者重叠关系,那么以后面的匹配规则为准,比如:123*.md!README*.mdREADME-secret.md 这么写的意思是将根路径下所有以 .md 结尾的文件排除,以 README 开头 .md 结尾的文件保留,但是 README-secret.md 文件排除。 再来看看下面这种写法(同上面那种写法只是对换了后面两行的位置):123*.mdREADME-secret.md!README*.md 这么写的意思是将根路径下所有以 .md 结尾和名称为 README-secret.md 的文件排除,但所有以 README 开头 .md 结尾的文件保留。这样的话 README-secret.md 依旧会被保留,并不会被排除,因为 README-secret.md 符合 !README*.md 规则。 使用案例前段时间帮前端同学写了一个 Dockerfile,Dockerfile 放在 git 仓库根路径下,发现 git 工程中有很多真正应用跑起来用不到的文件,如果直接在 Dockerfile 中使用 COPY 或 ADD 指令拷贝文件,那么很显然会把很多不需要的文件拷贝到镜像中,从而会拖慢构建镜像的过程,产生的镜像也比较臃肿。解决方法就是编写 .dockerignore 文件,忽略掉不需要的文件,然后放到 docker 构建上下文的根路径下。.dockerignore 及 Dockerfile 文件内容如下:.dockerignore:123456.git_mockDatadeletedemail-templatesscriptstatic Dockerfile:123456789FROM node:8-alpineCOPY . /app/nodeWORKDIR /app/nodeRUN yarn installEXPOSE 8026CMD ["yarn", "run", "tool-dev"] 使用 .dockerignore 前后上传到 docker 服务端的构建上下文大小对比:使用前(73.36MB):123[vagrant@docker]$ docker build -t tool:5.0 -f Dockerfile-frontend-tool .Sending build context to Docker daemon 73.36MBStep 1/6 : FROM node:8-alpine 使用后(11.38MB):123[vagrant@docker]$ docker build -t tool:6.0 -f Dockerfile-frontend-tool .Sending build context to Docker daemon 11.38MBStep 1/6 : FROM node:8-alpine 参考资料https://docs.docker.com/engine/reference/builder/#dockerignore-file]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解 Docker 构建上下文]]></title>
<url>%2F2019%2F02%2F17%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-Docker-%E6%9E%84%E5%BB%BA%E4%B8%8A%E4%B8%8B%E6%96%87%2F</url>
<content type="text"><![CDATA[本文通过具体实践深入解读 Docker 构建上下文的含义,解惑或者纠正很大一部分人对 Docker 构建上下文的理解误区。本文主要讨论如下主题: 对 Docker 构建上下文的理解误区 理解 Docker 的架构 理解 docker build 的工作原理 正确理解 Docker 构建上下文 对 Docker 构建上下文的理解误区我们都知道,构建一个 Docker 镜像非常简单,大家一般都会这么做(当然这么做是完全正确的): 跳到 Dockerfile 所在目录; 执行 docker build 构建命令:1docker build -t <imageName:imageTag> . 通过上面的工作流,很容易形成这样的理解误区: docker build 后面的 . 为 Dockerfile 所在的目录; Dockerfile 文件名 必须为 Dockerfile; 其实上面这种理解是错误的,要想准确理解其含义,首先我们需要先了解下 Docker 的架构和 docker build 的工作原理。 理解 Docker 的架构Docker 是一个典型的 C/S 架构的应用,分为 Docker 客户端(即平时敲的 docker 命令) Docker 服务端(dockerd 守护进程)。 Docker 客户端通过 REST API 和服务端进行交互,docker 客户端每发送一条指令,底层都会转化成 REST API 调用的形式发送给服务端,服务端处理客户端发送的请求并给出响应。 Docker 镜像的构建、容器创建、容器运行等工作都是 Docker 服务端来完成的,Docker 客户端只是承担发送指令的角色。 Docker 客户端和服务端可以在同一个宿主机,也可以在不同的宿主机,如果在同一个宿主机的话,Docker 客户端默认通过 UNIX 套接字(/var/run/docker.sock)和服务端通信。 理解 docker build 的工作原理理解了 Docker 的架构就很容易理解 docker build 构建镜像的工作原理了。docker build 构建镜像的流程大概如下: 执行 docker build -t <imageName:imageTag> . ; Docker 客户端会将构建命令后面指定的路径(.)下的所有文件打包成一个 tar 包,发送给 Docker 服务端; Docker 服务端收到客户端发送的 tar 包,然后解压,根据 Dockerfile 里面的指令进行镜像的分层构建; 正确理解 Docker 构建上下文了解了 Docker 的架构和镜像构建的工作原理后,Docker 构建上下文也就容易理解了。Docker 构建上下文就是 Docker 客户端上传给服务端的 tar 文件解压后的内容,也即 docker build 命令行后面指定路径下的文件。 Docker 镜像的构建是在远程服务端进行的,所以客户端需要把构建所需要的文件传输给服务端。服务端以客户端发送的文件为上下文,也就是说 Dockerfile 中指令的工作目录就是服务端解压客户端传输的 tar 包的路径。 关于 docker build 指令的几点重要的说明: 如果构建镜像时没有明确指定 Dockerfile,那么 Docker 客户端默认在构建镜像时指定的上下文路径下找名字为 Dockerfile 的构建文件; Dockerfile 可以不在构建上下文路径下,此时需要构建时通过 -f 参数明确指定使用哪个构建文件,并且名称可以自己任意命名。 下面通过具体的实例来理解下: 首先创建一个简单的 demo 工程,工程结构如下:1234567helloworld-app├── Dockerfile└── docker ├── app-1.0-SNAPSHOT.jar ├── hello.txt └── html └── index.html Dockerfile 内容:123FROM busyboxCOPY hello.txt .COPY html/index.html . 实践1:直接进入 helloworld-app 目录进行镜像构建,以 docker 目录为构建上下文:12$ docker build -t hello-app:1.0 dockerunable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /Users/haohao/opensource/helloworld-app/docker/Dockerfile: no such file or directory 可以看出默认 docker 客户端从 docker 构建上下文路径下找名字为 Dockerfile 的构建文件。 实践2:明确指定 Dockerfile 文件进行镜像构建,还是以 docker 目录为构建上下文:12345678910$ docker build -f Dockerfile -t hello-app:1.0 docker Sending build context to Docker daemon 96.61MBStep 1/3 : FROM busybox ---> d8233ab899d4Step 2/3 : COPY hello.txt . ---> 3305fc373120Step 3/3 : COPY html/index.html . ---> efdefc4e6eb2Successfully built efdefc4e6eb2Successfully tagged hello-app:1.0 从输出结果可以得知: 构建镜像时客户端会先给服务端发送构建上下路径下的内容(即 docker 目录下的文件); Dockerfile 可以不在构建上下文路径下; Dockerfile 中指令的工作目录是服务端解压客户端传输的 tar 包的路径; 实践3:以当前目录为构建上下文路径:12345678$ lsDockerfile docker$ docker build -t hello-app:2.0 .Sending build context to Docker daemon 96.62MBStep 1/3 : FROM busybox ---> d8233ab899d4Step 2/3 : COPY hello.txt .COPY failed: stat /var/lib/docker/tmp/docker-builder375982663/hello.txt: no such file or directory 可以看出: 镜像构建上下文路径并不是 Dockerfile 文件所在的路径; Dockerfile 中指令的工作目录是服务端解压客户端传输的 tar 包的路径,因为 COPY 指令失败了,意味着当前目录并没有 hello.txt 文件; 相关资料https://docs.docker.com/engine/reference/commandline/build/https://yeasy.gitbooks.io/docker_practice/content/image/build.html]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux 进程树查看工具 pstree]]></title>
<url>%2F2019%2F02%2F16%2FLinux-%E8%BF%9B%E7%A8%8B%E6%A0%91%E6%9F%A5%E7%9C%8B%E5%B7%A5%E5%85%B7-pstree%2F</url>
<content type="text"><![CDATA[简介pstree 是 Linux 下的一个用于展示进程树结构的工具,类似于 tree 展示目录树一样,可视化地查看进程的继承关系。pstree 工具其实是 PSmisc 工具集的成员之一,PSmisc 工具集由 4 个实用的 Linux 进程管理工具(通过 Linux 的 /proc 文件系统实现)组成: fuser - identifies what processes are using files. killall - kills a process by its name, similar to a pkill found in some other Unices. pstree - Shows currently running processes in a tree format. peekfd - Peek at file descriptors of running processes. pstree 带来的方便之处:一条命令就可以很轻松地追溯某个进程的继承关系,再也不需要通过多次执行 ps -ef 一级一级的查看进程的继承关系。 安装On Fedora/Red Hat/CentOS1sudo yum install -y psmisc On Mac OS1brew install pstree On Ubuntu/Debian APT1sudo apt-get install psmisc 使用语法pstree [选项] 选项 -a:显示每个程序的完整指令,包含路径,参数或是常驻服务的标示;-c:不使用精简标示法;-G:使用VT100终端机的列绘图字符;-h:列出树状图时,特别标明现在执行的程序;-H<程序识别码>:此参数的效果和指定”-h”参数类似,但特别标明指定的程序;-l:采用长列格式显示树状图;-n:用程序识别码排序。预设是以程序名称来排序;-p:显示程序识别码;-u:显示用户名称;-U:使用UTF-8列绘图字符;-V:显示版本信息。 示例 显示 PID 为 2858 进程的进程树; 123[vagrant@docker ~]$ pstree 2858dockerd─┬─2*[docker-proxy───4*[{docker-proxy}]] └─9*[{dockerd}] 显示 PID 为 2858 进程的进程树,同时列出每个进程的 pid;注意:可以观察出,大括号括起来的为线程! 123456789101112131415161718[vagrant@docker ~]$ pstree -p 2858dockerd(2858)─┬─docker-proxy(4378)─┬─{docker-proxy}(4379) │ ├─{docker-proxy}(4380) │ ├─{docker-proxy}(4381) │ └─{docker-proxy}(4382) ├─docker-proxy(6582)─┬─{docker-proxy}(6583) │ ├─{docker-proxy}(6585) │ ├─{docker-proxy}(6586) │ └─{docker-proxy}(6587) ├─{dockerd}(2997) ├─{dockerd}(2998) ├─{dockerd}(2999) ├─{dockerd}(3000) ├─{dockerd}(3222) ├─{dockerd}(3223) ├─{dockerd}(3224) ├─{dockerd}(4480) └─{dockerd}(4493) 显示 PID 为 2858 进程的进程树,同时列出每个进程的 pid 和启动进程的命令行; 123456789101112131415161718192021[vagrant@docker ~]$ pstree -p 2858 -adockerd,2858 -H fd:// ├─docker-proxy,4378 -proto tcp -host-ip 0.0.0.0 -host-port 3306 -container-ip 172.17.0.2 -container-port 3306 │ ├─{docker-proxy},4379 │ ├─{docker-proxy},4380 │ ├─{docker-proxy},4381 │ └─{docker-proxy},4382 ├─docker-proxy,6582 -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 80 │ ├─{docker-proxy},6583 │ ├─{docker-proxy},6585 │ ├─{docker-proxy},6586 │ └─{docker-proxy},6587 ├─{dockerd},2997 ├─{dockerd},2998 ├─{dockerd},2999 ├─{dockerd},3000 ├─{dockerd},3222 ├─{dockerd},3223 ├─{dockerd},3224 ├─{dockerd},4480 └─{dockerd},4493 直接执行 pstree 默认列出整个系统的进程树; 相关资料http://man.linuxde.net/pstreehttp://psmisc.sourceforge.nethttps://www.wikiwand.com/en/Pstree]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker 启动 MySQL 最佳实践]]></title>
<url>%2F2019%2F01%2F27%2FDocker-%E5%90%AF%E5%8A%A8-MySQL-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[本文主要介绍使用 Docker 启动 MySQL 服务的最佳实践,Docker 镜像来自 docker 官方镜像。 启动一个 MySql 5.7 实例关于版本的选择,修改镜像 tag 即可,支持的 tag 在 docker hub 仓库 有说明。12docker run --name mysql5.7 --restart always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=12345 \-v /home/vagrant/mysql5.7/data:/var/lib/mysql -d mysql:5.7 参数说明 --name mysql5.7: 指定运行容器名称 --restart always: 容器意外退出后自动重启 -p 3306:3306: 映射主机 3306 端口到容器 3306 端口 -e MYSQL_ROOT_PASSWORD=12345: 指定 msyql root 密码,该参数是为必须的 -v /home/vagrant/mysql5.7/data:/var/lib/mysql: mysql 数据持久化,主机 /home/vagrant/mysql5.7/data 目录挂载到容器 /var/lib/mysql 目录 连接 MySqlmysql 容器连接服务端:1docker run -it --rm mysql:5.7 mysql -hxxx -uxxx -p*** 注意:如果在 mysql server 端所在的主机连接,-h 参数不能是 localhost,应该为主机所在的内网 ip。]]></content>
<categories>
<category>MySQL</category>
</categories>
<tags>
<tag>MySQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis 常用命令总结]]></title>
<url>%2F2019%2F01%2F19%2FRedis-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[Redis 常用命令总结redis-cliredis-cli 是 redis 的客户端工具,有很多实用的参数。 redis-benchmarkredis-benchmark 为 redis 提供的性能测试工具,对 redis 各种数据的操作进行测试,并给出测试结果。如下为 GET 操作的测试报告样例:1234567891011====== GET ====== 20000 requests completed in 0.36 seconds 100 parallel clients 3 bytes payload keep alive: 162.01% <= 1 milliseconds97.57% <= 2 milliseconds99.99% <= 3 milliseconds100.00% <= 3 milliseconds55865.92 requests per second]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 Docker Compose 容器化搭建 Wordpress]]></title>
<url>%2F2019%2F01%2F14%2F%E5%9F%BA%E4%BA%8E-Docker-Compose-%E5%AE%B9%E5%99%A8%E5%8C%96%E6%90%AD%E5%BB%BA-Wordpress%2F</url>
<content type="text"><![CDATA[最近由于业务需求帮公司搞了几个 Wordpress 作为官网,中间也是踩了不少坑,倒不是搭建 wordpress 难,主要是 wordpress 本身坑就挺多的,比如迁移、使用过程中文件上传大小的限制问题、迁移后域名无法变更问题等等。 接下来演示如何基于 Docker Compose 来容器化搭建一个可靠、易维护的 Wordpress 网站,可靠指的是服务挂了会自愈(当然是 docker 本身的功能了),易维护指的是即使后面做服务的迁移也是非常方便的,只是简单的文件拷贝,然后 docker compose 启动,没有任何其他的维护成本。 架构:非容器化 nginx 反向代理 + Docker Compose ( Wordpress + MySql) Docker Compose 工程Wordpress Docker Compose 工程目录结构:12345wordpress├── db_data # mysql 数据目录├── docker-compose.yaml # docker-compose 文件├── upload.ini # php 文件上传相关配置└── wp_site # wordpress 静态资源存储目录 docker-compose.yaml:12345678910111213141516171819202122232425262728version: '3.3'services: db: image: mysql:5.7 volumes: - ./db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: somewordpress MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: example wordpress: depends_on: - db image: wordpress:5.0.3 volumes: - ./wp_site:/var/www/html - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini ports: - "9000:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: example uploads.ini:12345file_uploads = Onmemory_limit = 128Mupload_max_filesize = 512Mpost_max_size = 128Mmax_execution_time = 600 外部 Nginx 配置文件https_server.conf(网站配置文件):123456789101112131415161718server { listen 80; server_name example.com; rewrite ^(.*)$ https://$host$1 permanent;}server { listen 443; ssl on; ssl_certificate crts/example/example_com.crt; ssl_certificate_key crts/example/example_com.key; server_name example.com; location / { proxy_pass http://localhost:9002; include conf.d/common.cfg; proxy_set_header X-Forwarded-Proto https; }} common.cfg(Nginx 相关配置项):1234567891011proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;client_max_body_size 128m;client_body_buffer_size 10m;client_body_temp_path /tmp/client_body_temp;proxy_connect_timeout 40;proxy_send_timeout 20;proxy_read_timeout 20;proxy_buffer_size 256k;proxy_buffers 32 64k; 启动服务进入 docker compose 工程目录执行:1docker-compose up -d Tips相关 docker compose 指令:docker-compose stop: 停止已启动的服务,停止后容器还在,只是退出了;docker-compose start: 启动已停止的服务;docker-compose down: 停止并清理掉启动的 Docker 容器、卷、网络等相关资源;docker-compose logs -f: 实时查看日志]]></content>
<categories>
<category>Wordpress</category>
</categories>
<tags>
<tag>Wordpress</tag>
</tags>
</entry>
<entry>
<title><![CDATA[构建 Docker 镜像上传到 docker hub]]></title>
<url>%2F2019%2F01%2F13%2F%E6%9E%84%E5%BB%BA-Docker-%E9%95%9C%E5%83%8F%E4%B8%8A%E4%BC%A0%E5%88%B0-docker-hub%2F</url>
<content type="text"><![CDATA[本文演示如何将自己构建的 Docker 镜像推送到 docker hub来实现镜像的共享。 注册一个 docker hub 账号举例:账号名为 qhh0205 写一个 Dockerfile举例:该 Dockerfile 安装了指定版本的 ant 和 jmeter,GitHub 仓库地址:https://github.com/qhh0205/docker-ant-jmeter 12345678910111213141516171819202122232425262728293031FROM openjdk:8MAINTAINER qhh0205 <[email protected]># ant default version: 1.10.5# jmeter default version: 5.0# Specify version by docker build --build-arg <varname>=<value> ...ARG ANT_VERSION=1.10.5ENV ANT_HOME=/opt/antARG JMETER_VERSION=5.0ENV JMETER_HOME /opt/jmeterRUN apt-get -y update && \apt-get -y install wget# Installs AntRUN wget --no-check-certificate --no-cookies http://archive.apache.org/dist//ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz \&& tar -zvxf apache-ant-${ANT_VERSION}-bin.tar.gz -C /opt/ \&& ln -s /opt/apache-ant-${ANT_VERSION} /opt/ant \&& rm -f apache-ant-${ANT_VERSION}-bin.tar.gzENV PATH ${PATH}:/opt/ant/bin# Installs JmeterRUN wget --no-check-certificate --no-cookies https://archive.apache.org/dist//jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz \&& tar -zvxf apache-jmeter-${JMETER_VERSION}.tgz -C /opt/ \&& ln -s /opt/apache-jmeter-${JMETER_VERSION} /opt/jmeter \&& rm -f apache-jmeter-${JMETER_VERSION}.tgzENV PATH $PATH:/opt/jmeter/bin 构建镜像进入 Dockerfile 所在目录,执行构建命令: 1docker build -t qhh0205/ant-jmeter:1.10.5-5.0 . 参数说明: qhh0205/ant-jmeter:1.10.5-5.0: docker 镜像 tag 名称 qhh0205: docker hub 账号名 ant-jmeter: dcoker hub 仓库名 1.10.5-5.0: 镜像 tag 登陆 docker hub 账号 1docker login 上传镜像 1docker push qhh0205/ant-jmeter:1.10.5-5.0]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 Ansible 统计服务器资源利用率]]></title>
<url>%2F2019%2F01%2F10%2F%E4%BD%BF%E7%94%A8-Ansible-%E7%BB%9F%E8%AE%A1%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%B5%84%E6%BA%90%E5%88%A9%E7%94%A8%E7%8E%87%2F</url>
<content type="text"><![CDATA[分享一个 ansible playbook,统计服务器 CPU、内存、磁盘利用率,3 条 shell 脚本实现统计: CPU 利用率统计:1top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}' 内存利用率统计:1free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }' 磁盘利用率统计(列出每块磁盘利用率):1df -h -t ext2 -t ext4 | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print "Disk Usage:"" " $1 " " $3"/"$2" ""("$5")"}' Ansible playbook: server-cpu-mem-disk-usage.yml1234567891011121314---- name: Statistics CPU Memory Disk Utilization hosts: "{{ hosts }}" become: no remote_user: "{{ user }}" gather_facts: no tasks: - name: "Statistics CPU Memory Disk Utilization..." shell: | free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }' df -h -t ext2 -t ext4 | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print "Disk Usage:"" " $1 " " $3"/"$2" ""("$5")"}' top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}' register: out - debug: var=out.stdout_lines 输出结果样例:123456789ok: [gke-test-standard-pool] => { "out.stdout_lines": [ "Memory Usage: 8766/16052MB (54.61%)", "Disk Usage: /dev/root 449M/1.2G (37%)", "Disk Usage: /dev/sda8 28K/12M (1%)", "Disk Usage: /dev/sda1 61G/95G (64%)", "CPU Load: 0.92" ]}]]></content>
<categories>
<category>Ansible</category>
</categories>
<tags>
<tag>Ansible</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于谷歌云 gcp 的动态 Ansible inventory 实践]]></title>
<url>%2F2019%2F01%2F09%2F%E5%9F%BA%E4%BA%8E%E8%B0%B7%E6%AD%8C%E4%BA%91-gcp-%E7%9A%84%E5%8A%A8%E6%80%81-Ansible-inventory-%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[关于 Ansible inventory 说明ansible inventory 文件可以分为如下两类: 静态 inventory:主机信息写死到文件,这种情况一般适用于管理少量主机,对于成百上千规模的主机人工管理主机清单文件显然是不合理的; 动态 inventory:ansible 根据脚本动态获取云提供商的主机清单文件,这样可以省去人工维护静态清单文件的繁琐工作,对于大批量主机管理也是非常可靠的; Ansible 动态获取云提供商主机 inventory 原理ansible 通过 -i 参数指定动态 inventory 目录,该目录底下放置获取云提供商主机清单的脚本(ansible 社区提供的一般是 Python 脚本),ansible 在执行时该脚本会自动执行并将结果保存到内存中。 那么上面说的获取云提供商主机清单的可执行脚本在哪里获取呢?在 这里 (ansible 官方源码仓库:社区提供的脚本)获取,这里有各个云提供商对应的主机清单脚本(*.py)及配置文件(*.ini),比如谷歌的 gce.py 和 gce.ini,Aws 的 ec2.py 和 ec2.ini 等等。 基于 gcp 的动态 inventory 使用下面是配置使用谷歌云动态 ansible inventory 的详细步骤 相关软件包安装; 1pip install apache-libcloud pycrypto 谷歌云控制台创建一个服务账号(需要有 gce 的访问权限),获取 json 认证文件; 从 ansible 官方仓库 下载 gce.py 和 gce.ini 文件; 1234mkdir -p inventories/gcp-dynamic-inventorycd inventories/gcp-dynamic-inventorywget https://github.com/ansible/ansible/blob/devel/contrib/inventory/gce.pywget https://github.com/ansible/ansible/blob/devel/contrib/inventory/gce.ini 编辑 gce.ini 配置文件 123456[gce]libcloud_secrets =gce_service_account_email_address = <服务账号邮箱:在第 2 步的 json 认证文件里面可以找到>gce_service_account_pem_file_path = <第 2 步中 json 认证文件路径:绝对路径>gce_project_id = <gcp 项目 id>gce_zone = 测试配置的正确性 12# 如果输出一个很长的 json 串表示没问题./gce.py --list 执行 ansible 任务 12345ansible -i inventories/gcp-dynamic-inventory <pattern> -m <module_name> -a 'module_args'Ex: # 查看 asia-east1-a 区域的所有主机时间 ansible -i inventories/gcp-dynamic-inventory asia-east1-a -m shell -a 'date' 参数说明: inventories/gcp-dynamic-inventory: gce.py 脚本所在的目录,ansible 运行时会自动在该目录下执行该脚本获取主机清单; pattern:./gce.py –list 执行结果的 json 顶级节点都可以作为 ansible 的目标主机; 最佳实践:可以给 gce 主机添加 tag,然后通过 tag 对主机分组; gce.ini 文件位置gce.ini 文件没必要必须和 gce.py 在一个目录,可以设置环境变量放到系统其他目录,这样就可以将配置和脚本分离,避免敏感配置放到代码仓库。设置方法:~/.bashrc 文件添加如下内容: 1[[ -s "$HOME/.ansible/gce.ini" ]] && export GCE_INI_PATH="$HOME/.ansible/gce.ini" Tips ansible 执行时可以通过 --list-host 参数先测试下本次操作影响到哪些主机,不会真正执行 task; 参考文档https://temikus.net/ansible-gcp-dynamic-inventory-bootstrap https://medium.com/vimeo-engineering-blog/orchestrating-gce-instances-with-ansible-d825a33793cd]]></content>
<categories>
<category>Ansible</category>
</categories>
<tags>
<tag>Ansible</tag>
</tags>
</entry>
<entry>
<title><![CDATA[谷歌云对象存储 gcs 开启日志记录功能]]></title>
<url>%2F2019%2F01%2F06%2F%E8%B0%B7%E6%AD%8C%E4%BA%91%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8-gcs-%E5%BC%80%E5%90%AF%E6%97%A5%E5%BF%97%E8%AE%B0%E5%BD%95%E5%8A%9F%E8%83%BD%2F</url>
<content type="text"><![CDATA[问题描述有时候我们需要对 gcs 开启日志记录功能,一方面可以分析统计每个针对 gcs 的 http 请求的详细信息,另一方面还可以用于问题调试用途,比如我们对一个存储分区的对象配置了生命周期,可以看其访问日志判断配置是否生效。 gcs 日志记录功能记录两种类型日志 访问日志:访问日志是每小时创建一次,记录对指定存储分区发出的所有请求的信息;存储日志:存储分区过去 24 小时内存储空间平均使用量,以字节为单位; https://cloud.google.com/storage/docs/access-logs?hl=zh-cn&refresh=1 gcs 开启日志记录功能步骤以开启 gs://gcs-bucket 存储分区日志记录功能为例: 创建一个存储分区用于存储日志记录,名字随便起: 1gsutil mb -l asia gs://gcs-bucket-logs-record 设置权限以使 Cloud Storage 对该存储分区具有 WRITE 权限 1gsutil acl ch -g [email protected]:W gs://gcs-bucket-logs-record 为存储分区开启日志记录功能命令格式:gsutil logging set on -b <日志存储分区> <要开启日志记录功能的存储分区> 1gsutil logging set on -b gs://gcs-bucket-logs-record gs://gcs-bucket 检查日志记录功能是否开启成功 1gsutil logging get gs://gcs-bucket 如果开启成功会显示:{"logBucket": "gcs-bucket-logs-record", "logObjectPrefix": "gcs-bucket"} 另外,开启成功后过 2 小时左右就可以在 gs://gcs-bucket-logs-record 看到日志文件产生了,日志文件格式为 csv。 关闭日志记录功能 1gsutil logging set off gs://gcs-bucket 日志文件命名格式及日志内容格式见文档详细说明 https://cloud.google.com/storage/docs/access-logs?hl=zh-cn&refresh=1]]></content>
<categories>
<category>Google Cloud Platform</category>
</categories>
<tags>
<tag>Google Cloud Platform</tag>
</tags>
</entry>
<entry>
<title><![CDATA[非容器化 gitlab 进行容器化改造]]></title>
<url>%2F2018%2F12%2F02%2F%E9%9D%9E%E5%AE%B9%E5%99%A8%E5%8C%96-gitlab-%E8%BF%9B%E8%A1%8C%E5%AE%B9%E5%99%A8%E5%8C%96%E6%94%B9%E9%80%A0%2F</url>
<content type="text"><![CDATA[本文主要介绍非容器化(通过 yum 在 Linux 服务器安装)gitlab 进行容器化改造的两种方法,都是基于 Kubernetes 平台,均采用 helm 部署。第一种是基于自建 k8s 平台部署 gitlab,第二种是基于 Google GKE 平台部署 gitlab。 Docker 镜像采用基于 Omnibus 安装包的镜像,gitlab 的各个组件都运行在同一个容器中。关于 GitLab Ominibus 镜像和云原生镜像的区别见这里。 gitlab 容器化改造(基于自建 k8s 平台部署 gitlab)一、搭建和原先版本一致的 gitlabgithub helm gitlab-ee chart:https://github.com/helm/charts/tree/master/stable/gitlab-ee 在此 helm chart 基础上将备份目录也(/var/opt/gitlab/backups)通过PVC持久化,方便数据的备份恢复: https://github.com/qhh0205/helm-charts/tree/master/gitlab-ee 手动创建需要的 pv(基于 nfs)https://github.com/qhh0205/kubernetes-resources/tree/master/gitlab-pv 部署其他自定义参数修改 values-custom.yaml 文件,比如镜像版本、硬件配置等参数。123git clone [email protected]:qhh0205/helm-charts.gitcd helm-charts/gitlab-eehelm install --name gitlab --set externalUrl=http://domain/,gitlabRootPassword=xxxx -f values-custom.yaml ./ --namespace=gitlab 二、数据恢复 拷贝 gitlab 备份文件到容器外挂nfs目录(/data/nfs/gitlab/gitlab-data-backups(nfs路径)—>/var/opt/gitlab/backups(容器路径)); 进入容器:kubectl exec -it pod_name /bin/sh -n gitlab gitlab-ctl reconfigure chown git:git 1543237967_2018_11_26_10.1.3-ee_gitlab_backup.tar chown -R git:root /gitlab-data # 由于 gitlab-rake 执行过程中 默认用户名是 git,所以需要把该目录的属主改成 git,否则恢复时报错权限问题; gitlab-rake gitlab:backup:restore根据提示输入相关信息YESYESgitlab-ctl restart gitlab 容器化改造(基于 Google 云 GKE 平台)一、搭建和原先版本一致的 gitlabgithub helm gitlab-ee chart:https://github.com/helm/charts/tree/master/stable/gitlab-ee 在此 helm chart 基础上将备份目录也(/var/opt/gitlab/backups)通过PVC持久化,方便数据的备份恢复:https://github.com/qhh0205/helm-charts/tree/master/gitlab-ee 其他自定义参数修改 values-custom.yaml 文件,比如镜像版本、硬件配置等参数。123git clone [email protected]:qhh0205/helm-charts.gitcd helm-charts/gitlab-eehelm install --name gitlab --set externalUrl=http://domain/,gitlabRootPassword=xxxx -f values-custom.yaml ./ --namespace=gitlab 二、数据迁移恢复 将 gitlab 备份文件拷贝到 k8s gitlab pod 容器目录: 12kubectl cp 1543237967_2018_11_26_10.1.3-ee_gitlab_backup.tar \namespace/pod_name:/var/opt/gitlab/backups -n gitlab gitlab-ctl reconfigure chown git:git 1543237967_2018_11_26_10.1.3-ee_gitlab_backup.tar chown -R git:root /gitlab-data # 由于 git-rake 执行过程中 默认用户名是 git,所以需要把该目录的属主改成git,否则恢复时报错权限问题; gitlab-rake gitlab:backup:restore根据提示输入相关信息YESYESgitlab-ctl restart 外部访问 Kong Ingress NodePort LoadBalancer(云提供商平台,比如 Google GKE) 相关链接 容器化安装 gitlab:https://docs.gitlab.com/ee/install/docker.html gitlab 数据存放目录修改:https://blog.whsir.com/post-1490.html gitlab 安装软件和硬件需求:https://docs.gitlab.com/ce/install/requirements.html Omnibus GitLab documentation: https://docs.gitlab.com/omnibus/README.html]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux 文件与目录管理相关命令总结]]></title>
<url>%2F2018%2F12%2F02%2FLinux-%E6%96%87%E4%BB%B6%E4%B8%8E%E7%9B%AE%E5%BD%95%E7%AE%A1%E7%90%86%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Python 解析 Spring pom 文件获取 jar 包名称]]></title>
<url>%2F2018%2F11%2F18%2FPython-%E8%A7%A3%E6%9E%90-Spring-pom-%E6%96%87%E4%BB%B6%E8%8E%B7%E5%8F%96-jar-%E5%8C%85%E5%90%8D%E7%A7%B0%2F</url>
<content type="text"><![CDATA[前段时间在做持续集成有个小需求是根据 pom 文件获取 jar 包名称,在网上搜寻一番,整理了一份脚本,可以直接使用,通过解析 pom 文件获取(xml2pydict.py):使用示例:python xml2pydict.py pom.xml输出结果:jar 包名称123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384#!/usr/bin/env python# -*- coding: utf-8 -*-# @Time : 2018/10/22 下午5:53# @Author : haohao.qiang# @Mail : [email protected]# @File : xml2pydict.pyimport sysimport warningsfrom xml.parsers import expatimport xml.etree.ElementTree as ElementTreewarnings.filterwarnings("ignore")class XmlListConfig(list): def __init__(self, aList): for element in aList: if element: # treat like dict if len(element) == 1 or element[0].tag != element[1].tag: self.append(XmlDictConfig(element)) # treat like list elif element[0].tag == element[1].tag: self.append(XmlListConfig(element)) elif element.text: text = element.text.strip() if text: self.append(text)class XmlDictConfig(dict): ''' Example usage: tree = ElementTree.parse('your_file.xml') root = tree.getroot() xmldict = XmlDictConfig(root) Or, if you want to use an XML string: root = ElementTree.XML(xml_string) xmldict = XmlDictConfig(root) And then use xmldict for what it is... a dict. ''' def __init__(self, parent_element): if parent_element.items(): self.update(dict(parent_element.items())) for element in parent_element: if element: # treat like dict - we assume that if the first two tags # in a series are different, then they are all different. if len(element) == 1 or element[0].tag != element[1].tag: aDict = XmlDictConfig(element) # treat like list - we assume that if the first two tags # in a series are the same, then the rest are the same. else: # here, we put the list in dictionary; the key is the # tag name the list elements all share in common, and # the value is the list itself aDict = {element[0].tag: XmlListConfig(element)} # if the tag has attributes, add those to the dict if element.items(): aDict.update(dict(element.items())) self.update({element.tag: aDict}) # this assumes that if you've got an attribute in a tag, # you won't be having any text. This may or may not be a # good idea -- time will tell. It works for the way we are # currently doing XML configuration files... elif element.items(): self.update({element.tag: dict(element.items())}) # finally, if there are no child tags and no attributes, extract # the text else: self.update({element.tag: element.text})if __name__ == "__main__": xml = sys.argv[1] oldcreate = expat.ParserCreate expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None) tree = ElementTree.parse(xml) root = tree.getroot() xmldict = XmlDictConfig(root) print "{}-{}.{}".format(xmldict.get('artifactId'), xmldict.get('version'), xmldict.get('packaging'))]]></content>
<categories>
<category>Python</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux 文件权限属性相关总结]]></title>
<url>%2F2018%2F11%2F18%2FLinux-%E6%96%87%E4%BB%B6%E6%9D%83%E9%99%90%E5%B1%9E%E6%80%A7%E7%9B%B8%E5%85%B3%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[Linux 文件权限属性相关总结基础知识 文件权限与属性修改 chgrp: 更改文件属组 chown: 更改文件属主 chmod: 更改文件权限,SUID、SGID、SBIT 等属性 1. 更改文件属组更改时组名必须存在,即在必须在 /etc/group 文件内存在,否则报错。12345678910命令格式: chgrp [-R] group_name dirname/filename ...选顷参数:-R : 递归(recursive)更改,即连同子目彔下的所有文件、目录[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant vagrant 5 Nov 17 19:04 aa[root@centos7 vagrant]# chgrp root aa[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant root 5 Nov 17 19:04 aa 2. 更改文件属主12345678910111213141516命令格式: chown [-R] 账号名称 文件或目彔(只更改属主)chown [-R] 账号名称:组名 文件或目彔(属主和属组同时更改)选顷参数:-R : 递归(recursive)更改,即连同子目彔下的所有文件、目录[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant root 5 Nov 17 19:04 aa[root@centos7 vagrant]# chown root aa[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 root root 5 Nov 17 19:04 aa[root@centos7 vagrant]# chown vagrant:vagrant aa[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant vagrant 5 Nov 17 19:04 aa Tips:chown 也能修改属组:chown .group_name filename1234567[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant vagrant 5 Nov 17 19:04 aa[root@centos7 vagrant]# chown .root aa[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant root 5 Nov 17 19:04 aa 3. 更改文件权限更改文件权限使用 chmod 命令,该命令有两种使用方式:以数字或者符号来进行权限的变更。 数字方式更改 1234567891011命令格式: chmod [-R] xyz 文件或目录选项参数:-R: 递归(recursive)更改,即连同子目彔下的所有文件、目录xyz: 权限数字[root@centos7 vagrant]# ls -ltotal 4-rw-rw-r--. 1 vagrant root 5 Nov 17 19:04 aa[root@centos7 vagrant]# chmod 755 aa[root@centos7 vagrant]# ls -ltotal 4-rwxr-xr-x. 1 vagrant root 5 Nov 17 19:04 aa 符号方式更改 123456将文件权限设置为: -rwxr-xr-x[root@centos7 vagrant]# chmod u=rwx,g=rx,o=rx aa给所有人赋予文件可写权限[root@centos7 vagrant]# chmod a+w aa所在组和其他组人去除可写权限[root@centos7 vagrant]# chmod g-x,o-x aa 文件与目录的权限区别Linux 下文件与目录的权限(r、w、x)有很大的不同,具体如下: Linux FHS 标准Linux FHS(Filesystem Hierarchy Standard)文一种规范,规范 Linux 各发行版的目录结构,什么目录下该存放什么类型的文件。大概规范如下(其中灰色部分目录不能在系统的不同磁盘设备,因为都是和系统启动有关的,必须在系统盘所在的磁盘):]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 Hexo 的 GitHub Pages 配置 CloudFlare CDN]]></title>
<url>%2F2018%2F11%2F04%2F%E5%9F%BA%E4%BA%8E-Hexo-%E7%9A%84-GitHub-Pages-%E9%85%8D%E7%BD%AE-CloudFlare-CDN%2F</url>
<content type="text"><![CDATA[概述由于 GitHub Pages 在国外,静态博客页面在国内访问速度可能会非常慢,我们可以用 CDN 来加速,对比了下 CloudFlare CDN 和 腾讯云 CDN,发现 CloudFlare 免费版没有流量限制(腾讯云 CDN 每月由流量限制),而且配置起来非常简单,所以在此选用 CloudFlare CDN 来加速页面访问。 准备工作 个人域名 CloudFlare 账号 基于 hexo 的 github_username.github.io 静态博客 配置流程 在 Hexo 博客 source 文件夹新建名为 CNAME 的文件,内容为个人域名; hexo g && hexo d 部署生产的静态页面到 GitHub; 进入 CloudFlare 控制台,点击添加站点,输入个人域名,根据向导进行操作; 在 CloudFlare DNS 配置页面配置两个 CNAME 均指向 github_username.github.io 地址:根域名(@) CNAME 到 github_username.github.io子域名(www) CNAME 到 github_username.github.io ⚠️注意:其实一般的域名提供商是不支持根域名 CNAME ,只有子域名才可以,但是 CloudFlare 通过 CNAME Flattening 技术支持这种配置。这么做的好处是我们不需要再一个个添加以 GitHub Pages 的 IP 为值的 A 记录了,同时还能提高后续的可维护性,后续即使 GitHub Pages 的 IP 发生了变化,也不影响,CloudFlare 会通过 CNAME Flattening 技术 自动解析出来。 将个人域名的 NS 记录修改为 CloudFlare 的 NS; 等 CloudFlare DNS 解析生效后,并且 CloudFlare 站点状态为 Active 即表示配置生效。 CloudFlare CDN HTTP 强制跳转 HTTPS默认情况下配置完成后 HTTPS 是开启的,会在 24 小时内给你配的站点颁发 https 证书,并且证书是自动更新的。我们可以在 CloudFlare 控制台配置 HTTP 强制跳转 HTTPS:]]></content>
<categories>
<category>Hexo</category>
</categories>
<tags>
<tag>Hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 Kubernetes 的 Jenkins 主从通信异常解决]]></title>
<url>%2F2018%2F10%2F14%2F%E5%9F%BA%E4%BA%8E-Kubernetes-%E7%9A%84-Jenkins-%E4%B8%BB%E4%BB%8E%E9%80%9A%E4%BF%A1%E5%BC%82%E5%B8%B8%E8%A7%A3%E5%86%B3%2F</url>
<content type="text"><![CDATA[问题描述基于 Kubernetes 部署 Jenkins 动态 slave 后,运行 Jenkins Job 会抛java.nio.channels.ClosedChannelException 异常完整的异常栈如下:1234567891011121314151617181920212223242526272829303132333435FATAL: java.nio.channels.ClosedChannelExceptionjava.nio.channels.ClosedChannelExceptionAlso: hudson.remoting.Channel$CallSiteStackTrace: Remote call to JNLP4-connect connection from 10.244.8.1/10.244.8.1:55340 at hudson.remoting.Channel.attachCallSiteStackTrace(Channel.java:1741) at hudson.remoting.Request.call(Request.java:202) at hudson.remoting.Channel.call(Channel.java:954) at hudson.FilePath.act(FilePath.java:1071) at hudson.FilePath.act(FilePath.java:1060) at hudson.FilePath.mkdirs(FilePath.java:1245) at hudson.model.AbstractProject.checkout(AbstractProject.java:1202) at hudson.model.AbstractBuild$AbstractBuildExecution.defaultCheckout(AbstractBuild.java:574) at jenkins.scm.SCMCheckoutStrategy.checkout(SCMCheckoutStrategy.java:86) at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:499) at hudson.model.Run.execute(Run.java:1819) at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:43) at hudson.model.ResourceController.execute(ResourceController.java:97) at hudson.model.Executor.run(Executor.java:429)Caused: hudson.remoting.RequestAbortedException at hudson.remoting.Request.abort(Request.java:340) at hudson.remoting.Channel.terminate(Channel.java:1038) at org.jenkinsci.remoting.protocol.impl.ChannelApplicationLayer.onReadClosed(ChannelApplicationLayer.java:209) at org.jenkinsci.remoting.protocol.ApplicationLayer.onRecvClosed(ApplicationLayer.java:222) at org.jenkinsci.remoting.protocol.ProtocolStack$Ptr.onRecvClosed(ProtocolStack.java:832) at org.jenkinsci.remoting.protocol.FilterLayer.onRecvClosed(FilterLayer.java:287) at org.jenkinsci.remoting.protocol.impl.SSLEngineFilterLayer.onRecvClosed(SSLEngineFilterLayer.java:172) at org.jenkinsci.remoting.protocol.ProtocolStack$Ptr.onRecvClosed(ProtocolStack.java:832) at org.jenkinsci.remoting.protocol.NetworkLayer.onRecvClosed(NetworkLayer.java:154) at org.jenkinsci.remoting.protocol.impl.NIONetworkLayer.ready(NIONetworkLayer.java:142) at org.jenkinsci.remoting.protocol.IOHub$OnReady.run(IOHub.java:795) at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28) at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:59) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)Finished: FAILURE 原因及解决方法原因抛 java.nio.channels.ClosedChannelException 异常的原因是 Jenkins Slave Pod 在 Jenkins Job 运行时突然挂掉,然后 Master Pod 无法和 Slave Pod 进行通信。那么解决方法就是找到 Slave Pod 经常挂掉的原因,经排查是 Slave Pod 的资源限制不合理,配置的 CPU 和内存太小,导致 Pod 在运行是很容易超出资源限制,然后被 k8s Kill 掉。 解决方法 打开 Jenkins 设置 Slave Pod 模版的资源限制:Jenkins->系统管理->系统设置->云->镜像->Kubernetes Pod Template->Container Template->高级,然后根据实际情况调整 CPU 和内存需求。 相关文档https://github.com/GoogleCloudPlatform/continuous-deployment-on-kubernetes/issues/118https://medium.com/@garunski/closedchannelexception-in-jenkins-with-kubernetes-plugin-a7788f1c62a9]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>Jenkins</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于 Kubernetes 的动态 Jenkins slave 部署]]></title>
<url>%2F2018%2F10%2F14%2F%E5%9F%BA%E4%BA%8E-Kubernetes-%E7%9A%84%E5%8A%A8%E6%80%81-Jenkins-slave-%E9%83%A8%E7%BD%B2%2F</url>
<content type="text"><![CDATA[采用官方 Helm Chart 部署,服务对外暴露方式为 KongIngress. 官方 Jenkins Chart 仓库:https://github.com/helm/charts/tree/master/stable/jenkins 1. 创建 jenkins pvpv 底层类型为 nfsjenkins_pv.yaml:12345678910111213141516kubectl create -f jenkins_pv.yamlapiVersion: v1kind: PersistentVolumemetadata: name: jenkins-pv labels: app: jenkinsspec: capacity: storage: 50Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain nfs: path: /data1/nfs/jenkins server: 10.4.37.91 2. 创建 namespace1kubectl create ns jenkins 3. 采用 KongIngress 方式对外暴露服务修改 values.yml 文件:3.1. Master.ServiceType 改为 ClusterIP3.2. HostName 取消注释,值设置为 Jenkins 访问域名:example.com3.3. rbac 设置为 true;3.4. Master.Ingress.Annotations 添加如下内容:12ingress.plugin.konghq.com: jenkins-kong-ingresskubernetes.io/ingress.class: nginx 3.5. values.yaml Master 节点下添加 Kong Ingress 相关变量12345678910KongIngress: Name: jenkins-kong-ingress Route: StripPath: true PreserveHost: true Proxy: ConnectTimeout: 10000 Retries: 5 ReadTimeout: 60000 WriteTimeout: 60000 4. 编辑 jenkins-master-ingress.yaml 添加 KongIngress 资源对象123456789101112apiVersion: configuration.konghq.com/v1kind: KongIngressmetadata: name: {{ .Values.Master.KongIngress.Name }}route: strip_path: {{ .Values.Master.KongIngress.Route.StripPath }} preserve_host: {{ .Values.Master.KongIngress.Route.PreserveHost }}proxy: connect_timeout: {{ .Values.Master.KongIngress.Proxy.ConnectTimeout }} retries: {{ .Values.Master.KongIngress.Proxy.Retries }} read_timeout: {{ .Values.Master.KongIngress.Proxy.ReadTimeout }} write_timeout: {{ .Values.Master.KongIngress.Proxy.WriteTimeout }} 5. helm 打包1helm package jenkins 6. 重新生成 chart 索引1helm repo index . 7. helm 部署1helm install --name jenkins helm_local_repo/jenkins --namespace jenkins 8. 获取 Jenkins 初始密码 执行 kubectl get secret jenkins -n jenkins -o yaml 得到 jenkins-admin-password 的 base64 编码值,然后通过 base64 解码,得到密码:1echo 'base64d_str' | base64 -d 参考链接https://mp.weixin.qq.com/s/OoTEtPNEORn_sFYG8rzaqA]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>Jenkins</tag>
</tags>
</entry>
<entry>
<title><![CDATA[容器化部署 Wordpress 的一个坑]]></title>
<url>%2F2018%2F10%2F13%2F%E5%AE%B9%E5%99%A8%E5%8C%96%E9%83%A8%E7%BD%B2-Wordpress-%E7%9A%84%E4%B8%80%E4%B8%AA%E5%9D%91%2F</url>
<content type="text"><![CDATA[问题描述非容器化 nginx + docker-compose 容器化 wordpress 后,媒体库上传图片报错:HTTP 错误 问题解决其实这个问题的原因非常多,网上文章一大堆(https://www.duoluodeyu.com/2402.html ),但是本文中所遇到同样问题的原因却比较诡异:nginx client_max_body_size 参数必须要和 PHP 的 post_max_size 参数值一致。 1.修改 Wordpress 容器 PHP 参数新建 uploads.ini 文件,将该文件挂载到容器:/usr/local/etc/php/conf.d/uploads.ini 文件uploads.ini:12345file_uploads = Onmemory_limit = 128Mupload_max_filesize = 512Mpost_max_size = 128Mmax_execution_time = 600 docker-compose 文件添加卷,将文件挂载到容器123volumes: - ./wp_site:/var/www/html - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini 2. 修改 nginx client_max_body_size 参数配置这个是坑的地方,这个参数的值必须要和上一步 PHP post_max_size 参数的值一致,否则还是报同样的 HTTP 错误。之前没注意这个问题,按照网上各种配置调整,均不起作用,后来经过各种猜测测试,其实问题的根因就在这里:nginx client_max_body_size 参数必须要和 php post_max_size 参数的值一致。 附件(完整的 Wordpress docker-compose.yaml)容器外挂文件 uploads.ini 是定义 PHP 的一些参数配置,比如最大文件上传大小、POST 请求体大小限制、内存大小限制等等,这个文件挂载是可选的,但是如果要自定义 PHP 参数可以这么做。1234567891011121314151617181920212223242526version: '3.3'services: db: image: mysql:5.7 volumes: - ./db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: somewordpress MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress wordpress: depends_on: - db image: wordpress:latest volumes: - ./wp_site:/var/www/html - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini ports: - "9001:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress 相关参考文档https://www.duoluodeyu.com/2402.htmlhttps://github.com/docker-library/wordpress/issues/10]]></content>
<categories>
<category>Wordpress</category>
</categories>
<tags>
<tag>Wordpress</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubeadm 安装的 k8s 集群 delete node 后重新添加回集群问题解决]]></title>
<url>%2F2018%2F09%2F11%2Fkubeadm-%E5%AE%89%E8%A3%85%E7%9A%84-k8s-%E9%9B%86%E7%BE%A4-delete-node-%E5%90%8E%E9%87%8D%E6%96%B0%E6%B7%BB%E5%8A%A0%E5%9B%9E%E9%9B%86%E7%BE%A4%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%2F</url>
<content type="text"><![CDATA[问题描述前不久公司同事误操作,直接 kubectl delete node node_ip 从集群中删除了一个 node,后来未知原因服务器给宕机了,重启服务器后 docker、kubelet 等服务器都自动重启了(用 systemd 管理),但是 node 一直是 Not Ready 状态,按理来说执行如下命令把节点添加回集群即可:1kubeadm join --token xxxxxxx master_ip:6443 --discovery-token-ca-cert-hash sha256:xxxxxxx 但是执行如上命令后报错如下(提示 10250 端口被占用):12345678[root@com10-81 ~]# kubeadm join --token xxxx 10.4.37.167:6443 --discovery-token-ca-cert-hash sha256:xxxxxx[preflight] Running pre-flight checks. [WARNING SystemVerification]: docker version is greater than the most recently validated version. Docker version: 17.12.1-ce. Max validated version: 17.03 [WARNING FileExisting-crictl]: crictl not found in system path[preflight] Some fatal errors occurred: [ERROR Port-10250]: Port 10250 is in use [ERROR FileAvailable--etc-kubernetes-pki-ca.crt]: /etc/kubernetes/pki/ca.crt already exists [ERROR FileAvailable--etc-kubernetes-kubelet.conf]: /etc/kubernetes/kubelet.conf already exists 解决方法出现如上问题的主要原因是之前 kubeadm init 初始化过,所以一些配置文件及服务均已存在,重新执行 kubeadm join 时必然会导致冲突,解决方法如下:1.先执行 kubeadm reset,重新初始化节点配置:kubeadm reset1234567[root@com10-81 ~]# kubeadm reset[preflight] Running pre-flight checks.[reset] Stopping the kubelet service.[reset] Unmounting mounted directories in "/var/lib/kubelet"[reset] Removing kubernetes-managed containers.[reset] No etcd manifest found in "/etc/kubernetes/manifests/etcd.yaml". Assuming external etcd.[reset] Deleting contents of stateful directories: [/var/lib/kubelet /etc/cni/net.d /var/lib/dockershim /var/run/kubernetes] 2.然后执行 kubeadm join 添加节点到集群(如果 token 失效,到主节点执行:kubeadm token create 重新生成):kubeadm join --token xxxxx master_ip:6443 --discovery-token-ca-cert-hash sha256:xxxx1234567891011121314151617[root@com10-81 ~]# kubeadm join --token xxxxx 10.4.37.167:6443 --discovery-token-ca-cert-hash sha256:xxxxxxx[preflight] Running pre-flight checks. [WARNING SystemVerification]: docker version is greater than the most recently validated version. Docker version: 17.12.1-ce. Max validated version: 17.03 [WARNING FileExisting-crictl]: crictl not found in system path[preflight] Starting the kubelet service[discovery] Trying to connect to API Server "10.4.37.167:6443"[discovery] Created cluster-info discovery client, requesting info from "https://10.4.37.167:6443"[discovery] Requesting info from "https://10.4.37.167:6443" again to validate TLS against the pinned public key[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "10.4.37.167:6443"[discovery] Successfully established connection with API Server "10.4.37.167:6443"This node has joined the cluster:* Certificate signing request was sent to master and a response was received.* The Kubelet was informed of the new secure connection details.Run 'kubectl get nodes' on the master to see this node join the cluster. PS: k8s 集群 /etc/kubernetes/pki/ca.crt 证书(任何一节点都有该文件) sha256 编码获取(kubeadm join 添加集群节点时需要该证书的 sha256 编码串认证):openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //' 到此节点添加回集群了,但是直接执行 kubectl 相关的命令可能还会报如下错误:123[root@com10-81 ~]# kubectl get podThe connection to the server localhost:8080 was refused - did you specify the right host or port?You have mail in /var/spool/mail/root 问题原因及解决方法:很明显 kubelet 加载的配置文件(/etc/kubernetes/kubelet.conf)有问题,可能服务器重启的缘故,启动后该文件丢失了,导致里面的连接 master 节点的配置及其他配置给丢了,因此会默认连接 localhost:8080 端口。解决方法很简单:拷贝其他任一节点的该文件,然后重启 kubelet (systemctl restart kublete)即可。 参考链接https://stackoverflow.com/questions/41732265/how-to-use-kubeadm-to-create-kubernetest-clusterhttps://blog.csdn.net/mailjoin/article/details/79686934]]></content>
<categories>
<category>Kubernetes</category>
</categories>
<tags>
<tag>k8s</tag>
</tags>
</entry>
<entry>
<title><![CDATA[通过 CeSi + Supervisor 可视化集中管理服务器节点进程]]></title>
<url>%2F2018%2F07%2F21%2F%E9%80%9A%E8%BF%87-CeSi-Supervisor-%E5%8F%AF%E8%A7%86%E5%8C%96%E9%9B%86%E4%B8%AD%E7%AE%A1%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%8A%82%E7%82%B9%E8%BF%9B%E7%A8%8B%2F</url>
<content type="text"><![CDATA[通过 CeSi + Supervisor 可视化集中管理服务器节点进程 简介 Supervisor 的安装及基本使用 1. 安装 2. 基本使用 2.1 启动 supervisor 2.2 Supervisor 客户端 supervisorctl 安装配置 CeSi 1. 简介 2. 安装 3. 配置 4. 启动 Supervisor 服务设置开机自启动 参考链接 简介Supervisor 是一个用 Python 写的进程管理工具,可以很方便的用来启动、重启、关闭进程。类似于 Linux 的 systemd 守护进程一样,通过统一的命令来管理系统的各个服务,当管理的服务挂掉时会自动重新拉起。Supervisor 还提供了很多第三方插件,比如后面会讲到的 CeSi,该工具是 Supervisor 的 WebUI,可以通过这个统一的 WebUI 集中化管理各个服务器节点的进程。 Supervisor 和 Docker 的架构类似,也是 C/S 架构,服务端是 supervisord,客户端是 supervisorctl 。客户端主要是用来控制服务端所管理的进程,比如控制服务的启动、关闭、重启、查看服务状态,还可以重启服务端、重载配置文件等。服务端管控各个服务的正常运行,当有服务异常退出时会自动拉起。 Supervisor 的安装及基本使用1. 安装Supervisor 的安装特别简单,由于是 Python 写的,因此可以通过 pip 一键安装:1pip install supervisor 在此我提供了一个 Sueprvisor 一键安装配置脚本,简化了 Supervisor 的初始配置。 2. 基本使用安装完成后系统会多出如下三个命令: supervisord :Supervisor 的服务端;supervisorctl:Supervisor 的客户端;echo_supervisord_conf:Supervisor 服务端默认配置文件生成工具; 2.1 启动 supervisor首先通过如下命令将 supervisor 的默认配置生成到 /etc/supervisord.conf:1echo_supervisord_conf > /etc/supervisord.conf Supervisor 配置文件格式是 INI 格式,因此看起来比较直观,很多配置项的含义已在上面生成的配置文件中以注释的形式说明,以下简要说明一下我在生产环境目前使用的配置,为了减少篇幅,在此只列出了非注释的内容:12345678910111213141516171819202122[unix_http_server]file=/tmp/supervisor.sock ; 服务端套接字文件路径,supervisorctl 客户端会使用该文件和服务端通信[inet_http_server] ; Supervisor 服务端提供的 http 服务,很多 Supervisor 的 WebUI ;都是通过访问该服务来实现统一管理的,比如后面要讲的 CeSi Web UIport=0.0.0.0:9001 ; ip_address:port specifier, *:port for all iface[supervisord] ; Supervisor 服务端配置logfile=/tmp/supervisord.log ; 服务端日志文件路径logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MBlogfile_backups=10 ; # of main logfile backups; 0 means none, default 10loglevel=debug ; log level; default info; others: debug,warn,tracepidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pidnodaemon=false ; start in foreground if true; default falseminfds=1024 ; min. avail startup file descriptors; default 1024minprocs=200 ; min. avail process descriptors;default 200user=root[rpcinterface:supervisor]supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface[supervisorctl] ; Supervisor 客户端配置serverurl=unix:///tmp/supervisor.sock ; 配置客户端和服务端的通信方式,默认 supervisorctl ;和 supervisor 通信是通过该套接字通信,也可以配成通过 http 方式通信。[include] ; 在此我将 Supervisor 所管理的服务配置文件都放到了 /etc/supervisor/ 目录,然后通过 include 统一引入files = /etc/supervisor/*.conf 接下来在 /etc/supervisor/ 放入需要 Supervisor 管理的各服务的配置文件,一般一个服务一个配置文件,当然也可以写到一起,比如逻辑上有关联的一组服务可以放到一个配置文件,这样方便管理,下面以一个实例来介绍下要通过 Supervisor 管理服务,相应的配置文件该如何编写(使用 Supervisor 管理 cesi 服务的配置):123456789101112131415; cesi.conf[program:cesi-5000] ; program 表示 Supervisor 管理的服务实例,cesi-5000 是自己命名 ;的服务名称,名字可以随便其,我为了方便管理统一命名为:服务名称-端口directory = /home/ec2-user/cesi ; 程序的启动目录command = python cesi/web.py ; 启动服务的命令autostart = true ; 在 supervisord 启动的时候也自动启动startsecs = 5 ; 启动 5 秒后没有异常退出,就当作已经正常启动了autorestart = true ; 程序异常退出后自动重启startretries = 3 ; 启动失败自动重试次数,默认是 3user = ec2-user ; 用哪个用户启动redirect_stderr = true ; 把 stderr 重定向到 stdout,默认 falsestdout_logfile_maxbytes = 50MB ; stdout 日志文件大小,默认 50MBstdout_logfile_backups = 7 ; stdout 日志文件备份数; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)stdout_logfile = /home/ec2-user/cesi/stdout.log 将上述配置保存为 cesi.conf,放到 /etc/supervisor/。 前面已经对 echo_supervisord_conf 生成的默认配置文件做了微调,接下来启动 Supervisor 服务端(建议用 root 用户启动):1sudo supervisord -c /etc/supervisord.conf 如果不指定 -c 参数,会通过如下顺序来搜索配置文件:1234$PWD/supervisord.conf$PWD/etc/supervisord.conf/etc/supervisord.conf/etc/supervisor/supervisord.conf 2.2 Supervisor 客户端 supervisorctlsupervisorctl 有两种使用方式:一种是直接执行 supervisorctl ,这样会进入交互式的 Shell, 然后在该交互式 Shell 中输入管理命令,举例:123456[root@awsuw supervisor]# supervisorctlcesi-5000 RUNNING pid 6538, uptime 1 day, 1:21:02zipkinstage-9411 RUNNING pid 30919, uptime 1 day, 19:51:43supervisor> statuscesi-5000 RUNNING pid 6538, uptime 1 day, 1:21:09zipkinstage-9411 RUNNING pid 30919, uptime 1 day, 19:51:50 另一种是 supervisorctl [action] 的方式,这样不会陷入交互式 Shell,直接会返回命令的执行结果,其中 action 就是管理服务进程的各个命令,举例(查看目前所管理的服务的进程状态):123[root@awsuw supervisor]# supervisorctl statuscesi-5000 RUNNING pid 6538, uptime 1 day, 1:24:53zipkinstage-9411 RUNNING pid 30919, uptime 1 day, 19:55:34 其中常用的 action 有如下(更多选项参数见 这里): supervisorctl status : 查看所管理的服务状态;supervisorctl start <program_name>:启动一个服务;supervisorctl restart <program_name>:重启一个服务(注意:重启服务不会重新加载配置文件);supervisorctl stop <program_name>:关闭一个服务;supervisorctl update:重新加载配置文件,并重启配置有变动的服务;supervisorctl reread:重新加载配置文件,但不会重启配置有变动的服务;supervisorctl reload:重启 Supervisor 服务端;supervisorctl clear <program_name>:清理一个服务的 stdout log; 安装配置 CeSi1. 简介CeSi 是 Supervisor 官方推荐的集中化管理 Supervisor 实例的 Web UI,该工具是用 Python 编写,基于 Flask Web 框架 。 Superviosr 自带的 Web UI 不支持跨机器管理Supervisor 进程,功能比较简单,通过 CeSi 可以集中管理各个服务器节点的进程,在 Web 界面就可以轻松管理各个服务的启动、关闭、重启等,很方便使用。 2. 安装安装 CeSi 有三个依赖:Python,Flask,sqlite3一般的 Linux 发行版都默认安装了 Python,所以 Python 不需要再次安装;从 Python 2.5 开始 sqlite3 已经在标准库内置了,所以也不需要安装 sqlite3 模块了;另外很多 Linux 发行版已经自带 sqlite3,所以无需另外安装;只需要安装 flask web 框架即可; CeSi 已经有了新的版本,在 GitHub 仓库的 v2_api 分支下,提供了比之前版本更加美观的界面,以下为 CeSi 一键安装配置脚本:12345678910111213141516# !/bin/bashset -esudo pip install flaskgit clone https://github.com/gamegos/cesi.gitcd cesi# 使用最新版, 最新版的 Web UI 做了很大改动git checkout -b v2_api origin/v2_apisudo cp cesi.conf.sample /etc/cesi.confsudo ln -s /etc/cesi.conf cesi.conf#创建用户信息表:sqlite3 userinfo.db < userinfo.sql#CeSi log 目录sudo mkdir -p /var/logs/cesisudo chmod 777 -R /var/logsexit 0 注意:CeSi 的配置文件路径必须是 /etc/cesi.conf ,否则启动会报错,简单看下 CeSi 的源码就知道为什么了。在这里我在仓库目录弄了个软连接指向了 /etc/cesi.conf,完全是为了编辑方便弄的。 3. 配置CeSi 的配置非常简单,和 Supervisor 的配置文件类似,也是 INI 格式,关于配置文件的各项说明在 cesi.conf.sample 配置样例中已经通过注释的形式给了明确的说明,稍微看下就能明白,以下为我目前使用的配置(为了减小篇幅,去掉了注释):1234567891011121314151617181920212223[node:node1] ;各 Supervisor 节点的配置username = ; 如果 Supervisor 节点没有设置账号密码,这里就保持为空,但不能不写password =host = 127.0.0.1port = 9001[node:node2]username =password =host = node2.d.comport = 9001[node:node3]username =password =host = node3.d.comport = 9001[cesi] ; CeSi 自身的配置database = userinfo.dbactivity_log = /var/logs/cesi/activity.log ;log目录没有的话需要提前建好host = 0.0.0.0port = 5000 ; CeSi 启动端口name = CeSItheme = superhero 4. 启动CeSi 的启动非常简单,直接通过 Python 启动即可:1python cesi/web.py 为了方便管理,我把 CeSi 也通过 Supervisor 来管理,以下为对应的 Supervisor 配置:1234567891011121314;cesi.conf[program:cesi-5000]directory = /home/ec2-user/cesi ; 程序的启动目录command = python cesi/web.pyautostart = true ; 在 supervisord 启动的时候也自动启动startsecs = 5 ; 启动 5 秒后没有异常退出,就当作已经正常启动了autorestart = true ; 程序异常退出后自动重启startretries = 3 ; 启动失败自动重试次数,默认是 3user = ec2-user ; 用哪个用户启动redirect_stderr = true ; 把 stderr 重定向到 stdout,默认 falsestdout_logfile_maxbytes = 50MB ; stdout 日志文件大小,默认 50MBstdout_logfile_backups = 7 ; stdout 日志文件备份数; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)stdout_logfile = /home/ec2-user/cesi/stdout.log 启动完成后,做个 Nginx 反向代理即可通过浏览器访问,最终效果如下: Supervisor 服务设置开机自启动以下为在 RedHat7下配置 Supervisor 开机自启动过程,编写 Unit 文件,使用 systemd 管理 Supervisor: 编写 Unit 文件:supervisord.service: 12345678910111213141516#supervisord.service[Unit]Description=Supervisor daemon[Service]Type=forkingExecStart=/bin/supervisord -c /etc/supervisord.confExecStop=/bin/supervisorctl shutdownExecReload=/bin/supervisorctl -c /etc/supervisord.conf reloadKillMode=processRestart=on-failureRestartSec=42s[Install]WantedBy=multi-user.target 将上述文件拷贝到 /usr/lib/systemd/system/ 目录下 将 supervisor.service 注册到系统中 12[root@awsuw ~]# systemctl enable supervisord.serviceCreated symlink from /etc/systemd/system/multi-user.target.wants/supervisord.service to /usr/lib/systemd/system/supervisord.service. 可以看出注册过程就是在 /etc/systemd/system/multi-user.target.wants/ 目录下创建一个软链接指向第二步中的中拷贝到 /usr/lib/systemd/system/ 的文件。 参考链接http://supervisord.org/index.htmlhttp://www.bjhee.com/supervisor.htmlhttps://www.jianshu.com/p/03619bf7d7f5http://liyangliang.me/posts/2015/06/using-supervisor]]></content>
<categories>
<category>Supervisor</category>
</categories>
<tags>
<tag>Supervisor</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis 慢查询分析]]></title>
<url>%2F2018%2F07%2F15%2FRedis-%E6%85%A2%E6%9F%A5%E8%AF%A2%E5%88%86%E6%9E%90%2F</url>
<content type="text"><![CDATA[简介和很多关系型数据库(例如:MySQL)一样, Redis 也提供了慢查询日志记录,Redis 会把命令执行时间超过 slowlog-log-slower-than 的都记录在 Reids 内部的一个列表(list)中,该列表的长度最大为 slowlog-max-len 。需要注意的是,慢查询记录的只是命令的执行时间,不包括网络传输和排队时间: 慢查询分析配置关于 Redis 慢查询的配置有两个,分别是 slowlog-log-slower-than 和 slowlog-max-len。 slowlog-log-slower-than,用来控制慢查询的阈值,所有执行时间超过该值的命令都会被记录下来。该值的单位为微秒,默认值为 10000,如果设置为 0,那么所有的记录都会被记录下来,如果设置为小于 0 的值,那么对于任何命令都不会记录,即关闭了慢查询。可以通过在配置文件中设置,或者用 config set 命令来设置: 1config set slowlog-log-slower-than 10000 slowlog-max-len,用来设置存储慢查询记录列表的大小,默认值为 128,当该列表满了时,如果有新的记录进来,那么 Redis 会把队最旧的记录清理掉,然后存储新的记录。在生产环境我们可以适当调大,比如调成 1000,这样就可以缓冲更多的记录,方便故障的排查。配置方法和 slowlog-log-slower-than 类似,可以在配置文件中指定,也可以在命令行执行 config set 来设置: 1config set slowlog-max-len 1000 查看慢查询日志尽管 Redis 把慢查询日志记录到了内部的列表,但我们不能直接操作该列表,Redis 专门提供了一组命令来查询慢查询日志: 获取慢查询日志:slowlog get [n]下面操作返回当前 Redis 的所有慢查询记录,可以通过参数 n 指定查看条数: 123456789101112131415127.0.0.1:6379> slowlog get 1) 1) (integer) 456 2) (integer) 1531632044 3) (integer) 3 4) 1) "get" 2) "m" 5) "127.0.0.1:50106" 6) "" 2) 1) (integer) 455 2) (integer) 1531632037 3) (integer) 14 4) 1) "keys" 2) "*" 5) "127.0.0.1:50106" 6) "" 结果说明:1) 慢查询记录 id;2) 发起命令的时间戳;3) 命令耗时,单位为微秒;4) 该条记录的命令及参数;5) 客户端网络套接字(ip: port);6) “” 获取当前慢查询日志记录数slowlog len 12127.0.0.1:6379> slowlog len(integer) 458 慢查询日志重置slowlog reset实际上是对慢查询列表做清理操作: 123456127.0.0.1:6379> slowlog len(integer) 461127.0.0.1:6379> slowlog resetOK127.0.0.1:6379> slowlog len(integer) 1]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis 数据库管理]]></title>
<url>%2F2018%2F07%2F14%2FRedis-%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%A1%E7%90%86%2F</url>
<content type="text"><![CDATA[Redis 数据库管理概要Redis 提供了几个面向数据库的操作,分别是 dbsize, select, flushdb/flushall。其实在一个 Redis 实例内部也是有多个数据库的,与 MySQL 等其他关系型数据库不同的是,Redis 内部的数据库使用数字索引来标识,而不是像 MySQL 那样一个实例中的数据库是通过数据库名称来标识。在 Redis 中数据库默认有 16 个,数据库标识分别是 0, 1, …, 15,我们默认使用的是 0 号数据库,不同数据库之间是隔离的,可以拥有同名的键。 各数据库管理命令介绍1. dbsize 查看当前数据库 key 的个数123456127.0.0.1:6379> set name tomOK127.0.0.1:6379> set score 99OK127.0.0.1:6379> dbsize(integer) 2 2. select 切换数据select 命令格式为:select index,index 为数据库的标识。举例如下:123456789101112127.0.0.1:6379> set name tomOK127.0.0.1:6379> set score 99OK127.0.0.1:6379> select 8 // 切换到 8 号数据库OK127.0.0.1:6379[8]> get name //可以看出不同 db 是隔离的(nil)127.0.0.1:6379[8]> set name tomOK127.0.0.1:6379[8]> get name"tom" 3. flushdb/flushall 清理数据库flushdb 和 flushall 的区别为:flushdb 清空当前数据库,而 flushall 清空所有数据库。举例如下:123456789101112131415161718192021127.0.0.1:6379> keys *1) "score"2) "name"127.0.0.1:6379> select 8OK127.0.0.1:6379[8]> keys *1) "score"2) "name"127.0.0.1:6379[8]> flushdbOK127.0.0.1:6379[8]> keys *(empty list or set)127.0.0.1:6379[8]> select 0OK127.0.0.1:6379> keys *1) "score"2) "name"127.0.0.1:6379> flushallOK127.0.0.1:6379> keys *(empty list or set) 总结目前 Redis 对多数据库的支持开始弱化了,因为 Redis 是单线程架构,同一时间只有一个 CPU 为 Redis 服务,多个数据库同时存在不仅不会利用系统的多核优势,反而会由于单实例资源共享问题互相会有影响,导致出现问题时排错非常困难,Redis 实例如果一旦阻塞,那么所有的数据库都会受到影响。所以这是一个很鸡肋的功能,Redis 官方对其支持也在逐步弱化。更合理的方式是一台机器启动多个 Redis 实例,互相隔离,充分利用 CPU 的多核优势。]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis 基础]]></title>
<url>%2F2018%2F07%2F11%2FRedis-%E5%9F%BA%E7%A1%80%2F</url>
<content type="text"><![CDATA[简介Redis 是一种基于键值对的 No-SQL 数据库,与很多键值对数据库不同的是,Redis 中的值可以由 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法组成,因此 Redis 可以满足很多应用场景,而且因为 Redis 会将所有数据都存放在内存中,所以它的读写性能非常惊人。另外 Redis 提供了 RDB 和 AOF 两种持久化方式,使得即使发生断电或者机器故障,数据也可以持久化到磁盘上,防止了数据的意外丢失。 安装 RedisLinux 安装软件一般由两种方式,第一种是通过各个操作系统的软件包管理器进行安装,比如 Ubuntu 使用 apt-get,RedHat 系列使用 yum 安装。但是由于 Redis 的更新速度比较快,而各大 Linux 发行版的相应软件源更新却比较慢,因此直接通过这种方式安装无法获取较新的版本。所以一般推荐第二种方式:源码方式安装。Redis 的源码安装特别简单,没有第三方依赖,直接下载源码编译安装即可。通过以下命令编译安装 Redis 最新稳定版:12345678910111213# 安装 gcc 相关编译工具sudo apt-get install -y build-essential# 安装 make 打包工具sudo apt-get -y install make# Use latest stable 下载最新稳定版源码wget -q http://download.redis.io/redis-stable.tar.gztar zxvf redis-stable.tar.gzcd redis-stable# 编译源码make# 安装sudo make install 安装完成后可以通过如下命令查看 Redis 版本:12vagrant@redis:~$ redis-cli -vredis-cli 4.0.10 配置、启动、操作、关闭 RedisRedis 安装之后,Redis 源码目录 src 和 /usr/local/bin 目录多了几个以 redis 开头的可执行文件,我们称之为 Redis Shell,这些文件包括 Redis server 和 client 以及其他操作 Redis 的实用工具: 可执行文件 作用 redis-server Redis 服务端 redis-cli Redis 命令行客户端 redis-benchmark Redis 基准测试工具 redis-check-rdb Redis AOF 持久化文件检测和修复工具 redis-check-aof Redis RDB 持久化文件检测和修复工具 redis-sentinel 启动 Redis Sentinel 启动 Redis有三种方法启动 Redis:默认配置、运行配置、配置文件启动。 默认配置这种方式启动是直接执行 redis-server 来启动,后面没有任何参数,以默认的配置来启动。因为这种启动方式无法自定义配置,所以这种方式是不会在生产环境中使用。: 123456789101112131415161718192021222324252627vagrant@redis:~$ redis-server13622:C 11 Jul 02:27:09.542 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo13622:C 11 Jul 02:27:09.543 # Redis version=4.0.10, bits=64, commit=00000000, modified=0, pid=13622, just started13622:C 11 Jul 02:27:09.543 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf13622:M 11 Jul 02:27:09.546 # You requested maxclients of 10000 requiring at least 10032 max file descriptors.13622:M 11 Jul 02:27:09.546 # Server can't set maximum open files to 10032 because of OS error: Operation not permitted.13622:M 11 Jul 02:27:09.547 # Current maximum open files is 4096. maxclients has been reduced to 4064 to compensate for low ulimit. If you need higher maxclients increase 'ulimit -n'. _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 4.0.10 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 13622 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-'13622:M 11 Jul 02:27:09.551 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.13622:M 11 Jul 02:27:09.552 # Server initialized 运行启动这种方式是执行 redis-server 时把配置参数通过命令行指定,没有设置的配置将使用默认配置: 1# redis-server --configKey1 configValue1 --configKey2 configValue2 例如要用 6380 作为端口启动 Redis,那么执行: 1# redis-server --port 6380 虽然这种方式可以自定义配置,但是如果需要修改的配置较多或者希望将配置保存到文件中,不建议使用这种方式。 配置文件启动将配置写到文件里,例如我们将配置写到了 /opt/redis/redis.conf 中,那么只需执行如下命令即可启动 Redis: 1# redis-server /opt/redis/redis.conf Redis 有 60 多种配置,这里给出一些基本的配置: 配置名 配置说明 port 端口 logfile 日志文件 dir Redis 工作目录(存放持久化文件和日志文件) daemonize 是否以守护进程的方式启动 Redis Redis 命令行客户端现在我们已经启动了 Redis 服务,下面介绍如何使用 redis-cli 连接、操作 Redis 服务。redis-cli 可以使用两种方式连接 Redis 服务。 交互方式:通过 redis-cli -h {host} -p {port} 方式连接到 Redis 服务: 123vagrant@redis:~$ redis-cli -h localhost -p 6379localhost:6379> keys *(empty list or set) 命令方式:通过 redis-cli -h {host} -p {port} {command} 就可以直接得到返回结果,不需要启动 Redis shell 来交互访问: 12vagrant@redis:~$ redis-cli -h localhost -p 6379 get name"haohao" 注意:如果 -h 参数没有指定,那么默认 host 是 127.0.0.1 ,如果没有 -p 参数,那么默认 6379 端口,也就是说 -h 和 -p 都没写,就是连接 127.0.0.1:6379 这个实例。 停止 Redis 服务Redis 提供了 shutdown 命令来停止 Redis 服务,例如要停掉 127.0.0.1 上 6379 端口上的 Redis 服务,可以执行如下操作:1redis-cli shutdown 以这方式关闭 Redis 是一种优雅的方式,在关闭时会先将内存中的数据持久化到磁盘上(在配置文件中 dir 指定的目录中产生),然后关闭。如果直接 kill -9 强制杀掉不会产生持久化文件。shutdown 还有一个参数,代表是否在关闭 Redis 前生产持久化文件:1redis-cli shutdown nosave|save 通过 Vagrantfile 安装配置 Redis在此提供一个安装 Redis 的 vagrant 工程,通过 vagrant up 一键安装并配置 Redis,使用方式:123git clone [email protected]:qhh0205/infra-vagrant.gitcd infra-vagrant/redisvagrant up Redis 重大版本新增功能]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Nginx upstream 失效转移机制研究]]></title>
<url>%2F2018%2F07%2F08%2FNginx-upstream-%E5%A4%B1%E6%95%88%E8%BD%AC%E7%A7%BB%E6%9C%BA%E5%88%B6%E7%A0%94%E7%A9%B6%2F</url>
<content type="text"><def failover_post_method(name): print name return '<h1>I am Server2, My name is %s </h1>' % nameif __name__ == '__main__': app.run(debug=True, host='0.0.0.0') 第二台上游服务器超时代码 12345678910111213141516171819202122import timefrom flask import Flaskapp = Flask(__name__)@app.route('/a/<name>')def failover_get_method(name): print name while True: time.sleep(256) return '<h1>I am Server2, My name is %s </h1>' % [email protected]('/b/<name>', methods=["POST"])def failover_post_method(name): print name while True: time.sleep(256) return '<h1>I am Server2, my name is %s </h1>' % nameif __name__ == '__main__': app.run(debug=True, host='0.0.0.0') 论证过程以下为前面 4 种案例的论证过程: 案例 1上游服务器有两台,一台处于 down 状态,另一台处于正常服务状态。 在这种情况下,通过 curl 多次发送 GET 和 POST 请求,发现不管怎么请求,返回都是正常状态,如果 Nginx 发生了失败尝试操作,那么会在 Nginx access 日志中的 upstream 字段看到有两个服务器的地址。 发送 GET 和 POST 请求: 12curl -XGET https://ngxfailover.xxx.me/a/hellocurl -XPOST https://ngxfailover.xxx.me/b/hello 观察日志: 可以看出所有请求都成功了,红框框圈起来的请求表示发生了失效转移,并且请求成功。 案例 2上游服务器有两台,两台服务器都处于 down 状态。 在这种情况下不管是 GET 还是 POST 请求都会直接返回给客户端 502 错误。 发送 GET 和 POST 请求: 12curl -XGET https://ngxfailover.xxx.me/a/hellocurl -XPOST https://ngxfailover.xxx.me/b/hello 观察日志:可以看出所有请求全部返回 502 错误,红框框圈起来的请求表示发生了失效转移,但是还是失败了。 案例 3上游服务器有两台,一台机器的 http GET 和 POST 接口都正常 return,另一台相同的接口死循环,模拟超时。 这种情况下如果客户端的请求路由到了正常机器,那么直接返回 200。 如果请求路由到了死循环的接口,并且是 GET 请求,那么会等待 Nginx 设置的超时时间过后,然后将请求转发到另一台机器的正常接口。 如果请求路由到了死循环的接口,并且是 POST 请求,那么等待 Nginx 设置的超时时间过后直接返回客户端 504 错误,没有进行失效转移,防止请求的重复发送。 发送 GET 请求: 1curl -XGET https://ngxfailover.xxx.me/a/hello 观察日志: 可以看到对于 GET 请求全部成功,红框框圈起来的表示发生了失效转移,第一台超时后会是继续尝试第二台,最终成功。 发送 POST 请求: 1curl -XPOST https://ngxfailover.xxx.me/b/hello 观察日志: 可以看到对于 POST 请求,如果 Nginx 等待上游服务器处理请求超时,并不会发生失效转移,直接返回给客户端 504 错误。 案例 4上游服务器有两台,两台机器的 http GET 和 POST 接口都死循环,模拟超时。 这种情况下对于 GET 请求会将请求转发到另一台尝试,对于 POST 请求直接返回 504 错误,不会进行进一步尝试。 发送 GET 请求: 1curl -XGET https://ngxfailover.xxx.me/a/hello 观察日志: 可以看出对于 GET 请求,Nginx 在等待超时会继续进行尝试,两台都尝试失败后返回了 504 错误。 发送 POST 请求: 1curl -XPOST https://ngxfailover.xxx.me/b/hello 观察日志: 可以看出对于 POST 请求,Nginx 在等待超时会不继续进行尝试其他上游服务器,直接返回 504 错误。 总结总体来看 Nginx 的失效转移技术已经非常成熟,Nginx 默认情况下对于 connect refused(状态码表现为 502)和 time out(状态码表现为 504)已经做了失效转移,并且 Nginx 根据请求的类型不同,对失效转移的策略也不同。对于服务器后台状态没有改变的请求(比如 GET 请求)会进行失效转移,对于服务后台状态有改变的请求(比如 POST 请求),有失效转移机制,这也符合 Rest API 的冪等性标准。如果要强行加其他状态码的失效转移,比如 500、503 等,需要考量下业务请求是否能容忍请求的重复发送。]]></content>
<categories>
<category>Nginx</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu 下添加开机启动脚本]]></title>
<url>%2F2018%2F07%2F08%2FUbuntu-%E4%B8%8B%E6%B7%BB%E5%8A%A0%E5%BC%80%E6%9C%BA%E5%90%AF%E5%8A%A8%E8%84%9A%E6%9C%AC%2F</url>
<content type="text"><![CDATA[Ubuntu 下添加开机启动脚本本文介绍在 Ubuntu 下添加开机启动脚本的两种方法: 编辑 /etc/rc.local 文件Ubuntu 会在启动时自动执行 /etc/rc.local 文件中的脚本,默认该文件中有效的脚本代码为空,把需要执行的脚本添加到该文件的 exit 0 之前即可,举例如下: 123456789101112131415#!/bin/sh -e## rc.local## This script is executed at the end of each multiuser runlevel.# Make sure that the script will "exit 0" on success or any other# value on error.## In order to enable or disable this script just change the execution# bits.## By default this script does nothing.cd /home/ubuntuecho 'hello,world' >> rc.local.logexit 0 通过 update-rc.d 命令添加开机自启动脚本Ubuntu 服务器在启动时会自动执行 /etc/init.d 目录下的脚本,所以我们可以将需要执行的脚本放到 /etc/init.d 目录下,或者在该目录下创建一个软件链接指向其他位置的脚本路径,然后通过 update-rc.d 将脚本添加到开机自启动。启动脚本必须以 #!/bin/bash 开头。举例如下:新建开机启动脚本 start_when_boot,放置到 /etc/init.d 目录 1234#!/bin/bashcd /home/ubuntudate >> boot.logecho 'hello, world' >> boot.log 执行 update-rc.d start_when_boot defaults 将上述脚本添加为开机启动; 执行 update-rc.d -f start_when_boot remove 将上述开机启动脚本移除; 参考文章https://wangheng.org/ubuntu-to-add-boot-script.html]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用 cli53 自动化管理 Aws Route53]]></title>
<url>%2F2018%2F06%2F10%2F%E4%BD%BF%E7%94%A8-cli53-%E8%87%AA%E5%8A%A8%E5%8C%96%E7%AE%A1%E7%90%86-Aws-Route53%2F</url>
<content type="text"><![CDATA[cli53 工具cli53 是一个开源的命令行管理 Aws Route53 的工具,非常实用,可以通过命令行来进行域名及相关记录的创建、更新、删除、记录的导出备份、记录的导入恢复等。配置域名时无需在 Aws 界面控制台操作,只需用命令操作即可,能在一定程度上提高效率,将工作代码化。工具地址(Go 语言版):https://github.com/barnybug/cli53该工具还有个 Python 版本,是同一个作者,但是 Python 版的已不再维护,目前主要支持 Go 语言版的。 1. 安装https://github.com/barnybug/cli53 Mac 下安装:1brew install cli53 2. 配置 AWS 访问密钥 在控制台新建拥有 Route53 访问权限的 IAM 账号,获取 aws key 将 aws key 添加到环境变量12export AWS_ACCESS_KEY_ID="xxxx"export AWS_SECRET_ACCESS_KEY="xxxxx" 使用方法总结1.创建一个域名托管(指定的域名必须是有效的,否则报错)1cli53 create example.com --comment 'my first zone' 2.列出 Rout53 当前所有域名1cli53 list 3.导入 BIND 区域文件(用来做域名迁移)1cli53 import --file zonefile.txt example.com 4.导出域名 BIND 区域文件(用来备份,防止误操作导致不可恢复)12345# 导出非完全符合标准的 bind 文件cli53 export example.com# 导出完全符合标准的 bind 文件(一般使用该命令备份)cli53 export --full example.com 5.创建一个 A 记录指向 192.168.0.1,并设置 TTL 为 60s1cli53 rrcreate example.com 'www 60 A 192.168.0.1' 6.更新上面创建的 A 记录,指向 192.168.0.21cli53 rrcreate --replace example.com 'www 60 A 192.168.0.2' 7.删除一个 A 记录1cli53 rrdelete example.com www A 8.创建一个 MX 记录1cli53 rrcreate example.com '@ MX 10 mail1.' '@ MX 20 mail2.' 9.创建一个轮询的 A 记录1cli53 rrcreate example.com '@ A 127.0.0.1' '@ A 127.0.0.2' 10.创建 CNAME 记录12cli53 rrcreate example.com 'login CNAME www'cli53 rrcreate example.com 'mail CNAME ghs.googlehosted.com.' 11.创建 ELB 别名记录1cli53 rrcreate example.com 'www AWS ALIAS A dns-name.elb.amazonaws.com. ABCDEFABCDE false' 12.删除一个域名(⚠️危险 删除时如果域名有记录则必须指定 --purge 选项)1cli53 delete --purge example.com 13.删除一个域名的所有记录(⚠️危险)1cli53 rrpurge example.com 域名导入注意事项有的域名提供商,比如 GoDaddy 提供域名记录导出功能,但是导出来后的 BIND 域文件并不是符合标准的,CNAME或者 MX 记录末尾没有圆点 .,这样在导入 Route53 后会出现问题,需要在导入之前用如下命令处理一下文件(MX 和 CNAME 记录末尾添加圆点)再导入:1perl -pe 's/((CNAME|MX\s+\d+)\s+[-a-zA-Z0-9._]+)(?!.)$/$1./i' broken.txt > fixed.txt]]></content>
<categories>
<category>Aws</category>
</categories>
<tags>
<tag>AWS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Python 根据本地时间获取 UTC 偏移量]]></title>
<url>%2F2018%2F06%2F09%2FPython-%E6%A0%B9%E6%8D%AE%E6%9C%AC%E5%9C%B0%E6%97%B6%E9%97%B4%E8%8E%B7%E5%8F%96-UTC-%E5%81%8F%E7%A7%BB%E9%87%8F%2F</url>
<content type="text"><![CDATA[在 so 上查了一下,可以用如下代码获取本地时间相对于 UTC 时间的偏移量,代码实现思路比较简单,分别获取本地时间和和 UTC 时间,然后本地时间减去 UTC 时间即可得到相对于 UTC 的偏移小时,代码如下:123456789101112#!/usr/bin/env python# -*- coding: utf-8 -*-# @Time : 2018/4/6 上午10:28# @Author : qhh0205# @Mail : [email protected]# @File : utc_offset.pyimport timefrom datetime import datetimets = time.time()utc_offset = int((datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts)).total_seconds() / 3600)print "UTC%+-d" % utc_offset 参考链接(so 讨论):https://stackoverflow.com/questions/3168096/getting-computers-utc-offset-in-python?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa]]></content>
<categories>
<category>Python</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Shadowsocks + Privoxy 搭建 http 代理服务]]></title>
<url>%2F2018%2F05%2F20%2FShadowsocks-Privoxy-%E6%90%AD%E5%BB%BA-http-%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%2F</url>
<content type="text"><![CDATA[目前很多软件都支持配置 http 代理来加速访问,或者绕过 GFW 来获取需要的资源。比如 docker、git、gcloud、curl 这些软件都支持 http 代理。那么我们该如何轻松地基于 shadowsocks 搭建一个 http 代理,其实很简单,使用 privoxy 代理软件将收到的 http 请求转发给 shadowsocks 客户端即可。 Shadowsocks 软件整体架构如下图所示,shadowsocks 由两部分组成:客户端(SS Local),服务端(SS Server)。客户端就是用来做本地 Sock5 代理的,代理本地 PC 的请求和服务端通信,我们一般在手机、平板、PC 上安装的图形化 shadowsokcs 软件就是 SS 客户端软件,当然如果在 Linux 下,也有SS 客户端:sslocal,后面会介绍到如何配合 privoxy 来实现 http 代理服务。服务端就是在 Linux 服务器上安装的 shadowsocks 服务端软件,供客户端连接,我们一般说的搭建 shadowsocks 代理就是在服务器上安装并配置 SS Server。 Shadowsocks + privoxy 搭建 http 代理服务步骤整体架构如下图所示,我们需要找一台机器将 SS Server 搭建好,然后在局域网内的任何一台 Linux 服务器安装 SS Local 和 Privoxy,Privoxy 暴露 8118 端口作为 http 代理的端口: 1. 安装配置 Shadowsocks Server 端(ssserver)Shadowsocks 是用 Python 编写的,因此可以通过如下命令直接安装(sslocal 和 ssserver 均已安装):1sudo pip install shadowsocks 接下来编写 Shadowsocks Server 端的配置文件,配置监听端口,加密方式,密码等,新建 /etc/shadowsocks.json 文件,填入如下内容:12345678910{ "server":"0.0.0.0", "server_port":1851, # SS Server 端口 "local_address": "127.0.0.1", #SS Local 端配置,不影响Server端使用 "local_port":1080, #SS Local 端配置,不影响Server端使用 "password":"xxxxx", "timeout":300, "method":"aes-256-cfb", "fast_open": false} 使用配置文件启动 SS Server:1ssserver -c /etc/shadowsocks.json -d start 2. 安装配置 Shadowsocks 客户端(sslocal)第一步已将 SS Server 安装并配置完成,服务端口为 1851 ,接下来在需要安装 http 代理的机器上安装配置 shadowsocks 客户端,安装方法和第一步一样:1sudo pip install shadowsocks 编写 SS Local 客户端配置文件,配置远程连接 SS Server 的 IP,端口,密码,加密方式等,新建 /etc/shadowsocks.json 文件,填入如下内容:12345678910{ "server":"xxx.xxx.xxx.xxx", # SS Server 端服务器公网 IP "server_port":1851, # SS Server 端口 "local_address": "127.0.0.1", # SS Local 本地监听 IP "local_port:":1080, # SS Local 本地监听端口 "password":"xxxxxx", "timeout":300, "method":"aes-256-cfb", "fast_open": false} 使用配置文件启动 SS Local:1sslocal -c /etc/shadowsocks.json -d start 3. 安装并配置 PrivoxyPrivoxy 是一款代理软件,我们这里用该代理软件实现 HTTP 到 Socks5 的转换,所有来自 Privoxy 的请求被转发到 SS Local,从而实现了一个 HTTP 代理服务,Privoxy 的安装非常简单,直接 yum 一键搞定:1sudo yum install privoxy 编辑 Privoxy 配置文件 /etc/privoxy/config,搜索关键字 listen-address 找到 listen-address 127.0.0.1:8118 这一句,改成 listen-address 0.0.0.0:8118,表示该代理可以对外访问。接下来在该配置该文件末尾添加 HTTP 请求转发到 SS Local Socks5 的配置:1forward-socks5t / 127.0.0.1:1080 . forward-socks5t: 表示 Privoxy 转发请求到 Socks5 协议; 127.0.0.1: 第二步中启动 SS Local 本地绑定 IP; 1080: 第二步中启动 SS Local 本地监听端口; 启动 Privoxy:12systemctl restart privoxysystemctl enable privoxy 4. 测试代理是否可用1curl -x privoxy_ip:8118 https://www.google.com 参考文章https://vc2tea.com/whats-shadowsockshttps://docs.lvrui.io/2016/12/12/Linux%E4%B8%AD%E4%BD%BF%E7%94%A8ShadowSocks-Privoxy%E4%BB%A3%E7%90%86]]></content>
<categories>
<category>Shadowsocks</category>
</categories>
<tags>
<tag>http 代理</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何 dump jvm 内存及线程栈]]></title>
<url>%2F2018%2F05%2F19%2F%E5%A6%82%E4%BD%95-dump-jvm-%E5%86%85%E5%AD%98%E5%8F%8A%E7%BA%BF%E7%A8%8B%E6%A0%88%2F</url>
<content type="text"><![CDATA[目前很多企业的后台服务都是 java 服务,在故障出现时能及时 dump jvm 内存和线程栈对于故障的分析及定位是非常重要的。接下来介绍如何进行 dump 操作,并分享一个简单脚本实现服务器线程数超过一定阀值时自动 dump 线程数最高的 java 进程的内存及线程栈。 1. dump jvm 内存命令格式:1jmap -dump:format=b,file=dump_file_name pid 举例:dump pid 为 4738 的 java 进程的内存到 app_mem_dump.bin 文件1jmap -dump:format=b,file=app_mem_dump.bin 4738 2. dump jvm 线程栈命令格式:1jstack pid > dump_file_name 举例:dump pid 为 4738 的 java 进程的线程栈到 app_thread_dump.txt 文件1jstack 4738 > app_thread_dump.txt 脚本分享当服务器线程数超过 2500 时自动 dump 线程数最高的 java 进程的内存及线程栈。1234567891011121314151617181920#!/usr/bin/env bash# # 服务器线程数达到 2500 以上时 dump 线程数最多的 java 进程的线程及内存#source ~/.bashrccur_thread_num=`ps -efL | wc -l`if [ $cur_thread_num -le 2500 ]; then exit 0ficur_date=`date +"%Y-%m-%d_%H-%M-%S"`cd ./dumpfile# 服务器当前线程 dump 到文件:按照线程数由大到小排序显示ps -efL --sort -nlwp > server_thread_dump_$cur_date# dump 线程数最多的 jvm 的线程及内存most_thread_num_pid=`cat server_thread_dump_$cur_date | sed -n '2p' | awk '{print $2}'`nohup jstack -l $most_thread_num_pid > java_app_thread_dump_${cur_date}_pid_${most_thread_num_pid} &nohup jmap -dump:format=b,file=java_app_mem_dump_${cur_date}_pid_${most_thread_num_pid} $most_thread_num_pid &exit 0]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Jvm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自定义 Zabbix 监控指标及图表]]></title>
<url>%2F2018%2F05%2F19%2F%E8%87%AA%E5%AE%9A%E4%B9%89-Zabbix-%E7%9B%91%E6%8E%A7%E6%8C%87%E6%A0%87%E5%8F%8A%E5%9B%BE%E8%A1%A8%2F</url>
<content type="text"><![CDATA[问题描述 有时候 Zabbix 监控系统的模版提供的监控指标并不能满足我们的需求,比如我们要监控服务器的线程数、TCP 连接数等,这些指标在 Zabbix 自带的模板中是没有的,这时候我们就需要自定义监控指标来实现可视化监控。本文以监控服务器的 TCP 连接数为例来说明如何自定义监控指标来实现可视化监控。 解决问题总体思路是:修改 Zabbix Agent 端的配置文件,添加监控指标的键值对 –> 重启 Zabbix Agent –> 在 Zabbix Server 端界面化控制台中的模板添加监控指标,指定配置文件中的键 –> 创建指标的可视化展示。以下分步图文列出如何操作: 1. 修改 Zabbix Agent 端配置文件,添加监控指标的键值对vim 打开 Zabbix Agent 端配置文件 /home/zabbix/zabbix/etc/zabbix_agentd.conf ,末尾添加如下内容:12UnsafeUserParameters=1UserParameter=tcp.num,netstat -atunp | grep ESTABLISHED | wc -l UnsafeUserParameters: 自定义指标必需要添加该行; UserParameter: 自定义指标的参数; tcp.num: 监控指标的键,在 Zabbix Server 端创建监控指标时会用到,可以随意命名,比如 tcp.count; netstat -atunp | grep ESTABLISHED | wc -l:监控指标的值(注意:该值必须是数值类型,否则报错),获取服务器的 TCP 连接数,键和值之间通过英文逗号分隔; 2. 重启 Zabbix Agent 端1/home/zabbix/zabbix/sbin/zabbix_agentd -c /home/zabbix/zabbix/etc/zabbix_agentd.conf 3. 在 Zabbix Server 端界面化控制台创建监控指标为了让所有监控机器都能生效,所以在这里选择 Zabbix Server 自带的系统模板 Template OS Linux 中添加指标: 4. 创建指标的可视化展示在第三步中已经完成了监控指标的创建,即 Zabbix Server 已经开始收集 Agent 端的数据,但是我们还没有配置相应指标的可视化图表展示,无法看到该指标随时间的推移的变化趋势,接下来我们创建一个梯度图来可视化展示该指标的变化: 到这里该指标的可视化图表已创建完成,可以跳转到控制台首页查看相应 Zabbix Agent 的图表: 注意事项由于 netstat 命令在非 root 用户下使用会有警告信息:1234[zabbix@awsuw7-189 ~]$ netstat -atunp | grep ESTABLISHED | wc -l(Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.)329 这样 Zabbix Agent 配置文件中使用 netstat -atunp | grep ESTABLISHED | wc -l 获取到的 value 会是上面所有的输出(即字符串类型),而不是 wc -l 得到的数值类型,从而导致配置自定义监控指标后会报错: 解决方法其实比较简单,切换到 root 用户给 netstat 命令添加 s 权限即可解决:1[root@awsuw7-189 ~]# chmod u+s /bin/netstat]]></content>
<categories>
<category>Zabbix</category>
</categories>
<tags>