作者:鲍凤其

背景

在即将发布的 dble 2.19.09.0 版本中,我们将升级 dble 中 JSW 的版本,将 JSW 版本从 3.2.3 版本升级到 3.5.41 版本。升级的原因在于我们在使用过程中发现了几个比较严重的 bug,这几个 bug 会导致 dble 的守护进程异常退出和 hang 死。hang 死的案例可参考 issue:https://github.com/actiontech...。看到这里的同学,有些同学可能对 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 守护进程的关系,如下图:

global.png

dble 在启动后,如果通过系统命令查看后台进程,会发现其实后台运行了两个进程。

  • 其中之一是 JSW 守护进程,下面直接叫做守护进程。它会守护我们的 dble 程序,挂掉后立马拉起。守护进程会开启一个 ServerSocket 端口,通过这个端口守护进程可以对 wrapper 下发指令,比如 ping,重启 dble 等等。
  • 另一个就是 Java 程序的进程了。JSW 会在 dble 程序之外包装一层 wrapper,这个 wrapper 的主要作用有两个:一是监听端口,处理守护进程发送来的指令;二是在合适的时机加载 dble。
  • 这两个进程是父子进程的关系,守护线程是父进程,Java 程序是其子进程。在上面的图中,其实漏掉了其中最重要的部分,那就是守护进程对 JVM 状态的处理和变更,这个会在下面详细介绍。

守护进程启动过程

守护进程的启动大致我大致分为两个阶段:1. 初始化阶段,2. 状态和事件处理阶段。

初始化阶段

  1. 守护进程内部会初始化 Java 程序状态的变量,比如记录当前 Java 程序是停止,正在启动还是运行等。初始时状态即为停止状态即下文中的 down。
  2. 注册信号事件处理函数,其中特别需要注意的是注册了 SIGCHLD 这个信号的处理函数。这个信号是当子进程退出时,它会向父进程发送 SIGCHLD 信号。
  3. 开启一个 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 在正常的运行状态。

status.png

  • 在运行状态期间,守护线程会定期向 wrapper 发送 ping 包,若 wrapper 按时返回 ping 的 response 包,则守护进程则认为它正常。

异常处理

在这里我们假设 JVM 由于某种原因 hang 了一段时间。我们来看下守护进程是如何处理的。一旦 JVM hang 住了,则守护进程的 ping 命令不能及时返回,此时守护进程将状态置为 killing 并准备杀死 Java 程序。守护进程调用系统调用 kill 向子进程发送 SIGKILL 信号,发送信号之后,守护进程会等待 0.5s 以确保这期间子进程被回收和状态的正确性。

kill.png

问题分析

到这里我们已经介绍完 JWS 的启动流程和当 Java 程序异常时的处理。现在我们来看下之前我们遇到的问题:

  1. issue:https://github.com/actiontech...

在最后的异常处理那节,从 killing 到 down 状态的转换过程中,主线程内会打印 log 来提示用户。打印日志前需要获取日志锁,但是此时主线程接收到 SIGCHLD 信号并回调了该信号的处理函数,在处理函数中也需要打印日志,此时也去获取日志锁,但是此锁是不可重入锁,因此发生死锁,导致守护进程主线程 hang 死。在 JWS 新的版本中,打印日志的操作会放在单独的线程中处理来解决这个问题。

  1. 守护进程异常退出

这个问题是一系列问题的总成,但是根本原因是相同的,还是在于 SIGCHLD 信号。在最后一个异常处理那个图中 SIGCHLD 信号的返回我画了两个。正常情况下是在守护进程等待 Java 子进程被回收的过程中收到该信号。但是如果 Java 子进程资源回收超过 0.5s 时,守护进程已经往下执行准备重启 Java 程序了,此时再回调到 SIGCHLD 信号处理函数,会导致状态异常而退出。新版本在这方面做出了一些改善。

总结

上面我对 dble 中使用的 JWS 从启动流程和错误处理两个方面做了简单的介绍,在最后对平常工作中遇到的 bug 进行了探究,希望对你们有帮助。


爱可生开源社区
426 声望207 粉丝

成立于 2017 年,以开源高质量的运维工具、日常分享技术干货内容、持续的全国性的社区活动为社区己任;目前开源的产品有:SQL审核工具 SQLE,分布式中间件 DBLE、数据传输组件DTLE。