• 运维事故:消失的Pod,和那个错得离谱的YAML

    今天差点因为一个YAML文件把我的血压送走。你说这年头,K8S用了这么久,居然还能有人犯这种低级错误,真让人怀疑他是不是把YAML当成了薛定谔的猫,写之前都不知道里面是个什么东西。
    事情是这样的,早上接到监控告警,一个关键业务的Pod时不时地消失,然后又自动拉起来,就像得了间歇性失忆症。开始我还以为是节点资源不足,或者OOM Killer又出来搞事情了。结果登上去一看,CPU、内存都稳得很,日志里也没有OOM的痕迹。
    这就有点意思了。Pod自己反复重启,那肯定得查探一下Deployment或者StatefulSet的配置。结果不看不知道,一看吓一跳。这位“大神”写的YAML文件,简直就是行为艺术。
    我先不说他缩进乱得像狗啃的一样,光是那个livenessProbereadinessProbe的配置,就能让人原地去世。他大概是觉得健康检查不重要,直接把initialDelaySeconds设置成了3600秒,periodSeconds设置成了7200秒。
    好家伙,这是要让Pod启动一个小时之后再开始检查,而且每两个小时才检查一次? 这Pod要是真出了问题,估计早就被用户骂上天了,还等着你来检查?更离谱的是,他还把failureThreshold设置成了9999。这是什么概念?意思是这个Pod得挂掉近一万次,才会被认为是不健康的?
    我当时就想问问他,是不是觉得Pod有九条命?
    我毫不客气地把他的YAML文件扔进了垃圾桶,然后自己重新写了一份。initialDelaySeconds改成了5秒,periodSeconds改成了10秒,failureThreshold设置成3。这样一来,Pod启动后5秒就开始健康检查,每10秒检查一次,如果连续三次检查失败,就会被认为是不健康的,然后K8S就会自动重启Pod。
    改完之后,问题立刻解决了。Pod再也没有无故消失,业务也恢复了正常。
    这件事告诉我一个深刻的道理:K8S的配置,特别是健康检查这种关键配置,绝对不能想当然。livenessProbereadinessProbe的配置,直接关系到应用的可用性和稳定性。如果配置不合理,不仅不能及时发现问题,反而会掩盖问题,甚至引发更严重的故障。
    记住,健康检查的目的是为了在问题发生时,能够快速定位和解决问题,而不是为了让问题变得更加隐蔽。对于这类配置,一定要深思熟虑,结合应用的实际情况进行设置,不能随便拍脑袋决定。
    技术结论:
    livenessProbereadinessProbe的配置直接影响Pod的可用性。initialDelaySeconds应该根据应用的启动时间设置,periodSeconds应该根据应用的健康状况变化频率设置,failureThreshold应该根据应用的容错能力设置。永远不要想当然,要根据实际情况进行调整。YAML文件不是艺术品,它是生产力工具,写之前先搞清楚每个参数的含义。

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-deployment
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: my-app
      template:
        metadata:
          labels:
            app: my-app
        spec:
          containers:
          - name: my-container
            image: my-image:latest
            ports:
            - containerPort: 8080
            livenessProbe:
              httpGet:
                path: /healthz
                port: 8080
              initialDelaySeconds: 5
              periodSeconds: 10
              timeoutSeconds: 5
              failureThreshold: 3
              successThreshold: 1
            readinessProbe:
              httpGet:
                path: /readyz
                port: 8080
              initialDelaySeconds: 5
              periodSeconds: 10
              timeoutSeconds: 5
              failureThreshold: 3
              successThreshold: 1
    
  • 又是网络背锅?这次真的冤枉!

    今天差点被气笑了,真想把那位的键盘给扬了。上午十点多,业务部门突然炸锅,说用户访问速度慢的像蜗牛爬,卡的让人怀疑人生。第一反应肯定是网络问题,这年头,网络就是背锅侠,有问题先甩锅网络。
    赶紧登录监控平台,一看,CPU、内存、磁盘IO,一切正常,K8S集群里的Pod也都活蹦乱跳的。网络带宽使用率也平稳的很,根本没啥异常流量。 心里嘀咕,这网络这次真是躺枪了。
    然后开始抓包,tcpdump抓起来,Wireshark 分析走起。 盯着屏幕看了半天,发现大量的TCP Retransmission,重传率高的吓人。这下有点意思了,网络虽然没拥塞,但数据包丢的厉害,导致TCP疯狂重传,用户体验能好才怪。
    顺着IP地址,一路追踪,发现问题集中在某一个特定的微服务上。 登录服务器,查看日志,满屏的Exception。定睛一看,尼玛, NullPointerException, 空指针异常!
    代码大致如下:

    public class OrderService {
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        public Order getOrder(String orderId) {
            // 从Redis获取订单信息
            Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
            // 如果Redis中没有,则从数据库获取
            if (order == null) {
                order = orderRepository.findById(orderId).orElse(null);
                // 尼玛,这里没判断就直接用了!
                redisTemplate.opsForValue().set("order:" + orderId, order, 60, TimeUnit.SECONDS);
            }
            return order;
        }
    }
    

    我瞬间血压就上来了。 Redis做缓存,数据库做持久化没问题。 从数据库查出来的数据,如果是空,你倒是判断一下啊! 直接把一个 null 值塞到 Redis 里面,下次再来查,永远都是 null,然后每次都去数据库查,每次都是 null,然后又往 Redis 里面塞 null,死循环了! 这不是自己给自己挖坑吗?
    更可气的是,这家伙为了所谓的“性能优化”,给 Redis 设置了 60 秒的过期时间。 这下好了,每隔60秒,缓存失效,大量的请求涌向数据库,数据库扛不住了,连接数被打满,开始丢包。 TCP 重传率飙升,最终用户体验直线下降。
    这已经不是初级程序员才会犯的错误了,这种连最基本的空指针判断都忘记的,我真怀疑他是怎么混进来的。 这就好比开车忘记系安全带,还猛踩油门,出事是必然的。
    最后,我把代码改成了下面这样:

    public class OrderService {
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        public Order getOrder(String orderId) {
            // 从Redis获取订单信息
            Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
            // 如果Redis中没有,则从数据库获取
            if (order == null) {
                order = orderRepository.findById(orderId).orElse(null);
                // 尼玛,这里做一下判断!
                if (order != null) {
                    redisTemplate.opsForValue().set("order:" + orderId, order, 60, TimeUnit.SECONDS);
                }
            }
            return order;
        }
    }
    

    简单粗暴,加个判断就完事了。 然后重启服务, 清空Redis缓存。 一切恢复正常,用户又可以愉快的剁手了。
    技术结论:
    这次事故再次证明了,基础知识的重要性。 空指针异常这种低级错误,绝对不应该出现在生产环境中。 同时,缓存的使用一定要谨慎,避免缓存穿透、缓存击穿等问题。 对于不存在的数据,可以考虑在缓存中设置一个特殊的标记(例如一个特殊的字符串或者一个特定的对象),避免大量的请求穿透到数据库。 另外,完善的监控体系是必不可少的,能够帮助我们及时发现问题,避免损失扩大。
    哎,希望以后能少碰到这种让人哭笑不得的事情。 运维不易,且行且珍惜啊!

  • 刚他妈开完会,一群SB又在扯什么“云原生最佳实践”、“降本增效”,放他娘的屁!一个个就知道用Helm装个Operator,真他妈以为自己是架构师了? 容器跑起来了就万事大吉了?监控呢?资源利用率呢?稳定性呢? 真想把他们的脑壳撬开,看看里面是不是装满了YAML文件。

    K8S调度器:你真懂它的调度策略?

    行了,先压压火,还是得干活。今天就跟你们聊聊K8S调度器,别以为Pod扔上去就完事了,里面的水深着呢。 尤其是现在动不动就搞什么“AI训练”、“大数据分析”,资源调度稍微有点问题,性能直接爆炸。

    K8S默认的调度器(kube-scheduler)那套东西,Predicate和Priority,看似简单,实际上里面的门道多得很。 不知道你们有没有仔细看过它的源码,或者至少看过它的配置?

    Predicate负责过滤掉不满足条件的Node,Priority负责对剩下的Node进行打分排序,最终选出最优的Node来运行Pod。 这流程没错,但是关键在于,Predicate和Priority怎么配置? 默认的那些东西,对于复杂的应用场景,根本不够用。

    自定义调度器:别只会用默认的

    我给你们举个例子,假设你们有个AI训练任务,需要GPU资源,而且最好是同一批次的GPU卡,这样才能发挥最大的并行计算能力。 用默认的调度器,它能保证Pod跑到有GPU的Node上,但是它能保证Pod使用的GPU卡是同一批次的吗? 绝对不能!

    所以,这种情况下,就必须自定义调度器。 至少要自定义Predicate和Priority,让调度器能够感知到GPU卡的型号、数量、拓扑结构,并根据这些信息进行调度。

    怎么搞? 别跟我说不会,K8S提供了扩展调度器的机制,你可以自己写一个调度器,或者使用现成的调度器框架。

    下面是一个简单的自定义Predicate的例子,用Go写的,别跟我说你不会Go,不会就去学!

    package main
    import (
        "context"
        "fmt"
        "os"
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/kubernetes/pkg/scheduler/framework"
        "k8s.io/kubernetes/pkg/scheduler/framework/plugins/names"
        "k8s.io/kubernetes/pkg/scheduler/framework/runtime/testing"
    )
    // GPUAffinity is a plugin that checks if the node has the required GPU resources.
    type GPUAffinity struct{}
    var _ framework.FilterPlugin = &GPUAffinity{}
    const (
        // Name is the name of the plugin used in the plugin registry and configurations.
        Name = "GPUAffinity"
    )
    // Name returns name of the plugin.
    func (pl *GPUAffinity) Name() string {
        return Name
    }
    // Filter invoked at the filter extension point.
    func (pl *GPUAffinity) Filter(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
        requiredGPUs := 1 // 假设每个Pod需要1个GPU
        availableGPUs := 0
        for _, resource := range nodeInfo.Allocatable.Capacity {
            if resource.Name == "nvidia.com/gpu" {
                availableGPUs = int(resource.Value())
                break
            }
        }
        if availableGPUs < requiredGPUs {
            return framework.NewStatus(framework.Unschedulable, "Insufficient GPU resources")
        }
        return framework.NewStatus(framework.Success)
    }
    // New creates a GPUAffinity plugin.
    func New(_ runtime.Object, handle framework.Handle) (framework.Plugin, error) {
        return &GPUAffinity{}, nil
    }
    func main() {
        // Example usage
        fmt.Println("GPU Affinity Plugin")
    }
    

    这个代码只是一个简单的示例,它检查Node上是否有足够的GPU资源。 实际情况要复杂得多,你需要根据你的具体需求进行修改。 比如,你可以检查GPU卡的型号,或者检查GPU卡的拓扑结构。

    不要只会抄YAML,底层原理才是王道

    现在很多年轻人,只会抄YAML,改改参数,根本不知道背后的原理。 遇到问题,只会Google,Stack Overflow,根本没有自己解决问题的能力。 这种人,我只能说,呵呵。

    K8S调度器看似简单,实际上涉及到很多底层的算法和数据结构。 比如,如何高效地进行Node的过滤和打分? 如何保证调度的公平性? 如何处理资源的竞争? 这些问题,都需要深入理解K8S的源码才能解决。

    所以,我奉劝各位,不要只会用K8S,要深入理解K8S的原理。 否则,你永远只是一个K8S的使用者,而不是一个K8S的专家。

    行了,今天就说到这里。 晚上还有个DB的坑要填,真是操蛋!

  • 今天TMD开会,一群SB高管又在扯什么“云原生”、“中台战略”,搞得好像用了K8S就能上天似的。我呸!真正的性能瓶颈在哪儿他们懂个屁?一个个PPT画得飞起,Linux内核调优、TCP/IP协议栈优化他们碰过吗?监控系统只会看个Dashboard,Prometheus的PromQL写过几条?只会喊口号,真TM恶心!

    K8S集群网络性能问题排查:别光盯着应用层,看看内核参数!

    现在K8S集群的网络问题,十个有八个都是内核参数没调好。应用层出了问题,抓抓包、看看日志,这没毛病。但如果应用层没问题,网络延迟就是高,丢包就是严重,那就要往下挖了!别TM就知道重启Pod,先看看你的内核参数是不是屎一样!

    TCP/IP内核参数优化:不要用默认配置,除非你想被吊打

    Linux内核的TCP/IP协议栈那一堆参数,默认配置就是给你们这些小白用的。生产环境,特别是高并发、大数据量的场景,必须根据实际情况调整。不然,等着被吊打吧!

    我这里列几个最容易出问题的参数,你们自己好好看看:

    关键TCP/IP参数详解

    1. tcp_tw_recycle 和 tcp_timestamps:

    这两个参数,网上TM一堆文章说可以解决TIME_WAIT过多的问题。在NAT环境下,开了tcp_tw_recycle,直接导致连接失败!为啥?tcp_timestamps开启后,TCP连接会记录时间戳,tcp_tw_recycle会拒绝时间戳比之前老的连接。NAT后面的客户端,可能时间戳都是一样的,直接被内核给丢弃了。正确的姿势是关闭tcp_tw_recycle,用tcp_tw_reuse + tcp_timestamps 才是王道。

    # 禁用 tcp_tw_recycle
    sysctl -w net.ipv4.tcp_tw_recycle=0
    # 开启 tcp_tw_reuse
    sysctl -w net.ipv4.tcp_tw_reuse=1
    # 确保 tcp_timestamps 开启
    sysctl -w net.ipv4.tcp_timestamps=1
    

    2. tcp_syn_retries 和 tcp_synack_retries:

    这两个参数控制TCP握手失败时的重试次数。默认值一般都偏大,在高并发场景下,会占用大量资源,导致SYN Flood攻击。适当降低这两个值,可以提高系统的抗攻击能力。

    # 降低 SYN 重试次数
    sysctl -w net.ipv4.tcp_syn_retries=3
    # 降低 SYN-ACK 重试次数
    sysctl -w net.ipv4.tcp_synack_retries=3
    

    3. tcp_rmem 和 tcp_wmem:

    这两个参数定义了TCP接收和发送缓冲区的大小。如果你的应用需要处理大量数据,默认值肯定不够用。适当增大这两个值,可以提高TCP的吞吐量。注意,不要设置太大,否则会占用大量内存。

    # 增大 TCP 接收缓冲区
    sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216'
    # 增大 TCP 发送缓冲区
    sysctl -w net.ipv4.tcp_wmem='4096 87380 16777216'
    

    4. net.core.somaxconn 和 tcp_max_syn_backlog:

    这两个参数控制TCP连接的backlog队列大小。在高并发场景下,如果backlog队列满了,新的连接会被拒绝。增大这两个值,可以提高系统的并发连接能力。

    # 增大 backlog 队列大小
    sysctl -w net.core.somaxconn=65535
    # 增大 SYN backlog 队列大小
    sysctl -w net.ipv4.tcp_max_syn_backlog=65535
    

    应用到K8S集群:DaemonSet才是正解

    改内核参数,最TM忌讳的就是手动一台台改。集群几百台机器,你改到猴年马月?用DaemonSet啊!把sysctl命令写到脚本里,做成一个镜像,然后用DaemonSet跑起来,保证每台Node都执行。这才是运维的正确姿势!

    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
      name: sysctl-tuning
      namespace: kube-system
    spec:
      selector:
        matchLabels:
          name: sysctl-tuning
      template:
        metadata:
          labels:
            name: sysctl-tuning
        spec:
          hostPID: true
          containers:
          - name: sysctl
            image: your-sysctl-image:latest #换成你自己的镜像
            securityContext:
              privileged: true
            command: ["/bin/sh", "-c"]
            args:
            - |
              #!/bin/bash
              sysctl -w net.ipv4.tcp_tw_recycle=0
              sysctl -w net.ipv4.tcp_tw_reuse=1
              sysctl -w net.ipv4.tcp_timestamps=1
              sysctl -w net.ipv4.tcp_syn_retries=3
              sysctl -w net.ipv4.tcp_synack_retries=3
              sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216'
              sysctl -w net.ipv4.tcp_wmem='4096 87380 16777216'
              sysctl -w net.core.somaxconn=65535
              sysctl -w net.ipv4.tcp_max_syn_backlog=65535
    

    记住,这些参数不是一成不变的,要根据你的应用场景和硬件环境进行调整。监控才是王道!用Prometheus监控TCP连接数、丢包率、延迟等指标,根据监控数据不断优化。

    现在这些年轻人,就知道抄别人的配置,也不想想自己环境适不适合。网络调优这玩意儿,必须自己动手,深入理解原理,才能真正解决问题。别TM只会喊“云原生”,先把Linux内核搞明白了再说!

  • 这TM也能出事?我真是服了!

    今天真是他妈的倒了血霉了!上午刚来,屁股还没坐热,就被DBA那SB玩意儿叫过去,说数据库连不上了,应用死活起不来,让老子过去看看。我心里就一万只草泥马奔腾而过,这种JB问题也来找我?数据库连不上,第一反应不应该是自己查日志,看是不是密码过期,或者网络问题吗?
    到了现场,那孙子还在那儿抓耳挠腮,我就问他,你TM倒是说说,啥情况?他支支吾吾地说,就是…就是…应用报错,说连接不上数据库。我直接怼他:“连不上就看日志啊!你TM是摆设吗?日志呢?拿来我看看!”
    日志拿过来,一眼就看到ORA-12514: TNS:listener does not currently know of service requested in connect descriptor。 这TM不是TNS配置有问题吗?listener不知道你请求的服务。我深吸一口气,告诉自己冷静,这SB玩意儿可能真的一窍不通。
    我就问他:“你改过tnsnames.ora文件没?” 他说:“改过啊,昨天晚上改的。” 我TM就想一巴掌扇死他,改完TNS文件,listener没 reload,你指望它能找到新的service name? 这TM是DBA的基本素养好吧?
    然后我就开始检查tnsnames.ora文件,一看,我艹,更离谱的事情发生了!这SB玩意儿把SERVICE_NAME写成了SID! 难怪listener找不到服务! 我指着文件,对着他的鼻子吼:“你TM脑子被驴踢了吗? SERVICE_NAME和SID都分不清? 你怎么当上DBA的?走后门进来的?”
    他还在那儿狡辩:“我…我以为都一样…” 我真是无语了,这TM根本不是“以为都一样”的问题,这是基本概念都没搞清楚! Oracle的SERVICE_NAME是用来在listener上注册服务的,而SID是数据库实例的唯一标识。它们虽然都指向同一个数据库实例,但是用途完全不同!
    更可气的是,这孙子改完TNS文件,没重启应用服务器,也没刷新连接池! 我TM真是要被气死了! 一通操作下来,解决问题的方法简单的令人发指:
    1. 修改tnsnames.ora文件,将SID改成正确的SERVICE_NAME. (例如,把 SID = ORCL 改成 SERVICE_NAME = ORCL.example.com)
    2. 登录数据库服务器,执行lsnrctl reload 命令,重新加载listener配置。
    3. 重启应用服务器,或者至少刷新连接池。
    就这么简单的三步,这SB愣是搞了一上午! 我真想把他的工牌摘下来,扔到垃圾桶里!
    技术结论:
    这种傻逼错误,根本不应该发生。tnsnames.ora 文件是连接Oracle数据库的基础。记住,SERVICE_NAME和SID不是一个东西,用错的后果就是连接失败。修改tnsnames.ora之后,必须reload listener,并且重启应用或者刷新连接池,确保新的配置生效。 别TM以为DBA就是个点点鼠标的活儿,基本功不扎实,迟早要出事! 以后谁TM再犯这种低级错误,我就直接让他滚蛋! 简直是浪费老子的时间!

  • 今天差点被一个YAML空格送走!

    草!今天真是日了狗了!
    早上刚来,屁股还没坐热,就被DBA那边拉过去救火。他们一个新上的业务,死活连不上数据库。报错信息那叫一个五花八门,什么“连接超时”、“认证失败”、“找不到服务”……反正你能想到的网络问题,它都给你报一遍。
    一开始我以为是网络策略或者防火墙的问题,抓包一看,TMD三层都没通。这还玩个屁?
    然后就去查K8S的Service和Endpoint,确认服务暴露没问题,Pod也正常Running。再往下看,傻逼的地方就来了。
    DBA那边自己瞎JB改了一个ConfigMap,里面存着数据库的连接信息,包括地址、端口、用户名、密码。我一看,地址和端口都是对的,用户名密码也没问题。但是!尼玛在password:后面多加了一个空格!

    database:
      host: db.example.com
      port: 3306
      username: app_user
      password:  your_secret_password # 注意这里,SB空格!
    

    你敢信?就这么一个空格,卡了老子半天!
    这个SB的程序,直接把your_secret_password(后面带个空格)当密码去连接数据库。数据库一看,你这什么玩意?认证直接给你拒了。
    草,一个ConfigMap,YAML文件,最基本的格式都没搞明白,key: value,冒号后面加空格是规范,不是让你在value后面加空格!这是小学生都会的东西!他妈的还DBA,我看就是个Delicate Boy Adjuster!
    更操蛋的是,这SB出问题了也不自己好好查,直接跑过来找我,浪费老子时间!
    我直接把kubectl edit命令甩他脸上,让他自己看,自己改。
    改完之后,连上了,屁事没有了。
    这件事让我意识到,这些傻逼玩意儿根本不理解YAML的本质。YAML他妈的本质就是数据!是配置文件!是给人读,给程序用的!你加个空格,程序就认不出来,就报错!这和你在JSON里乱加逗号有什么区别?
    教做人时间:
    记住!以后写YAML,特别是涉及到敏感信息的,一定要用编辑器或者IDE自带的YAML校验功能!别他妈的手贱瞎JB敲空格!
    更重要的是,程序读取ConfigMap里的配置信息的时候,一定要进行Trim操作!去掉字符串两端的空格!这是一个最基本的安全原则!也是一个良好的编程习惯!

    import os
    # 从环境变量中读取数据库密码,并进行trim操作
    database_password = os.environ.get("DATABASE_PASSWORD").strip()
    # 或者,如果你用的是字典
    config = {
        "database": {
            "password": os.getenv("DATABASE_PASSWORD").strip()
        }
    }
    

    如果你用的是Java,也一样,用String.trim()方法。
    别再他妈的犯这种低级错误了!丢人!真他妈丢人!浪费老子时间!

  • 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
    }