【深度解析】详细梳理Kubernetes的网络模型,总结网络故障排查核心思路

本文旨在梳理网络模型,总结出通用并且高可行性的故障排查思路,并且能通过自动化检测减少中大规模集群的手动排查工作。

1
默认读者已熟悉四层/七层网络模型,相关概念不再赘述

一、Linux中的基础网络技术

这里只会提及相关的Linux指令,不深入技术原理,只会一笔带过,不然文章会很冗长。

1. Network namespace

我们知道两个POD的网络相互隔离,实际在操作系统中是通过命名空间实现的。

Network namespace用于支持网络协议栈的多个实例。通过对网络资源的隔离,就能在一个宿主机上虚拟出多个不同的网络环境。docker利用NS实现了不同容器的网络隔离。 Network namespace可以提供独立的路由表和iptables来设置包转发、nat以及ip包过滤等功能,提供完整且独立的协议栈。

1
2
3
4
## 创建一个新的网络命名空间
sudo ip netns add my_namespace
## 进入my_namespace的内部 shell 界面
sudo ip netns exec my_namespace bash

2. veth设备对

那如何我们如何为两个不同命名空间下的进程之间实现通信呢?

可以通过引入Veth设备对,Veth设备都是成对出现的,其中一端成为另一端的peer,在Veth设备的一端发送数据时,会将数据发送到另一端,并触发接收数据的操作。

Read more...

【问题大排查】通用Linux环境的网络排障(部分包含容器环境)

因为最近一段时间,一直在处理各种网路问题,所以痛定思痛从头梳理一般运维环境下的网络状况

网络不通(持续性)

这个情况没啥好说的,只要不是 DNS 问题,就是服务器挂掉或者端口被禁

端口监听挂掉

如果容器内的端口已经没有进程监听了,内核就会返回 Reset 包,客户端就会报错连接被拒绝,可以进容器 netns 检查下端口是否存活:

1
netstat -tunlp

iptables 规则问题

检查报文是否有命中丢弃报文的 iptables 规则:

1
2
3
4
5
iptables -t filter -nvL
iptables -t nat -nvL
iptables -t raw -nvL
iptables -t mangle -nvL
iptables-save
Read more...

【生产问题】K8s退出信号处理和僵尸进程问题

接上一篇容器多进程的内容延伸到僵尸进程,也是一个真实的生产问题

  1. 公司有大量的Python + Selenium爬虫服务,据开发所说一个服务有很多个并行任务
  2. 一天早上告警类似Resource temporarily unavailable的错误,对于这类问题其实只需根据ulimit -a查看各项资源即可
  3. 因为确实部分资源使用率指标,所以只能在宿主机查看缺失的资源利用情况,如果只关心进程数直接ps -aux | wc -l
  4. 僵尸进程对于多进程服务来说是常有的事,但需要通过一些自动化手段帮助k8s清理宿主机僵尸进程

一、什么是僵尸进程

通常来说就是,在 Unix-like 操作系统中已经完成执行(终止)但仍然保留在系统进程表中的进程记录。 这种状态的进程实际上已经停止运行,不占用除进程表外的任何资源,比如CPU和内存, 但它仍然保留了一个PID和终止状态信息,等待父进程通过调用wait()waitpid()函数来进行回收。

1. 生命周期

  • 子进程执行完毕后,会发送一个SIGCHLD信号给父进程,并变为僵尸状态。

  • 父进程通过wait()waitpid()读取子进程的终止状态,此时操作系统会清理僵尸进程的记录,释放其PID供其他进程使用。

Read more...

【生产问题】在容器中运行多进程服务OOMKilled未能被K8s检测识别的解决方案

这是两个月前公司的图片AI训练模型集群出现的一个生产问题,是这样的:

well-known, Python项目因为GIL普遍使用多进程代替多线程,使得container中存在1号进程之外的其他进程。

  1. 算法组的同学曾在群里反馈模型服务并没有问题,但多次跑出来的数据有缺失
  2. 开始运维方任务是算法代码问题,并没有在意,但随手发现相关的Pod内存曲线有断崖下降并且没有再回升
  3. 直觉告诉内部有进程挂了,在算法同学允许下重跑了一边服务,ps aux命令观察了一下果然若干小时候被强退,预计OOMKilled了
  4. 但主要问题是,监控系统并没有抓取到这一事件,无法发出OOMKilled告警

一、container以及Pod的状态

1. container的异常指标

总所周知,这个异常指标可以用过kube-state-metrics获得

kube_pod_container_status_terminated_reason{ container="nginx",  namespace="default", node="xxxx", pod="nginx-dep-123", reason="OOMKilled", service="kube-state-metrics"}

解读一下:意思是pod nginx-dep-123中的某个容器 nginx 的状态是terminated,并且它进入terminated状态的reason原因是因为OOMKilled

值得注意的是,kubectl get展示的status即可能是容器也可能是pod的状态。

具体可以参考这两个官方文档容器状态Pod阶段

容器状态只有三种:

  • Waiting(等待)处于Waiting状态的容器仍在运行它完成启动所需要的操作:例如从某个容器镜像仓库拉取容器镜像,或者向容器应用Secret数据等等
  • Running(运行中) 状态表明容器正在执行状态并且没有问题发生
  • Terminated(已终止) 处于 Terminated 状态的容器已经开始执行并且或者正常结束或者因为某些原因失败。

kubectl get打印的源码可以在kubernetes\pkg\printers\internalversion\printers.go这里看printPod()方法

2. containerd如何获取容器状态的

我们都知道的Pod状态均来自于CRI,kubelet的pleg会通过cri接口获取containerd的状态信息,pleg是个大坑回头有精力可以讲。

可以直接定位到pod.Status.Reason获取的位置kubernetes\pkg\kubelet\pleg\generic.go

 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
func (g *GenericPLEG) updateCache(ctx context.Context, pod *kubecontainer.Pod, pid types.UID) (error, bool) {
	if pod == nil {
		klog.V(4).InfoS("PLEG: Delete status for pod", "podUID", string(pid))
		g.cache.Delete(pid)
		return nil, true
	}

	g.podCacheMutex.Lock()
	defer g.podCacheMutex.Unlock()
	timestamp := g.clock.Now()

	// 这里是pleg的非常重的逻辑就不展开了
	// 1. 用m.runtimeService.PodSandboxStatus获取sandbox的网络容器状态
	// 2. 再通过m.getPodContainerStatuses(uid, name, namespace)获取业务容器状态
	//    a. 这里回去调用对应的CRI GRPC接口,即(r *remoteRuntimeService) ContainerStatus(containerID string)
	// 3. 最后拼装为&kubecontainer.PodStatus
	status, err := g.runtime.GetPodStatus(ctx, pod.ID, pod.Name, pod.Namespace)
	if err != nil {
	} else {
		// ...
		status.IPs = g.getPodIPs(pid, status)
	}
	// ...

	return err, g.cache.Set(pod.ID, status, err, timestamp)
}
Read more...

【基础知识】Docker镜像存储原理及其优化方式

本文旨在通过Dockerfile的指令展开Docker镜像的原理,不再介绍Docker是什么了。

一、核心指令及其增强语法

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
# FROM 每个 Dockerfile 都必须以 FROM 指令开始
FROM ubuntu:20.04
# RUN 执行命令并将结果打包到镜像中,通常用于安装软件或执行某些配置操作
RUN apt-get update && apt-get install -y nginx
# CMD 指定容器启动时默认执行的命令。不同于 RUN,CMD 只在容器启动时执行。
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT 设置容器启动时要执行的主命令。与 CMD 类似,但更适合于设置不可更改的启动命令。
# 一个Dockerfile只能有一个ENTRYPOINT,出现多个时,前面会被最后一个所覆盖
# 通常来说可以和CMD配合使用,如
# ENTRYPOINT ["nginx"]
# CMD ["-g", "daemon off;"]
# 这样启动的时候,可以通过 docker run my_image -g "daemon on;" 这种方式覆盖CMD同时保留默认启动参数的效果
ENTRYPOINT ["nginx", "-g", "daemon off;"]

WORKDIR /app

# COPY 从构建主机将文件或目录复制到镜像中
# ADD 与 COPY 类似,但可以处理本地 tar 文件的自动解压以及 URL 的下载
COPY . /app
ADD https://example.com/app.tar.gz /app

ENV APP_VERSION 1.0
# 声明容器的外部端口,但不自动发布端口
EXPOSE 80
# VOLUME 声明挂载点,将容器的数据目录映射到宿主机目录或其他容器。
# 即使不使用该指令,在docker run启动时,仍然可以使用-v或--mount进行挂载
# docker run -v <宿主机目录>:<容器内目录> <镜像名>
# docker run --mount type=bind,source=<宿主机目录>,target=<容器内目录> <镜像名>
VOLUME ["/data"]

# ONBUILD 用于定义`延迟执行`的构建指令,即当该镜像被用作基础镜像来构建其他镜像时才会执行。一般情况下,它不会在构建当前镜像时触发。
# 举我用到的场景:
# 1. 同步私有仓库管理的config文件,可能有因为不同项目或者语言导致相同的配置数据出现异构
# 2. 安装依赖包搭建编译环境
# 一般来说都是在构建派生镜像时需要拉取最新配置或者依赖包时,可以通过ONBUILD降低派生镜像Dockerfile的复杂度
ONBUILD COPY . /app
ONBUILD RUN cd /app && npm install

上面命令基本足够编写一个复杂的镜像构建脚本,但个别有一些高级选项,可以继续研究一下

2.基础指令上高阶用法

RUN其实有两种缓存layer的方式,分别是

1
2
3
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

RUN --mount=type=bind,source=/path/to/local/.m2,target=/root/.m2 mvn install

COPYADD都有一个link选项,用于让Docker在将文件或目录从宿主机复制到镜像时,创建硬链接而不是直接复制文件。

在普通Web项目里面很少用到,但在AI训练机器上会很常见,例如这里Depot AI。 因为AI的数据集非常庞大,不可能完全复制到镜像当中,这会导致构建的镜像过大而占用制品库空间,可以通过link的形式将文件链接到镜像的文件系统中。

1
2
3
4
FROM python:3.10

COPY --link --from=depot.ai/runwayml/stable-diffusion-v1-5 /v1-inference.yaml .
COPY --link --from=depot.ai/runwayml/stable-diffusion-v1-5 /v1-5-pruned.ckpt .

二、镜像构建原理,Layers之间的关系

我觉得需要搞明白一件事情就是,Docker不是VM,任意基础镜像都不存在安装一个完整的操作系统。

docker-image.png

Read more...

【生产问题】如何将传统运维环境服务优雅地迁移至Kubernetes集群从而实现全量容器化

最近尝试着面试几家公司,偶尔会被问到传统环境如何向Kubernetes迁移的方案。

坦白说,其实这方面并不缺简单可行性高的方案,我就以屈臣氏中国的迁移方案为例,给访问本博客的同行借鉴一下。

环境的迁移,迁移的是什么?

毋庸置疑,只要外网请求全量并正常地访问Kubernetes环境,我们就可以认为实现了容器化。

流量导入可能还不够,有的公司可能想实现全面云原生,持久层也想迁移进来,涉及到数据库如何尽最大可能无缝迁移。

流量迁移

我这里直接按照阿里云传统的ECS环境迁移到自建K8s环境为例

Read more...

【问题小解决】无需重启,基于Prometheus对Pod进行垂直扩缩容

通常情况下,要修改 Pod 的资源定义,是需要重启 Pod 的。 在Kubernetes 1.27中,有一个 Alpha 状态的InPlacePodVerticalScaling开关,开启这一特性, 就能在不重启 Pod 的情况下,修改 Pod 的资源定义。

要使用这个功能,需要在kube-apiserverfeatureGates中显式地设置启用,启用这一特性之后,就可以进行测试了。

测试一下

假设下面的 Pod 定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
  name: stress
spec:
  containers:
  - name: stress
    image: myimages/stress-ng:latest
    resizePolicy:
    - resourceName: cpu
      restartPolicy: NotRequired
    - resourceName: memory
      restartPolicy: RestartContainer    
    command: ["sleep", "3600"]
    resources:
      limits:
        cpu: 200m
        memory: 200M
      requests:
        cpu: 200m
        memory: 200M

可以看到,spec 中加入了resizePolicy字段,用来指定对 CPU 和内存的扩缩容策略。内容很直白:

  • CPU 的扩缩容策略是NotRequired,即不重启 Pod;
  • 内存的扩缩容策略是RestartContainer,即重启 Pod。
Read more...

【CKA专题】梳理RBAC权限构成,并创建一个用户及其权限

为一个开发人员配置客户端证书

首先,为用户生成私钥和证书签名请求(CSR),然后使用Kubernetes集群的CA来签署证书

使用 OpenSSL 生成私钥和证书签名请求(CSR):

1
2
3
4
5
# 生成用户的私钥
openssl genrsa -out user.key 2048

# 生成CSR
openssl req -new -key user.key -out user.csr -subj "/CN=username/O=groupname"

在这里:

  • /CN=usernameusername表示用户的名字
  • /O=groupnamegroupname表示用户所在的组,可以帮助后续RBAC授权(例如管理员、开发者等不同组)

使用 Kubernetes 集群的 CA 签署 CSR,生成用户证书:

1
openssl x509 -req -in user.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out user.crt -days 365
  • -CA /etc/kubernetes/pki/ca.crt 和 -CAkey /etc/kubernetes/pki/ca.key:指向 Kubernetes 集群的CA证书和CA密钥,用于签署用户的CSR
  • -out user.crt:生成的用户证书

配置 kubeconfig 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
kubectl config set-cluster kubernetes-cluster \
  --certificate-authority=ca.crt \
  --server=https://<API_SERVER_IP>:<PORT> \
  --kubeconfig=$KUBECONFIG_FILE
## 添加用户的证书和私钥
kubectl config set-credentials username --client-certificate=user.crt --client-key=user.key --kubeconfig=$KUBECONFIG_FILE
## 为用户设置一个使用上下文,以便访问集群
kubectl config set-context username-context --cluster=kubernetes-cluster --namespace=default --user=username --kubeconfig=$KUBECONFIG_FILE
## 切换到新创建的上下文
kubectl config use-context username-context --kubeconfig=$KUBECONFIG_FILE

将文件给到开发同学即可

Read more...

【问题小解决】如何解决多容器下修改hosts失效

问题现象

业务容器启动的逻辑中,修改了/etc/hosts文件,当 Pod 只存在这一个业务容器时,文件可以修改成功, 但存在多个时 (比如注入了 istio 的 sidecar),修改可能会失效。

分析

  1. 容器中的/etc/hosts是由 kubelet 生成并挂载到 Pod 中所有容器, 如果 Pod 有多个容器,它们挂载的/etc/hosts文件都对应宿主机上同一个文件, 路径通常为/var/lib/kubelet/pods/<pod-uid>/etc-hosts

如果是 docker 运行时,可以通过docker inspect <container-id> -f {{.HostsPath}}查看。

  1. kubelet 在启动容器时,都会走如下的调用链(makeMounts->makeHostsMount->ensureHostsFile)来给容器挂载 /etc/hosts, 而在 ensureHostsFile 函数中都会重新创建一个新的 etc-hosts 文件,导致在其他容器中对 /etc/hosts 文件做的任何修改都被还原了。

所以,当 Pod 中存在多个容器时,容器内修改 /etc/hosts 的操作可能会被覆盖回去。

解决方案

使用 HostAliases

如果只是某一个 workload 需要 hosts,可以用 HostAliases:

Read more...

【问题小解决】解决容器内时区不一致问题

业务程序在使用时间的时候(比如打印日志),没有指定时区,使用的系统默认时区, 而基础镜像一般默认使用 UTC 时间,程序输出时间戳的时候,就与国内的时间相差 8 小时。

游戏全球对战服场景这个问题更要命,开发需要通过时间计算同步帧的偏移量,如果这个时候还要重新计算时区是非常浪费计算资源的(-_-)

方案一:指定 TZ 环境变量

很多编程语言都支持TZ这个用于设置时区的环境变量,可以在部署工作负载的时候,为容器指定该环境变量,示例:

Read more...