为了账号安全,请及时绑定邮箱和手机立即绑定

微服务部署与优雅停机

标签:
大数据

00 前言

微服务部署是一个非常严谨的话题,微服务开发完成需要上线部署,在整个部署过程中怎么保证业务的连续性,怎么能让服务的客户端无感知,这是一个具有一定挑战性的问题。

为了达到不同目的,微服务的部署方式有很多种方式:滚动部署、蓝绿部署、灰度/金丝雀部署。无论是哪一种部署方式,都需要三步操作:停止老版本应用、部署新版本应用、切流量,这三步操作可能是手动也可能是自动,而且它们的顺序也不一定。这其中的两步是非常关键:切流量停止老版本应用,要想保证业务的连续性和客户端无感知,需要在这两个步骤上下功夫。

在上线部署过程中保证业务连续性的问题,在软件行业是一直存在,只是在不同的时期解决方案不一样。

  • 单体应用:依靠负载均衡器(例如nginx)手动切流量,逐步实现多节点部署;

  • 微服务(分布式):服务客户端自动同步服务端节点在线情况,以及丰富的容错机制;

  • 微服务(service Mesh):service Mesh 组件的智能负载均衡和容错机制;

上面的操作只是让服务调用方避开正在部署的节点,这样就能保证应用部署过程中业务的连续性了吗?不能。在这个过程忽略了一个关键点,应用停止的过程,想象一个场景:客户端刚发送完请求,到达服务端,服务端正在处理的过程中(还没有完成并响应给客户端),这时重新部署触发了停机操作。在这个场景中可以想象到,这时立即停止应用,这部分服务端正在处理的业务操作就会中断,这样的错误往往是很严重的。如果能解决这个问题,才能真正地在部署应用的时候保证业务的连续性,客户端无感知。

上面说到这个问题其实就是优雅停机解决的问题,前面已经有一篇文章从 Java 和 Spring boot 的角度介绍了优雅停机,里面包含了很多基础知识,详细请参见文章 Spring boot 2.0 之优雅停机
。这里总结一下这片文章的知识点:

  • 优雅停机的概念:在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响;

  • 优雅停机的测试方案;

  • Java语言是如何支持优雅停机的;

  • 为什么 Spring boot 的 actuator/shutdown 不支持优雅停机;

  • Spring boot 2.0 + tomcat(undertow)如何支持优雅停机的;

阅读本文之前最好先阅读一下上面这篇文章,了解一下基础知识。本文换个姿势再说优雅停机,主要从容器云平台(DCOS)、service Mesh组件(Linkerd)和应用开发框架(Spring boot)结合的角度介绍优雅停机,以及微服务的部署。

01 准备知识

在做下面的实现、测试和验证之前需要了解一些基础知识:

1. Spring boot 优雅停机

我们使用的开发框架组合方案是:Spring boot 2.0 + tomcat8,我们的应用进程需要实现优雅停机,我们的实现方式:

package com.epay.demox.unipay.provider;import org.apache.catalina.connector.Connector;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;import org.springframework.context.ApplicationListener;import org.springframework.context.event.ContextClosedEvent;import org.springframework.stereotype.Component;import java.util.concurrent.Executor;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/**
 * @Author: guoyankui
 * @DATE: 2018/5/20 12:59 PM
 *
 * 优雅关闭 Spring Boot tomcat
 */@Componentpublic class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {    private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);    private volatile Connector connector;    private final int waitTime = 30;    @Override
    public void customize(Connector connector) {        this.connector = connector;
    }    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {        if (connector == null) {            return;
        }        this.connector.pause();
        Executor executor = this.connector.getProtocolHandler().getExecutor();        if (executor instanceof ThreadPoolExecutor) {            try {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();                if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

spring boot配置:

@Autowiredprivate GracefulShutdownTomcat gracefulShutdownTomcat;@Beanpublic ServletWebServerFactory servletContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    tomcat.addConnectorCustomizers(gracefulShutdownTomcat);    return tomcat;
}

2. kill 命令

命令格式:kill[参数][进程号]

命令功能:

发送指定的信号到相应进程。不指定型号将发送SIGTERM(15)终止指定进程。如果任无法终止该程序可用“-KILL” 参数,其发送的信号为SIGKILL(9) ,将强制结束进程,使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。

kill 命令的信号:共有64个信号值,其中常用的是 2(SIGINT:中断,ctrl+c)、15(SIGTERM:终止,默认值)和 9(SIGKILL:强制终止)。

3. Docker 进程管理

Docker鼓励“一个容器一个进程(one process per container)”的方式,这种方式非常适合以单进程为主的微服务架构的应用。在Docker中,进程管理的基础就是Linux内核中的PID namespace技术。每个Container都是Docker Daemon的子进程,每个Container进程缺省都具有不同的PID namespace。

在ENTRYPOINT和CMD指令中,提供两种不同的进程执行方式 shell 和 exec,shell的方式启动PID1进程不是你的应用进程,子进程是你的应用进程,要想应用进程是PID1,需要使用exec方式。

当执行docker stop命令时,docker会首先向容器的PID1进程发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有结束, 那么Docker Daemon会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死变为退出状态。这种方式给Docker应用提供了一个优雅的退出(graceful stop)机制,允许应用在收到stop命令时清理和释放使用中的资源。而docker kill可以向容器内PID1进程发送任何信号,缺省是发送SIGKILL信号来强制退出应用。强制停止的等待时间可以通过docker stop命令的-t参数设置。

  • 容器的PID1进程需要能够正确的处理SIGTERM信号来支持优雅退出。

  • 如果容器中包含多个进程,需要PID1进程能够正确的传播SIGTERM信号来结束所有的子进程之后再退出。

  • 确保PID1进程是期望的进程。缺省sh/bash进程没有提供SIGTERM的处理,需要通过shell脚本来设置正确的PID1进程,或捕获SIGTERM信号。

参考文章

4. DCOS 基本操作

在DCOS平台上,针对某一个容器的操作:restart、scale、stop等,还可以通过marathon docker管理工具后台重新部署容器。

5. 模拟待测试的业务功能

@ApiOperation(value = "模拟长时间处理业务")@GetMapping(value = "/sleep/one", produces = "application/json")public ResultEntity<Long> sleepOne(String systemNo){
    logger.info("模拟长时间业务处理,请求参数:{}", systemNo);
    Long serverTime = System.currentTimeMillis();    while (System.currentTimeMillis() < serverTime + sleepTime) {
        logger.info("正在处理业务,处理时间设置:{},当前时间:{},开始时间:{}", sleepTime, System.currentTimeMillis(), serverTime);
    }
    ResultEntity<Long> resultEntity = new ResultEntity<>(serverTime);
    logger.info("模拟长时间业务处理,响应参数:{}", resultEntity);    return resultEntity;
}@ApiOperation(value = "设置业务处理时间")@GetMapping(value = "/biz/time/set", produces = "application/json")public ResultEntity<Long> bizTime(Long sleepTime){
    logger.info("设置业务处理时间,请求参数:{}", sleepTime);    this.sleepTime = sleepTime;
    ResultEntity<Long> resultEntity = new ResultEntity<>(sleepTime);
    logger.info("设置业务处理时间,响应参数:{}", resultEntity);    return resultEntity;
}

02 优雅停机测试结果

产生下述测试结果的测试方发是:业务处理时间设置40s,使用jmeter工具发起连续性测试(模拟10个用户,进行10轮测试),然后从测试环境、应用是否实现优雅停机、停止方法、jmeter客户端失败原因几个维度进行对比。测试环境选择了本地和DCOS容器云平台对比,应用是是否添加优雅停机的配置。

环境是否实现优雅停止方法客户端失败原因
本地idea stop(kill -2/-15)connecttion reset
本地idea stop(kill -2/-15)failed to respond
DCOSstop servicefailed to respond,connecttion refused
DCOSstop servicefailed to respond,connecttion reset,connecttion refused
DCOS重新发布failed to respond,connecttion refused
DCOS重新发布failed to respond,connecttion reset,connecttion refused
DCOSdocker kill -s 15connecttion reset

测试结果数据解释:

先说明一下,客户端报出的这几种错误的含义:

  • failed to respond:客户端和服务端建立了socket连接,并发送了数据,但是没有收到响应,客户端会报该错误。

  • connecttion reset :客户端和服务端建立了socket连接,在发送数据之前,服务端关闭了连接,客户端再发送数据就会报该错误。

  • connecttion refused:客户端连接服务端的时候,服务端ip或端口不存在,客户端会报该错误。

所以,要实现了优雅停机之后,客户端报错不能有failed to respond。从测试结果来看,只有本地环境测试实现了优雅停机,以及DCOS环境下使用docker kill命令停止实现了优雅停机。

为什么在DCOS平台上正常操作容器停止不能实现优雅停机?分析原因,DCOS上容器停止操作发送的是 docker stop 命令,根据上面 docker stop 命令的实现原理(docker kill -s 15 之后,等待一段时间(默认10s)之后,如果还不能停止,会在发送docker kill -s 9强制停止),容器应用是被kill -9强制停止了,应用实现的优雅停机是不能hook信号9,而应用的业务处理时间是40s,所以客户端不能收到响应。

于是,开始寻找解决办法,后来发现DCOS中有个配置来控制这个时间,在marathon.json中优雅的时间区间设置方式:"taskKillGracePeriodSeconds": 50。设置这个参数之后,在DCOS上再次测试,就能正常实现优雅停机了。

03 微服务部署

这时,回头看看我们的目标:整个部署过程中保证业务的连续性,让服务的客户端无感知

1. 要做到应用容器停止不影响正在执行的业务

需要将 marathon 中的配置 "taskKillGracePeriodSeconds" 配合业务处理时间做调整,建议这个参数最大设置为30s,因为设置时间过大的化,而且你的业务处理时间又很长的话,会导致应用容器停止需要很长时间。一般的应用不会有这样的问题,一般的处理时间都在10s以内。

需要重点关注批处理应用可能处理时间比较长,如果业务处理时间确实特别长的话,需要在接收到停止指令之后,在30s内做一些善后的处理,比如记录一下任务执行到的位置,下次启动的时候重新从此开始。

2. 负载均衡组件能自动感知服务节点下线和上线

比如,如果请求发送到了一个已经停止了的服务节点,客户端会收到 connecttion reset 或者 connecttion refused,这时该负载均衡组件能自动尝试别的在线节点,有了这种容错机制就能保证请求的成功率了。或者负载均衡组件实时自动更新了在线的服务节点列表,直接不会将请求发往已经下线的服务节点了。

有了以上两点的保证就能完美实现我们微服务部署的目标了。



作者:rabbitGYK
链接:https://www.jianshu.com/p/073a0da36d48


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
51
获赞与收藏
178

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消