微服务部署与优雅停机

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
 */

@Component
public 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配置:

@Autowired
private GracefulShutdownTomcat gracefulShutdownTomcat;
@Bean
public 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
DCOS stop service failed to respond,connecttion refused
DCOS stop service failed to respond,connecttion reset,connecttion refused
DCOS 重新发布 failed to respond,connecttion refused
DCOS 重新发布 failed to respond,connecttion reset,connecttion refused
DCOS docker kill -s 15 connecttion 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,这时该负载均衡组件能自动尝试别的在线节点,有了这种容错机制就能保证请求的成功率了。或者负载均衡组件实时自动更新了在线的服务节点列表,直接不会将请求发往已经下线的服务节点了。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容