在应用的整个生命周期里,开发和运维都和它密不可分。一个塑造它,一个保养它。 如果应用需要部署到K8S中,开发和运维在其中都做了什么呢? 开发侧 从开发侧来说,我们的应用应该具备以下能力: 具有健康检测接口 具有优雅退出能力 具有metrics接口 能够接入链路追踪系统 日志输出标准统一 定义健康检测接口 健康检测接口用于检测应用的健康状态,在K8S中,使用Readiness和Liveness分别来探测应用是否就绪和是否存活,如果未就绪或者未存活,K8S会采取相应的措施来确保应用可用。 如果我们应用未定义好相应的健康检测接口,K8S就无法判断应用是否正常可用,整个应用对我们来说就是黑匣子,也就谈不上应用稳定性了。 定义一个简单的健康检测接口如下:packagerouter import( github。comgingonicgin v1gohelloworldapphttpcontrollersv1 ) strongtoutiaooriginspanfuncstrongstrongtoutiaooriginspanSetupRouterstrong(routergin。Engine){ ruc:new(v1。RootController) router。GET(,ruc。Root) huc:new(v1。HealthController) router。GET(health,huc。HealthCheck) } packagev1 import( github。comgingonicgin gohelloworldapphttpcontrollers gohelloworldpkgresponse nethttp ) typeHealthControllerstruct{ controllers。BaseController } strongtoutiaooriginspanfuncstrong(hHealthController)strongtoutiaooriginspanHealthCheckstrong(cgin。Context){ response。WriteResponse(c,http。StatusOK,nil,gin。H{ result:ahrefhttps:www。q578。coml160targetblankclassinfotextkey健康a检测页面, status:OK, }) } 如上我们定义了health接口,当应用启动后,只需要探测这个接口,如果返回OK,表示应用是正常的。 当然,上面的接口是非常简单的,在实际情况下,应用本身也许还依赖起来应用,比如redis,mysql,mq等,如果它们异常,应用是不是异常的呢?那我们的应用健康检测需不需要检测其他应用的健康状态呢? 既然我们定义好了健康检测接口,那我们的YAML模板就可以增加健康检测功能,如下:readinessProbe: httpGet: path:health port:http timeoutSeconds:3 initialDelaySeconds:20 livenessProbe: httpGet: path:health port:http timeoutSeconds:3 initialDelaySeconds:30 定义优雅下线功能 应用发版是常规不能再常规的操作,通常情况下都是滚动更新的方式上线,也就是先起一个新应用,再删一个老应用。 如果这时候老应用有部分的流量,突然把老应用的进程杀了,这部分流量就无法得到正确的处理,部分用户也会因此受到影响。 怎么才会不受影响呢? 假如我们在停止应用之前先告诉网关或者注册中心,等对方把我们应用摘除后再下线,这样就不会有任何流量受到影响了。 在K8S中,当我们要删除Pod的时候,Pod会变成Terminating状态,kubelet看到Pod的状态如果为Terminating,就会开始执行关闭Pod的流程,给Pod发SIGTERM信号,如果达到宽限期Pod还未结束就给Pod发SIGKILL信号,从Endpoints中摘除Pod等。 从上面可知,Pod在停止之前会收到SIG信号,如果应用本身没有处理这些信号的能力,那应用如果知道什么时候该结束呢? 下面简单定义一个处理SIG信号的功能。packageshutdown import( context fmt nethttp os ossignal time ) 优雅退出 typeShutdownstruct{ chchanos。Signal timeouttime。Duration } funcNew(ttime。Duration)Shutdown{ returnShutdown{ ch:make(chanos。Signal), timeout:t, } } strongtoutiaooriginspanfuncstrong(sShutdown)strongtoutiaooriginspanAddstrong(signals。。。os。Signal){ signal。Notify(s。ch,signals。。。) } strongtoutiaooriginspanfuncstrong(sShutdown)strongtoutiaooriginspanStartstrong(serverhttp。Server){ s。ch fmt。Println(startexist。。。。。。) ctx,cannel:context。WithTimeout(context。Background,s。timeouttime。Second) defercannel iferr:server。Shutdown(ctx);err!nil{ fmt。Println(Gracefulexitfailed。err:,err) } fmt。Println(Gracefulexitsuccess。) } packagemain import( github。comgingonicgin gohelloworldpkgshutdown gohelloworldrouter log nethttp syscall time ) strongtoutiaooriginspanfuncstrongstrongtoutiaooriginspanmainstrong{ r:gin。New 注册路由 router。SetupRouter(r) server:http。Server{ Addr::8080, Handler:r, } 运行服务 gostrongtoutiaooriginspanfuncstrong{ err:server。ListenAndServe iferr!nilerr!http。ErrServerClosed{ log。Fatalf(server。ListenAndServeerr:v,err) } } 优雅退出 quit:shutdown。New(10) quit。Add(syscall。SIGINT,syscall。SIGTERM) quit。Start(server) } 当接收到SIG信号的时候,就会调用Shutdown方法做应用退出处理。 除此,还要结合K8S的PreStopHook来定义结束前的钩子,如下:lifecycle: preStop: exec: command: binsh c sleep30 如果使用注册中心,比如nacos,我们可以在PreStopHook中先告诉nacos要下线,如下:lifecycle: preStop: exec: command: binsh c curlXDELETEyournacosip:8848nacosv1nsinstance?serviceNamenacos。test。1ip{PODIP}port8880clusterNameDEFAULTsleep30 定义Metrics接口 Metrics主要用来暴露应用指标,可以根据实际情况自定义指标,以便于监控工具Prometheus进行数据收集展示。 有些语言有现成的exporter,比如java的jmxexporter,没有的就需要自己在应用中集成。 比如:packagemain import( github。comSkyAPMgo2sky v3github。comSkyAPMgo2skypluginsginv3 github。comSkyAPMgo2skyreporter github。comgingonicgin github。comprometheusclientgolangprometheuspromhttp gohelloworldpkgshutdown gohelloworldrouter log nethttp syscall time ) varSKYWALKINGENABLEDfalse strongtoutiaooriginspanfuncstrongstrongtoutiaooriginspanmainstrong{ r:gin。New 注册路由 router。SetupRouter(r) server:http。Server{ Addr::8080, Handler:r, } 启动metrics服务 gostrongtoutiaooriginspanfuncstrong{ http。Handle(metrics,promhttp。Handler) iferr:http。ListenAndServe(:9527,nil);err!nil{ log。Printf(metricsportlistenfailed。err:s,err) } } 运行服务 gostrongtoutiaooriginspanfuncstrong{ err:server。ListenAndServe iferr!nilerr!http。ErrServerClosed{ log。Fatalf(server。ListenAndServeerr:v,err) } } 优雅退出 quit:shutdown。New(10) quit。Add(syscall。SIGINT,syscall。SIGTERM) quit。Start(server) } 这种会暴露默认的Http指标,可以通过curl127。0。0。1:9527metrics获取指标。。。。。。。 strongtoutiaooriginspanclasshighlighttextHELPpromhttpmetrichandlerrequeststotalTotalnumberofscrapesbyHTTPstatuscode。strong strongtoutiaooriginspanclasshighlighttextTYPEpromhttpmetrichandlerrequeststotalcounterstrong promhttpmetrichandlerrequeststotal{code200}0 promhttpmetrichandlerrequeststotal{code500}0 promhttpmetrichandlerrequeststotal{code503}0 如果需要自定义指标的话,只需按规则定义即可,如下:packagemetrics import( github。comprometheusclientgolangprometheus nethttp time ) var( HttpserverRequestTotal表示接收http请求总数 HttpserverRequestTotalprometheus。NewCounterVec(prometheus。CounterOpts{ Name:httpserverrequesttotal, Help:TheTotalnumberofhttpserverrequests, }, 设置标签:请求方法和路径 〔〕string{method,endpoint}) HttpserverRequestDurationprometheus。NewHistogramVec(prometheus。HistogramOpts{ Name:httpserverrequestdurationseconds, Help:httpserverrequestdurationdistribution, Buckets:〔〕float64{0。1,0。3,0。5,0。7,0。9,1}, }, 〔〕string{method,endpoint}) ) 注册监控指标 strongtoutiaooriginspanfuncstrongstrongtoutiaooriginspaninitstrong{ prometheus。MustRegister(HttpserverRequestTotal) prometheus。MustRegister(HttpserverRequestDuration) } funcNewMetrics(routerhttp。HandlerFunc)http。HandlerFunc{ returnstrongtoutiaooriginspanfuncstrong(whttp。ResponseWriter,rhttp。Request){ start:time。Now router(w,r) duration:time。Since(start) httpserverRequestTotal记录 HttpserverRequestTotal。With(prometheus。Labels{method:r。Method,endpoint:r。URL。Path})。Inc httpserverRequestDuration记录 HttpserverRequestDuration。With(prometheus。Labels{method:r。Method,endpoint:r。URL。Path})。Observe(duration。Seconds) } } 这样就定义了httpserverrequesttotal和httpserverrequestdurationseconds指标,引用过后就能在metrics中看到对应的数据。 定义好了指标,下面就是收集了。既可以通过自定义收集规则收集,也可以通过自动发现的方式收集,为了方便,主要采用自动发现的方式。 我们只需要在deployment的templates中定义好annotation,prometheeus就会自动添加采集目标,如下:apiVersion:appsv1 kind:Deployment metadata: labels: app:httpserver name:httpserver namespace:default spec: replicas:2 selector: matchLabels: app:httpserver template: metadata: annotations: prometheus。ioscrape:true prometheus。ioport:metrics labels: app:httpserver spec: containers: image:baidjayhttpserver:ubuntuv3metrics imagePullPolicy:IfNotPresent lifecycle: preStop: exec: command: binsh c sleep15 livenessProbe: failureThreshold:3 httpGet: path:healthz port:http scheme:HTTP initialDelaySeconds:30 periodSeconds:10 successThreshold:1 timeoutSeconds:3 name:httpserver ports: containerPort:8080 name:http protocol:TCP name:metrics protocol:TCP containerPort:9527 readinessProbe: failureThreshold:3 httpGet: path:healthz port:http scheme:HTTP initialDelaySeconds:20 periodSeconds:10 successThreshold:1 timeoutSeconds:3 定义Trace功能 Trace用于跟踪,每个请求都会生成一个TraceID,这个ID会伴随请求的整个生命周期,我们也可以根据这个ID查询请求的整个链路情况。 链路追踪,目前市面上有很多开源系统,比如Skywalking,Jeager,Zipkin等,它们各有各的特点,如下。 PinpointZipkinJaegerSkywalkingOpenTracing兼容否是是是客户端支持语言javaphpjavagophp等javagophp等javaodejsphp等存储hbaseesmysql内存等eskafka内存等esmysqlh2等传输协议支持thrifthttpmqudphttpgrpcUI丰富程度高低中中实现方式字节码注入拦截请求拦截请求字节码注入扩展性低高高中Trace查询不支持支持支持支持告警支持支持不支持不支持支持JVM监控支持不支持不支持支持性能损失高中中低 我比较推荐使用Jaeger,它是CNCF的毕业项目,成长空间和云原生的系统架构兼容性比较好。 不过,我这里采用的Skywalking。 Skywalking有许多现成的客户端,比如Java、Python等,可以直接使用,它们都会自动埋点,但是对于Go来说就只有自己手动埋点了,需要我们自己去写代码。 比如:packagemain import( github。comSkyAPMgo2sky v3github。comSkyAPMgo2skypluginsginv3 github。comSkyAPMgo2skyreporter github。comgingonicgin github。comprometheusclientgolangprometheuspromhttp gohelloworldpkgshutdown gohelloworldrouter log nethttp syscall time ) varSKYWALKINGENABLEDfalse strongtoutiaooriginspanfuncstrongstrongtoutiaooriginspanmainstrong{ r:gin。New 配置skywalking ifSKYWALKINGENABLED{ rp,err:reporter。NewGRPCReporter(skywalkingoap:11800,reporter。WithCheckInterval(time。Second)) iferr!nil{ log。Printf(creategoskyreporterfailed。err:s,err) } deferrp。Close tracer,:go2sky。NewTracer(gohelloworld,go2sky。WithReporter(rp)) r。Use(v3。Middleware(r,tracer)) } 注册路由 router。SetupRouter(r) server:http。Server{ Addr::8080, Handler:r, } 启动metrics服务 gostrongtoutiaooriginspanfuncstrong{ http。Handle(metrics,promhttp。Handler) iferr:http。ListenAndServe(:9527,nil);err!nil{ log。Printf(metricsportlistenfailed。err:s,err) } } 运行服务 gostrongtoutiaooriginspanfuncstrong{ err:server。ListenAndServe iferr!nilerr!http。ErrServerClosed{ log。Fatalf(server。ListenAndServeerr:v,err) } } 优雅退出 quit:shutdown。New(10) quit。Add(syscall。SIGINT,syscall。SIGTERM) quit。Start(server) } 定义reporter用于上报数据给Skywalking,这就是一个简单的集成Trace的例子。 定义标准的日志 应用的可观测性主要来源日志、监控、链路追踪,标准的日志有利于日志收集以及排查问题。 原则上,不论是什么类型的日志输出,什么格式的日志内容,都能收集。但是为了方便友好,建议把日志输出到标准输出,这样收集更方便。 我个人理解,在K8s中,完全没必要把日志输出到文件,浪费不说,没多大意义,因为所有的日志我们都会收集到日志系统,而输出到文件的日志也会随着应用发版而丢失,所以输出到文件的意义是什么呢? 运维侧 开发把系统开发完,就会交付给运维部署。为了保障应用的稳定性,运维在部署应用的时候应该考虑以下几点。 应用尽可能保持无状态 应用尽可能保持高可用 应该具备优雅上线能力 应该具备异常自愈能力 可以使用HTTPS访问 应用尽可能保持无状态 K8S中可以部署有状态应用,也可以部署无状态应用。对于有状态应用,我其实很少部署到K8S中,大部分还是部署的无状态应用,至于为什么,用多了就晓得了。 对于业务应用,强烈建议使其保持无状态,就算有需要持久化的东西,要么保存到数据库,要么保存到对象存储或者其他单独的文件系统中,不要挂载到应用Pod上。 这样的好处是,应用和数据是分开的,应用可以随意启停、扩展、迁移等。 应用尽可能的保持高可用 保持高可用应该是每个运维人员的使命。 在K8S中,我们应该怎么配置呢?(1)应用Pod应该是多副本 (2)应用Pod之间做反亲和性,避免同一应用调度到同一台主机,如下。。。。。。。 spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: labelSelector: matchExpressions: key:app operator:In values:〔httpserver〕 topologyKey:kubernetes。iohostname 。。。。。。 (3)为了避免应用因为节点维护等原因驱逐Pod,导致全部Pod被驱逐,特别配置了PodDisruptionBudget,保障应用至少有一个可用,如下。apiVersion:policyv1beta1 kind:PodDisruptionBudget metadata: name:httpserver spec: minAvailable:1 selector: matchLables: app:httpserver (4)如果某个节点因为一些原因需要驱逐一些Pod,为了避免重要应用被驱逐,应该给应用配置较高的QoS,如下:resources: limits: cpu:1 memory:2Gi requests: cpu:1 memory:2Gi 应用具备优雅上线能力 所谓优雅上线能力,就是要确保应用能够提供服务了,再接入外界流量,不能在还没完全启动的情况下就提供服务。 在K8S中,应用在启动后会加入endpoints中,然后通过service接入流量,那在什么情况下才算启动成功呢?主要是通过K8S的ReadinessProbe来进行检测。这时候开发的健康检测接口就派上用场了,如下:。。。 readinessProbe: failureThreshold:3 httpGet: path:health port:http scheme:HTTP initialDelaySeconds:20 periodSeconds:10 successThreshold:1 timeoutSeconds:3 。。。 所以我们K8S的YAML文件应该加上如上的配置。 应该具备异常自愈能力 所谓异常自愈,就是应用本身在出现Crash,或者应用Pod所在节点出现异常的情况,应用能够自动重启或者迁移。这时候就需要通过K8S的LivenessProbe来进行检测了,如下。。。。。。。 livenessProbe: failureThreshold:3 httpGet: path:health port:http scheme:HTTP initialDelaySeconds:30 periodSeconds:10 successThreshold:1 timeoutSeconds:3 。。。。。。 当K8S的YAML清单加上如上配置过后,就会定时去探测应用是否正常,如果异常,就会触发重启的动作。如果是节点异常,K8S会对Pod进行重新调度。 可以使用HTTPS进行访问 应用通过HTTPS访问是比较常见的,企业级应用建议自己购买相应的SSL证书,然后进行配置即可。 比如。strongtoutiaooriginspanclasshighlighttext创建证书secretstrong kubectlcreatesecrettlshttpservertlssecretcertpathtotls。certkeypathtotls。key strongtoutiaooriginspanclasshighlighttext在ingress中引用strong 。。。。。。 spec: tls: hosts: httpserver。coolops。cn secretName:httpservertlssecret rules: host:httpserver。coolops。cn 。。。。。。 总结 上面介绍了开发和运维对于应用上线应该做的工作,不全但够用。 在不同的企业都有不同的尿性,但是作为运维,我们都要牢牢记住稳定永远是第一尿性。通过上面的梳理,我们的应用模板就整理如下:apiVersion:appsv1 kind:Deployment metadata: labels: app:httpserver name:httpserver namespace:default spec: progressDeadlineSeconds:600 replicas:2 revisionHistoryLimit:10 selector: matchLabels: app:httpserver strategy: rollingUpdate: maxSurge:25 maxUnavailable:25 type:RollingUpdate template: metadata: annotations: prometheus。ioscrape:true prometheus。ioport:metrics labels: app:httpserver spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: labelSelector: matchExpressions: key:app operator:In values:〔httpserver〕 topologyKey:kubernetes。iohostname containers: env: name:TZ value:AsiaShanghai name:PODNAME valueFrom: fieldRef: apiVersion:v1 fieldPath:metadata。name name:PODNAMESPACE valueFrom: fieldRef: apiVersion:v1 fieldPath:metadata。namespace image:baidjayhttpserver:ubuntuv3metrics imagePullPolicy:IfNotPresent lifecycle: preStop: exec: command: binsh c sleep15 livenessProbe: failureThreshold:3 httpGet: path:healthz port:http scheme:HTTP initialDelaySeconds:30 periodSeconds:10 successThreshold:1 timeoutSeconds:3 name:httpserver ports: containerPort:8080 name:http protocol:TCP name:metrics protocol:TCP containerPort:9527 readinessProbe: failureThreshold:3 httpGet: path:healthz port:http scheme:HTTP initialDelaySeconds:20 periodSeconds:10 successThreshold:1 timeoutSeconds:3 resources: limits: cpu:1 memory:2Gi requests: cpu:1 memory:2Gi securityContext:{} terminationMessagePath:devterminationlog terminationMessagePolicy:File dnsPolicy:ClusterFirst restartPolicy:Always schedulerName:defaultscheduler strongtoutiaooriginspanclasshighlighttextstrong apiVersion:v1 kind:Service metadata: name:httpserver spec: ports: name:http port:8080 protocol:TCP targetPort:http name:metrics port:9527 protocol:TCP targetPort:metrics selector: app:httpserver sessionAffinity:None type:ClusterIP strongtoutiaooriginspanclasshighlighttextstrong apiVersion:networking。k8s。iov1 kind:Ingress metadata: annotations: nginx。ingress。kubernetes。ioproxybodysize:100m nginx。ingress。kubernetes。ioproxyconnecttimeout:600 nginx。ingress。kubernetes。ioproxyreadtimeout:600 nginx。ingress。kubernetes。ioproxysendtimeout:600 nginx。ingress。kubernetes。ioserviceweight: nginx。orgclientmaxbodysize:100m name:httpservertls spec: tls: hosts: httpserver。coolops。cn secretName:httpservertlssecret rules: host:httpserver。coolops。cn http: paths: pathType:Prefix path: backend: service: name:httpserver port: number:8080 strongtoutiaooriginspanclasshighlighttextstrong apiVersion:policyv1 kind:PodDisruptionBudget metadata: name:httpserver spec: minAvailable:1 selector: matchLabels: app:httpserver 为了凑字数,写了一大堆,大家凑合看,觉得有用就点个赞! 最后,求关注。如果你还想看更多优质原创文章,欢迎关注我们的公众号运维开发故事。 如果我的文章对你有所帮助,还请帮忙一下,你的支持会激励我输出更高质量的文章,非常感谢! 你还可以把我的公众号设为星标,这样当公众号文章更新时,你会在第一时间收到推送消息,避免错过我的文章更新。