配置进镜像、弹性靠人手------三个都会炸的 P0 隐患
把配置打进镜像、密码硬编码进代码、弹性伸缩靠人手动敲命令------这三个坑任何一个在生产环境爆了都是 P0。典型场面:镜像里硬编码了测试库密码,上线连不上数据库、探针反复失败、Pod 全部 CrashLoopBackOff;白天流量一涨 CPU 打满,网关超时像雪崩蔓延,只能一群人边改镜像边手动扩容。K8s 用三个标准对象一次性解决:ConfigMap 管非敏感配置(与镜像分离)、Secret 管敏感凭据、HPA 管基于指标的自动扩缩容。
本文按生产标准把 Spring Boot 微服务搬进 K8s:先用 ConfigMap 剥离环境配置、用 Secret 存密码与 API Key(含 base64 只是编码的提醒),再给出可直接 apply 的生产级 Deployment(三探针分工、非 root 运行、资源限制),接着演示 Spring AI 服务读取注入的环境变量,最后配 HPA(metrics-server + 自定义指标)并对 AI 服务"不能只看 CPU"给出进阶方案。读完能交付一个配置分离、凭据安全、能随流
一、ConfigMap:让配置从镜像里滚出去
ConfigMap 是 K8s 里专门用来存非敏感配置的对象。它的作用就一个:把环境相关的配置(数据库地址、缓存地址、日志级别、业务开关等)从容器镜像里剥离出来。
镜像里只放代码和不变的东西;环境相关的配置,交给 ConfigMap。这样同一个镜像,在开发、测试、生产三套环境里都能复用。
1.1 准备一个 ConfigMap
下面这个例子,给一个 Spring Boot + Spring AI 的问答服务配置外部依赖:
java
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ai-service-config
namespace: default
data:
# 应用层配置
SPRING_PROFILES_ACTIVE: "prod"
SERVER_PORT: "8080"
LOGGING_LEVEL_ROOT: "INFO"
# 外部依赖地址
SPRING_DATASOURCE_URL: "jdbc:postgresql://pg-primary.default.svc.cluster.local:5432/ai_db"
SPRING_REDIS_HOST: "redis.default.svc.cluster.local"
SPRING_REDIS_PORT: "6379"
# Spring AI 配置(模型地址、默认模型名)
SPRING_AI_DASHSCOPE_API_URL: "https://dashscope.aliyuncs.com/api/v1"
SPRING_AI_DASHSCOPE_CHAT_OPTIONS_MODEL: "qwen-turbo"
K8s 会把
data里的键值对注入到容器环境变量。Spring Boot 的application.yml或者@ConfigurationProperties会自动读取这些SPRING_*开头的变量,因为它们遵循 relaxed binding 规则。
1.2 两种使用姿势
| 方式 | 适用场景 | 注意点 |
|---|---|---|
env / envFrom 注入环境变量 |
少量键值对、需要被应用直接读取 | 变量名会被转换成大写,注意兼容 |
| 挂载为 Volume 文件 | 整个配置文件,例如 application-prod.yml |
修改 ConfigMap 后,Volume 里的文件会热更新,但应用需自己监听重载 |
Spring Boot 云原生部署,推荐用环境变量方式。简单、直观、和 12-Factor App 一致。
二、Secret:别把密码当成普通配置
Secret 和 ConfigMap 长得很像,但它专门存敏感数据:数据库密码、API Key、TLS 证书、OAuth2 Client Secret 等。
很多人有个误解,以为 Secret 里的 base64 是加密。不是,它只是编码。任何拿到 YAML 的人都能一眼解码。所以 Secret 的核心价值是:
-
与 ConfigMap 职责分离,最小权限管理;
-
K8s 对 Secret 有额外的存储和处理限制(默认不落地 etcd 明文,可开启静态加密);
-
可以挂载为文件,避免密码出现在环境变量里被
ps/env看到。
2.1 创建 Secret
先准备好密码文件,避免在 shell history 里留下明文:
bash
# 推荐:从文件创建,命令行历史不会留密码
echo -n 'P@ssw0rd!2024' > db-password.txt
echo -n 'sk-xxxxxxxxxxxxxxxx' > ai-api-key.txt
kubectl create secret generic ai-service-secret \
--from-file=DB_PASSWORD=db-password.txt \
--from-file=AI_API_KEY=ai-api-key.txt \
--dry-run=client -o yaml > secret.yaml
# 用完立刻删掉本地明文文件
rm db-password.txt ai-api-key.txt
生成的 secret.yaml 大概长这样:
java
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: ai-service-secret
namespace: default
type: Opaque
stringData:
# 这里用 stringData,kubectl 会自动帮你 base64
DB_PASSWORD: "P@ssw0rd!2024"
AI_API_KEY: "sk-xxxxxxxxxxxxxxxx"
生产环境务必启用 etcd 静态加密,并配合 RBAC 限制谁能读取 Secret。更严格的企业可以用 External Secrets Operator 对接 Vault / 阿里云 KMS。
三、Deployment:把 ConfigMap 和 Secret 用起来
下面是一个可以直接 apply 的生产级 Deployment。重点看几个地方:
-
envFrom把 ConfigMap 和 Secret 批量注入环境变量; -
配置了
resources.requests/limits,这是 HPA 计算 CPU 利用率的前提; -
配置了
startup/liveness/readiness三个探针; -
使用非 root 用户运行容器。
java
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-service
namespace: default
labels:
app: ai-service
spec:
replicas: 2 # HPA 接管后会动态调整,这里只是初始值
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # 滚动更新时不允许中断
selector:
matchLabels:
app: ai-service
template:
metadata:
labels:
app: ai-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: ai-service
image: registry.cn-hangzhou.aliyuncs.com/demo/ai-service:1.2.0
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: ai-service-config
- secretRef:
name: ai-service-secret
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
# 启动探针:应用启动慢时用,防止启动阶段被 liveness 误杀
startupProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 12 # 最多给 60 秒启动时间
# 存活探针:应用卡死时重启容器
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 10
failureThreshold: 3
# 就绪探针:流量只打给 Ready 的 Pod
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
periodSeconds: 5
failureThreshold: 3
3.1 三个探针的分工
很多人把三个探针配成一样的路径,其实浪费。Spring Boot 3.x 的 Actuator 已经内置分组:
-
/actuator/health/liveness:只要 JVM 还活着就返回 UP。适合 livenessProbe。 -
/actuator/health/readiness:应用依赖(数据库、Redis)都就绪才返回 UP。适合 readinessProbe。
如果你把它们都配成 /actuator/health,会出现一种很坑的情况:数据库挂了, readiness 应该让 Pod 下线,但 liveness 也会失败,K8s 就会把 Pod 重启。结果数据库没好,Pod 反复重启,形成"重启风暴"。
3.2 Spring Boot 端开启 Actuator 健康分组
java
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
probes:
enabled: true # 自动生成 liveness/readiness 分组
show-details: always
metrics:
tags:
application: ${spring.application.name}
四、Java 代码:让应用拥抱 K8s 环境
下面是一段极简的 Spring AI 服务代码,演示如何读取 ConfigMap/Secret 注入的环境变量,并暴露一个问答接口。
java
// AiChatController.java
package com.example.ai.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/ai")
public class AiChatController {
private final ChatClient chatClient;
// 从 Secret 注入的 API Key 已经被 Spring AI starter 自动读取,
// 这里仅作演示如何显式读取环境变量做校验或日志
@Value("${spring.ai.dashscope.api-key:}")
private String apiKey;
@Value("${spring.ai.dashscope.chat.options.model:qwen-turbo}")
private String modelName;
public AiChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一位经验丰富的 Java 后端工程师,回答简洁、有代码示例。")
.build();
}
@PostMapping("/chat")
public String chat(@RequestParam String question) {
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("AI API Key 未配置,请检查 Secret 是否挂载");
}
return chatClient.prompt()
.user(question)
.call()
.content();
}
@GetMapping("/model")
public String currentModel() {
return "当前模型: " + modelName;
}
}
4.1 配置类读取环境变量
java
// AiServiceProperties.java
package com.example.ai.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "ai.service")
public class AiServiceProperties {
private int maxTokens = 1024;
private double temperature = 0.7;
private boolean logRequest = false;
// getter / setter 省略...
}
通过 envFrom 注入的环境变量,例如 AI_SERVICE_MAX_TOKENS=2048,Spring Boot 会自动绑定到 AiServiceProperties 上。配合 ConfigMap,你可以在不重新打包镜像的情况下,调整业务参数。
五、HPA:让服务自己"长"出来
Horizontal Pod Autoscaler(HPA)是 K8s 的横向扩缩容控制器。它会根据 CPU、内存或自定义指标,自动调整 Deployment 的副本数。
5.1 先装 metrics-server
HPA 需要 metrics-server 提供 Pod 的实时资源使用数据:
java
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 如果内网环境没有有效证书,加上 --kubelet-insecure-tls
kubectl patch deployment metrics-server -n kube-system \
--type='json' \
-p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'
5.2 HPA 配置示例
java
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ai-service-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ai-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # CPU 平均利用率达到 60% 开始扩容
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 15 # 15 秒内最多扩容 100%
scaleDown:
stabilizationWindowSeconds: 300 # 缩容更保守,等 5 分钟确认流量真的降了
policies:
- type: Percent
value: 10
periodSeconds: 60
5.3 压测验证
bash
# 1. 查看当前状态
kubectl get hpa ai-service-hpa --watch
# 2. 用 hey 或 wrk 打流量
hey -z 60s -c 50 -q 10 \
-m POST \
-d "question=解释一下Spring%20Boot自动配置原理" \
http://<ai-service-ip>/api/ai/chat
# 3. 观察 Pod 数量变化
kubectl get pods -l app=ai-service --watch
5.4 AI 服务为什么不要只看 CPU
大模型调用有个特点:单次请求 CPU 不高,但耗时很长。如果你只按 CPU 扩容,可能在请求排队时已经超时了。
更合理的做法是用自定义指标,比如:
-
正在处理的请求数(in-flight requests)
-
请求队列长度
-
Token 生成延迟 P99
可以用 Prometheus Adapter 把这些指标暴露给 HPA:
java
metrics:
- type: Pods
pods:
metric:
name: http_server_requests_seconds_max
target:
type: AverageValue
averageValue: "1.5" # 平均响应时间超过 1.5s 就扩容
这算是给 AI 服务做弹性伸缩的进阶版本,后续在云厂商托管 K8s 上可以用更成熟的 Custom Metrics API。
六、完整部署流程
把前面的 YAML 按顺序 apply 一遍:
bash
# 1. 创建配置和凭据
kubectl apply -f secret.yaml
kubectl apply -f configmap.yaml
# 2. 部署应用
kubectl apply -f deployment.yaml
# 3. 暴露服务
kubectl apply -f service.yaml
# 4. 开启弹性伸缩
kubectl apply -f hpa.yaml
# 5. 验证
kubectl get pods -l app=ai-service
kubectl get svc ai-service
kubectl get hpa ai-service-hpa
Service 可以用 ClusterIP,配合 Ingress 做七层路由:
java
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: ai-service
namespace: default
spec:
selector:
app: ai-service
ports:
- port: 80
targetPort: 8080
name: http
type: ClusterIP
七、实战建议:老兵的三个血泪经验
1. Secret 不要只依赖 base64,生产必须上加密和权限
-
开启 etcd 静态加密;
-
用 RBAC 限制 Secret 读取范围,只给需要的 ServiceAccount;
-
考虑 External Secrets Operator,把凭据存在 Vault / 云厂商 KMS 里,K8s 里只存引用。
2. HPA 的阈值不是默认值 50%,而是压测出来的
-
先用
hey/wrk/ JMeter 压出服务的 CPU-吞吐量曲线; -
找到 CPU 利用率与响应时间开始恶化的拐点,把 HPA 目标设在这个点之前;
-
设置
behavior.scaleDown.stabilizationWindowSeconds,防止流量抖动导致 Pod 反复创建销毁。
3. readiness 探针必须能真实反映服务能力
-
不要只检查
/actuator/health,要检查/actuator/health/readiness; -
如果依赖了数据库、Redis、消息队列,要让 readiness 在这些依赖不可用时返回 DOWN;
-
配合
PodDisruptionBudget保证发布和缩容时至少有 N 个可用副本。
java
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ai-service-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: ai-service
下篇预告
"云原生不是把应用搬到 K8s 上,而是让应用学会在云里自己呼吸。"
ConfigMap 让配置流动起来,Secret 让敏感信息有边界,HPA 让服务能随流量起伏而生长。把这三件事做对,你的 Spring Boot 服务才算真正在 K8s 里站稳了脚跟。
下篇 Day59,我们走进阿里云 ACK 生产级集群:节点池怎么规划、SLS 日志采集怎么配、ARMS 监控告警怎么做。云上的坑,比本地 Minikube 多得多,但也爽得多。
参考版本:
-
Spring Boot 3.2.x
-
Spring AI 1.0.0-M3
-
Kubernetes 1.29+
-
metrics-server v0.7.x