
Kubernetes部署Go服务:从Deployment到Service摘要: 本篇讲解Go服务部署到Kubernetes的完整流程编写Deployment YAML管理Pod副本与滚动更新配置Service暴露服务(ClusterIP和NodePort)使用ConfigMap和Secret管理配置与敏感信息分享liveness probe配置不当导致Pod频繁重启的踩坑经验对比手动部署、Helm和Kustomize三种K8s部署方案。开篇故事上个月我们做灰度发布直接改了Deployment的镜像版本号kubectl apply一下。结果新版本有bug启动时panicPod疯狂崩溃重启旧版本Pod同时被杀掉了。用户全报502持续了5分钟才回滚。后来加了就绪探针和最大不可用数配置再灰度时新版本起不来旧版本不会被杀平滑多了。K8s的部署配置看着简单但细节没配好就是线上事故。这篇把Deployment和Service的配置讲透。一、Deployment YAML编写Deployment管理Pod的副本数、更新策略和回滚。写好Deployment的关键是探针配置和更新策略。# deployment.yamlapiVersion:apps/v1kind:Deploymentmetadata:name:order-servicenamespace:productionlabels:app:order-servicespec:# 副本数根据QPS调整replicas:3# 选择器匹配Pod标签selector:matchLabels:app:order-service# 更新策略strategy:type:RollingUpdaterollingUpdate:# 滚动更新时最多多出多少个Pod# 比如replicas3, maxSurge1, 最多4个PodmaxSurge:1# 滚动更新时最多不可用多少个Pod# 设为0表示不允许减少可用Pod数量# 新版本起不来时旧版本不会被杀maxUnavailable:0template:metadata:labels:app:order-servicespec:containers:-name:order-service# 镜像地址tag用具体版本号image:registry.example.com/order-service:v1.2.3# 镜像拉取策略imagePullPolicy:IfNotPresentports:-containerPort:8080name:httpprotocol:TCP# 资源限制必配resources:requests:# 调度依据保证Pod有足够资源cpu:100mmemory:128Milimits:# 硬上限超过会被限制或杀掉cpu:500mmemory:256Mi# 存活探针: 检查Pod是否健康# 失败会重启PodlivenessProbe:httpGet:path:/healthport:8080# 启动后10秒开始探测initialDelaySeconds:10# 每10秒探测一次periodSeconds:10# 探测超时时间timeoutSeconds:3# 连续失败3次判定不健康failureThreshold:3# 就绪探针: 检查Pod是否准备好接流量# 失败会从Service endpoints摘除readinessProbe:httpGet:path:/readyport:8080initialDelaySeconds:5periodSeconds:5timeoutSeconds:2failureThreshold:3# 启动探针: 检查Pod是否启动完成# 启动期间不跑liveness避免慢启动被杀startupProbe:httpGet:path:/healthport:8080# 启动最长等待60秒periodSeconds:5failureThreshold:12# 环境变量从ConfigMap注入env:-name:APP_ENVvalueFrom:configMapKeyRef:name:order-configkey:app-env-name:DB_PASSWORDvalueFrom:secretKeyRef:name:order-secretkey:db-password三个探针的职责不同。liveness检查Pod是否活着失败了重启Pod。readiness检查Pod是否能接流量失败了从Service摘除但不会重启。startup检查Pod是否启动完成启动期间不跑liveness。Go服务启动慢的话startupProbe特别有用避免liveness在启动阶段误杀Pod。二、Service暴露服务Deployment创建的Pod IP是动态的每次重启都变。Service提供固定访问入口背后通过label selector自动关联Pod。# service.yaml# ClusterIP: 集群内部访问apiVersion:v1kind:Servicemetadata:name:order-servicenamespace:productionlabels:app:order-servicespec:# ClusterIP类型仅在集群内部可访问type:ClusterIPselector:# 匹配Pod标签自动关联到order-service的Podapp:order-serviceports:-name:httpport:80# 集群内访问80端口转发到Pod的8080targetPort:8080protocol:TCP# service-nodeport.yaml# NodePort: 集群外部访问apiVersion:v1kind:Servicemetadata:name:order-service-externalnamespace:productionspec:# NodePort类型在每个节点上开一个端口type:NodePortselector:app:order-serviceports:-name:httpport:80targetPort:8080# 节点端口范围30000-32767# 不指定则自动分配nodePort:30080# service-loadbalancer.yaml# LoadBalancer: 云厂商负载均衡器apiVersion:v1kind:Servicemetadata:name:order-service-lbnamespace:productionannotations:# 阿里云SLB注解service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec:slb.s1.smallspec:# LoadBalancer类型云厂商自动创建SLBtype:LoadBalancerselector:app:order-serviceports:-name:httpport:80targetPort:8080生产环境一般不直接用NodePort暴露服务。用Ingress做七层路由更合适支持域名和TLS。三、ConfigMap和Secret管理配置配置不应该写死在镜像里。ConfigMap存普通配置Secret存敏感信息。# configmap.yamlapiVersion:v1kind:ConfigMapmetadata:name:order-confignamespace:productiondata:# 简单的键值对配置app-env:productionlog-level:info# 可以存整个配置文件config.yaml:|server: port: 8080 timeout: 30s database: host: mysql.production port: 3306 name: orders max_open_conns: 20 redis: host: redis.production port: 6379# secret.yamlapiVersion:v1kind:Secretmetadata:name:order-secretnamespace:productiontype:Opaquedata:# base64编码的密码不是明文# echo -n your-password | base64db-password:eW91ci1wYXNzd29yZAredis-password:cmVkaXMtcGFzc3dvcmQ在Deployment里引用ConfigMap和Secret有两种方式。环境变量注入适合简单配置Volume挂载适合整个配置文件。# 方式1: 环境变量引用(已在Deployment中展示)env:-name:DB_PASSWORDvalueFrom:secretKeyRef:name:order-secretkey:db-password# 方式2: Volume挂载配置文件# 在Deployment的spec.template.spec里添加volumes:-name:config-volumeconfigMap:name:order-config# 挂载指定的keyitems:-key:config.yamlpath:config.yamlcontainers:-name:order-servicevolumeMounts:-name:config-volume# 挂载到容器内的路径mountPath:/etc/app# 只读挂载防止程序意外修改readOnly:trueGo代码里读取挂载的配置文件。packageconfigimport(logosgopkg.in/yaml.v3)// Config 应用配置结构体typeConfigstruct{Server ServerConfigyaml:serverDatabase DatabaseConfigyaml:databaseRedis RedisConfigyaml:redis}typeServerConfigstruct{Portintyaml:portTimeoutstringyaml:timeout}typeDatabaseConfigstruct{Hoststringyaml:hostPortintyaml:portNamestringyaml:nameMaxOpenConnsintyaml:max_open_conns}typeRedisConfigstruct{Hoststringyaml:hostPortintyaml:port}// Load 从K8s ConfigMap挂载的文件加载配置funcLoad(pathstring)*Config{// 默认路径: /etc/app/config.yamldata,err:os.ReadFile(path)iferr!nil{log.Fatalf(读取配置文件失败: %v,err)}varcfg Config// 解析YAML配置iferr:yaml.Unmarshal(data,cfg);err!nil{log.Fatalf(解析配置文件失败: %v,err)}// 数据库密码从环境变量读取(Secret注入)cfg.Database.Passwordos.Getenv(DB_PASSWORD)returncfg}四、独家踩坑:liveness probe配置不当导致Pod频繁重启这个坑前面提过详细说排查过程。现象: 订单服务部署到K8s后Pod频繁重启每5分钟重启一次。看日志没有任何错误服务运行正常。但kubectl get pods显示RESTARTS一直在涨。排查过程:第一步看Pod事件。kubectl describe pod order-service-xxx看到Events里有Liveness probe failed: HTTP probe failed with status code: 503。第二步看liveness配置。path是/healthperiod是10秒timeout是1秒failureThreshold是3次。第三步看/health接口的实现。接口里检查了数据库和Redis连接数据库响应慢时这个接口要500毫秒。但timeout只有1秒网络抖动一下就超时了。问题根因: liveness探针的timeout太短健康检查接口又太重。连续3次超时(总共30秒)后K8s判定Pod不健康杀掉重启。// 问题代码: /health接口做了太多检查funchealthHandler(w http.ResponseWriter,r*http.Request){// 检查数据库连接可能超时iferr:db.Ping();err!nil{w.WriteHeader(http.StatusServiceUnavailable)return}// 检查Redis连接也可能超时iferr:redis.Ping();err!nil{w.WriteHeader(http.StatusUnavailable)return}w.WriteHeader(http.StatusOK)}解决方案: 把liveness和readiness分开。liveness只检查进程是否活着readiness检查依赖是否可用。// liveness: 只检查进程活着不做依赖检查// 进程能响应就算活着funclivenessHandler(w http.ResponseWriter,r*http.Request){// 只检查进程是否存活// 不检查数据库和Redisw.WriteHeader(http.StatusOK)w.Write([]byte(ok))}// readiness: 检查依赖是否可用// 依赖检查失败时从Service摘除流量funcreadinessHandler(w http.ResponseWriter,r*http.Request){// 检查数据库连接iferr:db.Ping();err!nil{w.WriteHeader(http.StatusServiceUnavailable)return}// 检查Redis连接iferr:redis.Ping();err!nil{w.WriteHeader(http.StatusServiceUnavailable)return}w.WriteHeader(http.StatusOK)w.Write([]byte(ready))}# 修复后的探针配置# liveness用轻量接口timeout长一点livenessProbe:httpGet:path:/health# 轻量接口port:8080initialDelaySeconds:10periodSeconds:10timeoutSeconds:3# 从1秒改到3秒failureThreshold:3# readiness用重量接口timeout短一点没关系# 失败只是摘除流量不会重启PodreadinessProbe:httpGet:path:/ready# 检查依赖port:8080initialDelaySeconds:5periodSeconds:5timeoutSeconds:2failureThreshold:3修复后Pod不再重启。数据库慢的时候readiness失败Pod从Service摘除但不会被杀掉重启。数据库恢复后readiness通过流量自动恢复。五、对比分析部署方案学习成本模板复用适合规模生态手动YAML低差(copy改)小规模(几个服务)原生Helm中好(chart仓库)中大规模最丰富Kustomize低好(overlay叠加)中小规模K8s原生手动写YAML适合学习和简单场景几个服务直接用kubectl apply就行。Helm是K8s的包管理器用模板生成YAML适合管理大量服务有完善的chart仓库。Kustomize是K8s原生的配置管理工具不用模板用base和overlay叠加的方式管理多环境配置。总结与预告K8s部署Go服务的核心就三个资源。Deployment管Pod生命周期探针和更新策略是重点。Service管流量入口ClusterIP内部用Ingress外部用。ConfigMap和Secret管配置别把密码写进镜像。liveness探针要轻量重检查放readiness。专栏到这里从Go基础到并发到Web到数据库再到微服务部署完整的技术栈就串起来了。