说实话,我第一次把服务从虚拟机搬到 Kubernetes (K8s) 的时候,心情就像是把家里的老家具扔进了一个巨大的、会自己移动的传送带。你以为你在控场,其实传送带可能随时把椅子塞进洗碗机里。
市面上教 K8s 的书多如牛毛,但大多数都在讲“怎么配置 YAML”,很少有人跟你掏心窝子说:“嘿,这里有个坑,掉进去你会哭断肠。” 今天我不打算给你念教科书,咱们就聊聊那些在深夜报警声中换来的血泪教训,以及怎么让你的微服务和集群真的“跑得顺”。
一、 别把 K8s 当成更强的虚拟机:状态与无状态的真相
很多团队刚上手 K8s 的第一个坑,就是把那些“皮实”的单体应用或者带状态的服务(比如 MySQL、Redis,甚至是一些有本地日志的服务)直接扔进集群,然后发现数据丢了、连接断了、或者启动慢得离谱。
1.1 无状态服务的“假象”
你要明白,Pod 是短暂的。它们可以被杀掉、重启、迁移到另一个节点,这一切都是毫秒级甚至秒级完成的。如果你的应用依赖本地磁盘写入某些临时文件,或者在内存里维护了不可序列化的会话状态,那么在没有外部存储或会话同步机制的情况下,Pod 一旦重启,你的业务逻辑就断了。
实战建议:
- 彻底无状态化:设计服务时,确保所有需要持久化的数据都写入外部存储(对象存储 S3/OSS、数据库、缓存)。
- 会话外置:如果有用户会话,务必使用 Redis 或类似的共享会话存储,而不是存在 Session 里。
1.2 状态服务的“正确打开方式”
当然,我们确实需要在 K8s 里跑 MySQL 或 Kafka。这时候,不要直接用 PVC(Persistent Volume Claim)随便挂载,而应该考虑 StatefulSet。
StatefulSet 解决了两个核心问题:
- 稳定的网络标识:每个 Pod 有固定的主机名(如
mysql-0,mysql-1),即使重启,IP 和名字不变。 - 有序的部署和扩展:先启动
mysql-0,它正常了再启动mysql-1。
如果你只是简单地把 MySQL 放进 Deployment 里,当副本 2 重启时,它可能抢占了副本 1 的资源或网络资源,导致主从复制乱套。
注意:即使是 StatefulSet,也不建议直接管理生产级的 MySQL 主从切换,因为 K8s 的 etcd 存储性能和网络延迟并不适合高频的分布式共识操作。对于复杂的状态服务,推荐使用专门的原生算子(Operator),比如 MySQL Operator 或 Patroni,它们比原生 K8s 资源更懂如何维护状态。
二、 微服务治理:不是加了 Ingress 就叫云原生
微服务架构的核心价值是解耦和独立部署,但在 K8s 里,很多团队只做了一半:把服务拆分了,但没做治理。结果就是服务间调用乱成一锅粥,排查问题靠 grep,监控靠猜。
2.1 服务发现与 Service 的误解
K8s 的 Service 资源提供了简单的负载均衡和服务发现。但你要知道,Service 是基于 kube-proxy 实现的,它在每个节点上维护 iptables 或 IPVS 规则。当服务数量达到几百个,流量巨大时,iptables 规则的更新会导致明显的延迟和 CPU 飙升。
避坑指南:
- 如果规模不大,Service 没问题。
- 如果规模大、延迟敏感,考虑 eBPF 方案(如 Cilium)来替代传统的 kube-proxy,性能提升显著。
2.2 Sidecar 模式:可观测性的黄金搭档
在 K8s 里,最优雅的可观测性方案不是让你的业务代码里埋点,而是使用 Sidecar 容器。
想象一下,你的主容器是 app,旁边挂着一个 envoy 或 fluentd 容器。所有的入口流量和出口流量都被 Sidecar 拦截。
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:v1.0
- name: sidecar-proxy
image: envoyproxy/envoy:v1.20
ports:
- containerPort: 9901
为什么这样做?
- 零侵入:业务代码不需要引入任何监控 SDK。
- 统一标准:所有服务的日志格式、指标暴露、链路追踪都通过 Sidecar 统一处理。
- 责任分离:运维团队负责 Sidecar 的配置,开发团队只关心业务逻辑。
2.3 配置管理:ConfigMap 和 Secret 的正确用法
很多新手喜欢把配置文件塞进镜像里,或者硬编码在代码中。这在 K8s 里是大忌。
原则:
- 使用 ConfigMap 存放非敏感配置(如
app.properties)。 - 使用 Secret 存放密码、证书(注意:Secret 默认是 Base64 编码,不是加密,生产环境务必开启 etcd 加密或对接外部密钥管理如 Vault)。
- 通过 环境变量 或 挂载卷 的方式注入到 Pod 中。
小技巧:如果配置变更频繁,建议使用 ConfigMap 挂载卷 + inotify 机制,让应用能热重载配置,避免每次改配置都要重启 Pod。
三、 扩缩容:HPA 不是万能药
水平Pod自动伸缩(HPA)听起来很美好:流量来了自动加副本,流量走了自动减副本。但现实中,很多团队配置 HPA 后,发现应用在“震荡”,或者根本缩不回去。
3.1 扩缩容的滞后性
K8s 的扩缩容不是魔法。当流量突增时:
- HPA 检测到指标(通常是 CPU 或自定义指标)超过阈值。
- HPA 调用 API Server 增加 Pod 数量。
- 调度器 找到一个合适的节点。
- 容器运行时 拉取镜像、启动容器。
- 探针 通过,流量才进入。
这个过程可能需要几十秒甚至几分钟。如果你的流量是尖峰脉冲式的(比如秒杀活动),等你缩完容,活动已经结束了。
解决方案:
- 预热策略:对于已知的高峰期(如双11),使用 CronHPA 或手动预扩容。
- 合理的资源请求(Requests):这是最关键的一点。如果你设置的
requests.cpu远低于实际使用,HPA 可能永远触发不了扩容;如果你设置得太高,Pod 永远调度不到节点上,集群会处于“虚假的满载”状态。务必通过 vPA (Vertical Pod Autoscaler) 或长期监控数据来调优 Requests 和 Limits。
3.2 缩容的“冷却期”
很多团队发现,流量一降,Pod 很快就缩了,但紧接着流量又上来,Pod 又扩了,如此反复。这就是震荡(Thrashing)。
避坑指南:
- 设置合理的
max-delay和scale-down-stabilization-window:在 HPA 配置中,增加缩容的等待时间。比如,让 Pod 至少运行 5-10 分钟后再决定删除,避免误判短暂的流量低谷。 - 使用 PDB (PodDisruptionBudget):确保在手动缩容或节点维护时,不会同时删除过多 Pod,保证服务可用性。
3.3 Keda:事件驱动扩缩容的救星
如果你的服务不是由 HTTP 流量触发,而是由 Kafka 消息队列、RabbitMQ、数据库积压 等事件触发,那么标准的 HPA 就不适用了。
这时候,Keda (Kubernetes Event-driven Autoscaling) 是你的好朋友。
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: kafka-trigger-auth
spec:
secretTargetRef:
- parameter: broker
name: kafka-secret
key: broker
- parameter: consumerGroup
name: kafka-secret
key: consumerGroup
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kafka-consumer
spec:
scaleTargetRef:
name: my-app
triggers:
- type: kafka
metadata:
bootstrapServers: my-kafka:9092
consumerGroup: my-group
topic: my-topic
lagThreshold: "100" # 当队列积压超过100条消息时扩容
authenticationRef:
name: kafka-trigger-auth
Keda 能直接对接各种消息队列、Prometheus、甚至 AWS SQS,实现真正的事件驱动扩缩容。这不仅节省成本,还能避免业务代码里写复杂的轮询逻辑。
四、 可观测性:没有监控的 K8s 就是裸奔
在物理机时代,我们有 Zabbix、Nagios。在 K8s 时代,这套体系不够用了,因为环境变了。
4.1 三个支柱缺一不可
- Metrics (指标):Prometheus 是事实标准。但注意,不要只监控 CPU 和内存。业务指标才是最重要的。比如:QPS、错误率、P99 延迟、消息队列积压量。
- Logs (日志):EFK (Elasticsearch, Fluentd, Kibana) 或 Loki + Grafana 是常见选择。关键是结构化日志,便于检索和分析。不要把你应用的
System.out.println直接扔进 ES,那样会撑爆你的存储和预算。 - Tracing (链路追踪):当请求跨越十个微服务时,只有指标和日志是不够的。你需要 Jaeger 或 SkyWalking 来追踪一个请求的完整生命周期,定位是哪个环节慢了。
4.2 日志采集的“三不”原则
- 不采集请求体:除非必要,否则不要记录 POST/PUT 请求的 body,信息量太大且敏感。
- 不采集成功且快速的请求:如果 QPS 很高,只记录错误日志和慢日志,避免日志洪流淹没关键信息。
- 不乱打时间戳:确保日志的时间戳是服务端生成的,而不是客户端生成的,因为容器之间可能存在时钟偏差。
五、 安全:被忽视的最后一道防线
很多团队把 K8s 集群搭建得漂漂亮亮,但安全配置几乎为零。
5.1 镜像安全
- 不要使用
latest标签:这会导致不可重复构建,你今天构建的和明天构建的可能不是同一个版本。始终使用具体的版本号,如v1.2.3。 - 扫描镜像漏洞:在 CI/CD 流水线中加入镜像扫描步骤(如 Trivy、Clair),拦截已知的高危漏洞镜像。
5.2 权限最小化
- ServiceAccount 权限:默认情况下,Pod 里的应用拥有访问 API Server 的权限,这非常危险。务必使用 RBAC (Role-Based Access Control) 限制 ServiceAccount 的权限,只给它需要的最小权限。
- NetworkPolicy:启用 NetworkPolicy,明确允许哪些 Pod 之间可以通信。默认情况下,K8s 是允许所有 Pod 互通的,这给横向攻击提供了便利。
5.3 安全上下文 (Security Context)
在 Pod 或 Container 级别设置安全上下文,禁止容器以 root 用户运行:
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
这能极大地减少容器逃逸的风险。
六、 结语:K8s 是一种文化,不仅仅是工具
写到这里,我想强调的是,K8s 的难点从来不是 YAML 的语法,而是架构思维的转变。
- 从“管理服务器”转变为“管理声明式期望”。
- 从“个人英雄主义”转变为“平台工程”。
- 从“一次性部署”转变为“持续演进”。
避坑最好的方式,不是在坑里挣扎,而是在设计阶段就考虑好可观测性、安全性和自动化运维。希望这篇指南能帮你在 K8s 的浪潮中,少摔几个跟头,多享受一些云原生带来的便利。
记住,没有银弹,只有不断适配和调整的最佳实践。你的集群,最终会根据你的业务特点,长出独特的形状。
