如何正确关闭游戏服务器
2020-12-24 20:51:04 【

最简单粗爆的方法

在Linux系统上,使用ps -aux|grep java 可以查到所有运行的java程序的pid,即进程号,然后使用kill - 9 进程号,杀死一个进程。

这样做虽然简单快速,但是会有一个问题,如果我们运行的服务器有缓存的数据,还没有来得及进行持久化存储,那么这样操作,内存中的数据就会丢失。kill - 9是一个必杀命令,不管进程处于什么状态,都是杀无赦,它不会给进程留下任何善后的机会。那么该如何正确的关闭游戏服务器吧?

优雅的关闭进程

优雅的关闭进程,就是在收到关闭进程的命令后,进程进行一些数据处理,比如:

  • 不再接收连接

  • 不再接收数据

  • 把未持久化的数据进行持久化

  • 清理一些临时文件等

  • 执行一些已经提交到线程池中但未执行的任务

Java进程如何接收进程停止命令

JVM 关闭钩子
关闭钩子本质上是一个线程(也称为Hook线程),用来监听JVM的关闭。通过使用Runtime的addShutdownHook(Thread hook)可以向JVM注册一个关闭钩子。Hook线程在JVM 正常关闭才会执行,在强制关闭时不会执行。

这个钩子可以在一下几种场景中被调用:

  • 程序正常退出

  • 使用System.exit()

  • 终端使用Ctrl+C触发的中断

  • 系统关闭

  • OutOfMemory宕机

  • 使用Kill pid命令干掉进程(注:在使用kill -9 pid时,是不会被调用的)

对于一个JVM中注册的多个关闭钩子它们将会并发执行,所以JVM并不能保证它的执行顺行。当所有的Hook线程执行完毕后,如果此时runFinalizersOnExit为true,那么JVM将先运行终结器,然后停止。Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程中必须要尽可能的减少Hook线程的执行时间。另外由于多个钩子是并发执行的,那么很可能因为代码不当导致出现竞态条件或死锁等问题,为了避免该问题,强烈建议在一个钩子中执行一系列操作。

另外在使用关闭钩子还要注意以下几点:

  • 不能在钩子调用System.exit(),否则卡住JVM的关闭过程,但是可以调用Runtime.halt()。

  • 不能再钩子中再进行钩子的添加和删掉操作,否则将会抛出IllegalStateException。

  • 在System.exit()之后添加的钩子无效。

  • 当JVM收到SIGTERM命令(比如操作系统在关闭时)后,如果钩子线程在一定时间没有完成,那么Hook线程可能在执行过程中被终止。

  • Hool线程中同样会抛出异常,如果抛出异常又不处理,那么钩子的执行序列就会被停止。

下面是一个简单的示例:

publicclassT {    @SuppressWarnings("deprecation")    publicstaticvoidmain(String[] args) throws Exception {        //启用退出JVM时执行Finalizer        Runtime.runFinalizersOnExit(true);        MyHook hook1 = new MyHook("Hook1");        MyHook hook2 = new MyHook("Hook2");        MyHook hook3 = new MyHook("Hook3");        //注册关闭钩子        Runtime.getRuntime().addShutdownHook(hook1);        Runtime.getRuntime().addShutdownHook(hook2);        Runtime.getRuntime().addShutdownHook(hook3);        //移除关闭钩子        Runtime.getRuntime().removeShutdownHook(hook3);        //Main线程将在执行这句之后退出        System.out.println("Main Thread Ends.");    }}classMyHookextendsThread {    private String name;    publicMyHook (String name) {        this.name = name;        setName(name);    }    publicvoidrun() {        System.out.println(name + " Ends.");    }    //重写Finalizer,将在关闭钩子后调用protectedvoidfinalize() throws Throwable {        System.out.println(name + " Finalize.");    }}

和(可能的)执行结果(因为JVM不保证关闭钩子的调用顺序,因此结果中的第二、三行可能出现相反的顺序):

MainThread Ends.Hook2Ends.Hook1Ends.Hook3Finalize.Hook2Finalize.Hook1Finalize.

可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:JVM确实不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。

另外,由于MyHook类实现了finalize方法,而main函数中第一行又通过Runtime.runFinalizersOnExit(true)打开了退出JVM时执行Finalizer的开关,于是3个hook对象的finalize方法被调用,输出了3行Finalize。

注意,多次调用addShutdownHook来注册同一个关闭钩子将会抛出IllegalArgumentException:

Exceptioninthread "main" java.lang.IllegalArgumentException: Hookpreviouslyregisteredatjava.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:72)atjava.lang.Runtime.addShutdownHook(Runtime.java:211)atT.main(T.java:12)

另外,从JavaDoc中得知:

Once the shutdown sequence has begun it can be stopped only by invoking the halt method, which forcibly terminates the virtual machine.Once the shutdown sequence has begun it is impossible to register a new shutdown hook or de-register a previously-registered hook. Attempting either of these operations will cause an IllegalStateException to be thrown.“一旦JVM关闭流程开始,就只能通过调用halt方法来停止该流程,也不可能再注册或移除关闭钩子了,这些操作将导致抛出IllegalStateException”。

如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:

        Runtime.getRuntime().addShutdownHook(newThread() {            publicvoidrun() {                try {                    LogService.this.stop();                } catch (InterruptedException ignored){                    //ignored                }            }        });

由于关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题。为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务。实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。

在游戏服务器中添加关闭钩子

publicclassShutDownService {    privatestatic CommonLog gameLogger = CommonLog.getInstance();    privatestaticList<IShutDown> shutDownList = new ArrayList<>();    //注册需要在关闭钩子中执行的任务publicstaticvoidregistShutDown(IShutDown shutDownServer) {        shutDownList.add(shutDownServer);    }    publicstaticvoidstartShutDownHook() {        Runtime.getRuntime().addShutdownHook(new Thread() {            @Override            publicvoidrun() {                gameLogger.warn(0, "开始关闭服务器,正在清理资源.....");                for (IShutDown shutDown : shutDownList) {                    if (shutDown != null) {                        shutDown.shutDown();                        while (!shutDown.isTerminated()) {                        }                        gameLogger.warn(0, "----关闭" + shutDown.getClass().getName() + "成功----");                    }                }                gameLogger.warn(0, "###---服务器关闭成功---###");            }        });    }}

Linux脚本根据端口杀死一个进程

#!/bin/bashecho"重新启动服务" jar_name=GameLogicServer.jar PROCESS=`ps -ef|grep ${jar_name} |grep -v grep|grep -v PPID|awk '{ print $2}'` for i in$PROCESSdoecho######Kill the ${jar_name} process [ $i ] ########"kill -15 $idonewhiletruedo OLD_PROCESS=`ps -ef|grep ${jar_name} |grep -v grep|grep -v PPID|awk '{ print $2}'` if [ "${OLD_PROCESS}" = "" ] thenecho"${PROCESS}进程已杀死成功,开始启动新的进程"breakelseecho"正在等待${PROCESS}进程关闭....." sleep 1s fidonenohup java -server -agentpath:/usr/jprofiler9/jprofiler9/bin/linux-x64/libjprofilerti.so=port=8849,nowait -jar ${jar_name} > console.out 2>&1 & echo"服务器启动完成"


】【打印关闭】 【返回顶部
上一篇传奇游戏服务器租用应该怎么选择.. 下一篇如何选择开服游戏的服务器?