MyBilibili 日志问题与解决方案

发布时间:2026/8/29 10:10:40
MyBilibili 日志问题与解决方案 MyBilibili 日志问题与解决方案1. 问题描述1.1 硬件约束MyBilibili 需要部署在弱设备上Android 盒子1GB RAMeMMC/SD 卡存储老电脑2-4GB RAMHDD无云服务器无外部存储1.2 eMMC/SD 卡写入寿命问题eMMC 和 SD 卡有写入寿命限制SLC 模式约 10 万次擦写MLC 模式约 3000-1 万次擦写TLC 模式约 500-3000 次擦写传统日志方案会频繁写入磁盘加速损耗每秒写入 100 条日志 × 每条 1KB 100KB/s一天写入100KB × 86400 8.6GB一个月8.6GB × 30 258GBeMMC 通常只有 8-32GB频繁写入会导致存储空间快速耗尽eMMC 寿命缩短设备变慢或损坏1.3 需求减少磁盘写入保护 eMMC/SD 卡寿命自动清理日志不能无限增长可配置不同环境选择不同策略可调试保留足够的调试信息审计保留支付等关键操作必须持久化2. 日志方案对比2.1 方案列表方案实现方式磁盘写入自动清理复杂度适用场景journald volatile改一行配置❌ 内存✅ 写满清理最低系统日志logrotate 轮转改配置文件✅ 但可控✅ 按大小/时间低应用日志Go 环形缓冲区写 50 行代码❌ 内存✅ 覆盖旧数据中调试日志lumberjackGo 第三方库✅ 但可控✅ 自动轮转低应用日志tmpfs 脚本挂载内存盘 清理脚本❌ 内存⚠️ 需要脚本中临时日志只写聚合指标应用层实现❌ 内存✅ 内存聚合中监控数据远程日志发到外部服务器❌ 本地不写✅ 外部存储高生产环境2.2 方案详细说明方案 1journald volatile原理systemd-journald 支持将日志存储到内存tmpfs不写磁盘。配置# /etc/systemd/journald.conf Storagevolatile RuntimeMaxUse50M RuntimeMaxFileSize10M优点最简单改一行配置系统集成稳定可靠写满自动清理最旧的日志缺点只管系统日志不管应用日志重启后日志丢失效果日志存储在 /run/log/journaltmpfs最多使用 50MB 内存写满后自动清理最旧的日志方案 2logrotate 轮转原理Linux 标准日志轮转工具按大小/时间轮转日志文件。配置/var/log/mybilibili/*.log { daily rotate 3 size 10M missingok notifempty compress }优点标准 Linux 工具稳定可靠支持按大小、时间、数量轮转支持压缩旧日志缺点写磁盘只是控制大小需要配置文件效果日志按大小轮转最大 10MB最多保留 3 个旧文件超过 3 天自动删除方案 3Go 环形缓冲区原理应用内维护固定大小的环形缓冲区写满自动覆盖最旧的日志。实现typeRingLoggerstruct{entries[]LogEntry sizeintheadintcountintmu sync.Mutex}funcNewRingLogger(sizeint)*RingLogger{returnRingLogger{entries:make([]LogEntry,size),size:size,}}func(rl*RingLogger)Add(entry LogEntry){rl.mu.Lock()deferrl.mu.Unlock()rl.entries[rl.head]entry rl.head(rl.head1)%rl.sizeifrl.countrl.size{rl.count}// 写满自动覆盖最旧的不刷盘}func(rl*RingLogger)GetAll()[]LogEntry{rl.mu.Lock()deferrl.mu.Unlock()result:make([]LogEntry,rl.count)fori:0;irl.count;i{idx:(rl.head-rl.countirl.size)%rl.size result[i]rl.entries[idx]}returnresult}优点完全内存零磁盘写入写满自动覆盖最旧的不需要外部依赖缺点需要写代码重启后日志丢失内存占用固定效果内存里固定 1000 条日志写满后自动覆盖最旧的不刷盘零磁盘写入方案 4lumberjack原理Go 第三方日志轮转库自动按大小/时间轮转日志文件。实现importgopkg.in/natefinch/lumberjack.v2log.SetOutput(lumberjack.Logger{Filename:/var/log/myapp.log,MaxSize:10,// MBMaxBackups:3,MaxAge:7,// daysCompress:true,})优点Go 生态标准简单易用自动轮转按大小/时间清理支持压缩缺点写磁盘只是控制大小需要依赖第三方库效果日志按大小轮转最大 10MB最多保留 3 个旧文件超过 7 天自动删除支持 gzip 压缩方案 5tmpfs 脚本原理将日志目录挂载到内存盘tmpfs配合定时清理脚本。配置# 挂载 tmpfsmount-ttmpfs-osize50M tmpfs /var/log/mybilibili# /etc/fstab 永久生效tmpfs /var/log/mybilibili tmpfs defaults,size50M00# 定时清理脚本*/5 * * * *find/var/log/mybilibili-name*.log-size40M-delete优点内存存储零磁盘写入可定制清理策略灵活性高缺点需要维护脚本重启后日志丢失需要手动管理效果日志在内存里最多 50MB脚本定时清理超过 40MB 的文件不刷盘零磁盘写入方案 6只写聚合指标原理不写原始日志只在内存里统计聚合指标。实现typeMetricsstruct{countersmap[string]int64// 请求次数、错误次数gaugesmap[string]float64// 延迟、队列长度mu sync.RWMutex}func(m*Metrics)Increment(keystring){m.mu.Lock()m.counters[key]m.mu.Unlock()}func(m*Metrics)Set(keystring,valuefloat64){m.mu.Lock()m.gauges[key]value m.mu.Unlock()}// 定时导出到 Prometheusfunc(m*Metrics)ExportToPrometheus(){// 暴露 /metrics 端点}优点零磁盘写入只保留最有价值的数据适合监控和告警缺点没有原始日志调试困难需要额外的监控系统Prometheus/Grafana效果只保留统计指标QPS、错误率、延迟通过 Prometheus/Grafana 查看不写日志文件方案 7远程日志原理将日志发送到外部日志服务器本地不保留。实现// 发送到远程日志服务器typeRemoteLoggerstruct{endpointstringclient*http.Client}func(l*RemoteLogger)Info(msgstring,fields...Field){entry:LogEntry{Time:time.Now(),Level:INFO,Msg:msg,Fields:fields}data,_:json.Marshal(entry)l.client.Post(l.endpoint,application/json,bytes.NewReader(data))}优点本地零写入集中管理便于搜索和分析可以保留长期历史缺点需要外部服务器网络依赖需要额外成本效果日志发到外部存储本地不保留集中搜索和分析3. 可插拔日志系统设计3.1 设计原则接口定义定义标准日志接口多实现每个接口有多个实现配置驱动通过配置文件选择实现运行时切换改配置就能切换实现不用改代码3.2 接口定义// Logger 日志接口typeLoggerinterface{Debug(msgstring,fields...Field)Info(msgstring,fields...Field)Warn(msgstring,fields...Field)Error(msgstring,fields...Field)Close()error}// Field 日志字段typeFieldstruct{KeystringValueinterface{}}// LoggerConfig 日志配置typeLoggerConfigstruct{Typestringyaml:type// ringbuffer / file / journald / remote / compositeLevelstringyaml:level// debug / info / warn / errorConfigmap[string]interface{}yaml:config}3.3 多实现实现类型适用场景配置参数RingBufferLogger内存eMMC/SD 卡担心寿命size缓冲区大小FileLogger磁盘SSD/硬盘不担心寿命path, maxSize, maxBackups, maxAgeJournaldLogger系统systemd 环境无RemoteLogger网络有外部日志服务器endpoint, timeoutCompositeLogger组合需要多种策略loggers子日志器列表3.4 工厂函数funcNewLogger(config LoggerConfig)(Logger,error){switchconfig.Type{caseringbuffer:returnNewRingBufferLogger(config.Config)casefile:returnNewFileLogger(config.Config)casejournald:returnNewJournaldLogger(config.Config)caseremote:returnNewRemoteLogger(config.Config)casecomposite:returnNewCompositeLogger(config.Config)default:returnnil,fmt.Errorf(unknown logger type: %s,config.Type)}}3.5 配置示例RingBufferLoggereMMC 设备logging:type:ringbufferlevel:infoconfig:size:1000# 缓冲区大小条数format:json# 输出格式FileLoggerSSD/硬盘logging:type:filelevel:infoconfig:path:/var/log/mybilibili/app.logmaxSize:10# MBmaxBackups:3maxAge:7# dayscompress:trueJournaldLoggersystemd 环境logging:type:journaldlevel:infoconfig:{}RemoteLogger有外部服务器logging:type:remotelevel:infoconfig:endpoint:http://log-server:9200timeout:5sCompositeLogger组合策略logging:type:compositelevel:debugconfig:loggers:-type:ringbufferlevel:debug-type:filelevel:errorconfig:path:/var/log/mybilibili/error.log4. 推荐方案4.1 按环境选择环境推荐方案原因Android 盒子eMMCringbuffer零磁盘写入保护寿命老电脑HDDfile logrotate标准做法稳定云服务器SSDfile lumberjack不担心寿命生产环境remote composite集中管理本地备份开发环境journald简单系统集成4.2 分层日志策略日志类型存储位置持久化说明调试日志内存ringbuffer❌调试用重启丢弃错误日志磁盘file✅保留用于排查问题访问日志内存 采样❌只采样 1% 的请求审计日志支付等磁盘file✅必须持久化用于对账指标数据内存 Prometheus❌聚合统计定时导出4.3 配置示例Android 盒子logging:type:compositelevel:infoconfig:loggers:# 调试日志内存环形缓冲-type:ringbufferlevel:debugconfig:size:1000# 错误日志持久化到磁盘-type:filelevel:errorconfig:path:/var/log/mybilibili/error.logmaxSize:10maxBackups:3compress:true# 审计日志持久化到磁盘-type:filelevel:infoconfig:path:/var/log/mybilibili/audit.logmaxSize:50maxBackups:10compress:true5. 实现步骤5.1 Phase 1基础接口定义 Logger 接口实现 RingBufferLogger最简单实现 FileLogger用 lumberjack5.2 Phase 2更多实现实现 JournaldLogger实现 RemoteLogger实现 CompositeLogger5.3 Phase 3配置集成配置文件解析工厂函数全局日志实例5.4 Phase 4测试单元测试集成测试性能测试6. 总结问题eMMC/SD 卡写入寿命有限传统日志方案会加速损耗方案多种日志方案可选从零磁盘写入到完全持久化设计可插拔日志系统配置驱动部署者自由选择推荐Android 盒子用 ringbuffer老电脑用 file logrotate云服务器用 file lumberjack分层调试日志用内存错误/审计日志持久化指标数据聚合导出