Kubernetes 服务部署最佳实践(二) ——如何提升服务可用性

引言

上一篇文章咱们围绕如何合理利用资源的主题作了一些最佳实践的分享,这一次咱们就如何提升服务可用性的主题来展开探讨。html

怎样提升咱们部署服务的可用性呢?K8S 设计自己就考虑到了各类故障的可能性,并提供了一些自愈机制以提升系统的容错性,但有些状况仍是可能致使较长时间不可用,拉低服务可用性的指标。本文将结合生产实践经验,为你们提供一些最佳实践来最大化的提升服务可用性。java

如何避免单点故障?

K8S 的设计就是假设节点是不可靠的。节点越多,发生软硬件故障致使节点不可用的概率就越高,因此咱们一般须要给服务部署多个副本,根据实际状况调整 replicas 的值,若是值为 1 就必然存在单点故障,若是大于 1 但全部副本都调度到同一个节点了,那仍是有单点故障,有时候还要考虑到灾难,好比整个机房不可用。node

因此咱们不只要有合理的副本数量,还须要让这些不一样副本调度到不一样的拓扑域(节点、可用区),打散调度以免单点故障,这个能够利用 Pod 反亲和性来作到,反亲和主要分强反亲和与弱反亲和两种。更多亲和与反亲和信息可参考官方文档Affinity and anti-affinitynginx

先来看个强反亲和的示例,将 DNS 服务强制打散调度到不一样节点上:api

affinity: podAntiAffinity:   requiredDuringSchedulingIgnoredDuringExecution:   - labelSelector:       matchExpressions:       - key: k8s-app         operator: In         values:         - kube-dns     topologyKey: kubernetes.io/hostname
  • labelSelector.matchExpressions 写该服务对应 pod 中 labels 的 key 与 value,由于 Pod 反亲和性是经过判断 replicas 的 pod label 来实现的。
  • topologyKey 指定反亲和的拓扑域,即节点 label 的 key。这里用的 kubernetes.io/hostname 表示避免 pod 调度到同一节点,若是你有更高的要求,好比避免调度到同一个可用区,实现异地多活,能够用 failure-domain.beta.kubernetes.io/zone。一般不会去避免调度到同一个地域,由于通常同一个集群的节点都在一个地域,若是跨地域,即便用专线时延也会很大,因此 topologyKey 通常不至于用 failure-domain.beta.kubernetes.io/region
  • requiredDuringSchedulingIgnoredDuringExecution 调度时必须知足该反亲和性条件,若是没有节点知足条件就不调度到任何节点 (Pending)。

若是不用这种硬性条件可使用 preferredDuringSchedulingIgnoredDuringExecution 来指示调度器尽可能知足反亲和性条件,即弱反亲和性,若是实在没有知足条件的,只要节点有足够资源,仍是可让其调度到某个节点,至少不会 Pending。bash

咱们再来看个弱反亲和的示例:app

affinity:  podAntiAffinity:    preferredDuringSchedulingIgnoredDuringExecution:    - weight: 100      podAffinityTerm:        labelSelector:          matchExpressions:          - key: k8s-app            operator: In            values:            - kube-dns      topologyKey: kubernetes.io/hostname

注意到了吗?相比强反亲和有些不一样哦,多了一个 weight,表示此匹配条件的权重,而匹配条件被挪到了 podAffinityTerm 下面。dom

如何避免节点维护或升级时致使服务不可用?

有时候咱们须要对节点进行维护或进行版本升级等操做,操做以前须要对节点执行驱逐 (kubectl drain),驱逐时会将节点上的 Pod 进行删除,以便它们漂移到其它节点上,当驱逐完毕以后,节点上的 Pod 都漂移到其它节点了,这时咱们就能够放心的对节点进行操做了。分布式

有一个问题就是,驱逐节点是一种有损操做,驱逐的原理:post

  1. 封锁节点 (设为不可调度,避免新的 Pod 调度上来)。
  2. 将该节点上的 Pod 删除。
  3. ReplicaSet 控制器检测到 Pod 减小,会从新建立一个 Pod,调度到新的节点上。

这个过程是先删除,再建立,并不是是滚动更新,所以更新过程当中,若是一个服务的全部副本都在被驱逐的节点上,则可能致使该服务不可用。

咱们再来下什么状况下驱逐会致使服务不可用:

  1. 服务存在单点故障,全部副本都在同一个节点,驱逐该节点时,就可能形成服务不可用。
  2. 服务没有单点故障,但恰好这个服务涉及的 Pod 所有都部署在这一批被驱逐的节点上,因此这个服务的全部 Pod 同时被删,也会形成服务不可用。
  3. 服务没有单点故障,也没有所有部署到这一批被驱逐的节点上,但驱逐时形成这个服务的一部分 Pod 被删,短期内服务的处理能力降低致使服务过载,部分请求没法处理,也就下降了服务可用性。

针对第一点,咱们可使用前面讲的反亲和性来避免单点故障。

针对第二和第三点,咱们能够经过配置 PDB (PodDisruptionBudget) 来避免全部副本同时被删除,驱逐时 K8S 会 "观察" nginx 的当前可用与指望的副本数,根据定义的 PDB 来控制 Pod 删除速率,达到阀值时会等待 Pod 在其它节点上启动并就绪后再继续删除,以免同时删除太多的 Pod 致使服务不可用或可用性下降,下面给出两个示例。

示例一 (保证驱逐时 nginx 至少有 90% 的副本可用):

apiVersion: policy/v1beta1kind: PodDisruptionBudgetmetadata:  name: zk-pdbspec:  minAvailable: 90%  selector:    matchLabels:      app: zookeeper

示例二 (保证驱逐时 zookeeper 最多有一个副本不可用,至关于逐个删除并等待在其它节点完成重建):

apiVersion: policy/v1beta1kind: PodDisruptionBudgetmetadata:  name: zk-pdbspec:  maxUnavailable: 1  selector:    matchLabels:      app: zookeeper

如何让服务进行平滑更新?

解决了服务单点故障和驱逐节点时致使的可用性下降问题后,咱们还须要考虑一种可能致使可用性下降的场景,那就是滚动更新。为何服务正常滚动更新也可能影响服务的可用性呢?别急,下面我来解释下缘由。

假如集群内存在服务间调用:

img

当 server 端发生滚动更新时:

img

发生两种尴尬的状况:

  1. 旧的副本很快销毁,而 client 所在节点 kube-proxy 还没更新完转发规则,仍然将新链接调度给旧副本,形成链接异常,可能会报 "connection refused" (进程中止过程当中,再也不接受新请求) 或 "no route to host" (容器已经彻底销毁,网卡和 IP 已不存在)。
  2. 新副本启动,client 所在节点 kube-proxy 很快 watch 到了新副本,更新了转发规则,并将新链接调度给新副本,但容器内的进程启动很慢 (好比 Tomcat 这种 java 进程),还在启动过程当中,端口还未监听,没法处理链接,也形成链接异常,一般会报 "connection refused" 的错误。

针对第一种状况,能够给 container 加 preStop,让 Pod 真正销毁前先 sleep 等待一段时间,等待 client 所在节点 kube-proxy 更新转发规则,而后再真正去销毁容器。这样能保证在 Pod Terminating 后还能继续正常运行一段时间,这段时间若是由于 client 侧的转发规则更新不及时致使还有新请求转发过来,Pod 仍是能够正常处理请求,避免了链接异常的发生。听起来感受有点不优雅,但实际效果仍是比较好的,分布式的世界没有银弹,咱们只能尽可能在当前设计现状下找到并实践可以解决问题的最优解。

针对第二种状况,能够给 container 加 ReadinessProbe (就绪检查),让容器内进程真正启动完成后才更新 Service 的 Endpoint,而后 client 所在节点 kube-proxy 再更新转发规则,让流量进来。这样可以保证等 Pod 彻底就绪了才会被转发流量,也就避免了连接异常的发生。

最佳实践 yaml 示例:

readinessProbe:          httpGet:            path: /healthz            port: 80            httpHeaders:            - name: X-Custom-Header              value: Awesome          initialDelaySeconds: 10          timeoutSeconds: 1        lifecycle:          preStop:            exec:              command: ["/bin/bash", "-c", "sleep 10"]

更多信息请参考 Specifying a Disruption Budget for your Application

健康检查怎么配才好?

咱们都知道,给 Pod 配置健康检查也是提升服务可用性的一种手段,配置 ReadinessProbe (就绪检查) 能够避免将流量转发给还没启动彻底或出现异常的 Pod;配置 LivenessProbe (存活检查) 可让存在 bug 致使死锁或 hang 住的应用重启来恢复。可是,若是配置配置很差,也可能引起其它问题,这里根据一些踩坑经验总结了一些指导性的建议:

  • 不要轻易使用 LivenessProbe,除非你了解后果而且明白为何你须要它,参考 Liveness Probes are Dangerous
  • 若是使用 LivenessProbe,不要和 ReadinessProbe 设置成同样 (failureThreshold 更大)
  • 探测逻辑里不要有外部依赖 (db, 其它 pod 等),避免抖动致使级联故障
  • 业务程序应尽可能暴露 HTTP 探测接口来适配健康检查,避免使用 TCP 探测,由于程序 hang 死时, TCP 探测仍然能经过 (TCP 的 SYN 包探测端口是否存活在内核态完成,应用层不感知)

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

相关文章
相关标签/搜索