作者: ningniu

  • Systemd 的坑:老子当年怎么就没发现这玩意这么能瞎JB优化?

    今天开会,又TM是一堆PPT。讲什么微服务架构,云原生最佳实践。我呸!最佳实践?连Systemd都没搞明白,玩个屁的云原生!一个个就知道抄概念,真要出了问题,抓瞎!
    就说Systemd这玩意,启动管理是方便了不少,但是后面加的那些花里胡哨的功能,简直就是瞎JB优化!尤其是那些什么资源限制,自动重启,网络配置,没一个省心的!稍微配置不对,就给你挖个坑,让你跳进去爬都爬不出来。
    就说我前几天遇到的一个破事。有个服务,用Systemd管理,设置了Restart=on-failure。正常情况下,服务挂了自动重启,看起来挺好。结果呢?这SB玩意,服务一挂,它疯狂重启,每次重启都失败,然后疯狂打印日志,直接把磁盘给干满了!你TM倒是给个重启次数限制啊!
    最后排查,发现是代码里有个空指针异常,导致服务直接core dump。这TM是代码的问题,没错。但是Systemd你能不能聪明点?你TM要是检测到连续重启失败超过三次,就应该直接停止重启啊!非得把磁盘干满才罢休?
    还有那些个什么LimitCPULimitMemory,设置起来倒是挺方便,但是你TM知道背后的实现原理吗?这玩意实际上就是cgroups!cgroups!你没听错,就是Linux内核里的cgroups!但是Systemd把这些东西封装的太好了,让你感觉好像很简单,点点鼠标就能搞定。但是一旦出了问题,你想debug,想深入理解,就TM懵逼了!
    我当年刚开始玩Linux的时候,哪有这些破玩意!那时候启动脚本都是手写的,一行一行敲出来的。虽然麻烦,但是每一个细节都清清楚楚。现在倒好,年轻人一个个只会用Systemd,出了问题就只会重启重启再重启!
    就拿配置来说,Systemd的unit文件,看着挺简洁,但是背后藏着一堆坑。比如这个Type=simpleType=forkingType=oneshot,你TM知道这些类型到底有什么区别吗?你TM知道Type=forking的服务,Systemd是怎么判断服务是否启动成功的吗?
    告诉你,Type=forking的服务,Systemd会根据PID文件来判断服务是否启动成功。如果服务启动后没有创建PID文件,或者PID文件里的PID不是当前进程的PID,Systemd就会认为服务启动失败,然后就开始TM的重启!
    所以,如果你用Type=forking,你的服务启动脚本一定要正确创建PID文件!否则,等着被Systemd坑死吧!
    举个例子,下面是一个典型的Type=forking的unit文件:

    [Unit]
    Description=My Awesome Service
    After=network.target
    [Service]
    Type=forking
    PIDFile=/var/run/my-awesome-service.pid
    ExecStart=/usr/local/bin/my-awesome-service start
    ExecStop=/usr/local/bin/my-awesome-service stop
    Restart=on-failure
    [Install]
    WantedBy=multi-user.target
    

    看到了吗?PIDFile这行至关重要!如果你的/usr/local/bin/my-awesome-service start脚本没有正确创建/var/run/my-awesome-service.pid文件,Systemd就会认为服务启动失败,然后就开始疯狂重启!
    还有那些个什么ExecStartPreExecStartPostExecStopPreExecStopPost,看着挺花哨,但是用不好就是给自己挖坑。比如,你可以在ExecStartPre里做一些初始化工作,但是如果ExecStartPre执行失败了,Systemd会直接停止启动服务,但是不会告诉你具体原因!你只能去看日志,一行一行排查,简直TM浪费时间!
    所以,我TM说,这些新技术,新工具,用起来是方便,但是一定要理解背后的原理。不要只会用,不会修。否则,等着被这些破玩意坑死吧!
    最后,再补充一句,Systemd的日志系统也是个坑。默认情况下,Systemd会将所有日志都写入到二进制日志文件里。你想查看日志,必须用journalctl命令。这TM简直反人类!我只想用tail -f /var/log/mylog.log来看日志不行吗?非得用journalctl,还要记住一堆参数,简直就是折磨!
    所以,我一般都会把Systemd的日志配置成同时写入到文本文件里,方便我用传统的工具来查看日志。
    修改/etc/systemd/journald.conf文件,设置:

    Storage=persistent
    SystemMaxFileSize=100M
    ForwardToSyslog=yes
    

    然后重启systemd-journald服务:

    systemctl restart systemd-journald
    

    这样,Systemd就会将日志同时写入到/var/log/syslog或者/var/log/messages里,方便你用tailgrep等命令来查看日志。
    总之,Systemd这玩意,用好了是神器,用不好就是坑。年轻人,多学点底层原理,少抄点概念。否则,等着被这些破玩意坑死吧!

  • TMD,K8S Ingress 流量转发给我整出幺蛾子,深挖一下 conntrack 这坨屎

    刚他妈开完会,一帮产品经理和不懂技术的领导在那儿扯淡,什么用户体验,什么快速迭代,迭代你奶奶个腿!出问题还不是老子来擦屁股?就说昨天晚上,K8S 集群里的一个 Ingress,流量突然就断了,监控告警刷屏一样。查了半天,发现是 conntrack 表满了!
    Conntrack 这玩意儿,说白了就是 Linux 内核用来跟踪 TCP 连接状态的。在 NAT 环境下,它尤其重要。因为 Ingress Controller 作为流量入口,通常会做 SNAT,把客户端 IP 转换成自己的 IP 去访问后端服务。没有 conntrack,内核就不知道哪个响应包该发给哪个客户端,整个连接就乱套了。
    现在 K8S 动不动就上千个 Pod,每个 Pod 后面可能还有成百上千的连接。Conntrack 表默认大小就那么点儿,稍微有点并发就满了。更操蛋的是,现在这帮年轻人,一上来就搞什么 Service Mesh,Envoy 代理满天飞,每经过一个代理,就多一层 NAT,conntrack 的压力就更大。他们懂个屁!就知道抄概念,底层的玩意儿一概不关心。
    好了,废话不多说,直接上排查步骤和解决方案。
    一、排查步骤
    1. 确认 conntrack 表是否满了:
    bash
    sysctl net.netfilter.nf_conntrack_count
    sysctl net.netfilter.nf_conntrack_max

    如果 nf_conntrack_count 接近或等于 nf_conntrack_max,那就是满了。
    2. 查看 conntrack 的丢包情况:
    bash
    ss -s

    看输出里的 TCP establishedTCP orphanedTCP timewait 等指标。如果 TCP timewait 数量异常高,说明 TIME_WAIT 状态的连接太多,占用了 conntrack 的资源。
    3. 抓包分析:
    bash
    tcpdump -i any -n -nn -vvv -s 0 port 80 or port 443

    抓包看看是不是有大量的 SYN 包,或者 RST 包。如果是 SYN 包,说明有大量的连接请求被拒绝;如果是 RST 包,说明连接被异常关闭。这两种情况都会导致 conntrack 资源快速消耗。
    二、解决方案
    1. 调整 conntrack 表大小:
    bash
    sysctl -w net.netfilter.nf_conntrack_max=262144
    sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600

    nf_conntrack_max 调大一点,但也不能太大,否则会占用大量的内存。nf_conntrack_tcp_timeout_established 可以适当调小,缩短 ESTABLISHED 状态连接的超时时间,释放 conntrack 资源。
    注意: 这个只是临时生效,重启机器就没了。要永久生效,需要修改 /etc/sysctl.conf 文件,然后执行 sysctl -p
    net.netfilter.nf_conntrack_max = 262144
    net.netfilter.nf_conntrack_tcp_timeout_established = 3600

    2. 优化 TCP 连接:
    * 启用 TCP Fast Open (TFO): 可以减少 TCP 连接建立的时间,降低 SYN 包的重传率。
    bash
    sysctl -w net.ipv4.tcp_fastopen=3

    同样,要永久生效,需要在 /etc/sysctl.conf 文件中添加 net.ipv4.tcp_fastopen = 3
    * 调整 TIME_WAIT 状态的连接: TIME_WAIT 状态的连接过多,会占用大量的 conntrack 资源。可以通过以下参数进行调整:
    bash
    sysctl -w net.ipv4.tcp_tw_reuse=1
    sysctl -w net.ipv4.tcp_tw_recycle=1

    注意: tcp_tw_recycle 在 NAT 环境下可能会有问题,不建议开启。
    3. 优化 Ingress Controller 配置:
    * 合理设置健康检查: 频繁的健康检查会产生大量的连接,增加 conntrack 的压力。
    * 使用连接池: Ingress Controller 可以使用连接池来复用 TCP 连接,减少连接建立和关闭的开销。
    4. 使用 eBPF 进行 conntrack 优化 (高级玩法,慎用):
    eBPF 允许你在内核中运行自定义代码,可以用来优化 conntrack 的行为。例如,你可以使用 eBPF 来快速删除不活跃的连接,或者根据特定的策略来选择是否跟踪某个连接。但这玩意儿调试起来很麻烦,一不小心就把内核搞崩了。
    总结
    Conntrack 满了,是个很常见的 K8S 问题。解决思路就是:要么增加 conntrack 表的大小,要么减少 conntrack 表的占用。但是,治标不治本。根本的解决方案还是要优化你的应用程序和网络架构,减少不必要的连接,提高连接的复用率。
    现在的年轻人,总想着搞一些花里胡哨的东西,Service Mesh、Serverless,搞得越来越复杂。底层的这些东西,还是要好好学学,否则,出了问题,只能抓瞎。
    TMD,说了这么多,还是解决不了这帮 SB 搞出来的烂摊子。老子要下班!

  • TMD,K8S这堆YAML,老子今天非得给你扒层皮——深挖Containerd CRI的Socket死锁

    早上开会,又听那帮产品经理在那儿吹逼,什么云原生、微服务,叨逼叨个没完,真想把他们的脑浆子抠出来看看是不是浆糊。一个个连DockerFile都不会写,还他妈云原生架构,原生个JB!
    行,既然他们这么喜欢K8S,那我就来好好聊聊这玩意儿,特别是Containerd这块儿。别以为写几个YAML就能上天了,底层的东西,你们这帮小年轻根本没概念。
    今天就说说Containerd CRI的Socket死锁。这玩意儿,说白了,就是Containerd在处理CRI请求的时候,Socket卡住了,导致Pod起不来,应用挂逼。别跟我说重启Containerd,重启能解决问题,还要我这老家伙干嘛?
    这事儿的根源往往在于Containerd处理CRI请求的逻辑不够健壮,特别是在并发高,或者网络环境复杂的情况下,容易出现Socket资源竞争,最终导致死锁。具体现象就是`ctr task exec`、`kubectl exec`之类的命令卡住,Pod状态一直是`ContainerCreating`或者`CrashLoopBackOff`。
    要排查这玩意儿,首先得看Containerd的日志。`/var/log/containerd/containerd.log`,瞪大你的狗眼,看看里面有没有类似下面的错误信息:
    “`
    ERRO[2024-10-27T10:00:00.000000000+08:00] failed to serve request error=”rpc error: code = Unavailable desc = connection error: desc = \”transport: Error while dialing: dial unix /run/containerd/containerd.sock: connect: connection refused\””
    “`
    这玩意儿,就是Containerd的Socket通信出了问题。要么是Containerd进程挂了,要么就是Socket连接被占满了。
    接下来,得祭出神器`strace`。这玩意儿能追踪进程的系统调用,是排查底层问题的利器。
    “`bash
    strace -p $(pidof containerd) -s 2048 -f -o containerd.strace
    “`
    这条命令会跟踪Containerd进程的所有系统调用,并将结果输出到`containerd.strace`文件中。然后,用你那可怜的脑子分析这个文件。重点关注`accept`、`send`、`recv`、`close`等Socket相关的系统调用。看看有没有长时间阻塞在某个调用上的情况。
    比如,你可能会发现大量的线程都阻塞在`accept`调用上,这意味着Containerd没有足够的线程来处理新的连接请求。这可能是由于线程池配置不合理,或者某些请求处理时间过长导致的。
    解决这个问题,可以尝试以下几种方法:
    1. **调整Containerd的配置**。在`/etc/containerd/config.toml`中,可以调整`grpc.max_concurrent_streams`参数,增加并发处理能力。
    “`toml
    [grpc]
    max_concurrent_streams = 1000 # 默认是500,可以适当增加
    “`
    重启Containerd生效。
    2. **检查网络环境**。确保Containerd和kubelet之间的网络通信正常。如果使用了网络插件,也要检查网络插件的配置是否正确。
    3. **优化应用代码**。如果某些请求处理时间过长,可能是由于应用代码存在性能问题。需要对应用代码进行优化,减少请求处理时间。
    还有一种情况,就是Socket文件被其他进程占用了。可以用`lsof`命令来查看:
    “`bash
    lsof /run/containerd/containerd.sock
    “`
    如果发现有其他进程占用了这个Socket文件,那就把那个进程干掉。
    最后,如果以上方法都无效,那就只能祭出终极大法:**升级Containerd版本**。新版本通常会修复一些已知的Bug,并优化性能。
    总之,解决Containerd CRI的Socket死锁问题,需要深入理解Containerd的底层原理,熟练掌握各种排查工具,以及具备丰富的实战经验。别指望百度一下就能解决问题,真正的技术,是靠积累和思考得来的。
    TMD,写完这篇文章,感觉又老了十岁。这帮小年轻,啥时候才能真正理解底层技术的重要性?天天只会玩YAML,迟早要出大事!

  • Docker容器中使用Nvidia GPU

    在容器环境中使用物理机的Nvidia GPU,宿主机自身需要安装好显卡驱动,还要结合使用官方提供的Toolkit,创建新的容器运行时,并在启动容器时使用对应的运行时环境。以下是在Ubuntu 22.04系统中,docker容器使用物理机显卡的简单配置过程.

    Bash
    nvidia-smi -L
    # 物理机上执行,检测显卡是否成功驱动

    参考:https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

    1. 配置APT仓库
    Bash
    curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
      && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
        sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
        sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
    sudo apt-get update
    1. 安装软件包
    Bash
    sudo apt-get install -y nvidia-container-toolkit
    1. docker容器运行时配置
    Bash
    sudo nvidia-ctk runtime configure --runtime=docker
    sudo systemctl restart docker
    1. 测试
    Bash
    docker run -it --rm --gpus=all ubuntu:22.04 nvidia-smi -L

    附:docker配置文件(/etc/docker/daemon.json)参考

    执行nvidia-ctk runtime configure –runtime=docker时会往配置中文件中写入相应的runtime配置

    JSON
    {
        "default-ulimits": {
            "memlock": {
                "Hard": -1,
                "Name": "memlock",
                "Soft": -1
            }
        },
        "exec-opts": [
            "native.cgroupdriver=cgroupfs"
        ],
        "runtimes": {
            "nvidia": {
                "path": "nvidia-container-runtime",
                "runtimeArgs": []
            }
        },
        "bridge": "none",
        "iptables": false,
        "live-restore": true
    }