互联网公司上线K8s后服务崩了3次 云原生应用架构避坑指南
上周,我们团队的Kubernetes集群又双叒叕崩了。这是上线以来的第三次。
第一次是数据库连接池爆掉,第二次是内存泄漏,第三次更离谱——一个看似无害的配置文件,让整个集群的API Server直接拒载。
说实话,上线前我们觉得K8s就是”部署容器嘛,能有多难”。结果现实狠狠给我们上了一课。今天把这些血泪教训整理出来,希望能帮到其他正在或准备上K8s的小伙伴。
一、第一次崩溃:数据库连接池的”隐形炸弹”
发生了什么
那天下午3点,监控报警突然炸了。所有Pod同时被标记为NotReady,数据库连接数直接飙到上限,应用层全报错。
我们查了半小时日志,发现一个诡异的现象:每个Pod都在疯狂建立数据库连接,但连接数始终居高不下,哪怕请求量并不太大。
2023-10-15 15:23:45 ERROR [pool-1-thread-12] Connection leak detected!
2023-10-15 15:23:46 WARN [pool-1-thread-3] Max connections reached: 100
2023-10-15 15:23:47 ERROR [pool-1-thread-8] Cannot acquire connection from pool
问题根源
我们当时犯了一个经典错误:把K8s当成虚拟机来用。
每个Pod都是一个独立的应用实例,每个实例都有自己的数据库连接池。假设我们设置了:
- Pod副本数:10个
- 每个Pod连接池大小:100
那么理论上,集群峰值时数据库需要承载 10 × 100 = 1000 个并发连接。
而我们的数据库配置是:
max_connections = 500
一眼看上去500够用了,但实际上K8s的Pod重启、滚动更新、弹性伸缩都会瞬间拉高连接数。当新Pod启动时,旧Pod还没完全终止,两边的连接池同时活跃,直接就把数据库干崩了。
解决方案
1. 统一数据库连接池配置
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 5
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:v1.2.3
env:
- name: DB_MAX_CONNECTIONS
value: "20" # 5个Pod × 20 = 100个连接,留足余量
- name: DB_POOL_INIT_SIZE
value: "5"
- name: DB_POOL_MIN_IDLE
value: "2"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
2. 使用连接池中间件
对于Java应用,推荐使用HikariCP并做如下配置:
// 连接池配置
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(Integer.parseInt(System.getenv("DB_MAX_CONNECTIONS")));
config.setMinimumIdle(Integer.parseInt(System.getenv("DB_POOL_MIN_IDLE")));
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
// 关键:优雅关闭连接池
@PreDestroy
public void shutdown() {
hikariDataSource.close();
}
3. 数据库侧做连接数限制
-- PostgreSQL配置
max_connections = 800
superuser_reserved_connections = 3
-- 为每个服务分配专用角色,限制连接数
CREATE ROLE user_service WITH LOGIN PASSWORD 'xxx';
ALTER ROLE user_service CONNECTION LIMIT 200;
二、第二次崩溃:内存泄漏的”温水煮青蛙”
发生了什么
第二次崩溃来得很隐蔽。一开始只是个别Pod被OOMKilled,我们以为是资源给少了,加大了内存限制。
结果几天后,更多Pod开始被驱逐,集群整体性能下降,最后整个服务不可用。
kubectl describe pod user-service-7d4b8c6f5-x2k9m
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2h default-scheduler Successfully assigned default/user-service-7d4b8c6f5-x2k9m to node-3
Normal Pulled 1h kubelet Container image "user-service:v1.3.0" already present on machine
Warning OOMKilled 30m kubelet Container user-service was OOM killed
问题根源
K8s的内存限制是cgroup级别的,包括堆内存 + 非堆内存 + 容器内其他进程。
很多应用只关注堆内存,忽略了:
- JVM的非堆内存(Metaspace)
- 线程栈内存
- 直接内存(Direct Memory)
- 其他守护进程
我们的应用是Java服务,问题出在这里:
// 问题代码:直接内存泄漏
public class MemoryLeakExample {
private static final int DIRECT_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
public void processRequest(Request req) {
// 每次请求分配直接内存,但没有释放
ByteBuffer buffer = ByteBuffer.allocateDirect(DIRECT_BUFFER_SIZE);
buffer.put(req.getData());
buffer.flip();
// 使用buffer...
// 忘记释放!内存泄漏
}
}
当JVM使用直接内存时,这些内存不受堆内存GC控制,但会受到cgroup内存限制的影响。一旦超出限制,就会被OOMKilled。
解决方案
1. 正确配置JVM内存参数
env:
- name: JAVA_OPTS
value: |-
-Xms256m
-Xmx384m
-XX:MetaspaceSize=64m
-XX:MaxMetaspaceSize=128m
-XX:MaxDirectMemorySize=128m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof
2. 修复直接内存泄漏
public class SafeMemoryExample {
private static final int DIRECT_BUFFER_SIZE = 10 * 1024 * 1024;
public void processRequest(Request req) {
// 使用try-with-resources确保释放
try (ByteBuffer buffer = ByteBuffer.allocateDirect(DIRECT_BUFFER_SIZE)) {
buffer.put(req.getData());
buffer.flip();
// 使用buffer...
} catch (Exception e) {
log.error("Processing failed", e);
}
}
}
3. 添加内存监控和告警
# Prometheus监控配置
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: user-service
spec:
selector:
matchLabels:
app: user-service
endpoints:
- port: metrics
path: /actuator/prometheus
interval: 15s
---
# AlertManager告警规则
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: memory-alerts
spec:
groups:
- name: memory
rules:
- alert: HighMemoryUsage
expr: container_memory_working_set_bytes / container_spec_memory_limit_bytes > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.container }}"
description: "Memory usage is above 80% of limit"
- alert: ContainerOOMKilled
expr: kube_pod_container_status_last_terminated_reason == "OOMKilled"
for: 0m
labels:
severity: critical
annotations:
summary: "Container {{ $labels.container }} was OOM killed"
三、第三次崩溃:配置管理的”蝴蝶效应”
发生了什么
第三次崩溃最冤枉。一个运维同学在更新ConfigMap时,误操作导致所有Pod同时重启。重启后,API Server因为配置不一致开始拒绝服务,整个集群瘫痪了20分钟。
kubectl get pods
NAME READY STATUS RESTARTS AGE
user-service-7d4b8c6f5-abc12 0/1 CrashLoopBackOff 5 10m
user-service-7d4b8c6f5-def34 0/1 CrashLoopBackOff 4 10m
api-gateway-5c8d9e7f6-ghi56 0/1 Error 3 10m
问题根源
K8s的ConfigMap更新不会自动触发Pod重启,但我们的应用配置了热加载,导致应用行为不一致。更严重的是,我们使用的某些中间件(比如Spring Cloud Config)在配置不一致时会触发集群级别的故障。
问题出在我们没有做好以下几点:
- 没有对ConfigMap做版本管理
- 没有测试配置变更的影响
- 没有设置适当的滚动更新策略
- 没有备份和回滚机制
解决方案
1. 使用ConfigMap版本化和GitOps
# 使用argocd进行配置管理
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: user-service
spec:
project: default
source:
repoURL: https://github.com/company/k8s-configs.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
2. 安全的滚动更新策略
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多比期望副本数多1个
maxUnavailable: 1 # 最多1个Pod不可用
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:v1.3.1
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
3. 配置变更的前置检查脚本
#!/bin/bash
# validate-config.sh - 配置变更前置检查
CONFIG_FILE=$1
DEPLOYMENT_NAME=$2
echo "=== 配置验证开始 ==="
# 检查配置文件语法
if ! kubectl apply --dry-run=server -f "$CONFIG_FILE" > /dev/null 2>&1; then
echo "ERROR: 配置语法验证失败"
exit 1
fi
# 检查是否会触发滚动更新
CURRENT_IMAGE=$(kubectl get deployment "$DEPLOYMENT_NAME" -o jsonpath='{.spec.template.spec.containers[0].image}')
NEW_IMAGE=$(kubectl diff -f "$CONFIG_FILE" | grep -A5 "image:" | tail -1 | awk '{print $2}')
if [ "$CURRENT_IMAGE" != "$NEW_IMAGE" ]; then
echo "WARNING: 检测到镜像版本变更: $CURRENT_IMAGE -> $NEW_IMAGE"
echo "请确认是否要继续?(yes/no)"
read response
if [ "$response" != "yes" ]; then
echo "已取消部署"
exit 0
fi
fi
# 检查资源限制是否合理
MEMORY_LIMIT=$(kubectl get deployment "$DEPLOYMENT_NAME" -o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}')
if [ -n "$MEMORY_LIMIT" ]; then
echo "当前内存限制: $MEMORY_LIMIT"
if [[ "$MEMORY_LIMIT" =~ ^[0-9]+Gi$ ]] && [ "${MEMORY_LIMIT%Gi}" -lt 1 ]; then
echo "WARNING: 内存限制小于1Gi,可能导致OOM"
fi
fi
echo "=== 验证通过 ==="
exit 0
四、避坑总结:K8s不是银弹
经历了三次崩溃后,我们总结了一套K8s部署 checklist,每次上线前都会过一遍:
1. 资源规划
| 检查项 | 建议 |
|---|---|
| 内存限制 | 根据实际使用量 × 1.5 设置,预留缓冲 |
| CPU限制 | 设置requests和limits,避免CPU饥饿 |
| 存储策略 | 使用PV/PVC,避免空目录挂载 |
| 网络策略 | 配置NetworkPolicy,限制Pod间通信 |
2. 健康检查
# 必须配置readiness和liveness探针
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
3. 日志和监控
# 日志采集配置
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
data:
fluent.conf: |
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
<parse>
@type json
time_key time
time_format %Y-%m-%dT%H:%M:%S.%NZ
</parse>
</source>
<match kubernetes.**>
@type elasticsearch
host elasticsearch
port 9200
logstash_format true
logstash_prefix k8s
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_interval 5s
chunk_limit_size 2M
timeout_wait_period 30s
</buffer>
</match>
4. 应急准备
# 常用故障排查命令
# 查看Pod状态
kubectl get pods -o wide
# 查看Pod事件
kubectl describe pod <pod-name>
# 查看日志
kubectl logs <pod-name> -f
kubectl logs <pod-name> --previous # 查看上次崩溃的日志
# 进入Pod调试
kubectl exec -it <pod-name> -- /bin/sh
# 查看资源使用
kubectl top pods
kubectl top nodes
五、写给正在犹豫上K8s的团队
说实话,K8s不是随便能上的。它带来的好处很明显:弹性伸缩、滚动更新、服务发现、自我修复。但代价也不小:
- 学习曲线陡峭:需要理解Pod、Service、Deployment、ConfigMap、Secret、PV/PVC等概念
- 运维成本高:需要专门的K8s运维人员,或者购买托管服务
- 故障排查复杂:问题可能出现在应用层、容器层、K8s层、云平台层
我们的建议是:
- 从小规模开始:先上线1-2个非核心服务,积累经验
- 做好监控和日志:没有监控的K8s就是盲人摸象
- 编写自动化脚本:把重复操作脚本化,减少人为错误
- 建立SOP和应急预案:出了问题知道怎么处理
最后说一句:K8s崩了三次不可怕,可怕的是崩了不知道原因,或者知道了还犯同样的错误。每次崩溃后我们都做了复盘,整理了经验教训,现在我们的K8s集群已经稳定运行了6个月。
希望我们的血泪教训能帮到大家。如果有问题,欢迎交流。
