Hike News
Hike News

使用Arthas trace定位并优化接口响应慢的问题

Arthas trace命令介绍

https://alibaba.github.io/arthas/trace.html

打印方法内部调用路径,并输出方法路径上的每个节点上耗时。

trace命令只会trace匹配到的函数里的子调用,并不会向下trace多层。因为trace是代价比较贵的,多层trace可能会导致最终要trace的类和函数非常多。

使用方法

命令格式:

1
trace class method

示例:

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
[arthas@30]$ trace com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl checkPrePub
Press Q or Ctrl+C to abort.
Affect(class-cnt:2 , method-cnt:4) cost in 408 ms.
`---ts=2019-11-12 14:47:28;thread_name=http-nio-8266-exec-147;id=7938;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6c902fd5
`---[3771.437071ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:checkPrePub()
+---[0.003039ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getClusterCode() #157
+---[0.001022ms] java.util.Map:get() #157
+---[0.002185ms] com.sunshanpeng.platform.pub.common.enums.PubStatusEnum:getCode() #161
+---[0.001843ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setStatus() #161
+---[1645.580557ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:selectBySelective() #162
+---[0.002757ms] org.springframework.util.CollectionUtils:isEmpty() #163
+---[0.001587ms] com.sunshanpeng.platform.pub.common.enums.PubStatusEnum:getCode() #169
+---[0.002123ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setStatus() #169
+---[1478.93462ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:selectBySelective() #170
+---[0.002329ms] org.springframework.util.CollectionUtils:isEmpty() #171
`---[646.735412ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:buildPublishDetailDTO() #178
`---[646.539364ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:buildPublishDetailDTO()
+---[0.011378ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getPubEnv() #513
+---[0.002328ms] org.springframework.util.StringUtils:isEmpty() #513
+---[0.001789ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getClusterCode() #516
+---[0.001076ms] org.springframework.util.StringUtils:isEmpty() #516
+---[0.005232ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getSysCode() #519
+---[0.001092ms] org.springframework.util.StringUtils:isEmpty() #519
+---[0.004531ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getSysName() #522
+---[9.95E-4ms] org.springframework.util.StringUtils:isEmpty() #522
+---[0.001071ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getPubEnv() #525
+---[0.001343ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getClusterCode() #525
+---[0.001108ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getSysCode() #525
+---[1.617415ms] com.sunshanpeng.platform.pub.service.EnvBindService:getRelationByArgs() #525
+---[0.002319ms] java.util.List:get() #526
+---[0.014244ms] java.util.List:size() #527
+---[0.011744ms] com.sunshanpeng.platform.pub.dto.envbind.EnvBindDetailDTO:getJobName() #527
+---[0.001469ms] org.springframework.util.StringUtils:isEmpty() #527
+---[0.001367ms] com.sunshanpeng.platform.pub.dto.envbind.EnvBindDetailDTO:getJobName() #532
+---[0.006322ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setJobName() #532
+---[0.001252ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getPubEnv() #535
+---[0.001285ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getClusterCode() #535
+---[0.001253ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getSysCode() #535
+---[643.723767ms] com.sunshanpeng.platform.pub.service.JenkinsService:getBuildArgs() #535
+---[0.010914ms] com.sunshanpeng.platform.pub.common.enums.CommonStatusEnum:getCode() #536
+---[0.006734ms] com.sunshanpeng.platform.pub.dto.envbind.EnvBindDetailDTO:getSwEnable() #536
+---[0.014911ms] java.lang.Integer:equals() #536
+---[0.002085ms] java.lang.Boolean:valueOf() #536
+---[0.011168ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setApmEnable() #536
+---[0.004956ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getPubParamKey() #542
+---[0.002016ms] org.springframework.util.StringUtils:isEmpty() #542
+---[0.002196ms] java.util.Map:get() #550
+---[0.004316ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getPubParamValue() #551
+---[0.001027ms] org.springframework.util.StringUtils:isEmpty() #551
+---[0.004536ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getRemark() #559
+---[0.001113ms] org.springframework.util.StringUtils:isEmpty() #559
+---[0.004637ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setRemark() #560
+---[0.001502ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getSysCode() #562
+---[0.322113ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:getCiCode() #562
+---[0.005395ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setCiCode() #562
+---[0.004249ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setConsoleLog() #563
+---[0.001335ms] java.lang.Integer:valueOf() #564
+---[0.004697ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setBuildNo() #564
+---[0.004276ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setCreator() #565
+---[0.004354ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setCreatorCode() #566
+---[0.020689ms] java.time.LocalDateTime:now() #567
+---[0.004397ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setGmtCreate() #567
+---[0.002075ms] java.time.LocalDateTime:now() #568
+---[0.004739ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setGmtModify() #568
+---[0.001317ms] com.sunshanpeng.platform.pub.common.enums.PubStatusEnum:getCode() #569
`---[0.001365ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setStatus() #569

可以看到目标方法的耗时为3771.437071ms,其中com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:selectBySelective()方法调用了两次,并且耗时都在1.5秒左右。

优化前执行计划

1
2
3
4
5
6
7
8
9
mysql> explain select id, ci_code, build_no, creator_code, sys_code, sys_name, pub_env,cluster_code, job_name, pub_param_key, pub_param_value, status, remark,  creator, gmt_create, gmt_modify
from t_pub_publish
where sys_code = "aim" and pub_env = "qa" and cluster_code = 'cn-hd-idc-test-1' and status = 1;
+----+-------------+---------------+------+---------------+------+---------+------+-------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+---------------+------+---------+------+-------+-------------+
| 1 | SIMPLE | t_pub_publish | ALL | NULL | NULL | NULL | NULL | 30544 | Using where |
+----+-------------+---------------+------+---------------+------+---------+------+-------+-------------+
1 row in set

加组合索引

1
2
ALTER TABLE `t_pub_publish`
ADD INDEX `idx_code` (`sys_code`, `pub_env`, `cluster_code`, `status`) ;

优化后执行计划

1
2
3
4
5
6
7
8
9
mysql> explain select id, ci_code, build_no, creator_code, sys_code, sys_name, pub_env,cluster_code, job_name, pub_param_key, pub_param_value, status, remark,  creator, gmt_create, gmt_modify
from t_pub_publish
where sys_code = "aim" and pub_env = "qa" and cluster_code = 'cn-hd-idc-test-1' and status = 1;
+----+-------------+---------------+------+---------------+----------+---------+-------------------------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+---------------+----------+---------+-------------------------+------+-----------------------+
| 1 | SIMPLE | t_pub_publish | ref | idx_code | idx_code | 249 | const,const,const,const | 1 | Using index condition |
+----+-------------+---------------+------+---------------+----------+---------+-------------------------+------+-----------------------+
1 row in set

优化后耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[arthas@30]$ trace com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl checkPrePub 
Press Q or Ctrl+C to abort.
Affect(class-cnt:2 , method-cnt:1) cost in 390 ms.
`---ts=2019-11-13 13:42:15;thread_name=http-nio-8266-exec-143;id=7934;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6c902fd5
`---[203.648108ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:checkPrePub()
+---[min=4.7E-4ms,max=0.010168ms,total=0.010638ms,count=2] java.lang.Integer:<init>() #157
+---[4.61E-4ms] java.lang.reflect.Method:invoke() #157
+---[0.003624ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:getClusterCode() #157
+---[0.001096ms] java.util.Map:get() #157
+---[0.002196ms] com.sunshanpeng.platform.pub.common.enums.PubStatusEnum:getCode() #161
+---[0.001674ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setStatus() #161
+---[0.578989ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:selectBySelective() #162
+---[6.84E-4ms] org.springframework.util.CollectionUtils:isEmpty() #163
+---[5.82E-4ms] com.sunshanpeng.platform.pub.common.enums.PubStatusEnum:getCode() #169
+---[5.45E-4ms] com.sunshanpeng.platform.pub.dto.publish.PublishDetailDTO:setStatus() #169
+---[0.407368ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:selectBySelective() #170
+---[5.59E-4ms] org.springframework.util.CollectionUtils:isEmpty() #171
`---[202.38997ms] com.sunshanpeng.platform.pub.service.impl.PublishServiceImpl:buildPublishDetailDTO() #178

优化后SQL查询时间降到1ms不到,总耗时200ms,效果明显。

使用Arthas tt命令记录调用信息

Arthas tt命令介绍

https://alibaba.github.io/arthas/tt.html

方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

使用方法

命令格式:

1
tt -t class method

记录指定方法的每次调用环境现场

1
2
3
4
5
6
7
8
9
10
$ tt -t demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 66 ms.
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors

检索调用记录

列出所有调用记录

1
2
3
4
5
6
7
8
9
10
11
$ tt -l
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 4 ms.

筛选调用记录

1
2
3
4
5
6
7
8
9
10
11
$ tt -s 'method.name=="primeFactors"'
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 607 ms.

查看调用信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tt -i 1003
INDEX 1003
GMT-CREATE 2018-12-04 11:15:41
COST(ms) 0.186073
OBJECT 0x4b67cf4d
CLASS demo.MathGame
METHOD primeFactors
IS-RETURN false
IS-EXCEPTION true
PARAMETERS[0] @Integer[-564322413]
THROW-EXCEPTION java.lang.IllegalArgumentException: number is: -564322413, need >= 2
at demo.MathGame.primeFactors(MathGame.java:46)
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(MathGame.java:16)

Affect(row-cnt:1) cost in 11 ms.

重新发起一次调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ tt -i 1004 -p
RE-INDEX 1004
GMT-REPLAY 2018-12-04 11:26:00
OBJECT 0x4b67cf4d
CLASS demo.MathGame
METHOD primeFactors
PARAMETERS[0] @Integer[946738738]
IS-RETURN true
IS-EXCEPTION false
COST(ms) 0.186073
RETURN-OBJ @ArrayList[
@Integer[2],
@Integer[11],
@Integer[17],
@Integer[2531387],
]
Time fragment[1004] successfully replayed.
Affect(row-cnt:1) cost in 14 ms.

replay的注意事项

  1. ThreadLocal 信息丢失

    很多框架偷偷的将一些环境变量信息塞到了发起调用线程的 ThreadLocal 中,由于调用线程发生了变化,这些 ThreadLocal 线程信息无法通过 Arthas 保存,所以这些信息将会丢失。

    一些常见的 CASE 比如:鹰眼的 TraceId 等。

  2. 引用的对象

    需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。

使用Arthas watch观察方法执行

Arthas watch命令介绍

https://alibaba.github.io/arthas/watch.html

方便的观察到指定方法的调用情况。

能观察到的范围为:入参返回值抛出异常当前对象的属性,通过编写 OGNL 表达式进行对应变量的查看。

使用方法

命令格式:

1
watch class method "{params,returnObj,throwExp}" -x 2

观察方法出参和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 44 ms.
ts=2018-12-03 19:16:51; [cost=1.280502ms] result=@ArrayList[
@Object[][
@Integer[535629513],
],
@ArrayList[
@Integer[3],
@Integer[19],
@Integer[191],
@Integer[49199],
],
]

调整-x的值,观察具体的方法参数值

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
$ watch demo.MathGame primeFactors "{params,target}" -x 3
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 58 ms.
ts=2018-12-03 19:34:19; [cost=0.587833ms] result=@ArrayList[
@Object[][
@Integer[47816758],
],
@MathGame[
random=@Random[
serialVersionUID=@Long[3905348978240129619],
seed=@AtomicLong[3133719055989],
multiplier=@Long[25214903917],
addend=@Long[11],
mask=@Long[281474976710655],
DOUBLE_UNIT=@Double[1.1102230246251565E-16],
BadBound=@String[bound must be positive],
BadRange=@String[bound must be greater than origin],
BadSize=@String[size must be non-negative],
seedUniquifier=@AtomicLong[-3282039941672302964],
nextNextGaussian=@Double[0.0],
haveNextNextGaussian=@Boolean[false],
serialPersistentFields=@ObjectStreamField[][isEmpty=false;size=3],
unsafe=@Unsafe[sun.misc.Unsafe@2eaa1027],
seedOffset=@Long[24],
],
illegalArgumentCount=@Integer[13159],
],
]

-x表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是1。

条件表达式的例子

1
2
3
4
5
6
7
$ watch demo.MathGame primeFactors "{params[0],target}" "params[0]<0"
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 68 ms.
ts=2018-12-03 19:36:04; [cost=0.530255ms] result=@ArrayList[
@Integer[-18178089],
@MathGame[demo.MathGame@41cf53f9],
]

按照耗时进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
$ watch demo.MathGame primeFactors '{params, returnObj}' '#cost>200' -x 2
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 66 ms.
ts=2018-12-03 19:40:28; [cost=2112.168897ms] result=@ArrayList[
@Object[][
@Integer[2141897465],
],
@ArrayList[
@Integer[5],
@Integer[428379493],
],
]

#cost>200(单位是ms)表示只有当耗时大于200ms时才会输出,过滤掉执行时间小于200ms的调用

观察当前对象中的属性

1
2
3
4
5
6
7
$ watch demo.MathGame primeFactors 'target'
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 52 ms.
ts=2018-12-03 19:41:52; [cost=0.477882ms] result=@MathGame[
random=@Random[java.util.Random@522b408a],
illegalArgumentCount=@Integer[13355],
]

Git开发规范

注释规范

1
2
3
4
5
6
7
8
9
10
feature:新增feature
fix: 修复bug
docs: 仅仅修改了文档,比如README, CHANGELOG, CONTRIBUTE等等
sql: 新增、修改SQL文件
style: 仅仅修改了空格、格式缩进、符号等等,不改变代码逻辑
refactor: 代码重构,没有加新功能或者修复bug
perf: 优化相关,比如提升性能、体验
test: 测试用例,包括单元测试、集成测试等
chore: 改变构建流程、或者增加依赖库、工具等
revert: 回滚到上一个版本

每个commit必须包含以上注释,方便review。

分支规范

  • master分支:生产分支,用于存放发布到生产环境的代码。该分支只能从其他分支合并,不能直接修改推送。
  • feature/*分支:功能分支,用来开发新功能。开发完成后合入测试或者生产分支,被合并后删除。
  • hotfix/*分支:补丁分支,用来紧急修复生产环境的缺陷。开发完成后合入测试或者生产分支,被合并后删除。
  • 环境分支:可选项,如果有要求可以加测试环境的分支,规范同生产分支。

每次开发从master分支中检出,命名为feature/version_no,开发完成后通过pull request/merger request合入环境分支进行测试,最后合入master分支。上线部署后,在master分支增加版本号tag。

Label规范

  • feature:新功能
  • enhancement:性能优化或者功能增强
  • bug:问题修复
  • backend:后端功能(如果前后端代码都有)
  • frontend:前端功能(如果前后端代码都有)
  • 其他特性label

Issues和Pull Request/Merge Request最好都加上Label,方便分类。

ChaosBlade使用

安装

二进制

1
2
3
4
5
6
7
#下载
wget https://github.com/chaosblade-io/chaosblade/releases/download/v0.3.0/chaosblade-0.3.0.linux-amd64.tar.gz
#解压
tar -zxvf chaosblade-0.3.0.linux-amd64.tar.gz
#运行
cd chaosblade-0.3.0/
./blade version

命令

命令介绍

最主要的命令是创建命令create和创建后的销毁命令destroy

创建了以后一定要销毁!!!

创建了以后一定要销毁!!!

创建了以后一定要销毁!!!

preparerevoke 命令调用混沌实验准备执行器准备或者恢复实验环境。

混沌实验和混沌实验环境准备记录都可以通过status 命令查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@master4 ~]# ./blade help
An easy to use and powerful chaos engineering experiment toolkit

Usage:
blade [command]

Available Commands:
create Create a chaos engineering experiment
destroy Destroy a chaos experiment
help Help about any command
prepare Prepare to experiment
query Query the parameter values required for chaos experiments
revoke Undo chaos engineering experiment preparation
server Server mode starts, exposes web services
status Query preparation stage or experiment status
version Print version info

Flags:
-d, --debug Set client to DEBUG mode
-h, --help help for blade

Use "blade [command] --help" for more information about a command.

创建命令

可以创建CPU、内存、磁盘、网络、进程、脚本、Docker、K8S的故障场景。

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
[root@master4 ~]# blade create help
Create a chaos engineering experiment

Usage:
blade create [command]

Aliases:
create, c

Examples:
create dubbo delay --time 3000 --offset 100 --service com.example.Service --consumer

Available Commands:
cplus c++ experiment
cpu Cpu experiment
disk Disk experiment
docker Execute a docker experiment
druid Druid experiment
dubbo dubbo experiment
http http experiment
jedis jedis experiment
jvm method
k8s Kubernetes experiment
mem Mem experiment
mysql mysql experiment
network Network experiment
process Process experiment
psql Postgrelsql experiment
rocketmq Rocketmq experiment,can make message send or pull delay and exception
script Script chaos experiment
servlet java servlet experiment

Flags:
-h, --help help for create

Global Flags:
-d, --debug Set client to DEBUG mode

Use "blade create [command] --help" for more information about a command.

CPU

CPU满负载

1
2
[root@master4 chaosblade-0.3.0]# ./blade create cpu fullload
{"code":200,"success":true,"result":"4077e008d9a1b63b"}

返回参数中的resultUid,后续destroy命令需要用到。

指定CPU负载

1
2
[root@master4 chaosblade-0.3.0]# ./blade create cpu fullload --cpu-percent 80
{"code":200,"success":true,"result":"8b465a05ec5f3468"}

80%负载。

另外也可以使用--cpu-count指定满负载的CPU个数或者--cpu-list指定具体满负载的CPU。

指定故障时间

1
2
[root@master4 chaosblade-0.3.0]# ./blade create cpu fullload --cpu-percent 80 --timeout 10
{"code":200,"success":true,"result":"0136822f8f505c61"}

--timeout可以指定该故障的持续时间,单位是秒,指定持续时间后自动终止该故障。

后续的其他故障也都可以指定故障时间。

查看CPU负载

  • top
  • iostat -c 1 1000

内存

指定内存负载

1
2
[root@master4 chaosblade-0.3.0]# ./blade c mem load --mem-percent 90
{"code":200,"success":true,"result":"55d737f88301d0a5"}

查看内存负载

  • top
  • free -lh

磁盘

磁盘容量不足

1
2
[root@master4 chaosblade-0.3.0]# ./blade create disk fill --path /home --size 60000
{"code":200,"success":true,"result":"5e16812a5393e610"}

在指定目录--path填充指定大小--size的数据,单位是M。

查看磁盘容量

1
2
3
4
5
6
7
[root@master4 chaosblade-0.3.0]# df -h /home
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 58G 5.4G 53G 10% /

[root@master4 chaosblade-0.3.0]# df -h /home
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 58G 53G 5.1G 92% /

磁盘读 IO高负载

1
./blade create disk burn --read --path /home

磁盘写IO高负载

1
./blade create disk burn --write --path /home

查看IO负载

1
iostat -x -t 2

网络

网络延时

参数

1
2
3
4
5
6
7
8
--destination-ip string   目标 IP. 支持通过子网掩码来指定一个网段的IP地址, 例如 192.168.1.0/24. 则 192.168.1.0~192.168.1.255 都生效。你也可以指定固定的 IP,如 192.168.1.1 或者 192.168.1.1/32。
--exclude-port string 排除掉的端口,可以指定多个,使用逗号分隔或者连接符表示范围,例如 22,8000 或者 8000-8010。 这个参数不能与 --local-port 或者 --remote-port 参数一起使用
--interface string 网卡设备,例如 eth0 (必要参数)
--local-port string 本地端口,一般是本机暴露服务的端口。可以指定多个,使用逗号分隔或者连接符表示范围,例如 80,8000-8080
--offset string 延迟时间上下浮动的值, 单位是毫秒
--remote-port string 远程端口,一般是要访问的外部暴露服务的端口。可以指定多个,使用逗号分隔或者连接符表示范围,例如 80,8000-8080
--time string 延迟时间,单位是毫秒 (必要参数)
--timeout string 设定运行时长,单位是秒,通用参数

指定本地端口延时

所有其他服务器访问本地端口延时。

1
2
[root@master4 chaosblade-0.3.0]# ./blade create network delay --time 3000 --offset 1000 --interface eno16780032 --local-port 9100
{"code":200,"success":true,"result":"88626fd1175317ff"}

在另一台机器验证:telnet xxx.xxx.xxx.xxx 9100

到远程IP端口延时

当前机器访问远程IP端口延时,其他机器不受影响。

1
2
[root@master4 chaosblade-0.3.0]# ./blade create network delay --time 3000 --interface eno16780032 --remote-port 8080 --destination-ip 10.22.19.8
{"code":200,"success":true,"result":"fb75e208d0793104"}

演练机器验证:

1
2
3
4
5
[root@master4 chaosblade-0.3.0]# curl -o /dev/null -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" "http://10.22.19.8:8080/"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11266 0 11266 0 0 1251 0 --:--:-- 0:00:09 --:--:-- 2819
0.000::3.005::6.009::9.002::1251.000

同时间正常机器:

1
2
3
4
5
[root@master1 ~]# curl -o /dev/null -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" "http://10.22.19.8:8080/"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11266 0 11266 0 0 4998k 0 --:--:-- --:--:-- --:--:-- 5500k
0.000::0.001::0.002::0.002::5118582.000

让一个容器服务超时

首先找到该服务的宿主机,再找到容器对应的网卡,然后在服务宿主机上设置该网卡超时。

1
./blade create network delay --time 3000 --interface cali4c37fec8d88 --timeout 30

网络丢包

参数

1
2
3
4
5
6
7
--destination-ip string   目标 IP. 支持通过子网掩码来指定一个网段的IP地址, 例如 192.168.1.0/24. 则 192.168.1.0~192.168.1.255 都生效。你也可以指定固定的 IP,如 192.168.1.1 或者 192.168.1.1/32。
--exclude-port string 排除掉的端口,可以指定多个,使用逗号分隔或者连接符表示范围,例如 22,8000 或者 8000-8010。 这个参数不能与 --local-port 或者 --remote-port 参数一起使用
--interface string 网卡设备,例如 eth0 (必要参数)
--local-port string 本地端口,一般是本机暴露服务的端口。可以指定多个,使用逗号分隔或者连接符表示范围,例如 80,8000-8080
--percent string 丢包百分比,取值在[0, 100]的正整数 (必要参数)
--remote-port string 远程端口,一般是要访问的外部暴露服务的端口。可以指定多个,使用逗号分隔或者连接符表示范围,例如 80,8000-8080
--timeout string 设定运行时长,单位是秒,通用参数

指定本地端口丢包

所有其他服务器访问本地端口延时。

1
2
[root@master4 chaosblade-0.3.0]# ./blade create network loss --percent 70 --interface eno16780032 --local-port 9100
{"code":200,"success":true,"result":"88626fd1175317ff"}

在另一台机器验证: curl xxx.xxx.xxx.xxx:8080,不使用 telnet 的原因是 telnet 内部有重试机制,影响实验验证。如果将 percent 的值设置为 100,可以使用 telnet 验证

到远程IP端口丢包

当前机器访问远程IP端口延时,其他机器不受影响。

1
2
[root@master4 chaosblade-0.3.0]# ./blade create network loss --percent 70 --interface eno16780032 --remote-port 8080 --destination-ip 10.22.19.8
{"code":200,"success":true,"result":"fb75e208d0793104"}

演练机器验证:

1
2
3
4
5
[root@master4 chaosblade-0.3.0]# curl -o /dev/null -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" "http://10.22.19.8:8080/"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11266 0 11266 0 0 1502 0 --:--:-- 0:00:07 --:--:-- 3223
0.000::7.082::7.499::7.499::1502.000

同时间正常机器:

1
2
3
4
5
[root@master1 ~]# curl -o /dev/null -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" "http://10.22.19.8:8080/"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11266 0 11266 0 0 5254k 0 --:--:-- --:--:-- --:--:-- 10.7M
0.000::0.000::0.002::0.002::5380133.000

让一个容器服务丢包

首先找到该服务的宿主机,再找到容器对应的网卡,然后在服务宿主机上设置该网卡丢包。

1
./blade create network loss --percent 70 --interface cali4c37fec8d88 --timeout 30

网络隔离

丢包率100%即网络隔离。

DNS解析

1
2
3
4
5
6
7
[root@master4 chaosblade-0.3.0]# ./blade create network dns --domain www.baidu.com --ip 10.0.0.0
{"code":200,"success":true,"result":"5923c8feae34ec52"}
[root@master4 chaosblade-0.3.0]# ping www.baidu.com
PING www.baidu.com (10.0.0.0) 56(84) bytes of data.
^C
--- www.baidu.com ping statistics ---
24 packets transmitted, 0 received, 100% packet loss, time 23533ms

销毁实验

1
2
[root@master4 chaosblade-0.3.0]# ./blade destroy a6fa3362742e8c6f
{"code":200,"success":true,"result":"command: network loss --help false --interface eno16780032 --debug false --local-port 9100 --percent 70, destroy time: 2019-10-22T17:35:36.557547578+08:00"}

每个实验要么在创建的时候指定销毁时间--timeout,要么手动销毁,不然会一直执行。

查看实验状态

创建只会如果忘记了uid,可以通过status命令查询。

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
[root@master4 chaosblade-0.3.0]# ./blade status --type create
{
"code": 200,
"success": true,
"result": [
{
"Uid": "4cdc5e72e623d449",
"Command": "network",
"SubCommand": "delay",
"Flag": "--debug false --interface cali4c37fec8d88 --time 3000 --timeout 30 --help false",
"Status": "Destroyed",
"Error": "",
"CreateTime": "2019-10-22T16:35:53.572066698+08:00",
"UpdateTime": "2019-10-22T16:36:23.898669337+08:00"
},
{
"Uid": "01e7e9b19770834c",
"Command": "network",
"SubCommand": "delay",
"Flag": "--timeout 60 --debug false --interface cali4c37fec8d88 --time 3000 --help false",
"Status": "Destroyed",
"Error": "",
"CreateTime": "2019-10-22T16:42:30.821076685+08:00",
"UpdateTime": "2019-10-22T16:43:30.937855907+08:00"
}
]
}

混沌工程

为什么需要混沌工程

减少故障的最好方法就是让故障经常性的发生。通过不断重复失败过程,持续提升系统的容错和弹性能力。

在分布式架构环境下,服务间的依赖日益复杂,没有人能说清单个故障对整个系统的影响,构建一个高可用的分布式系统面临着很大挑战。

分布式系统天生包含大量的交互、依赖点,可以出错的地方数不胜数。硬盘故障、网络不通、流量激增压垮某些组件,这都是每天要面临的常事儿,处理不好就会导致业务停滞,性能低下,或者是其他各种无法预期的异常行为。

在复杂的分布式系统中,人力并不能够阻止这些故障的发生,我们应该致力于在这些异常行为被触发之前,尽可能多地识别出会导致这些异常的,在系统中脆弱的、易出故障的环节。 在线上事故出现之前, 通过观察分布式系统在受控的故障注入测试中的行为变化,提前识别出系统中有哪些弱点以及这些弱点的影响范围。 当我们识别出这些风险,我们就可以有针对性地进行加固、防范, 提高系统可靠性,建立系统抵御失控条件的能力,从而避免故障发生时所带来的严重后果。

相比等待故障发生然后被动式的故障响应流程,混沌工程可以在可控范围或环境下对系统注入各种故障,持续提升分布式系统的容错和弹性能力,从而构建高可用的分布式系统。

怎么样实践混沌工程

实践原则

完善的监控系统

如果没有对系统的可见能力,就无法从实验中得出有效的结论。

if you can't measure it you can't improve it.

尽可能的贴近生产

(流量、数据量、配置、架构等)越贴近生产环境演练效果越好。

演练事件真的可能发生

  • 故障类:服务器异常、断网等硬件故障,外部服务不可用,人工误操作等;
  • 非故障类:流量激增、 伸缩事件;

可以分析曾经引起生产系统故障的事件,针对性的排列优先级并实施,避免故障重现。

让故障演练成为常态

服务迭代频繁,系统不断变化,需要经常性的演练才能覆盖到系统的变更。

混沌工程不是制造问题,而是揭示问题。

最小化影响范围

混沌工程可能导致线上功能不可用,所以在以找出系统弱点为目标的前提下,需要最小化故障影响范围,并且当出现问题时可以迅速恢复,即故障是可控的。

演练方案

先测试后生产

演练计划先在测试环境验证,故障可控的前提下再到生产环境实施。

先次要后主要

先在边缘系统进行故障演练,再对在线业务实施故障演练。

跟踪演练计划

记录每次演练的故障对象、故障场景、期望状态、实际状态、影响范围、修复方案。

  1. 如果故障对象对该故障场景的实际状态和期望状态一致,则可以认为该故障对象对这种故障是高可用的。
  2. 如果故障对象对该故障场景的实际状态和期望状态不一致,那就是一个系统弱点,需要制定方案来修复,修复后继续演练来验证。

如果不能准确的观察结果和深入分析原因并得出修复方案,那就只是在搞破坏,而不是在做混沌工程。

演练目标

提升系统的容错和弹性能力,达到小的故障不需要人工介入,大故障人工介入可以快速恢复的目的。

  • 不需要人工介入的故障:做成不定时不定目标不定异常类型的自动化实现,常态化执行。
  • 需要人工介入的故障:告警及时、准确,监控数据清晰、有效,定位问题迅速,预案完善。

阶段一

在测试环境制造故障,观察服务状态和监控数据,评估系统可靠性,寻找系统弱点。

建立查找问题-解决问题-验证问题的循环,明确该问题是否需要人工介入。

对于需要人工介入的故障优化监控、告警,制定预案。

阶段二

将测试环境演练成功的计划放到生产环境执行验证。明确故障影响范围及解决时效,汇总实验数据。

  • 不需要人工介入的故障是否有对用户造成影响,自动解决的时间多久。
  • 需要人工介入的故障影响范围及问题解决时长。

持续提高团队对故障的检测、响应、处理还有恢复能力。

阶段三

建设全链路压测来模拟流量激增现象,建设A/B测试来最小化影响范围,增强混沌工程演练能力。

阶段四

建设故障演练平台,自动化演练(启动执行、监控、终止、结果分析),常态化执行。

实施步骤

  1. 首先,用系统在正常行为下的一些可测量的输出来定义“稳定状态”。
  2. 其次,假设系统在控制组和实验组都会继续保持稳定状态。
  3. 然后,在实验组中引入反映真实世界事件的变量,如服务器崩溃、硬盘故障、网络连接断开等。
  4. 最后,通过控制组和实验组之间的状态差异来反驳稳定状态的假说。

img

破坏稳态的难度越大,我们对系统行为的信心就越强。如果发现了一个弱点,那么我们就有了一个改进目标。可以避免在系统规模化之后被放大。

系统弱点

以 A 调用 B,B 调用 C,A 同时也调用 D 举例,B1、B2 是 B 服务的多个实例,依次类推。

受上游服务影响

被上游服务的高并发压垮

请求限流

场景模拟:高并发请求B1。

预期方案:B1设有最大并发量,达到最大并发量后B1服务触发限流,新请求快速失败。

修复方案:添加限流能力。

受下游服务影响

下游服务异常导致本服务异常

失败重试

场景模拟:对B1注入故障,A服务调用到B1时会出现调用失败。

预期方案:A服务的请求路由到B2进行重试。

修复方案:添加失败检测和请求重试能力,B服务的接口需要有幂等性。

实例隔离

场景模拟:对B1注入宕机故障,A服务调用到B1时会出现调用失败。

预期方案:给定的失败时间或者给定的失败次数后,自动隔离或下线 B1 实例。

修复方案:添加服务质量检查,下线不可用的服务实例。

下游服务异常拖垮本服务

服务降级

场景模拟:对B所有实例注入故障,A服务调用B时都失败。

预期方案:调用本地降级方法。

修复方案:准备一个本地的fallback回调,返回一个默认值。

服务熔断

场景模拟:对B所有实例注入故障,A服务调用到B时都失败。

预期方案:给定的失败时间或者给定的失败次数后,A服务触发熔断,快速失败。

修复方案:添加熔断能力,下游服务不可用时能立即熔断,快速失败。

受中间件影响

中间件异常导致的程序逻辑出错

中间件异常导致的服务不可用

受第三方服务商影响

第三方服务不可用导致本服务功能不可用

场景模拟:A短信供应商不可用。

预期方案:切换B短信供应商。

修复方案:供应商备份。

第三方服务异常拖垮本服务

场景模拟:调用OSS服务响应慢。

预期方案:熔断第三方服务,暂不提供该服务。

修复方案:供应商备份。

故障场景

CPU异常

  • CPU高负载

内存异常

  • 内存高负载

磁盘异常

  • 磁盘高负载
  • 磁盘空间不足
  • 部分数据丢失
  • 全部数据丢失
  • 不可读
  • 不可写

网络异常

  • 瞬断
  • 延时
  • 丢包
  • 隔离
  • 外部服务不可达

进程异常

服务进程

  • Tomcat线程池打满
  • 异常关闭
  • 重启
  • 内存溢出

系统进程

  • 系统时间不同步
  • OOM kill

中间件

Redis

  • 节点故障
  • 重启
  • 批量删除
  • 数据丢失
  • 响应慢

MySQL

  • CPU升高(*2019-06-01)
  • IO升高
  • 节点故障
  • 数据丢失
  • 响应慢
  • 大量慢SQL
  • 连接池占满
  • 死锁
  • 不可写
  • 不可读
  • 数据同步延迟
  • 主备延迟

RocketMQ/Kafka

  • NameServer故障
  • Broker故障
  • 数据丢失
  • 消息推送延迟
  • 大量消息挤压

Apollo

  • ConfigService故障
  • AdminService故障
  • Portal故障
  • 数据丢失

XXL-Job

  • 调度器故障
  • 执行器故障

ElasticSearch

  • 节点故障
  • 内存溢出
  • 写请求飙高
  • 读请求飙高

DNS

  • 解析超时(*2019-10-01)
  • 解析错误

代码异常

  • 空指针

异常操作

  • 删除数据
  • 强杀进程

服务器异常

  • 宕机
  • 重启
  • 中木马
  • 修改系统配置

第三方服务不可用

  • 响应慢
  • 访问不了

数据中心故障

  • 断电
  • 断网

演练工具

chaosblade

GitHub:https://github.com/chaosblade-io/chaosblade

文档:https://chaosblade-io.gitbook.io/chaosblade-help-zh-cn/blade

chaosmonkey

GitHub:https://github.com/Netflix/chaosmonkey

pumba

GitHub:https://github.com/alexei-led/pumba

kube-monkey

GitHub:https://github.com/asobti/kube-monkey

参考

https://www.infoq.cn/article/jjp0c2bR4*Ulld0wb88r

https://tech.youzan.com/chaosmonkey/

https://www.cnblogs.com/tianqing/p/10499611.html

在k8s中使用eureka的几种姿势

前言

在Kubernetes中,service通过label关联pod,使用service的clusterIp即可访问到对应pod。

如果觉得clusterIp不好记,可以用coredns将service name解析成service的clusterIp,通过访问service name来访问对应的pod。

通过coredns+service,可以在单个kubernetes集群中无需其他依赖即可实现服务发现。

但由于某些其他原因我们需要使用额外的注册中心eureka,本文列举了几种注册方式和注意事项。

使用Pod IP注册

因为Pod的IP是跟随生命周期的,重新发布以后IP就变更了。

但是eureka的缓存机制相当长,重新发布后服务消费者很长一段时间会访问之前的Pod IP。

要解决这个问题有以下几个优化点:

优雅停止Pod

如果Pod被强杀掉,就不能立即从Eureka Server下线,直到Eureka Server的EvictionTask 来定时清理过期的客户端,默认60秒清理一次,清理掉之前还能被其他客户端发现并调用。

kubernetes提供了生命周期回调函数,会在容器被终止前调用preStop,执行停止Java进程的命令

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill"]

因为遵循一个容器只运行一个进程的原则,所以可以直接查找Java的pid然后kill,也可以用pidof命令查找Java的pid。

缩短服务发现时间

Eureka客户端默认30秒的缓存时间,ribbon默认30秒的缓存时间,加上Eureka Server的三级缓存机制readOnlyCache也有30秒的缓存时间,在开启Eureka Server的自我保护模式时,即使客户端从Eureka Server下线了,极端情况下也有可能在90秒以后才能在其他客户端的缓存中清除。

可以通过设置deployment中的环境变量修改默认的缓存时间:

eureka client缓存(单位:秒)

1
eureka.client.registryFetchIntervalSeconds = 10

ribbon缓存(单位:秒)

1
ribbon.ServerListRefreshInterval = 10

关闭Eureka Server的readOnlyCache(可选)

因为readWriteCacheMap用的LoadingCache有读写锁,使用readOnlyCacheMap可以增加吞吐量,中小集群可以关闭readOnlyCacheMap

关闭readWriteCacheMap

1
eureka.server.use-read-only-response-cache = false

也可以缩短readOnlyCache的刷新时间:

1
eureka.server.response-cache-update-interval-ms = 10

强制下线

即使优雅停止Pod、缩短客户端和Eureka Server的缓存时间,但是Eureka客户端还是有几率请求到已下线的Pod。

Eureka提供了强制下线的接口,可以先从Eureka Server强制下线,等待缓存时间过期再停止Pod。

client强制下线接口:

1
POST /pause
1
POST /service-registry

比如:

1
2
curl -X POST "http://localhost:$MANAGEMENT_PORT/pause"
curl -X POST "http://localhost:$MANAGEMENT_PORT/service-registry/instance-status" -H "Content-Type: text/plain; charset=utf-8" -d "OUT_OF_SERVICE"

Eureka Server强制下线接口:

1
PUT /eureka/apps/${appId}/${ip:port}/status?value=OUT_OF_SERVICE

比如:

1
curl -X PUT http://eureka:8080/eureka/apps/EUREKA-1/192.168.0.10:8080/status?value=OUT_OF_SERVICE

关于强制下线可以看一下参考里的文章。

具体配置

实践配置为Eureka客户端缓存10秒,ribbon缓存10秒,不关闭readOnlyCache(缓存30秒),加起来缓存时间50秒。

将停止脚本放在指定目录,并在preStop调用。

脚本逻辑:1.强制下线当前实例;2.暂停总缓存时间;3.停止服务。

配置环境变量:

1
2
3
4
5
6
7
8
9
10
- name: eureka.client.registryFetchIntervalSeconds # Eureka客户端缓存时间
value: "10"
- name: ribbon.ServerListRefreshInterval #ribbon缓存时间
value: "10"
- name: MANAGEMENT_PORT #management.port的端口
value: "${management.port}"
- name: SLEEP_SECOND #总缓存时间
value: "50"
- name: management.endpoints.web.exposure.include #启用并暴露service-registry
value: service-registry

preStop

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/sh", "/app/script/stop.sh"]

stop.sh

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
# 如果没有管理端口直接下线
if [ -z $MANAGEMENT_PORT ];then
echo "MANAGEMENT_PORT is empty, kill java"
ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill
exit 1
fi

# 默认暂停时间
if [ -z $SLEEP_SECOND ];then
echo "SLEEP_SECOND is empty, set 50"
SLEEP_SECOND=50
fi

# 从EUREKA下线
echo "curl pause $MANAGEMENT_PORT"
# curl -X POST "http://localhost:$MANAGEMENT_PORT/pause"
curl -X "POST" "http://localhost:$MANAGEMENT_PORT/service-registry/instance-status" -H "Content-Type: text/plain; charset=utf-8" -d "OUT_OF_SERVICE"

# 等待SLEEP_SECOND秒,SLEEP_SECOND = 客户端缓存时间(eureka+ribbon)+ readOnlyCache时间
echo ""
echo "sleep $SLEEP_SECOND s"
sleep $SLEEP_SECOND

# 停止服务
echo "kill java"
ps -ef | grep jav[a] | awk 'NR==1{print $2}' | xargs kill

使用Service的Cluster IP注册

Service的Cluster IP是一个虚拟IP,会经kube-proxy把流量负载均衡到后端endpoint。

而且Service的Cluster IP是固定的,除非主动删除后重建,不然可以一直固定。

使用Cluster IP等于使用了Kubernetes的服务发现,当Pod不可用时就会从service的endpoint列表中移除,流量就不会再负载均衡到该Pod。

配置方式(properties):

1
2
eureka.instance.ipAddress = ${Cluster IP}
eureka.instance.nonSecurePort = ${port} #service的port

使用Service注册虽然可以自动过滤下线的服务,但是ribbon内部使用keepalive进行会话保持,因为是客户端负载均衡所以A服务的A1实例所有请求都会经过service转发到同一个B服务的B1实例,从而出现负载不均衡的情况

使用Service的NodePort注册

默认情况下Pod的IP只能在本集群内的Node上访问,非本集群的服务器访问不了。

可以通过写路由表或者BGP来发布Pod路由。

NodePort是Pod对外提供访问的一种方式,使用Host IP+NodePort可以简单实现跨集群及虚拟机之间的服务互相访问。

配置方式(环境变量):

1
2
3
4
5
6
- name: eureka.instance.ipAddress
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: eureka.instance.nonSecurePort
value: "${NodePort}"

NodePort方式本质也是Service,所以也存在负载均衡的情况。

另外管理NodePort也是件麻烦事。

参考

http://www.itmuch.com/spring-cloud-sum/how-to-unregister-service-in-eureka/

Eureka服务注册发现

注册发现流程

通常服务注册发现都分为服务提供者(provider)、服务消费者(consumer)、注册中心(register)三个部分。

一个服务可以是服务提供者可以是服务消费者,也可以即是提供者也是消费者

其中provider和consumer作为客户端client,注册中心作为服务端server,都可以同时存在多个实例。

服务提供者把自身的实例信息(比如IP、PORT、状态等)在注册中心做登记,服务消费者通过注册中心获取服务提供者的信息然后发起调用。

常用的注册中心有Zookeeper、ETCD、Eureka、Consul、Nacos。

服务注册

首先我们会在client配置eureka server的地址,如:

1
2
eureka.client.service-url.defaultZone=
http://localhost:8081/eureka/,http://localhost:8082/eureka/,http://localhost:8083/eureka/

客户端会尝试向配置的eureka server发起注册请求。

入口:com.netflix.discovery.DiscoveryClient#register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}

核心代码:com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute

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
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
List<EurekaEndpoint> candidateHosts = null;
int endpointIdx = 0;
for (int retry = 0; retry < numberOfRetries; retry++) {
//currentHttpClient是上面配置的三个eureka server中的其中一个
//如果eureka server能用,就会一直请求同一个eureka server
EurekaHttpClient currentHttpClient = delegate.get();
EurekaEndpoint currentEndpoint = null;
if (currentHttpClient == null) {
if (candidateHosts == null) {
//获取eureka server列表
candidateHosts = getHostCandidates();
if (candidateHosts.isEmpty()) {
throw new TransportException("There is no known eureka server; cluster server list is empty");
}
}
if (endpointIdx >= candidateHosts.size()) {
throw new TransportException("Cannot execute request on any known server");
}

currentEndpoint = candidateHosts.get(endpointIdx++);
currentHttpClient = clientFactory.newClient(currentEndpoint);
}

try {
EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
delegate.set(currentHttpClient);
if (retry > 0) {
logger.info("Request execution succeeded on retry #{}", retry);
}
return response;
}
logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
} catch (Exception e) {
logger.warn("Request execution failed with message: {}", e.getMessage()); // just log message as the underlying client should log the stacktrace
}

//如果请求则把eureka server置为空,下次重试时会换一个eureka server
delegate.compareAndSet(currentHttpClient, null);
if (currentEndpoint != null) {
quarantineSet.add(currentEndpoint);
}
}
throw new TransportException("Retry limit reached; giving up on completing the request");
}

Eureka server获取

我们实际注册的eureka server和配置的eureka server顺序是不一样的,比如我们配置的顺序是http://localhost:8081/eureka,http://localhost:8082/eureka,http://localhost:8083/eureka,原先以为会先注册到http://localhost:8081/eureka,但实际是先尝试注册到http://localhost:8083/eureka

因为获取列表的时候做了一次randomize,如果是三个server实例,会交互1和3的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static <T extends EurekaEndpoint> List<T> randomize(List<T> list) {
List<T> randomList = new ArrayList<>(list);
if (randomList.size() < 2) {
return randomList;
}
Random random = new Random(LOCAL_IPV4_ADDRESS.hashCode());
int last = randomList.size() - 1;
for (int i = 0; i < last; i++) {
int pos = random.nextInt(randomList.size() - i);
if (pos != i) {
Collections.swap(randomList, i, pos);
}
}
return randomList;
}

客户端发起注册请求

源码:com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}

服务端接受注册请求

入口:com.netflix.eureka.resources.ApplicationResource#addInstance

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
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
// validate that the instanceinfo contains all the necessary required fields
if (isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
} else if (isBlank(info.getAppName())) {
return Response.status(400).entity("Missing appName").build();
} else if (!appName.equals(info.getAppName())) {
return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
} else if (info.getDataCenterInfo() == null) {
return Response.status(400).entity("Missing dataCenterInfo").build();
} else if (info.getDataCenterInfo().getName() == null) {
return Response.status(400).entity("Missing dataCenterInfo Name").build();
}

// handle cases where clients may be registering with bad DataCenterInfo with missing data
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
if (dataCenterInfo instanceof UniqueIdentifier) {
String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
if (isBlank(dataCenterInfoId)) {
boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
if (experimental) {
String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
return Response.status(400).entity(entity).build();
} else if (dataCenterInfo instanceof AmazonInfo) {
AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
if (effectiveId == null) {
amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
}
} else {
logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
}
}
}

registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}

核心代码:com.netflix.eureka.registry.AbstractInstanceRegistry

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
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
// 上只读锁
read.lock();
// 从本地MAP里面获取当前实例的信息。
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
// 增加注册次数到监控信息里面去。
REGISTER.increment(isReplication);
if (gMap == null) {
// 如果第一次进来,那么gMap为空,则创建一个ConcurrentHashMap放入到registry里面去
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
// putIfAbsent方法主要是在向ConcurrentHashMap中添加键—值对的时候,它会先判断该键值对是否已经存在。
// 如果不存在(新的entry),那么会向map中添加该键值对,并返回null。
// 如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值。
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
// 表明map中确实不存在,则设置gMap为最新创建的那个
gMap = gNewMap;
}
}
// 从MAP中查询已经存在的Lease信息 (比如第二次来)
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// 当Lease的对象不为空时。
if (existingLease != null && (existingLease.getHolder() != null)) {
// 当instance已经存在是,和客户端的instance的信息做比较,时间最新的那个,为有效instance信息
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp(); // server
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp(); // client
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// 这里只有当existinglease不存在时,才会进来。 像那种恢复心跳,信息过期的,都不会进入这里。
// Eureka-Server的自我保护机制做的操作,为每分钟最大续约数+2 ,同时重新计算每分钟最小续约数
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// Since the client wants to cancel it, reduce the threshold
// (1
// for 30 seconds, 2 for a minute)
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
// 构建一个最新的Lease信息
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
// 当原来存在Lease的信息时,设置他的serviceUpTimestamp, 保证服务开启的时间一直是第一次的那个
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 放入本地Map中
gMap.put(registrant.getId(), lease);
// 添加到最近的注册队列里面去,以时间戳作为Key, 名称作为value,主要是为了运维界面的统计数据。
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
// 分析instanceStatus
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}

// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);

// If the lease is registered with UP status, set lease service up timestamp
// 得到instanceStatus,判断是否是UP状态,
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
// 设置注册类型为添加
registrant.setActionType(ActionType.ADDED);
// 租约变更记录队列,记录了实例的每次变化, 用于注册信息的增量获取、
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
// 清理缓存 ,传入的参数为key
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}

到这里,服务注册就算完啦。

服务发现

Eureka server缓存

eureka server默认情况下有三个地方存储了服务信息,分别是com.netflix.eureka.registry.AbstractInstanceRegistry#registrycom.netflix.eureka.registry.ResponseCacheImpl#readWriteCacheMapcom.netflix.eureka.registry.ResponseCacheImpl#readOnlyCacheMap

registry

数据结构ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>,外面Map的key是AppName,就是注册在eureka上的服务;里面Map的key是IP,value是服务的具体实例。

所有的服务都是注册到registry上面的,eureka server提供的UI界面查询到的服务信息也是直接从registry中获取的

入口:org.springframework.cloud.netflix.eureka.server.EurekaController#status

核心代码:com.netflix.eureka.registry.AbstractInstanceRegistry#getApplicationsFromMultipleRegions

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
public Applications getApplicationsFromMultipleRegions(String[] remoteRegions) {

boolean includeRemoteRegion = null != remoteRegions && remoteRegions.length != 0;

logger.debug("Fetching applications registry with remote regions: {}, Regions argument {}",
includeRemoteRegion, Arrays.toString(remoteRegions));

if (includeRemoteRegion) {
GET_ALL_WITH_REMOTE_REGIONS_CACHE_MISS.increment();
} else {
GET_ALL_CACHE_MISS.increment();
}
Applications apps = new Applications();
apps.setVersion(1L);
for (Entry<String, Map<String, Lease<InstanceInfo>>> entry : registry.entrySet()) {
Application app = null;

if (entry.getValue() != null) {
for (Entry<String, Lease<InstanceInfo>> stringLeaseEntry : entry.getValue().entrySet()) {
Lease<InstanceInfo> lease = stringLeaseEntry.getValue();
if (app == null) {
app = new Application(lease.getHolder().getAppName());
}
app.addInstance(decorateInstanceInfo(lease));
}
}
if (app != null) {
apps.addApplication(app);
}
}
if (includeRemoteRegion) {
for (String remoteRegion : remoteRegions) {
RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);
if (null != remoteRegistry) {
Applications remoteApps = remoteRegistry.getApplications();
for (Application application : remoteApps.getRegisteredApplications()) {
if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {
logger.info("Application {} fetched from the remote region {}",
application.getName(), remoteRegion);

Application appInstanceTillNow = apps.getRegisteredApplications(application.getName());
if (appInstanceTillNow == null) {
appInstanceTillNow = new Application(application.getName());
apps.addApplication(appInstanceTillNow);
}
for (InstanceInfo instanceInfo : application.getInstances()) {
appInstanceTillNow.addInstance(instanceInfo);
}
} else {
logger.debug("Application {} not fetched from the remote region {} as there exists a "
+ "whitelist and this app is not in the whitelist.",
application.getName(), remoteRegion);
}
}
} else {
logger.warn("No remote registry available for the remote region {}", remoteRegion);
}
}
}
apps.setAppsHashCode(apps.getReconcileHashCode());
return apps;
}

readWriteCacheMap

数据结构LoadingCache<Key, Value>

LoadingCache是谷歌guava提供的一个缓存实现,核心代码如下:

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
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(1000)
//缓存失效时间,可以配置,默认180秒
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
//如果key对应的value为空,调用该方法获取value并放到缓存中,该方法是线程安全的
//这里是去registry中获取
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});

/*
* Generate pay load for the given key.
*/
private Value generatePayload(Key key) {
Stopwatch tracer = null;
try {
String payload;
switch (key.getEntityType()) {
case Application:
boolean isRemoteRegionRequested = key.hasRegions();

if (ALL_APPS.equals(key.getName())) {
if (isRemoteRegionRequested) {
tracer = serializeAllAppsWithRemoteRegionTimer.start();
payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeAllAppsTimer.start();
payload = getPayLoad(key, registry.getApplications());
}
} else if (ALL_APPS_DELTA.equals(key.getName())) {
if (isRemoteRegionRequested) {
tracer = serializeDeltaAppsWithRemoteRegionTimer.start();
versionDeltaWithRegions.incrementAndGet();
versionDeltaWithRegionsLegacy.incrementAndGet();
payload = getPayLoad(key, registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeDeltaAppsTimer.start();
versionDelta.incrementAndGet();
versionDeltaLegacy.incrementAndGet();
payload = getPayLoad(key, registry.getApplicationDeltas());
}
} else {
tracer = serializeOneApptimer.start();
payload = getPayLoad(key, registry.getApplication(key.getName()));
}
break;
case VIP:
case SVIP:
tracer = serializeViptimer.start();
payload = getPayLoad(key, getApplicationsForVip(key, registry));
break;
default:
logger.error("Unidentified entity type: " + key.getEntityType() + " found in the cache key.");
payload = "";
break;
}
return new Value(payload);
} finally {
if (tracer != null) {
tracer.stop();
}
}
}

缓存除了到失效时间后会失效以外,每次对registry进行写操作的时候也会调用com.netflix.eureka.registry.ResponseCacheImpl#invalidate删除缓存。

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
public void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
for (Key.KeyType type : Key.KeyType.values()) {
for (Version v : Version.values()) {
invalidate(
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.compact),
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.compact),
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.compact)
);
if (null != vipAddress) {
invalidate(new Key(Key.EntityType.VIP, vipAddress, type, v, EurekaAccept.full));
}
if (null != secureVipAddress) {
invalidate(new Key(Key.EntityType.SVIP, secureVipAddress, type, v, EurekaAccept.full));
}
}
}
}
public void invalidate(Key... keys) {
for (Key key : keys) {
logger.debug("Invalidating the response cache key : {} {} {} {}, {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());

readWriteCacheMap.invalidate(key);
Collection<Key> keysWithRegions = regionSpecificKeys.get(key);
if (null != keysWithRegions && !keysWithRegions.isEmpty()) {
for (Key keysWithRegion : keysWithRegions) {
logger.debug("Invalidating the response cache key : {} {} {} {} {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
readWriteCacheMap.invalidate(keysWithRegion);
}
}
}
}

由此可以看出,readWriteCacheMap的数据和registry的数据基本是一致的

readOnlyCacheMap

数据结构ConcurrentMap<Key, Value>

可以用useReadOnlyResponseCache配置控制是否启用readOnlyCacheMap,默认为true启用readOnlyCacheMap

初始化com.netflix.eureka.registry.ResponseCacheImpl的时候会判断useReadOnlyResponseCache,启用的话会开一个更新readOnlyCacheMap的定时任务:

1
2
3
4
5
6
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}

responseCacheUpdateIntervalMsreadOnlyCacheMapreadWriteCacheMap同步缓存的时间,默认30s。

核心代码:com.netflix.eureka.registry.ResponseCacheImpl#getCacheUpdateTask

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
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
logger.debug("Updating the client cache from response cache");
for (Key key : readOnlyCacheMap.keySet()) {
if (logger.isDebugEnabled()) {
Object[] args = {key.getEntityType(), key.getName(), key.getVersion(), key.getType()};
logger.debug("Updating the client cache from response cache for key : {} {} {} {}", args);
}
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
//从readWriteCacheMap取出对应Key的value,
//如果不一样以readWriteCacheMap的为准
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
logger.error("Error while updating the client cache from response cache", th);
}
}
}
};
}

由此可以看出,默认配置下从readOnlyCacheMap获取的实例信息,可能有30S的缓存时间数据是不一致的。

服务消费者缓存

服务消费者从eureka server获取实例信息并缓存在eureka client,如果用ribbon调用的话也会在ribbon内缓存一份实例信息。

Eureka Client缓存

Eureka Client的缓存放在com.netflix.discovery.shared.Applications#appNameApplicationMap中,appNameApplicationMap的数据结构是Map<String, Application>,key是appName。

应用启动初始化com.netflix.discovery.DiscoveryClient的时候会全量拉取一次服务信息,后面增量获取。

com.netflix.discovery.DiscoveryClient#initScheduledTasks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}

registryFetchIntervalSeconds是eureka client从eureka server同步实例信息的时间,默认30S。

核心代码:com.netflix.discovery.DiscoveryClient#fetchRegistry

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
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

try {
// If the delta is disabled or if it is the first time, get all
// applications
Applications applications = getApplications();

if (clientConfig.shouldDisableDelta()
|| (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
|| forceFullRegistryFetch
|| (applications == null)
|| (applications.getRegisteredApplications().size() == 0)
|| (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
{
logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
logger.info("Application is null : {}", (applications == null));
logger.info("Registered Applications size is zero : {}",
(applications.getRegisteredApplications().size() == 0));
logger.info("Application version is -1: {}", (applications.getVersion() == -1));
//全量获取
getAndStoreFullRegistry();
} else {
//增量获取
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}

// Notify about cache refresh before updating the instance remote status
onCacheRefreshed();

// Update remote status based on refreshed data held in the cache
updateInstanceRemoteStatus();

// registry was fetched successfully, so return true
return true;
}

Ribbon缓存

Ribbon的缓存放在com.netflix.loadbalancer.BaseLoadBalancer#allServerList中,应用启动的时候调用com.netflix.loadbalancer.PollingServerListUpdater#start启动一个更新ribbon缓存的定时任务。

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
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};

scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}

refreshIntervalMs是ribbon缓存的刷新时间,可以配置单个应用的刷新时间aim-ms.ribbon.ServerListRefreshInterval,也可以配置全局的刷新时间ribbon.ServerListRefreshInterval,都不配置的话默认是30S

获取配置代码:com.netflix.loadbalancer.PollingServerListUpdater

1
2
3
4
5
6
public PollingServerListUpdater(IClientConfig clientConfig) {
this(LISTOFSERVERS_CACHE_UPDATE_DELAY, getRefreshIntervalMs(clientConfig));
}
private static long getRefreshIntervalMs(IClientConfig clientConfig) {
return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
}

com.netflix.client.config.DefaultClientConfigImpl

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
public <T> T get(IClientConfigKey<T> key, T defaultValue) {
T value = get(key);
if (value == null) {
value = defaultValue;
}
return value;
}

public <T> T get(IClientConfigKey<T> key) {
Object obj = getProperty(key.key());
if (obj == null) {
return null;
}
Class<T> type = key.type();
if (type.isInstance(obj)) {
return type.cast(obj);
} else {
if (obj instanceof String) {
String stringValue = (String) obj;
if (Integer.class.equals(type)) {
return (T) Integer.valueOf(stringValue);
} else if (Boolean.class.equals(type)) {
return (T) Boolean.valueOf(stringValue);
} else if (Float.class.equals(type)) {
return (T) Float.valueOf(stringValue);
} else if (Long.class.equals(type)) {
return (T) Long.valueOf(stringValue);
} else if (Double.class.equals(type)) {
return (T) Double.valueOf(stringValue);
} else if (TimeUnit.class.equals(type)) {
return (T) TimeUnit.valueOf(stringValue);
}
throw new IllegalArgumentException("Unable to convert string value to desired type " + type);
}

throw new IllegalArgumentException("Unable to convert value to desired type " + type);
}
}
protected Object getProperty(String key) {
if (enableDynamicProperties) {
String dynamicValue = null;
DynamicStringProperty dynamicProperty = dynamicProperties.get(key);
if (dynamicProperty != null) {
dynamicValue = dynamicProperty.get();
}
if (dynamicValue == null) {
dynamicValue = DynamicProperty.getInstance(getConfigKey(key)).getString();
if (dynamicValue == null) {
dynamicValue = DynamicProperty.getInstance(getDefaultPropName(key)).getString();
}
}
if (dynamicValue != null) {
return dynamicValue;
}
}
return properties.get(key);
}

刷新缓存代码:com.netflix.loadbalancer.BaseLoadBalancer

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
public void setServersList(List lsrv) {
Lock writeLock = allServerLock.writeLock();
logger.debug("LoadBalancer [{}]: clearing server list (SET op)", name);

ArrayList<Server> newServers = new ArrayList<Server>();
writeLock.lock();
try {
ArrayList<Server> allServers = new ArrayList<Server>();
for (Object server : lsrv) {
if (server == null) {
continue;
}

if (server instanceof String) {
server = new Server((String) server);
}

if (server instanceof Server) {
logger.debug("LoadBalancer [{}]: addServer [{}]", name, ((Server) server).getId());
allServers.add((Server) server);
} else {
throw new IllegalArgumentException(
"Type String or Server expected, instead found:"
+ server.getClass());
}

}
boolean listChanged = false;
if (!allServerList.equals(allServers)) {
listChanged = true;
if (changeListeners != null && changeListeners.size() > 0) {
List<Server> oldList = ImmutableList.copyOf(allServerList);
List<Server> newList = ImmutableList.copyOf(allServers);
for (ServerListChangeListener l: changeListeners) {
try {
l.serverListChanged(oldList, newList);
} catch (Exception e) {
logger.error("LoadBalancer [{}]: Error invoking server list change listener", name, e);
}
}
}
}
if (isEnablePrimingConnections()) {
for (Server server : allServers) {
if (!allServerList.contains(server)) {
server.setReadyToServe(false);
newServers.add((Server) server);
}
}
if (primeConnections != null) {
primeConnections.primeConnectionsAsync(newServers, this);
}
}
// This will reset readyToServe flag to true on all servers
// regardless whether
// previous priming connections are success or not
allServerList = allServers;
if (canSkipPing()) {
for (Server s : allServerList) {
s.setAlive(true);
}
upServerList = allServerList;
} else if (listChanged) {
forceQuickPing();
}
} finally {
writeLock.unlock();
}
}

获取应用实例

eureka server入口:com.netflix.eureka.resources.ApplicationsResource#getContainers

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
@GET
public Response getContainers(@PathParam("version") String version,
@HeaderParam(HEADER_ACCEPT) String acceptHeader,
@HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
@HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
@Context UriInfo uriInfo,
@Nullable @QueryParam("regions") String regionsStr) {

boolean isRemoteRegionRequested = null != regionsStr && !regionsStr.isEmpty();
String[] regions = null;
if (!isRemoteRegionRequested) {
EurekaMonitors.GET_ALL.increment();
} else {
regions = regionsStr.toLowerCase().split(",");
Arrays.sort(regions); // So we don't have different caches for same regions queried in different order.
EurekaMonitors.GET_ALL_WITH_REMOTE_REGIONS.increment();
}

// Check if the server allows the access to the registry. The server can
// restrict access if it is not
// ready to serve traffic depending on various reasons.
if (!registry.shouldAllowAccess(isRemoteRegionRequested)) {
return Response.status(Status.FORBIDDEN).build();
}
CurrentRequestVersion.set(Version.toEnum(version));
KeyType keyType = Key.KeyType.JSON;
String returnMediaType = MediaType.APPLICATION_JSON;
if (acceptHeader == null || !acceptHeader.contains(HEADER_JSON_VALUE)) {
keyType = Key.KeyType.XML;
returnMediaType = MediaType.APPLICATION_XML;
}

Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);

Response response;
if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
response = Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
response = Response.ok(responseCache.get(cacheKey))
.build();
}
return response;
}

核心代码:com.netflix.eureka.registry.ResponseCacheImpl#getValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key :" + key, t);
}
return payload;
}

如果useReadOnlyCache为true从readOnlyCacheMap中获取实例信息,如果false从readWriteCacheMap中获取实例信息,默认为true。

因为readWriteCacheMap用的LoadingCache有读写锁,使用readOnlyCacheMap可以增加吞吐量,中小集群可以关闭readOnlyCacheMap

参考文档

https://blog.csdn.net/u012394095/article/details/80693713

http://blog.didispace.com/spring-cloud-eureka-register-detail/

https://www.infoq.cn/article/jlDJQ*3wtN2PcqTDyokh

Kubernetes高可用部署

基本架构

kubernetes

Kubernetes分为Master Node和Worker Node,其中Master Node作为控制节点,调度管理整个系统;Worker Node作为工作节点,运行业务容器。

Master Node

  • api-server

    作为kubernetes系统的入口,对外暴露了Kubernetes API,封装了核心对象的增删改查操作,以RESTFul接口方式提供给外部客户和内部组件调用。它维护的REST对象将持久化到etcd(一个分布式强一致性的key/value存储)。

  • scheduler

    负责集群的资源调度,为新建的pod分配机器。这部分工作分出来变成一个组件,意味着可以很方便地替换成其他的调度器。

  • controller-manager

    运行控制器,它们是处理集群中常规任务的后台线程。逻辑上,每个控制器是一个单独的进程,但为了降低复杂性,它们都被编译成独立的可执行文件,并在单个进程中运行。
    这些控制器包括:

    • 节点控制器: 当节点移除时,负责注意和响应。
    • 副本控制器: 负责维护系统中每个副本控制器对象正确数量的 Pod。
    • 端点控制器: 填充 端点(Endpoints) 对象(即连接 Services & Pods)。
    • 服务帐户和令牌控制器: 为新的命名空间创建默认帐户和 API 访问令牌

Worker Node

  • kubelet

    kubelet是主要的节点代理,它监测已分配给其节点的 Pod(通过 apiserver 或通过本地配置文件),提供如下功能:

    • 挂载 Pod 所需要的数据卷(Volume)。
    • 下载 Pod 的 secrets。
    • 通过 Docker 运行(或通过其他容器运行时)运行 Pod 的容器。
    • 周期性的对容器生命周期进行探测。
    • 如果需要,通过创建 镜像 Pod(Mirror Pod) 将 Pod 的状态报告回系统的其余部分。
      将节点的状态报告回系统的其余部分。
  • kube-proxy

    通过维护主机上的网络规则并执行连接转发,实现了Kubernetes服务抽象。

存储

Kubernetes 所有集群数据都存储在etcd里。

etcd通常部署在Master Node上,也可以单独部署。

官网地址:https://github.com/etcd-io/etcd

网络

CNI(Container Network Interface)是CNCF旗下的一个项目,由一组用于配置Linux容器的网络接口的规范和库组成,同时还包含了一些插件。CNI仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。

每个pod都会有一个集群内唯一的IP,不同Node上的Pod可以互相访问。

常用的网络插件有CalicoFlannel

容器

CRI(Container Runtime Interface)是容器运行时接口,kubelet可以运行实现了CRI的各种容器运行时而不仅仅是Docker。

高可用架构

要实现高可用,就要实现所有Node没有单点故障,任何一台服务器宕机均不影响Kubernetes的正常工作。Kubernetes的高可用主要是Master Node和存储的高可用,包括以下几个组件:

  • etcd属于CP架构,保证一致性和分区容错性。因为其内部使用 Raft 协议进行选举,所以需要部署基数个实例,比如3、5、7个。可以和Master Node一起部署也可以外置部署。
  • controller-manager和scheduler的高可用相对容易,是基于etcd的锁进行leader election。相当于基于etcd的分布式锁,谁拿到谁干活,同时只会有一个实例在工作,部署2个及以上的实例数量。
  • api-server是无状态的,需要部署2个及以上的实例数量,然后使用HAProxy和Keepalived作为负载均衡实现高可用。除了HAProxy也可以使用Nginx或其他负载均衡策略。

方案一

Kubernetes HA1

使用三台4核8G的服务器当Master Node,上面部署api-server、scheduler、controller-manager、kubelet、kube-proxy、docker、calico、etcd、HAProxy、keepalived。

Worker Node的配置根据实际情况,最好CPU和内存的大小、比例都一样。上面部署kubelet、kube-proxy、docker、calico。

该方案集群内外连接api-server都是高可用,使用方便。但是公有云不能用虚拟IP所以不能用该方案。另外所有的网络请求都会集中到一台服务器(虚拟IP所在的服务器)进行转发,负载不均衡,HAProxy的上限即api-server的上限。

方案二

Kubernetes HA2

使用三台4核8G的服务器当Master Node,上面部署api-server、scheduler、controller-manager、kubelet、kube-proxy、docker、calico、etcd。

Worker Node的配置根据实际情况,最好CPU和内存的大小、比例都一样。上面部署kubelet、kube-proxy、docker、calico、HAProxy。

该方案兼容自有机房和公有云,Master Node负载比较均衡。但是集群外访问api-server没有做到高可用,需要额外解决。如果Worker Node上的HAProxy宕机则该节点不能正常工作。另外如果添加和删除Master Node需要更新所有Worker Node的负载均衡配置(可选项,不修改也可以)。

部署方式

二进制部署

所有组件均使用二进制文件部署,手动部署可以参考:https://github.com/opsnull/follow-me-install-kubernetes-cluster;ansible脚本部署可以用:https://github.com/easzlab/kubeasz

容器化部署

kubernetes的所有组件中,除了docker和kubelet是运行和管理容器的,其他组件都可以使用容器化的方式部署,最流行的方式就是使用kubeadm。

官网地址:https://github.com/kubernetes/kubeadm。

参考

https://kubernetes.io/zh/docs/concepts/overview/components/

https://www.kubernetes.org.cn/2931.html

https://jimmysong.io/kubernetes-handbook/concepts/cni.html

https://jimmysong.io/kubernetes-handbook/concepts/cri.html

Docker常用命令

拉取镜像

1
docker pull mysql:5.6

格式:docker pull [OPTIONS] NAME[:TAG|@DIGEST],tag不写的话默认latest。

登录镜像仓库

1
docker login --username sunshanpeng --password 123456 harbor.sunshanpeng.com

格式:docker login [OPTIONS] [SERVER]

  • SERVER为空的情况下默认登录hub.docker.com

  • password建议不要显示在控制台上

列出所有镜像

1
docker images

格式:docker images [OPTIONS] [REPOSITORY[:TAG]],可选参数显示全部镜像或者过滤镜像。

列出所有容器

1
docker ps -a

格式:docker ps [OPTIONS],去掉-a显示运行中的容器。

给镜像打tag

1
docker tag mysql:5.6 harbor.sunshanpeng.com/database/mysql:5.6

格式:docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]

当我们从公共仓库拉取的镜像,想推送到我们自己搭建的私有仓库,需要先打一个tag;

当我们拉取不到一些需要翻墙才能拉取的镜像时,也可以从国内镜像仓库拉取镜像然后打个tag给成目标镜像。

推送镜像

1
docker push harbor.sunshanpeng.com/database/mysql:5.6

格式:docker push [OPTIONS] NAME[:TAG]

推送镜像必须先登录要推送的目标仓库,而且需要对目标仓库有推送的权限。

类似docker push mysql:5.6是不行的。

制作镜像

1
docker build -t simpleApp:v1 .

格式:docker build [OPTIONS] PATH | URL | -

根据Dockerfile制作镜像。

删除镜像

1
docker rmi mysql:5.6

格式:docker rmi [OPTIONS] IMAGE [IMAGE...],不能删除已经创建了容器的镜像,必须先删除对应容器。

运行容器

1
docker run nginx

格式:docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

OPTIONS参数有很多,比较常用的如下:

  • --name指定容器名字

  • -p指定容器映射宿主机的端口号

  • -e指定环境变量

  • -v指定挂载卷

  • -it指定容器前台运行

  • -d指定容器后台运行

    其他还有很多指定IP、Hostname、CPU、内存、网络、内核参数、重启策略等等参数,可以通过docker run --help查询。

    mysql容器启动命令

    1
    docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v=/docker/mysql/data:/var/lib/mysql -v=/docker/mysql/my.cnf:/etc/mysql/my.cnf -v=/usr/share/zoneinfo/Asia/Shanghai:/etc/localtime -d mysql:5.6

    redis容器启动命令

    1
    docker run --name redis -p 6379:6379 -v=/docker/redis/data:/data -d redis:3.2

重命名容器

1
docker rename mysql mysql1

格式:docker rename CONTAINER NEW_NAME

停止容器

1
docker stop mysql

格式:docker stop [OPTIONS] CONTAINER [CONTAINER...],停止多个容器用空格分隔容器名字或者容器id。

强制停止容器

1
docker kill mysql

格式:docker kill [OPTIONS] CONTAINER [CONTAINER...]

启动/重启容器

1
docker start/restart mysql

格式:docker start/restart [OPTIONS] CONTAINER [CONTAINER...]

删除容器

1
docker rm mysql

格式:docker rm [OPTIONS] CONTAINER [CONTAINER...],不带参数时只能删除已停止的容器,带上参数-f可以强制删除运行中的容器。

更新容器

1
docker update --restart always mysql

格式:docker update [OPTIONS] CONTAINER [CONTAINER...]

更新容器的重启策略、CPU和内存大小。

进入容器

1
docker exec -it mysql /bin/bash

格式:docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

  • CONTAINER 可以是容器Id或者容器名字;

  • /bin/bash是进入容器后执行的命令,有些容器可能不存在/bin/bash

导出镜像文件

1
docker save -o rook-ceph-093.tar rook/ceph:v0.9.3

格式:docker save [OPTIONS] IMAGE [IMAGE...]

如果同时导出多个镜像到一个文件里,后面的镜像列表用空格来分隔。

导入镜像文件

1
docker load -i /root/rook-ceph-093.tar

格式:docker load [OPTIONS]

当不方便使用自建的镜像仓库来传输镜像时,可以用镜像的导出导入功能来移动镜像。

查看容器资源消耗

1
2
3
4
5
6
docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
63ebc0e88e5a happy_volhard 0.02% 35.3MiB / 15.46GiB 0.22% 11.5MB / 19MB 13.3MB / 45MB 22
7ed66f584a25 redis 0.08% 7.82MiB / 15.46GiB 0.05% 304kB / 274kB 5.59MB / 0B 3
500eed21d914 dockercompose_mqbroker_1 2.98% 752.3MiB / 15.46GiB 4.75% 0B / 0B 3.07GB / 19GB 204
ddd919a5bf52 dockercompose_mqnamesrv_1 0.21% 329.2MiB / 15.46GiB 2.08% 0B / 0B 60.3MB / 16.4kB 41

查看Docker占用磁盘

1
2
3
4
5
6
7
docker system df

TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 29 28 8.575GB 1.873GB (21%)
Containers 29 17 7.545GB 410.8MB (5%)
Local Volumes 303 23 6.339GB 4.138GB (65%)
Build Cache 0 0 0B 0B

可以查看镜像文件大小、容器大小、本地存储使用的磁盘大小。

清理Docker镜像和容器

1
docker system prune -a

此命令会清除所有未运行的容器和清理所有未使用的镜像。