Kubernetes部署Go服务:从Deployment到Service
摘要: 本篇讲解Go服务部署到Kubernetes的完整流程,编写Deployment YAML管理Pod副本与滚动更新,配置Service暴露服务(ClusterIP和NodePort),使用ConfigMap和Secret管理配置与敏感信息,分享liveness probe配置不当导致Pod频繁重启的踩坑经验,对比手动部署、Helm和Kustomize三种K8s部署方案。
开篇故事
上个月我们做灰度发布,直接改了Deployment的镜像版本号,kubectl apply一下。结果新版本有bug,启动时panic,Pod疯狂崩溃重启,旧版本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# 比如replicas=3, maxSurge=1, 最多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:"100m"memory:"128Mi"limits:# 硬上限,超过会被限制或杀掉cpu:"500m"memory:"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:"production"log-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:eW91ci1wYXNzd29yZA==redis-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("log""os""gopkg.in/yaml.v3")// Config 应用配置结构体typeConfigstruct{Server ServerConfig`yaml:"server"`Database DatabaseConfig`yaml:"database"`Redis RedisConfig`yaml:"redis"`}typeServerConfigstruct{Portint`yaml:"port"`Timeoutstring`yaml:"timeout"`}typeDatabaseConfigstruct{Hoststring`yaml:"host"`Portint`yaml:"port"`Namestring`yaml:"name"`MaxOpenConnsint`yaml:"max_open_conns"`}typeRedisConfigstruct{Hoststring`yaml:"host"`Portint`yaml:"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.Password=os.Getenv("DB_PASSWORD")return&cfg}四、独家踩坑: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是/health,period是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到数据库再到微服务部署,完整的技术栈就串起来了。