Kubernetes 控制平面组件:生命周期管理和服务发现

深入理解 Pod 的生命周期

如何优雅的管理 Pod 的完整生命周期

image-20240125094756635

Pod 的不同状态代表处于不同的生命周期

Pod 状态机

image-20240125094834163

当有组件出现故障时,就会出现 Unknown 的状态。

Pod Phase

kubectl get pod csi-cephfsplugin-provisioner-865b98f797-wx6mv -o yaml 可以查看 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
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
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-01-24T08:55:06Z"
status: "True"
type: Initialized # 不同的阶段
- lastProbeTime: null
lastTransitionTime: "2024-01-25T02:07:35Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-01-25T02:07:35Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-01-24T08:55:06Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://34bc31f39369e95e39337fe6602c17eefb27906831ce5f3c73f13eb0a13d1771
image: registry.aliyuncs.com/google_containers/csi-attacher:v4.4.2
imageID: registry.aliyuncs.com/google_containers/csi-attacher@sha256:c7dd111e0442ba1eb7b0b7a8a60b95a9324661bbdd6895a7f46f9d9c2ad2c51d
lastState:
terminated:
containerID: containerd://ab9d2aeb457b4fabd08ea4a617cdc3b9ba1673bb1667cbdd44fd028ec738fde3
exitCode: 255
finishedAt: "2024-01-25T02:06:22Z"
reason: Unknown
startedAt: "2024-01-24T08:55:07Z"
name: csi-attacher
ready: true
restartCount: 1
started: true
state:
running:
startedAt: "2024-01-25T02:07:27Z"
- containerID: containerd://983ac57c0388af981ba8a37e96d259a935d4914972f0de7c33f38305ce61b8f8
image: quay.io/cephcsi/cephcsi:v3.10.1
imageID: quay.io/cephcsi/cephcsi@sha256:5dd50ad6f3f9a1e8c8186fde0048ea241f056ca755acbeab42f5ebf723313e9c
lastState:
terminated:
containerID: containerd://de9aa9bd66a0b4415b76b3861043d8e9765d877b35844a135b9252e60a7c5526
exitCode: 255
finishedAt: "2024-01-25T02:06:23Z"
reason: Unknown
startedAt: "2024-01-24T08:55:07Z"
name: csi-cephfsplugin
ready: true
restartCount: 1
started: true
state:
running:
startedAt: "2024-01-25T02:07:34Z"
- containerID: containerd://671d4e3e2258d8cf16a34a89ee293d598c4386b0cf6c88a1edaf133ce7212208
image: registry.aliyuncs.com/google_containers/csi-provisioner:v3.6.2
imageID: registry.aliyuncs.com/google_containers/csi-provisioner@sha256:fc620614c85f144fddd58dc0bf191ce72f9975e11fea782ef89f5f57221704b6
lastState:
terminated:
containerID: containerd://ec257626b1ee829c6aeb55fdd7042757e13896c80e5cc709be1b897872428c21
exitCode: 255
finishedAt: "2024-01-25T02:06:23Z"
reason: Unknown
startedAt: "2024-01-24T08:55:07Z"
name: csi-provisioner
ready: true
restartCount: 1
started: true
state:
running:
startedAt: "2024-01-25T02:07:32Z"
- containerID: containerd://93e26473ee4527fffcc2eb2f97bf153d8d2514c7188659eb3848bfd797c3a78f
image: registry.aliyuncs.com/google_containers/csi-resizer:v1.9.2
imageID: registry.aliyuncs.com/google_containers/csi-resizer@sha256:84fa123b726b65ca2ba1b51cbc0d2d3dbfc82491a7df8b81a140cf1a30bca2f0
lastState:
terminated:
containerID: containerd://c365283eedc52062bc33db3afef81df466ceb69c05f4b30fa51fe08bfc1fe6e0
exitCode: 255
finishedAt: "2024-01-25T02:06:22Z"
reason: Unknown
startedAt: "2024-01-24T08:55:07Z"
name: csi-resizer
ready: true
restartCount: 1
started: true
state:
running:
startedAt: "2024-01-25T02:07:30Z"
- containerID: containerd://55bd4c06a8f00a05bc64802cf00b80a7eb9e3d3f3fb31813bef666c5fba7322f
image: registry.aliyuncs.com/google_containers/csi-snapshotter:v6.3.2
imageID: registry.aliyuncs.com/google_containers/csi-snapshotter@sha256:63615cae75b89752a0a665d8b7add7b3ad61734143254edcabbf99a39c095312
lastState:
terminated:
containerID: containerd://969fe0eff18ca32b8e103d2463dd9af84ac735bdcfef06c4f334bc0b96d1e8a1
exitCode: 255
finishedAt: "2024-01-25T02:06:22Z"
reason: Unknown
startedAt: "2024-01-24T08:55:07Z"
name: csi-snapshotter
ready: true
restartCount: 1
started: true
state:
running:
startedAt: "2024-01-25T02:07:29Z"
hostIP: 192.168.239.130 # Pod 当前状态
phase: Running
podIP: 10.244.57.230
podIPs:
- ip: 10.244.57.230
qosClass: BestEffort
startTime: "2024-01-24T08:55:06Z"

Pod Phase

  • Pending:待调度
  • Running:正常运行
  • Succeeded:正常运行结束
  • Failed:异常退出
  • Unknown:由于某个插件异常,例如当 csi 插件被卸载,原先使用这个 csi 插件的 Pod 就会出现 Unknown

kubectl get pod 显示的状态信息是由 podstatusconditionsphase 计算出来的

  • 查看 pod 细节

    1
    kubectl get pod $podname -o yaml 
  • 查看 pod 相关事件

    1
    kubectl describe pod $podname

Pod 状态计算细节

kubectl get pod 返回的状态 Pod Phase Conditions
Completed Succeeded
ContainerCreating Pending
CrashLoopBackOff Running Container exits(一般是由于业务异常)
CreateContainerConfigError Pending Configmap “test” not found
secret “my-secret” not found
ErrImagePull
ImagePullBackOff
Init:ImagePullBackOff
InvalidImageName
Pending Back-off pulling image
Error Failed restartPolicy: Never
container exits with Error(not 0)
Evicted Failed Message: ‘Usage of EmptyDir volume “myworkdir” exceeds the limit “40Gi”.’
reason: Evicted
Init: 0/1 Pending Init containers don’t exit
Init: CrashLoopBackOff/
Init: Error
Pending Init container crashed (exit with not 1)
OOMKilled Running Containers are OOMKilled
StartError Running Containers cannot be started
Unknown Running Node NotReady
OutOfCpu
OutOfMemory
Failed Scheduled, but it cannot pass kubelet admit

如何确保 Pod 的高可用

  • 避免容器进程被终止,避免 Pod 被驱逐
    • 设置合理的 resource.memory limits 防止容器进程被 OOMKill
    • 设置合理的 emptydir.sizeLimit 并且确保数据写入不超过 emptyDir 的限制,防止 Pod 被驱逐

Pod 的 QoS 分类

  • [Qos & Quota] GuaranteedBustable and BestEffort

从高到低:

  • Guaranteed:代表 Pod 调度到这个节点之后,可以确保这个节点上的资源可用,保障 limits 的资源量

    • Pod 的每个容器都设置了资源 CPU 和内存需求

    • Limitsrequests 的值完全一致

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      spec:
      containers:
      ...
      resources:
      limits:
      cpu: 700m
      memory: 200Mi
      requests:
      cpu: 700m
      memory: 200Mi
      ...
      qosClass: Guaranteed
  • Burstable:可以保证 requests 的,无法保证 limits 里面的

    • 至少一个容器设置了 CPU 或内存 request

    • Pod 的资源需求不符合 Guaranteed QoS 的条件,也就是 requestslimits 不一致

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      spec:
      containers:
      ...
      resources:
      limits:
      memory: 200Mi
      requests:
      memory: 100Mi
      ...
      qosClass: Burstable
  • BestEffort:不保证任何资源

    • Pod 中的所有容器都未指定 CPU 或内存资源需求 requests

      1
      2
      3
      4
      5
      6
      spec:
      containers:
      ...
      resources: {}
      ...
      qosClass: BestEffort

当计算节点检测到内存压力时,Kubernetes 会按 BestEffort -> Burstable -> Guaranteed 的顺序依次驱逐 Pod

1
2
~ kubectl get pod air-matrix-service-5b6f74d96f-jwl5k -o yaml | grep qosClass
qosClass: BestEffort

质量保证 Guaranteed,Burstable and BestEffort

定义 Guaranteed 类型的资源需求来保护你的重要 Pod

认真考量 Pod 需要的真实需求并设置 limitresource,这有利于将集群资源利用率控制在合理范围并减少 Pod 被驱逐的现象。

尽量避免将生产 Pod 设置为 BestEffort,但是对测试环境来讲,BestEffort Pod 能确保大多数应用不会因为资源不足而处于 Pending 状态。(使用压力测试来预估应用资源)

Burstable 适用于大多数场景。

基于 Taint 的 Evictions

NotReady Node

Node 上有 NotReadyTaint

1
2
3
4
5
6
7
8
9
10
11
spec:
podCIDR: 10.244.2.0/24
podCIDRs:
- 10.244.2.0/24
taints:
- effect: NoSchedule
key: node.kubernetes.io/unreachable
timeAdded: "2024-01-26T07:08:14Z"
- effect: NoExecute
key: node.kubernetes.io/unreachable
timeAdded: "2024-01-26T07:08:20Z"

Kubernetes 为 Pod 自动增加的 Tolerating

1
2
3
4
5
6
7
8
9
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300

可能出现的情况以及解决方案:

  • 节点临时不可达
    • 网络分区
    • kubelet, contained 不工作
    • 节点重启超过了 15 分钟
  • 增大 tolerationSeconds 以避免被驱逐(例如节点可能重启,默认的 15 分钟可能不够,导致 Pod 被驱逐)
    • 特别是依赖于本地存储状态的有状态应用

健康检查探针

  • 健康探针类型分为

    • livenessProbe

      • 探活,当检查失败时,意味着该应用进程已经无法正常提供服务,kubelet 会终止该进程并按照 restartPolicy 决定是否重启
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      apiVersion: v1
      kind: Pod
      metadata:
      name: liveness1
      spec:
      containers:
      - name: liveness
      image: centos
      args:
      - /bin/sh
      - -c
      - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
      # 30s 之后会删掉。Pod 会正常运行 30s,30s 后,探活失败,Pod 会重启(restartPolicy 一般是 Always)
      livenessProbe:
      exec: # 通过命令行判断就绪状态
      command:
      - cat
      - /tmp/healthy
      initialDelaySeconds: 10 # 10s 之后开始探针检测
      periodSeconds: 5
    • readinessProbe

      • 就绪状态检查,当检查失败时,意味着应用进程正在运行,但因为某些原因不能提供服务,Pod 状态会被标记为 NotReady
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      apiVersion: v1
      kind: Pod
      metadata:
      name: http-probe
      spec:
      containers:
      - name: http-probe
      image: nginx
      readinessProbe:
      httpGet:
      ### this probe will fail with 404 error code
      ### only httpcode between 200-400 is retreated as success
      path: /healthz
      port: 80
      initialDelaySeconds: 30
      periodSeconds: 5
      successThreshold: 2

      Pod 状态

      1
      2
      3
      4
      5
      6
      - lastProbeTime: null
      lastTransitionTime: "2024-01-25T08:41:01Z"
      message: 'containers with unready status: [http-probe]'
      reason: ContainersNotReady
      status: "False"
      type: ContainersReady
    • startupProbe

      • 在初始化阶段(Ready 之前)进行的健康检查,通常用来避免过于频繁的监测影响应用启动
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      apiVersion: v1
      kind: Pod
      metadata:
      name: initial-delay
      spec:
      containers:
      - name: initial-delay
      image: centos
      args:
      - /bin/sh
      - -c
      - touch /tmp/healthy; sleep 300; rm -rf /tmp/healthy; sleep 600
      startupProbe:
      exec:
      command:
      - cat
      - /tmp/healthy
      initialDelaySeconds: 30 # 等30s 之后判断文件是否存在
      periodSeconds: 5

    这三种探针一般搭配使用,让应用在准备时和启动时有足够的时间。

  • 探针方法包括

    • ExecAction:在容器内部运行指定命令,当返回码为 0 时,探测结果为成功
    • TCPSocketAction:由 kubelet 发起,通过 TCP 协议检查容器 IP 和端口,当端口可达时,探测结果为成功
    • HTTPGetAction:由 kubelet 发起,对 PodIP 和指定端口以及路径进行 HTTPGet 操作,当返回码为 200-400 之间时,探测结果为成功
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    readinessProbe:
    httpGet:
    path: /healthz
    port: 80
    httoHeaders:
    - name: X-Custom-Header
    value: Awesome
    initialDelaySeconds: 30
    periodSeconds: 5
    successThreshold: 2
    timeoutSeconds: 1

探针属性

parameters Description
initialDelaySeconds Defaults to 0 seconds. Minimum value is 0. 推迟时间
periodSeconds Defaults to 0 seconds. Minimum value is 1. 间隔
timeoutSeconds Defaults to 1 seconds. Minimum value is 1. 请求超时时间
successThreshold Defaults to 1. Must be 1 for liveness. Minimum value is 1. 判定成功的连续成功次数
failureThreshold Defaults to 3. Minimum value is 1. 判定失败的连续失败次数

ReadinessGates

  • Readiness 允许在 Kubernetes 自带的 Pod Conditions 之外引入自定义的就绪条件
  • 新引入的 readinessGates condition 需要为 True 状态后,加上内置的 Conditions,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
apiVersion: v1
kind: Pod
metadata:
labels:
app: readiness-gate
name: readiness-gate
spec:
readinessGates: # 扩展属性,可通过外部因素决定 Pod 是否准备就绪
- conditionType: "www.example.com/feature-1"
containers:
- name: readiness-gate
image: nginx
---
apiVersion: v1
kind: Service
metadata:
name: readiness-gate
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: readiness-gate
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
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-01-25T08:49:08Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-01-25T08:49:08Z"
message: corresponding condition of pod readiness gate "www.example.com/feature-1"
does not exist.
reason: ReadinessGatesNotReady
status: "False"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-01-25T08:51:20Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-01-25T08:49:08Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://55992e4c7c95329b1aaba1a698a774f3a2dd15d9254e4b845e8d11c1effc53ff
image: docker.io/library/nginx:latest
imageID: docker.io/library/nginx@sha256:4c0fdaa8b6341bfdeca5f18f7837462c80cff90527ee35ef185571e1c327beac
lastState: {}
name: readiness-gate
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2024-01-25T08:51:20Z"
hostIP: 192.168.239.129
phase: Running # 状态是 Runnig
podIP: 10.244.34.126
podIPs:
- ip: 10.244.34.126
qosClass: BestEffort
startTime: "2024-01-25T08:49:08Z"

虽然状态是 Running ,但是这个 Pod 不会接受和处理流量。一般用于服务需要其他外部服务可以正常访问才接受流量。

1
2
3
4
5
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
readiness-gate ClusterIP 10.1.70.89 <none> 80/TCP 4m4s
# curl 10.1.70.89:80
curl: (7) Failed to connect to 10.1.70.89 port 80: Connection refused

Post-start 和 Pre-Stop Hook

优雅启动

启动钩子:用于在服务启动后,提供服务前做预先处理。

1
kubectl explain pod.spec.containers.lifecycle.postStart

image-20240125111202525

postStart 结束之前,容器不会被标记为 running 状态。

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: poststart
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]

无法保证 postStart 脚本和容器的 Entrypoint (指的是 Docker 镜像中的 Entrypoint )哪个先执行。

优雅终止

在容器终止之前,执行的脚本。执行完了之后(不执行完不发送后面的信号),再给容器中的进程发送 SIGTERM,然后再发送 SIGKILL。(给容器先发送信号,让容器可以捕捉到并且自己处理一些终止逻辑,然后才终止。)

image-20240125180343393

Pre-stopSIGKILL 之间的间隔时间可以通过 terminationGracePeriodSeconds 设置。

1
2
spec:
terminationGracePeriodSeconds: 60

Grace period 默认值:terminationGracePeriodSeconds = 30

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: no-sigterm
spec:
terminationGracePeriodSeconds: 60
containers:
- name: no-sigterm
image: centos
command: ["/bin/sh"] # sh 会忽略 SIGTERM 信号,只会被 SIGKILL 终止
args: ["-c", "while true; do echo hello; sleep 10;done"]

杀死这个容器时,先执行 preStop,然后发送 SIGTERM 信号,由于 SIGTERM 会被忽略,所以会等待 60s,再发送 SIGKILL 信号。容器内进程忽略 SIGTERM 信号,收到 SIGKILL,进程终止。

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: prestop
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
preStop:
exec:
command: [ "/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done" ]

退出时,执行 nginx -s quit,也就是在优雅终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2024/01/25 09:55:24 [notice] 1#1: start worker process 31
2024/01/25 09:55:24 [notice] 1#1: start worker process 32
2024/01/25 09:58:14 [notice] 1#1: signal 3 (SIGQUIT) received from 39, shutting down
2024/01/25 09:58:14 [notice] 29#29: gracefully shutting down
2024/01/25 09:58:14 [notice] 29#29: exiting
2024/01/25 09:58:14 [notice] 32#32: gracefully shutting down
2024/01/25 09:58:14 [notice] 32#32: exiting
2024/01/25 09:58:14 [notice] 30#30: gracefully shutting down
2024/01/25 09:58:14 [notice] 31#31: gracefully shutting down
2024/01/25 09:58:14 [notice] 32#32: exit
2024/01/25 09:58:14 [notice] 31#31: exiting
2024/01/25 09:58:14 [notice] 29#29: exit
2024/01/25 09:58:14 [notice] 31#31: exit
2024/01/25 09:58:14 [notice] 30#30: exiting
2024/01/25 09:58:14 [notice] 30#30: exit
2024/01/25 09:58:14 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2024/01/25 09:58:14 [notice] 1#1: signal 17 (SIGCHLD) received from 32
2024/01/25 09:58:14 [notice] 1#1: worker process 31 exited with code 0
2024/01/25 09:58:14 [notice] 1#1: worker process 32 exited with code 0
2024/01/25 09:58:14 [notice] 1#1: signal 29 (SIGIO) received
2024/01/25 09:58:14 [notice] 1#1: signal 17 (SIGCHLD) received from 29
2024/01/25 09:58:14 [notice] 1#1: worker process 29 exited with code 0
2024/01/25 09:58:14 [notice] 1#1: worker process 30 exited with code 0
2024/01/25 09:58:14 [notice] 1#1: exit

只有当 Pod 被终止时,Kubernetes 才会执行 preStop 脚本,这意味着当 Pod 完成或容器退出时,preStop 脚本不会被执行。

terminationGracePeriodSeconds 的分解

terminationGracePeriodSeconds 包括两部分,一部分是 PreStopSIGTERM 阶段,另一部分是 SIGTERMSIGKILL 阶段,两个阶段加起来的时间是 terminationGracePeriodSeconds

image-20240125111641310

Terminating Pod 的误用

image-20240125111705147

image-20240125111711223

bash/sh 会忽略 SIGTERM 信号量,因此 kill -SIGTERM 会永远超时,若应用使用 bash/sh 作为 Entrypoint,则应避免过长的 grace period。例如上图,无论是 Docker 镜像里面使用 bash/sh 还是在 k8scommand 中调用 shell 脚本,都应注意,不然会一直等待 grace period 的时长之后,进程才会终止。

Time Taken by PreStop
(duration 1)
Time Taken by kill -SIGTERM
(duration 2)
Total Time
Timeout: grace period
< grace period
Time out: grace period - duration 1
cannot be killed
Grace period

Terminating Pod 的经验分享

terminationGracePeriodSeconds 默认时长 30s。

如果不关心 Pod 的终止时长,那么无需采取特殊措施。

如果希望快速终止应用进程,那么可采取如下方案:

  • preStop script 中主动退出进程
  • 在主容器进程中使用特定的初始化进程

优雅的初始化进程应该:

  • 正确处理系统信号量,将信号量转发给子进程
  • 在主进程退出之前,需要先等待并确保所有子进程退出
  • 监控并清理孤儿子进程

推荐使用:https://github.com/krallin/tini

在 Kubernetes 上部署应用的挑战

资源规划

  • 每个实例需要多少计算资源
    • CPU/GPU
    • Memory
  • 超售需求以及 QoS Class
  • 每个实例需要多少存储资源
    • 大小
    • 本地还是网络存储盘
    • 读写性能
    • Disk I/O
  • 网络需求
    • 整个应用总体 QPS 和带宽

存储带来的挑战

多容器之间共享存储,最简方案是 emptyDir

带来的挑战:

  • emptyDir 需要控制 size limit,否则无限扩张的应用会撑爆主机磁盘导致主机不可用,进而导致大规模集群故障
  • emptyDir size limit 生效以后,kubelet 会定期对容器目录执行 du 操作,会导致些许的性能影响(满了之后会驱逐这个 Pod,后面 kubelet 更新了,不使用 du 操作)
  • size limit 达到以后,Pod 会被驱逐,原 Pod 的日志配置等信息会消失

应用配置

传入方式,不会固定到容器中,否则修改配置需要重新打镜像

  • Environment Variables
  • Volume Mount

数据来源

  • ConfigMap

  • Secret

  • Downward API:也就是将 Pod 信息注入容器内部,例如上面两种,环境变量和卷挂载,也可以是一些其他信息

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    volumes:
    - name: kube-api-access-vhbrx
    projected:
    defaultMode: 420
    sources:
    - serviceAccountToken:
    expirationSeconds: 3607
    path: token
    - configMap:
    items:
    - key: ca.crt
    path: ca.crt
    name: kube-root-ca.crt
    - downwardAPI:
    items:
    - fieldRef:
    apiVersion: v1
    fieldPath: metadata.namespace
    path: namespace

数据应该如何保存

image-20240125112720034

存储卷类型 容器重启后是否存在 Pod 重建后数据是否存在 是否有大小控制 注意
emptyDir 如果被调度到其他节点,数据会消失
hostPath 需要额外权限控制
如果被调度到其他节点,也会消失
Local volume 无备份
Network volume
rootFS 不要写任何数据

注意控制日志写入速度,防止操作系统在配置日志回滚窗口期内把硬盘写满。

容器应用可能面临的进程中断

系统问题类型:

  • kubelet 升级

    影响:

    • 不重建容器,无影响
    • 重建容器,则 Pod 进程会被重启,服务会收到影响( kubelt 通过 hash 算法唯一确认一个容器,升级可能导致算法变更)

    建议:(升级之前查看 updatelog, 细化影响)

    • 冗余部署
    • 跨故障域部署
  • 主机操作系统升级;节点手工重启

    影响:

    • 节点重启
    • Pod 进程会被终止数分钟(10 分钟左右)

    建议:

    • 跨故障域部署
    • 增加 LivenessReadiness 探针
    • 设置合理的 NotReady nodeToleration 时间,在重启节点过程中不被重新调度
  • 节点下架,送修

    影响:

    • 节点会 drain(优雅的驱逐节点),重启或者从集群中删除
    • Pod 进程会被终止数分钟

    建议:

    • 跨故障域部署
    • 利用 Pod distruption budgetPod 最多有几个实例不为 Ready,应用层和基础架构层通过这个约束来,当 drain 节点的时候,会被阻拦) 避免节点被 drain 导致 Pod 被意外删除而影响业务
    • 利用 preStop script 做数据备份等操作
  • 节点长时间下线

    影响:

    • Pods will be down for about 10 minutes

    建议:

    • 跨故障域部署
    • 设置合理的 NotReady nodeToleration 时间
  • 节点崩溃

    影响:

    • Pod 进程会被终止 15 分钟左右

    建议:

    • 跨故障域部署

高可用部署方式

需要注意:

  • 多少实例

  • 更新策略

    • maxSurge:更新时,先不杀掉老版本,先拉取新版本的个数
    • maxUnavailable(需要考虑 ResourceQuota 的限制):用于保护应用,最大不可用的 Pod 个数,当达到满足个数,就不会继续升级
  • 深入理解 PodTemplateHash 导致的应用的易变性,比如更换 label 时,可能导致 hash 变更,也导致 Pod 重建

服务发现

由于 Pod 的不稳定性,例如在节点驱逐或是异常时,Pod 的 IP 地址会变化,Pod 名称会变化,因此需要引入服务发现实现 Pod 稳定的访问。

服务发布

服务发布需要提供的功能:

需要把服务发布至集群内部或者外部,服务的不同类型

  • ClusterIP(Headless):``cluster` 对应服务生成一个虚拟 IP
  • NodePort:IP 之外,额外增加一个端口
  • LoadBalancer:在外部的负载均衡器上配置 IP,通过访问这个 IP 实现负载均衡。由这个外部的负载均衡决定请求转发到哪个 Pod
  • ExternalName:通过域名绑定一个外部服务地址

证书管理(https)和七层负载均衡的需求

需要 gRPC 负载均衡:gRPC 是基于 HTTP/2,会复用 TCP 连接,需要在应用层实现负载均衡

DNS 需求(服务发布时,通过域名绑定,使用更加简便)

与上下游服务的关系

服务发布的挑战

kube-dns

  • DNS TTL 问题:每次 DNS 的请求都会携带一个对应的 TTL,减少域名服务器的压力;但是坏处是变更无法及时生效。

Service

  • ClusterIP 只能对内
  • Kube-proxy 支持的 iptables/ipvs 规模有限
  • IPVS 的性能和生产化问题
  • kube-proxydrift 问题(多节点下一致性问题)
  • 频繁的 Pod 变动(spec change, failover, crashloop)导致 LB 频繁变更
  • 对外发布的 Service 需要与企业 ELB 集成
  • 不支持 gRPC
  • 不支持自定义 DNS 和高级路由功能

Ingress:外部流量进入集群的方式

  • Spec 的成熟度

其他可选方案或多或少都有一些问题,无法全面覆盖。

跨地域部署

需要多少实例

如何控制失败域,部署在几个地区,AZ,集群?

如何进行精细的流量控制

如何做按地域的顺序更新

如何回滚

微服务架构下的高可用挑战

服务发现

  • 微服务架构是由一系列职责单一的细粒度服务构成的分布式网状结构,服务之间通过轻量机制进行通信,这时候必然引入一个服务注册发现问题,也就是说服务提供方要注册通告服务地址,服务的调用方要能发现目标服务。
  • 同时服务提供方一般以集群方式提供服务,也就引入了负载均衡和健康检查问题。

互联网架构发展历程

image-20240126091834767

  • 单一网页阶段:一个后端服务即可提供服务
  • 负载均衡和高可用阶段:使用 nginx 做转发或者代理
  • L4/L7 层负载均衡阶段:通过硬件 LB 实现负载均衡,更好的管理和接收所有的流量
  • 多个 L4 负载均衡阶段:通过路由(或者其他技术)将流量转发到不同的 L4 负载均衡器
  • 多数据中心阶段

理解网络包格式

例如一个 HTTP 请求的数据包格式:

image-20240126091859036

负载均衡的目的:简化的讲,获取的数据包之后,修改数据包的包头,修改其中的目标地址以及目标端口,将数据包转发到对应服务器上。

集中式 LB 服务发现

在集群外部,加一层 LB

  • 在服务消费者和服务提供者之间有一个独立的 LB (硬件级别集中式负载均衡器,所有的请求流量都从这个负载均衡器进来)
  • LB 上所有服务的地址映射表,通常由运维配置注册
  • 当服务消费方调用某个目标服务时,它向 LB 发起请求,由 LB 以某种策略(比如 Round-Robin)做负载均衡后将请求转发到目标服务
  • LB 一般具备健康检查能力,能自动摘除不健康的服务实例
  • 服务消费方通过 DNS 发现 LB,运维人员为服务配置一个 DNS 域名,这个域名指向 LB

image-20240126092101131

  • 集中式 LB 方案实现简单,在 LB 上也容易做集中式的访问控制,这一方案目前还是业界主流
  • 集中式 LB 的主要问题是单点问题,所有服务调用流量都经过 LB,当服务数量和调用量大的时候,LB 容易成为瓶颈,且一旦 LB 发生故障对整个系统的影响是灾难性的
  • LB 在服务消费方和服务提供方之间增加了一跳(hop),有一定性能开销

进程内 LB 服务发现

集中式 LB 有一些缺陷,例如某个流量较大时会占满带宽。因此,一些常见下引入进程内 LB 服务发现

  • 进程内 LB 方案将 LB 的功能以库的形式集成到服务消费方进程里面,该方案也被称为客户端负载方案。
  • 服务注册表(Service Registry)配合支持服务自注册和自发现,服务提供方启动时,首先将服务地址注册到服务注册表(同时定期报心跳到服务注册表以表明服务的存活状态)。
  • 服务消费方要访问某个服务时,它通过内置的 LB 组件向服务注册表查询(同时缓存并定期刷新)目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。
  • 这一方案对服务注册表的可用性(Availability)要求很高,一般采用能满足高可用分布式一致的组件(例如 ZookeeperConsuletcd 等)来实现。

image-20240126092751995

  • 进程内 LB 是一种分布式模式,LB 和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案以客户端(Client Library)的方式集成到服务调用方进程里面,如果企业内有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本。(一般会集中在中间件中,例如 gRPC
  • 一旦客户端跟随服务调用方发布到生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,所以该方案的升级推广有不小的阻力。

独立 LB 进程服务发现

  • 针对进程内 LB模式的不足而提出的一种折中方案,原理和第二种方案基本类似
  • 不同之处是,将 LB 和服务发现功能从进程内移出来,变成主机上的一个独立进程,主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立 LB 进程做服务发现和负载均衡(sidecar 的方式)
  • LB 独立进程可以进一步与服务消费方进行解耦,以独立集群的形式提供高可用的负载均衡服务
  • 这种模式可以称之为真正的 软负载Soft Load Balancing

image-20240129145957931

  • 独立 LB 进程也是一种分布式方案,没有单点问题,一个 LB 进程挂了只影响该主机上的服务调用方
  • 服务调用方和 LB 之间是进程间调用,性能好
  • 简化了服务调用方,不需要为不同语言开发客户库(使用 SDK 替换为本地进程间通信),LB 的升级不需要服务调用方改代码
  • 不足是部署较复杂,环节多,出错调试排查问题不方便

负载均衡

  • 系统的扩展可分为纵向(垂直)扩展(实例增加资源)和横向(水平)扩展(增加实例个数)
    • 纵向扩展:是从单机的角度通过增加硬件处理能力,比如 CPU 处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升,不能满足大型分布式系统(网站),大流量,高并发,海量数据的问题
    • 横向扩展:通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力,这就是典型的集群和负载均衡架构(横向扩展需要预先提供优雅终止和优雅启动)
  • 负载均衡的作用(解决的问题):
    • 解决并发压力,提高应用处理性能,增加吞吐量,加强网络处理能力
    • 提供故障转移,实现高可用
    • 通过添加或减少服务器数量,提供网站伸缩性,扩展性
    • 安全防护,负载均衡设备上做一些过滤,黑白名单等处理

DNS 负载均衡

最早的负载均衡技术,利用域名解析实现负载均衡,在 DNS 服务器,配置多个 A 记录,这些 A 记录对应的服务器构成集群。

image-20240126140350817

优点:

  • 使用简单
    • 负载均衡工作,交给 DNS 服务器处理,省掉了负载均衡服务器维护的麻烦
  • 提高性能
    • 可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能

缺点:

  • 可用性差
    • DNS 解析是多级解析,新增/修改 DNS 后,解析时间较长
    • 解析过程中,用户访问网站将失败
  • 扩展性差
    • DNS 负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展
    • 一般是轮询解析结果,并不能起到真正的负载均衡效果
  • 维护性差:
    • 也不能反映服务器的当前运行状态;
    • 支持的算法少;
    • 不能区分服务器的差异,不能根据系统与服务的状态来判断负载

负载均衡技术概览

  • Envoy(数据面)/Istio(控制面)

    对应的 LB 技术:

    • TLS termination
    • L7 path forwardingredirecting
    • URL/Header rewrite

    对应 OSI 层次模型中的第 7 层,应用层(例如 nginx 转发,拆解出 HTTP 的报文 header 中的数据以及配置,获取具体需要转发到的节点)

  • Kube Services/ Kube-Proxy

    ELB Provider

    IP 分配

    IP 路由

    网络策略

    三层隧道

    对应的 LB 技术:

    • 网络地址转换(NAT):改原始包的包头目标地址和源地址
    • 隧道技术(Tunnel):在原始包的基础上封装一层

    对应 OSI 层次模型中的第 4 层,传输层

    • 数据链路层修改 MAC 地址进行负载均衡
    • 响应数据包直接返回给用户浏览器

    对应 OSI 层次模型中的第 2 层,链路层

具体使用几层负载均衡技术,可以根据实际情况来决定。例如如果所有服务都在同一个二层网络,则可以使用链路层的 LB 技术。

网络地址转换

网络地址转换(Network Address TranslationNAT)通常通过修改数据包的源地址(Source NAT)或目标地址(Destination NAT)来控制数据包的转发行为。

image-20240126141408190

新建 TCP 连接

为记录原始客户端 IP 地址,负载均衡功能不仅要进行数据包的源地址修改,同时要记录原始客户端 IP 地址,基于简单的 NAT 无法满足此需求,于是衍生出了基于传输层协议的负载均衡的另一种方案– TCP/UDP Termination 方案

image-20240126141535864

链路层负载均衡

  • 在通信协议的数据链路层修改 MAC 地址进行负载均衡
  • 数据分发时,不修改 IP 地址,指修改目标 MAC 地址,配置真实物理服务器集群所有机器虚拟 IP 和负载均衡服务器 IP 地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的
  • 实际处理服务器 IP 和数据请求目的 IP 一致,不需要经过负载均衡服务器进行地址转换,可将相应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR 模式)

image-20240126141801917

隧道技术

负载均衡中常用的隧道技术是 IP over IP,其原理是保持原始数据包 IP 头不变,在 IP 头外层增加额外的 IP 包头后转发给上游服务器。

上游服务器接收 IP 数据包,解开外层 IP 包头后,剩下的是原始数据包。

同样的,原始数据包中的目标 IP 地址要配置在上游服务器中,上游服务器处理完数据请求以后,响应包通过网关直接返回给客户端。

Service 对象

  • Service Selector
    • Kubernetes 允许将 Pod 对象通过标签 Label 进行标记,并通过 Service Selector 定义基于 Pod 标签的过滤规则,以便选择服务的上游应用实例
  • Ports
    • Ports 属性中定义了服务的端口、协议目标端口等信息
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: nginx-basic
spec:
type: ClusterIP
ports:
- port: 80 # 外部访问的端口
protocol: TCP
name: http
targetPort: 80 # 映射到的端口,也就是后端服务器的真实端口
selector:
app: nginx # 标签,将标签关联到负载均衡下面的真实服务器

Endpoint 对象

创建 svc 对象

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: nginx-basic
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: nginx
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
# kubectl get svc nginx-basic -o yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2024-01-29T07:43:57Z"
name: nginx-basic
namespace: practice
resourceVersion: "468661"
uid: adb510b5-738d-451f-9fcb-ce7524b70923
spec:
clusterIP: 10.1.39.38
clusterIPs:
- 10.1.39.38
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
  • Serviceselector 不为空时,Kubernetes Endpoint Controller 会侦听服务创建事件,创建与 Service 同名的 Endpoint 对象

    1
    2
    3
    # kubectl get endpoints
    NAME ENDPOINTS AGE
    nginx-basic <none> 3s # ENDPOINTS 是空的,代表无法通过 svc 访问到 Pod

    创建一个无法正常 readydeployment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: nginx-deployment
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: nginx
    template:
    metadata:
    labels:
    app: nginx
    spec:
    containers:
    - name: nginx
    image: nginx
    readinessProbe: # ready 设置
    exec:
    command:
    - cat
    - /tmp/healthy # 5s 之后获取一个不存在的 文件,获取到状态才 ready
    initialDelaySeconds: 5
    periodSeconds: 5
    1
    2
    3
    # kubectl get pod
    NAME READY STATUS RESTARTS AGE
    nginx-deployment-6b5b456bdb-vghq2 0/1 Running 0 26s
  • selector 能够选取的所有 PodIP 都会被配置到 address 属性中

    • 如果此时 selector 所对应的 filter 查询不到对应的 Pod,则 address 列表为空
    • 默认配置下,如果此时对应的 Podnot ready 状态,则对应的 PodIP 只会出现在 subsetsnotReadyAddress 属性中,这意味着对应的 Pod 还没准备好提供服务,不能作为流量转发的目标
    • 如果设置了 PublishNotReadyAddresstrue,则无论 Pod 是否就绪都会被加入 readyAddress list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# kubectl get endpoints nginx-basic -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-01-29T07:45:46Z"
creationTimestamp: "2024-01-29T07:43:58Z"
name: nginx-basic
namespace: practice
resourceVersion: "468949"
uid: 0daf1665-f155-4392-958a-9faee3b69c0b
subsets:
- notReadyAddresses: # 未准备好
- ip: 10.244.57.210
nodeName: slave02
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-vghq2
namespace: practice
uid: 4f489f66-a438-40e9-8f14-1a128186f623
ports:
- name: http
port: 80
protocol: TCP

endpoint 可以理解为 svcpod 之间的中间对象,维护所有 pod 对应的 IP。一个 svc 可以映射多个 pod,一个 pod 也可以同时被多个 svc 映射,所以 podsvc 之间的关系是多对多,endpoint 的作用就是维护多对多的关系。

例如增加一个 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
# kubectl scale deployment nginx-deployment --replicas 2
# kubectl get endpoints nginx-basic -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-01-29T07:48:20Z"
creationTimestamp: "2024-01-29T07:43:58Z"
name: nginx-basic
namespace: practice
resourceVersion: "469393"
uid: 0daf1665-f155-4392-958a-9faee3b69c0b
subsets:
- notReadyAddresses:
- ip: 10.244.34.94
nodeName: slave01
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-dv97r
namespace: practice
uid: 2cc4d019-7421-447e-8a30-48ab08ef09b0
- ip: 10.244.57.210
nodeName: slave02
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-vghq2
namespace: practice
uid: 4f489f66-a438-40e9-8f14-1a128186f623
ports:
- name: http
port: 80
protocol: TCP

在其中一个 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
# kubectl exec -it nginx-deployment-6b5b456bdb-dv97r -- touch /tmp/healthy
# kubectl get endpoints
NAME ENDPOINTS AGE
nginx-basic 10.244.34.94:80 7m21s
# kubectl get endpoints -o yaml
apiVersion: v1
items:
- apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-01-29T07:51:04Z"
creationTimestamp: "2024-01-29T07:43:58Z"
name: nginx-basic
namespace: practice
resourceVersion: "469742"
uid: 0daf1665-f155-4392-958a-9faee3b69c0b
subsets:
- addresses:
- ip: 10.244.34.94 # 正常地址
nodeName: slave01
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-dv97r
namespace: practice
uid: 2cc4d019-7421-447e-8a30-48ab08ef09b0
notReadyAddresses:
- ip: 10.244.57.210 # 没准备好的地址
nodeName: slave02
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-vghq2
namespace: practice
uid: 4f489f66-a438-40e9-8f14-1a128186f623
ports:
- name: http
port: 80
protocol: TCP
kind: List
metadata:
resourceVersion: ""

PublishNotReadyAddress 语义

1
2
3
4
5
6
7
8
9
10
11
12
13
# kubectl explain service.spec.publishNotReadyAddresses
KIND: Service
VERSION: v1

FIELD: publishNotReadyAddresses <boolean>

DESCRIPTION:
publishNotReadyAddresses, when set to true, indicates that DNS
implementations must publish the notReadyAddresses of subsets for the
Endpoints associated with the Service. The default value is false. The
primary use case for setting this field is to use a StatefulSet's Headless
Service to propagate SRV records for its Pods without respect to their
readiness for purpose of peer discovery.

kube-proxy 会监听 svcendpoint,并且通过调用接口,在每个节点上都创建出对应转发关系,每个节点的 Pod 和主机之间,都可以通过 svc 互通。

这样会引发其他的问题,在集群规模变得很大的时候,任何一个 Pod\SVC 的变动都会引发 endpoint 的变动,导致同步数据流量占用巨大。因此社区提出一个新的对象,endpointSlice

EndpointSlice 对象

  • 当某个 Service 对应的 backend Pod 较多时,Endpoint 对象就会因保存的地址信息过多而变得异常庞大
  • Pod 状态的变更会引起 Endpoint 的变更,Endpoint 的变更会被推送至所有节点,从而导致持续占用大量网络带宽
  • EndpointSlice 对象,用于对 Pod 较多的 Endpoint 进行切片,切片大小可以自定义
  • 切片之后,当 Pod 发生变动,只会推送部分变动的 endpoint 信息到所有节点的 endpoint
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
# kubectl get endpointslices.discovery.k8s.io
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
nginx-basic-tqdzm IPv4 80 10.244.57.210,10.244.34.94 8m41s
# kubectl get endpointslices.discovery.k8s.io nginx-basic-tqdzm -o yaml
addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
- 10.244.57.210
conditions:
ready: false
serving: false
terminating: false
nodeName: slave02
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-vghq2
namespace: practice
uid: 4f489f66-a438-40e9-8f14-1a128186f623
- addresses:
- 10.244.34.94
conditions:
ready: true
serving: true
terminating: false
nodeName: slave01
targetRef:
kind: Pod
name: nginx-deployment-6b5b456bdb-dv97r
namespace: practice
uid: 2cc4d019-7421-447e-8a30-48ab08ef09b0
kind: EndpointSlice
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-01-29T07:51:04Z"
creationTimestamp: "2024-01-29T07:43:58Z"
generateName: nginx-basic-
generation: 4
labels:
endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
kubernetes.io/service-name: nginx-basic
name: nginx-basic-tqdzm
namespace: practice
ownerReferences:
- apiVersion: v1
blockOwnerDeletion: true
controller: true
kind: Service
name: nginx-basic
uid: adb510b5-738d-451f-9fcb-ce7524b70923
resourceVersion: "469743"
uid: 238f5c1d-b094-4b64-a40a-18f3cadf57a8
ports:
- name: http
port: 80
protocol: TCP

不定义 Selector 的 Service

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: service-without-selector
spec:
ports:
- port: 80
protocol: TCP
name: http

创建出来后,不会产生 endpoint

1
2
3
4
5
6
7
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-basic ClusterIP 10.1.39.38 <none> 80/TCP 19m
service-without-selector ClusterIP 10.1.134.211 <none> 80/TCP 2s
# kubectl get endpoints
NAME ENDPOINTS AGE
nginx-basic 10.244.34.94:80 19m
  • 用户创建了 Service 但不定义 Selector
    • Endpoint Controller 不会为该 Service 自动创建 Endpoint
    • 用户可以手动创建 Endpoint 对象,并设置任意 IP 地址到 Address 属性
    • 访问该服务的请求会被转发至目标地址
  • 通过该类型服务,可以为集群外的一组 Endpoint 创建服务

这个功能可以用于使用集群外部服务,并且由 svc 自动实现负载均衡。

Service、Endpoint 和 Pod 的对应关系

image-20240126143605168

svc 的目的是让外部服务访问 clusterIP:port ,由 svc 通过 LB 将流量转发到背后的 Pod

Service 类型

  • ClusterIP
    • Service 的默认类型,服务被发布至仅集群内部可见的虚拟 IP 地址上
    • API Server 启动时,需要通过 --service-cluster-ip-range 参数配置虚拟 IP 地址段,API Server 中有用于分配 IP 地址和端口的组件,当该组件捕获 Service 对象并创建事件时,会从配置的虚拟 IP 地址段中取一个有效的 IP 地址,分配给该 Service 对象
  • NodePort
    • API Server 启动时,需要通过 --node-port-range 参数配置 nodePort 的范围,同样的,API Server 组件会捕获 Service 对象并创建事件,即从配置好的 nodePort 范围取一个有效端口,分配给该 Service(默认 30000 - 32000)
    • 每个节点的 kube-proxy 会尝试在服务分配的 nodePort 上建立监听器接收请求,并转发给服务对应的后端 Pod 实例
  • LoadBalancer
    • 企业数据中心一般会采购一些负载均衡器,作为外网请求进入数据中心内部的统一流量入口
    • 针对不同的基础架构云平台,Kubernetes Cloud Manager 提供支持不同供应商 APIService Controller。如果需要在 OpenStack 云平台上搭建 Kubernetes 集群,那么只需要提供一份 openstack.rc,OpenStack Service Controller 即可通过调用 LBaaS API 完成负载均衡配置
    • 使用 LoadBalancer 后,流量会从 LoadBalancer 进来,转发到对应 Pod

Service 类型不是并列关系,而是包含关系,从下到上,一层一层包含。

其他类型服务

  • Headless Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    apiVersion: v1
    kind: Service
    metadata:
    name: nginx-headless
    spec:
    ClusterIP: None # 指定 None 代表 Headless,不需要负载均衡配置,
    ports:
    - port: 80
    protocol: TCP
    name: http
    selector:
    app: nginx

    Headless 服务是用户将 clusterIP 显示定义为 None 的服务。

    无头的服务意味着 Kubernetes 不会为该服务分配统一入口,包括 clusterIPnodePort

    一般用于 StatefulSet 服务,给每一个 Pod 创建一个独立的 Service 入口

  • ExternalName Service

    1
    2
    3
    4
    5
    6
    7
    8
    apiVersion: v1
    kind: Service
    metadata:
    name: my-service
    namespace: prod
    spec:
    type: ExternalName
    externalName: tencent.com

    为一个服务创建别名,一般用于访问外部服务或者给内部服务做替换。

Service Topology

  • 一个网络调用的延迟受客户端和服务器所处位置的影响,两者是否在同一节点、同一机架、同一可用区、同一数据中心,都会影响参与数据传输的设备数量
  • 在分布式系统中,为保证系统的高可用,往往需要控制应用的错误域(Failure Domain),比如通过反亲和性配置,将一个应用的多个副本部署在不同机架,甚至不同的数据中心
  • Kubernetes 提供通用标签标记节点所处的物理位置,如:
1
2
3
4
5
topology.kubernetes.io/zone: us-west2-a
failure-domain.beta.kubernetes.io/region: us-west
failure-domain.tess.io/network-device: us-westo5-ra053
failure-domain.tess.io/rack: us_west02_02-314_19_12
kubernetes.io/hostname: node-1
  • Service 引入了 topologyKeys 属性,可以通过如下设置来控制流量,只会将流量转发到某个节点或者优先转发到某个节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apiVersion: v1
    kind: Service
    metadata:
    name: nodelocal
    spec:
    ports:
    - port: 80
    protocol: TCP
    name: http
    selector:
    app: nginx
    topologyKeys:
    - "kubernetes.io/hostname" # 硬性,按照 topologyKeys 执行当本节点有对应 Pod 才转发请求,否则直接拒绝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    apiVersion: v1
    kind: Service
    metadata:
    name: prefer-nodelocal
    spec:
    ports:
    - port: 80
    protocol: TCP
    name: http
    selector:
    app: nginx
    topologyKeys: # 软性,按照 topologyKeys 指定的顺序层级来优先转发,最终 * 兜底
    - "kubernetes.io/hostname"
    - "topology.kubernetes.io/zone"
    - "topology.kubernetes.io/region"
    - "*"
    • 当 topologyKeys 设置为 ["kubernetes.io/hostname"] 时,调用服务的客户端所在节点上如果有服务实例正在运行,则该实例处理请求,否则,调用失败。
    • 当 topologyKeys 设置为 ["kubernetes.io/hostname", "topology.kubernetes.io/zone", "topology.kubernetes.io/region"] 时,若同一节点有对应的服务实例,则请求会优先转发至该实例。否则,顺序查找当前 zone 及当前 region 是否有服务实例,并将请求按顺序转发。
    • 当 topologyKeys 设置为 ["topology.kubernetes.io/zone", "*"] 时,请求会被优先转发至当前 zone 的服务实例。如果当前 zone 不存在服务实例,则请求会被转发至任意服务实例。

kube-proxy

每台机器上都运行一个 kube-proxy 服务,它监听 API Serverservice (负载均衡 IP 和对应 endpoint )和 endpoint ( Pod 信息)的变化情况,并通过 iptables 等来为服务配置负载均衡(仅支持 TCPUDP

kube-proxy 可以直接运行在物理机上,也可以以 static pod 或者 DaemonSet 的方式运行。

kube-proxy 当前支持一下几种实现:

  • userspace:最早的负载均衡方案,它在用户空间监听一个端口,所有服务通过 iptables 转发到这个端口,然后在其内部负载均衡到实际的 Pod。该方式最主要的问题是效率低,有明显的性能瓶颈(内核态 网卡处理 -> 用户态 kube-proxy -> 内核态 转发到对应网口)
  • iptables:目前推荐的方案,完全以 iptables 规则的方式来实现 service 负载均衡。该方式最主要的问题是在服务多的时候产生太多的 iptables 规则,非增量式更新会引入一定的时延,大规模情况下有明显的性能问题(通过 kernel 中的 netfilter 处理,netfileter 包含了 iptablesipvs
  • ipvs:为解决 iptables 模式的性能问题,v 1.8 新增了 ipvs 模式,采用增量式更新,并可以保证 service 更新期间连接保持不断开
  • winuserspace:同 userspace,但仅工作在 windows

Linux 内核处理数据包:Netfilter 框架

image-20240126155500883

  • 数据包在内核中处理会通过很多 hook(图中绿白色),ipatbles 通过接口配置这些 hook
  • 内核处理可以在多个层级中实现不同的处理方式,可以在不同层进行过滤、转发,例如 nat 在 网络层 prerouting 时实现

Netfilter 和 iptables

image-20240126155535125

  • 网卡接收到数据包之后,通过硬件中断提醒 CPU

  • 避免每次都有数据包都进入硬中断,影响效率,CPU 通过生产者消费者模型,让 kernel 的一个进程 ksoftirqd 处理软中断,进程在 kernel 中构造数据包 bufferbuffer 中包含包头和数据信息

    1
    2
    3
    # ps -ef | grep -i irq
    root 9 2 0 01:24 ? 00:00:02 [ksoftirqd/0]
    root 18 2 0 01:24 ? 00:00:03 [ksoftirqd/1]
  • buffer 交由 Netfiler 处理,Netfilter 读取 iptables 的规则,判断是否 nat 以及是否访问本机,如果不是则 FORWARD 处理

iptables

image-20240126155552175

五链(链:chain, 包括 PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING)四表(managernatfilterraw

  1. 数据包进来,在 PREROUTING 中可以通过 dnat 做转换,dnat 是转换目标 IP 和端口
  2. 通过路由规则判断是访问本机还是访问其他地址
  3. 如果访问其他地址,则通过 FORWARD 将数据转发出去
  4. 本地进程可以通过 LOCAL_OUT 将请求转发到其他地址,也是通过 snat 表实现

iptables 支持的锚点

Table/chain PREROUTING INPUT FORWARD OUTPUT POSTROUTING
raw 支持 支持
mangle 支持 支持 支持 支持 支持
dnat 支持 支持
filter 支持 支持 支持
snat 支持 支持 支持

简单解释:例如上面的 nginx ,在 iptables 中生成的条目

1
2
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
  • -A 代表添加,PREROUTING 代表在 PREROUTING 这个 chain 中添加一条规则;
  • -m 是注释
  • -jjump,代表跳转到 KUBE-SERVICES

也就是出口和入口的流量,都会交给 KUBE-SERVICES 处理

过滤 nginxsvc 地址(svc 的地址只会记录在 iptables 里面,不会绑定到任何的网卡上,因此 ping 不会有响应)

1
-A KUBE-SERVICES -d 10.1.39.38/32 -p tcp -m comment --comment "practice/nginx-basic:http cluster IP" -m tcp --dport 80 -j KUBE-SVC-6QMKDF5V6IOJPWNA
  • -d 是目标地址
  • -p 是协议
  • --dport 是目标端口 也是跳转到 KUBE-SVC-6QMKDF5V6IOJPWNA
1
2
-A KUBE-SVC-6QMKDF5V6IOJPWNA ! -s 10.244.0.0/16 -d 10.1.39.38/32 -p tcp -m comment --comment "practice/nginx-basic:http cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-6QMKDF5V6IOJPWNA -m comment --comment "practice/nginx-basic:http -> 10.244.34.94:80" -j KUBE-SEP-2Q45Q4BWORYFEJSP

多条规则,会从上到下,匹配到则直接处理。

第一条,当源地址不是 10.244.0.0/16 ,目标地址是 10.1.39.38/32 ,协议是 tcp,目标端口协议是 tcp,端口是 80,则交由 KUBE-MARK-MASQ 这个 chain 处理

第二条,交由 KUBE-SEP-2Q45Q4BWORYFEJSP 处理

1
2
-A KUBE-SEP-2Q45Q4BWORYFEJSP -s 10.244.34.94/32 -m comment --comment "practice/nginx-basic:http" -j KUBE-MARK-MASQ
-A KUBE-SEP-2Q45Q4BWORYFEJSP -p tcp -m comment --comment "practice/nginx-basic:http" -m tcp -j DNAT --to-destination 10.244.34.94:80

第一条,如果源是 10.244.34.94/32,交由 KUBE-MARK-MASQ 处理

第二条,如果是 tcp 协议的包,且目标协议是 tcp,则交由 DNAT 处理,转发到目标 10.244.34.94:80 (也就是状态为 readypod

如果将 podreadinessProbe 去掉,并且副本数改为 3 个

1
2
3
4
5
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-7854ff8877-gpp7c 1/1 Running 0 2m1s 10.244.34.93 slave01 <none> <none>
nginx-deployment-7854ff8877-pbbwb 1/1 Running 0 2m10s 10.244.57.212 slave02 <none> <none>
nginx-deployment-7854ff8877-qs72v 1/1 Running 0 112s 10.244.219.102 master <none> <none>

iptables-save 查看 iptables

1
2
3
4
-A KUBE-SVC-6QMKDF5V6IOJPWNA ! -s 10.244.0.0/16 -d 10.1.39.38/32 -p tcp -m comment --comment "practice/nginx-basic:http cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-6QMKDF5V6IOJPWNA -m comment --comment "practice/nginx-basic:http -> 10.244.219.102:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-UGS2DPNHJS6SK3GB
-A KUBE-SVC-6QMKDF5V6IOJPWNA -m comment --comment "practice/nginx-basic:http -> 10.244.34.93:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-7YSYEPK2LDZNPWSP
-A KUBE-SVC-6QMKDF5V6IOJPWNA -m comment --comment "practice/nginx-basic:http -> 10.244.57.212:80" -j KUBE-SEP-NLI5YEOT7OM374BP

可以看到,转发给 3 个 Pod 的过程其实是通过随机数实现,转发到第一个节点有 33% 的概率,如果没有转发到,则转发到第二个节点的概率是 50%,如果也没有命中,则转发给第三个节点。

可以看到这个负载均衡策略是比较简单的,就是随机轮询。同理,三个 pod 一个 svc 就有很多条数,如果上千个 pod 则每个节点的条目会非常多,在匹配的时候效率也会很慢。因此 iptables 并不适合大规模集群(iptables 本意也不是大规模控制,设计时也没有考虑这种情况),因此后续需要通过 ipvs 来解决这些问题。

kube-proxy 工作原理

image-20240126155727758

每个节点上的 kube-proxy 通过监听 kube-apiserver 的信息,将 ClusterIP 规则、NodePort 规则、LB IP规则添加到 iptables 中(不是增量添加,而是全局覆盖)。

通常来讲,LB 是使用外部 LB 硬件设备,当指定了外部 LB 并且创建了对应 LB 类型的 svcsvc 会带有对应 IPport,通过节点 label 设定规则,外部 LB 设备虽然无法直接访问 ClusterIP,社区解决方案是在 LBnat 之后,流量请求到集群,集群通过 label 将流量转发到对应 node,在 node 上通过 iptables 判断转发到哪个 PodIP

Kubernetes iptables 规则

image-20240126155824553

iptables 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# iptables -L 
# iptables-save
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

-A KUBE-SEP-GDQR6ZCHBNMEZUK3 -s 10.244.57.194/32 -m comment --comment "default/nginx:80" -j KUBE-MARK-MASQ
-A KUBE-SEP-GDQR6ZCHBNMEZUK3 -p tcp -m comment --comment "default/nginx:80" -m tcp -j DNAT --to-destination 10.244.57.194:80
-A KUBE-SEP-GN2IRW3YLVS6SMFW -s 10.244.57.193/32 -m comment --comment "default/nginx:80" -j KUBE-MARK-MASQ
-A KUBE-SEP-GN2IRW3YLVS6SMFW -p tcp -m comment --comment "default/nginx:80" -m tcp -j DNAT --to-destination 10.244.57.193:80

-A KUBE-SERVICES -d 10.1.186.208/32 -p tcp -m comment --comment "default/nginx:80 cluster IP" -m tcp --dport 80 -j KUBE-SVC-PV5PUP4CQH7C4UEJ

-A KUBE-SVC-PV5PUP4CQH7C4UEJ ! -s 10.244.0.0/16 -d 10.1.186.208/32 -p tcp -m comment --comment "default/nginx:80 cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-PV5PUP4CQH7C4UEJ -m comment --comment "default/nginx:80 -> 10.244.219.103:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-KJ4JNZEHO7FA6FRR
-A KUBE-SVC-PV5PUP4CQH7C4UEJ -m comment --comment "default/nginx:80 -> 10.244.57.193:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GN2IRW3YLVS6SMFW
-A KUBE-SVC-PV5PUP4CQH7C4UEJ -m comment --comment "default/nginx:80 -> 10.244.57.194:80" -j KUBE-SEP-GDQR6ZCHBNMEZUK3

IPVS

image-20240126161259797

ipvs 只有三个 hook 点:LOCAL_INFORWARDLOCAL_OUT,因此,当有外部访问 cluster IP(集群中跨主机通信) 时,无法在 PREROUTING 中判断请求是否在本机,需要在路由中处理。

所以,ipvs 会在本机创建一个虚拟网卡,配置相应路由,接收请求,转发到 LOCAL_IN 进行处理。

iptables 切换到 ipvs,修改 configmap

1
2
# kubectl edit cm kube-proxy -n kube-system
mode: "ipvs" # 如果是空的,代表使用默认 iptabels

手动删除 kube-proxyPod

1
2
3
4
# kubectl delete pod kube-proxy-d9hnp kube-proxy-mkn6x kube-proxy-wmt5w -n kube-system
pod "kube-proxy-d9hnp" deleted
pod "kube-proxy-mkn6x" deleted
pod "kube-proxy-wmt5w" deleted

更新 iptablesnat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# iptables -F -t nat
# iptables-save

*nat

-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-LOAD-BALANCER -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m comment --comment "Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose" -m set --match-set KUBE-LOOP-BACK dst,dst,src -j MASQUERADE
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
-A KUBE-SERVICES -s 127.0.0.0/8 -j RETURN
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ // 使用 ipset
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT
-A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT
COMMIT
# Completed on Tue Jan 30 05:50:05 2024

下载 ipvsadm 名称查看

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
# ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.1.0.1:443 rr
-> 192.168.239.128:6443 Masq 1 0 0
TCP 10.1.0.10:53 rr
-> 10.244.57.193:53 Masq 1 0 0
-> 10.244.57.194:53 Masq 1 0 0
TCP 10.1.0.10:9153 rr
-> 10.244.57.193:9153 Masq 1 0 0
-> 10.244.57.194:9153 Masq 1 0 0
TCP 10.1.39.38:80 rr # nginx 的 svc,代表访问这个ip的80端口,使用 rr 轮询,转发到下面三个对应地址和端口
-> 10.244.34.66:80 Masq 1 0 0
-> 10.244.57.197:80 Masq 1 0 0
-> 10.244.219.66:80 Masq 1 0 0
TCP 10.1.62.215:443 rr
-> 10.244.34.67:5443 Masq 1 0 0
-> 10.244.57.198:5443 Masq 1 0 0
TCP 10.1.134.211:80 rr
TCP 10.1.203.17:5473 rr
-> 192.168.239.129:5473 Masq 1 0 0
-> 192.168.239.130:5473 Masq 1 0 0
UDP 10.1.0.10:53 rr
-> 10.244.57.193:53 Masq 1 0 0
-> 10.244.57.194:53 Masq 1 0 0

由于 ipvsPOSROUTING 中没有锚点,在 Pod 跨主机通信时,为了让接收者能够回包(也就是接收端有回来的路由),依然需要包伪装,因此需要 iptables 配合使用,通过 ipset 封装数据包

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
# ipset -L

Name: KUBE-IPVS-IPS
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 488
References: 1
Number of entries: 6
Members:
10.1.39.38
10.1.203.17
10.1.0.1
10.1.0.10
10.1.134.211
10.1.62.215

Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 704
References: 3
Number of entries: 8
Members:
10.1.62.215,tcp:443
10.1.0.1,tcp:443
10.1.39.38,tcp:80
10.1.0.10,tcp:53
10.1.0.10,tcp:9153
10.1.134.211,tcp:80
10.1.0.10,udp:53
10.1.203.17,tcp:5473

通过聚合一些相同类型的 IP、协议、端口,减少 iptables 里面的条目。

端口信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 3e:1d:c5:7e:b9:52 brd ff:ff:ff:ff:ff:ff
inet 10.1.62.215/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.1.203.17/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.1.0.1/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.1.0.10/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.1.39.38/32 scope global kube-ipvs0 // 这个是 nginx svc 的IP
valid_lft forever preferred_lft forever
inet 10.1.134.211/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever

IPVS 支持的锚点和核心函数

HOOK 函数 核心函数 Priority
NF_INET_LOCAL_IN ip_vs_reply4 ip_vs_out NF_IP_PRI_NAT_SRC - 2
NF_INET_LOCAL_IN ip_vs_remote_request4 ip_vs_in NF_IP_PRI_NAT_SRC - 1
NF_INET_LOCAL_OUT ip_vs_local_reply4 ip_vs_out NF_IP_PRI_NAT_DST + 1
NF_INET_LOCAL_OUT ip_vs_local_request4 ip_vs_in NF_IP_PRI_NAT_DST + 2
NF_INET_FORWARD ip_vs_forward_icmp ip_vs_in_icmp 99
NF_INET_FORWARD Ip_vs_reply4 ip_vs_out 100

域名服务

  • Kubernetes Service 通过虚拟 IP 地址或者节点端口为用户应用提供访问入口
  • 然而这些 IP 地址和端口是动态分配的,如果用户重建一个服务,其分配的 clusterIPnodePort,以及 LoadBalancerIP 都是会变化的,我们无法把一个可变的入口发布出去供他人访问
  • Kubernetes 提供了内置的域名服务,用户定义的服务会自动获得域名,而无论服务重建多少次,只要服务名不可改变,其对应的域名就不会改变

CoreDNS

CoreDNS 包含一个内存态 DNS,以及与其他 controller 类似的控制器

CoreDNS 的实现原理是,控制器监听 ServiceEndpoint 的变化并配置 DNS,客户端 Pod 在进行域名解析时,从 CoreDNS 中查询服务对应的地址记录

image-20240126162005249

也就是监听 svcendpoint 的变化,将 svc1.ns1.svc.clusterdomain 作为 A 记录写入内存中的 dns 记录中,指向 svc 对应的 ip

在客户端中,使用 coredns 的方法是在 /etc/resolv.conf 中写入 coredns 的 ip

1
2
3
4
# cat /etc/resolv.conf
search practice.svc.cluster.local svc.cluster.local cluster.local localdomain
nameserver 10.1.0.10
options ndots:5

ndots:5:指的是解析的域名的 . 号的个数,如果小于 5 个,则使用上面 search 作为后缀补充,例如如果解析的是

1
nginx-basic

实际会解析

1
nginx-basic.practice.svc.cluster.local

如果解析不出来,则会换第二个拼接然后解析

dns 指向的是 corednssvc IP

1
2
3
# kubectl get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.1.0.10 <none> 53/UDP,53/TCP,9153/TCP 6d16h

当使用 coredns 作为 dns 解析可以解析出来

1
2
3
4
5
6
7
8
9
10
# kubectl get pod -n kube-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-857d9ff4c9-5k642 1/1 Running 0 16h 10.244.57.194 slave02 <none> <none>

# nslookup nginx-basic.practice.svc.cluster.local 10.244.57.194
Server: 10.244.57.194
Address: 10.244.57.194#53

Name: nginx-basic.practice.svc.cluster.local
Address: 10.1.39.38

如果需要解析外网,例如 www.baidu.compod 会首先在 coredns 中查询,coredns 中会往上游查询:

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
# kubectl get cm coredns -n kube-system -o yaml
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf { # 转发指向主机上的 /etc/resolv.conf 文件
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
creationTimestamp: "2024-01-23T14:19:03Z"
name: coredns
namespace: kube-system
resourceVersion: "236"
uid: 2325dc28-57a2-4290-a3a1-f1eb7f2b33d0

不同类型服务的 DNS 记录

  • 普通 Service
    • ClusterIPNodePortLoadBalancer 类型的 Service 都拥有 API Server 分配的 ClusterIPCoreDNS 会为这些 Service 创建 FQDN 格式为 $svcname.$namespace.svc.$clusterdomain: clusterIPA 记录及 PTR 记录,并为端口创建 SRV 记录
  • Headless Service
    • 顾名思义,无头,是用户在 Spec 显式置顶 ClusterIPNoneService,对于这类 ServiceAPI Service 不会为其分配 ClusterIPCoreDNS 为此类 Service 创建多条 A 记录,并且目标为每个就绪的 PodIP
    • 另外,每个 Pod 会拥有一个 FQDN 格式为 $podname.$svcname.$namespace.svc.$clusterdomain 的 A 记录指向 PodIP
  • ExternalName Service
    • 此类 Service 用来引用一个已经存在的域名,CoreDNS 会为该 Service 创建一个 CName 记录指向目标域名

Kubernetes 中的域名解析

  • Kubernetes Pod 有一个 DNS 策略相关的属性 DNSPolicy,默认值是 ClusterFirst,这个默认值会修改 Pod 的 resolv.conf

    如果是 default,则会使用主机上的 resolv.conf

    如果是 None,则可以使用自己配置的 dns

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spec:
    dnsPolicy: "None"
    dnsConfig:
    nameservers:
    - 1.2.3.4
    searchs:
    - xx.ns1.svc.cluster.local
    - xx.daemon.com
    options:
    - name: ndots
    values: "2"
  • Pod 启动后的 /etc/resolv.conf 会被改写,所有的地址解析优先发送至 CoreDNS

1
2
3
4
# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local localdomain
nameserver 10.1.0.10
options ndots:5
  • 当 Pod 启动时,同一 Namespace 的所有 Service 都会以环境变量的形式设置到容器内

关于 DNS 的落地实践

Kubernetes 作为企业基础架构的一部分,Kubernetes 服务也需要发布到企业 DNS,需要定制企业 DNS 控制器

  • 对于 Kubernetes 中的服务,在企业 DNS 同样创建 A/PTR/SRV records (通常解析地址是 LoadBalancer VIP
  • 针对 Headless service,在 PodIP 可全局路由的前提下,按需创建 DNS records
  • Headless service 的 DNS 记录,应该按需创建,否则对企业 DNS 冲击过大

如果使用企业 DNS 全部托管,这种方式会有些问题:

  • 虽然 dns 服务端可以设置 ttl,会影响客户端解析时效性
  • 如果 ttl 设置为 0 ,那么 dns 服务端压力会非常大
  • 另外客户端也会有 dns 缓存,也会影响解析时效性

服务在集群内通过 CoreDNS 寻址,在集群外通过企业 DNS 寻址,服务在集群内外有统一标识。

例如 coredns 的配置

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
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . 172.16.0.1
cache 30
loop
reload
loadbalance
}
consul.local:53 { # 对应后缀的解析执行特定 dns 服务器
errors
cache 30
forward . 10.150.0.1
}

Kubernetes 中的负载均衡技术

  • 基于 L4 的服务:也就是五元组(源IP、源端口、协议、目标IP、目标端口)
    • 基于 iptables/ipvs 的分布式四层负载均衡技术
    • 多种 Load Balancer Provider 提供与企业现有 ELB 的整合
    • kube-proxy 基于 iptables rules 为 Kubernetes 形成全局统一的 distributed load balancer
    • kube-proxy 是一种 meshInternal Client 无论通过 podipnodeport 还是 LB VIP 都经由 kube-proxy 跳转至 pod
    • 属于 Kubernetes core
  • 基于 L7Ingress:应用层级别,例如 nginxenvoy
    • 基于七层应用层,提供更多功能:通过请求的 header
    • TLS termination:安全,将 https 解密成 http
    • L7 path forwarding
    • URL/http header rewrite
    • 与采用 7 层软件紧密相关

Service 中的 Ingress 的对比

  • 基于 L4 的服务

    • 每个应用独占 ELB,浪费资源
    • 为每个服务动态创建 DNS 记录,频繁的 DNS 更新
    • 支持 TCPUDP,业务部门需要启动 HTTPS 服务,自己管理证书

    image-20240126163710182

  • 基于 L7Ingress

    • 多个应用共享 ELB,节省资源
    • 多个应用共享一个 Domain,可采用静态 DNS 配置
    • TLS termination 发生在 Ingress 层,可集中管理证书
    • 更多复杂性,更多的网络 hop

    image-20240126163807240

Ingress

安装部署:https://kubernetes.github.io/ingress-nginx/deploy/

1
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml

由于 registry.k8s.io 镜像拉不下来,因此需要将 yaml 下载下来,修改其中镜像地址,然后再部署

1
2
image: registry.aliyuncs.com/google_containers/nginx-ingress-controller:v1.8.1
image: registry.aliyuncs.com/google_containers/kube-webhook-certgen:v20230407
1
2
3
4
5
# kubectl get pod -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-4wr5f 0/1 Completed 0 4m14s
ingress-nginx-admission-patch-z689n 0/1 Completed 1 4m14s
ingress-nginx-controller-88b784f6-s86db 1/1 Running 0 61s

切换默认 nsingress-nginx

1
2
3
4
5
6
7
8
9
10
11
12
# kubectl exec -it ingress-nginx-controller-88b784f6-s86db ps aux
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
PID USER TIME COMMAND
1 www-data 0:00 /usr/bin/dumb-init -- /nginx-ingress-controller --publish-
7 www-data 0:01 /nginx-ingress-controller --publish-service=ingress-nginx/
23 www-data 0:00 nginx: master process /usr/bin/nginx -c /etc/nginx/nginx.c
27 www-data 0:00 nginx: worker process
28 www-data 0:00 nginx: worker process
29 www-data 0:00 nginx: worker process
30 www-data 0:00 nginx: worker process
31 www-data 0:00 nginx: cache manager process
161 www-data 0:00 ps aux

其中 ingress-nginx-controller 有两个进程,一个是 controller,检测 ingress 配置变化,一个是 nginx,转发流量

1
2
3
4
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.1.223.52 <pending> 80:31994/TCP,443:31495/TCP 8m
ingress-nginx-controller-admission ClusterIP 10.1.149.89 <none> 443/TCP 8m

默认创建一个LoadBalancer 类型的 svc ,提供给外部访问,对外提供的统一入口是 31994

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
# kubectl get svc ingress-nginx-controller -o yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2024-01-30T08:33:30Z"
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.8.1
name: ingress-nginx-controller
namespace: ingress-nginx
resourceVersion: "575296"
uid: 64ec39b1-7714-4d57-81d5-c0386e95be8a
spec:
allocateLoadBalancerNodePorts: true
clusterIP: 10.1.223.52
clusterIPs:
- 10.1.223.52
externalTrafficPolicy: Local
healthCheckNodePort: 32190
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- appProtocol: http
name: http
nodePort: 31994
port: 80
protocol: TCP
targetPort: http
- appProtocol: https
name: https
nodePort: 31495
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}

创建证书

如果需要通过 IP 访问而不是域名,需要将 DNS 设置为 *

1
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=xiaoyeshiyu.com/O=xiaoyeshiyu" -addext "subjectAltName = DNS:xiaoyeshiyu.com"

通过证书,创建 secret

1
kubectl create secret tls xiaoyeshiyu-tls --cert=./tls.crt --key=./tls.key

创建 ingress 对象,要指定为要转发出去的 svcns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway
annotations:
kubernetes.io/ingress.class: "nginx" # 指定 ingress 控制器
spec:
tls: # 如果提供的是 https
- hosts:
- xiaoyeshiyu.com
secretName: xiaoyeshiyu-tls # https 提供的证书
rules: # 转发规则,如果用 IP,设置为 *
- host: xiaoyeshiyu.com # 域名
http:
paths:
- path: "/" # 访问域名的路径
pathType: Prefix # 流量转发规则,前缀匹配,只要访问 xiaoyeshiyu.com/ 就会转发到 svc 的 80 端口
backend:
service:
name: nginx-basic # 指定 svc
port:
number: 80 # 指定端口

创建 Ingress

1
# kubectl create -f ingress.yaml

查看对象

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
# kubectl get ingresses.networking.k8s.io
NAME CLASS HOSTS ADDRESS PORTS AGE
gateway <none> xiaoyeshiyu.com 80, 443 2m30s

# kubectl get ingresses.networking.k8s.io gateway -o yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
creationTimestamp: "2024-01-30T09:25:34Z"
generation: 1
name: gateway
namespace: practice
resourceVersion: "581596"
uid: 2eb49a40-3454-4266-834b-7235277f9968
spec:
rules:
- host: xiaoyeshiyu.com
http:
paths:
- backend:
service:
name: nginx-basic
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- xiaoyeshiyu.com
secretName: xiaoyeshiyu-tls
status:
loadBalancer: {}

访问

1
# curl -H "Host: xiaoyeshiyu.com" https://192.168.239.128:31495 -v -k
  • Ingress
    • Ingress 是一层代理
    • 负责根据 hostnamepath 将流量转发到不同的服务上,使得一个负载均衡器用于多个后台应用
    • Kubernetes Ingress Spec 是转发规则的集合
  • Ingress Controller
    • 确保实际状态(Actual)与期望状态(Desired)一致的 ControlLoop
    • Ingress Controller 确保
    • 负载均衡配置
    • 边缘路由配置
    • DNS 配置

ingress 的局限性:

  1. tls:版本、加密方式
  2. 无法通过 httpheader 做区分
  3. 无法改写 httpheaderURL

对于这些附加功能,在 ingressspec 已经完善确定的情况下,会加在 annotation 里面,实现一些简单的功能

1
2
3
4
5
6
Name    Description	Values
nginx.ingress.kubernetes.io/rewrite-target Target URI where the traffic must be redirected string
nginx.ingress.kubernetes.io/ssl-redirect Indicates if the location section is accessible SSL only (defaults to True when Ingress contains a Certificate) bool
nginx.ingress.kubernetes.io/force-ssl-redirect Forces the redirection to HTTPS even if the Ingress is not TLS Enabled bool
nginx.ingress.kubernetes.io/app-root Defines the Application Root that the Controller must redirect if it's in '/' context string
nginx.ingress.kubernetes.io/use-regex Indicates if the paths defined on an Ingress use regular expressions bool

但是即使 annotation 里面实现了一些功能,依然无法具备 L7 下所有的能力,因此社区提供 service-api 的一堆模型,以及其他扩展的项目,例如数据层面的 envoy 和更加推荐的 Istio

image-20240126164003415

传统应用网络拓扑

image-20240126164018301

一般的架构模式是 3 个实例(如果一个卡在发布,这时坏了一个,还有一个可以正常工作),为了实现高可用,在本区域通过 APP Tier LB 实现高可用。

这种情况下,如果通过 dns 做转发,当区域级别异常时,可能由于 DNS 有缓存,继续将流量转发到异常节点。此时可以在区域内加一层 Web Tier LB,正常情况下将 99% 的流量转发到本机房,1% 的流量转发到其他区域机房。当机房故障,则将所有流量均分到另外两个区域,实现故障瞬间恢复。

三步构建 Ingress Controller

针对不同的业务场景,构建符合业务的 Ingress Controller

  • 复用 Kubernetes LoadBalancer 接口

    staging/src/k8s.io/cloud-provider/cloud.go

    1
    2
    3
    4
    5
    6
    7
    type LoadBalancer interface {
    GetLoadBalancer(ctx context.Context, clusterName string, service *v1.Service) (status *v1.LoadBalancerStatus, exists bool, err error)
    GetLoadBalancerName(ctx context.Context, clusterName string, service *v1.Service) string
    EnsureLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) (*v1.LoadBalancerStatus, error)
    UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error
    EnsureLoadBalancerDeleted(ctx context.Context, clusterName string, service *v1.Service) error
    }
  • 定义 Informer,监控 ingresssecretserviceendpoint 的变化,加入相应的队列:生产者

    image-20240126164536689

  • 启动 worker,遍历 ingress 队列:消费者

    • Ingress Domain 创建 LoadBalancer service,依赖 service controller 创建 ingress vip
    • Ingress Domain 创建 DNS A record 并指向 Ingress VIP
    • 更新 Ingress 状态

为什么需要构建 SLB 方案

  • 成本
    • 硬件 LB 价格昂贵
    • 固定的 tech refresh 周期
  • 配置管理
    • LB 设置的 onboard/offboard 由不同 team 管理
    • 不同设备 API 不一样,不支持 L7 API
    • 基于传统的 ssh 接口,效率低下
    • Flex up/down 以及 Migration 复杂
  • 部署模式
    • 1 + 1 模式
    • 隔离性差

ebay案例分享

亟待解决的问题

  1. 负载均衡配置
    1. 7 层方案(选型之后,实现 ingress controller
      1. Envoy vs. Nginx vs. Haproxy
    2. 4 层方案(使用 nat 则改包头,使用 tunnel 则加包头)
      1. 抛弃传统 HLB 的支持,用 IPVS 取代
  2. 边缘路由配置(提供四层 LB,将本机的路由发布出去)
    1. Ingress VIP 通过 BGP 协议发布给 TOR
  3. DNS 配置(一台节点上有非常多的域名)
    1. Ingress VIP 通过 BGP 协议发布给 TOR

需要解决问题:如何让 domain 用户自定义后台应用的访问地址,如何优化访问路径

  • L4 Provicer:Citrix NetScaler

    IPVS 插件:

    • 基于 IPAM 分配 VIP
    • VIP 绑定至 IPVS Directors
    • 创建 IP tunnel
    • 通过 BGP 发布 VIP 路由
  • L7 Provider:envoy

    • 基于 Envoy 提供 L7 Path forwarding
    • 可以提供 TLS Termination
    • 基于 upstreamIstio 实现配置管理和热加载
    • 基于 Istio 实现 Service Mesh
  • DNS Provider:DNS

    • 创建 DNS 记录
    • 为多个 clusteringress VIP 生成相同的 DNS 记录实现 DNS 联邦
  • Ingress Controller:Ingress

    • 编排控制器
    • Ingress 创建 service object,将 LB 配置委派给 service controller
    • 创建 configmap 为每个 ingress 创建 envoy 配置
    • 创建 L7 poddeployment 加载 envoy 集群
    • 生成 DNS 记录

L4 集群架构

image-20240126165717940

四层 Elb-Director 在集群的某几个特殊节点上,这几个 ELB 节点通过 BGP 与外部路由器宣告本地路由,外部路由器通过路由指向以及路由条目上的条数判断优先转发。

ELB 上的 IP 是虚拟 IP,使用 tunnel 将 IP 进行封装,IP-in-IP ,ELB 上获取数据包之后,将虚拟 IP 转换成底层 Pod IP,转发到 L7 LB。

ELB 上主要用于数据包告诉转发以及负载均衡,主要使用 Linux Kernel 进行封包,使用 ipvs 实现负载均衡。

L7 集群架构

image-20240126165734208

L7 Porxy 将流量分发给后面的 Pod,会用到一些长连接,拆出请求,获得响应之后封装发给上层。

数据流

image-20240126165746319

三层路由转发,通过外部封装一层源地址和目标地址,实现可路由转发。

跨大陆的互联网调用

image-20240126165810958

边缘加速:使用场景是跨大陆访问,例如用户端到核心机房,TCP 三次握手以及墙的问题会往往会导致访问超时。使用 CDN 加速可以将一些静态资源推送到全球范围的 CDN,用户在使用时通过域名解析解析到到就近 CDN 机房,而不需要请求到数据中心机房。

互联网路径的不确定性

使用 CDN 就是为了解决互联网路径的不确定性。例如加拿大的两个城市访问美国的路径可能不是同一条。

玻斯到美国

image-20240126165913594

墨尔本到美国

image-20240126165926586

一般可以通过统计用户活跃度,将用户集中划分到区域,在某个区域里面部署一套边缘节点,用于加速。

边缘加速方案综述

  • 在全球业务需求较大的城市创建边缘数据中心
    • 2017 年在建阿姆斯特丹,悉尼等 8 个城市
    • 迷你型数据中心,每个 site 15 台服务器
  • 在边缘数据中心搭建 Kubernetes 集群
  • 部署纯软件的 ingress 组件
    • 启动 Ingress Controller 完成 Ingress 配置
    • IPVS 负责 VIP 绑定,4 层负载均衡以及一致性哈希
    • EnvoyTLS terminate 在边缘节点,并提供 L7 路由转发
    • BGP 协议将 VIP 发布给路由器
  • 规划 Ingress 规则
    • 手工创建 endpoint 指向数据中心的应用
    • 创建 Ingress 并指向 endpoint 对应的 service

边缘加速组件

image-20240126170235929

用户先通过域名解析到就近边缘节点,在边缘节点上有一些缓存,存储静态文件,同时通过 L7 Terminator 将 HTTPS 的请求转成明文。

在边缘节点和 CDN 之间有专线 MTBB,流量更加稳定和安全。

对网络路径的优化

image-20240126170249220

优化路径:

  • 通过 CDN 服务提供商到数据中心;
  • CDN 访问边缘节点,边缘节点通过专线与数据中心通信;
  • 去掉 CDN 服务提供商,通过边缘节点与用户直接接入;(还需要一些安全加固,例如 DDoS

不同方案的响应时间对比

image-20240126170306668

通过方案转换,访问速度提高 100ms

Reference

Enterprise-Class Availability, Security, and Visibility for Apps