作者: ningniu

  • 记一次令人窒息的“性能优化”:别把无知当极客

    上午十点半,阳光刚好打在机房外围办公室的玻璃上。正逢早高峰流量拉升的阶段,我正盯着Grafana上的大盘,手里这杯美式还没喝到一半,监控告警群直接炸了。
    核心交易链路的Redis集群,P99延迟曲线像一根突然勃起的中指,直插云霄。接着就是铺天盖地的 5xx 报错,网关层的Timeout日志刷得终端根本看不清。
    第一反应:Redis挂了?
    切到终端,kubectl get pods -n data-layer,所有Redis Pod状态全是 RunningREADY 也是 1/1。
    查看 CPU 和内存,风平浪静,连个OOM的影子都没有。
    我顺手找了个应用Pod进去,直接 ping Redis的Pod IP,通的。
    再用 nc -vz 6379,秒连。
    但只要用K8s的 Service (ClusterIP) 去连,nc -vz 6379,直接卡死,直到超时。
    Pod网络正常,Service网络瘫痪。而且只针对这一个Redis Service瘫痪。
    排查到这里,我脑子里大概有底了。Kube-proxy的规则出问题了,或者底层的网络栈被动了手脚。就在我准备拉取宿主机的 iptables 规则时,群里一个刚入职没半年的“资深”DevOps同事冒泡了:
    “我刚才通过Ansible推了一个内核网络优化脚本,会不会跟这个有关系?我看网上说这样能大幅降低高并发下的网络延迟。”
    听到这句话,我眼皮跳了一下。直接让他把推的脚本发来看看。
    不看不知道,一看血压直接飙到180。脚本里赫然躺着这么两行:

    iptables -t raw -A PREROUTING -p tcp --dport 6379 -j NOTRACK
    iptables -t raw -A OUTPUT -p tcp --sport 6379 -j NOTRACK
    

    我深吸了一口气,强压着顺着网线过去砸他键盘的冲动。
    这是一个极其经典的“只知其一不知其二”的愚蠢操作。
    这位同事大概是看了某篇不知哪年哪月的“高并发调优指南”,知道Linux内核的 nf_conntrack(连接跟踪)表在高并发下容易被打满,导致 nf_conntrack: table full, dropping packet 的丢包报错,并且连接跟踪确实会消耗一点点CPU。所以他觉得,既然Redis是高频调用的内网服务,直接在 raw 表把它 Bypass 掉(NOTRACK),不就能榨干最后一点性能了吗?
    听起来很极客,对吧?但他根本没搞懂Kubernetes的网络基石是什么。
    让我用最底层的逻辑来拆解一下,为什么在这个技术点上犯错是不可原谅的。
    在Linux的Netfilter数据包处理流水线中,生命周期是这样的:
    raw 表 -> Connection Tracking -> mangle 表 -> nat 表 -> 路由决策 -> filter
    Kubernetes的Service(这里指ClusterIP)是基于DNAT(目标地址转换)实现的。不管你底层是 iptables 模式还是 IPVS 模式,Kube-proxy都需要拦截发往ClusterIP的数据包,并将其目标IP修改为后端真实的Pod IP。
    这个动作,发生在哪里?发生在 nat 表。
    重点来了:Linux内核的 nat 表是强依赖于连接跟踪(conntrack)的。
    nat 表只处理一条连接的第一个数据包(状态为 NEW)。一旦第一个包完成了地址转换,内核会在 conntrack 表里记录下这层映射关系。后续属于这条连接的数据包,甚至回包,都会直接根据 conntrack 里的记录进行自动转换,根本不会再去走一遍 nat 表的规则。
    当你自作聪明地在第一关 raw 表里加上了 -j NOTRACK
    数据包带着免检金牌大摇大摆地绕过了连接跟踪机制。
    紧接着,它来到了 nat 表。nat 表一看:这包没有被跟踪?那对不起,我没法处理。
    于是,这个数据包直接跳过了Service IP到Pod IP的DNAT转换环节。
    结果是什么?
    应用端发往 10.96.x.x (ClusterIP) 的SYN包,带着原始的目的地址进入了底层的网络插件(Calico/Flannel),路由器一看,这个IP根本不在我的Pod网段路由表里,直接黑洞丢弃。
    应用端永远等不到ACK,直到连接超时,业务全盘崩溃。
    你在K8s环境里,把Kube-proxy赖以生存的底座给抽了,还美其名曰“性能优化”。这就好比嫌汽车发动机发热,所以把冷却液全抽干一样荒谬。
    我切到生产机,两行命令把这该死的规则删了:

    iptables -t raw -D PREROUTING -p tcp --dport 6379 -j NOTRACK
    iptables -t raw -D OUTPUT -p tcp --sport 6379 -j NOTRACK
    

    回车敲下的瞬间,监控大盘上的延迟曲线断崖式下跌,5xx报错清零,早高峰的流量重新平稳涌入。
    在这个行业干了三十年,我见过无数华丽的故障,大多源于对底层原理的无知和对网文盲目的崇拜。
    技术结论:
    在Kubernetes集群或任何重度依赖NAT/SNAT/DNAT的网络架构中,严禁对业务端口使用 NOTRACK
    如果你真的遇到了 conntrack 瓶颈:
    1. 请去调大 net.netfilter.nf_conntrack_max 并配合增加相应的哈希表大小 (hashsize)。
    2. 缩短TIME_WAIT的跟踪超时时间 (net.netfilter.nf_conntrack_tcp_timeout_time_wait)。
    3. 如果真的到了千万级并发连接,不要试图通过魔改内核网络栈来硬抗,去重构你的应用架构,引入本地缓存或者连接池复用。
    永远记住:架构是一个精密的齿轮组,任何一行底层的参数修改,都必须建立在对整个数据流转路径的绝对掌控之上。没有这种敬畏心,你敲下的每一个回车,都是一颗定时炸弹。

  • 深度剖析:K8s环境下CFS Bandwidth Control引发的P99延时毛刺与内核级调优

    今天一上午都在配合业务团队排查一个诡异的问题。几个核心的Go服务在白天流量高峰期,频繁出现毫无规律的P99延时尖峰。业务开发查了链路追踪,发现时间全消耗在了服务内部的逻辑处理上,接着他们开始怀疑是宿主机网络丢包或者底层数据库响应慢。
    我扫了一眼Prometheus的监控大盘,节点负载不高,网络TCP Retrans也处于极低水位,DB的Slow Log更是干干净净。既然外部依赖没问题,那问题只能出在计算资源的调度上。
    当查到容器的CPU使用率时,业务同学指着图表说:“你看,Pod的CPU使用率最高才不到40%,资源非常充裕。”
    我没有反驳,只是默默切到终端,找了一台发生过毛刺的Node,直接进入该Pod对应的Cgroup层级,敲下了这行命令:

    cat /sys/fs/cgroup/cpu/kubepods/burstable/pod//cpu.stat
    

    输出结果极为刺眼:

    nr_periods 145321
    nr_throttled 38452
    throttled_time 1928430000000
    

    nr_throttled 占比超过了26%。这意味着,在过去的调度周期里,这个容器有四分之一以上的时间被内核强制挂起(Throttled)。这就是典型的“看监控CPU使用率很低,但应用实实在在被卡死”的场景。
    很多没有深入过Linux Kernel调度的工程师,对K8s的 limits.cpu 存在严重的误解。K8s的 limits.cpu 底层依赖的是Linux Cgroup的 CFS(Completely Fair Scheduler)Bandwidth Control 机制。
    我们来看这背后的两个核心参数:

    # cat /sys/fs/cgroup/cpu/kubepods/burstable/.../cpu.cfs_period_us
    100000
    # cat /sys/fs/cgroup/cpu/kubepods/burstable/.../cpu.cfs_quota_us
    200000
    

    默认情况下,cfs_period_us 是100ms(100000微秒),而这里的 cfs_quota_us 是200ms,对应K8s里的 limits.cpu: "2"(2个核)。
    Go或者Java这类多线程/Goroutine并发模型,在面对突发流量时,会瞬间唤醒大量线程去抢占CPU。假设在这100ms的周期内,Go runtime唤醒了20个线程并发处理请求,每个线程跑了10ms。那么总的CPU配额在周期刚开始的10ms内,就被这20个线程瞬间耗尽(20 * 10ms = 200ms)。
    配额一旦耗尽,内核的 CFS 调度器就会冷酷无情地把这个Cgroup下的所有进程全部移出 Runqueue。接下来的90ms内,这个容器仿佛陷入了时间停止,任何进来的网络请求、等待处理的任务,只能干等。这就是业务代码内部出现诡异耗时的根本原因。在Prometheus看来(通常是15秒或1分钟抓取一次),平均下来的CPU使用率甚至不到1核,完全掩盖了微秒级别的“饥饿”现象。
    为了拿到最确凿的证据,我习惯用 eBPF 工具直接观测调度队列的延迟。在宿主机上跑了一下 runqlat

    # /usr/share/bcc/tools/runqlat -T -p 
    Tracing run queue latency... Hit Ctrl-C to end.
         usecs               : count     distribution
             0 -> 1          : 123      |                                        |
             2 -> 3          : 412      |*                                       |
             4 -> 7          : 1530     |****                                    |
             ...
         65536 -> 131071     : 284      |*                                       |
        131072 -> 262143     : 11       |                                        |
    

    果然,有相当一部分调度的延迟落在了 65ms – 131ms 这个区间。这与 CFS 100ms 周期被限流后的等待时间高度吻合。
    既然定位了,如何解决?
    针对这种由多线程并发突发(Burst)引起的 CFS Throttling,有几套不同的架构解法,我通常根据集群的具体环境来实施:
    方案一:简单粗暴,去掉 CPU Limits
    如果在业务节点资源相对充裕的情况下,最佳实践是只设置 requests.cpu,不设置 limits.cpu。这样K8s会将该Pod划分为 Burstable QoS 级别。只要宿主机有空闲CPU,它就能尽情抢占。

    resources:
      requests:
        cpu: "2"
        memory: "4Gi"
      limits:
        # 删掉 cpu limits
        memory: "4Gi"
    

    缺点是:如果宿主机混部了大量高负载应用,会导致节点CPU被彻底打满,引发更严重的雪崩。这需要有完善的节点负载监控和驱逐策略做兜底。
    方案二:调整 Kubelet 的 CFS Quota Period
    如果因为多租户隔离必须限制 limits.cpu,可以通过修改 Kubelet 的配置,把 cpuCFSQuotaPeriod 调小。
    修改 /var/lib/kubelet/config.yaml

    cpuCFSQuota: true
    cpuCFSQuotaPeriod: "10ms" # 默认是 100ms
    

    把周期缩短到10ms,意味着即使配额被瞬间耗尽,容器最多也只会被挂起几个毫秒,而不是之前的90ms。这能极大平滑P99的毛刺。但代价是内核调度器的开销会显著增加,上下文切换(Context Switch)更频繁,总体吞吐量会下降1%~3%。
    方案三:内核级调优 – 引入 CPU Burst 特性
    这也是我最推荐的高阶玩法。在比较新的内核(比如阿里开源的 Anolis OS,或者内核版本 >= 5.14)中,引入了 CPU Burst 技术。它允许 Cgroup 积累之前周期未使用的 CPU 时间,在应对突发流量时,突破单周期的 Quota 限制。
    如果内核支持,直接写 sysfs 即可开启:

    # 开启 cfs burst 特性
    echo 1 > /sys/fs/cgroup/cpu/kubepods/burstable/cpu.cfs_burst_us
    

    通过调整 cpu.cfs_burst_us 参数,可以在不取消 limits.cpu 的前提下,优雅地解决突发并发带来的 Throttling 问题。
    今天最后,我让运维团队批量下发了 Kubelet 配置变更,把核心服务所在节点池的 cpuCFSQuotaPeriod 从100ms切到了20ms,平滑重启后,业务监控上的P99延时曲线瞬间像刀切一样平整。
    技术做到深处,没有所谓的玄学。所有的“偶尔卡顿”,本质上都是因为对底层调度机制不够敬畏。趁着中午这段时间把排查过程记录下来,希望以后团队遇到类似现象,能直接把手伸到内核里去找答案,而不是对着一层薄薄的监控面板瞎猜。

  • 记一次早高峰的TCP黑洞:当“安全合规”遇上网络文盲

    今天上午10点15分,正值业务早高峰,监控大盘上的QPS曲线还在往上爬。我手里的咖啡还没喝完,Prometheus的报警就响了:好几个核心微服务的接口响应时间P99直接飙到了5秒以上,甚至开始大面积出现 504 Gateway Timeoutcontext deadline exceeded
    我看了一眼底层资源面板,CPU使用率不到30%,内存充足,磁盘IO安静得像死海。再切到K8S集群层面的监控,Pod没有发生驱逐,OOM指标也是0。
    不是资源瓶颈。直觉告诉我,底层网络出事了。
    我挑了一台报错最密集的K8S Worker节点,直接SSH登上去。进入报错微服务的网络命名空间,用 curl 测试下游接口。现象很诡异:请求小载荷的 /health 接口,瞬间返回;但请求带有几KB大载荷的业务接口,直接挂起(Hang死),直到超时。
    小包能通,大包必死。这是教科书般的MTU(最大传输单元)不匹配导致的TCP黑洞现象。
    我随手敲了一行抓包命令:

    tcpdump -i any -nn -S -v host 10.244.15.12
    

    屏幕上疯狂滚动的输出印证了我的猜想。TCP三次握手(SYN, SYN-ACK, ACK)极其顺畅,但随后推送业务数据的大包(长度1514)发出去之后,全都没有收到ACK响应,接着就是无止境的 TCP Retransmission
    我退回宿主机,看了一眼路由和网卡配置:

    ip link show | grep -E "mtu|tunl"
    

    当前K8S集群用的是Calico的IPIP模式。因为宿主机的物理网卡 eth0 MTU是默认的1500,IPIP封装需要额外增加20字节的外部IP头,所以Calico自动创建的隧道网卡 tunl0 的MTU应该是1480。
    但离谱的事情出现了:我在那些业务Pod里用 ip link 查看,容器内 eth0(veth pair的一端)的MTU居然被硬生生改成了 1500!
    更离谱的是,即使Pod的MTU被错误地设置成了1500,当这个1500字节的包被路由到宿主机的 tunl0(1480)时,由于TCP包默认带有 DF(Don’t Fragment,不可分片)标志,Linux内核协议栈理应丢弃这个包,并向发送方(Pod)回复一个 ICMP Type 3 Code 4(Fragmentation Needed)的消息。发送方收到这个ICMP消息后,会触发PMTUD(路径MTU发现)机制,自动调小后续发送包的MSS(最大报文段长度),问题也就自动修复了。
    可是,这个ICMP回包去哪了?
    我调出宿主机的iptables规则,看到了让我血压瞬间拉满的一幕:

    iptables -nvL FORWARD | grep icmp
    

    赫然存在一条:

     120M  10G DROP       icmp --  *      *       0.0.0.0/0            0.0.0.0/0
    

    所有的ICMP包,在FORWARD链上被无差别、全局DROP了。数据包高达10GB,说明它已经在这里默默干掉无数个原本能拯救网络的ICMP消息。
    我把昨天刚入职不久的那个高级运维拉到了屏幕前。
    经过五分钟的对峙,破案了。
    昨天下午临近下班,这位老兄收到了一份所谓“安全合规扫描工具”生成的报告,提示集群节点响应了ICMP Timestamp Request,有极低风险的信息泄露可能。为了图省事,他直接写了个Ansible Playbook,在所有节点的FORWARD链和INPUT链上加了全局DROP ICMP的规则。
    这还没完。今天早上9点,有开发反映内部文件上传接口变慢(其实就是因为丢ICMP导致重传)。这位天才不仅没有排查防火墙,反而去百度了一篇不知道是2008年还是2010年的“网络调优”博客,认为是容器MTU太小导致分包损耗性能,于是又推了个DaemonSet,用 nsenter 暴力把所有运行中Pod的网卡MTU刷成了1500。
    完美。他用两次愚蠢的操作,亲手打造了一个坚不可摧的TCP黑洞。
    第一步,修改Pod MTU为1500,导致超出 tunl0 1480 的限制,触发内核由于DF位存在的强制丢包。
    第二步,全局DROP ICMP,彻底瞎了TCP协议栈的眼睛,把PMTUD机制连根拔起,让发送方永远等不到那句“你需要分片”的通知,只能在超时和重传中耗死连接。
    我冷着脸,用两行命令收拾了这个烂摊子:

    # 删掉那条极其业余的防火墙规则
    iptables -D FORWARD -p icmp -j DROP
    # 恢复正确的防探测规则(如果真要过合规,也是精细控制类型)
    iptables -A FORWARD -p icmp --icmp-type timestamp-request -j DROP
    

    并让他立刻停掉那个暴力刷MTU的DaemonSet,重启关联的Pod恢复Calico分配的默认1480 MTU。两分钟后,监控大盘上的报错断崖式下跌,QPS重回巅峰,早高峰的危机解除了。
    在这个行业干了这么多年,我对各种诡异的Kernel Bug都能一笑而过,但唯独对这种缺乏底层敬畏心的操作无法容忍。
    TCP/IP协议栈是一个精密咬合的齿轮组。在没有彻底搞懂IPIP隧道封装的20字节开销(Outer MAC + Outer IP)与内层数据包边界的关系之前,去碰网卡MTU无异于蒙眼走钢丝。而在现代数据中心网络里,把ICMP视作洪水猛兽并无脑DROP,不仅是对RFC 1191(PMTUD)的无知,更是对Linux内核网络设计的侮辱。
    记住,网络安全不是靠把协议栈的嘴堵上建立的。搞不懂底层数据包的流转路径,敲下的每一行 DROP,最终都会化作系统崩溃时流下的泪。

  • 运维事故:消失的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这玩意,用好了是神器,用不好就是坑。年轻人,多学点底层原理,少抄点概念。否则,等着被这些破玩意坑死吧!