ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringAI 智能审核上 K8s:容器化到高可用部署全解

SpringAI 智能审核上 K8s:容器化到高可用部署全解 这套 SpringAI 智能审核项目写到现在已经是第十八掌。前面我们已经折腾过模型接入、提示词调优、Redis 集群缓存也解决过并发压测下各种玄学问题但说实话代码能跑和能稳定跑完全是两回事。这一掌取名“神龙摆尾”意思很简单前面十七掌攒下的功力到最后要真正落到生产环境——用 K8s 把整套服务托上云。神龙摆尾不是花架子它是整套降龙掌法里最需要回头审视自己弱点的那一招。在这一章里我会把 SpringAI 服务容器化、推上 Kubernetes 集群、接好配置中心、连上 Redis 集群再把常见故障一个个拆开揉碎讲清楚。适合已经跑通 SpringAI、但还没正经把服务送上 K8s 的读者照着做能少踩一大半我踩过的坑。1. 关于这一章神龙摆尾要解决什么1.1 为什么把部署放到第十八掌我先说一个很多人的误区项目做到能跑跟项目做到能上线中间还隔着一条很宽的河。我之前接触过不少 SpringAI 项目很多朋友在本地跑得飞快一说到部署就犯怵。要么是 Docker 镜像没做精简几百 MB 的 jar 包塞进去启动要一分多钟要么是把 Redis 连接地址写死在 application.yml 里换套环境就得改代码重新打包还有更头大的多副本一开日志散得到处都是排查问题两眼一抹黑。这一掌之所以放在第十八掌是因为它依赖前面所有内容没有合理的提示词配置策略上 K8s 之后就要频繁改镜像没有 Redis 集群做缓存线上突发流量一来大模型接口被调用方限流整个审核服务就会被打穿。所以我一直建议部署不是最后一公里部署是前面所有设计的总验收。神龙摆尾这一招本质上是回头把自己的弱点全部补齐。1.2 整条技术链路的全貌为了保证后面操作不跑偏我先把这一整套 SpringAI 智能审核服务的架构摆出来。入口层用户通过接口提交待审核内容比如合同文本、工单描述、用户评论。审核服务层基于 SpringAI 的智能审核服务负责调度大模型完成敏感信息识别、合规风险判断、内容打分等任务。大模型层这里以阿里云通义千问接入为例SpringAI 官方也支持 OpenAI、Ollama 等改配置就能切换。缓存与限流层Redis 集群用来缓存审核结果、做接口限流和幂等控制。部署层K8s 集群运行上面所有服务的容器副本并提供滚动更新、自动扩缩容、故障自愈能力。这个链路里SpringAI 解决的是“怎么把大模型能力编排进业务代码”Redis 集群解决的是“高并发下别把模型侧打出问题”K8s 解决的是“整条链路上的服务如何稳定跑起来”。三者缺一不可。后面每一节我都会围绕这三者展开。2. 容器化SpringAI 上 K8s 前必须想清楚的事2.1 先别急着写 YAML镜像得做得能打我见过很多第一次上 K8s 的人上来就是一把梭写 Deployment结果 Pod 一直 CrashLoopBackOff查了半天发现是镜像本身有问题。容器化这一步虽然不在 K8s 内部但它决定了后面所有步骤能不能顺利。先说镜像选型。SpringAI 应用本质是一个 Spring Boot 项目我用的基础镜像是 eclipse-temurin:17-jre-alpine。为什么不用带 JDK 的因为运行只需要 JRE镜像体积能少三分之一。为什么用 alpine 而不是 ubuntu因为它小而且我们这个场景只做 JSON 解析和 HTTP 调用没有复杂的 native 依赖。但这里有一个坑alpine 镜像默认没有时区数据和字体。如果审核服务要处理 PDF 或者生成图片验证码缺 fontconfig 会直接报字体相关的 NoClassDefFoundError。所以我在 Dockerfile 里做了两步补充。FROM eclipse-temurin:17-jre-alpine RUN apk add --no-cache tzdata fontconfig ttf-dejavu \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ echo Asia/Shanghai /etc/timezone WORKDIR /app COPY target/springai-review.jar app.jar ENV JAVA_OPTS EXPOSE 8080 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]解释一下几个关键点第一设置时区这一步不能省否则容器内时间会是 UTC审核记录的时间戳会跟业务系统对不上。第二ttf-dejavu 是为了保证 Java 图形相关操作不因字体缺失而崩溃。第三JAVA_OPTS 留空方便后期在 K8s 的 env 里注入 JVM 参数。再补一个 .dockerignore这个很多人会漏target/ logs/ .idea/ *.iml漏掉 .dockerignore 的后果是构建上下文会把本地几 GB 的 target 目录发到 Docker daemon轻则构建慢重则直接打爆镜像仓库。这个模板我用了很久SpringAI 项目只要不引入特殊 native 库都能直接套用。2.2 Docker 和 K8s 到底差在哪这个热搜词我每次写部署都要解释一遍因为真的有人以为 K8s 是 Docker 的替代品。不是。Docker 解决的是“单台机器上怎么把应用装进容器并跑起来”K8s 解决的是“很多台机器组成的集群里怎么让容器自动分配到合适的节点、挂了自动重启、流量高峰自动扩展”。打个生活化的比方Docker 是集装箱K8s 是港口调度系统。集装箱解决货物标准化装载调度系统解决几十台吊车、几千个集装箱怎么协调。没有集装箱调度系统无从谈起只有集装箱没有调度系统港口就只能靠人肉搬运。具体到 SpringAI 项目Docker 阶段你只需要保证 docker run -p 8080:8080 springai-review 能起服务。但线上不可能只跑一个实例你至少需要两个副本做高可用流量大了可能要扩到十个。这时候手工 docker run 已经失控你需要告诉 K8s我要跑 3 个副本内存上限 2GiCPU 超过 60% 就再扩两个。K8s 负责把这些事变成声明式配置持续执行。有一点要注意K8s 的容器运行时默认已经不是 Docker而是 containerd。从用户视角看差别不大但排查问题时你会发现 docker ps 看不到 K8s 创建的容器要用 crictl ps。这个我在后面的问题排查里会专门说。3. 环境准备Rocky Linux 安装 K8s 集群3.1 节点规划与系统初始化这一节内容基于我在 Rocky Linux 上的实操版本以 K8s 1.36 为例。版本迭代很快大家操作时以实际安装的官方稳定版本为准核心命令基本一致。先说节点规划。我这边准备了 3 台机器1 台 master 节点2 台 worker 节点。生产环境建议 master 至少 3 台做 HA但我们项目阶段用单 master 够用。硬件上 master 给 4C8Gworker 给 8C16G。SpringAI 服务本身不重但 JVM 和 Redis 集群会吃内存worker 内存给足一点不吃亏。系统初始化这步不能省下面是几个关键操作。第一关闭 swap。K8s 要求节点 swap 关闭否则 kubelet 无法正常工作。用 swapoff -a 只是临时关闭必须注释掉 /etc/fstab 里 swap 相关行。第二加载内核模块并调整系统参数。需要 br_netfilter 和 overlay 模块还要把 net.bridge.bridge-nf-call-iptables 设为 1否则集群内 Service 转发会出问题。cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter cat EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.ipv4.ip_forward 1 net.bridge.bridge-nf-call-ip6tables 1 EOF sudo sysctl --system第三关闭防火墙。按说生产环境防火墙应该开着但 K8s 组件之间的端口很杂教学阶段为了不被各种端口问题折腾我建议先 disable等整个集群跑通再按需放行。真实生产建议用安全组规则替代本机防火墙明确放行 6443、2379、2380、10250、10256 等重点端口。容器运行时我选 containerd这也是目前 K8s 默认推荐的运行时。安装方式很简单yum 装好之后把 config.toml 里的 SystemdCgroup 改成 true否则后面 kubelet 的 cgroup 驱动会和 containerd 冲突Pod 怎么起都起不来。3.2 用 kubeadm 初始化集群镜像源这一步我直接配的是国内镜像源K8s 组件下载速度会快很多。安装 kubelet、kubeadm、kubectl 三件套版本都固定在 1.36。cat EOF /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el9-x86_64/ enabled1 gpgcheck0 EOF sudo yum install -y kubelet kubeadm kubectl sudo systemctl enable --now kubelet初始化 master 节点时我指定了 Pod 网段为 10.244.0.0/16这个是给后面 Flannel 网络插件用的。如果你用 Calico建议换成 192.168.0.0/16。sudo kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --pod-network-cidr10.244.0.0/16执行完之后kubeadm 会输出一段 kubeadm join 命令保存好worker 节点加入集群就要用它。初始化完成后需要配置 kubectl 的 kubeconfigmkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config网络插件我习惯用 Flannel原因很简单配置少、链路干净、对 SpringAI 这种普通业务没有额外性能要求。安装只要一条 manifest 应用kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml装完之后等几分钟就能看到所有节点 Ready。worker 节点上直接跑之前保存的 kubeadm join 命令就行join 命令里带着 token不用额外处理。3.3 namespace 规划是部署前的第一次分治好多人第一次用 K8s 会忽略 namespace所有东西都丢到 default 里。一开始可能没感觉等接入了 Redis 集群、ConfigMap、监控告警就会发现 namespace 其实就是 K8s 里的“租户”概念用来做资源隔离、权限隔离、网络策略隔离。我在这个项目里规划了两个 namespaceprod-ai 和 test-ai。prod-ai 放生产审核服务和 Redis 集群test-ai 放联调环境。这样两个环境之间的逻辑互不干扰而且后面做 RBAC 授权时可以做到只给测试同学分配 test-ai 的权限生产环境他们连看都看不到。创建 namespace 很简单kubectl create ns prod-ai kubectl create ns test-ai有一点要注意很多新人部署 Redis 集群时明明 yaml 都写对了但 Spring 服务连接不上查到最后发现是 Redis 的 Service 在另一个 namespace 里DNS 域名没带 namespace 后缀。K8s 里跨 namespace 访问服务必须用完整域名服务名.命名空间.svc.cluster.local本 namespace 内访问才能用短名。这个坑我在排查实录里还会强调。4. 配置中心与 Redis 集群联动4.1 系统提示词配置怎么落到 K8s 里“SpringAI 系统提示词怎么配置”这个搜索词能上热榜说明大家确实被这个问题卡过。SpringAI 里配置系统提示词其实很灵活但很多人第一反应是把提示词写死在 Java 代码里。我不建议这么做因为提示词是要反复调的。今天审核规则变了明天要加一条敏感词策略如果每次改提示词都要重新打包镜像用不了两周你就会疯。我的做法是把系统提示词放到 K8s 的 ConfigMap 里然后在 Deployment 中用环境变量注入到 Spring 容器。先定义一个 ConfigMapapiVersion: v1 kind: ConfigMap metadata: name: ai-config namespace: prod-ai data: system-prompt: | 你是一个专业的智能审核助手。请从以下维度审核用户提交的文本内容 1. 敏感信息识别身份证号、手机号、银行账号等。 2. 合规风险判断是否存在虚假宣传、绝对化用语。 3. 内容安全识别辱骂、暴力、违禁内容。 4. 格式规范检查是否包含必要字段。 请输出 JSON格式为{level:safe/warn/danger,riskTags:[],score:0}Java 侧只需用 Spring 的 Value 注解读取环境变量Service public class ReviewService { Value(${ai.prompt.system}) private String systemPrompt; public ReviewResult review(String input) { PromptTemplate template new PromptTemplate( {systemPrompt} 待审核内容{input} ); MapString, Object params Map.of( systemPrompt, systemPrompt, input, input ); // 调用大模型并解析结果 } }然后在 Deployment 的 env 里引用 ConfigMapapplication.yml 里写ai.prompt.system: ${AI_PROMPT_SYSTEM}就能对上 Spring 占位符语法env: - name: AI_PROMPT_SYSTEM valueFrom: configMapKeyRef: name: ai-config key: system-prompt这个方式的优势很明显改提示词只需要 kubectl edit configmap然后重启 Pod 或触发滚动更新镜像是完全不变的。线上调提示词十分钟内就能生效这对运营和算法团队来说非常友好。4.2 Redis 集群部署与 Spring 连接参数SpringAI 智能审核服务里Redis 集群承担两个任务一是审核结果缓存相同内容在短时间内重复提交直接返回缓存结果不给大模型重复计费二是限流每个调用方单位时间内的请求数控制在一个阈值防止恶意刷接口把大模型调用额度打光。Redis 集群我用了 3 主 3 从的标准架构在 K8s 里通过 StatefulSet 部署配合 Headless Service 来固定网络标识。如果嫌手动写 StatefulSet 太啰嗦可以直接用 Helm chart 装 bitnami/redis-cluster十分钟能起一套。不过为了讲清楚原理我建议至少手动部一遍理解 StatefulSet 为什么要用稳定网络标识。Spring Boot 连接 Redis 集群的配置是这样spring: data: redis: cluster: nodes: - redis-cluster-0.redis-cluster-headless.prod-ai.svc.cluster.local:6379 - redis-cluster-1.redis-cluster-headless.prod-ai.svc.cluster.local:6379 - redis-cluster-2.redis-cluster-headless.prod-ai.svc.cluster.local:6379 password: ${REDIS_PASSWORD} timeout: 3s配置里的域名必须写完整这个我前面说过跨 namespace 或者通过 Headless Service 访问短名在解析时可能有延迟写完整最稳妥。还有一个容易被忽视的参数是 timeout默认可能只有几秒网络一抖动就报连接超时。我建议调整到 3 秒到 5 秒之间太长会让限流功能失效。Secret 别直接写在 yaml 里。用 kubectl create secret 生成然后在 Deployment 里通过 secretKeyRef 注入。比如kubectl create secret generic redis-secret \ --from-literalpasswordYourStrongPass \ -n prod-ai这个习惯从第一天就要养成不然后面一条配置泄露可能牵出整个集群的访问权限。5. 部署 SpringAI 智能审核服务5.1 编写 Deployment 与 Service探针和资源限制是重点配置中心、Redis、镜像都已经就位现在可以正式把 SpringAI 服务推到 K8s 里。我直接给一份我项目里在用的 Deployment 精简版几乎没有多余字段但该有的都有。apiVersion: apps/v1 kind: Deployment metadata: name: springai-review namespace: prod-ai labels: app: springai-review spec: replicas: 3 selector: matchLabels: app: springai-review template: metadata: labels: app: springai-review spec: imagePullSecrets: - name: registry-secret containers: - name: app image: registry.example.com/springai-review:v1.0.0 ports: - containerPort: 8080 env: - name: AI_PROMPT_SYSTEM valueFrom: configMapKeyRef: name: ai-config key: system-prompt - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-secret key: password - name: JAVA_OPTS value: -Xms512m -Xmx1024m -XX:UseG1GC resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 2Gi readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 30 terminationGracePeriodSeconds: 60讲几个我踩过的关键点。第一resources 一定要写。不写 resources 的 Pod 在调度时会被当成 0 资源需求多个 Pod 可能挤到一台机器上直接导致整机内存超卖OOM 之后 kubelet 开始无差别杀 Pod。requests 设 512Milimits 设 2Gi是给 JVM 留余量。JAVA_OPTS 里 Xmx 写 1024m就是防止 JVM 觉得宿主机内存很大然后无脑申请。第二探针分两种readiness 告诉 K8s“这个 Pod 能不能接流量”liveness 告诉 K8s“这个 Pod 还活着吗”。Spring 应用启动慢类扫描加上 SpringAI 初始化模型客户端三十秒起步很正常所以 initialDelaySeconds 给大一点避免启动阶段就被误杀。第三优雅停机。Spring Boot 要开优雅停机配置server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s同时把 terminationGracePeriodSeconds 设成 60这样 K8s 在滚动更新时会给旧 Pod 一分钟时间处理完当前请求再退出不会出现用户请求发到一半被断连。Service 就简单了内部服务直接 ClusterIPapiVersion: v1 kind: Service metadata: name: springai-review namespace: prod-ai spec: selector: app: springai-review ports: - port: 8080 targetPort: 8080如果要对外暴露可以在上层加一层 Ingress按域名把请求导到 springai-review Service。这个项目里因为审核服务是内部接口我暂时没暴露公网如果你想做成对外 API再补一个 ingress-nginx 就行。5.2 滚动更新与自动扩缩容让服务自己长大服务上了 K8s 之后最爽的一点是发版变成了一个动作而不是一个工程。以前在虚拟机上线先备份 jar再停服务再启动出问题还要回滚。现在改镜像 tag 就行。kubectl set image deployment/springai-review \ appregistry.example.com/springai-review:v1.0.1 \ -n prod-ai这行命令会触发一次滚动更新K8s 会先起一个新 Pod等 readiness 通过后再杀掉一个旧 Pod以此类推整个过程用户无感知。如果新版本启动失败readiness 一直不通过滚动更新会自动暂停不会把流量全部切到坏版本上。自动扩缩容这块我给这个服务配了一个基于 CPU 的 HPA。SpringAI 服务在处理审核请求时CPU 占用和内存通常同步上升以 CPU 为目标比较直接。apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: springai-review-hpa namespace: prod-ai spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: springai-review minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60注意HPA 要生效必须装 metrics-server没有它kubectl top pod 拿不到指标HPA 会一直处于 Unknown 状态。安装非常简单kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml装完之后配好探针HPA 才会“看到”Pod 的实际负载流量一上来Pod 数量会自动从 3 扩到 10。高峰过去又会缓慢缩回 3。这个能力对 SpringAI 这种可能被突发请求打爆的服务价值非常大。6. 常见问题与排查实录6.1 Pod 一直 CrashLoopBackOff先别急着删这是所有 K8s 新手遇到最多的报错。看到 CrashLoopBackOff 第一反应是重新部署其实没用。正确做法是按顺序排查。第一步 kubectl logs 看容器当前输出。SpringAI 应用最常见的原因是提示词配置没拿到或者 Redis 密码错误。日志里会直接看到Could not resolve placeholder AI_PROMPT_SYSTEM或者 Redis 连接异常。第二步看 Eventskubectl describe pod 会显示 OOMKilled 或探针失败。如果是 OOMKilled说明 limits.memory 设小了把 JVM Xmx 调低或者 limits 调大。如果是 Liveness probe failed说明健康检查路径不对或者应用启动真的超过 60 秒把 initialDelaySeconds 再调大。我还遇到过一种情况容器启动到一半就退出日志里没有任何报错。最后发现是 Dockerfile 里的 ENTRYPOINT 写成了 exec 格式变量没展开。这个比较坑但把 ENTRYPOINT 换成我前面那种 sh -c 写法就能解决。6.2 镜像拉取超时和私有仓库认证ImagePullBackOff 是最容易解决的但它背后有两个常见原因。一个是镜像仓库地址不通或者镜像 tag 不存在。先 kubectl describe pod 看 Events 里的具体拉取错误404 就是 tag 不存在timeout 就检查网络。另一个是私有仓库认证。从我的实践经验看很多人是项目初期用 docker login 在本机登录了仓库但 K8s 里的 Pod 并不会自动继承这个登录状态。你需要先创建一个 imagePullSecretskubectl create secret docker-registry registry-secret \ --docker-serverregistry.example.com \ --docker-usernamedeploy \ --docker-passwordxxx \ -n prod-ai然后在 Deployment 的 spec 里加 imagePullSecrets。不加这个东西私有镜像永远拉不下来。另外如果你用的是 containerd 做运行时某些节点上可能还要额外配置 containerd 的 registry 配置否则 crictl 拉取时一样会认证失败。6.3 Redis 集群连接超时DNS 和 namespace 的恩恩怨怨我在 4.2 节埋了个伏笔这里展开讲。曾有一次我在 test-ai namespace 里部署了一个 SpringAI 服务去连 prod-ai namespace 里的 Redis 集群配置写得也没问题但就是连不上日志里报 UnknownHostException。排查到最后发现测试服务的 Deployment 里配置的 Redis 节点是短名redis-cluster-0.redis-cluster-headless这个短名只有在 prod-ai namespace 内部的 Pod 才能直接解析。跨 namespace 必须写完整 FQDN也就是加上 .prod-ai.svc.cluster.local。还有一个隐蔽问题Spring Data Redis 的 cluster nodes 如果只写一个节点地址即使这里配了集群模式客户端也可能只连这一个节点。建议把 3 个 master 的地址都写进配置让客户端自己去探测集群拓扑。6.4 疑难杂症速查表我把上面这些问题整理成一张表方便大家现场照方抓药。现象常见原因排查命令解决动作CrashLoopBackOff启动参数/环境变量缺失kubectl logs检查 ConfigMap 引用和环境变量名CrashLoopBackOffJVM 内存超限kubectl describe pod调大 limits 或调小 XmxImagePullBackOfftag 不存在/仓库认证失败kubectl describe pod修正 tag添加 imagePullSecrets探针一直失败路径写错/启动太慢kubectl describe pod改 actuator 路径加大 initialDelaySecondsRedis UnknownHostException跨 namespace 用短名kubectl exec -- nslookup 域名改成完整 FQDNPod 调度不上去资源 requests 超过节点剩余kubectl describe pod降低 requests 或加节点kubectl top 没数据metrics-server 没装kubectl get pods -n kube-systemapply metrics-server实用的小技巧K8s 里排查问题不要只盯 kubectl logskubectl describe pod 里的 Events 往往比日志早一步暴露问题。两个命令配合使用大部分问题五分钟内能定位。7. 这一掌的收尾体会写到这第十八掌“神龙摆尾”的核心内容就全部落地了。从容器化开始我们把 SpringAI 智能审核服务的镜像做小、做强然后在 Rocky Linux 上搭起了 1.36 的 K8s 集群规划好 namespace把系统提示词配置从代码里解放出来接上 Redis 集群最后用 Deployment、探针、HPA 让整个服务具备生产级的高可用能力。说点我自己的真实感受。这套流程我在不同项目里重复了很多次每次最有价值的收获不是命令背得更熟而是越来越清楚“为什么要这样设计”。比如给 Pod 写 resources表面上是 K8s 的一个参数实际上背后是对 SpringAI 服务内存画像的掌控。再比如把系统提示词放进 ConfigMap表面上是部署方式的选择实际上让运营和算法可以自助调优不用再求着开发发版。另外给准备复现的朋友一个建议不要一上来就追求三 master 双 worker 的大集群。先在单 master 上把整条链路跑通看明白每个组件之间的依赖关系再考虑扩节点。过程中多记录自己踩过的坑多积累 kubectl describe 和 logs 结合排查的习惯。K8s 这东西门槛不在命令在于你对整个系统运转逻辑的体感。神龙摆尾这一招练成之后后面再上什么服务都会顺很多。
返回列表