1. 預警機器人的定義
預警機器人就是當線上有任何錯誤發生時,它會把錯誤信息以某種形式通知到某處。
2. 思路
2.1 報錯攔截
在 skynet 構建的系統中,報錯一般是 lua 引起的,比如 attemp index a nil value, 這些報錯的位置雖然分散在成百上千個文件裏,但入口其實非常有限,我們只要在入口處攔截掉這些報錯,就能把錯誤信息發送到出去。
以下列出不同業務類型其報錯的入口點:
函數類型 | 入口點 | 底層代碼位置 |
---|---|---|
skynet.dispatch | dispatch 函數自身 | |
skynet.fork | error | skynet.lua Line 184, 607行resume後協程執行報錯,協程中斷,不會直接報錯,只是結束協程並讓出執行權,resume返回nil傳到suspend內,184行調用error |
skynet.timeout | assert | skynet.lua Line 617, 這個跟普通消息一樣都是走 dispatch_message, pcall+ assert |
snax 服務 | assert | snaxd.lua Line 46 |
可以看到,除了 dispatch 外,其他幾類都在 assert/error 裏,所以我們只需要改寫 assert/error 函數,並在 dispatch 裏使用 pcall 把真正的業務函數包起來,再把結果送到 assert 或 error 裏,整個錯誤攔截就完成了。
之所以這麼簡單,還是得益於雲風良好的編碼風格,幾乎底層在執行上層業務函數時,都是 pcall 包起來,再由 assert/error 拋出這麼個模式。
pcall+assert 示例代碼:
local ok, err = pcall(f, ...)
assert(ok, err)
2.2 服務設計
我們需要創建一個專門發錯誤信息的服務,在改寫 assert/error 時,把錯誤信息統一轉發到這個服務,再由這個服務處理。
給這個服務起名叫 error_monitor 吧,這裏面只做一件事,接收其他服務發來的報錯信息,整理一下(帶上集羣名和節點ID),發到目的地即可。
2.3 錯誤信息發送目的地
攔截到錯誤信息後,想發到哪都可以,我們不弄太複雜,直接用釘釘機器人。不熟悉的可以看看這裏。
發送的方式也不打算用 libcurl, 而是直接用終端裏的 curl, os.execute(“curl xxxx”), 這裏只是爲了簡單,因爲假設我們的報錯信息不是很多,用這種方式也完全夠用了。如果你覺得你們的報錯很多,可以改用 libcurl, 當然這樣30行代碼搞不定了。
或者如果不想在遊戲進程內處理,也可以把報錯信息發到一個類似 rabbitmq
這樣的消息隊列裏,再在外面起一個進程去消息這些報錯。
3. 實現
3.1 實現 error_monitor 服務
local skynet = require "skynet"
skynet.start(function()
local worldName = skynet.getenv("world_name") or "Unknown(Please add world_name to config)" -- worldName 也就是集羣名,用來判斷是哪個服務器,默認可以用 require("skynet.cluster.core").nodename()
local url = "https://oapi.dingtalk.com/robot/send?access_token=xxxxx" -- 後面的xxx改成自己機器人的token
local selfnodeid = 1 -- 自己的節點id
skynet.dispatch("lua", function(_,_,...)
os.execute(string.format([[curl '%s' -H 'Content-Type: application/json' -d '{"msgtype":"markdown","markdown":{"title":"ERROR","text":"* World: %s\n* Node: %d\n* Traceback:\n`%s`"}}']],
url, worldName, selfnodeid, select("#",...)>1 and table.concat({...}," ") or tostring(...)))
end)
end)
3.2 改寫 assert/error
function util.registerErrorMonitor()
local addr = skynet.uniqueservice("error_monitor")
sutil.registerServerLaunchCallback(function()
local _error = error
function error(...)
skynet.send(addr, "lua", ...)
_error(...)
end
local _assert = assert
function assert(...)
if not ... then
skynet.send(addr, "lua", ...)
end
return _assert(...)
end
end)
end
改寫完後,在需要監控報錯的服務的初始化那裏,調一下util.registerErrorMonitor
即可。
這裏不推薦把改寫函數放到 preload 裏去,因爲並不是所有服務都需要改寫的,一般我們只改幾個大的業務服務即可,再說也不能在 preload 裏調 uniqueservice。
最後不要忘記 skynet.dispatch 改成 pcall+assert 的模式,這樣纔可以把報錯拋出去,如果不想要重複寫這段代碼,可以簡單封裝一下:
function skynet_dispatch(typename, func)
skynet.dispatch(typename, function(session, source, cmd, ...)
assert(pcall, func, session, source, cmd, ...)
end)
end