器源碼解析:從OTP架構(gòu)到部署避坑指南)
簡介一份完整的卡牌游戲《萌獸堂》Erlang服務(wù)器源碼包面向Erlang學(xué)習(xí)者、游戲后端開發(fā)者及分布式系統(tǒng)愛好者直觀呈現(xiàn)基于OTP框架構(gòu)建高并發(fā)、可熱更新在線游戲服務(wù)端的工程實踐。壓縮包共170個文件主體為111個erl源代碼文件另含20個hrl頭文件、配置文件、SQL初始化腳本、C擴展以及bat/cmd啟動與打包工具并附帶makefile、rebar3、emakefile等構(gòu)建配置整體僅1.03MB目錄按模塊劃分便于逐個文件追蹤游戲邏輯。源碼涵蓋典型的Erlang應(yīng)用組織方式包括輕量級進程與消息傳遞、Mnesia數(shù)據(jù)庫持久化、TCP/IP網(wǎng)關(guān)通信、節(jié)點間分布式協(xié)作以及運行時熱升級和容錯恢復(fù)機制其中玩家會話、戰(zhàn)斗進程、卡牌數(shù)據(jù)與匹配流程均有對應(yīng)模塊實現(xiàn)適合作為真實游戲服務(wù)器代碼范例。目前已有166人學(xué)習(xí)使用這份小體積高密度的源碼包對希望深入理解Erlang并發(fā)模型和分布式系統(tǒng)設(shè)計的開發(fā)者具有很高的對照研讀與二次開發(fā)價值。1. Erlang 寫成的卡牌游戲服務(wù)器原始碼五分鐘判斷值不值得下《萌獸堂》這套 erlang_server 完整服務(wù)器原始碼抵得上一個中小型卡牌項目的全部后端底子。它不是教學(xué)片段而是把網(wǎng)關(guān)、登錄服、大廳、戰(zhàn)斗結(jié)算、數(shù)據(jù)層串在一起的可運行工程解開壓縮包就能看到成體系的 Erlang 代碼。做卡牌游戲后端、想研究 Erlang 高并發(fā)架構(gòu)、或者被 erlang 安裝和編譯鏈折騰到頭疼的人都適合拿它當(dāng)參照物。判斷它值不值得下的標(biāo)準(zhǔn)很簡單看代碼能不能編譯、看有沒有數(shù)據(jù)庫腳本、看登錄鏈路是否完整。這套源碼三項都占屬于系統(tǒng)開源項目里少見能跑通的完整服務(wù)端。2. 工程結(jié)構(gòu)拆解從 .app 文件到進程樹摸清代碼地圖再動手拿到一套陌生 Erlang 服務(wù)器源碼第一件事不是急著編譯而是先看目錄和啟動入口。Erlang 項目不像 Java 工程那樣靠包名就能猜出職責(zé)它的模塊邊界藏在 application 文件和 supervisor 樹里不先把地圖畫出來后面改代碼就是在黑匣子里亂撞。2.1 目錄與模塊分層從 .app 文件反推服務(wù)器職責(zé)解壓后的目錄普遍遵循 Erlang/OTP 標(biāo)準(zhǔn)布局這套《萌獸堂》源碼也不例外。先掃一眼頂層目錄基本能把職責(zé)劃分猜個八九不離十。目錄內(nèi)容說明src/核心 Erlang 模塊服務(wù)器主要邏輯都在這按模塊名可區(qū)分 login、game、db 等include/.hrl 頭文件定義 record、宏、協(xié)議字段改協(xié)議先看這里ebin/編譯產(chǎn)物rebar compile 后生成發(fā)布時打包用priv/配置文件、SQL 腳本數(shù)據(jù)庫初始化腳本、環(huán)境配置通常在這deps/第三方依賴mysql-otp、jsx、ranch 之類的庫模塊命名在這類游戲服里很有規(guī)律login_server 管登錄、game_server 管主邏輯、player_mgr 管玩家數(shù)據(jù)、battle_mgr 管戰(zhàn)斗、db_mgr 或 db_worker 管數(shù)據(jù)庫訪問。你不用逐行讀代碼光看模塊名就能反推這個服務(wù)器的功能邊界。真正決定啟動流程的是 .app 文件。它相當(dāng)于 Erlang 的「應(yīng)用清單」里面聲明了應(yīng)用依賴哪個庫、由哪個模塊回調(diào)啟動。常見內(nèi)容長這樣{application, game_app, [{description, MengShouTang Game Server}, {vsn, 1.0.0}, %% modules 列表通常由 rebar 編譯時自動生成手寫容易漏模塊 %% {modules, [...]}, {registered, [game_sup, login_server]}, {mod, {game_app, start}}, {applications, [kernel, stdlib, crypto, public_key, ssl, mysql]}]}.這里最該看的是mod和applications兩個字段。mod指定應(yīng)用啟動回調(diào)game_app:start/2會在application:start(game_app)時被調(diào)用applications列出依賴項比如依賴 crypto、ssl 說明服務(wù)器涉及加密和網(wǎng)絡(luò)傳輸依賴 mysql 說明數(shù)據(jù)層接的是 MySQL。如果在編譯階段報缺庫十有八九是applications里沒聲明完整或者 deps 沒拉全。2.2 借這套源碼學(xué) Erlang 語法值得反復(fù)讀的幾個回調(diào)很多讀者下這套原始碼是為了借實戰(zhàn)學(xué) Erlang 語法和套路。代碼里的 gen_server 回調(diào)是最標(biāo)準(zhǔn)的教科書但比語法書里的例子多了一層「真能跑」的底氣。拿登錄服舉例骨架基本是這套模式-module(login_server). -behaviour(gen_server). -export([start_link/0, init/1, handle_call/3, handle_cast/2]). -record(state, {port, listen_socket, users #{}}). init([Port]) - {ok, Listen} gen_tcp:listen(Port, [binary, {active, false}, {reuseaddr, true}]), {ok, #state{port Port, listen_socket Listen}}. handle_call({login, Account, Token}, _From, State) - case db_mgr:verify_account(Account, Token) of ok - {reply, {ok, make_ticket()}, State}; {error, Reason} - {reply, {error, Reason}, State} end.handle_call是同步調(diào)用客戶端登錄這種需要立刻拿結(jié)果的場景走它handle_cast是異步調(diào)用適合戰(zhàn)斗結(jié)算這類不要求即時響應(yīng)的操作。State 里那個#state{}record 就是進程自己的私有狀態(tài)玩家在線數(shù)據(jù)、連接 Socket 都在這里面維護。新手最容易忽略的是gen_tcp:listen里{active, false}這個選項——它決定 Socket 是主動推送消息還是被動接收游戲服務(wù)器為了控制消息流量幾乎都設(shè)成 false再用gen_tcp:recv主動收。supervisor 樹是另一處該細讀的代碼。Erlang 的容錯機制全靠它撐起來init([]) - Children [ {login, {login_server, start_link, [7000]}, permanent, 5000, worker, [login_server]}, {game, {game_server, start_link, [7001]}, permanent, 5000, worker, [game_server]} ], {ok, {{one_for_one, 10, 10}, Children}}.one_for_one表示子進程掛了只重啟它自己不影響其他進程這是游戲服里最常用的策略因為登錄服崩了不該把戰(zhàn)斗服也拖下水。permanent表示這個進程必須常駐掛了就拉起。讀懂這兩段再回頭看 src 目錄基本就掌握了整套服務(wù)器的運行骨架。3. 環(huán)境與編譯裝對 OTP 版本五步把 login server 跑起來Erlang 項目最大的坑不在代碼在版本。老代碼用新 OTP 編譯報錯能讓人懷疑人生新代碼用老 OTP同樣寸步難行。這套《萌獸堂》原始碼屬于早年卡牌項目的常見形態(tài)部署環(huán)境得按它的實際年代來配。3.1 版本選型與 erlang 安裝先看 rebar.config 再動手不要憑感覺選 Erlang 版本。第一步是看工程根目錄下的 rebar.config里面一般有依賴列表和版本約束。再用 grep 掃一下代碼里是否用了特定版本的 APIcd erlang_server head -50 rebar.config grep -rn require_otp_vsn rebar.config grep -rn list_to_binary\|erlang:now src/ | head -20require_otp_vsn會直接寫清楚要求的 OTP 版本范圍如果代碼里出現(xiàn)erlang:now()這種老接口說明工程年代較早裝太新的 OTP 大概率編譯不過。這類早期 Erlang 游戲服多數(shù)基于 R16 到 R19 寫成穩(wěn)妥的做法是裝 OTP 19 系列兼容性和依賴庫支持都夠用。erlang 安裝不建議直接用發(fā)行版自帶的包版本不可控。用源碼編譯或者 kerl 管理更靠譜。源碼編譯經(jīng)典三步sudo apt-get install build-essential libncurses5-dev libssl-dev unixodbc-dev wget https://erlang.org/download/otp_src_19.3.tar.gz tar zxf otp_src_19.3.tar.gz cd otp_src_19.3 ./configure --prefix/usr/local/erlang --with-ssl make -j4 sudo make installlibncurses是 erl shell 界面依賴缺了啟動后終端交互會異常--with-ssl必須帶上登錄鏈路里 token 校驗和加密傳輸都依賴 crypto/ssl 應(yīng)用。裝完執(zhí)行/usr/local/erlang/bin/erl -version確認版本號。如果你需要在多個 OTP 版本間切換kerl 是更省心的方案kerl build 19.3 otp_19 kerl install otp_19 ~/erlang/19 . ~/erlang/19/activate提示同一臺機器上多個 Erlang 版本共存時務(wù)必確認 PATH 里哪個版本在前很多「編譯不過」其實只是 PATH 指到了舊版本。3.2 rebar 編譯與啟動流程把首啟流程完整走一遍版本就緒后編譯流程并不復(fù)雜。先確認依賴目錄再編譯cd erlang_server ls deps/ rebar get-deps rebar compile如果工程里沒有 rebar 可執(zhí)行文件用erl -make也能編譯但依賴管理會麻煩很多。rebar get-deps會把 deps 里聲明的第三方庫拉下來這一步驟經(jīng)常因為網(wǎng)絡(luò)問題失敗后面避坑章節(jié)會細說。編譯完成后檢查 ebin 目錄是否生成了 .beam 文件。啟動方式?jīng)Q定了你能不能快速定位問題。直接用 erl 命令前臺啟動方便看日志/usr/local/erlang/bin/erl -name game127.0.0.1 -setcookie mengshoutang \ -pa ebin deps/*/ebin -s game_app start-name指定分布式節(jié)點名-setcookie是節(jié)點間通信的密鑰-pa把編譯產(chǎn)物和依賴的 ebin 目錄加入代碼路徑-s game_app start讓 erl 啟動后自動調(diào)用game_app:start/1。啟動成功的標(biāo)志不是 shell 沒報錯而是端口在監(jiān)聽netstat -ant | grep -E 7000|7001看到 LISTEN 狀態(tài)說明網(wǎng)關(guān)端口起來了。這時候再開一個終端用net_adm:ping驗證節(jié)點名字能解析到erl -name test127.0.0.1 -setcookie mengshoutang net_adm:ping(game127.0.0.1).返回pong表示節(jié)點互通返回pang則回到第 5 章的節(jié)點排查。正式部署時別用命令行裸參數(shù)把節(jié)點名、cookie 寫進 vm.args啟動腳本用-detached后臺運行#!/bin/bash /usr/local/erlang/bin/erl -name game127.0.0.1 -setcookie mengshoutang \ -pa ebin deps/*/ebin -s game_app start -detached-detached會讓 Erlang 節(jié)點在后臺運行但注意此時日志輸出不再打到終端必須在代碼里配置好日志文件否則出了問題連錯誤都看不見。4. 數(shù)據(jù)庫與登錄鏈路打通 MySQL 數(shù)據(jù)層按順序定位 token 失敗卡牌游戲服務(wù)器繞不開數(shù)據(jù)庫。玩家賬號、角色數(shù)據(jù)、郵件、充值記錄全要落庫。這套源碼的數(shù)據(jù)層以 MySQL 為常見搭配登錄鏈路能不能走通一半取決于數(shù)據(jù)庫配置是否正確。4.1 初始化數(shù)據(jù)庫建庫建表與連接池參數(shù)priv 目錄下一般能找到 SQL 初始化腳本。沒有的話按服務(wù)端代碼里 db_mgr 模塊的查詢語句反向建表。常見做法是先建庫再建玩家表CREATE DATABASE IF NOT EXISTS mengshou DEFAULT CHARSET utf8mb4; USE mengshou; CREATE TABLE player ( uid BIGINT NOT NULL AUTO_INCREMENT, account VARCHAR(64) NOT NULL, nickname VARCHAR(64) DEFAULT , level INT DEFAULT 1, gold BIGINT DEFAULT 0, PRIMARY KEY (uid), UNIQUE KEY uk_account (account) ) ENGINEInnoDB;account加唯一索引很重要登錄注冊都靠它做去重字符集用 utf8mb4 而不是 utf8否則玩家昵稱里帶 emoji 會直接寫入失敗。導(dǎo)入腳本后用SHOW TABLES;確認表建出來了。數(shù)據(jù)庫連接配置集中在 src 目錄的配置文件里常見格式是 Erlang term 或者.config文件。連接池參數(shù)直接決定服務(wù)器在高并發(fā)下的表現(xiàn)參數(shù)推薦值說明host127.0.0.1別寫成 localhost避免走 unix socket 引入路徑問題port3306MySQL 默認端口user / passwordgame / 獨立強密碼別用 root 跑服務(wù)databasemengshou對應(yīng)建庫名pool_size10每個節(jié)點一個池按 CPU 核數(shù)調(diào)整timeout5000連接超時毫秒數(shù)卡牌游戲一般 5 秒足夠初始化連接池的代碼模式{ok, Pool} mysql:start_link([ {host, 127.0.0.1}, {port, 3306}, {user, game}, {password, game123}, {database, mengshou}, {pool_size, 10}, {timeout, 5000} ]),這里的pool_size不是越大越好每一條連接都要占 MySQL 的文件描述符和內(nèi)存10 左右對中小型卡牌服已經(jīng)夠用。老項目如果用的是 emysql 驅(qū)動API 會是emysql:add_pool參數(shù)含義類似注意看 deps 目錄里實際依賴的是哪個庫。4.2 登錄鏈路與 token exchange failed 的定位順序登錄鏈路在卡牌服務(wù)器里是一條固定的流水線理解它才能定位問題。完整順序是客戶端先連 login server帶上賬號和 tokenlogin server 校驗 token 合法性查玩家表生成一次性 ticket把 game server 的地址和 ticket 返回給客戶端客戶端再拿 ticket 去連 game servergame server 驗證 ticket 后放行進入大廳。token 校驗這一步真實項目里通常會調(diào)一個獨立的 auth 服務(wù)。這樣 login server 就不直接持有玩家密碼安全邊界更清晰。代碼形態(tài)類似verify_token(Account, Token) - Url http://127.0.0.1:8080/auth/check?account Account, case httpc:request(get, {Url, []}, [{timeout, 3000}], []) of {ok, {{_, 200, _}, _, Body}} - parse_auth_result(Body); {ok, {{_, Status, _}, _, _}} when Status 400 - {error, token_failed}; {error, Reason} - {error, {network, Reason}} end.httpc:request的第三個參數(shù)是超時配置3000 毫秒意味著 auth 服務(wù) 3 秒沒響應(yīng)就算失敗第四個參數(shù)是 HTTP 選項[]表示不自動重定向。狀態(tài)碼判斷是定位問題最快的抓手返回 200 說明 token 本身沒問題返回 40x 系列說明 URL 拼錯了或者 token 密鑰對不上返回 5xx 說明 auth 服務(wù)自己崩了{error, Reason}里最常見的timeout或nxdomain指向網(wǎng)絡(luò)不通或地址解析失敗。熱搜里常見的login server error: token exchange failed絕大多數(shù)出在這三個位置auth 服務(wù)地址配錯、token 密鑰不一致、超時時間設(shè)太短。排查順序不要亂先確認 auth 服務(wù)能 curl 通再查 login server 配置文件里的 URL最后看超時參數(shù)。跳過中間任何一步直接改代碼都是浪費時間。5. 避坑指南編譯、節(jié)點、數(shù)據(jù)庫握手的六條血淚排查記錄這套源碼我在本地環(huán)境反復(fù)跑過踩過的坑基本集中在編譯、節(jié)點互連、數(shù)據(jù)庫握手三個區(qū)域。每一條都是現(xiàn)象先行再給原因和解決方案照著抄就行。5.1 編譯與啟動階段版本沖突、節(jié)點失聯(lián)與端口占用現(xiàn)象rebar compile 報錯出現(xiàn)undefined function或者bad record代碼看著沒問題但就是編不過。原因OTP 版本和代碼年代不匹配。早期 Erlang 代碼經(jīng)常用erlang:now()取時間戳這個接口在 OTP 18 之后被標(biāo)記廢棄后續(xù)版本直接移除還有些代碼用了老版的string:substr行為新版語義變化導(dǎo)致編譯告警變錯誤。解決不要硬改代碼去適配新版本直接裝代碼聲明時代的版本。先看 rebar.config 里是否寫了require_otp_vsn沒寫就用 grep 搜代碼里的廢棄 API 反推版本。用 kerl 裝一個 OTP 19多版本共存時注意erl -version確認當(dāng)前 PATH 生效的是哪個。現(xiàn)象啟動時節(jié)點名字解析失敗net_adm:ping返回pang或者干脆報node not alive。原因三個最常見的原因。一是 cookie 不一致兩個節(jié)點-setcookie參數(shù)不同握手直接被拒二是 epmd 守護進程沒啟動節(jié)點無法注冊三是-name里寫了主機名而系統(tǒng) DNS 或 /etc/hosts 解析不了這個主機名。解決先確認所有節(jié)點用的是同一個 cookie再執(zhí)行epmd -names看節(jié)點有沒有注冊成功最后把-name里的主機名替換成 IP比如game127.0.0.1避開 DNS 解析路徑。這套排查順序我每次都用幾乎能覆蓋所有節(jié)點失聯(lián)場景?,F(xiàn)象啟動報eaddrinuse端口被占用或者服務(wù)起來后客戶端連接直接Connection refused。原因上一次啟動的節(jié)點沒被干凈殺掉或者同機多開了多個實例搶同一個端口。Erlang 節(jié)點被 CtrlC 強制中斷時監(jiān)聽 Socket 可能還沒釋放處于 TIME_WAIT 狀態(tài)。解決用netstat -ant | grep 7000找到占用進程的 PIDkill -9清掉再啟動。正式環(huán)境建議在啟動腳本里先做端口檢查起服前自動清理殘留進程而不是等報錯了才處理。5.2 數(shù)據(jù)庫與登錄鏈路socket 握手、token 交換與內(nèi)存膨脹現(xiàn)象啟動后日志報ERROR 2002 (HY000): cant connect to local MySQL server through socket /tmp/mysql.sock數(shù)據(jù)庫連接池一直初始化失敗。原因連接配置里 host 寫成了localhostmysql 驅(qū)動會優(yōu)先走 unix socket 而不是 TCP但 socket 文件的位置不一定在/tmp/mysql.sock。有些 MySQL 發(fā)行版把 socket 放在/var/run/mysqld/mysqld.sock路徑對不上自然握手失敗。這一步和前面配置表里的 host 要求是呼應(yīng)的。解決連接參數(shù)里把 host 改成127.0.0.1強制走 TCP如果想繼續(xù)用 socket先執(zhí)行SHOW VARIABLES LIKE socket;查到真實路徑再填到配置里。記得同時確認 MySQL 服務(wù)本身是啟動狀態(tài)systemctl status mysql一眼能看出來?,F(xiàn)象客戶端登錄報login server error: token exchange failed: error sending request for url ...有時還帶token endpoint returned status 40x。原因login server 調(diào)用 auth 服務(wù)時網(wǎng)絡(luò)請求失敗或者 auth 服務(wù)返回了錯誤狀態(tài)碼。40x 說明請求本身有問題最常見的是 URL 里拼接的 account 參數(shù)帶了非法字符、簽名密鑰不一致網(wǎng)絡(luò)錯誤則指向 URL 寫錯、auth 服務(wù)沒啟動、超時設(shè)太短三選一。解決先手動 curl auth 服務(wù)的地址看服務(wù)本身通不通再檢查 login server 配置里的 auth 服務(wù) IP、端口、路徑最后把 httpc 超時從默認值調(diào)到 5000 毫秒以上。這里有個血淚教訓(xùn)千萬別在沒確認 auth 服務(wù)存活的情況下改代碼先把請求鏈路用 curl 打一遍?,F(xiàn)象服務(wù)器運行兩三天后內(nèi)存漲到幾個 G玩家在線數(shù)沒增長但內(nèi)存一直不降erl shell 里查ets:info發(fā)現(xiàn)某張表 size 大得離譜。原因玩家下線時狀態(tài)沒清理干凈ETS 表里積累了大量離線數(shù)據(jù)。還有一種情況是 ETS 表的所有者進程崩潰后表數(shù)據(jù)沒有 heir 接管導(dǎo)致內(nèi)存無法釋放??ㄅ朴螒蛲婕疑舷戮€頻繁這個問題幾乎每個長跑服都會撞上。解決打開 observer 定位大表observer:start()后切到 Table Viewer 按 size 排序找到問題表后在玩家 logout 邏輯里補ets:delete或者給表設(shè)置{heir, ...}讓 owner 進程崩潰時數(shù)據(jù)能被其他進程接管。日常運維建議腳本定期掃描 ETS 表大小超過閾值就告警?,F(xiàn)象rebar get-deps執(zhí)行失敗提示拉取依賴超時或找不到倉庫編譯卡在依賴階段。原因早期項目的 deps 依賴一般指向 GitHub 上的倉庫地址時間長了倉庫可能改名、遷移或刪除舊 URL 失效很常見。另外老版本 rebar 對 git 協(xié)議的兼容性也差容易在 clone 階段翻車。解決優(yōu)先看 deps 目錄里是否已經(jīng)帶了依賴源碼帶了就直接注釋掉 rebar.config 里對應(yīng)的依賴項改用本地 deps 目錄編譯。第二種辦法是手動去倉庫 clone 對應(yīng)版本放到 deps 目錄下。最省事的是改 rebar.config把依賴地址換成當(dāng)前可訪問的新地址版本號保持不變。6. 進階驗證用 observer 和 tcpdump 給服務(wù)器做一次體檢服務(wù)器能啟動、能登錄只是起點真正判斷這套原始碼「能不能扛事」還要看運行期的進程狀態(tài)和網(wǎng)絡(luò)行為。我一般會給服務(wù)器做兩件事用 observer 看進程和內(nèi)存用 tcpdump 驗證協(xié)議幀。observer 是 Erlang 自帶的圖形化監(jiān)控工具啟動后在 erl shell 里執(zhí)行observer:start().它會打開一個圖形界面能看到整個 supervision 樹、每個進程的內(nèi)存占用、ETS 表大小。重點看兩處System 頁里的內(nèi)存曲線以及 Table Viewer 里各張 ETS 表的記錄數(shù)。如果某張表的數(shù)據(jù)量在玩家離線后不下降說明清理邏輯有遺漏趁早補別等內(nèi)存爆了再救。tcpdump 用來驗證客戶端和 login server 之間真實的協(xié)議幀tcpdump -i any port 7000 -w login.pcap抓完包用 Wireshark 打開對照 login_server.erl 里的接收邏輯看幀格式。比如代碼里用gen_tcp:recv按二進制模式收包幀頭、長度、命令字、參數(shù)各占幾個字節(jié)都能在 pcap 里對上。這一步能驗證客戶端發(fā)來的包是不是符合預(yù)期也能反向確認協(xié)議解析代碼沒寫錯。如果想模擬登錄請求可以寫一個小的 Erlang 客戶端腳本做冒煙測試-module(smoke). -export([run/0]). run() - {ok, S} gen_tcp:connect(127.0.0.1, 7000, [binary, {active, false}]), %% 幀格式按 login_server.erl 中 recv 分支實際解析規(guī)則填寫 gen_tcp:send(S, 16#AA, 16#01, test001), {ok, Data} gen_tcp:recv(S, 0, 5000), io:format(resp: ~p~n, [Data]).幀頭16#AA和命令字位置要按源碼里實際的協(xié)議定義來填別照抄。返回數(shù)據(jù)能解析出 ticket 或錯誤碼說明套接字鏈路和邏輯鏈路都是通的。以前我拿到老代碼總想著一步跑起來結(jié)果每次都在版本和配置上浪費一整晚后來養(yǎng)成一個習(xí)慣拿到源碼先看 rebar.config 定版本再掃一遍所有配置文件的 IP、端口、cookie最后才啟動。按這個順序走啟動階段基本不再翻車。希望幫到你。本文還有配套的精品資源點擊獲取