基于共享内存的 LOG 异步化
原有日志接口的问题
-
写日志耗时高
- 实时写入文件单行 LOG,耗时约 0.25ms(1KB 以内的 LOG 长度并不会显著的影响写入文件的耗时,固态硬盘和机械硬盘也没有显著的差异)
-
异常时日志丢失
- 缓存型日志,单文件 4K 缓存,可以极大的减少写入文件的开销,但是宕机时,可能导致文件内容丢失
解决方案
-
降低写日志开销
- 异步化
-
防止日志丢失
- 共享内存
具体实现
- 数据结构:
typedef struct AsyncLogInfo_s
{
ASYNC_LOG_OPT_TYPE nOpt; // 操作类型
DWORD dwData2; // 自定义数据
int64_t i64Data1; // 自定义数据
uint64_t uIdx; // 块索引
char szFileName[ASYNC_LOG_FILENAME_MAX_LEN];
char szLog[ASYNC_LOG_CONTENT_MAX_LEN];
}AsyncLogInfo_t;
- 使用共享内存,并映射到文件。将内存切块使用对象池管理。
- 内存块增加块索引用来记录顺序,增加标记用来标示是否已用。线程申请内存块时,生成块索引。同时线程写入日志后,将内存块标记为使用,日志线程将日志写入文件后,将内存块标记为空闲并放回对象池。
- 若游服宕机,重启后加载同一个共享内存文件,根据标记和块索引重建对象池和工作队列,启动日志线程。
- 对于业务线程来说,写日志的 API 未发生改变,只需照常使用即可。
- 对于日志线程来说,无论是游服正常开服、正常运行还是宕机恢复,要做的都是从工作队列中读取内存块,写入文件,标记为空闲并放回对象池。
- 一个反直觉的实验结果:
C++ 标准 API:ofstream
比 C 的 API fwrite
写入性能要更高。
根据上述结果,我们将文件写入接口从 fwrite
改造成了 ofstream
。
- 开服时进行以下操作(以下为伪代码):
list<AsyncLogInfo_t> lstInfo = AllocFromShrdMem(4*1024*sizeof(AsyncLogInfo_t), "shrmem.dat")
for info in lstInfo :
if info.nOpt == OPT_NONE:
AddToObejctPool(&info)
else:
AddToPendingList(&info)
SortPendingList([](lhs, rhs)->bool { return lhs->uIdx < rhs->uIdx; })
StartLogThread()
- 原有的
MyLogSave
等接口改造为:
void MyLogSave(const char* pszName, const char* fmt, ...)
{
//... 原有格式化log逻辑...
// 获取内存块
Lock(&objectPool)
info = objectPool.BorrowObject()
info.uIdx = objectPool.SpawnNewIdx()
UnLock(&objectPool)
//将日志内容填充内存块
FillInfo(&info)
//放入工作队列
Lock(&pendingList)
AddToPendingList(&info)
UnLock(&pendingList)
}
日志线程的逻辑
while(true)
{
Lock(&pendingList)
list<AsyncLogInfo_t> lstTmpInfo = GetAllInfoFromPendingList(&pendingList)
UnLock(&pendingList)
for info in lstTmpInfo:
WriteToFile(info.szFileName, info.szLog)
info.nOpt = OPT_NONE
Lock(&objectPool)
objectPool.ReturnObject(&lstTmpInfo)
UnLock(&objectPool)
}
优化结果对比
- 性能对比(压测环境)
耗时(单次) | |
---|---|
修改前 | 0.0123 ms |
修改后 | 0.0069 ms |
修改前(实时写) | 修改前(缓存写) | 修改后(异步化) | |
---|---|---|---|
S1 | 丢失 | 丢失 | 丢失 |
S2 | / | 丢失 | 保存 |
S3 | 保存 | 保存 | 保存 |