Kubernetes 在应用的整个生命周期里,开发和运维都和它密不可分。一个塑造它,一个保养它。 如果应用需要部署到K8S中,开发和运维在其中都做了什么呢?

开发侧

从开发侧来说,应用应该具备以下能力:
  • 具有健康检测接口
  • 具有优雅退出能力
  • 具有metrics接口
  • 能够接入链路追踪系统
  • 日志输出标准统一

定义健康检测接口

健康检测接口用于检测应用的健康状态,在K8S中,使用Readiness和Liveness分别来探测应用是否就绪和是否存活,如果未就绪或者未存活,K8S会采取相应的措施来确保应用可用。 如果应用未定义好相应的健康检测接口,K8S就无法判断应用是否正常可用,整个应用对运维来说就是黑匣子,也就谈不上应用稳定性了。 定义一个简单的健康检测接口如下:
  1. package router
  2. import (
  3. "github.com/gin-gonic/gin"
  4. v1 "go-hello-world/app/http/controllers/v1"
  5. )
  6. func SetupRouter(router *gin.Engine) {
  7. ruc := new(v1.RootController)
  8. router.GET("/", ruc.Root)
  9. huc := new(v1.HealthController)
  10. router.GET("/health", huc.HealthCheck)
  11. }
  1. package v1
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "go-hello-world/app/http/controllers"
  5. "go-hello-world/pkg/response"
  6. "net/http"
  7. )
  8. type HealthController struct {
  9. controllers.BaseController
  10. }
  11. func (h *HealthController) HealthCheck(c *gin.Context) {
  12. response.WriteResponse(c, http.StatusOK, nil, gin.H{
  13. "result": "健康检测页面",
  14. "status": "OK",
  15. })
  16. }
如上定义了health接口,当应用启动后,只需要探测这个接口,如果返回OK,表示应用是正常的。 当然,上面的接口是非常简单的,在实际情况下,应用本身也许还依赖起来应用,比如redis,mysql,mq等,如果它们异常,应用是不是异常的呢?那应用健康检测需不需要检测其他应用的健康状态呢? 既然定义好了健康检测接口,那YAML模板就可以增加健康检测功能,如下:
  1. readinessProbe:
  2. httpGet:
  3. path: /health
  4. port: http
  5. timeoutSeconds: 3
  6. initialDelaySeconds: 20
  7. livenessProbe:
  8. httpGet:
  9. path: /health
  10. port: http
  11. timeoutSeconds: 3
  12. initialDelaySeconds: 30

定义优雅下线功能

应用发版是常规不能再常规的操作,通常情况下都是滚动更新的方式上线,也就是先起一个新应用,再删一个老应用。 如果这时候老应用有部分的流量,突然把老应用的进程杀了,这部分流量就无法得到正确的处理,部分用户也会因此受到影响。 怎么才会不受影响呢? 假如在停止应用之前先告诉网关或者注册中心,等对方把应用摘除后再下线,这样就不会有任何流量受到影响了。 在K8S中,当要删除Pod的时候,Pod会变成Terminating状态,kubelet看到Pod的状态如果为Terminating,就会开始执行关闭Pod的流程,给Pod发SIGTERM信号,如果达到宽限期Pod还未结束就给Pod发SIGKILL信号,从Endpoints中摘除Pod等。 从上面可知,Pod在停止之前会收到SIG信号,如果应用本身没有处理这些信号的能力,那应用如果知道什么时候该结束呢? 下面简单定义一个处理SIG信号的功能。
  1. package shutdown
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "os/signal"
  8. "time"
  9. )
  10. // 优雅退出
  11. type Shutdown struct {
  12. ch chan os.Signal
  13. timeout time.Duration
  14. }
  15. func New(t time.Duration) *Shutdown {
  16. return &Shutdown{
  17. ch: make(chan os.Signal),
  18. timeout: t,
  19. }
  20. }
  21. func (s *Shutdown) Add(signals ...os.Signal) {
  22. signal.Notify(s.ch, signals...)
  23. }
  24. func (s *Shutdown) Start(server *http.Server) {
  25. <-s.ch
  26. fmt.Println("start exist......")
  27. ctx, cannel := context.WithTimeout(context.Background(), s.timeout*time.Second)
  28. defer cannel()
  29. if err := server.Shutdown(ctx); err != nil {
  30. fmt.Println("Graceful exit failed. err: ", err)
  31. }
  32. fmt.Println("Graceful exit success.")
  33. }
  1. package main
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "go-hello-world/pkg/shutdown"
  5. "go-hello-world/router"
  6. "log"
  7. "net/http"
  8. "syscall"
  9. "time"
  10. )
  11. func main() {
  12. r := gin.New()
  13. // 注册路由
  14. router.SetupRouter(r)
  15. server := &http.Server{
  16. Addr: ":8080",
  17. Handler: r,
  18. }
  19. // 运行服务
  20. go func() {
  21. err := server.ListenAndServe()
  22. if err != nil && err != http.ErrServerClosed {
  23. log.Fatalf("server.ListenAndServe err: %v", err)
  24. }
  25. }()
  26. // 优雅退出
  27. quit := shutdown.New(10)
  28. quit.Add(syscall.SIGINT, syscall.SIGTERM)
  29. quit.Start(server)
  30. }
当接收到SIG信号的时候,就会调用<font style="color:rgb(239, 112, 96);">Shutdown</font>方法做应用退出处理。 除此,还要结合K8S的PreStop Hook来定义结束前的钩子,如下:
  1. lifecycle:
  2. preStop:
  3. exec:
  4. command:
  5. - /bin/sh
  6. - '-c'
  7. - sleep 30
如果使用注册中心,比如nacos,可以在PreStop Hook中先告诉nacos要下线,如下:
  1. lifecycle:
  2. preStop:
  3. exec:
  4. command:
  5. - /bin/sh
  6. - -c
  7. - "curl -X DELETE your_nacos_ip:8848/nacos/v1/ns/instance?serviceName=nacos.test.1&ip=${POD_IP}&port=8880&clusterName=DEFAULT" && sleep 30

定义Metrics接口

Metrics主要用来暴露应用指标,可以根据实际情况自定义指标,以便于监控工具Prometheus进行数据收集展示。 有些语言有现成的<font style="color:rgb(0, 0, 0);">exporter</font>,比如java的jmx_exporter,没有的就需要自己在应用中集成。 比如:
  1. package main
  2. import (
  3. "github.com/SkyAPM/go2sky"
  4. v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
  5. "github.com/SkyAPM/go2sky/reporter"
  6. "github.com/gin-gonic/gin"
  7. "github.com/prometheus/client_golang/prometheus/promhttp"
  8. "go-hello-world/pkg/shutdown"
  9. "go-hello-world/router"
  10. "log"
  11. "net/http"
  12. "syscall"
  13. "time"
  14. )
  15. var SKYWALKING_ENABLED = false
  16. func main() {
  17. r := gin.New()
  18. // 注册路由
  19. router.SetupRouter(r)
  20. server := &http.Server{
  21. Addr: ":8080",
  22. Handler: r,
  23. }
  24. // 启动metrics服务
  25. go func() {
  26. http.Handle("/metrics", promhttp.Handler())
  27. if err := http.ListenAndServe(":9527", nil); err != nil {
  28. log.Printf("metrics port listen failed. err: %s", err)
  29. }
  30. }()
  31. // 运行服务
  32. go func() {
  33. err := server.ListenAndServe()
  34. if err != nil && err != http.ErrServerClosed {
  35. log.Fatalf("server.ListenAndServe err: %v", err)
  36. }
  37. }()
  38. // 优雅退出
  39. quit := shutdown.New(10)
  40. quit.Add(syscall.SIGINT, syscall.SIGTERM)
  41. quit.Start(server)
  42. }
这种会暴露默认的Http指标,可以通过<font style="color:rgb(239, 112, 96);">curl 127.0.0.1:9527/metrics</font>获取指标。
  1. ......
  2. # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
  3. # TYPE promhttp_metric_handler_requests_total counter
  4. promhttp_metric_handler_requests_total{code="200"} 0
  5. promhttp_metric_handler_requests_total{code="500"} 0
  6. promhttp_metric_handler_requests_total{code="503"} 0
如果需要自定义指标的话,只需按规则定义即可,如下:
  1. package metrics
  2. import (
  3. "github.com/prometheus/client_golang/prometheus"
  4. "net/http"
  5. "time"
  6. )
  7. var (
  8. // HttpserverRequestTotal 表示接收http请求总数
  9. HttpserverRequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
  10. Name: "httpserver_request_total",
  11. Help: "The Total number of httpserver requests",
  12. },
  13. // 设置标签:请求方法和路径
  14. []string{"method", "endpoint"})
  15. HttpserverRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
  16. Name: "httpserver_request_duration_seconds",
  17. Help: "httpserver request duration distribution",
  18. Buckets: []float64{0.1, 0.3, 0.5, 0.7, 0.9, 1},
  19. },
  20. []string{"method", "endpoint"})
  21. )
  22. // 注册监控指标
  23. func init() {
  24. prometheus.MustRegister(HttpserverRequestTotal)
  25. prometheus.MustRegister(HttpserverRequestDuration)
  26. }
  27. func NewMetrics(router http.HandlerFunc) http.HandlerFunc {
  28. return func(w http.ResponseWriter, r *http.Request) {
  29. start := time.Now()
  30. router(w, r)
  31. duration := time.Since(start)
  32. // httpserverRequestTotal 记录
  33. HttpserverRequestTotal.With(prometheus.Labels{"method": r.Method, "endpoint": r.URL.Path}).Inc()
  34. // httpserverRequestDuration 记录
  35. HttpserverRequestDuration.With(prometheus.Labels{"method": r.Method, "endpoint": r.URL.Path}).Observe(duration.Seconds())
  36. }
  37. }
这样就定义了httpserver_request_total和httpserver_request_duration_seconds指标,引用过后就能在/metrics中看到对应的数据。 定义好了指标,下面就是收集了。既可以通过自定义收集规则收集,也可以通过自动发现的方式收集,为了方便,主要采用自动发现的方式。 只需要在deployment的templates中定义好annotation,prometheeus就会自动添加采集目标,如下:
  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. labels:
  5. app: httpserver
  6. name: httpserver
  7. namespace: default
  8. spec:
  9. replicas: 2
  10. selector:
  11. matchLabels:
  12. app: httpserver
  13. template:
  14. metadata:
  15. annotations:
  16. prometheus.io/scrape: "true"
  17. prometheus.io/port: "metrics"
  18. labels:
  19. app: httpserver
  20. spec:
  21. containers:
  22. image: baidjay/httpserver:ubuntu-v3-metrics
  23. imagePullPolicy: IfNotPresent
  24. lifecycle:
  25. preStop:
  26. exec:
  27. command:
  28. - /bin/sh
  29. - -c
  30. - sleep 15
  31. livenessProbe:
  32. failureThreshold: 3
  33. httpGet:
  34. path: /healthz
  35. port: http
  36. scheme: HTTP
  37. initialDelaySeconds: 30
  38. periodSeconds: 10
  39. successThreshold: 1
  40. timeoutSeconds: 3
  41. name: httpserver
  42. ports:
  43. - containerPort: 8080
  44. name: http
  45. protocol: TCP
  46. - name: metrics
  47. protocol: TCP
  48. containerPort: 9527
  49. readinessProbe:
  50. failureThreshold: 3
  51. httpGet:
  52. path: /healthz
  53. port: http
  54. scheme: HTTP
  55. initialDelaySeconds: 20
  56. periodSeconds: 10
  57. successThreshold: 1
  58. timeoutSeconds: 3

定义Trace功能

Trace用于跟踪,每个请求都会生成一个TraceID,这个ID会伴随请求的整个生命周期,也可以根据这个ID查询请求的整个链路情况。 链路追踪,目前市面上有很多开源系统,比如Skywalking,Jeager,Zipkin等,它们各有各的特点,如下。
Pinpoint Zipkin Jaeger Skywalking
OpenTracing兼容 否 是 是 是
客户端支持语言 java\php java\go\php等 java\go\php等 java\nodejs\php等
存储 hbase es\mysql\内存等 es\kafka\内存等 es\mysql\h2等
传输协议支持 thrift http\mq udp\http grpc
UI丰富程度 高 低 中 中
实现方式 字节码注入 拦截请求 拦截请求 字节码注入
扩展性 低 高 高 中
Trace查询 不支持 支持 支持 支持
告警支持 支持 不支持 不支持 支持
JVM监控 支持 不支持 不支持 支持
性能损失 高 中 中 低
比较推荐使用Jaeger,它是CNCF的毕业项目,成长空间和云原生的系统架构兼容性比较好。 不过,这里采用的Skywalking。 Skywalking有许多现成的客户端,比如Java、Python等,可以直接使用,它们都会自动埋点,但是对于Go来说就只有自己手动埋点了,需要自己去写代码。 比如:
  1. package main
  2. import (
  3. "github.com/SkyAPM/go2sky"
  4. v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
  5. "github.com/SkyAPM/go2sky/reporter"
  6. "github.com/gin-gonic/gin"
  7. "github.com/prometheus/client_golang/prometheus/promhttp"
  8. "go-hello-world/pkg/shutdown"
  9. "go-hello-world/router"
  10. "log"
  11. "net/http"
  12. "syscall"
  13. "time"
  14. )
  15. var SKYWALKING_ENABLED = false
  16. func main() {
  17. r := gin.New()
  18. // 配置skywalking
  19. if SKYWALKING_ENABLED {
  20. rp, err := reporter.NewGRPCReporter("skywalking-oap:11800", reporter.WithCheckInterval(time.Second))
  21. if err != nil {
  22. log.Printf("create gosky reporter failed. err: %s", err)
  23. }
  24. defer rp.Close()
  25. tracer, _ := go2sky.NewTracer("go-hello-world", go2sky.WithReporter(rp))
  26. r.Use(v3.Middleware(r, tracer))
  27. }
  28. // 注册路由
  29. router.SetupRouter(r)
  30. server := &http.Server{
  31. Addr: ":8080",
  32. Handler: r,
  33. }
  34. // 启动metrics服务
  35. go func() {
  36. http.Handle("/metrics", promhttp.Handler())
  37. if err := http.ListenAndServe(":9527", nil); err != nil {
  38. log.Printf("metrics port listen failed. err: %s", err)
  39. }
  40. }()
  41. // 运行服务
  42. go func() {
  43. err := server.ListenAndServe()
  44. if err != nil && err != http.ErrServerClosed {
  45. log.Fatalf("server.ListenAndServe err: %v", err)
  46. }
  47. }()
  48. // 优雅退出
  49. quit := shutdown.New(10)
  50. quit.Add(syscall.SIGINT, syscall.SIGTERM)
  51. quit.Start(server)
  52. }
定义reporter用于上报数据给Skywalking,这就是一个简单的集成Trace的例子。

定义标准的日志

应用的可观测性主要来源日志、监控、链路追踪,标准的日志有利于日志收集以及排查问题。 原则上,不论是什么类型的日志输出,什么格式的日志内容,都能收集。但是为了方便友好,建议把日志输出到标准输出,这样收集更方便。 个人理解,在K8s中,完全没必要把日志输出到文件,浪费不说,没多大意义,因为所有的日志都会收集到日志系统,而输出到文件的日志也会随着应用发版而丢失,所以输出到文件的意义是什么呢?

运维侧

开发把系统开发完,就会交付给运维部署。为了保障应用的稳定性,运维在部署应用的时候应该考虑以下几点。
  • 应用尽可能保持无状态
  • 应用尽可能保持高可用
  • 应该具备优雅上线能力
  • 应该具备异常自愈能力
  • 可以使用HTTPS访问

应用尽可能保持无状态

K8S中可以部署有状态应用,也可以部署无状态应用。对于有状态应用,其实很少部署到K8S中,大部分还是部署的无状态应用,至于为什么,用多了就晓得了。

对于业务应用,强烈建议使其保持无状态,就算有需要持久化的东西,要么保存到数据库,要么保存到对象存储或者其他单独的文件系统中,不要挂载到应用Pod上。

这样的好处是,应用和数据是分开的,应用可以随意启停、扩展、迁移等。

应用尽可能的保持高可用

保持高可用应该是每个运维人员的使命。 在K8S中,应该怎么配置呢? (1)应用Pod应该是多副本 (2)应用Pod之间做反亲和性,避免同一应用调度到同一台主机,如下。
  1. ......
  2. spec:
  3. affinity:
  4. podAntiAffinity:
  5. requiredDuringSchedulingIgnoredDuringExecution:
  6. - labelSelector:
  7. matchExpressions:
  8. - key: app
  9. operator: In
  10. values: [ "httpserver" ]
  11. topologyKey: kubernetes.io/hostname
  12. ......
(3) 为了避免应用因为节点维护等原因驱逐Pod,导致全部Pod被驱逐,特别配置了PodDisruptionBudget,保障应用至少有一个可用,如下。
  1. apiVersion: policy/v1beta1
  2. kind: PodDisruptionBudget
  3. metadata:
  4. name: httpserver
  5. spec:
  6. minAvailable: 1
  7. selector:
  8. matchLables:
  9. app: httpserver
(4)如果某个节点因为一些原因需要驱逐一些Pod,为了避免重要应用被驱逐,应该给应用配置较高的QoS,如下:
  1. resources:
  2. limits:
  3. cpu: "1"
  4. memory: 2Gi
  5. requests:
  6. cpu: "1"
  7. memory: 2Gi

应用具备优雅上线能力

所谓优雅上线能力,就是要确保应用能够提供服务了,再接入外界流量,不能在还没完全启动的情况下就提供服务。 在K8S中,应用在启动后会加入endpoints中,然后通过service接入流量,那在什么情况下才算启动成功呢?主要是通过K8S的ReadinessProbe来进行检测。这时候开发的健康检测接口就派上用场了,如下:
  1. ...
  2. readinessProbe:
  3. failureThreshold: 3
  4. httpGet:
  5. path: /health
  6. port: http
  7. scheme: HTTP
  8. initialDelaySeconds: 20
  9. periodSeconds: 10
  10. successThreshold: 1
  11. timeoutSeconds: 3
  12. ...
所以K8S的YAML文件应该加上如上的配置。

应该具备异常自愈能力

所谓异常自愈,就是应用本身在出现Crash,或者应用Pod所在节点出现异常的情况,应用能够自动重启或者迁移。这时候就需要通过K8S的LivenessProbe来进行检测了,如下。
  1. ......
  2. livenessProbe:
  3. failureThreshold: 3
  4. httpGet:
  5. path: /health
  6. port: http
  7. scheme: HTTP
  8. initialDelaySeconds: 30
  9. periodSeconds: 10
  10. successThreshold: 1
  11. timeoutSeconds: 3
  12. ......
当K8S的YAML清单加上如上配置过后,就会定时去探测应用是否正常,如果异常,就会触发重启的动作。如果是节点异常,K8S会对Pod进行重新调度。

可以使用HTTPS进行访问

应用通过HTTPS访问是比较常见的,企业级应用建议自己购买相应的SSL证书,然后进行配置即可。 比如。
  1. # 创建证书secret
  2. kubectl create secret tls httpserver-tls-secret --cert=path/to/tls.cert --key=path/to/tls.key
  1. # 在ingress中引用
  2. ......
  3. spec:
  4. tls:
  5. hosts:
  6. - httpserver.coolops.cn
  7. secretName: httpserver-tls-secret
  8. rules:
  9. - host: httpserver.coolops.cn
  10. ......

总结

上面介绍了开发和运维对于应用上线应该做的工作,不全但够用。 在不同的企业都有不同的习性,但是作为运维,都要牢牢记住稳定永远是第一习性。通过上面的梳理,应用模板就整理如下:
  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. labels:
  5. app: httpserver
  6. name: httpserver
  7. namespace: default
  8. spec:
  9. progressDeadlineSeconds: 600
  10. replicas: 2
  11. revisionHistoryLimit: 10
  12. selector:
  13. matchLabels:
  14. app: httpserver
  15. strategy:
  16. rollingUpdate:
  17. maxSurge: 25%
  18. maxUnavailable: 25%
  19. type: RollingUpdate
  20. template:
  21. metadata:
  22. annotations:
  23. prometheus.io/scrape: "true"
  24. prometheus.io/port: "metrics"
  25. labels:
  26. app: httpserver
  27. spec:
  28. affinity:
  29. podAntiAffinity:
  30. requiredDuringSchedulingIgnoredDuringExecution:
  31. - labelSelector:
  32. matchExpressions:
  33. - key: app
  34. operator: In
  35. values: [ "httpserver" ]
  36. topologyKey: kubernetes.io/hostname
  37. containers:
  38. - env:
  39. - name: TZ
  40. value: Asia/Shanghai
  41. - name: POD_NAME
  42. valueFrom:
  43. fieldRef:
  44. apiVersion: v1
  45. fieldPath: metadata.name
  46. - name: POD_NAMESPACE
  47. valueFrom:
  48. fieldRef:
  49. apiVersion: v1
  50. fieldPath: metadata.namespace
  51. image: baidjay/httpserver:ubuntu-v3-metrics
  52. imagePullPolicy: IfNotPresent
  53. lifecycle:
  54. preStop:
  55. exec:
  56. command:
  57. - /bin/sh
  58. - -c
  59. - sleep 15
  60. livenessProbe:
  61. failureThreshold: 3
  62. httpGet:
  63. path: /healthz
  64. port: http
  65. scheme: HTTP
  66. initialDelaySeconds: 30
  67. periodSeconds: 10
  68. successThreshold: 1
  69. timeoutSeconds: 3
  70. name: httpserver
  71. ports:
  72. - containerPort: 8080
  73. name: http
  74. protocol: TCP
  75. - name: metrics
  76. protocol: TCP
  77. containerPort: 9527
  78. readinessProbe:
  79. failureThreshold: 3
  80. httpGet:
  81. path: /healthz
  82. port: http
  83. scheme: HTTP
  84. initialDelaySeconds: 20
  85. periodSeconds: 10
  86. successThreshold: 1
  87. timeoutSeconds: 3
  88. resources:
  89. limits:
  90. cpu: "1"
  91. memory: 2Gi
  92. requests:
  93. cpu: "1"
  94. memory: 2Gi
  95. securityContext: {}
  96. terminationMessagePath: /dev/termination-log
  97. terminationMessagePolicy: File
  98. dnsPolicy: ClusterFirst
  99. restartPolicy: Always
  100. schedulerName: default-scheduler
  101. ---
  102. apiVersion: v1
  103. kind: Service
  104. metadata:
  105. name: httpserver
  106. spec:
  107. ports:
  108. - name: http
  109. port: 8080
  110. protocol: TCP
  111. targetPort: http
  112. - name: metrics
  113. port: 9527
  114. protocol: TCP
  115. targetPort: metrics
  116. selector:
  117. app: httpserver
  118. sessionAffinity: None
  119. type: ClusterIP
  120. ---
  121. apiVersion: networking.k8s.io/v1
  122. kind: Ingress
  123. metadata:
  124. annotations:
  125. nginx.ingress.kubernetes.io/proxy-body-size: 100m
  126. nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
  127. nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
  128. nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
  129. nginx.ingress.kubernetes.io/service-weight: ""
  130. nginx.org/client-max-body-size: 100m
  131. name: httpserver-tls
  132. spec:
  133. tls:
  134. - hosts:
  135. - httpserver.coolops.cn
  136. secretName: httpserver-tls-secret
  137. rules:
  138. - host: httpserver.coolops.cn
  139. http:
  140. paths:
  141. - pathType: Prefix
  142. path: /
  143. backend:
  144. service:
  145. name: httpserver
  146. port:
  147. number: 8080
  148. ---
  149. apiVersion: policy/v1
  150. kind: PodDisruptionBudget
  151. metadata:
  152. name: httpserver
  153. spec:
  154. minAvailable: 1
  155. selector:
  156. matchLabels:
  157. app: httpserver