基于共享内存的 LOG 异步化

游戏开发 Jan 10, 2024

原有日志接口的问题

  • 写日志耗时高

    • 实时写入文件单行 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 保存 保存 保存

标签