本文是希望可以從軟體工程師的角度出發 紀錄一下是如何一步一步的架設伺服器的 希望也幫助到其他想架設 Minecraft 伺服器的人
https://rock-mc.github.io/
磐石伺服器是一個邀請制原味生存 Minecraft 伺服器
目標是提供一個長期 、穩定 、流暢 的伺服器 目前由數位軟體工程師鎮守,務求提供優質的服務給各位
我們沒有商店、沒有領地,專注在提供最流暢的遊玩環境 盡可能保持最少的插件,有新版本的時候可以用最快的速度升級 更採用邀請制 ,讓每個玩家都可以一起維持玩家品質
如果你沒有邀請碼,可以透過面試管道取得邀請碼
目前正在 Java 1.17.1 營運,歡迎加入我們
伺服主 CodingMan
下面會分成幾個部分解說
自動安裝腳本快速在新環境部署你的 Minecraft 環境的腳本 啟動腳本設定 Java option 啟動你的 Minecraft 伺服器 管理程式在伺服器上一層,一邊讀取 Minecraft 伺服器的系統訊息,一邊可以自動對 Minecraft 伺服器下指令 Linux service將管理程式包裝成 Linux service,讓系統監看你的程式是否有持續運作 自動開機 自動安裝腳本 一開始在 GCP 上面測試,為了可以快速切換機器 所以寫了一個自動安裝腳本,可以在幾分鐘內在全新環境建置好你的伺服器
在這裡我們選用 Ubuntu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 java_installer=https://cdn.azul.com/zulu/bin/zulu16.30.15-ca-jre16.0.1-linux_x64.tar.gz installer_name_full="$(basename -- $java_installer) " installer_name="${installer_name_full%.*} " installer_name="${installer_name%.*} " sudo apt-get update && sudo apt-get -y upgrade sudo apt-get -y install git screen sudo timedatectl set-timezone Asia/Taipei sudo chmod 600 ./github eval `ssh-agent -s`ssh-add ./github cd ..curl -O $java_installer tar -xf $installer_name_full if [ -d "./server" ]then rm -r ./server fi git clone [email protected] :rock-mc/server.git cd servergit config core.filemode false git config --global user.email "[email protected] " git config --global user.name "PttCodingMan" mchome="$(pwd) " if [ -f "startup.sh" ]; then rm startup.sh fi cp startup_sample.sh startup.shsed -i "s/THIS_VALUE_WILL_REPLACE_WITH_JAVA_PATH/$installer_name /" startup.sh git clone [email protected] :rock-mc/world.git git clone [email protected] :rock-mc/world_nether.git git clone [email protected] :rock-mc/world_the_end.git sudo chmod -R 777 . cd ..if [ -d "./__MACOSX" ]then sudo rm -r __MACOSX/ fi cd initsed -i --expression "[email protected] _VALUE_WILL_REPLACE_WITH_MCHOME@$mchome @" minecraft.service sudo cp minecraft.service /etc/systemd/system/ sudo systemctl enable minecraft sudo systemctl start minecraft
啟動腳本 啟動腳本除了負責紀錄 Java option 啟動伺服器以外,我也希望可以把之前當掉的 Minecraft 伺服器紀錄可以保存下來,以方便追蹤錯誤
一開始我發現如果只是單純的使用以下指令,會看不到日誌輸出到螢幕上
1 SomeCommand > system.log
經過一番追尋之後,我發現了 tee 指令,更棒的是可以搭配日期輸出 於是乎我們就得到了,具備日期的輸出日誌的啟動腳本!
Java option 則可以參考 Aikar 的文https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/
啟動腳本範例如下
1 2 3 mkdir -p run_log./$JAVA_PATH /java <java option> -jar paper.jar nogui | tee "run_log/$(date +%m-%d_%H-%M) .log"
管理程式 現在我們可以透過啟動腳本啟動 Minecraft 伺服器了,但如果是遇到錯誤需要自動重啟,或者是任何有需要自動化對伺服器下指令的時候,光靠啟動腳本無法滿足我們的需求
所以我另外使用 Python 開發了管理程式,除了遇到當掉重啟外,也可以偵測沒玩家在線上之後,自動將伺服器關閉,更可以偵測到一些錯誤時,可以快速對伺服器下指令做緊急的處置
我的管理程式實作如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 import datetimeimport osimport sysimport threadingimport timeimport uuidfrom os import pathfrom SingleLog.log import Loggerfrom watchdog.events import FileSystemEventHandler, FileModifiedEventfrom watchdog.observers import Observerimport server_utildef detect_server (test: bool = False ) -> dict : global lock global next_check_time global is_stopping if test: random_id = uuid.uuid4().hex [:5 ] logger.info('id' , random_id) joined_count = 0 left_count = 0 is_duplicate_uuid = False system_start_time = None emergency_restart = False tolerance_range = 2 if next_check_time is not None : if time.time() < next_check_time: next_check_time = time.time() + tolerance_range if test: logger.info('wait next check time' , random_id) return {'emergency_restart' : emergency_restart, 'system_start_time' : system_start_time, 'joined_count' : joined_count, 'left_count' : left_count, 'is_duplicate_uuid' : is_duplicate_uuid} with lock: next_check_time = time.time() + tolerance_range time.sleep(tolerance_range) while time.time() < next_check_time: time.sleep(int (tolerance_range / 10 )) if test: logger.info('read file' , random_id) else : run_log = server_util.get_server_run_log() for i in range (len (run_log)): if '>' not in run_log[i]: if 'Done' in run_log[i] and 'For help' in run_log[i]: if system_start_time is None : system_start_time = time.time() if 'joined the game' in run_log[i]: joined_count += 1 if 'left the game' in run_log[i]: left_count += 1 if 'UUID of added entity already exists' in run_log[i]: is_duplicate_uuid = True if 'Can\'t keep up!' in run_log[i]: emergency_restart = True if 'Stopping the server' in run_log[i]: is_stopping = True if 'Stopping server' in run_log[i]: is_stopping = True if system_start_time is None : next_check_time = time.time() + 10 return {'emergency_restart' : emergency_restart, 'system_start_time' : system_start_time, 'joined_count' : joined_count, 'left_count' : left_count, 'is_duplicate_uuid' : is_duplicate_uuid} class LogHandler (FileSystemEventHandler ): def on_modified (self, event: FileModifiedEvent ): global is_stopping global is_wait_stop if is_stopping: if not is_wait_stop: is_wait_stop = True server_util.wait_server_stop() return if not str (event.src_path).endswith('.log' ): return detect_log_result = detect_server() if detect_log_result['is_duplicate_uuid' ]: for count_down in reversed (range (0 , 6 )): server_util.send_server_command(f'say 偵測到問題實體,緊急重新啟動,倒數...{count_down} ' ) time.sleep(1 ) server_util.send_server_command('stop' ) server_util.wait_server_stop() if detect_log_result['emergency_restart' ]: for count_down in reversed (range (0 , 6 )): server_util.send_server_command(f'say 偵測到系統問題,緊急重新啟動,倒數...{count_down} ' ) time.sleep(1 ) server_util.send_server_command('stop' ) server_util.wait_server_stop() wait_player_max_min = 10 check_time_sec = 10 check_server_status_time_sec = 60 next_check_time = None is_stopping = False is_wait_stop = False lock = threading.Lock() logger = Logger('service' ) if __name__ == '__main__' : event_handler = LogHandler() observer = Observer() observer.schedule(event_handler, path='./run_log' , recursive=False ) observer.start() server_util.reset_command() log_check = list () log_count = None check_time_start = time.time() save_map = False update_list = [] no_player_time = None while True : time.sleep(check_time_sec) check_result = os.popen('ps aux | grep jar' ).read() if path.exists('/tmp/server_close' ): logger.info('stop service' ) server_util.send_server_command('stop' ) server_util.wait_server_stop() sys.exit() if 'Xms' in check_result and 'Xmx' in check_result: if time.time() - check_time_start < check_server_status_time_sec: continue this_year = datetime.datetime.now().year this_month = datetime.datetime.now().month this_day = datetime.datetime.now().day start_time = datetime.datetime(year=this_year, month=this_month, day=this_day, hour=2 , minute=30 ) end_time = datetime.datetime(year=this_year, month=this_month, day=this_day, hour=5 , minute=30 ) if not server_util.is_in_time_range(start_time, end_time): continue now = datetime.datetime.now() backup_folder = now.strftime('%Y-%m-%d' ) backup_path = f'/data/backup/{backup_folder} ' if backup_path in update_list: continue if os.path.exists(backup_path): update_list.append(backup_path) continue check_time_start = time.time() detect_result = detect_server() if detect_result['system_start_time' ] is None : logger.info('Server is startup' ) continue current_last_time = None if detect_result['joined_count' ] == 0 and detect_result['left_count' ] == 0 : if log_count != 'count from system_start' : log_count = 'count from system_start' logger.info('count from system_start' ) current_last_time = detect_result['system_start_time' ] elif detect_result['left_count' ] < detect_result['joined_count' ]: save_map = True no_player_time = None if log_count != 'player online' : log_count = 'player online' logger.info('player online' ) must_backup_time = datetime.datetime(year=this_year, month=this_month, day=this_day, hour=5 , minute=00 ) if server_util.is_in_time_range(must_backup_time, end_time): server_util.backup_map() os.system('sudo reboot' ) time.sleep(30 ) elif detect_result['joined_count' ] == detect_result['left_count' ]: save_map = True if log_count != 'count from player left' : log_count = 'count from player left' logger.info('count from player left' ) if no_player_time is None : no_player_time = time.time() current_last_time = no_player_time current_elapsed_time = time.time() - current_last_time if current_elapsed_time >= wait_player_max_min * 60 : server_util.backup_map() os.system('sudo reboot' ) time.sleep(30 ) else : logger.info('server is down!' ) logger.info('start server' ) os.system('screen -d -m sh startup.sh' ) time.sleep(10 ) check_time_start = time.time()
Linux service 現在我們可以透過管理程式配合啟動腳本,持續的處理伺服器程式本身發生的錯誤,但如果我們遇到系統上的錯誤,更甚者是停電後重新開機的情況,還是無法應對的
所以我們的最後一步是把管理程式打包成 Linux service,如此一來就可以讓作業系統來管理與監視管理程式是否有正確的執行
而打包成 Linux service 之前,我們還有些事情要做。需要先把操作管理程式的腳本先寫好 實作如下
啟動
1 python3 start_service.py
關閉
1 2 3 4 5 6 7 8 9 10 import osimport timeimport server_utilif __name__ == '__main__' : os.system('touch /tmp/server_close' ) time.sleep(3 ) server_util.wait_service_stop()
接下來就是把開關的 Python 腳本整合到 service 裡面 在這裡會建議你用使用者權限來啟動伺服器
1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=Minecraft service After=network.target [Service] User=your_username WorkingDirectory=THIS_VALUE_WILL_REPLACE_WITH_MCHOME ExecStart=/usr/bin/python3 THIS_VALUE_WILL_REPLACE_WITH_MCHOME/start_service.py ExecStop=/usr/bin/python3 THIS_VALUE_WILL_REPLACE_WITH_MCHOME/stop_service.py Restart=on-failure [Install] WantedBy=multi-user.target
使用以下指令註冊你剛寫好的 Linux service
1 2 3 sudo cp minecraft.service /etc/systemd/system/ sudo systemctl enable minecraft sudo systemctl start minecraft
註冊之後,就可以透過以下指令來開關伺服器
1 sudo systemctl start minecraft
1 sudo systemctl stop minecraft
完成!
自動開機 由於雲端服務掛著就會需要費用,所以沒人的時候就關機 因此也需要有一個自動開機的功能,可以讓玩家自行把伺服器打開
在這裡我們用的是 Could Functions 官方教學
實作如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var Compute = require ('@google-cloud/compute' );var compute = new Compute ();exports .startInstance = function startInstance (req, res ) { var zone = compute.zone ('asia-south1-c' ); var vm = zone.vm ('your server name' ); vm.start (function (err, operation, apiResponse ) { console .log ('instance start successfully' ); }); res.status (200 ).send ('server has Started!!' ); };
這樣玩家就可以透過 cloud functions 的網址,來啟動伺服器了! 如此一來,Minecraft 伺服器在 GCP 上基本的維持運作架構就完成了 玩家跟伺服主從此過著幸福快樂的日子 並不會