image frame

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

GC—全流程

1、minorGC 和 Full GC 区别

     新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

     老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

2、minorGC 过程详解

     在初始阶段,新创建的对象被分配到 Eden 区,Survivor 的两块空间都为空。

图一

     当Eden区满了的时候,minor garbage 被触发。

图二

     经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁。

图三

     在下一次的 Minor GC 中,Eden 区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到 Survivor区。当 Eden 和 s0区空间满了,S0 的所有的数据都被复制到S1,需要注意的是,在上次 Minor GC 过程中移动到S0 中的两个对象在复制到 S1 后其年龄要加1。此时 Eden 区 S0 区被清空,所有存活的数据都复制到了 S1 区,并且 S1 区存在着年龄不一样的对象,过程如下图所示:

图四

     再下一次 Minor GC 则重复这个过程,这一次 Survivor 的两个区对换,存活的对象被复制到 S0,存活的对象年龄加1,Eden 区和另一个 Survivor 区被清空。

图五

     再经过几次 Minor GC 之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold 默认是15),就会被从年轻代 Promotion 到老年代。

图六

     随着 MinorGC 一次又一次的进行,不断会有新的对象被 Promote 到老年代。

图七

     上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩(标记-清除或者标记-整理)。从上面的过程可以看出,Eden 区是连续的空间,且 Survivor 总有一个为空。经过一次 GC 和复制,一个 Survivor 中保存着当前还活着的对象,而 Eden 区和另一个 Survivor 区的内容都不再需要了,可以直接清空,到下一次 GC 时,两个 Survivor 的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将 Eden 区和一个 Survivor 中仍然存活的对象拷贝到另一个 Survivor 中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。

     老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生 Minor GC 时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次 Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行 MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC( 这代表着如果设置-
XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

3、整体描述

     大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1(“To”),并且对象的年龄还会加 1( Eden 区 -> Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From ,新的 From 就是上次 GC 前的 To。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 To 区被填满,To 区被填满之后,会将所有对象移动到年老代中。

4、GC 触发条件

     Minor GC 触发条件:Eden 区满时。Full GC 触发条件:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行;
  • 老年代空间不足;
  • 方法去空间不足;
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  • 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

5、对象进入老年代的四种情况

     假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代。

图八

     大对象直接进入老年代:假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代。

图九

     长期存活的对象将进入老年代:此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳,那么将被移动到To区,并且把对象的年龄设置为1,对象没"熬过"一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中。

     动态对象年龄判定:还有一种情况,如果在From空间中,相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认)。

图十

6、空间分配担保

     在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlerPromotionFailure 这个参数设置的值( true 或 flase )是否允许担保失败(如果这个值为 true,代表着 JVM 说,我允许在这种条件下尝试执行 Minor GC,出了事我负责)。

     如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlerPromotionFailure 为 false,那么这次 Minor GC 将升级为 Full GC。如果老年代最大可用的连续空间大于历次晋升到老年代对象的平均大小,那么 HandlerPromotionFailure 为 true 的情况下,可以尝试进行一次 Minor GC,但这是有风险的,如果本次将要晋升到老年代的对象很多,那么 Minor GC 还是无法执行,此时还得改为 Full GC。

     注意:JDK 6Update 24 之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大 小就会进行 Minor GC,否则进行 Full GC。

GC—基础知识

     JVM 的 GC 是指垃圾回收,主要是对堆内存的回收。本文将介绍 JVM 中一次完整的 GC 流程是怎样
的,首先抛出第一个问题,什么样的对象会是 JVM 回收的目标?

1、可达性分析算法(GC Roots)

     有一种引用计数法,可以用来判断对象被引用的次数,如果引用次数为0,则代表可以被回收。这种实现方式比较简单,但对于循环引用的情况束手无策,所以 Java 采用了可达性分析算法。即判断某个对象是否与 GC Roots 的这类对象之间的路径可达,若不可达,则有可能成为回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。在 Java 中,可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

2、JVM中的堆结构

     JVM 中的堆可划分为两大部分,新生代和老年代,大小比例为1:2,如下:

JVM 分代比例

     其中,新生代分为 Eden 区和 Survivor 区, Survivor 幸存者区又分为大小相等的两块 from 和 to
区。这便是 JVM 中堆的结构和各部分默认的比例,当然这些比例都可通过对应 JVM 参数来调整。完整的 JMM 如下:

JVM 内存模型全景

2.1、为何新生代要分为三个区

     这里需要介绍新生代的垃圾回收算法——复制算法。该算法的核心是将可用内存按容量划分为大小
相等的两块,每次回收周期只用其中一块,当这一块的内存用完,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间清理掉。

     优点:不必考虑内存碎片问题;效率高。

     缺点:可用容量减少为原来的一半,比较浪费。

     最优设置:根据权威数据分析,90%的对象都是朝生夕死的,所以采用10%的空间用作交换区,因为交换区必须要有等量的两个,所以采用复制算法中新生代中三个区默认分配比例为8:1:1。

2.2、新生代对象的分配和回收

     基本上新的对象优先在 Eden 区分配;

     当 Eden 区没有足够空间时,会发起一次 Minor GC;

     Minor GC 回收新生代采用复制回收算法的改进版本。即:
from 区和 to 区的两个交换区,这两个区只有一个区有数据。采用8:1:1的默认分配比例(-XX:SurvivorRatio默认为8,代表 Eden 区与 Survivor 区的大小比例)

2.3、老年代对象的分配和回收

     老年代的对象一般来自于新生代中的长期存活对象。这里有一概念叫做年龄阈值,每个对象定义了年龄计数器,经
过一次 Minor GC (在交换区)后年龄加1,对象年龄达到15次后将会晋升到老年代,老年代空间不够时进行 Full GC。当然这个参数仍是可以通过 JVM 参数(-XX:MaxTenuringThreshold,默认15)来调整。

     大对象直接进入老年代。即超过 Eden 区空间,或超过一个参数值(-
XX:PretenureSizeThreshold=30m,无默认值)。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

     对象提前晋升到老年代(组团)。动态年龄判定:如果在 Survivor 区中相同年龄所有对象大小总和
大于 Survivor 区大小的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无须等到自己
的晋升年龄。

3、JVM完整的GC流程

     对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。

     新生代GC:Minor GC;老年代GC:Full GC,比 Minor GC 慢10倍,JVM 会“stop the world”,严重
影响性能。

     总结:内存区域不够用了,就会引发GC。Minor GC 避免不了,Full GC 尽量避免。
处理方式:保存堆栈快照日志、分析内存泄漏、调整内存设置控制垃圾回收频率,选择合适的垃圾
回收器等。

三段式国密

三段式国密通讯

     三段式国密的问题

     1、三段式国密一次一密,每次通信都需要进行 SM2 运算,SM2 运算是对性能的极大损耗;

     2、签名不能代替摘要,签名说的是不可抵赖性,摘要说的是防篡改性;

     3、私钥放在了客户端。

兜底在系统设计中的重要性

系统调用关系

     生产环境存在如上图的调用关系,应用 AB 在往下游发起 RPC 调用时,都先去向全局的序列服务申请序列号,序列号用来作为全局流水号或者追踪号。TDSQL 架构如图:

TDSQL示意图

     三周前,TDSQL 做了序列服务数据库的机房迁移,从甲机房迁移到了乙机房,当晚验证没有问题。但是三周过后,硬件负载 F5 出了问题,导致 B 获取序列失败。由于 B 在实现上强依赖于序列服务,序列服务,整个 B 系统的所有交易都走不通了。但是 A 仍然可以正常发起 RPC 调用,因为 A 在实现上,如果调用失败,兜底的方案是会在本地生成序列。

     由于 A 应用依赖于 B 应用,而 B 缺少类似于 A 的兜底策略,导致整体对客的服务全都不可用。

     事实上,序列号只不过是用来追踪程序的一个功能,A 应用的情况,就算不走全局序列服务来生成,本地生成一个序列号也没有任何影响,只不过序列服务那边溯源的时候查不到这笔交易。但是总好过 B 应用,在序列服务出问题的时候,连核心的交易都走不通要好。

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

请我喝杯咖啡吧~

支付宝
微信