正文
上面我大体列举了四种类型的日志,真实的生产环境中还会有更多其他类型的日志,比如操作系统的 dmesg,语言运行时或 VM 的 gc 日志,甚至是为了某个特定用途而打印的日志等等。可以看到,上述日志其实是不同时代的产物,产生时面临的环境、要解决的问题随着时间也正在慢慢改变;这给我们带来很多思考:所有的日志都是必要的吗?日志打印的方式适合当前这个时代吗?以前的需求现在还成立吗?能否站在当下的角度抽象为更精简的几种类型的日志(而不是四种),按什么维度去抽象和精简? 在这里我无法对所有问题给出答案,但这些问题在我们设计容器化日志收集方案时,却可以提醒我们重新审视当前的日志打印和收集方式,避免落入固化思维。虽然无法对所有问题解答,但有些方面我们已经可以明显的察觉到变化。
面向机器而不是面向人
这可能是传统日志和大数据时代日志最明显的差别,传统的运行日志和访问日志,基本是为开发或运维提供排查问题的信息,是面向人去设计的,人脑天生适合处理非结构化的数据,因此日志信息非常随意,可能只是一句话,关联的数据项和格式也不固定,比如:
26109 2017-08-03 07:35:49 [DEBUG] server.go:122 Callback PostSubscribe SubTopics:
ClientID:"251ac7af-8cdc-4b23-a5c6-faa6c0d4925f"
这条日志是从推送服务线上运行日志摘取的,其特点很明显,日志行本身有一定的格式( pid,日志,日志级别等),但日志信息本身却是程序员(我)随意去写的,“ Callback PostSubscribe”我们知道是调用了回调函数,关联的信息格式看起来是冒号分割的 key-value,但 SubTopics 这个字段的 value 很明显又有自己的格式,比如可以理解为尖括号括起来的对象( key-object),那么问题是这里为什么是尖括号呢,为什么不是大括号圆括号呢?或者我们有没有统一的标准来表达这种 key-object 的的语义呢?答案是有的,比如 JSON 就可以做到。 大数据时代,处理日志信息的不再是人工,而是代码,只有结构化的数据才会便于代码处理。
服务与运行环境解耦
这可能是很多人还未曾察觉到的变化, SaaS、容器化、微服务等技术的发展,促使我们交付的成品不是一个独立的可运行的系统,而是高度模块化的,相互协作的服务。
容器的出现,提供了操作系统运行时的抽象,服务可以不用关心操作系统之间的差异,也就是将服务跟真实的运行环境解耦。 Docker 容器化的服务,可以作为一个独立的单位,部署在任何支持 Docker 容器的操作系统平台。
因此我们在做服务开发时,应该避免对运行环境的依赖。 一个思路是,我们将日志抽象为一个流,服务只将日志信息输出到这个流,不应该去关心日志存储的位置 (12factor
https://12factor.net/zh_cn/logs
)
目前这种思路已经被广泛应用,比如程序运行日志输出到 stdout(标准输出),然后由运行环境截获,比如运行在实体机上由 journald 截获,运行在容器化平台,由 Docker 的 log driver 截获。而服务只是将日志打印出来,不需要关系运行在哪个环境,是否需要落到磁盘等等。
下图就是 journald 截获的运行服务的日志:
传统的日志输出
非结构化
上面提到,日志产生的背景,是便于人工排查问题,因此日志的格式一般是非结构化的,便于人类阅读的;而现在一个功能可能由多个服务组件协作完成,这些服务组件都是远程调用关系,排查问题已经不是简单的单机查看日志,而是综合整个系统的日志去分析。这样不可避免,我们需要将日志收集整合,并通过专业的分析系统进行分析和展示;比如业界通常使用的 ELK 就是实现这个功能。
输出到文件
输出到文件使得服务的开发跟运行环境耦合,开发者需要关心日志输出的路径,或者,是由运维根据实际的运行状况去配置路径。这在我们人工部署的时代是可行的,容器化后,服务的运行环境则变成未知的,不同的服务可能运行在同一个物理机上,日志的路径可能产生冲突,而为了解决冲突,我们又需要退化到人工规划路径的时代;或者通过一些手段,将容器的唯一标识作为路径的一部分,这样实际会造成反向依赖,运行服务的容器外获取到标识信息,并创建目录,通过一定的手段(配置或环境变量)传递给容器内的服务。之所有会产生反向依赖,是因为容器内运行的服务,需要感知其底层的操作系统环境。