前言:
有段时间没写博客了,这次写一篇水文,关于 golang log为什么加锁 ? 很水么? 对的…
有天同事跟我说,原来go log在写入的时候也有锁,我寻思不应该呀,为啥加锁呀。在我想来写个日志没必要加锁, 但事实来说不是这样的。 好多一些语言的日志模块也有加锁,比如 python logging 。 他加锁的目的在于 避免logrotate日志切割的时候,多线程发生冲突。 我们需要说明的是,单纯的写日志不需要加锁的,因为写日志采用了文件的O_APPEND模式,原子方式一直追加后面。
原因
那么golang 标准库里的log为啥加锁? 看下面的代码,我们在调用log.Print的时候,其实在调用Output。 output方法里有各种行为加锁,总结一句话,他写日志的对象是共享的,而不是每条日志分别一个Logger.buf 对象,既然不是分离的,那么要保护好buf了,要不然写串了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# xiaorui.cc func (l *Logger) Output(calldepth int, s string) error { now := time.Now() // get this early. var file string var line int l.mu.Lock() defer l.mu.Unlock() if l.flag&(Lshortfile|Llongfile) != 0 { // Release lock while getting caller info - it's expensive. l.mu.Unlock() var ok bool _, file, line, ok = runtime.Caller(calldepth) if !ok { file = "???" line = 0 } l.mu.Lock() } l.buf = l.buf[:0] l.formatHeader(&l.buf, now, file, line) l.buf = append(l.buf, s...) if len(s) == 0 || s[len(s)-1] != '\n' { l.buf = append(l.buf, '\n') } _, err := l.out.Write(l.buf) return err } func (l *Logger) Printf(format string, v ...interface{}) { l.Output(2, fmt.Sprintf(format, v...)) } func (l *Logger) Print(v ...interface{}) { l.Output(2, fmt.Sprint(v...)) } |
Logger 结构体对象
1 2 3 4 5 6 7 8 9 |
# xiaorui.cc type Logger struct { mu sync.Mutex // ensures atomic writes; protects the following fields prefix string // prefix to write at beginning of each line flag int // properties out io.Writer // destination for output buf []byte // for accumulating text to write } |
日志加锁操作,听起来没啥问题,但如果你的日志很疯狂输出,那么问题就来了,这么多的syscall对于性能来说很是有杀伤力。 当然,正常线上系统肯定也没人去疯狂的benchmark日志,我这边只是为了测试说明问题,syscall 是有 代价的。
下面是阿里云四核主机的cpu占用比
pprof的火焰图分析也是runtime.futex高,剩下的就是syscall Write了。
总结
没啥总结的。 只是好奇go log为啥频繁加锁而已,算是给我解除疑惑了吧。
有疑问加站长微信联系(非本文作者)