按时间线来看,
glog 地位如同达摩祖师,开分级日志和高性能日志先河,衣钵(代码)和精神(接口)分别被 klog,grpclog,logr 继承,一代宗师高山仰止。
logrus 可比做大侠郭靖,资质平平(性能和易用度都不算上佳),但承上启下(上接标准库下开结构化),是里程碑式的人物。
zap 就厉害了,如同武当祖师张三丰,泰山北斗,玄门正宗(优步巨作)。九派十二支开枝散叶,各路好手(公司/团队)争相传颂。
zerolog 可比明教教主张无忌,名门之后(网飞首席),青出于蓝而胜于蓝(自述受 zap 启发),是当世绝顶高手(性能和接口皆为一时之选)。
李杜文章在,光焰万丈长。但是,为何还有各路英雄豪杰(尤其国人)前仆后继络绎不绝发明日志库呢?当真是蚍蜉撼大树,可笑不自量?
作为一个资深的日志生产者和搬运工,卡红心经过深入的观察和自身的心得体会,得出 go 主流日志库四大“罪”
-
没必要的依赖
目前主流的日志库或多或少都有第三方依赖,一部分是单元测试引入的,一部分是使用第三方终端库,还有部分是引入额外的序列化库。前者可以回退到标准测试库实现,后者可以自己从头实现。我等只想记个日志而已,实在不值得 go get 时还引入了不必要的依赖。 -
欠佳彩色输出支持
由于开发者群体的关系,主流日志库比较关注 macOS 和 Linux 平台的彩色输出,Windows 平台(尤其是 Win7/Win8) 的支持欠佳。另外具体各项的色彩定制功能目前也是空白。 -
文件轮转和多分级文件的欠缺
日志轮转和多分级文件是常见的需求,目前主流日志库缺乏内置的支持,都需要借助 Hook 或第三方 Writer 实现,这造成了使用上的不便和额外的依赖。 -
繁琐的初始构造
由于上述种种,于是出现了一个奇特现象:我们只是想输出一条想要的日志,却在构造日志对象时候做了半天初始化的工作,就好像我只想累了睡觉,却不得不先跳半小时健身操才能上床一样。
这又导致很多大手子就会常备一个类似日志库脚手架一样的函数或结构,美其名曰:适当定制以后再用。
Enough is Enough,于是我们见证到,持续以来,无数国人揭竿而起,斩木为兵。可惜质量参差不齐,功能缺斤少两,文档黑屁滚滚(自指完美)。卡红心忍耐生老练,老练生感召,于是也下场造了个轮子,
卡红心在推特上曾有大言:“一直以来,go 都缺少一个性能出众,功能完备,实现简单,接口易用,文档清晰,质量上乘的(结构化)日志库”,眼高如此,恣意妄评,想必这库必然有一些自持之处:
-
性能出众
很显然,作为一个新入场的日志库,即使功能和设计做的再好,下场也是鲜有问津。 因为 logrus/zap 珠玉在前,深入人心。和它们比功能和设计,几乎没有人会买单。即使上一节我列举了这么多痛点,但是有谁真正在意呢?
路在何方,唯有念咒:“千破万破,唯快不破”,不过说起来容易,但是要比当世第一高手 zerolog 性能还高,可不是一件容易的事情。
主要做了两件小事:一是重新实现 Logger 时候尽量手动内联,减少函数调用开销。二是手动格式化时间戳(可把我累坏了),去掉在跑分中占比最高的一个开销。
性能获得优势之后,颇有先声夺人之效,剩下的只是按照自己的想法来完善这个新生日志库了。 -
功能完备
如上节所说,此库去除了三方依赖,自研了文件轮转、完美彩色支持、自定义格式、系统日志、多分级文件、异步日志、日志上下文、日志堆栈信息,以及和第三方日志接口的互操作。
功能玲琅满目、整整齐齐,大手子们如果来阅读代码,我相信会油然而生一股面面俱到的感觉。 -
实现简单
平坦胜于嵌套,以此为纲,此库没有出现类似自己引用自己的累赘之感,反而有一种一气呵成绝不拖泥带水的畅快,所有功能性代码写得非常直白,可谓唾手可得立等可取!这一点虽说对用户来说不是特别要紧,但恰恰是很多日志库忽视的。 -
接口易用
经典的日志库设计,一般是三层结构,Logger -> Formatter/Encoder -> Writer,虽说这种设计是堂堂之阵正正之旗,但是作为用户,用起来不免困惑:“我想要的那个功能选项在哪一层?”
痛定思痛,此库挥泪斩 Formatter,硬生生去掉了中间的转换层。让日志库的使用,变得直观且无脑(大嘘)!并且 Writer 是可以嵌套的,一些复杂的需求可以通过这个机制实现,具体请参考 multi.go 的代码。 -
文档清晰
作为一个严肃的日志库,清晰的文档必不可少。但是观察下来,即使是主流日志库也做的不算出色,大多在谈自己的优点、特色和动机。令人扼腕。
还好我在撰写 README 时候考量再三,基本把此库的接口和用法说清楚了,一直到忍到最后才得瑟了一下自己的性能。当然了,最后还是不能免俗,借这个文档暴论了一番。 -
质量上乘
自称质量上乘有两点,一是代码已经找过内部大手子的初步评审,普遍认为还行。二是测试覆盖率的话,看起来也不错。
卡红心对此库颇为自得,常把它比作笑傲江湖的令狐冲。虽初入江湖名不见经传,但是“以快打快”、“唯快不破”,假以时日,必然为日志届开一新气象。 不过同事豆良民不以为然,曾经点评道:“你这个顶多算是鸠摩智,到处偷学武功,但也似模象样。”,可谓一针见血!
此子当真已经恐怖如斯,傲绝天下了吗?其实不然,自黑如下:
-
FileWriter 的无锁化
Logger + FileWriter 是此库最常见的线上场景,Logger 的性能已经大杀四方,但是 FileWriter 其实略有遗憾。 虽然和主流的实现(lumberjack)一样的原理,但是 go 运行时已经对文件描述符加了锁,那么 FileWriter 自带的锁属实多余。 理论上是可以用无锁算法优化掉,事实上我也实现过一版,可惜实现跑起来优势很小(请翻看提交记录),真是遗憾。 -
ConsoleWriter 记录解析的低效
两层结构的日志库(Logger -> Writer)固然无脑,但是它把序列化的时间已经提前到 Logger 层(当然也这是 Logger 性能出众的原因)。导致如果有重新“排版”的需求,如美观的控制台输出,我们不得不再次反序列化然后按照特定的方式或用户提供的模板再序列化一次。 这个性能开销是明显的,很多内部用户和外部大手子向我指出这点。我一般这么辩解:“反正控制台输出这种也只是调试和开发的时候用用,线上不会因为这个引入性能影响。” 大部分时候这个说辞能勉强过关,但是毕竟反序列化的成本有点高,即使我已经使用了一个优化版本的解析器,但是性能还是不尽人意。 更新:随着进一步的优化,目前 ConsoleWriter 性能只稍逊 zap.ConsoleEncoder 了,大约是 zap 的 80% 左右,比 logrus/zerolog 是快出许多了。 -
Context 的显式和隐式
卡红心在推特上说过,目前主流的日志库的 Context 处理手法都不是面向性能的,对待全局性的需求还可以。但是请求级的,比如每次请求的用户地址和浏览器标识都不一样,使用隐式的 Context 会不可避免的造成额外的内存分配。 为此我选择使用显式的 Context 解决这个难题,Context 构造参数可以传入旧的 Context 起始内存地址,这样使用追加风格的序列化构造方法,实现了内存的复用,性能过关了。 不过显式的 Context 在很多场合显然不如隐式的好用,所以这点上和宣称的“接口易用”是相悖的。 -
Logger 性能悖论
平心而论,Logger 的性能其实在真实场景中并没有比其他的日志库高太多,比 zerolog 还要快一倍多属实是跑分的结果。线上观察的结果,大约在快 75% 左右。 那么,回到上节的第一个问题:“这一切值得吗?”,值得思索。
好吧,从头到脚都是破绽,但是这结束了么,并没有。 请允许我继续大放阙词,指点友商江山一番,
(此处还在琢磨如何下笔,一言不慎可能会被橄榄)
也许我们目前已经到达了 go 语言的日志库边界,但是随着 go 语言的自身发展,让我来推演一下日志库会有什么样的未来吧,
glog 现状是敬神而远之,但是它的继承者们 klog、grpclog、logr 有融合的迹象,考虑到它们和 go team 的渊源,很有可能推动出现一个官方版的 slf4j。对此我们来静候行程。
logrus 目前已经处于维护状态,并且受限自身资质,对其前途不必说太多。
zap 目前开发还算活跃,贡献者众多,但是上限早早就决定了,所以它正在成为“新一代的 logrus”,易用度和性能都在向 logrus 靠拢。
zerolog 是我比较偏爱的一个库,虽然相对小众,但是我还是对其默默祝福。
至于国人出品的日志库,还是会像雨后春笋一样割而复生,对其评价,我只能表示请参见上节内容。
随着泛型的落地,结构化日志库的接口必然会出现崭新的设计,这一过程会不会涌现出一个个新型日志库?让我们拭目以待!