添加自动文件压缩的实现

This commit is contained in:
yun
2026-09-13 21:53:37 +08:00
parent ab11fa71a0
commit 316c6420a7
21 changed files with 2037 additions and 66 deletions
+106 -19
View File
@@ -11,6 +11,8 @@ import (
"log"
"os"
"sync"
"sync/atomic"
"time"
// "sync_log/global"
@@ -28,6 +30,7 @@ type Logger struct {
writeType writeType // 是否异步落盘,这里作用范围是本条,优先判断这里
closeOnce *sync.Once
closed *atomic.Bool // 是否已经关闭:关闭后再写不再新开句柄
done chan struct{} // 通用关闭信号(delete / ctx 相关 goroutine 用)
workerDone chan struct{} // 异步消费 goroutine 退出信号
closing chan struct{} // 异步投递关闭信号:只关一次
@@ -36,6 +39,16 @@ type Logger struct {
// 浅拷贝,如果状态直接放在 Logger 里,拷贝出的实例就会各看各的
// (曾经因此让 Close 看不到真正的队列,白等一场、最后一条日志丢失)
async *asyncCtl
// basePath 缓存(见 format.go basePath),避免每条日志都算一次 filepath.Abs
basePathOnce *sync.Once
basePathVal string
// 归档压缩相关状态(同样挂在指针后面供拷贝共享)
idxOnce *sync.Once // 归档序号只从目录里初始化一次
idxMu *sync.Mutex // 保护 idx
idx int // 归档序号
compWg *sync.WaitGroup // 后台压缩协程计数,Close 需要等它们收尾
}
// asyncCtl 异步队列的共享控制块:所有拷贝共享同一份
@@ -43,6 +56,17 @@ type asyncCtl struct {
mu sync.Mutex
ch chan cacheData
closed bool // 只能读写于 mu 保护下
wg sync.WaitGroup
}
// pendingAsync 记录「已通过投递检查、正在往队列里放」的写入数量。
// 调用方必须持有 mu,Done 必须紧跟在 mu 解锁之后(见 toAsync)
func (c *asyncCtl) begin() {
c.wg.Add(1)
}
func (c *asyncCtl) end() {
c.wg.Done()
}
// fileKey 文件缓存的键
@@ -65,16 +89,21 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
}
l := &Logger{
ctx: ctx,
filePath: make(map[fileKey]*logFile),
mu: &sync.RWMutex{},
writeMu: &sync.Mutex{},
option: opt,
writeType: writeTypeDefault,
closeOnce: &sync.Once{},
done: make(chan struct{}),
workerDone: make(chan struct{}),
async: &asyncCtl{},
ctx: ctx,
filePath: make(map[fileKey]*logFile),
mu: &sync.RWMutex{},
writeMu: &sync.Mutex{},
option: opt,
writeType: writeTypeDefault,
closeOnce: &sync.Once{},
closed: &atomic.Bool{},
done: make(chan struct{}),
workerDone: make(chan struct{}),
async: &asyncCtl{},
basePathOnce: &sync.Once{},
idxOnce: &sync.Once{},
idxMu: &sync.Mutex{},
compWg: &sync.WaitGroup{},
}
log.SetOutput(l)
@@ -89,6 +118,11 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
// 日志删除
go l.delete()
// 定时刷盘
if opt.flushEvery > 0 {
go l.flushLoop()
}
// 强制刷盘
// 用 done 而不是只监听 ctxctx 为 Background 时 Close 也要能把 goroutine 收回去
go func() {
@@ -102,30 +136,78 @@ func NewLogger(ctx context.Context, opts ...Option) *Logger {
return l
}
// flushTick 定时刷盘的检查频率
// 实际刷盘间隔由 SetFlushInterval 决定;这里用固定的小 tick 做检查,
// 这样即使调用方设了很小的间隔,也不会因为「每个 tick 都抢全局写锁」而拖垮吞吐。
// 写锁是全局的,高频抢锁会直接顶住所有写入方
const flushTick = 50 * time.Millisecond
// flushLoop 定时把内存缓冲刷到磁盘
// 作用是把「进程崩溃时可能丢的数据量」从「最多一整个 32KB 缓冲」
// 缩小到「一个刷盘间隔内产生的日志量」
func (l *Logger) flushLoop() {
tick := time.NewTicker(flushTick)
defer tick.Stop()
last := time.Now()
for {
select {
case <-tick.C:
interval := l.option.flushEvery
if interval <= 0 {
continue
}
if now := time.Now(); now.Sub(last) >= interval {
last = now
// 单个文件刷盘失败不应该让这个循环退出
if err := l.MustSync(); err != nil {
log.Println("loggerx: 定时刷盘失败:", err)
}
}
case <-l.done:
return
}
}
}
// 关闭日志:关闭异步队列、落盘并关闭所有文件句柄
// 重复调用安全,调用后本实例的后台 goroutine 会全部退出
func (l *Logger) Close() error {
var err error
l.closeOnce.Do(func() {
// 1. 停投递 + 等消费协程把队列里已入队的任务全部写完
// 1. 停投递 + 等消费协程把队列里已入队的任务全部写完
// 这一步必须在「封盘」之前:队列里可能还有别的 channel / event 的任务,
// 它们需要新开自己的文件句柄。如果提前封盘,这些任务会直接写失败被丢掉
l.drainAsync()
// 2. 在写锁内完成最后一次刷盘与关闭
// 此后不会再有写入(投递已关闭,同步写入也要先抢到这把锁)
l.writeMu.Lock()
syncErr := l.MustSync()
closeErr := l.close()
l.writeMu.Unlock()
err = joinErrors([]error{syncErr, closeErr})
// 2. 封盘:此后不再新开句柄,避免 Close 扫完句柄表之后又冒出没人负责关的句柄
l.closed.Store(true)
// 3. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出
// 3. 在写锁内完成最后一次刷盘并关闭所有句柄。此时无锁等待,不会和谁抢锁
l.writeMu.Lock()
err = joinErrors([]error{l.mustSyncLocked(), l.close()})
l.writeMu.Unlock()
// 4. 等后台压缩协程收尾:不等的话 Close 返回后去读归档会读到半截 .gz
l.compWg.Wait()
// 5. 最后再发通用关闭信号,让 delete / ctx 相关 goroutine 退出
close(l.done)
})
return err
}
// 强制刷盘:只 flush 缓存数据,文件继续可写
// 与写入并发调用是安全的(内部会先拿到写锁)
func (l *Logger) MustSync() error {
l.writeMu.Lock()
defer l.writeMu.Unlock()
return l.mustSyncLocked()
}
// mustSyncLocked 调用方必须持有 writeMu
// 缓冲的 pending 与 buf 只有在写锁内访问才安全,否则会和 store 并发改动同一块内存
func (l *Logger) mustSyncLocked() error {
l.mu.RLock()
files := make([]*logFile, 0, len(l.filePath))
for _, f := range l.filePath {
@@ -135,6 +217,11 @@ func (l *Logger) MustSync() error {
var errs []error
for _, f := range files {
// 没有待落盘数据的文件直接跳过:省掉一次 fsync。
// Close 仍然安全:logFile.Close 自己会再 Flush 一次并关句柄
if !f.hasPending() {
continue
}
errs = append(errs, f.Sync())
}
return joinErrors(errs)