Bootstrap

一个糟糕的决策带来无尽的折磨

一个糟糕的决策带来无尽的折磨

在这里插入图片描述

你也时常有这种感觉么?怎么每次迭代都让人感觉很费劲,很疲惫,似乎每次都要对之前的代码进行修改才能满足本次迭代的需求。
整个项目像是一团乱麻一样理不清楚,项目工程给人一次性纸杯的感觉,似乎只能用一次。团队成员只有靠夜以继日的加班,
才能完成本次迭代,同时它的质量并不高,其实就是以量取胜的策略。如果你是一个团队的管理者你需要好好反思一下这究竟
是为什么呢。

一个糟糕的决策缺乏思考,在未来不仅仅会折磨自己也会折磨别人。然后整个团队默默忍受着这种折磨,直到摆烂。

下面分享一个案例,希望大家看了能开心,毕竟当事人真的很痛苦。

在一个项目中包含业务接口,业务类,和Job类,Job类通常是执行一些批处理作业,令人惊讶的是这些类竟然运行在同一个 JVM 中,
这可真是一场灾难。后面会说明这么做的坑在哪里。

现在该如何优雅停机?

现在这些类都运行在同一个 JVM 中该怎么优雅停机呢? 当我被问到这个问题的时候,老实说我也不知道该怎么做。

什么叫优雅停机?

简单的说,就是向应用进程发出停止指令之后,能保证正在执行的业务操作不受影响,直到操作运行完毕之后再停止服务。应用程序接收到停止指令之后,会进行如下操作:

  1. 停止接收新的访问请求
  2. 正在处理的请求,等待请求处理完毕;对于内部正在执行的其他任务,比如定时任务、mq 消费等等,也要等当前正在执行的任务执行完毕,并且不再启动新的任务
  3. 当应用准备关闭的时候,按需向外发出信号,告知其他应用服务准备接手,以保证服务高可用。
  4. 触发 shutdown hook 释放或关闭一些资源。

如果暴力的关闭应用程序,比如通过 kill -9 <pid> 命令强制直接关闭应用程序进程,可能会导致正在执行的任务数据丢失或者错乱,也可能会导致任务所持有的全局资源等不到释放,比如当前任务持有 redis 的锁,并且没有设置过期时间,当任务突然被终止并且没有主动释放锁,会导致其他进程因无法获取锁而不能处理业务。

暴力停机可能导致数据的不一致,例如调用了某个事务性的第三方接口,接口调用已经成功在第三方系统中事务已经成功,但此时暴力停机导致本地事务失败。
这时候就造成了本地数据和第三方数据的不一致。

由于有批处理 Job 可能正在执行,而且这种 Job 执行耗时时间都比较长,可能是几个小时,这时候如果我们使用 kill -15 <pid> 则会等待的时间很长而且是不确定的。
这就导致无法快速的完成发版,为了实现能够快速的发版也只好硬着头皮的 kill -9 <pid> 杀掉进程。

kill -9 与 kill -15

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

使用 kill -l 可以列举所有支持的信号:

当我们使用kill pid时,实际相当于kill -15 pid。也就是说默认信号为15。使用kill -15时,系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后,具体要如何处理自己可以决定。
这时候,应用程序可以选择:

  1. 立即停止程序
  2. 释放响应资源后停止程序
  3. 忽略该信号,继续执行程序

因为kill -15信号只是通知对应的进程要进行"安全、干净的退出",程序接到信号之后,退出前一般会进行一些"准备工作",如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。
但是,如果在"准备工作"进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。
这也就是为什么我们有的时候使用kill命令是没办法"杀死"应用的原因,因为默认的kill信号是SIGTERM(15),而SIGTERM(15)的信号是可以被阻塞和忽略的。
kill -15相比,kill -9 就相对强硬得多,系统会发出SIGKILL信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
所以,kill -9在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。

Java应用对SIGTERM信号的处理

Java应用在Linux中是以一个独立进程的形式运行的,Java程序的终止运行基于JVM的关闭实现,JVM关闭方式分为3种:

  1. 正常关闭:当最后一个非守护线程结束或者调用了System.exit或者通过其他特定平台的方法关闭(接收到SIGINT(2)、SIGTERM(15)信号等)
  2. 强制关闭:通过调用Runtime.halt方法或者是在操作系统中强制kill(接收到SIGKILL(9)信号)
  3. 异常关闭:运行中遇到RuntimeException异常等。

JVM 进程在接收到kill -15信号通知的时候,会做一些清理动作的,例如删除临时文件。同时,也提供了hook机制,来让开发者自定义清理动作,对应的方法为:Java.Runtime.addShutdownHook(Thread hook)。我们可以自定义一个shuthook并测试效果:

public class MyShutdownHook {
    public static void main(String[] args) throws InterruptedException{
        boolean flag = true;
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("my hook execute end");
        }));
        while (flag) {
            System.out.println("my app is running");
            TimeUnit.SECONDS.sleep(3);
        }
        System.out.println("main thread execute end");
    }
}

当使用 kill -9 的时候该 shutdown hook 是不会执行的。

接口响应怎么这么慢没有反应?

接口响应非常的慢,给人的感觉好像服务僵死了一样。这时候你可能会去首先排查网络层面看网络是否畅通,带宽是否被占满,再查查服务器文件句柄数量是否足够,再查查服务器内存是否足够,再查查服务器磁盘是否足够,再查查服务器cpu是否足够,再查查服务器网络是否足够,再查查服务器磁盘是否足够,再查查服务器内存是否足够,再查查服务器cpu是否足够,再查查服务器网络是否足够,再查查服务器磁盘是否足够,再查查服务器
看看服务内存是否被跑满,CPU 使用率是否非常的高等等。当然这些都是有可能发生的,是导致问题现象产生的原因之一。

来看看下图的例子,http 线程都处于 WAITING 状态,而Job线程池中的线程都处于 RUNNABLE 状态。
也就是 Job 和接口请求在争夺资源。谁让它们都运行在同一个 JVM 中呢。

已经不想多说什么了,心有点累…

解决问题

首先我建议先规范工程结构,比如我们有一个订单服务 order-service ,其中包含这几个模块 order-commonorder-facade
order-coreorder-weborder-job ,其中 order-weborder-job 可以作为启动模块,进行单独的部署。
同时它们都可以依赖 order-commonorder-facadeorder-core 模块,很大程度上的解决了代码复用问题。

;