image frame

一些JVM平台上的并发知识

1、处理器缓存设计

       处理器缓存通过减少写入延迟、批量刷新数据、限定缓存的局部可见性,并合理安排内存操作顺序,有效提升了处理器的运行效率和内存总线的利用率。

处理器缓存设计

2、重排序

       源代码从编译器到最终执行的指令序列的优化过程,通过编译器重排、指令级并行重排以及内存系统重排,逐步优化指令执行顺序,以提升处理器的执行效率。

重排序

3、happens-before

       “happens-before” 确保操作结果的可见性而不一定要求实际执行顺序。主要规则包括:程序顺序规则、监视器锁规则、volatile 变量规则和传递性规则,用于保证多线程环境下操作的有序性和一致性。

happens-before

4、数据依赖性

       数据依赖性在单个处理器和单线程操作中的重要性,强调了写后读、写后写和读后写等依赖类型。如果这些操作顺序被交换,执行结果会发生变化。然而,不同处理器或线程之间的依赖性不在编译器和处理器的考虑范围内。

数据依赖性

5、as-if-serial

       “as-if-serial” 语义,保证在单线程环境下操作的执行结果不受重排序的影响,无需担心内存可见性。代码执行顺序可以在不改变最终结果的前提下进行优化,例如图中 A、B、C 操作的顺序在编译器和处理器的重排序下仍保持一致的执行结果。

as-if-serial

6、程序顺序规则

       程序顺序规则中的传递性和可见性要求:如果操作 A happens-before B,且 B happens-before C,则 A happens-before C,但这并不强制要求执行顺序。只要B能够看到 A 的结果,即使 B 先于 A 执行也是合法的,JVM 允许这种非严格顺序的优化。

程序顺序规则

7、内存屏障

       下图介绍了内存屏障的不同类型及其作用,包括 LoadLoad、StoreStore、LoadStore 和 StoreLoad 屏障。每种屏障用于确保不同类型的内存操作顺序,从而在多处理器环境下保持数据一致性。其中,StoreLoad 屏障最强大且开销最高,用于在执行后续指令前确保当前处理器的所有写操作已刷新到内存。

内存屏障

8、重排序导致的问题

       在多线程环境下,线程 A 的写操作 a=1 和 flag=true 可能被重排序,线程 B 在读取 flag 后立即使用变量 a,但此时 a 可能尚未更新,导致不正确的结果。这种重排序问题会破坏程序的预期执行顺序,可能引发逻辑错误。

重排序导致的问题

9、数据竞争问题

       在多线程环境中,通过同步机制(如synchronized、volatile等)可以确保数据一致性和操作的原子性,避免数据竞争问题,但Java内存模型(JMM)不能对顺序一致性和原子性同时提供保证。

数据竞争问题

关于调优

     任何的调优手段,都是补救措施。调优再厉害,都只是补救。

     写出正规的代码,规范的研发流程,及时有效的运维与预警机制,胜过一切调优。

呃,多谢 ChatGPT 认可

用于实验的千万级数据库记录的创建脚本

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
CREATE DATABASE `order_center` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE order_center;

-- 创建 users 表
CREATE TABLE users
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_name_email (name, email), -- 索引覆盖场景
INDEX idx_created_at (created_at) -- 针对时间的查询
);

-- 创建 orders 表
CREATE TABLE orders
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
INDEX idx_user_id (user_id), -- 最左前缀法则示范
INDEX idx_order_date_status (order_date, status) -- 查询组合索引
);

-- 创建 order_items 表
CREATE TABLE order_items
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10, 2) NOT NULL,
INDEX idx_order_product (order_id, product_id), -- 联表查询时提升性能
INDEX idx_price_quantity (price, quantity) -- 数量和价格查询优化
);

DELIMITER //
CREATE PROCEDURE GenerateData()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE user_id INT;
DECLARE order_id INT;
DECLARE num_orders INT;
DECLARE j INT;

WHILE i < 10000000 DO
-- 插入 user 数据
INSERT INTO users (name, email)
VALUES (CONCAT('user', FLOOR(RAND() * 1000000)), CONCAT(FLOOR(RAND() * 1000000), '@example.com'));

SET user_id = LAST_INSERT_ID();

-- 随机生成1到5个订单
SET num_orders = FLOOR(RAND() * 5) + 1;

SET j = 0;
WHILE j < num_orders DO
-- 插入 order 数据
INSERT INTO orders (user_id, status, total_amount)
VALUES (user_id, 'completed', RAND() * 1000);

SET order_id = LAST_INSERT_ID();

-- 插入 order_items 数据
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (order_id,
FLOOR(RAND() * 10000), -- 随机生成 product_id
FLOOR(RAND() * 100), -- 随机生成数量
RAND() * 100); -- 随机生成价格

SET j = j + 1;
END WHILE;

SET i = i + 1;
END WHILE;
END
//
DELIMITER ;

-- 调用存储过程以生成数据
CALL GenerateData();

设计一个监控模块

1、监控模块设计

       要在开发平台层设计一个监控模块,提供平台级监控能力。几乎所有的监控系统都基于 CS 模式设计,监控模块也基于 CS 模式。但是开发平台层不需要自己完全实现完整的 Server 端和 Client 端。就这样设计了第一版监控模块。

第一版监控模块

  • server 端几乎无需自己实现,Client 只需要能以主动推送或者暴露接口的形式将监控数据提交至 server 端并展示即可。

  • client 端在逻辑上划分三层:数据源、采集层、接口层;

  • 数据层是指被采集对象的整体,监控数据源自于这里。监控数据可以是业务数据,可以是开发平台层数据,例如 rpc 调用信息,还可以直接是应用环境三方的,任何可以被应用程序获取的信息,例如:Spring Boot Actuator。

  • 采集层是真正需要开发平台层实现的逻辑,需要采集的各项指标待定.

  • 接口层负责将数据交付给展示层。

       UML 设计如下:

UML

2、为何 Server 端不采用 Spring Boot Admin

       Spring Boot Actuator + Spring Boot Admin 是 Spring 官方标配。但是 Spring Boot Admin 生产部署作为监控平台的很少见,并且 Spring Boot Admin 由于在数据交互上没有统一的数据规范,展示层必须针对特定数据进行前端开发(重新开发spring-boot-admin-server-ui )。目前 Spring Boot Admin 能展示的数据主要是 JVM 的进程信息以及依赖于 Spring 环境的相关信息(环境配置、beans、web)。

Spring Boot Admin

3、各层的理想组合

       micrometer 的数据结构与普罗米修斯数据结构设计上一致,生产环境的监控平台 prometheus + grafana 几乎是最常见的,Sping Boot Actuator 对 prometheus 已经天然支持(spring-boot-actuator-autoconfigure @org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration)。因此将 Sping Boot Actuator 的 prometheus 端点作为接口层,将 prometheus + grafana 作为 Server 端,micrometer 作为采集层的采集工具,是最理想的组合。可行性验证效果展示:

prometheus + grafana

4、定制化监控系统的引入

       因为引入了一体化监控平台 XXX,开发平台要考虑将 XXX 作为 Server 端。Xxx 将监控信息抽象为 XxxBean 对象,并通过派生 XxxBean 对象得到三种监控场景。设计方案如图所示:

Xxx 设计

       除了支持上报 XxxBean 监控对象,Xxx 还支持基于日志的交易监控:

Xxx 设计

       其中需要特别说明:1)业务系统接入 Xxx 上报监控的能力,必须在业务逻辑中组装 XxxBean 模型(可用性模型、事件模型、调度模型);2)基于日志的交易监控,必须调整应用系统的日志输出格式。

5、基于度量指标的通用监控设计思路

       Xxx 从使用场景出发设计了监控平台,开发平台需要考虑更加通用开放的监控标准,基于开发的监控标准设计监控能力,再进一步将通用的监控能力运用于具体场景。

       在信息技术中,监控指的是对系统、网络、应用程序、基础设施等的持续观察、测量和分析,以确保其正常运行、性能优化和安全性。让我们回到监控这件事情本身来考虑,能否基于 V1 方案中 prometheus 协议进行设计,形成通用开发的监控能力,以应对类似于一体化监控平台等诸多展示层的接入需求。

       假设系统生成有度量指指标:demo_cpu_usage、demo_mem_usage、demo_transfer_amount。分别记作 a、b、c。

1
2
3
4
5
6
7
8
9
10
11
# prometheus 协议数据
<metric_name>{<label_name>=<label_value>, ...} <value> <timestamp>

a:
demo_cpu_usage{target="192.168.1.2"} 81 1694116786000

b:
demo_mem_uasge{target="192.168.1.2"} 81 1694116786000

c:
demo_transfer_amount{vip_user="000000123"} 20000000 1694116786000

       另外设:

       bool = g(x):x.value 必须小于 80,否则 g 函数返回 false。

       float = f(x):当 x.tag.vip_user in vip_user_list,x.value 如果大于一百万,f 则返回 x.value。

       以上 g 函数和 f 函数目的是分别通用的程序监控场景和业务场景,可以根据实际需要定义出更加复杂灵活的函数法则,函数法则施加于若干监控指标,从而得到一个具体的监控场景。以 g 函数和 f 函数为例:

       场景一:当 cpu 或者 mem 内存低于 80 的时候,触发可用性监控,伪代码如下:

1
2
3
if g(a) || g(b) 
# 组装可用性 XxxBean 模型
endif

       场景二:当 VIP 用户转账金额超过 100 万时,触发事件监控,伪代码如下:

1
2
3
if  f(c) > 1000000
# 组装事件 XxxBean 模型
endif

       基于以上具体案例的分析可以看出,监控模型的上报场景,完全是可以从一些自定义的监控指标中得出的,因此基于 prometheus 协议监控度量指标的封装,可以当做一种通用监控能力。基于这种能力,也可以很方便对交易进行监控,不仅是基于 Xxx 模型地监控,还是基于 prometheus 协议的监控,都有能力做到,此不赘述。

6、监控优化方案

       对 V1 方案优化如下:

       1)采集层基于 prometheus 协议设计出度量指标公共服务 MetricsService;

       2)在接口层引入度量指标汇聚层,用于汇聚各项指标数据,根据展示层实际的接入方式进行转换。

监控优化方案

7、知识补充一:prometheus

       Prometheus 是一个开源的监控系统,可以看做是一个时序数据库,它使用一种特定的数据格式来收集和存储时间序列数据。数据类型:

  • Counter(计数器):一个单调递增的度量值,通常用于记录事件发生的次数。例如, HTTP 请求的计数。

  • Gauge(量表):一个可以任意增减的度量值,通常用于记录某一时刻的状态或值。例如,当前的内存使用量、CPU 使用率等。

  • Histogram(直方图):用于测量样本的分布情况,比如请求延迟。它将数据样本划分到不同的桶(buckets)中,并记录每个桶中的样本数量。

  • Summary(摘要):类似于直方图,但会额外提供请求的总数和总和,通常用于计算百分位数等聚合度量。

       数据格式:

1
<metric_name>{<label_name>=<label_value>, ...} <value> <timestamp>
  • metric_name: 度量名称,必须符合正则表达式 [a-zA-Z_:][a-zA-Z0-9_:]*,例如:http_requests_total。

  • {<label_name>=<label_value>, …}: 可选的标签部分,用来区分不同的时间序列,例如:{method=“GET”, endpoint="/api"}。

  • value: 度量的值,是一个浮点数。

  • timestamp: 可选的 UNIX 时间戳,以毫秒为单位。

       数据样例:

1
2
3
4
# HELP http_requests_total Total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="GET", handler="/home"} 1027 1694116786000
http_requests_total{method="POST", handler="/api"} 521 1694116786000

8、知识补充二:采集工具比较

采集工具比较

设计模式之模板方法模式和策略模式

     这篇博客的设计模式应用案例来自于这个仓库,完整代码可以参考本仓库:

     微信大模型接入

仓库主页

     简单介绍一下这个仓库:1)实现了微信接入;2)实现了大模型接入;3)将微信的提问发给大模型,将大模型的回答返回给微信(欢迎给个 star)。

1、背景分析

     1)我们预期接入的大模型肯定不止一种,现在市面上除了最牛的 GhatGPT,国内也陆续退出了豆包、文心一言、星火大模型等。为了获得良好的扩展性,我们可以基于策略模式对模型通讯模块进行封装,将不同的模型定义为一种通讯策略,程序中可以通过参数指定不同的模型工作;

     2)通讯的过程无非就是三个阶段:通信前参数组装、进行通讯、通讯完成处理结果,这里显然是可以通过模板方法进行封装的。结合策略模式,我们可以规定将来接入新模型的时候,有统一的代码组织形式和良好的扩展接口。

2、知识补充

     我们这里不再对设计模式本身进行专门的讲解。

     策略模式

     模板方法模式

3、代码分析

     如下图所示,DefaultHandler 是程序写给微信接入模块的一个回调(实现了消息处理接口 IMsgHandlerFace),当微信接入模块接收到微信消息,便会触发此回调,执行用户预定义行为。也就是在这个地方,我们接入了大模型,并将模型的问答结果返回给微信。

模型接入的地方

     一下三行代码的作用分别是:获取聊天模型的策略上下文(请参考上文中菜鸟教程——策略模式),返回的策略上下文会包含具体的执行策略,执行策略的选择是程序参数定义的。

1
2
3
4
5
6
// 聊天模型策略
StrategyContext context = getStrategyContext();
// 构建聊天请求
ChatRequest request = buildChatRequest(msg);
// 进行聊天
ChatResponse response = context.executeStrategy(request);

程序参数指定执行策略

3、策略模式实现

     首先,定义策略接口,策略接口中的 exec 方法是所有具体的策略类都需要实现的。

策略接口

     定义策略上下文,上下文是统一交给用户侧的一个”句柄”(可以参考上文 DefaultHandler 的代码,用户侧通过获取策略上下文来执行具体的策略实现的),用于持有具体的策略实现。

策略上下文

     我们这里的策略实现类稍有不同,没有直接实现 exec 方法,也没有直接实现 IStrategy 接口。这涉及到另外一个设计模式——模板方法模式。

策略实现类

4、 模板方法模式

     抽象策略类定义了一个算法模板方法,这个模板方法规定了 exec 方法执行时发生的三个算法步骤:postChatRequest (执行前的参数处理)、doExec (执行通讯请求)、postChatResponse(通讯完成之后的响应报文处理)。但是我并没有对这三个步骤进行实现,他们都是抽象的,延迟到了将来的策略实现类去实现。

抽象策略类

     所有的策略实现类,实现的不是策略接口 IStrategy,而是继承抽象策略类 AbstractStrategy,也不再去实现 exec 方法,而是实现抽象类中算法模板规定的三个算法步骤。

实现抽象策略类中规定的三个既定步骤

5、 其他一些想法

     面向对象语言最重要的三个基本特性:封装、多态、继承,是软件工程七大原则开闭原则,里氏代换原则,依赖倒转原则,接口隔离原则,迪米特原则和合成复用原则的重要支撑点,设计模式是一种如何最大化发挥三个基本特性,从而能够遵循七大原则的一种编码层级上的技术,这也是 Java、C++ 等完美支持 OOP 编程范式语言,在面临庞大复杂工程时,总能将源代码组织得很好的原因之一吧。从这个角度出发,Go 语言在多态、继承的表现力上不足,也许是因为我还比较缺乏 Go 开发的实战经验,所以我不确定在面临复杂的建模场景的时候,Go 语言的编程方式还能不能进行有效表达。

实现一个健康检查模块

     参考 SpringBoot 的健康检查设计,在平台层实现一套健康检查机制。挺优雅。

1、健康检查器接口类设计

1
2
3
4
5
6
7
8
/**
* IHealthChecker
*
*/
public interface IHealthChecker {

Health doHealth();
}

2、健康检查器抽象实现类

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
/**
* AbstractHealthChecker
*
* 有一些公共逻辑需要定义在这一层抽象层,所有的自检实现都必须具备,例如开关、
*
*/
public abstract class AbstractHealthChecker implements IHealthChecker {

/**
* 模板方法确保 Health 初始状态为 up
*
* @return
*/
public Health doHealth() {
Health.Builder builder = Health.up();
return health(builder);
}

public abstract Health health(Health.Builder builder);

// 自检开关
private boolean toggle = true;

// 严格模式,严格模式下的健康检查器状态,将影响最终健康状态
private boolean strict = true;

// 省略了 getter/setter
}

3、健康状态的枚举

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
/**
* 检查状态
*
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public final class Status {

public static final Status UNKNOWN = new Status("UNKNOWN");

public static final Status UP = new Status("UP");


public static final Status DOWN = new Status("DOWN");


public static final Status OUT_OF_SERVICE = new Status("OUT_OF_SERVICE");

private final String code;

private final String description;

public Status(String code) {
this(code, "");
}


public Status(String code, String description) {
this.code = code;
this.description = description;
}

@JsonProperty("status")
public String getCode() {
return this.code;
}


@JsonInclude(JsonInclude.Include.NON_EMPTY)
public String getDescription() {
return this.description;
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Status) {
return ObjectUtils.nullSafeEquals(this.code, ((Status) obj).code);
}
return false;
}

@Override
public int hashCode() {
return this.code.hashCode();
}

@Override
public String toString() {
return this.code;
}

}

4、健康信息

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
/**
* Health
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public final class Health {

private final Status status;

private final Map<String, Object> details;

private Health(Builder builder) {
this.status = builder.status;
this.details = Collections.unmodifiableMap(builder.details);
}

@JsonUnwrapped
public Status getStatus() {
return this.status;
}

public Map<String, Object> getDetails() {
return this.details;
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Health) {
Health other = (Health) obj;
return this.status.equals(other.status) && this.details.equals(other.details);
}
return false;
}

@Override
public int hashCode() {
int hashCode = this.status.hashCode();
return 13 * hashCode + this.details.hashCode();
}

@Override
public String toString() {
return getStatus() + " " + getDetails();
}

public static Builder unknown() {
return status(Status.UNKNOWN);
}


public static Builder up() {
return status(Status.UP);
}

public static Builder down(Exception ex) {
return down().withException(ex);
}


public static Builder down() {
return status(Status.DOWN);
}


public static Builder outOfService() {
return status(Status.OUT_OF_SERVICE);
}


public static Builder status(String statusCode) {
return status(new Status(statusCode));
}

public static Builder status(Status status) {
return new Builder(status);
}

public static class Builder {

private Status status;

private final Map<String, Object> details;

public Builder() {
this.status = Status.UNKNOWN;
this.details = new LinkedHashMap<>();
}

public Builder(Status status) {
this.status = status;
this.details = new LinkedHashMap<>();
}

public Builder(Status status, Map<String, ?> details) {
this.status = status;
this.details = new LinkedHashMap<>(details);
}

public Builder withException(Throwable ex) {
return withDetail("error", ex.getClass().getName() + ": " + ex.getMessage());
}


public Builder withDetail(String key, Object value) {
this.details.put(key, value);
return this;
}

public Builder withDetails(Map<String, ?> details) {
this.details.putAll(details);
return this;
}

public Builder unknown() {
return status(Status.UNKNOWN);
}

public Builder up() {
return status(Status.UP);
}

public Builder down(Throwable ex) {
return down().withException(ex);
}

public Builder down() {
return status(Status.DOWN);
}


public Builder outOfService() {
return status(Status.OUT_OF_SERVICE);
}

public Builder status(String statusCode) {
return status(new Status(statusCode));
}

public Builder status(Status status) {
this.status = status;
return this;
}

public Health build() {
return new Health(this);
}

}

}

5、健康检查器注册中心

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
/**
* HealthCheckerRegistry
*
*/
@Component
public class HealthCheckerRegistry {

/**
* 同步锁
*/
private final Object monitor = new Object();

/**
* 健康检查器集合
*/
private Map<String, AbstractHealthChecker> checkers = new LinkedHashMap<>();

@Autowired
public void setCheckers(Map<String, AbstractHealthChecker> checkers) {
this.checkers = checkers;
}


/**
* 注册检查者
*
* @param name
* @param checker
*/
public void register(String name, AbstractHealthChecker checker) {
synchronized (this.monitor) {
AbstractHealthChecker existing = this.checkers.putIfAbsent(name, checker);
if (existing != null) {
throw new AresRuntimeException("健康检查器{}已存在,请排查", name);
}
}

}

/**
* 卸载检查器
*
* @param name
* @return
*/
public AbstractHealthChecker unregister(String name) {
Assert.notNull(name, "检查器名称不能为空");
synchronized (this.monitor) {
return this.checkers.remove(name);
}
}

/**
* 获取所有的检查器
*
* @return
*/
public Map<String, AbstractHealthChecker> getAllCheckers() {
synchronized (this.monitor) {
return Collections.unmodifiableMap(new LinkedHashMap<>(this.checkers));
}
}
}

6、健康检查服务

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
/**
* HealthCheckService 收集并组合所有的健康状态信息
*
*/
@Component
public class HealthCheckService {

@Resource
private HealthCheckerRegistry checkerRegistry;

/**
* 执行健康检查
*
* @return
*/
public Health doHealthCheck() {
return compositeHealth(checkerRegistry.getAllCheckers());
}

/**
* 聚合自检信息
*
* @param allCheckers
* @return
*/
public Health compositeHealth(Map<String, AbstractHealthChecker> allCheckers) {
Health.Builder builder = Health.up();
for (Map.Entry<String, AbstractHealthChecker> entry : allCheckers.entrySet()) {
String name = entry.getKey();
AbstractHealthChecker checker = entry.getValue();

// 如果当前检查器没有打开则跳过
if (!checker.isEnable()) {
continue;
}

// 执行自检逻辑
try {
Health health = checker.doHealth();
builder.withDetail(name, health);
if (health.getStatus() != Status.UP && checker.isStrict()) {
// 只要有一个健康检查器检查失败并且当前检查器处于严格模式,就设置为失败
builder.status(Status.DOWN);
}
} catch (Exception e) {
// 如果自检异常,则将该健康自检信息记录下来
builder.withDetail(name, Health.down(e).build());
// 如果检查器处于严格模式,DOWN之
if(checker.isStrict()){
// 并标记为 DOWN
builder.status(Status.DOWN);
}
}
}

return builder.build();
}

}

7、检查器实现案例

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
/**
* RedisHealthChecker
* <p>
* 缓存健康检查
*
*/
@Component
public class RedisHealthChecker extends AbstractHealthChecker {

@Value("${xxxx.cache.health.checker.enable:false}")
private boolean enable;

@Value("${xxxx.cache.health.checker.strict:true}")
private boolean strict;

// 数据缓存
@Resource
private RedisService cacheService;

private final static String PONG = "PONG";


@Override
public Health health(Health.Builder builder) {

// 数据缓存就绪性自检
if (null == cacheService) {
return builder.down().withDetail("redisHealthChecker", "cacheService not prepared").build();
}

RedisTemplate<String, Object> cacheTemplate = cacheService.getRedisTemplate();

if (null == cacheTemplate) {
return builder.down().withDetail("redisHealthChecker", "cacheTemplate not prepared").build();
}

RedisConnectionFactory cacheConnectionFactory = cacheTemplate.getConnectionFactory();
if (null == cacheConnectionFactory || cacheConnectionFactory.getConnection().isClosed()) {
return builder.down().withDetail("redisHealthChecker", "cache server is closed").build();
}

String cachePong = cacheConnectionFactory.getConnection().ping();

if (!StringUtil.isEmpty(cachePong) &&
PONG.equalsIgnoreCase(cachePong)) {
return builder.up().withDetail("redisHealthChecker", "session server is ready").build();
}

// 未知状态
return builder.up().withDetail("redisHealthChecker", "UNKNOWN").build();
}

@PostConstruct
public void init() {
// 检查开关
if (enable) {
enable();
} else {
disable();
}

// 严格模式
if (strict) {
enStrict();
} else {
deStrict();
}
}
}

8、Restful 方式接入

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
/**
*
*/
@RestController
public class HealthHandler {

// 聚合检查信息详情的服务
@Resource
private HealthCheckService healthCheckService;


ObjectMapper mapper = new ObjectMapper();

/**
* 存活性健康检查
*
* @return
*/
@RequestMapping(value = {"/liveness"}, produces = {"text/plain;charset=UTF-8"})
public String liveness() {
return "ok";
}

/**
* 就绪性健康检查
*
* @return
*/
@RequestMapping(value = {"/readiness"}, produces = {"text/plain;charset=UTF-8"})
public String readiness(HttpServletResponse response) {
Health health = healthCheckService.doHealthCheck();

// 如果就绪检查失败,则修改返回状态码
if(health.getStatus() == null || health.getStatus() != Status.UP){
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}

String jsonStr;
try {
jsonStr = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(health);
} catch (JsonProcessingException e) {
throw new AresRuntimeException("健康检查模块执行失败");
}

return jsonStr;
}
}

机房服务器网口

     服务器的网络端口分为两种:电口和光口,电口就是双绞线的水晶头接口,光口是光纤接口。光口的速率远远高于电口,因此实际生产机房中,光口一般用于数据通信,电口一般用于管理通信。

     下面视频中,绿色线是光纤线,白色线是双绞线:

Kubernetes中原生的Sidecar容器

1、Sidecar容器的概念

     sidecar 容器的概念在 Kubernetes 早期就已经存在。多年来,sidecar 模式在应用程序中变得越来越普遍,使用场景也变得更加多样化。其中比较经典的就是 Istio 通过 sidecar 容器实现了服务网格的功能,Envoy 作为 sidecar 容器与应用程序容器一起运行,负责处理所有的网络流量,实现了服务之间的安全通信、流量管理、监控等功能。

sidecar

2、当前Sidecar容器的问题

     当前的 Kubernetes 原语可以很好地处理这种模式,但是对于几个用例来说,它们还存在着不足,并且迫使应用程序采用奇怪的变通方法。

2.1、问题 1:使用 Sidecar 容器的 Job

     假设你有一个 Job,其中包含两个容器:一个是用于执行作业的主容器,另一个只是完成辅助任务的 sidecar 容器。这个辅助容器可以是用于服务网格、收集指标或者日志的服务等等。当 Job 中的主容器完成任务退出时,由于 sidecar 容器还在运行,最终会导致 Pod 无法正常终止。此外,对于 restartPolicy:Never 的 Job,当 sidecar 容器因为 OOM 被杀死时,它不会被重新启动,如果 sidecar 容器为其他容器提供网络或者安全通信,这可能会导致 Pod 无法使用。

     下面我们可以通过一个简单的例子来演示这个问题。下面是一个 Job 的 YAML 文件,其中包含两个容器:一个是主容器 main-container-1,另一个是 sidecar 容器 sidecar-container-1。main-container-1 容器在完成一些任务后会正常退出,而 sidecar-container-1 容器会则一直运行。

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
apiVersion: batch/v1
kind: Job
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container is starting..."
for i in $(seq 1 5); do
echo "main container is doing some task: $i/5"
sleep 3
done
echo "main container completed tasks and exited"
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container is starting..."
while true; do
echo "sidecar container is collecting logs..."
sleep 1
done
restartPolicy: OnFailure

     执行以下命令应用上面的 Job 资源。

1
kubectl apply -f 1-job-cannot-complete.yaml

     在本文的实验中,我们会在一个 Pod 中同时运行多个容器,为了方便观察日志,我们可以使用 stern 这个开源工具。 stern 允许我们同时查看多个 Pod 中多个容器的日志,并且以不同颜色进行显示,方便我们直观地进行区分。

     执行以下命令查看 myapp Pod 中所有容器的日志:

1
2
# --diff-container 参数会为每个容器的日志添加不同的颜色,默认情况下,只会为每个 Pod 的日志添加不同的颜色
stern myapp --diff-container

     从日志中可以看到,main-container-1 容器完成任务后正常退出,而 sidecar-container-1 还在持续运行,最终导致这个 Job 无法正常结束。

Pod日志

     如果我们提前在另一个窗口执行 kubectl get pod -w 命令,可以观察到 Pod 的状态变化如下:

1
2
3
4
5
6
NAME          READY   STATUS    RESTARTS   AGE
myapp-fdpb7 0/2 Pending 0 0s
myapp-fdpb7 0/2 Pending 0 0s
myapp-fdpb7 0/2 ContainerCreating 0 0s
myapp-fdpb7 2/2 Running 0 2s
myapp-fdpb7 1/2 NotReady 0 17s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/1-job-cannot-complete-status.yaml 文件:

     1.Pod 创建后还未被调度,Pod 的 Phase 为 Pending。

1
2
3
status:
phase: Pending
qosClass: BestEffort

     2.Pod 已经被调度到 Node 上,但是容器还未被创建,Pod 的 Phase 还是 Pending。

1
2
3
4
5
6
7
8
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: PodScheduled # Pod 已经被调度到 Node 上
phase: Pending
qosClass: BestEffort

     3.正在等待创建容器,Pod 还没有处于 Ready 状态,Pod 的 Phase 仍为 Pending。

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
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
message: 'containers with unready status: [main-container-1 sidecar-container-1]'
reason: ContainersNotReady
status: "False" # Pod 还没有处于 Ready 状态
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
message: 'containers with unready status: [main-container-1 sidecar-container-1]'
reason: ContainersNotReady
status: "False"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: PodScheduled
containerStatuses:
- image: busybox:1.35
imageID: ""
lastState: {}
name: main-container-1
ready: false
restartCount: 0
started: false
state:
waiting: # 等待容器创建完成
reason: ContainerCreating
- image: busybox:1.35
imageID: ""
lastState: {}
name: sidecar-container-1
ready: false
restartCount: 0
started: false
state:
waiting: # 等待容器创建完成
reason: ContainerCreating
hostIP: 172.19.0.2
phase: Pending
qosClass: BestEffort
startTime: "2024-05-28T13:05:25Z"

     4.所有容器在运行中,Pod 的 Phase 变为 Running。

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
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:27Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:27Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://af182325a9bb106697dc56f7ff25e96d6dd22d45eb134990d9c4820349c11232
image: docker.io/library/busybox:1.35
imageID: docker.io/library/busybox@sha256:469d6089bc898ead80a47dab258a127ffdae15342eab860be3be9ed2acdee33b
lastState: {}
name: main-container-1
ready: true
restartCount: 0
started: true
state:
running: # 主容器在运行中
startedAt: "2024-05-28T13:05:26Z"
- containerID: containerd://fb24805ffe5ee1fddb64a728ee8853299f9c093b2722b77d54808d9821b90b0e
image: docker.io/library/busybox:1.35
imageID: docker.io/library/busybox@sha256:469d6089bc898ead80a47dab258a127ffdae15342eab860be3be9ed2acdee33b
lastState: {}
name: sidecar-container-1
ready: true
restartCount: 0
started: true
state:
running: # sidecar 容器在运行中
startedAt: "2024-05-28T13:05:26Z"
hostIP: 172.19.0.2
phase: Running
podIP: 10.244.0.7
podIPs:
- ip: 10.244.0.7
qosClass: BestEffort
startTime: "2024-05-28T13:05:25Z"

     5.main-container-1 容器完成任务后正常退出,而 sidecar-container-1 还在持续运行,最终这个 Job 无法正常结束。

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
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:42Z"
message: 'containers with unready status: [main-container-1]'
reason: ContainersNotReady
status: "False"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:42Z"
message: 'containers with unready status: [main-container-1]'
reason: ContainersNotReady
status: "False"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-05-28T13:05:25Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://af182325a9bb106697dc56f7ff25e96d6dd22d45eb134990d9c4820349c11232
image: docker.io/library/busybox:1.35
imageID: docker.io/library/busybox@sha256:469d6089bc898ead80a47dab258a127ffdae15342eab860be3be9ed2acdee33b
lastState: {}
name: main-container-1
ready: false
restartCount: 0
started: false
state:
terminated: # 主容器已经正常退出
containerID: containerd://af182325a9bb106697dc56f7ff25e96d6dd22d45eb134990d9c4820349c11232
exitCode: 0
finishedAt: "2024-05-28T13:05:41Z"
reason: Completed
startedAt: "2024-05-28T13:05:26Z"
- containerID: containerd://fb24805ffe5ee1fddb64a728ee8853299f9c093b2722b77d54808d9821b90b0e
image: docker.io/library/busybox:1.35
imageID: docker.io/library/busybox@sha256:469d6089bc898ead80a47dab258a127ffdae15342eab860be3be9ed2acdee33b
lastState: {}
name: sidecar-container-1
ready: true
restartCount: 0
started: true
state:
running: # sidecar 容器还在继续运行
startedAt: "2024-05-28T13:05:26Z"
hostIP: 172.19.0.2
phase: Running
podIP: 10.244.0.7
podIPs:
- ip: 10.244.0.7
qosClass: BestEffort
startTime: "2024-05-28T13:05:25Z"

     下面这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程

     这里有几个地方需要解释一下,在我们观察容器和 Pod 的状态时,Kubernetes 提供了一些字段来帮助我们理解 Pod 的状态:

     Pod phase: Pod phase 是对 Pod 在其生命周期中所处位置的一个高层次的概括,包括 Pending、Running、Succeeded、Failed 和 Unknown。

  • Pending:Pod 已被 Kubernetes 系统接受,但有一个或者多个容器尚未被创建。此阶段包括等待 Pod 被调度的时间和通过网络下载镜像的时间。

  • Running:Pod 中的所有容器都已经被创建,并且至少有一个容器正在运行,或者正在启动或者重启。

  • Succeeded:Pod 中的所有容器都已经成功终止,并且不会再重启。

  • Failed:Pod 中的所有容器都已经终止,但至少有一个容器是因为失败而终止。

  • Unknown:Pod 的状态无法被获取,通常是由于与 Pod 应该运行的节点通信失败导致的。

     Container states: 容器的状态,包括 Waiting、Running 和 Terminated。我在上图右边部分的方框中用不同的颜色标记了这三种 Container states,另外在括号内部还对相同 Container states 的不同情况作了区分。

  • Waiting:容器正在等待某些条件满足,例如正在拉取镜像,或者应用 Secret 数据。

  • Running:容器正在运行中。

  • Terminated:容器已经终止,可能是正常结束或者因为某些原因失败。如果你使用 kubectl describe pod 或者 kubectl get pod 命令来查询包含 Terminated 状态的容器的 Pod 时, 你会看到容器进入此状态的原因、退出代码以及容器执行期间的起止时间。

     Pod Status: 在执行 kubectl get pod 命令时返回的 Pod 状态,该字段是 Pod 内所有容器状态的一个聚合,具体的源代码参见 printPod 函数.有以下几个常见的状态:

  • Init:N/M:Pod 包含 M 个 init 容器,其中 N 个已经运行完成。

  • Init:Error:Pod 中的某个 init 容器执行失败。

  • Init:CrashLoopBackOff:Pod 中的某个 init 容器多次失败。

  • Pending:Pod 尚未开始创建 init 容器。

  • PodInitializing:Pod 已经执行完所有 init 容器,在等待创建主容器。

  • ContainerCreating:当 Pod 中不包含 init 容器时,在等待创建主容器时会显示这个状态。

  • Running:Pod 中的所有容器都在运行中。

     Pod Ready: 以 Ready 的容器数量 / 所有容器的数量的形式展示。

     测试完毕后,执行以下命令删除这个 Job。

1
kubectl delete -f 1-job-cannot-complete.yaml

2.2、问题 2:日志转发和指标收集的 Sidecar 容器

     日志转发和指标收集的 sidecar 容器应该在主应用容器之前启动,以便能够完整地收集日志和指标。如果 sidecar 容器在主应用容器之后启动,而主应用容器在启动时崩溃,则可能会导致日志丢失(取决于日志是否通过共享卷或者通过 Localhost 网络来进行收集)。 另外,在 Pod 停止时,如果 sidecar 容器先于其他容器退出,也会导致日志丢失的问题。

2.3、问题 3:服务网格

     服务网格的 sidecar 容器需要在其他容器之前运行并准备就绪,以确保流量能够正确地通过服务网格。在关闭时,如果服务网格容器先于其他容器终止,也可能会导致流量异常。

2.4、问题 4:配置/密钥

     当前,一些 Pod 使用 init 容器来获取配置/密钥,然后使用 sidecar 容器来持续监视变更并将更新推送给主容器,这需要两个独立的容器来实现。也许可以考虑使用同一个 sidecar 容器来处理这两种情况,以简化实现。

3、什么是原生 Sidecar 容器

     Kubernetes 1.28 引入了一种新型容器 - sidecar 容器。Kubernetes 将 sidecar 容器作为 init 容器的一个特例来实现,在 Pod 启动后,sidecar 容器仍将保持运行状态。

     具体的实现方式是在 init 容器中添加了一个新的 restartPolicy 字段, 该字段在 SidecarContainers 特性门控启用时可用(该特性自 Kubernetes v1.29 起默认启用)。该字段是可选的,如果对其设置,则唯一有效的值为 Always。设置了这个字段以后,init 容器就成为了 sidecar 容器,它们会在 Pod 的整个生命周期内持续运行,而不是像 init 容器那样在成功执行完任务后就退出。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
spec:
initContainers:
- name: secret-fetch
image: secret-fetch:1.0
- name: network-proxy
image: network-proxy:1.0
restartPolicy: Always
containers:
...

     接下来让我们通过一些实验来深入理解 sidecar 容器的特性。

4、环境准备

     在本文中,我们将使用 Kind 来创建一个 Kubernetes 集群。Kind 是一个用于在 Docker 容器中运行本地 Kubernetes 集群的工具,它使用 Docker 容器作为节点,并在这些节点上运行 Kubernetes 的相关组件。

     在 Kind 配置文件中启用 SidecarContainers 特性门控,如下所示:

1
2
3
4
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
SidecarContainers: true

     然后执行以下命令创建一个 Kubernetes 集群,集群的版本为 v1.28.0。

1
2
3
kind create cluster --name=sidecar-demo-cluster \
--image kindest/node:v1.28.0 \
--config sidecar-feature-enable.yaml

5、Init 容器和主容器

     首先我们先来观察一下只有 init 容器和普通主容器的情况。在下面的示例中,我们定义了 3 个 init 容器和 2 个主容器。

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
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
- name: init-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 2 is starting..."
echo "init container 2 is doing some tasks..."
sleep 10
echo "init container 2 completed tasks and exited"
- name: init-container-3
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 3 is starting..."
echo "init container 3 is doing some tasks..."
sleep 10
echo "init container 3 completed task and exited"
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done

     执行以下命令应用上面的 Pod 资源。

1
kubectl apply -f 2-init-and-main-containers.yaml

     执行 stern 命令查看 Pod 的日志,可以看到 3 个 init 容器是按照定义的顺序依次启动的。每个 init 容器在执行完任务后都会正常退出,而下一个 init 容器则会等到前一个 init 容器退出后才会开始启动。

     等到所有的 init 容器退出后,两个主容器才会开始启动,它们之间的启动并没有先后顺序。

Pod日志2

     如果我们提前在另一个窗口执行 kubectl get pod -w 命令,可以观察到 Pod 的状态变化。

1
2
3
4
5
6
7
8
9
10
11
NAME    READY   STATUS    RESTARTS   AGE
myapp 0/2 Pending 0 0s
myapp 0/2 Pending 0 0s
myapp 0/2 Init:0/3 0 0s
myapp 0/2 Init:0/3 0 1s
myapp 0/2 Init:1/3 0 12s
myapp 0/2 Init:1/3 0 13s
myapp 0/2 Init:2/3 0 23s
myapp 0/2 Init:2/3 0 24s
myapp 0/2 PodInitializing 0 34s
myapp 2/2 Running 0 35s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/2-init-and-main-containers-status.yaml 文件:

     1.Pod 创建后还未被调度。

     2.Pod 已经被调度到 Node 上,但是容器还未被创建。

     3.等待创建第 1 个 init 容器。

     4.第 1 个 init 容器正在运行。

     5.第 1 个 init 容器正常退出,等待创建第 2 个 init 容器。

     6.第 2 个 init 容器正在运行。

     7.第 2 个 init 容器正常退出,等待创建第 3 个 init 容器。

     8.第 3 个 init 容器正在运行。

     9.所有的 init 容器都已经正常退出,等待创建 main-container-1 和 main-container-2 两个主容器。

     10.两个主容器都正在运行。

     下面这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程2

     测试完毕后,执行以下命令删除这个 Pod。

1
2
3
4
# 执行 --force 参数立即删除 Pod,方便我们快速进行实验
# 不等待 terminationGracePeriodSeconds(默认是 30s)时间就让 Kubelet 强制发送 SIGKILL 信号,因为我们当前的容器并不会处理 SIGTERM 信号,这将在第 9 小节中会进一步说明
# 如果这里不使用 --force 参数,容器将等待 30s 后容器才会退出,
kubectl delete -f 2-init-and-main-containers.yaml --force

6、Init 容器、Sidecar 容器和主容器

     接下来,我们在 Pod 中引入 sidecar 容器,sidecar 容器是设置了 restartPolicy: Always 的 init 容器。在下面的示例中,我们定义了 3 个 init 容器、2 个 sidecar 容器和 2 个主容器。

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
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 1 is starting..."
while true; do
echo "sidecar container 1 is doing some tasks..."
sleep 3
done
restartPolicy: Always # sidecar 容器是设置了 restartPolicy: Always 的 init 容器
- name: sidecar-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 2 is starting..."
while true; do
echo "sidecar container 2 is doing some tasks..."
sleep 3
done
restartPolicy: Always
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done

     执行以下命令应用上面的 Pod 资源。

1
kubectl apply -f 3-init-and-sidecar-and-main-containers.yaml

     从日志中我们可以清晰地看到整个 Pod 的启动过程。首先,init-container-1 作为第一个 init 容器启动,在成功执行完任务后正常退出。

     接下来,sidecar-container-1 作为第一个 sidecar 容器开始启动,接着是 sidecar-container-2 容器 。与 init 容器类似,sidecar 容器也是按照定义的顺序逐个启动的。不同的是,sidecar 不会像 init 容器那样在完成任务后退出。这确保了 sidecar 容器可以在 Pod 的整个生命周期内提供辅助功能。

     当两个 sidecar 容器启动并成功运行后,两个主容器才会开始启动。两个主容器之间并没有固定的启动顺序,它们几乎是同时启动的。

     最后我们可以看到 sidecar 容器和主容器会一直运行,交替输出日志。

Pod日志3

     如果我们提前在另一个窗口执行 kubectl get pod -w 命令,可以观察到 Pod 的状态变化。

1
2
3
4
5
6
7
8
9
NAME    READY   STATUS    RESTARTS   AGE
myapp 0/4 Pending 0 0s
myapp 0/4 Pending 0 0s
myapp 0/4 Init:0/3 0 0s
myapp 0/4 Init:0/3 0 1s
myapp 0/4 Init:1/3 0 11s
myapp 1/4 Init:2/3 0 12s
myapp 2/4 PodInitializing 0 13s
myapp 4/4 Running 0 14s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/3-init-and-sidecar-and-main-containers-status.yaml 文件:

     1.Pod 创建后还未被调度。

     2.Pod 已经被调度到 Node 上,但是容器还未被创建。

     3.等待创建第 1 个 init 容器。

     4.第 1 个 init 容器正在运行。

     5.第 1 个 init 容器正常退出,等待创建第 1 个 sidecar 容器。

     6.第 1 个 sidecar 容器正在运行,等待创建第 2 个 sidecar 容器。

     7.第 2 个 sidecar 容器正在运行,等待创建 main-container-1 和 main-container-2 两个主容器。

     8.两个主容器都正在运行。

     注意 READY 字段显示的容器数量是 4(2个 sidecar 容器 + 2 个主容器),而不是 2。这是因为 sidecar 容器也被包含在内,而 init 容器并不会被计算在内,因为 init 容器执行完任务就退出了。

     下面这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程3

     测试完毕后,执行以下命令删除这个 Pod。

1
kubectl delete -f 3-init-and-sidecar-and-main-containers.yaml --force

7、Sidecar 容器的 RestartPolicy

     我们前面提到过,sidecar 容器是通过在原有的 init 容器中设置 restartPolicy: Always 来实现的。这意味着如果 sidecar 容器异常退出,kubelet 会自动重启它,从而确保 sidecar 容器在 Pod 的整个生命周期内都能一直运行。 在以下示例中,我们定义了两个 sidecar 容器,并分别设置它们运行 20 秒和 30 秒后退出。通过这种方式,我们可以观察 sidecar 容器的重启行为。

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
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args: # sidecar 容器完成任务后退出
- |
echo "sidecar container 1 is starting..."
echo "sidecar container 1 is doing some tasks..."
sleep 20
echo "sidecar container 1 completed tasks and exited"
restartPolicy: Always
- name: sidecar-container-2
image: busybox:1.35
command: ["sh", "-c"]
args: # sidecar 容器完成任务后退出
- |
echo "sidecar container 2 is starting..."
echo "sidecar container 2 is doing some tasks..."
sleep 30
echo "sidecar container 2 completed tasks and exited"
restartPolicy: Always
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done

     执行以下命令应用上面的 Pod 资源。

1
kubectl apply -f 4-sidecar-containers-restart.yaml

     通过日志我们可以看到 sidecar-container-1 和 sidecar-container-2 分别在 20 秒和 30 秒后退出,然后又被重新启动了。

Pod日志4

     如果我们提前在另一个窗口执行 kubectl get pod -w 命令,可以观察到 Pod 的状态变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
NAME    READY   STATUS    RESTARTS   AGE
myapp 0/4 Pending 0 0s
myapp 0/4 Pending 0 0s
myapp 0/4 Init:0/3 0 0s
myapp 0/4 Init:0/3 0 1s
myapp 0/4 Init:1/3 0 11s
myapp 1/4 Init:2/3 0 12s
myapp 2/4 PodInitializing 0 13s
myapp 4/4 Running 0 14s
myapp 3/4 Running 0 32s
myapp 4/4 Running 1 (2s ago) 33s
myapp 3/4 Running 1 (11s ago) 42s
myapp 4/4 Running 2 (1s ago) 43s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/4-sidecar-containers-restart-status.yaml 文件:

     Pod 创建后还未被调度。

     Pod 已经被调度到 Node 上,但是容器还未被创建。

     等待创建第 1 个 init 容器。

     第 1 个 init 容器正在运行。

     第 1 个 init 容器正常退出,等待创建第 1 个 sidecar 容器。

     第 1 个 sidecar 容器正在运行,等待创建第 2 个 sidecar 容器。

     第 2 个 sidecar 容器正在运行,等待创建 main-container-1 和 main-container-2 两个主容器。

     两个主容器都正在运行。

     sidecar-container-1 退出,kubelet 根据 restartPolicy: Always 自动重启 sidecar-container-1。

     sidecar-container-1 重启成功,所有容器都在运行状态。

     sidecar-container-2 退出,kubelet 根据 restartPolicy: Always 自动重启 sidecar-container-2。

     sidecar-container-2 重启成功,所有容器都在运行状态。

     下面这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程4

     测试完毕后,执行以下命令删除这个 Pod。

1
kubectl delete -f 4-sidecar-containers-restart.yaml --force

8、容器探针

     在上面的实验中,我们知道了主容器要等到 sidecar 容器运行以后才会开始启动。那么 sidecar 容器的探针是否会影响到主容器的启动呢?也就是说,主容器是否需要等到 sidecar 容器 Ready 后才能启动呢?让我们通过以下实验来寻找答案。

     sidecar 容器允许我们像主容器一样设置探针(startupProbe, readinessProbe, livenessProbe)来检查容器的健康状态。在下面的例子中,我们为两个 sidecar 容器分别添加了 readiness 探针,每个探针都会在容器启动后等待 30 秒才会通过。

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
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 1 is starting..."
while true; do
echo "sidecar container 1 is doing some tasks..."
sleep 3
done
restartPolicy: Always
readinessProbe: # sidecar 容器的 readiness 探针等待 30 秒通过
exec:
command:
- /bin/sh
- -c
- |
echo "readiness probe of sidecar container 1 is starting..." >> /proc/1/fd/1
sleep 30
echo "readiness probe of sidecar container 1 passed successfully" >> /proc/1/fd/1
timeoutSeconds: 999
- name: sidecar-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 2 is starting..."
while true; do
echo "sidecar container 2 is doing some tasks..."
sleep 3
done
restartPolicy: Always
readinessProbe: # sidecar 容器的 readiness 探针等待 30 秒通过
exec:
command:
- /bin/sh
- -c
- |
echo "readiness probe of sidecar container 2 is starting..." >> /proc/1/fd/1
sleep 30
echo "readiness probe of sidecar container 2 passed successfully" >> /proc/1/fd/1
timeoutSeconds: 999
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done

     通过观察容器日志可以发现,两个主容器在 sidecar 容器的 readiness 探针通过之前就已经启动并开始执行任务了。 这表明主容器的启动并不需要等待 sidecar 容器达到 Ready 状态,只要 sidecar 容器处于 Running 状态即可。

Pod日志5

     如果我们提前在另一个窗口执行 kubectl get pod -w 命令,可以观察到 Pod 的状态变化。

1
2
3
4
5
6
7
8
9
10
11
NAME    READY   STATUS    RESTARTS   AGE
myapp 0/4 Pending 0 0s
myapp 0/4 Pending 0 0s
myapp 0/4 Init:0/3 0 0s
myapp 0/4 Init:0/3 0 1s
myapp 0/4 Init:1/3 0 11s
myapp 0/4 Init:2/3 0 12s
myapp 0/4 PodInitializing 0 13s
myapp 2/4 Running 0 14s
myapp 3/4 Running 0 42s
myapp 4/4 Running 0 43s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/5-readiness-probe-status.yaml 文件:

     Pod 创建后还未被调度。

     Pod 已经被调度到 Node 上,但是容器还未创建。

     等待创建第 1 个 init 容器。

     第 1 个 init 容器正在运行。

     第 1 个 init 容器正常退出,等待创建第 1 个 sidecar 容器。

     第 1 个 sidecar 容器正在运行,等待创建第 2 个 sidecar 容器。注意,此时 sidecar-container-1 并未处于 ready 状态,因为就绪探针还在执行中,也就是说一旦容器处于 Running 状态,下一个容器就会开始启动。

     第 2 个 sidecar 容器正在运行但并未处于 Ready 状态,等待创建 main-container-1 和 main-container-2 两个主容器。

     两个主容器都正在运行,并且处于 Ready 状态。

     sidecar-container-1 通过就绪探针检查,进入 Ready 状态。

     sidecar-container-2 通过就绪探针检查,进入 Ready 状态。

     下面这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程5

测试完毕后,执行以下命令删除这个 Pod。

1
kubectl delete -f 5-readiness-probe.yaml --force

9、容器的停止顺序

     我们当前使用的 Kubernetes 版本是 1.28.0。让我们首先看看在这个版本中删除 Pod 时,sidecar 容器和主容器的停止顺序是怎样的。

     在下面的示例中,我们为主容器设置了 preStop hook。在容器停止之前,Kubelet 会先执行 preStop hook 中定义的命令,然后才会发送 SIGTERM 信号给容器。

     为了让容器接收到 SIGTERM 信号,我们在容器中使用了 trap 命令来捕获 SIGTERM 信号。另外,通常 hook 和 probe 的输出不会打印在 kubectl logs 中,为了方便我们通过日志来对容器的状态进行观察,这里使用了一种间接的方法:将输出重定向到 /proc/1/fd/1 文件中。

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
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
- name: sidecar-container-1
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "sidecar container 1 is starting..."
sh -c "
trap '
echo \"sidecar container 1 received SIGTERM\";
sleep 3;
echo \"sidecar container 1 stopped\";
exit 0
' TERM;

while true; do
echo \"sidecar container 1 is doing some tasks...\";
sleep 3;
done
"
restartPolicy: Always
- name: sidecar-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "sidecar container 2 is starting..."
sh -c "
trap '
echo \"sidecar container 2 received SIGTERM\";
sleep 3;
echo \"sidecar container 2 stopped\";
exit 0
' TERM;

while true; do
echo \"sidecar container 2 is doing some tasks...\";
sleep 3;
done
"
restartPolicy: Always
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
exec sh -c "
trap '
echo \"main container 1 received SIGTERM\";
sleep 10;
echo \"main container 1 stopped\";
exit 0
' TERM;

while true; do
echo \"main container 1 is doing some tasks...\";
sleep 3;
done
"
lifecycle:
preStop:
exec:
command: ["sh", "-c", "echo 'main container 1 preStop hook is running...' >> /proc/1/fd/1; sleep 5"]
- name: main-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 2 is starting..."
exec sh -c "
trap '
echo \"main container 2 received SIGTERM\";
sleep 10;
echo \"main container 2 stopped\";
exit 0
' TERM;

while true; do
echo \"main container 2 is doing some tasks...\";
sleep 3;
done
"
lifecycle:
preStop:
exec:
command: ["sh", "-c", "echo 'main container 2 preStop hook is running...' >> /proc/1/fd/1; sleep 5"]

     执行以下命令应用上面的 Pod 资源。

1
kubectl apply -f 6-prestop-hook.yaml

     通过查看日志,可以看到主容器首先执行了 preStop hook,然后接收到了 SIGTERM 信号,最后优雅地退出了。同时,sidecar 容器也接收到了 SIGTERM 信号,但是它在主容器停止之前就已经停止了。

Pod日志6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NAME    READY   STATUS    RESTARTS   AGE
myapp 0/4 Pending 0 0s
myapp 0/4 Pending 0 0s
myapp 0/4 Init:0/3 0 0s
myapp 0/4 Init:0/3 0 1s
myapp 0/4 Init:1/3 0 11s
myapp 1/4 Init:2/3 0 12s
myapp 2/4 PodInitializing 0 13s
myapp 4/4 Running 0 14s
myapp 4/4 Terminating 0 38s
myapp 0/4 Terminating 0 53s
myapp 0/4 Terminating 0 54s
myapp 0/4 Terminating 0 54s
myapp 0/4 Terminating 0 54s

     每行状态的解释如下,完整的 Pod 资源内容请查看 logs/6-prestop-hook-status.yaml 文件:

     Pod 创建后还未被调度。

     Pod 已经被调度到 Node 上,但是容器还未创建。

     等待创建第 1 个 init 容器。

     第 1 个 init 容器正在运行。

     第 1 个 init 容器正常退出,等待创建第 1 个 sidecar 容器。

     第 1 个 sidecar 容器正在运行,等待创建第 2 个 sidecar 容器。

     第 2 个 sidecar 容器正在运行,等待创建 main-container-1 和 main-container-2 两个主容器。

     两个主容器都正在运行。

     接收到 Pod 删除的请求,开始停止容器。

     sidecar 容器和主容器都已经停止。

     在后面的几行 Terminating 中, 容器的状态并不会发生变化。

     以下这张图展示了上面描述的 Pod 状态变化过程:

Pod状态变化过程6

     然而,由于 sidecar 容器可能会在主容器之前停止,这种情况仍然可能带来一些问题。例如,如果 sidecar 容器负责收集日志,那么可能会造成部分日志内容缺失。又比如,如果 sidecar 容器提供网络代理功能,那么它的提前退出可能会导致主容器的网络连接中断。

     为了解决这个问题,从 Kubernetes 1.29 版本开始,如果 Pod 中包含一个或多个 sidecar 容器,kubelet 将延迟向这些 sidecar 容器发送 SIGTERM 信号,直到最后一个主容器完全终止。sidecar 容器将按照它们在 Pod spec 中定义的相反顺序终止。这确保了 sidecar 容器继续为 Pod 中的其他容器提供服务,直到不再需要它们。

     下面让我们创建一个 Kubernetes 1.29 版本的集群,然后再次测试 sidecar 容器的停止顺序。由于在 1.29 版本中 SidecarContainers 这个特性已经成为 Beta 版本,因此默认是开启的。

1
2
kind create cluster --name=sidecar-demo-cluster-2 \
--image kindest/node:v1.29.0

     通过分析以下日志,我们可以清晰地看到容器退出的整个过程。首先两个主容器的 preStop hook 被执行,然后主容器接收到 Kubelet 发送的 SIGTERM 信号并正常退出。等到主容器完全退出后,sidecar-container-1 才会接收到 SIGTERM 信号并正常退出。最后,sidecar-container-2 接收到 SIGTERM 信号并正常退出。

Pod日志7

     下面这张图描述了 1.29 版本的 sidecar 容器的停止顺序:

Pod状态变化过程7

     测试完毕后,执行以下命令删除这个 Pod。

1
kubectl delete -f 6-prestop-hook.yaml --force

10、容器资源的 Request 和 Limit

     在 Kubernetes 中,我们可以为容器设置资源请求(request)和资源限制(limit)。当你为 Pod 中的容器指定了资源 request(请求)时,kube-scheduler 就根据该信息决定将 Pod 调度到哪个节点上。 当你为容器指定了资源 limit(限制) 时,kubelet 就可以确保运行的容器不会使用超出所设限制的资源。

     在评估节点是否有足够的资源来运行 Pod 时,kube-scheduler 会根据不同情况来计算 Pod 中容器资源的最大请求量。在没有引入 sidecar 容器的情况下,计算方式比较简单:资源的最大请求量是单个 init 容器的最大请求量与所有主容器请求量总和之间的最大值。

1
Max ( Max(initContainers), Sum(Containers) )

     有了 sidecar 容器之后,计算公式会变得复杂一些。可以简单地分为两种情况:

  • 1.所有 sidecar 容器都是在 init 容器之后启动的。对于这种情况,我们只需要把所有 sidecar 容器的资源请求量与主容器的资源请求量相加,然后与单个 init 容器的最大请求量进行比较即可。
1
Max ( Max(initContainers), Sum(Containers) + Sum(Sidecar Containers) )
  • 2.有一个或者多个 sidecar 容器是在 init 容器之前启动的。在这种情况下,当计算 init 容器的最大请求量时,我们需要把在该 init 容器之前启动的 sidecar 容器也考虑在内。在这里,我们使用 InitContainerUse(i) 来表示当启动 i 个 init 容器时,所需的最大资源请求量(等于该 init 容器的请求量 + 在该 init 容器之前启动的 sidecar 容器的请求量总和):
1
InitContainerUse(i) = Sum(sidecar containers with index < i) + InitContainer(i)

     最后将 InitContainerUse 与所有主容器以及在该 init 容器之后启动的 sidecar 容器的总和进行比较,取最大值。

1
Max ( Max( each InitContainerUse ) , Sum(Sidecar Containers) + Sum(Containers) )

     接下来我们用两个例子来验证上面的结论,当前 Kubernetes 集群中只有一个节点,使用 kubectl describe node 命令可以看到该节点总共有 10 个 CPU 可供分配,当前 CPU request 已经使用了 9% 的 CPU 资源,也就是说还有 9 个 完整的 CPU 可供分配。

节点CPU使用情况

     在下面的示例中,我们为 init 容器设置了 9 个 CPU 的 request,为 sidecar 容器和主容器也设置了总共 9 个 CPU 的 request,这样 Pod 刚刚好被允许调度到节点上。如果你尝试将 init 容器的 CPU request 设置为 10,或者将任一 sidecar 容器或主容器的 CPU request 增加 1,那么这个 Pod 都将无法被调度到节点上。

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
# 7-resource-requests.yaml
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
resources:
requests:
cpu: "9"
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 1 is starting..."
while true; do
echo "sidecar container 1 is doing some tasks..."
sleep 3
done
restartPolicy: Always
resources:
requests:
cpu: "1"
- name: sidecar-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 2 is starting..."
while true; do
echo "sidecar container 2 is doing some tasks..."
sleep 3
done
restartPolicy: Always
resources:
requests:
cpu: "1"
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
resources:
requests:
cpu: "3"
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done
resources:
requests:
cpu: "4"

     在第二个例子中,我们调整了容器的定义顺序,将 sidecar-container-1 移动到了 init-container-1 之前。虽然 Pod 的 CPU 资源请求总量没有改变,但你会发现这次 Pod 无法被调度到节点上了。这是因为在计算单个 init 容器的最大资源请求量时,sidecar-container-1 的资源请求量也被计入了。原本 init-container-1 的资源请求量是 9,现在加上了 sidecar-container-1 的资源请求量 1,最大的资源请求量就变为了 10,超过了节点的可用资源。

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
# 8-resource-requests-init-container-after-sidecar-container.yaml
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: sidecar-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 1 is starting..."
while true; do
echo "sidecar container 1 is doing some tasks..."
sleep 3
done
restartPolicy: Always
resources:
requests:
cpu: "1"
- name: init-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "init container 1 is starting..."
echo "init container 1 is doing some tasks..."
sleep 10
echo "init container 1 completed tasks and exited"
resources:
requests:
cpu: "9"
- name: sidecar-container-2
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "sidecar container 2 is starting..."
while true; do
echo "sidecar container 2 is doing some tasks..."
sleep 3
done
restartPolicy: Always
resources:
requests:
cpu: "1"
containers:
- name: main-container-1
image: busybox:1.35
command: ["sh", "-c"]
args:
- |
echo "main container 1 is starting..."
while true; do
echo "main container 1 is doing some tasks..."
sleep 3
done
resources:
requests:
cpu: "3"
- name: main-container-2
image: busybox:1.35
command: [ "sh", "-c" ]
args:
- |
echo "main container 2 is starting..."
while true; do
echo "main container 2 is doing some tasks..."
sleep 3
done
resources:
requests:
cpu: "4"

11、在 Istio 中使用 Sidecar 容器

     在没有原生 sidecar 容器的支持之前,Istio 采用了一种变通的方法来保证 sidecar 容器在主容器之前启动:通过为 Istio 的 sidecar 容器添加一个 postStart hook,该 hook 会阻塞其他容器的启动,直到 sidecar 代理完全运行为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
spec:
initContainers:
- name: istio-init
...
containers:
- name: istio-proxy
...
lifecycle:
postStart:
exec:
command:
- pilot-agent
- wait

     在 Istio 中可以通过将 holdApplicationUntilProxyStarts 设置为 true 来启用这个功能。

1
2
# 1.7 版本特性 https://github.com/istio/istio/pull/24737
istioctl install --set values.global.proxy.holdApplicationUntilProxyStarts=true

     在 Kubernetes 1.28 发布 SidecarContainers 的功能之后,现在我们直接可以在 Istio 中使用原生的 sidecar 容器了,只需将 pilot 的ENABLE_NATIVE_SIDECARS 环境变量设置为 true 即可。完整的教程请参见 Kubernetes Native Sidecars in Istio。

1
2
3
TAG=1.19.0-beta.0
curl -L https://github.com/istio/istio/releases/download/$TAG/istio-$TAG-linux-amd64.tar.gz | tar xz
./istioctl install --set values.pilot.env.ENABLE_NATIVE_SIDECARS=true -y

12、总结

     本文首先回顾了传统 sidecar 模式存在的问题,包括 Job 无法正常终止、日志和指标收集不完整以及服务网格流量异常等等。随后,我们介绍了 Kubernetes 1.28 版本中引入的原生 sidecar 容器功能,这一功能旨在解决传统 sidecar 模式的局限性。我们还深入探讨了 sidecar 容器的其他特性,包括 sidecar 容器的启动顺序、重启策略、容器探针、停止顺序等。最后,我们简要地介绍了如何在 Istio 中利用原生 sidecar 容器来提升服务网格的可靠性。

原文地址

一键部署单机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
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
#!/bin/bash
. /etc/init.d/functions

# IP地址,默认为本机第一块网卡IP地址(不包含lo网卡)
ip=
# 主机名称,默认为当前主机名称
hostName=master
# Docker版本
dockerVersion=20.10.6
# Kubernetes版本
k8sVersion=1.23.0
# Pod网段
podSubnet="10.244.0.0/16"
# Service网段
serviceSubnet="10.10.0.0/16"

networkCheck(){
ping -c 1 www.baidu.com > /dev/null 2>&1

if [ $? -eq 0 ];then
action "外网权限检查:"
else
action "外网权限检查:"
echo "此脚本需要访问外网权限才可成功执行,退出脚本"
exit 5
fi
}
cpuCheck(){
cpuCores=$(grep -c ^processor /proc/cpuinfo)
if [[ ${cpuCores} -lt 2 ]];then
action "CPU配置检查:" false
echo -e "\033[32m# 当前主机CPU ${cpuCores}核 < 2核,不满足安装K8s最低需求,请检查配置\033[0m"
exit 5
else
action "CPU配置检查:"
fi
}

menoryCheck(){
menorySize=$(free -m|grep -i mem|awk '{print $2}')

if [[ ${menorySize} -lt 1800 ]];then
action "内存配置检查:" false
echo -e "\033[32m# 当前主机内存 ${menorySize}M < 1800M(2G),不满足安装K8s最低需求,请检查配置\033[0m"
exit 5
else
action "内存配置检查:"
fi
}


stopFirewall(){
systemctl disable firewalld --now &>/dev/null
setenforce 0 &>/dev/null
sed -i.$(date +%F) -r 's/SELINUX=[ep].*/SELINUX=disabled/g' /etc/selinux/config

if (grep SELINUX=disabled /etc/selinux/config) &>/dev/null;then
action "关闭防火墙:"
else
action "关闭防火墙:" false
fi
}

hostName(){
if [[ -z ${ip} ]];then
ip=$(ip addr | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+'|egrep -v "127.0.0.1|172.17.0.1"|awk NR==1)
fi

if [[ -z ${hostName} ]];then
hostName="${HOSTNAME}"
fi

if ! (egrep -w "${ip} +${hostName}" /etc/hosts) &>/dev/null;then
hostnamectl set-hostname ${hostName}
echo "${ip} ${hostName}" >> /etc/hosts
fi

if (egrep -w "${ip} +${hostName}" /etc/hosts) &>/dev/null;then
action "添加本地域名解析:"
else
action "添加本地域名解析:" false
fi
}

timeSync(){
if ! (which ntpdate &>/dev/null);then
echo -e "\033[32m# ntpdate未安装,开始进行安装....\033[0m"
(yum -y install ntpdate) &>/dev/null;sleep 0.3
if (which ntpdate &>/dev/null);then
action "ntpdate安装成功:"
fi
fi

if (ntpdate ntp1.aliyun.com &>/dev/null);then
if ! (egrep "ntpdate +ntp1.aliyun.com" /var/spool/cron/root &>/dev/null);then
echo "0 1 * * * ntpdate ntp1.aliyun.com" >> /var/spool/cron/root
fi
action "时间同步:"
else
action "时间同步:" false
fi
}

swapOff(){
swapoff --all
sed -i -r '/swap/ s/^/#/' /etc/fstab

if [[ $(free | grep -i swap | awk '{print $2}') -eq 0 ]]; then
action "关闭交换分区:"
else
action "关闭交换分区:" false
fi
}

addKernelArg(){
KernelArg=("net.bridge.bridge-nf-call-ip6tables" "net.bridge.bridge-nf-call-iptables" "net.ipv4.ip_forward")

# 判断内核参数是否存在,如果不存在则添加
for ((i=0;i<${#KernelArg[@]};i++))do
if [[ $(sysctl -n ${KernelArg[i]}) -ne 1 ]];then
echo "${KernelArg[i]} = 1" >> /etc/sysctl.d/kubernetes.conf
fi
done
modprobe br_netfilter &>/dev/null
sysctl -p /etc/sysctl.d/kubernetes.conf &>/dev/null

if [[ $(sysctl -n ${KernelArg[0]}) -eq 1 && $(sysctl -n ${KernelArg[1]}) -eq 1 && $(sysctl -n ${KernelArg[2]}) -eq 1 ]]; then
action "添加内核参数:"
else
action "添加内核参数:" false
fi

}

ipvs(){
if (command -v ipset &>/dev/null && command -v ipvsadm &>/dev/null);then
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
EOF
chmod +x /etc/sysconfig/modules/ipvs.modules
/etc/sysconfig/modules/ipvs.modules
else
echo -e "\033[32m# ipvs未安装,开始进行安装....\033[0m"
yum -y install ipset ipvsadm &>/dev/null
if (command -v ipset &>/dev/null && command -v ipvsadm &>/dev/null);then
action "ipvs安装成功:"
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
EOF
chmod +x /etc/sysconfig/modules/ipvs.modules
/etc/sysconfig/modules/ipvs.modules

fi
fi
modprobe br_netfilter &>/dev/null

if (lsmod | grep -q -e ip_vs -e nf_conntrack_ipv4)&>/dev/null; then
action "启用ipvs模块:"
else
action "启用ipvs模块:" false
fi
}
dockerInstall(){
if ! (command -v docker &>/dev/null);then
echo -e "\033[32m# Docker未安装,开始进行安装....\033[0m"
(curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo) &>/dev/null
(wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo) &>/dev/null
(yum install -y yum-utils) &>/dev/null
(yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo) &>/dev/null
(yum install docker-ce-${dockerVersion} docker-ce-cli-${dockerVersion} -y) &>/dev/null
if (command -v docker &>/dev/null);then
action "Docker安装成功:"
else
action "Docker安装成功:" false
fi
fi


mkdir /etc/docker &>/dev/null
if [[ -f /etc/docker/daemon.json ]];then
mv /etc/docker/daemon.json{,.$(date +%F)}
fi

cat <<EOF > /etc/docker/daemon.json
{
"registry-mirrors": ["https://aoewjvel.mirror.aliyuncs.com"],
"exec-opts": ["native.cgroupdriver=systemd"]
}
EOF
(systemctl enable docker --now) &>/dev/null

if [[ -f /etc/docker/daemon.json ]];then
action "Docker镜像加速源:"
else
action "Docker镜像加速源:"
fi
}

k8sInstall(){
k8scommand=("kubeadm" "kubelet" "kubectl")

if [[ -f /etc/yum.repos.d/kubernetes.repo ]];then
mv /etc/yum.repos.d/kubernetes.repo{,.$(date +%F)}
fi

cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=0
EOF

echo -e "\033[32m# 正在安装K8S,请耐心等待......\033[0m"
(yum -y install --setopt=obsoletes=0 kubeadm-${k8sVersion} kubelet-${k8sVersion} kubectl-${k8sVersion}) &>/dev/null
systemctl enable kubelet.service --now &>/dev/null

for ((i=0;i<${#k8scommand[@]};i++))do
if (command -v ${k8scommand[i]} &>/dev/null);then
action "安装${k8scommand[i]}组件:"
else
action "安装${k8scommand[i]}组件:" false
fi
done
}

k8sInit(){
# 通过hosts文件获取IP地址
if [[ -z ${ip} ]];then
ip=$(grep ${HOSTNAME} /etc/hosts|awk '{print $1}'| awk NR==1)
fi

if [[ -f /root/kubeadm-config.yaml ]];then
mv /root/kubeadm-config.yaml{,.$(date +%F)}
fi

cat >> /root/kubeadm-config.yaml << EOF
apiVersion: kubeadm.k8s.io/v1beta3
bootstrapTokens:
- groups:
- system:bootstrappers:kubeadm:default-node-token
token: abcdef.0123456789abcdef
ttl: 24h0m0s
usages:
- signing
- authentication
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: ${ip}
bindPort: 6443
nodeRegistration:
imagePullPolicy: IfNotPresent
name: ${hostName}
taints: null
---
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta3
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.aliyuncs.com/google_containers
kind: ClusterConfiguration
kubernetesVersion: ${k8sVersion}
networking:
dnsDomain: cluster.local
serviceSubnet: ${serviceSubnet}
podSubnet: ${podSubnet}
scheduler: {}
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
EOF

if [[ -f /root/kubeadm-config.yaml ]];then
action "生成K8s初始化文件:"
else
action "生成K8s初始化文件:" false
fi
echo -e "\033[32m# K8s初始化中,时间可能较长,可以使用 tailf k8s_init.log 可追踪整个过程....\033[0m"
echo
kubeadm init --config /root/kubeadm-config.yaml --ignore-preflight-errors=SystemVerification &>k8s_init.log
if [[ $? -eq 0 ]];then
action "K8s初始化:"
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
else
action "K8s初始化:" false
exit 5
fi
}

k8sNetwork(){
(wget -O /root/calico.yaml https://gitee.com/qinziteng/K8S/raw/master/YMAL/calico.yaml) &>/dev/null
(kubectl apply -f /root/calico.yaml) &>/dev/null

if [[ $? -eq 0 ]];then
action "K8s网络插件:"
else
action "K8s网络插件:" false
fi
}

k8sTaint(){
(kubectl taint nodes --all node-role.kubernetes.io/master-) &>/dev/null

if [[ $? -eq 0 ]];then
action "设置Master节点可调度:"
else
action "设置Master节点可调度:" false
fi
}


confCheck(){
cpuCheck
menoryCheck
networkCheck
}

initEnv(){
clear;echo "一键部署单机版K8S脚本"
hostName
stopFirewall
swapOff
timeSync
ipvs
addKernelArg
dockerInstall
}

k8s(){
clear;k8sInstall
k8sInit
k8sNetwork
k8sTaint

echo
echo -e "\033[32m# K8s单机版部署完成,等待Pod全部运行成功即可使用 使用 kubectl get pods -n kube-system 关注Pod状态...\033[0m"
bash
}
confCheck
initEnv
k8s

  • Copyrights © 2017 - 2025 杨海波
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信