分布式日志管理:从单体最佳实践到云原生范式演进
最近在维护一个小项目时遇到了个有趣的问题,走了一遍单体到云原生的日志演化路线。
事情是这样的:花糕的手头有个项目,最开始就是个简单的服务,单机部署。这种简单的项目我并不想扔进k8s,不然还要容器化,写deployment,太麻烦了,我就选择了最简单的部署方式————单机部署,部署程序选择了 pm2。
单机部署
最开始的日志采集方案特别朴实:项目根目录下创建个 logs 文件夹,用 winston 配置好日志轮转,按大小和时间分割文件。
这种方案的好处是显而易见的:简单、直接、自包含。部署的时候不需要考虑外部依赖,出了问题直接 tail -f logs/app.log 就能看到实时日志,查历史问题翻翻轮转的文件就行。对于一个小项目来说,这完全够用了。
事实上,如果没有后续的事情,这种方式仍然是单机部署的最佳实践。但,事与愿违。
集群与高可用
随着这个项目的演进,功能的增加,也开始有高可用的需求了,要保障服务稳定,集群是很好的选择,考虑到之前单机部署就使用的 pm2,于是我便直接选用了 pm2 的集群模式进行部署,并做了 zero down time 不停机滚动升级。
事情看着很美好,但噩梦才刚刚开始,一切都正常,直到我开始查看日志,有的实例在悲鸣!它写不进去日志!
我一共启动了3个实例来做集群,只有一个实例能正常写日志,另外两个都在哀嚎,很快花糕就找到了原因————文件锁,日志文件被实例A独占了,B和C只能看着A写。
如果解除文件锁,那也是不合理的,这样全都写一起,日志内容会很混乱,几个进程抢着操作同一个文件,就不能给他们每实例一个日志吗?
答对了,这是我的解决方案:进程隔离
进程隔离
既然多个进程抢一个文件不行,那就给每个进程分配独立的日志文件呗。我修改了 winston 的配置,给每个实例用pid做区分:logs/app-${process.pid}.log
这个方案解决了写入冲突的问题,每个进程安安静静地写自己的文件,互不干扰。但新的问题随之而来:现在我有3个实例,每个实例一个 applog,一个 errorlog,加起来我有6个日志文件了,查问题的时候要在几个文件之间跳来跳去,非常的困难。
真正的痛点:分散的日志
这种体验真的很糟糕。
相比之下,喵御宅主站的日志就舒服很多,高可用集群,以及高度的自治运维和自动化,自动采集日志传输给日志服务,我只需要在日志服务看日志就行。
我着实不想把这个小东西扔k8s里面去,于是继续探索。
轻量级的中心化方案
经过一番思考,我决定采用一个相对轻量的方案:让应用程序把日志输出到 stdout,然后借助 pm2 内置的日志管理功能(pm2 logs 命令即可查看所有进程的合并输出,并且 pm2 本身也支持配置日志文件的路径和轮转),再通过 Filebeat 把日志采集走,然后花糕就又回到了他喜欢的 elastic search,不过其实不采集,直接用pm2看也可以,pm2本身的轮转功能已经足够支持了。
这样一来,pm2 会自动捕获并合并所有进程的 stdout 输出到统一的日志流中,我又能回到单文件日志的简单时代了。但这次是有区别的:应用程序本身不再关心日志的存储,只负责输出;pm2 负责收集和管理日志文件。
进一步的思考
解决了这个问题后,我开始反思整个过程。其实这就是一个典型的"技术选型要匹配业务规模"的案例。
对于大型分布式系统,ELK Stack 这样的重型武器是必要的,它们提供了强大的搜索、分析、告警能力。但对于小项目来说,可能一个简单的日志合并方案就够了。
更有趣的是,即使在云原生时代,理解这些"原始"的日志管理方式仍然是有价值的。它们帮助你理解问题的本质:多进程并发写入、日志分散查看困难、存储和轮转管理、文件锁、进程隔离等等。当我们真正理解了这些基础问题,才能更好地欣赏现代日志管理系统的设计思路。
技术选型没有标准答案,关键是要匹配当前的需求和团队能力。有时候,用"落后"的技术解决当前的问题,比过度设计要明智得多。
这次经历也让我重新思考了微服务和单体应用的边界。很多时候我们把 容器化、负载均衡、k8s、分布式日志这些东西当成了标配,但实际上大部分应用可能永远都用不到这些复杂的基础设施。
日志管理就是一个很好的例子。从本地文件到分布式聚合,每一步的演进都是为了解决特定规模下的特定问题。理解这个演进过程,比单纯地记住"最佳实践"要有价值得多。
也许当这个项目继续增长,我最后还是会把它丢进 k8s 集群做大规模负载,但到那个时候,这个项目的日志功能也不需要做任何改动,它已经符合了云原生时代下的标准。
