在Golang项目中,日志是排查问题、监控运行状态的核心依据,而日志轮转功能可以避免单个日志文件过大导致读写困难,也能方便按时间或大小归档历史日志。但很多开发者直接使用第三方日志库默认配置时,会发现高并发写入场景下日志轮转会拖慢整体性能,甚至出现日志丢失、写入阻塞的问题。

日志轮转性能损耗的常见原因
要优化日志轮转性能,首先需要明确哪些操作会带来性能损耗:
- 频繁的文件切换操作:每次轮转都需要关闭旧文件、创建新文件、重新打开文件句柄,这些系统调用在高并发下会消耗大量资源。
- 同步写入导致的阻塞:如果每次日志写入都直接刷盘,轮转时的文件操作会和正常写入竞争资源,导致整体写入延迟升高。
- 轮转判断的额外开销:每次写入都检查文件大小或时间是否符合轮转条件,会增加额外的计算逻辑,影响写入效率。
- 锁竞争问题:多协程同时写入日志时,如果轮转操作和写入操作共用同一把锁,会导致大量协程阻塞等待。
优化日志轮转性能的具体实践
1. 减少轮转时的文件操作次数
可以通过预创建轮转文件、复用文件句柄的方式减少系统调用。比如提前创建下一个轮转周期的文件,轮转时只需要切换写入的文件句柄,不需要实时创建文件。
以下是一个简单的文件句柄切换示例:
package main
import (
"os"
"sync"
)
// LogWriter 日志写入结构体
type LogWriter struct {
mu sync.Mutex
current *os.File // 当前写入的文件句柄
next *os.File // 下一个轮转的文件句柄
fileSize int64 // 当前文件大小
maxSize int64 // 单个文件最大大小
}
// NewLogWriter 初始化日志写入器
func NewLogWriter(initFile, nextFile string, maxSize int64) (*LogWriter, error) {
current, err := os.OpenFile(initFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
next, err := os.OpenFile(nextFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
current.Close()
return nil, err
}
// 获取当前文件大小
stat, err := current.Stat()
if err != nil {
current.Close()
next.Close()
return nil, err
}
return &LogWriter{
current: current,
next: next,
fileSize: stat.Size(),
maxSize: maxSize,
}, nil
}
// Write 写入日志
func (w *LogWriter) Write(data []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
// 检查是否需要轮转
if w.fileSize+int64(len(data)) > w.maxSize {
// 切换文件句柄,不需要重新创建文件
w.current.Close()
w.current = w.next
// 重新打开新的下一个文件
newNext, err := os.OpenFile("log_next.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return 0, err
}
w.next = newNext
w.fileSize = 0
}
n, err := w.current.Write(data)
w.fileSize += int64(n)
return n, err
}
2. 优化日志写入逻辑,减少刷盘频率
默认情况下很多日志库会每次写入都调用Sync方法刷盘,这会极大降低性能。可以设置批量刷盘策略,比如每积累一定数量的日志或者每隔固定时间再刷盘,轮转时再强制刷盘一次即可。
批量刷盘的示例实现:
package main
import (
"bufio"
"os"
"time"
)
// BufferedLogWriter 带缓冲的日志写入器
type BufferedLogWriter struct {
file *os.File
writer *bufio.Writer
maxSize int64
curSize int64
ticker *time.Ticker
}
// NewBufferedLogWriter 初始化带缓冲的写入器
func NewBufferedLogWriter(filePath string, maxSize int64, flushInterval time.Duration) (*BufferedLogWriter, error) {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
stat, _ := file.Stat()
w := &BufferedLogWriter{
file: file,
writer: bufio.NewWriterSize(file, 4096),
maxSize: maxSize,
curSize: stat.Size(),
ticker: time.NewTicker(flushInterval),
}
// 定时刷盘
go func() {
for range w.ticker.C {
w.writer.Flush()
}
}()
return w, nil
}
// WriteLog 写入日志
func (w *BufferedLogWriter) WriteLog(data []byte) error {
// 先写入缓冲区
n, err := w.writer.Write(data)
if err != nil {
return err
}
w.curSize += int64(n)
// 接近最大大小时主动刷盘,避免轮转时数据丢失
if w.curSize > w.maxSize*9/10 {
w.writer.Flush()
}
return nil
}
// Rotate 轮转日志时强制刷盘
func (w *BufferedLogWriter) Rotate() error {
return w.writer.Flush()
}
3. 优化轮转判断逻辑,降低额外开销
不要每次写入都做完整的轮转判断,可以设置阈值,比如每写入10次日志再检查一次文件大小,或者只在缓冲区快满的时候检查,减少不必要的计算。
还可以将轮转判断和写入操作解耦,单独启动一个协程定期检查是否需要轮转,避免写入协程被轮转判断逻辑阻塞。
4. 减少锁竞争,分离轮转和写入的锁
可以使用读写锁,正常写入时加读锁,轮转时加写锁,这样多协程写入时不会互相阻塞,只有轮转的时候才会阻塞写入,大幅降低锁竞争的概率。
读写锁优化的示例:
package main
import (
"os"
"sync"
)
// RwLockLogWriter 使用读写锁的日志写入器
type RwLockLogWriter struct {
rwMu sync.RWMutex
file *os.File
maxSize int64
curSize int64
}
// Write 写入日志,加读锁
func (w *RwLockLogWriter) Write(data []byte) (int, error) {
w.rwMu.RLock()
defer w.rwMu.RUnlock()
n, err := w.file.Write(data)
w.curSize += int64(n)
return n, err
}
// Rotate 轮转日志,加写锁
func (w *RwLockLogWriter) Rotate() error {
w.rwMu.Lock()
defer w.rwMu.Unlock()
// 轮转逻辑
w.file.Close()
newFile, err := os.OpenFile("new_log.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
w.file = newFile
w.curSize = 0
return nil
}
优化效果对比
以下是优化前后的性能对比参考:
| 优化项 | 优化前QPS | 优化后QPS | 写入延迟降低比例 |
|---|---|---|---|
| 默认轮转配置 | 12000 | - | - |
| 减少文件操作+批量刷盘 | - | 28000 | 55% |
| 读写锁优化+阈值判断 | - | 35000 | 70% |
注意事项
优化时需要注意几个问题:批量刷盘可能导致程序崩溃时丢失部分未刷盘的日志,需要根据业务对日志可靠性的要求调整刷盘策略;预创建文件需要注意文件权限和磁盘空间,避免创建失败导致轮转异常;读写锁的使用要避免死锁,轮转操作完成后及时释放写锁。
实际项目中可以根据自身的日志写入量、可靠性要求灵活组合上述优化方案,在性能和可靠性之间找到平衡。