Kubernetes: 生产级配置技巧
生产级配置技巧
以下都是生产环境中的普遍做法(见 Kubernetes
一文的「Charts
实践」),多数解决的是文档不会明说、但线上一定会踩的坑
优雅停机
Pod收到终止信号后,Service/Istio的端点列表传播存在延迟,若容器立即退出,仍会有少量请求被路由到已死的Pod解决方式是让容器先睡一会儿再退出,用
preStop钩子桥接端点传播延迟,同时放宽terminationGracePeriodSeconds给应用留足清理时间1
2
3
4
5
6
7
8spec:
terminationGracePeriodSeconds: 60 # 给 preStop + 应用关闭留足时间(默认仅 30s)
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 15"] # 等待 LB/网格端点列表更新,期间继续处理存量请求
HPA 与
replicas 的冲突
Deployment的spec.replicas与HPA会互相打架:HPA扩容后,下一次apply会把副本数改回yaml里的值解决方式是在模板层做互斥:仅当
HPA未启用时才渲染replicas字段,启用HPA后副本数完全交给自动扩缩1
2
3
4spec:
{{- if and (not .Values.hpa.enabled) (hasKey .Values "replicas") }}
replicas: {{ .Values.replicas }}
{{- end }}
配置变更也触发滚动重启
滚动更新只比较
Pod模板哈希,若仅改了环境变量之外的间接配置(如依赖的配置中心),helm upgrade后Pod不会重建,容易造成“升级了但没生效”解决方式是注入一个每次渲染都变化的随机注解,强制每次发布都滚动重启
1
2
3
4
5spec:
template:
metadata:
annotations:
rollme: {{ randAlphaNum 5 | quote }} # 每次 helm upgrade 都变化,强制重建 Pod
Job 与 sidecar
的死锁
被注入
Istiosidecar的Job可能永远不结束:业务容器退出后,Envoysidecar仍在运行,Pod无法到达Completed状态解决方式是给任务类
Pod显式关闭注入1
2
3
4
5
6spec:
template:
metadata:
annotations:
sidecar.istio.io/inject: "false" # 数据库迁移等一次性 Job 不需要网格能力
backoffLimit: 2 # 快速失败,避免反复重试污染数据同理,
CronJob若只是访问数据库或外部API,也应关闭注入以避免资源浪费与完成阻塞
探针的节奏控制
探针不是越灵敏越好:
periodSeconds过短 + 应用GC停顿会导致误判重启,形成重启风暴(流量涌入冷启动应用 → 更慢 → 更多失败)生产实践:存活探针放宽到
60s一次,就绪探针保持15s,timeoutSeconds: 10容忍慢响应Spring Boot应用直接使用actuator的分层健康端点,避免把依赖检查混入存活探针1
2
3
4
5
6
7
8
9
10
11
12
13
14livenessProbe:
httpGet:
path: /actuator/health/liveness # 只关心进程是否活着
port: 8080
initialDelaySeconds: 10
periodSeconds: 60 # 放宽节奏,避免 GC 停顿引发误杀
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness # 关心依赖是否就绪
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 10
免注解的指标采集
不部署
ServiceMonitor时,Prometheus可通过Pod注解自动发现抓取目标,应用零配置接入监控1
2
3
4
5
6
7spec:
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
容器感知的 JVM 调优
JVM默认按宿主机内存分配堆,在容器里会严重超配;同时CPU limit会被JVM识别为核心数,影响GC线程数生产实践:
requests与limits设为相同值保证资源独占,再用环境变量覆盖JVM行为1
2
3
4
5
6
7
8
9
10
11
12resources:
requests:
cpu: "2"
memory: 4Gi
limits: # 与 requests 一致,避免 CPU 限流引发长尾延迟
cpu: "2"
memory: 4Gi
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:ActiveProcessorCount=2 -XX:MaxDirectMemorySize=128M
-XX:InitialRAMPercentage=50 -XX:MinRAMPercentage=75 -XX:MaxRAMPercentage=75ActiveProcessorCount显式指定核心数,不受CPU配额干扰RAMPercentage按容器内存的百分比分配堆,替代写死的-Xmx
helm hook 编排发布顺序
私有镜像的拉取凭证必须在第一个
Pod创建前就绪,否则首个Deployment会卡在ImagePullBackOff用
pre-install/pre-upgrade钩子提前创建,hook-weight保证其先于其他钩子资源执行;数据库迁移等任务则用post-install钩子跟上,weight决定多个任务的先后1
2
3
4
5
6
7
8
9
10apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: my-app-regcred
annotations:
"helm.sh/hook": pre-install,pre-upgrade # 在 Deployment 之前创建
"helm.sh/hook-weight": "-5" # 数值越小越先执行
data:
.dockerconfigjson: {{ template "imagePullSecret" . }}1
2
3
4
5
6
7apiVersion: batch/v1
kind: Job
metadata:
name: my-app-migration
annotations:
"helm.sh/hook": post-install,post-upgrade # 应用就绪后执行数据库迁移
"helm.sh/hook-weight": "0"凭证
Secret需要在使用它的每个命名空间各放一份(如cert-manager拉取镜像的命名空间),模板中渲染多份即可
对象存储挂载为本地盘
通过各云厂商的
CSI驱动,把对象存储桶直接挂载为PV,应用像读写本地目录一样访问OSS/GCS,无需引入SDKGCS的方式最简单,注解即可启用GKE托管的gcsfusesidecar,还能单独限制sidecar资源1
2
3
4
5
6
7spec:
template:
metadata:
annotations:
gke-gcsfuse/volumes: "true" # 让 GKE 注入 gcsfuse sidecar
gke-gcsfuse/cpu-limit: "250m"
gke-gcsfuse/memory-limit: "256Mi"OSS需要手写静态PV,两个细节:每个桶用独立的storageClassName防止默认供给器抢占绑定;PVC用selector按标签钉死要绑定的PV1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34apiVersion: v1
kind: PersistentVolume
metadata:
name: my-app-oss-csi-pv-0
labels:
alicloud-pvname: my-app-oss-0 # 供 PVC selector 匹配
spec:
storageClassName: my-app-csi-oss-0 # 每桶唯一,避免被动态供给拦截
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
csi:
driver: ossplugin.csi.alibabacloud.com
volumeHandle: my-app-oss-csi-pv-0
volumeAttributes:
bucket: my-bucket
url: oss-cn-hangzhou-internal.aliyuncs.com
akId: "<access-key>"
akSecret: "<secret-key>"
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-app-pvc-0
spec:
storageClassName: my-app-csi-oss-0
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi # 对象存储不校验,仅作形式声明
selector:
matchLabels:
alicloud-pvname: my-app-oss-0 # 按标签绑定指定 PV有状态监控组件(如
Prometheus)重装时应保留数据:手写PV直接引用已存在的云盘(volumeHandle填云盘ID),配合persistentVolumeReclaimPolicy: Retain,删除chart重装后数据仍在1
2
3
4
5
6
7
8
9
10
11
12
13
14apiVersion: v1
kind: PersistentVolume
metadata:
name: monitoring
spec:
storageClassName: monitoring
accessModes:
- ReadWriteOnce
capacity:
storage: 500Gi
csi:
driver: udisk.csi.ucloud.cn
volumeHandle: "<已有云盘 ID>" # 复用存量云盘
persistentVolumeReclaimPolicy: Retain # PVC 删除后保留云盘与数据
出站默认拒绝与元数据防护
应用被
SSRF攻击时,攻击者最先尝试的就是云厂商的元数据服务地址,拿到的是临时云凭证,可直接操纵云资源生产实践:
NetworkPolicy默认拒绝所有出站,仅放行DNS,并显式排除元数据网段1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-app
spec:
podSelector:
matchLabels:
app: my-app
policyTypes:
- Egress
egress:
- to: # 放行集群 DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- to: # 放行其余公网,但屏蔽元数据网段
- ipBlock:
cidr: 0.0.0.0/0
except:
- 100.64.0.0/10 # 阿里云等 CG-NAT 网段(含 metadata 100.100.100.200)
- 169.254.0.0/16 # link-local(含 AWS/GCP/Azure 169.254.169.254)在此之上可为特殊角色开白名单,例如调试用的
adminshellPod只允许访问API Server端口,不开内网通行证1
2
3
4
5
6
7
8
9egress:
- to: # 仅允许访问 apiserver
- ipBlock:
cidr: 10.0.0.0/8
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 6443
应用内嵌调试终端
让应用持有专属
ServiceAccount与最小RBAC(创建Job、pods/exec、读写Secret),即可在网页里一键拉起带完整工具链的调试Pod,进容器排查线上问题关键是权限最小化:
Role只授予单个命名空间内的少量动词,调试Pod再用NetworkPolicy隔离1
2
3
4
5
6
7
8
9
10
11
12
13
14apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-app-shell
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "get"]1
2
3
4
5
6# 应用 Pod 将自身命名空间下发给调试 Pod,无需硬编码
env:
- name: TOOLS_SHELL_POD_NAMESPACE
valueFrom:
fieldRef: # Downward API:把 Pod 自身信息注入环境变量
fieldPath: metadata.namespace
网关证书的域名数量上限
IstioGateway的单个server块在证书SAN数量上有实际上限(约100个域名),域名多时会直接报错解决方式是按
99个一组拆分server块,每组配独立的证书Secret,模板层用循环批量生成1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: my-app-tls-1 # 第一组证书
hosts: # ≤ 99 个域名
- a.example.com
- port:
number: 443
name: https-2 # 端口相同,name 必须不同
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: my-app-tls-2 # 第二组证书
hosts:
- b.example.com
入口层的最小暴露面
业务不需要的端点一律在网关层直接
404,而不是靠应用代码防御:管理端点(/actuator/*)只放行健康检查,文档路由(/docs)按环境开关1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24http:
- match: # 禁用文档路由
- uri:
prefix: /docs
directResponse: # 不经过后端直接返回
status: 404
- match: # 仅放行健康检查
- uri:
exact: /actuator/health
route:
- destination:
host: my-app
port:
number: 80
- match: # 其余管理端点全部 404
- uri:
prefix: /actuator
directResponse:
status: 404
- route: # 正常业务流量
- destination:
host: my-app
port:
number: 80多产品共用一套部署时,可在网关层按域名路由并注入产品标识
Header,后端无感知区分租户1
2
3
4
5
6
7
8
9
10
11
12
13
14http:
- match:
- headers:
":authority": # Envoy 的 Host 头
exact: product-a.example.com
route:
- destination:
host: my-app
port:
number: 80
headers:
request:
set:
X-Product: "a" # 后端读取该 Header 区分产品
全链路请求 ID 透传
外部客户端带来的
X-Request-Id默认不会在网关处保留,导致跨服务的日志无法串联用
EnvoyFilter直接修改网关配置,让网关透传外部请求ID,全链路日志与trace得以对齐1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22apiVersion: networking.istio.io/v1
kind: EnvoyFilter
metadata:
name: ingress-preserve-request-id
spec:
workloadSelector:
labels:
istio: ingress # 作用于 ingress gateway Pod
configPatches:
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
preserve_external_request_id: true
环境即命名空间
用污点 + 容忍度把节点按环境隔离:
staging命名空间的发布只调度到staging节点,互不抢资源;命名空间统一打上istio-injection标签,新服务自动入网格模板层以
Release.Namespace推导默认值,用户不配置则自动隔离1
2
3
4
5
6# 节点打污点: kubectl taint node node1 example.io/environment=production:NoSchedule
tolerations:
- key: example.io/environment
operator: Equal
value: {{ .Release.Namespace | quote }} # 容忍度跟随命名空间,自动匹配环境节点
effect: NoSchedule1
2
3
4
5
6apiVersion: v1
kind: Namespace
metadata:
name: staging
labels:
istio-injection: enabled # 命名空间内新建 Pod 自动注入 sidecar