阿里云k8s服务springboot项目应用升级时出现502错误
背景
随着小步快跑、快速迭代的开发模式被越来越多的互联网企业认同和采用,应用的变更、升级频率变得越来越频繁。为了应对不同的升级需求,保证升级过程平稳顺利地进行,诞生了一系列的部署发布模式。
- 停机发布 - 把老版的应用实例完全停止,再发布新的版本。这种发布模式主要为了解决新老版本互不兼容、无法共存的问题,缺点是一段时间内服务完全不可用。
- 蓝绿发布 - 在线上同时部署相同数量的新老版本应用实例。待新版本测试通过后,将流量一次性地切到新的服务实例上来。这种发布模式解决了停机发布中存在的服务完全不可用问题,但会造成比较大的资源消耗。
- 滚动发布 - 分批次逐步替换应用实例。这种发布模式不会中断服务,同时也不会消耗过多额外的资源,但由于新老版本实例同时在线,可能导致来自相同客户端的请求在新老版中切换而产生兼容性问题。
- 金丝雀发布 - 逐渐将流量从老版本切换到新版本上。如果观察一段时间后没有发现问题,就进一步扩大新版本流量,同时减少老版本上流量。
- A/B 测试 - 同时上线两个或多个版本,收集用户对这些版本的反馈,分析评估出最好版本正式采用。
随着越来越多的应用被容器化,如何方便地让容器应用平稳顺利升级受到了广泛关注。本文将介绍 k8s 中不同部署形式下应用的升级方法,并重点介绍如何对 Deployment 中的应用实施滚动发布(本文所作的调研基于k8s 1.13
)。
K8s 应用升级
在 k8s 中,pod 是部署和升级的基本单位。一般来说,一个 pod 代表一个应用实例,而 pod 又会以 Deployment、StatefulSet、DaemonSet、Job 等形式部署运行,下面依次介绍在这些部署形式下 pod 的升级方法。
Deployment
Deployment 是 pod 最常见的部署形式,这里将以基于 spring boot 的 java 应用为例进行介绍。该应用是基于真实应用抽象出来的简单版本,非常具有代表性,它有如下特点:
- 应用启动后,需要花费一定的时间加载配置,在这段时间内,无法对外提供服务。
- 应用能够启动并不意味着它能够正常提供服务。
- 应用如果无法提供服务不一定能自动退出。
- 在升级过程中需要保证即将下线的应用实例不会接收到新的请求且有足够时间处理完当前请求。
参数配置
为了让具有上述特点的应用实现零宕机时间和无生产中断的升级,需要精心地配置 Deployment 中的相关参数。这里和升级有关的配置如下(完整配置参见 spring-boot-probes-v1.yaml)。
kind: Deployment ... spec: replicas: 8 strategy: type: RollingUpdate rollingUpdate: maxSurge: 3 maxUnavailable: 2 minReadySeconds: 120 ... template: ... spec: containers: - name: spring-boot-probes image: registry.cn-hangzhou.aliyuncs.com/log-service/spring-boot-probes:1.0.0 ports: - containerPort: 8080 terminationGracePeriodSeconds: 60 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 successThreshold: 1 failureThreshold: 1 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 40 periodSeconds: 20 successThreshold: 1 failureThreshold: 3 ...
配置 strategy
通过 strategy 可以配置 pod 的替换策略,主要参数如下。
-
.spec.strategy.type
- 用于指定替换 pod 的策略类型。该参数可取值 Recreate 或 RollingUpdate,默认为 RollingUpdate。- Recreate - K8s 会先删掉全部原有 pod 再创建新的 pod。该方式适用于新老版本互不兼容、无法共存的场景。但由于该方式会造成一段时间内服务完全不可用,在上述场景之外须慎用。
- RollingUpdate - K8s 会将 pod 分批次逐步替换掉,可用来实现服务热升级。
.spec.strategy.rollingUpdate.maxSurge
- 指定在滚动更新过程中最多可创建多少个额外的 pod,可以是数字或百分比。该值设置得越大、升级速度越快,但会消耗更多的系统资源。.spec.strategy.rollingUpdate.maxUnavailable
- 指定在滚动更新过程中最多允许多少个 pod 不可用, 可以是数字或百分比。该值设置得越大、升级速度越快,但服务会越不稳定。
通过调节 maxSurge 和 maxUnavailable,可以满足不同场景下的升级需求。
- 如果您希望在保证系统可用性和稳定性的前提下尽可能快地进行升级,可以将 maxUnavailable 设置为 0,同时为 maxSurge 赋予一个较大值。
- 如果系统资源比较紧张,pod 负载又比较低,为了加快升级速度,可以将 maxSurge 设置为 0,同时为 maxUnavailable 赋予一个较大值。需要注意的是,如果 maxSurge 为 0,maxUnavailable 为 DESIRED,可能造成整个服务的不可用,此时 RollingUpdate 将退化成停机发布。
样例选择了一个折中方案,将 maxSurge 设置为 3,将 maxUnavailable 设置为 2,平衡了稳定性、资源消耗和升级速度。
配置探针
K8s 提供以下两类探针:
- ReadinessProbe - 默认情况下,一旦某个 pod 中的所有容器全部启动,k8s 就会认为该 pod 处于就绪状态,从而将流量发往该 pod。但某些应用启动后,还需要完成数据或配置文件的加载工作才能对外提供服务,因此通过容器是否启动来判断其是否就绪并不严谨。通过为容器配置就绪探针,能让 k8s 更准确地判断容器是否就绪,从而构建出更健壮的应用。K8s 保证只有 pod 中的所有容器全部通过了就绪探测,才允许 service 将流量发往该 pod。一旦就绪探测失败,k8s 会停止将流量发往该 pod。
- LivenessProbe - 默认情况下,k8s 会认为处于运行状态下的容器是可用的。但如果应用在出现问题或不健康时无法自动退出(例如发生严重死锁),这种判断就会出现问题。通过为容器配置活性探针,能让 k8s 更准确地判断容器是否正常运行。如果容器没有通过活性探测,kubelet 会将其停止,并根据重启策略决定下一步的动作。
探针的配置非常灵活,用户可以指定探针的探测频率、探测成功阈值、探测失败阈值等。各参数的含义和配置方法可参考文档 Configure Liveness and Readiness Probes。
样例为目标容器配置了就绪探针和活性探针:
- 就绪探针的 initialDelaySeconds 设置成 30,这是因为应用平均需要 30 秒时间完成初始化工作。
- 在配置活性探针时,需要保证容器有足够时间到达就绪状态。如果参数 initialDelaySeconds、periodSeconds、failureThreshold 设置得过小,可能造成容器还未就绪就被重启,以至于永远无法达到就绪状态。样例中的配置保证如果容器能在启动后的 80 秒内就绪就不会被重启,相对 30 秒的平均初始化时间有足够的缓冲。
- 就绪探针的 periodSeconds 设置成 10,failureThreshold 设置成 1。这样当容器异常时,大约 10 秒后就不会有流量发往它。
- 活性探针的 periodSeconds 设置成 20,failureThreshold 设置成 3。这样当容器异常时,大约 60 秒后就不会被重启。
配置 minReadySeconds
默认情况下,一旦新创建的 pod 变成就绪状态 k8s 就会认为该 pod 是可用的,从而将老的 pod 删除掉。但有时问题可能会在新 pod 真正处理用户请求时才暴露,因此一个更稳健的做法是当某个新 pod 就绪后对其观察一段时间再删掉老的 pod。
参数 minReadySeconds 可以控制 pod 处于就绪状态的观察时间。如果 pod 中的容器在这段时间内都能正常运行,k8s 才会认为新 pod 可用,从而将老的 pod 删除掉。在配置该参数时,需要仔细权衡,如果设置得过小,可能造成观察不充分,如果设置得过大,又会拖慢升级进度。样例将 minReadySeconds 设置成了 120 秒,这样能保证处于就绪状态的 pod 能经历一个完整的活性探测周期。
配置 terminationGracePeriodSeconds
当 k8s 准备删除一个 pod 时,会向该 pod 中的容器发送 TERM 信号并同时将 pod 从 service 的 endpoint 列表中移除。如果容器无法在规定时间(默认 30 秒)内终止,k8s 会向容器发送 SIGKILL 信号强制终止进程。Pod 终止的详细流程可参考文档 Termination of Pods。
由于应用处理请求最长耗时 40 秒,为了让其在关闭前能够处理完已到达服务端的请求,样例设置了 60 秒的优雅关闭时间。针对不同的应用,您可以根据实际情况调整 terminationGracePeriodSeconds 的取值。
观察升级行为
上述配置能够保证目标应用的平滑升级。我们可以通过更改 Deployment 中 PodTemplateSpec 的任意字段触发 pod 升级,并通过运行命令kubectl get rs -w
观察升级行为。这里观察到的新老版本的 pod 副本数的变化情况如下:
- 创建 maxSurge 个新 pod。这时 pod 总数达到了允许的上限,即 DESIRED + maxSurge。
- 不等新 pod 就绪或可用,立刻启动 maxUnavailable 个老 pod 的删除流程。这时可用 pod 数为 DESIRED - maxUnavailable。
- 某个老 pod 被完全删除,这时会立刻补充一个新 pod。
- 某个新 pod 通过了就绪探测变成了就绪态,k8s 会将流量发往该 pod。但由于未达到规定的观察时间,该 pod 并不会被视作可用。
- 某个就绪 pod 在观察期内运行正常被视作可用,这时可以再次启动某个老 pod 的删除流程。
- 重复步骤 3、4、5 直到所有老 pod 被删除,并且可用的新 pod 达到目标副本数。
失败回滚
应用的升级并不总会一帆风顺,在升级过程中或升级完成后都有可能遇到新版本行为不符合预期需要回滚到稳定版本的情况。K8s 会将 PodTemplateSpec 的每一次变更(如果更新模板标签或容器镜像)都记录下来。这样,如果新版本出现问题,就可以根据版本号方便地回滚到稳定版本。回滚 Deployment 的详细操作步骤可参考文档 Rolling Back a Deployment。
StatefulSet
StatefulSet 是针对有状态 pod 常用的部署形式。针对这类 pod,k8s 同样提供了许多参数用于灵活地控制它们的升级行为。好消息是这些参数大部分都和升级 Deployment 中的 pod 相同。这里重点介绍两者存在差异的地方。
策略类型
在 k8s 1.7 及之后的版本中,StatefulSet 支持 OnDelete 和 RollingUpdate 两种策略类型。
- OnDelete - 当更新了 StatefulSet 中的 PodTemplateSpec 后,只有手动删除旧的 pod 后才会创建新版本 pod。这是默认的更新策略,一方面是为了兼容 k8s 1.6 及之前的版本,另一方面也是为了支持升级过程中新老版本 pod 互不兼容、无法共存的场景。
- RollingUpdate - K8s 会将 StatefulSet 管理的 pod 分批次逐步替换掉。它与 Deployment 中 RollingUpdate 的区别在于 pod 的替换是有序的。例如一个 StatefulSet 中包含 N 个 pod,在部署的时候这些 pod 被分配了从 0 开始单调递增的序号,而在滚动更新时,它们会按逆序依次被替换。
Partition
可以通过参数.spec.updateStrategy.rollingUpdate.partition
实现只升级部分 pod 的目的。在配置了 partition 后,只有序号大于或等于 partition 的 pod 才会进行滚动升级,其余 pod 将保持不变。
Partition 的另一个应用是可以通过不断减少 partition 的取值实现金丝雀升级。具体操作方法可参考文档 Rolling Out a Canary。
DaemonSet
DaemonSet 保证在全部(或者一些)k8s 工作节点上运行一个 pod 的副本,常用来运行监控或日志收集程序。对于 DaemonSet 中的 pod,用于控制它们升级行为的参数与 Deployment 几乎一致,只是在策略类型方面略有差异。DaemonSet 支持 OnDelete 和 RollingUpdate 两种策略类型。
- OnDelete - 当更新了 DaemonSet 中的 PodTemplateSpec 后,只有手动删除旧的 pod 后才会创建新版本 pod。这是默认的更新策略,一方面是为了兼容 k8s 1.5 及之前的版本,另一方面也是为了支持升级过程中新老版本 pod 互不兼容、无法共存的场景。
- RollingUpdate - 其含义和可配参数与 Deployment 的 RollingUpdate 一致。
滚动更新 DaemonSet 的具体操作步骤可参考文档 Perform a Rolling Update on a DaemonSet。
Job
Deployment、StatefulSet、DaemonSet 一般用于部署运行常驻进程,而 Job 中的 pod 在执行完特定任务后就会退出,因此不存在滚动更新的概念。当您更改了一个 Job 中的 PodTemplateSpec 后,需要手动删掉老的 Job 和 pod,并以新的配置重新运行该 job。
总结
K8s 提供的功能可以让大部分应用实现零宕机时间和无生产中断的升级,但也存在一些没有解决的问题,主要包括以下几点:
- 目前 k8s 原生仅支持停机发布、滚动发布两类部署升级策略。如果应用有蓝绿发布、金丝雀发布、A/B 测试等需求,需要进行二次开发或使用一些第三方工具。
- K8s 虽然提供了回滚功能,但回滚操作必须手动完成,无法根据条件自动回滚。
- 有些应用在扩容或缩容时同样需要分批逐步执行,k8s 还未提供类似的功能。
实例配置:
livenessProbe: failureThreshold: 3 httpGet: path: /user/service/test port: 8080 scheme: HTTP initialDelaySeconds: 40 periodSeconds: 20 successThreshold: 1 timeoutSeconds: 1 name: dataline-dev ports: - containerPort: 8080 protocol: TCP readinessProbe: failureThreshold: 1 httpGet: path: /user/service/test port: 8080 scheme: HTTP initialDelaySeconds: 30 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1
经测试 , 再对sprintboot 应用进行更新时, 访问不再出现502的报错。