服务录像与重放

前言

对于复杂的业务逻辑,出现的bug,通过日志手段或者review代码都没法查明原因的情况下,这时候我们可以通过服务录像重放来还原整个事故现场。

实现方案

skynet是多线程的,不太方便对整个进程进行录像,而对于skynet的单个服务,能保证消息是同步处理的,所以可以比较方便实现对于单个服务录像,可以同时录制多个服务。实现思路就是把所有消息记录下来,重放时读取记录,解析重放即可。
其中有一些细节点,比如session分配,socket_id分配,time时间,math.random随机数,hotfix热更,sharedata,sharetable共享配置处理,都需要一些特殊处理才能保证录像重放时确保一致。

  • session socket_id skynet.time os.time
    这些只需要调用时,记录下值,重放时读取即可。

  • math.random 如何保证一致了
    只需要记录math.randomseed(x,y)随机数种子,相同的种子,随机出来的值会是一致的。

  • hotfix 热更
    每一次热更,会把热更过的文件拷贝存在在recordpath对应serverId 关联的目录下,用于重放时能加载到修改后的文件。

  • sharedata,sharetable
    因为录像记录的是共享数据之前由sharedata或者sharetable返回的C指针,重放时地址数据并不存在,这种访问肯定会崩溃。解决方式有2种,一种方法就是启动对应sharedata,sharetable并加载数据,重放时重新去拿数据的指针,第二种方法就是直接使用loadfile加载配置,显然第二种方式更简单直接一些。

如何录制?

可热更服务在load_mods.lua配置中配置is_record_on = 1即可,recordlimit 是录像大小限制,超过即不再写入(针对单个录像文件的限制),recordpath指定录像存放目录。普通skynet服务需要自行接入录像。

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
return {
--共享配置
share_config_m = {
launch_seq = 1,
launch_num = 1,
default_arg = {
server_cfg = {
recordlimit = 1024 * 10,
recordpath = "../records",
},

redis = {
test = {
host = '127.0.0.1',
port = 6379,
auth = '123456',
db = 0,
}
}
},
},
A_m = {
launch_seq = 2,
launch_num = 1,
},
B_m = {
launch_seq = 3,
launch_num = 1,
is_record_on = 1, --录像
}
}

如何重放?

make/script/run.shmake/script/restart.sh中增加了 recordfile参数,播放录像时加上录像文件路径即可。
sh make/script/restart.sh load_mods.lua 0 records/00000010.record

重放注意点

  1. 确保代码环境一致,代码不一致可能会导致录像没有按预期回放,这时候通常会代码出错。
    比如说 skynet.time(),os.time(),math.randomseed,还有会创建sessionsocket_id的函数调用,少调或者多调用一次都会导致录像对不上,因为重放录像的时候每次都是去记录中拿对应的值,少或多都会导致顺序错乱。

  2. 逻辑代码中pairs调用可能导致重放逻辑乱序
    因为pairs每次调用并不保证顺序,如果在pairs循环操作分配了session(call,time_out),socket_id(socket.connect),os.time, skynet.time, math.randomseed,math.random,都会因为pairs遍历无法保证顺序而导致重放乱序出错。目前没有想到非常完美的办法来解决这个问题,强行重写pairs让其变成有序遍历,太过损失其性能。我们只好在需要重放录像的业务逻辑中,通过规避上述情况下的pairs使用

对hotfix的处理

对于录像的服务,我们可能通过check_hotfix.sh对其进行了多次脚本热更,重放的时候我们怎么保证也能重放热更过程,有2种办法,1是把热更文件写进录像,2是把热更文件CP一份,我选择了第二种方法,因为我们重放过程中,一般需要加一些打印日志,这样热更后的文件,我们也能去增加日志打印。

对于sharedata,sharetable 共享配置热更的处理

同hotfix处理一样,cp被热更的文件。

如何正确的重放

我们应该想办法让代码先回到启动前,通过借助svn,git这些版本控制工具,不过这需要使用者自己去保证了。

示例

2024/12/01 优化改动

命名优化

录像的文件命名改为 room_game_hall_m-1-1-20241205-180640.record

  • room_game_hall_m 可热更服务名
  • 1 启动版本号
  • 1 启动索引
  • 20241205-180640 启动时间

录像的热更记录路径命名改为 hotfix_room_game_hall_m-1-20241205-194136\patch_20241205-204136\
共享配置数据的热更记录路径命名改为 sharedata_room_game_hall_m-1-20241205-194136\patch_20241205-204136\

  • 1 启动版本号
  • 20241205-194136 启动时间
  • 20241205-204136 热更时间

录像文件的切割方式

由于录像文件必须从头开始跑,所以我们并不能像切割日志文件一样去切割录像文件。只能另辟蹊径了。得益于框架的reload热更机制,我们可以定时每天reload自身服务,这样就开始记录新的录像,而旧的录像文件我们根据策略去删掉,比如用logrotate去整理历史的录像文件。这样一来,我们可以一直保留近期的录像文件,避免了过多占用系统磁盘资源,录像文件更小,也方便重放。

因此新增了可热更服务 2种机制配置

  • auto_reload 自动定时热更 skynet-fly.time_extend.time_point.lua 的配置项
  • record_backup 录像文件自动整理 需要启动logrotate_m
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
   --大厅服
room_game_hall_m = {
launch_seq = 6000,
launch_num = 6, --启动6个
is_record_on = 1, --录像

--自动定时热更 skynet-fly.time_extend.time_point.lua 的配置项
auto_reload = {
type = 3, --每天
hour = 5, --5点
min = 30, --30分
sec = 30, --30秒
},

--录像文件自动整理
--需要启动logrotate_m
record_backup = {
max_age = 3, --最大保留天数
max_backups = 50, --最大保留文件数
point_type = 3, --每天
hour = 5, --5点
sec = 20, --20
},

default_arg = {
hall_plug = "common.plug.hall_plug", --大厅加载的插件lua模块文件名
}
},

2024/12/22 优化改动

通过查看pairs的源码实现发现 pairs遍历string key 会依赖于 lua_state 创建时生成的strseed 种子。那么只需要录像时记录到strseed 种子。播放时设置好对应的strseed就能保证pairs 遍历 table string key 的顺序了。这样一来。对于number,string key都遍历顺序都能保证一致。
table, userdata 为 key 的没发保证顺序,因为hash是基于 table, userdata 的指针地址。 录像重复没发保证指针地址也一致。

测试

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
--测试 pairs遍历
local function test_pairs()
--测试 string key 播放时可以保证顺序
local t = {}
for i = 1, 10 do
t['i' .. i] = i
end
for k, v in pairs(t) do
log.info("string pairs:", k, v)
end

--测试number key 播放时可以保证顺序
local num_t = {}
for i = 1, 10 do
local num = math.random(1, 10000)
num_t[num] = i
end

for k,v in pairs(num_t) do
log.info("number pairs:", k, v)
end

--测试table key 播放时不能保证顺序
local table_t = {}
for i = 1, 10 do
table_t[{}] = i
end
for k,v in pairs(table_t) do
log.info("table pairs:", k, v)
end
end

正常跑服务结果

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
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i1" 1 
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i2" 2
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i10" 10
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i9" 9
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i3" 3
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i4" 4
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i6" 6
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i5" 5
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i7" 7
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:140]"string pairs:" "i8" 8
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 6165 8
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 7622 4
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 8647 1
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 6339 6
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 5005 5
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 432 10
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 3777 9
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 2564 2
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 5714 7
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:151]"number pairs:" 6690 3
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
1
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
5
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
9
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
2
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
6
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
10
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
3
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
7
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
4
[:00000013][20241221 12:46:43 31][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
8

重放结果

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
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i1" 1 
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i2" 2
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i10" 10
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i9" 9
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i3" 3
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i4" 4
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i6" 6
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i5" 5
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i7" 7
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:140]"string pairs:" "i8" 8
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 6165 8
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 7622 4
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 8647 1
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 6339 6
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 5005 5
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 432 10
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 3777 9
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 2564 2
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 5714 7
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:151]"number pairs:" 6690 3
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
3
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
7
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
4
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
8
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
5
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
9
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
1
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
6
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
10
[:00000013][20241221 12:46:43 20][info][B_m][./module/B_m.lua:160]"table pairs:" {
}
2

结论

可以发现number和string重放是一致的。


服务录像与重放
https://huahua132.github.io/2024/10/27/skynet_fly_word/word_3/S_record/
作者
huahua132
发布于
2024年10月27日
许可协议