作者:鲍凤其
背景
在即将发布的 dble 2.19.09.0 版本中,我们将升级 dble 中 JSW 的版本,将 JSW 版本从 3.2.3 版本升级到 3.5.41 版本。升级的原因在于我们在使用过程中发现了几个比较严重的 bug,这几个 bug 会导致 dble 的守护进程异常退出和 hang 死。hang 死的案例可参考 issue:https://github.com/actiontech/dble/issues/1402。看到这里的同学,有些同学可能对 JSW 可能还不太了解,我这里先简单介绍一下。
JSW 介绍
JSW 是 Java Service Wrapper 的缩写,也就是 dble 中通常所说的 wrapper。JSW 可用于将 Java 程序包装成一个后台服务运行。除此以外,JSW 还可以在你的 Java 程序宕掉以后,自动把服务拉起。相当于提供了一个守护进程的功能。它的一个主要目标就是,单点服务做到尽可能高可靠,宕掉后第一时间帮你把它拉起来!这样,能够最大化降低运维成本。
JSW 除了 Window 和 Linux 还支持其他平台,社区中经常听到有同学问 dble 有 Windows 版本此类的问题。在这里我想说可以有,但是 dble 官方只提供了 Linux 版,Windows 版本就需要各位同学自己动手编译打包了。Java Service Wrapper 分为社区版和企业版,企业版的功能更加强大,但是要收费。目前一般使用的都是社区版,免费并且开源。
JSW 如何守护 dble
概述
要了解上面的 bug,我们需要先了解一下 JSW 的整体流程。下面我们先从整体来看一下 dble 和 JSW 守护进程的关系,如下图:
dble 在启动后,如果通过系统命令查看后台进程,会发现其实后台运行了两个进程。
- 其中之一是 JSW 守护进程,下面直接叫做守护进程。它会守护我们的 dble 程序,挂掉后立马拉起。守护进程会开启一个 ServerSocket 端口,通过这个端口守护进程可以对 wrapper 下发指令,比如 ping,重启 dble 等等。
- 另一个就是 Java 程序的进程了。JSW 会在 dble 程序之外包装一层 wrapper,这个 wrapper 的主要作用有两个:一是监听端口,处理守护进程发送来的指令;二是在合适的时机加载 dble。
- 这两个进程是父子进程的关系,守护线程是父进程,Java 程序是其子进程。在上面的图中,其实漏掉了其中最重要的部分,那就是守护进程对 JVM 状态的处理和变更,这个会在下面详细介绍。
守护进程启动过程
守护进程的启动大致我大致分为两个阶段:1. 初始化阶段,2. 状态和事件处理阶段。
初始化阶段
- 守护进程内部会初始化 Java 程序状态的变量,比如记录当前 Java 程序是停止,正在启动还是运行等。初始时状态即为停止状态即下文中的 down。
- 注册信号事件处理函数,其中特别需要注意的是注册了 SIGCHLD 这个信号的处理函数。这个信号是当子进程退出时,它会向父进程发送 SIGCHLD 信号。
- 开启一个 ServerSocket 监听端口用于 wrapper 和守护进程通信。
状态和事件处理阶段
在此阶段,守护进程不停的轮询监听端口是否有事件达到,根据 Java 程序的状态执行不同的操作,以此反复。下面详细描述下过程,可对照下面的图来看。
- 在守护进程启动之初,内部 Java 程序的状态为 down,若是初次启动,此时守护进程会直接将状态置为 lauch。
- 在 lauch 状态下,守护进程调用 Linux fork 系统调用创建一个 Java 的子进程后将状态置为 lauching。
- 在 lauching 状态下,守护进程会一直等待 Java 程序启动成功的事件达到,若在超时时间内没有等到,则会将子进程杀死并重启。一旦等到启动成功事件,状态就会被守护进程置为 lauched。
- 在 lauched 状态下,守护进程对 wrapper 下发 start 命令,让 wrapper 加载 dble 的启动类并运行。命令被下发之后,状态被置为 starting,守护进程等待 dble 启动完成。与启动 Java 程序一样,这里也有个超时时间。
- 一旦dble加载成功,状态即变为 started,此时 dble 在正常的运行状态。
- 在运行状态期间,守护线程会定期向 wrapper 发送 ping 包,若 wrapper 按时返回 ping 的 response 包,则守护进程则认为它正常。
异常处理
在这里我们假设 JVM 由于某种原因 hang 了一段时间。我们来看下守护进程是如何处理的。一旦 JVM hang 住了,则守护进程的 ping 命令不能及时返回,此时守护进程将状态置为 killing 并准备杀死 Java 程序。守护进程调用系统调用 kill 向子进程发送 SIGKILL 信号,发送信号之后,守护进程会等待 0.5s 以确保这期间子进程被回收和状态的正确性。
问题分析
到这里我们已经介绍完 JWS 的启动流程和当 Java 程序异常时的处理。现在我们来看下之前我们遇到的问题:
- issue:https://github.com/actiontech/dble/issues/1402
在最后的异常处理那节,从 killing 到 down 状态的转换过程中,主线程内会打印 log 来提示用户。打印日志前需要获取日志锁,但是此时主线程接收到 SIGCHLD 信号并回调了该信号的处理函数,在处理函数中也需要打印日志,此时也去获取日志锁,但是此锁是不可重入锁,因此发生死锁,导致守护进程主线程 hang 死。在 JWS 新的版本中,打印日志的操作会放在单独的线程中处理来解决这个问题。
- 守护进程异常退出
这个问题是一系列问题的总成,但是根本原因是相同的,还是在于 SIGCHLD 信号。在最后一个异常处理那个图中 SIGCHLD 信号的返回我画了两个。正常情况下是在守护进程等待 Java 子进程被回收的过程中收到该信号。但是如果 Java 子进程资源回收超过 0.5s 时,守护进程已经往下执行准备重启 Java 程序了,此时再回调到 SIGCHLD 信号处理函数,会导致状态异常而退出。新版本在这方面做出了一些改善。
总结
上面我对 dble 中使用的 JWS 从启动流程和错误处理两个方面做了简单的介绍,在最后对平常工作中遇到的 bug 进行了探究,希望对你们有帮助。