Skip to content
This repository has been archived by the owner on Jun 18, 2024. It is now read-only.

Bottle-M/BottleM-Backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BottleM-Backend

咱Minecraft服务器的主控端

目录

面向

这个项目目前主要针对的是像朋友服一类的小型、即开即玩单个Minecraft服务器。

Minecraft服务器部署过程由Bash脚本驱动。

使用到的包

包名 开源协议
tencentcloud-sdk-nodejs Apache License 2.0
ssh2 MIT
chalk MIT
ws MIT
minecraft-protocol BSD-3-Clause license
rcon MIT

简介

这算是我第三次写这类应用了。梦开始的地方在这里:CloudMinecraft,而第二次我写的东西(叫LoCo来着,基于PHP)因为过于屎山没法开源(但尽管如此,LoCo竟然在我服务器强撑运行了一年,可以说是奇迹了)

好在这回,我勉强把这玩意写的能看下去了。(っ ̯ -。)

本项目包括两个部分:BottleM-BackendBottleM-InsSide,这个仓库存放的是Backend的源码。

Backend咱就称为“主控端”,而InsSide咱就称为“实例端”吧!

主控端主要负责接受用户请求,并管理实例的开通与回收;而实例端则负责Minecraft服务器的部署与管理。

绝大多数时候,用户只与主控端进行交互实例端:我透明啦๐·°(৹˃̵﹏˂̵৹)°·๐

主控端和实例端之间通过WebSocket协议进行通信。不过就算WebSocket连接断开了,实例端也能保证Minecraft服务器的数据安全

这玩意是怎么工作的?看看流程简述吧~

基本部署

注:Minecraft服务器进程监听的端口目前只能是25565

  1. 将项目克隆到本地,并进入目录

    git clone https://github.com/Bottle-M/BottleM-Backend.git
    cd BottleM-Backend
  2. 安装依赖包

    npm install
  3. 自制镜像,见下方

  4. 修改/重写./scripts/下的部署脚本(最重要的一步),并将Minecraft服务端文件上传到云储存桶中。

    如果你想使用我已经写好的脚本,不妨来看看这里

  5. 修改配置文件(见下方

  6. 启动HTTP API和WebSocket服务

    npm start

    输出示例:

    • [Extension]提示扩展模块已经载入
    • 图中第二行指出HTTP API服务监听2333端口
    • 图中第三行指出WebSocket服务监听2334端口

配置文件

详见配置文件文档

脚本与自制镜像

本项目中Minecraft服务器的部署是依赖于Bash脚本的,这些脚本都存放在./scripts目录下。

很容易能发现,./scripts目录下已经有了一套脚本,希望这套脚本能给大伙儿带来一些参考。

注:流程简述这节简述了这些脚本是如何参与部署过程的。

如果想直接使用这套脚本,那么请接着往下看吧。

自制镜像

为了配合Bash脚本,你首先需要制作一个自制镜像。

比如我为默认的脚本制作了这样的一个镜像:

  • 基于腾讯云镜像:CentOS 7.9 64位

  • 预装程序:

    • JDK 17.0.3.1 (Minecraft 1.19.x需要Java 17+的支持)
    • lz4 (快速压缩算法)
    • screen (多视窗管理)
    • axel (多线程下载)
    • coscli (腾讯云COS命令行工具,项目地址)

      注:可执行文件路径为/root/coscli

使用默认脚本

如果你想使用./scripts中已有的脚本,你首先得要稍微修改一下./scripts/setup_cos.sh这个脚本:

#!/bin/bash

# 设置COSCLI工具
./coscli config set --secret_id $QCLOUD_SECRET_ID --secret_key $QCLOUD_SECRET_KEY  

# 设置新的储存桶
./coscli config add -b <YOUR_BUCKET> -r ap-chengdu -a minecraft

在腾讯云COS对象储存控制台创建一个储存桶,然后将<YOUR_BUCKET>替换为你的储存桶名。而ap-chengdu则需要替换为你的储存桶所在的地域代号。

关于COSCLI工具的使用可以参考腾讯云官方文档

接下来,你需要在储存桶中创建一些目录并上传Minecraft服务端相关的文件,详见下一节 ↓

默认脚本对应的COS储存桶中存放的内容

└─server
        filelist.txt
        server_0
        server_1

储存桶根目录只有一个server目录,目录内存放着多个文件。

其中filelist.txt文件内记录的是部署时需要下载的Minecraft服务端压缩包文件名,每行一条:

server_0
server_1

注:记得最后要留一个换行符。另外一定要采用LF行尾序列,不然Bash脚本可能会报错。

server_0, server_1文件实际上是分块后的Minecraft服务端压缩包(采用lz4算法压缩),具体参考compress_and_path.sh

默认脚本中是按2GB一个文件进行分割的。


在完成自制镜像使用默认脚本默认脚本对应的COS储存桶中存放的内容这三节描述的步骤后,就可以进入下一步:修改配置文件了。

关于Bash脚本的环境变量

详见配置文件文档

关于主控端状态

主控端状态码

主控端状态码对应的提示语的配置详见配置文件文档

接下来列一下各状态码的含义:

状态码 含义
2000 实例未创建,主控端正在无所事事
2001 正在按照配置查询实例价格并挑选合适的实例
2002 正在创建SSH密匙对
2003 正在创建实例
2100 实例已创建,正在等待实例启动
2101 实例已启动,正在尝试通过SSH连接实例
2102 正在将部署所需脚本传向实例(通过SFTP)
2103 正在执行部署脚本搭建InsSide(实例端)
2200 正在尝试建立与InsSide(实例端)的WebSocket连接
2201 成功连接到InsSide(实例端),也说明实例端成功部署
2202 正在执行Minecraft服务器相关的部署脚本
2203 上述脚本执行成功,正在等待Minecraft服务器启动
2300 Minecraft服务器成功启动!!
2400 正在等待Minecraft服务器关闭
2401 Minecraft服务器已关闭,正在打包Minecraft服务端
2402 正在上传上述打包好的Minecraft服务端
2500 InsSide(实例端)工作结束,向主控端说再见。主控端此时在销毁实例,并删除SSH密匙对

主控端发生错误时的状态码

主控端发生错误时,状态码会在正常情况的基础上减去1000

比如实例已经创建,主控端在等待实例启动(此时状态码为2100)。然而实例启动花了很久,主控端认为超时了,此时状态码会变为1100(发生错误)。

发生错误时的操作

主控端发生错误后,大部分节点操作都无法执行,除了下面这两个,它们可以用于恢复主控端的运行:

点击查看文档 ↑

注:wipe_butt可能导致部分数据丢失,建议只在错误完全无法恢复时使用。

特殊的情况

在状态码为2001(正在按照配置查询实例价格并挑选合适的实例)时,如果腾讯云没有符合要求的实例资源了,主控端会报告:

No available instance (that meet the specified configuration)
(服务商没有符合你配置要求的实例资源了)

然而在这之后,主控端不会进入错误状态,而是回到2000(实例未创建,主控端正在无所事事)状态。

稍后如果服务商有资源了,你可以再次尝试创建实例。

使用

请求HTTP API

  • 请求路径形如

    http://<主控端IP>:<HTTP API端口>/<主节点>/<子节点>/<操作>
    

    主节点/子节点这些详见文档:节点及其权限

    比如我想正常创建一个实例并部署Minecraft服务器

    GET http://<主控端IP>:<HTTP API端口>/server/normal/launch


  • 鉴权方式

    通过Authorization请求头进行鉴权。

    Authorization: Bearer <token>
    

    其中<token>是你的访问令牌

    详见文档:访问令牌

    访问特定的节点需要特定的权限,详见文档:节点及其权限


  • 请求方式

    目前主要支持的方式是GET, POST以及OPTIONS

    • OPTIONS - 用于浏览器预检请求,直接返回200 OK,无任何其他操作

    • POST - 有少数几个节点操作仅支持POST请求方式:

      • /server/command/send
      • /backend/token/generate
    • GET - 除了上述的操作外,其他所有节点操作都支持GET请求方式(POST也行)


  • 关于POST请求

    • 请求头:Content-Type: application/json
    • 请求体:序列化后的JSON字符串

  • 返回内容的公共字段

    这里仅简述一下返回内容的公共字段,其他返回字段可见文档:节点及其权限

    返回示例(Content-Type: application/json):

    {
        "data": {}, // 返回的数据
        "code": -1, // 返回的执行状态码
        "msg": "Lack of valid action" // 返回的执行信息
    }

    关于code字段的值:

    • 1 -> 执行成功
    • 0 -> 递交给了异步/实例端处理,暂时未知执行结果
    • -1 -> 执行失败

  • 错误信息

    错误信息 说明
    Lack of valid action 对于节点缺少有效的操作,可能你请求的路径并不存在
    Request Entity Too Large 请求体过大,POST的数据超过了1MB
    Unauthorized 没有按照要求进行鉴权
    Permission Denied 你所持的令牌没有访问目前节点操作的权限
    Failed to generate: <msg> 生成临时令牌失败,<msg>是失败原因
    Non-existent Node 请求了一个不存在的节点
    Minecraft Server Not Running. Minecraft服务器不在运行中,指定节点操作无法执行
    Command not specified 向Minecraft服务器发送命令时没有指定命令(请求体JSON中没有command字段)
    Private key not found. 没有找到实例SSH私钥。这往往是因为尚未创建实例
    Method Not Allowed 该使用POST请求方式的地方没有用,详见这里
    Invalid Request 无效访问,一般是访问子节点错误,这个错误很少见
    Invalid Path 无效路径,这个问题我好像都没怎么遇到了
    There's no need to revive. 主控端没有发生错误,无须尝试恢复。仅在请求/server/maintenance/revive时可能出现
    Server is not running. Minecraft服务器不在运行中。仅在关闭和杀死服务器时可能出现
    Error exists, unable to launch the server 主控端发生了错误,无法启动Minecraft服务器。
    Server Already Launched Minecraft服务器已经在运行中。仅在启动服务器时可能出现
    Urgent backup exists, please use action: restore_and_launch or launch_and_discard_backup 紧急备份存在,只能通过restore_and_launchlaunch_and_discard_backup操作启动Minecraft服务器

通过WebSocket同步Minecraft服务器日志

主控端WebSocket服务目前只用于实时同步Minecraft服务器的控制台日志

值得注意的是,这里的实时同步是增量的,每次Minecraft服务器日志更新时,主控端这儿只会同步自上次同步以来新增的日志内容

如果你需要获得自Minecraft服务器启动以来的所有日志,建议你请求HTTP API的这个节点操作:/server/mc_logs/get

  • 连接地址

    ws://<主控端IP>:<WebSocket端口>
    

  • 鉴权方式

    建立WebSocket连接后,向主控端发送一个包含key字段的JSON字符串,以下是连接示例代码:

    const TOKEN = 'MY TOKEN....';
    const ws = new WebSocket('ws://localhost:2334');
    ws.addEventListener('open', () => {
        console.log('connected');
        let sendObj = {
            key: TOKEN // 你的访问令牌
        }
        ws.send(JSON.stringify(sendObj));
    });

    如果你具有websocket.mclog.receive权限(详见节点及其权限),就会正常收到来自Minecraft服务器的控制台日志。

    但如果你不具有这个权限,WebSocket连接会被立刻关闭,关闭理由是Nanoconnection, son.

扩展

为了实现和消息机器人通信一类的功能,主控端可以接纳一些JavaScript扩展模块。

这些扩展模块存放在项目的extensions目录下,在这个目录内有一个实例模块informer.js

扩展模块必须要有的

扩展中一定要导出(exports)一个函数,这个函数的返回值是一个布尔值,代表该扩展是否成功载入

/**
 * 扩展载入方法,由extensions-loader调用
 * @returns {Boolean} 是否正确载入
 */
module.exports = function () {
    return true; // 一切正常
};

主控端事件

扩展模块的运作依赖于事件,在informer.js中能看到头部引入了事件模块:

// 导入EventEmitters
const events = require('../basic/events');

events对象包含两个EventEmitter

  • MessageEvents - 主控端消息相关的事件

    事件名 callback参数 说明
    statusupdate (msg, inform, code) 主控端状态码更新时触发,msg是状态码对应的消息,inform是状态码对应的消息是否建议通知,code是状态码
    normalmsg (msg, inform) 主控端输出一条普通消息时触发,msg是消息内容,inform是消息是否建议通知
    warningmsg (msg, inform) 主控端输出一条警告消息时触发,msg是消息内容,inform是消息是否建议通知
    errormsg (msg, inform) 主控端输出一条错误消息时触发,msg是消息内容,inform是消息是否建议通知
  • ServerEvents - Minecraft服务器相关的事件

    事件名 callback参数 说明
    mclogupdate (logStr) Minecraft服务器控制台日志更新时触发,logStr新增的日志内容
    getip (ip) 获取到实例IP地址时触发,ip是IP地址
    launchsuccess (ip) Minecraft服务器成功启动后触发,ip是实例IP地址
    serverclosed (reason) Minecraft服务器被关闭时触发,reason是关闭原因

比如我要在Minecraft成功启动(ServerEventslaunchsuccess事件)后通知群内的所有人,可以这样写:

events.ServerEvents.on('launchsuccess', (ip) => {
    // 假设有sendToGroup这个方法
    sendToGroup(`Server successfully launched, ip: ${ip}`);
});

增量备份

详见这个文档

流程简述

这里简单叙述一下一次完整的从开服到关服的流程,忽略了一些我觉得没必要讲的细节。(有兴趣的话可以看看源码)

启动实例

假设此时用户请求的节点操作是/server/normal/launch,那么主控端会执行以下操作:

  1. 主控端进入2001状态。请求腾讯云API,获得(选定地域中的)所有实例的配置,然后根据api_configs中的qcloud配置对这些实例进行筛选,找出符合条件的实例,组成一个实例配置数组

    未经筛选过的所有实例的配置将被序列化为一个JSON文件,暂存在server_data/all_available_ins.json中。

  2. 如果这个实例配置数组为空,主控端会回到2000状态,并提示用户没有可用的实例资源。如果其不为空,进入第3步

  3. 主控端进入2002状态,请求腾讯云API生成一对SSH密匙对,接着取得其中的私匙,暂存在server_data/login.pem文件中。

    同时,主控端也将这个密匙对的唯一id以字段名instance_key_id储存在server_data/instance_details.json文件中。

  4. 主控端进入2003状态,请求腾讯云API创建实例,接着取得其中的实例id,以字段名instance_id暂存在server_data/instance_details.json文件中。

部署实例端(InsSide)程序

  1. 主控端进入2100状态,通过腾讯云API轮询实例是否启动(进入RUNNING状态),如果在规定时间内实例启动了,进入第2步,否则进入错误状态1100(实例启动超时)。

    规定时间指的是api_configs中的instance_run_timeout配置项。

  2. 第1步中如果实例进入了RUNNING状态,主控端就能拿到实例的IP地址,并将其以字段名instance_ip暂存在server_data/instance_details.json文件中。

  3. 主控端进入2101状态,开始尝试通过SSH连接到实例,如果连接成功,进入第4步,否则进入错误状态1101(SSH连接失败)。

  1. 主控端进入2102状态。接下来主控端将api_configs中的ins_side配置项单独序列化成一个JSON文件,作为实例端程序InsSide的配置文件,暂存在server_data/ins_side_configs.tmp.json中。

    这个配置文件中还包括了额外的字段:

    {
        // 是否在维护模式下启动
        'under_maintenance': [Boolean], 
        // 启动后是否恢复增量备份(如果backup_records为空的话,这一项无效)
        'restore_before_launch': [Boolean], 
        // 备份记录数组,如果没有就是null
        'backup_records': [Array|null],
        // 主控端和实例端通信时鉴权用的密钥, 自动生成的128位字符串
        'secret_key': [String],
        // Bash脚本的公共环境变量
        'env': [Object]
    }

  1. 主控端将./scripts目录下的所有Bash脚本第4步中的实例端配置文件一起上传到实例中的数据目录下。
    如果上传失败,会进入错误状态1102

    这里的数据目录指的是api_configs中的ins_side配置项的data_dir字段。

  2. 主控端进入2103状态,开始执行部署实例端(InsSide)程序的脚本以启动实例端。
    如果执行失败,会进入错误状态1103

    (对应配置项:api_configs.ins_side.instance_deploy_sh),默认的脚本是./scripts/set_up_base.sh

    执行这个脚本的时候不支持特殊的环境变量
    请务必多次调试以保证这些Bash脚本都能正常执行而不是抛出错误。

  3. 我写好的set_up_base.sh脚本主要会做这几件事:

    • 创建目录/root/BottleM-InsSide

    • Gitee Releases下载(axel多线程下载)最新的InsSide程序可执行文件到/root/BottleM-InsSide目录下。

    • screen进程管理下执行daemon.sh脚本。这个脚本主要作用是启动/root/BottleM-InsSide目录下的实例端(InsSide)程序,并实现实例端(InsSide)崩溃后的自动重启

建立和实例端(InsSide)的连接

  1. 主控端进入2200状态,尝试通过配置的端口(配置项:api_configs.ins_side.ws_port)连接到实例端(InsSide)进程。如果成功建立WebSocket连接,进入第2步
    如果连接超时,会进入错误状态1200

  2. 主控端向实例端发送status_sync请求,实例端会返回当前的状态码,这样来实现与实例端的状态码同步。

    实例端一经启动,会立即开始Minecraft服务器部署的工作,状态码从2201开始。

实例端(InsSide)部署Minecraft服务器

从这里开始,主角就是实例端了。主控端在建立和实例端的连接后只负责错误处理/状态码同步/用户操作传递/日志同步等工作。

实例端的日志会通过连接回传给主控端,实例端发生错误时会通知主控端,实例端状态码发生更新时也会同步到主控端。

而用户的关服、发送命令等操作则由主控端递交给实例端执行。可见二者是相互协作的关系。


实例端(InsSide)一经启动,会立即开始Minecraft服务器部署的工作,状态码从2201开始。

  1. 实例端进入2202状态,检查并创建必要的目录,如:

    • 打包后的Minecraft服务端存放的绝对路径 - api_configs.ins_side.packed_server_dir
    • Minecraft服务端的绝对路径 - api_configs.ins_side.mc_server_dir
  2. 执行所有的Minecraft服务器部署脚本(配置项:api_configs.ins_side.deploy_scripts)。
    ./scripts下有我写的两个脚本setup_cos.shget_server.sh,他们的作用分别是:

    • setup_cos.sh

      配置COSCLI,使得其能正常访问腾讯云COS储存桶。

    • get_server.sh

      1. 从COS储存桶下载分块的文件列表server/filelist.txt

      2. 根据filelist.txt,从COS储存桶下载分块的Minecraft服务端压缩包文件

        上面两步的文件都下载到了api_configs.ins_side.packed_server_dir配置的目录下

      3. 将分块的Minecraft服务端压缩包文件合并为一个完整的压缩包文件并解压api_configs.ins_side.mc_server_dir配置的目录下

  1. 初始化增量备份

    这一步扫描了api_configs.ins_side.incremental_backup.src_dirs配置的增量备份源目录中的所有文件,记录了它们的最新修改日期

  2. 检查是否需要恢复/丢弃增量备份

    这里可以看到主控端传给实例端的配置中包含了两项和增量备份有关的配置项:

    • restore_before_launch - 是否在启动Minecraft服务器前恢复增量备份

    • backup_records - 增量备份记录

    如果backup_records不为空,那么实例端会根据restore_before_launch的值来决定在启动Minecraft服务器前如何对待增量备份:

    • restore_before_launchtrue时,实例端会恢复增量备份

    • restore_before_launch'discard'时,实例端会抛弃增量备份

    如果backup_records为空,则restore_before_launch没有效果,不会对增量备份做出操作。

    backup_records是否为空,取决于主控端的server_data/backup_records.json文件是否存在。

  1. 扫描部署前服务端压缩包文件大小

    如果配置项api_configs.ins_side.check_packed_server_size大于0,实例端会对Minecraft服务端压缩包文件大小进行扫描和记录

    第2步中Bash脚本将Minecraft服务端文件下载到了api_configs.ins_side.packed_server_dir配置的目录中。

    ↑ 实例端程序此时扫描的也正是这个目录中所有文件的大小

    因此,如果你配置了api_configs.ins_side.check_packed_server_size > 0,那么在Bash脚本解压Minecraft服务端之后不要删掉这个目录中的内容,不然会报错:Packed server directory is empty!
    程序在扫描完大小后会自动清空这个目录。

  2. 执行Minecraft服务器启动脚本(配置项:api_configs.ins_side.launch_script

    我写的启动脚本是./scripts/launch_server.sh,Minecraft服务器进程受screen进程监管。

  3. 实例端进入2203状态,轮询Minecraft服务器是否启动(通过minecraft-protocol包的ping方法)。

    如果在api_configs.ins_side.mc_server_launch_timeout配置的时间内启动了,就进入下一个环节。
    反之,就会报错:Minecraft server launch timeout!,且进入错误状态1203

监控Minecraft服务器

Minecraft服务器成功启动后,实例端会进入2300状态,启动RCON连接,并同时启动几个定时器/事件监听器

  1. Minecraft服务器日志监听器

    实例端会监听Minecraft服务器的日志文件(配置项:api_configs.ins_side.mc_server_log)的新增内容,并将新增的日志内容回传给主控端。

  2. Minecraft服务器空闲时间计时器(仅限非维护模式

    服务器中没有玩家游玩时,就进入了空闲状态中。

    在Minecraft服务器空闲了一定长时间后(配置项:api_configs.ins_side.server_idling_timeout),实例端会自动关闭Minecraft服务器。

    每过一段时间,实例端会向主控端回传一次当前剩余的空闲时间(单位:秒)。

    关闭Minecraft服务器时是正常关闭

  3. Minecraft服务器进程监视器(仅限非维护模式

    实例端通过反复执行api_configs.ins_side.server_scripts.check_process配置的Bash脚本,来监视Minecraft服务器进程是否存在

    如果该脚本没有输出任何内容,则说明Java进程不存在

    我写的脚本是./scripts/check_process.sh,采用了netstat命令来找出监听25565端口的进程。

    如果进程不存在,实例端会跳过关闭Minecraft服务器的流程,直接进入下一个环节。

  4. 竞价实例回收监视器(仅限非维护模式

    实例端通过反复执行api_configs.ins_side.server_scripts.check_termination配置的Bash脚本,来监视竞价实例是否即将被回收

    竞价实例有半途被回收的可能,需要一定的应对措施。
    如果该脚本没有输出任何内容,则说明实例即将被回收

    实例即将被回收时,实例端会正常关闭Minecraft服务器,并紧急进行一次增量备份

  5. 玩家上线事件监听器(仅限非维护模式

    玩家上线后,实例端会暂停空闲时间计时器。

    如果api_configs.ins_side.player_login_reset_timeout配置为了true,那么玩家上线后,服务器目前闲置的时间也会归零

  6. 服务器最后一位玩家下线监听器(仅限非维护模式

    Minecraft服务器中最后一位玩家下线后,实例端会恢复空闲时间计时器

  1. 增量备份计时器(如果开启了增量备份

    配置项api_configs.ins_side.incremental_backup.interval指定了每两次增量备份之间的时间间隔

    如果开启了增量备份,实例端会每隔这段时间,进行一次增量备份。

    关于增量备份请看这里

  2. 停止Minecraft服务器事件的监听器

    用户对于Minecraft服务器的停止操作,会由主控端递交给实例端,并在实例端以事件的形式被触发。

    这个事件一旦被触发,实例端会正常关闭Minecraft服务器

  3. 杀死Minecraft服务器事件的监听器

    用户对于Minecraft服务器的杀死操作,会由主控端递交给实例端,并在实例端以事件的形式被触发。

    这个事件一旦被触发,实例端会跳过关闭Minecraft服务器的流程,直接打包Minecraft服务端进行上传。

  4. 紧急停止Minecraft服务器事件的监听器

    这个事件仅在竞价实例即将被回收时被触发。

    紧急模式下,实例端仍然会正常关闭Minecraft服务器,但不同的是Minecraft服务器停止后,实例端仅执行一次增量备份并回传增量备份记录给主控端,而不会执行完整的打包上传流程

    这是因为竞价实例即将被回收时往往只剩几分钟的缓冲时间了,只能尽快进行增量备份。

  5. 玩家人数监视器

    实例端会每隔一段时间查询Minecraft服务器中的玩家人数,并向主控端上报

    玩家上线事件服务器最后一位玩家下线在这里被触发。

主控端检查竞价实例是否即将被回收

鉴于各服务商提供的竞价实例状态查询接口不尽相同,除了在实例端有查询竞价实例是否被回收的脚本,主控端也会每隔一段时间查询竞价实例的状态。

比如腾讯云查询竞价实例是否被回收需要在实例上请求特定的URL来获得实例的元数据,
而阿里云的实例Describe API则可以直接查询实例的状态。

主控端检查竞价实例的部分写死在对应的模块里了,比如本项目我是和腾讯云进行对接的,就必须在qcloud.js内写一个checkTermination方法:

/**
 * 检查竞价实例是否即将被回收
 * @param {String} insId 实例id
 * @returns {Promise} resolve一个布尔值,true表示即将被回收,false表示不会被回收
 * @note 暂时直接resolve(false),因为腾讯云没有提供API,只能在实例端请求metadata
 */
function checkTermination(insId) {
    return Promise.resolve(false);
}

如果上面这个方法resolve了一个true,那么主控端会给实例端发送紧急停止Minecraft服务器的信号。

(会触发上面的紧急停止Minecraft服务器事件

断线重连

实例端和主控端通过WebSocket连接通信,但因为网络波动的原因,往往难以保证整个流程中连接不会断开。

每当实例端和主控端的连接断开时,主控端会尝试重新建立连接

  1. 创建一个新的临时配置文件ins_side_configs.tmp.json重新生成实例端和主控端连接鉴权用的128位密匙。

    本步骤可参考这里

  2. 尝试通过SSH连接到实例,连接成功后通过SFTP上传ins_side_configs.tmp.json到实例中的对应目录

  3. 尝试建立主控端和实例端的WebSocket连接,连接成功后删除本地的临时配置文件ins_side_configs.tmp.json

  4. 连接成功后主控端向实例端发送status_sync请求,实例端会返回当前的状态码,这样来实现与实例端的状态码同步。

其中第2步第3步有重试次数的,分别对应配置项:

  • api_configs.ssh_connect_retry - SSH连接建立重试次数

  • api_configs.instance_ws_connect_retry - 实例端WebSocket连接建立重试次数

⚠ 如果在有限的重试次数内,主控端一直都无法连接到实例端,那么主控端会直接进入收尾流程

实例端准备进入收尾流程

紧接监控Minecraft服务器这一节。

在接收到关闭指令后,实例端会退出监视器,进入收尾阶段,程序内对于这个阶段传递了三个字段:

{
    // Minecraft服务器关闭原因
    reason: [String],
    // 是否通过RCON向Minecraft服务器发送关闭指令并等待关闭
    stop: [Boolean], 
    // 是否是紧急模式
    urgent: [Boolean]
}

stoptrue时往往是软停止Minecraft服务器,用于:

  • Minecraft服务器闲置超时,自动关闭
  • 竞价实例即将被回收,紧急关闭
  • Minecraft服务器被用户(往往是管理员)手动关闭(stop)

stopfalse时往往不会等待Minecraft服务器关闭,用于:

  • Minecraft服务器进程已经结束
  • Minecraft服务器被用户(往往是管理员)手动杀死(kill)

urgent只有在竞价实例即将被回收时才会为true

关闭Minecraft服务器

实例端进入2400状态。

如果stoptrue,那么实例端会通过RCON向Minecraft服务器发送stop命令,然后等待Minecraft服务器关闭

等待Minecraft服务器关闭靠api_configs.ins_side.server_scripts.check_process配置的Bash脚本来实现。 如果stopfalse,跳过此流程。

打包并上传Minecraft服务端

实例端会先通知主控端Minecraft服务器已关闭

然后实例端进入了2401状态。


接下来,如果urgentfalse(非紧急模式):

  1. 实例端会执行api_configs.server_ending_scripts.pack配置的Bash脚本,压缩并打包Minecraft服务端,制成压缩包文件。

    这一部分我写的脚本是compress_and_pack.sh,它主要做的事是:

    • 通过tar命令和lz4压缩程序,将Minecraft服务端打包成压缩包文件serverAll.tar.lz4

      该压缩包和下面生成的文件都存放在api_configs.ins_side.packed_server_dir配置的目录下。

    • 将压缩包文件serverAll.tar.lz42GB一块分为几个文件server_0, server_1...

    • 删除serverAll.tar.lz4

    • 扫描分块得到的所有文件的名字,一行一条文件名地输出到filelist.txt文件

  2. 如果配置了api_configs.ins_side.check_packed_server_size > 0

    api_configs.ins_side.check_packed_server_size <= 0
    维护模式下,会跳过这一个流程,不对打包后的文件大小进行检查。

    实例端会再次扫描api_configs.ins_side.packed_server_dir(打包目录)配置的目录下所有的文件总大小(记为CURR_SIZE),并和部署开启前扫描到的文件总大小(记为PREV_SIZE)进行对比:

    (设PERCENTAGE= api_configs.ins_side.check_packed_server_size

    • 如果CURR_SIZE × PERCENTAGE% <= PREV_SIZE,这说明刚刚压缩打包制成的文件总大小过小了,这意味着打包过程中可能出现了问题,导致服务端文件有一部分丢失了

      这种情况下实例端会给主控端上报错误:There's something wrong with the compressed packs of Minecraft Server, please check it.,主控端状态转变为1401

  3. 实例端进入2402状态。

  4. 接下来实例端会执行api_configs.server_ending_scripts.upload配置的Bash脚本,将api_configs.ins_side.packed_server_dir(打包目录)目录中的文件上传到云储存中

    这一部分我写的脚本是upload_server.sh,它主要做的事是:

    • filelist.txt文件上传到云储存中,作为分块文件索引
    • 逐行读取filelist.txt文件,将分成块的压缩包文件上传到云储存中

  1. 如果启用了增量备份,这一步会抛弃掉所有现存的增量备份

    首先,实例端会删除所有云储存中的增量备份文件,
    接着实例端会通知主控端删除所有的增量备份记录(删除server_data/backup_records.json文件)。

    因为第4步中已经把所有的服务端文件都上传到云储存中了(相当于一次全量备份),所以现存的增量备份已经没有意义了。


如果urgenttrue紧急模式):

假使没有开启增量备份,整个收尾流程和非紧急模式下的流程是一样的。

假使开启了增量备份,紧急模式下的流程如下:

  1. 实例端直接进行一次增量备份,并把该条增量备份记录上报给主控端

没错,只有一步,够快吧!

实例端向主控端说再见

至此,实例端已经结束了所有流程,向主控端说“再见”。

“再见”的实现方式是1001状态码关闭WebSocket连接,连接关闭理由是Goodbye

主控端进行收尾工作

在接收到实例端的“再见”后,主控端会进行收尾工作:

  1. 主控端进入2500状态

  2. 销毁当前项目下创建的实例

  3. 销毁实例对应的SSH密匙对

  4. 清空主控端的server_data目录(实际上上面的第1、2步相关的文件都在这个目录下),但是server_data/backup_records.json不会被删除。

  5. 回到最初的2000状态

至此一整个流程就走完了喵!

一些建议

  1. 如果主控端实例之间的网络连接波动较大,建议提高配置项api_configs.ssh_connect_retry的值,而稍微降低配置项api_configs.ssh_ready_timeout的值。

    比如我主控端托管在香港的服务器上,但是创建的实例位于成都,这种情况下,我就会把ssh_connect_retry设置为15,而把ssh_ready_timeout设置为15000
    这样增加了重试次数,减少了每次重试要等待的时间

    ❗ 如果重试了指定次数,主控端却仍然无法连接上实例端,那么主控端会直接进入收尾流程,详见这里

License

Under Apache-2.0 License.