一、问题来了:Pod挤爆时CRI-O为啥“罢工”

1.1 啥是CRI-O假死?

你可以把CRI-O理解成K8s集群里管容器的“大管家”——当有新Pod要启动时,得找它申请资源、拉镜像、开容器。所谓假死,就是这个大管家突然“懵了”:要么收不到新的Pod启动请求,要么处理请求慢到超时,导致Pod一直卡在“等待调度”状态,没法真正运行,严重时还会让整个节点的容器服务瘫痪。

1.2 什么时候会触发这个问题?

最典型的场景就是业务猛增的时候:比如促销活动前突然涌入几百个Pod,集群调度时不小心把所有新Pod都塞到同一个节点上,这个节点的CRI-O就会被海量的启动请求“淹没”,忙到宕机。还有一种情况是节点资源被抢疯了:CPU、内存、磁盘IO全被现有Pod占满,CRI-O连自己运行的资源都拿不到,自然就“罢工”了。

二、先搞懂为啥崩:CRI-O假死的核心原因

2.1 资源抢疯,CRI-O扛不住

每个节点的CPU、内存、磁盘都是有限的,当Pod密集调度时,大量容器同时启动会疯狂抢占资源,CRI-O作为中间件本身也需要资源来处理请求,一旦它的资源被完全抢占,就没法响应新的Pod请求,出现假死。比如节点8核16G,现有Pod已经占了7核15G,剩下的资源连CRI-O自己的基础运行都不够,自然就卡了。

2.2 调度逻辑和容器启动的冲突

K8s的调度逻辑是尽量把Pod均匀分配,但如果没做好优化,比如大量标签相同的Pod都被调度到同一个节点,CRI-O要同时处理上百个容器创建请求,而它本身默认没做流量限流,请求队列溢出后就会超时,表现为假死。

三、实战:我遇到过的同款问题,怎么修?

3.1 先排查是不是真的CRI-O假死

遇到问题别乱改配置,先确认是不是CRI-O的锅。这里用Kubernetes 1.27 + CRI-O 1.27的标准集群做示例,直接看日志和进程状态:

# 1. 检查CRI-O进程是否正常,以及它的资源占用情况
pidof crio && ps aux | grep crio | head -10
# 2. 查看CRI-O最近10分钟的日志,找有没有超时、连接错误
journalctl -u crio -n 100 --since "10 minutes ago"
# 3. 看kubelet的日志,找Pod调度时的超时提示(比如"context deadline exceeded")
journalctl -u kubelet -n 50 | grep -i "timeout\|cri-o\|pod"

如果日志里出现大量“无法连接CRI-O”“容器创建超时”,同时CRI-O进程是存在的,那基本可以确认是假死问题。

3.2 第一招:给CRI-O加个“缓冲带”(限流)

CRI-O默认的并发处理能力比较保守,我们可以手动调整它的配置文件,增加同时处理容器请求的上限,避免请求溢出。配置文件是/etc/crio/crio.conf,修改这几个关键参数:

[crio.runtime]
# 最大同时创建容器的数量,默认是10,我们根据节点核心数调到20(每核心2-3个比较合适)
concurrent_container_restores = 20
# 容器创建的超时时间,默认30秒,改成60秒,给CRI-O更多缓冲时间
container_creation_timeout = "60s"
[crio.network]
# 调整网络队列的缓冲,避免密集创建容器时网络请求阻塞
network_plugin_dir = "/usr/libexec/cni"

这个方法的优点是快速缓解瞬间压力,缺点是超时时间变长可能会让部分Pod的启动时间延迟,注意别把并发数调太高——比如调到100的话,反而会让更多容器同时抢资源,更卡。

3.3 第二招:给节点的核心资源“划红线”(资源预留)

节点上不仅有Pod,还有系统组件和CRI-O本身需要资源,我们可以在K8s的kubelet配置里,给这些“必要服务”预留一部分CPU和内存,这样就算Pod挤爆,CRI-O也能拿到基础资源干活。修改kubelet的配置文件/var/lib/kubelet/config.yaml

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
# 预留1核CPU,1Gi内存给系统和CRI-O
reservedSystemCPUs: "1"
reservedSystemMemory: "1Gi"
# 开启资源限制,保证预留的资源不会被Pod抢
enforceNodeAllocatable: ["pods", "system-reserved", "kube-reserved"]

这个方法的优点是从底层避免CRI-O被饿死,缺点是预留的资源不能给业务Pod用,会浪费一点,注意预留量要根据节点总资源来——比如总节点是4核8G,预留1核1G就够,别预留太多(比如留2核2G),这样业务能用的资源就少了。

3.4 第三招:分散压力,别让一个节点扛所有(调度优化)

从根源解决问题的方法,就是不让大量Pod挤到同一个节点上。给Pod加反亲和性规则,让相同业务的Pod尽量分布在不同节点,这样单个节点的CRI-O压力就会小很多。写一个Pod的示例配置,加反亲和性:

apiVersion: v1
kind: Pod
metadata:
  name: test-online-shop
  labels:
    app: order-service # 这个标签用来标记相同业务的Pod
spec:
  # 反亲和性规则:这个Pod尽量不要和同app的Pod在同一个节点
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values: ["order-service"]
        topologyKey: "kubernetes.io/hostname"
  containers:
  - name: order-container
    image: nginx:alpine
    resources:
      # 给每个Pod申请固定的小资源,避免单个Pod占太多
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "200m"
        memory: "256Mi"

这个方法的优点是从根源减少单个节点的CRI-O压力,缺点是需要提前规划业务的Pod分布,注意反亲和性的规则别写太严——比如别加“必须不在任何同app节点”,不然可能会导致Pod调度失败,没法运行。

四、避坑指南:这些操作别乱做

4.1 别随便调大CRI-O的并发数

刚才说的concurrent_container_restores,很多人会直接调到100,这是错的——并发数太高会让更多容器同时抢CPU、内存,反而会加剧资源竞争,CRI-O会更卡。正确的做法是根据节点核心数调,比如8核节点调到20-25就够。

4.2 别把所有资源都留给Pod

很多运维会觉得“节点的资源都是给业务Pod用的”,关掉资源预留,结果CRI-O被挤得没资源,真的假死的时候连排障都没法做,比如连日志都写不了。至少要预留5%-10%的资源给系统和CRI-O。

4.3 别忽略磁盘IO的占用

密集创建容器会大量写入镜像层,导致节点磁盘IO满,就算CPU、内存够,CRI-O也没法写新的容器文件,照样会假死。要定期清理不用的镜像,用这个命令:

# 清理所有未被使用的镜像,释放磁盘空间
crictl rmi $(crictl images -q) --prune
# 检查磁盘是否满了,尤其是/var/lib/containers(CRI-O的镜像存储目录)
df -h /var/lib/containers

五、总结:稳定性兜底的核心思路

遇到Pod密集调度导致的CRI-O假死,别慌,按照“先排查原因→限流缓冲→资源保底→分散压力→日常维护”的步骤来:先确认是不是CRI-O的问题,再调整它的限流配置保障请求不溢出,给核心资源留兜底的空间,用调度优化减少单个节点的压力,最后定期清理镜像避免磁盘满。这套组合拳下来,大概率能解决假死问题,就算遇到突发流量,也能保证容器服务不会轻易罢工,给业务留够稳定的兜底空间。