优雅地关闭容器中的Java:为什么要仔细检查!

优美不仅是令人钦佩的人类素质:它还是任何应用程序的必备条件,尤其是当它承担着关键任务领域的负担时。

UltraESB在保持运行正常(包括关闭)的过程中一直保持良好的历史。 新的UltraESB-X遵循了这一传统,并在其17.07版本中实现了正常关机

当我们为集成平台(IPS)构建 ips-worker Docker映像作为UltraESB-X的定制版本时,我们可以保证在平台上运行的ESB可以正常关闭–或我们认为如此。

不幸的是没有。

一旦我们重新部署更改集群的复制计数 ,该集群下运行的所有ESB实例都将终止(并生成新实例来代替它们)。 终止应该是优雅的; ESB将首先停止接受任何新的传入消息,并推迟内部关闭序列几秒钟,直到运行中的消息处理完成或超时结束释放为止。

基于Kubernetes的主流IPS版本中,我们通过K8s API以及数据库附加程序检索ESB实例(荚)的日志,以便以后进行分析。 在分析日志时,我们注意到,无论日志存储的大小如何,我们都从未见过任何ESB关闭日志。 好像ESB在收到终止信号后就被残酷杀害。

为了研究这个问题,我从一个简化的Java程序开始:注册了一个关闭钩子 ,这是举世闻名的用Java实现优雅关闭的方式,我们在这两个ESB中都使用了该钩子 ,并且该方法永远运行,并打印一些文本定期(指示main线程处于活动状态)。 一旦触发了关闭挂钩,我便中断了main线程,更改了输出以指示我们正在关闭,然后让处理程序在几秒钟后完成(与“模拟”正常关闭相一致)。

class Kill {

	private static Thread main;

	public static void main(String[] a) throws Exception {

		Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
			public void run() {
				System.out.println("TERM");
				main.interrupt();
				for (int i = 0; i < 4; i++) {
					System.out.println("busy");
					try {
						Thread.sleep(1000);
					} catch (Exception e) {}
				}
				System.out.println("exit");
			}
		}));

		main = Thread.currentThread();
		while (true) {
			Thread.sleep(1000);
			System.out.println("run");
		}
	}
}

测试它非常简单:

javac Kill.java
java Kill

当程序继续打印时:

run
run
run
...

按Ctrl + C查看会发生什么:

...
run
run
^CTERM
busy
Exception in thread "main" java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at Kill.main(Kill.java:22)
busy
busy
busy
exit

看起来不错。

完成后,将其转换为成熟的Docker容器仅花费了几分钟,并生成了以下Dockerfile

FROM openjdk:8-jre-alpine
ADD Kill*.class /
ENTRYPOINT ["java", "Kill"]

docker build -t kill:v1 .

接下来,我用新图像运行了一个容器:

docker run -it --rm kill:v1

给出了预期的输出:

run
run
run
...

然后,我使用kill命令向过程发送一个TERM信号(以普通的行话映射到Ctrl + C,并且是Java关闭钩子的默认触发器):

# pardon the fancy functions;
# they are quite useful for me when dealing with processes

function pid() {
    ps -ef | grep $1 | grep -v grep | awk '{print $2}'
}

function killsig() {
    for i in pid $2; do
        sudo kill $1 $i
    done
}

alias termit='killsig -15'

# with all the above in place, I just have to run:
termit Kill

正如预期的那样,关闭挂钩被调用并顺利执行。

再往前走,我把整个东西变成了一个独立的K8s pod (由一个单副本Deployment支持 ):

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kill
spec:
  selector:
    matchLabels:
      k8s-app: kill
  template:
    metadata:
      labels:
        k8s-app: kill
    spec:
      containers:
      - name: kill
        image: kill:v1

并尝试了同样的事情,这个时候通过清空出spec.replicas通过(与我们做它在IPS) kubectl edit deployment命令,而不是手动kill -TERM

kubectl edit deployment kill

# vi is my default editor
# set "replicas" to 0 (line 20 in my case)
# <ESC>:wq<ENTER>

同时将Pod的控制台尾放在一个单独的窗口中:

# fancy stuff again

function findapp() {
    kubectl get pod -l k8s-app=$1 -oname | cut -b 6-;
}

function klog() {
    kubectl logs -f findapp $1;
}

# the final command
klog kill

显示输出:

run
run
...
run
TERM
busy
Exception in thread "main" java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at Kill.main(Kill.java:22)
busy
busy
busy
exit

该死,它仍然可以正常关闭!

那么我的ips-worker什么问题呢?

为了验证,我在IPS上运行了一个单副本集群,手动更改了映像( spec.template.spec.containers[0].image )和启动命令( spec.template.spec.containers[0].command )通过kubectl edit对K8进行了部署(保持所有其他因素(例如环境变量和体积安装)不变),并尝试了相同的归零序列;

结果一样! 正常关机!

然后我想到,虽然我的kill容器只使用了一个java Kill命令,但是ips-worker使用了一些更复杂的命令:

/bin/sh -c <copy some files> && <run some custom command> && <run ultraesb-x.sh>

在最后一部分中,我们构造(使用特制的类路径和一些JVM参数)并执行一个很长的java命令,以启动UltraESB-X野兽。

因此,最终,容器中的最终实时命令归结为:

/bin/sh -c <basepath>/ultraesb-x.sh

因此,我通过稍微更改Dockerfile在kill容器上尝试了shell命令:

通过稍微更改Dockerfile:

FROM openjdk:8-jre-alpine
ADD Kill*.class /
# note the missing brackets and quotes, so that the command gets the default /bin/sh -c prefix
ENTRYPOINT java Kill

和耶! 不再需要正常关机。 Java进程在Docker(docker docker stop )和K8s(副本零输出)中被残酷地杀死。

进一步调查,我在Google的指导下被引导至该受欢迎的SE帖子该帖子基本上说shell( sh )默认情况下不会将接收到的信号传递给其子进程。 建议的替代方法是将内部命令作为exec运行,基本上将父进程( sh )替换为子进程( java ,如果为kill ):

FROM openjdk:8-jre-alpine
ADD Kill*.class /
ENTRYPOINT exec java Kill

为了kill ,那立刻就成功了。

对于ips-worker情况有所不同,因为有两个级别的调用:容器的命令通过/bin/sh -c调用命令链,而内置的ultraesb-x.sh调用最终的java命令。 因此,我不得不在两个地方包括exec

在命令链末尾:

/bin/sh -c \
<copy some files> && \
<run some custom command> && \
exec <basepath>/ultraesb-x.sh

再一次在ultraesb-x.sh

# do some magic to compose the classpath and other info for ESB startup

exec $JAVA_HOME/bin/java <classpath and other params>

看起来很简单,这两个exec足以使ips-worker正常关闭,进而恢复到我们的Integration Platform

翻译自: https://www.javacodegeeks.com/2017/09/gracefully-shutting-java-containers-double-check.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值