写在前面

运行在容器内部的应用会在运行期间产生大量的日志,这些日志将作为我们 Debug,分析应用行为的重要依据。本文将带大家了解一下 Docker 面对的和「容器日志」有关的问题以及它的解决方案。

日志种类

通常来讲,容器内运行的应用会以两种方式输出日志:

  1. 标准输出
  2. 日志文件

日志需求

我们对于容器内应用输出的日志的需求比较简单,基本上可以归结为以下几点:

  1. 易使用:可以方便的通过 docker logs 或者 kubectl logs 等命令行查看容器内部的日志信息
  2. 易收集:可以很容易的找到容器内部日志最终持久化的位置,如某个文件或者说某个存储服务
  3. 自处理:当日志容量达到一定额度的时候,可以通过一些「滚动操作」将旧的日志清除掉一部分,维持一个固定大小的日志文件
  4. 可分析:可以方便的将所有的日志(包括被滚动掉的)内容送入第三方的日志分析服务,以便能够以更加有效的方式通过对日志的分析而了解服务的运行情况

Docker 的容器日志处理方案

标准输出日志

实质

回想一下 Docker 的实现原理,无非是「多进程」+「隔离机制」。其中所有容器(子进程)都是由 dockerdaemon(父进程)来创建的。父进程可以收集子进程 PID Namespace 内 PID=1 进程的和标准输出的内容。因为只有 PID = 1的进程才是父进程创建的。如果该 PID = 1的子进程再创建孙子进程的话,父进程是无法收集到孙子进程内的标准输出的内容的。

父进程是通过 Pipe 来获取子进程的和标准输出的。所以,在容器进程被创建的时候,会通过一个 Pipe 将输出到标准输出的日志信息传递给父进程。而这一切对容器内部应用都是透明的。

Docker Log Driver

当 dockerdaemon 进程从 Pipe 中拿到日志信息之后,会将它交给一个特殊的模块来进行处理—— Docker Log Driver。Log Driver 的职责也很简单,既然收集到了日志信息,那肯定需要将它写入到一个位置,这个位置可以是一个 JSON 格式的文件(默认行为),也可以是一个有特殊含义的文件,如 syslog,甚至是一个第三方日志的收集服务。

Log Driver 写入的位置,可以通过很灵活的方式进行配置。可以在 dockerdaemon 启动的时候通过 —log-driver 参数配置,也可以在每个容器启动的时候用同样的参数配置。容器级别的 LogDriver 配置会覆盖全局的。

生产环境中的问题——真正打日志的服务不是 dockerdaemon 的子进程

问题

在基于 Kubernetes 平台开发数据库应用的时候,为了能够实现一些「自运维」的功能,通常在一个容器内,我们会以一个baseImage 为基础,再启动我们的「业务进程」(负责自运维)和数据库进程(redisd,mysqld 等等)。此时,以 dockerdaemon 进程的视角来看的话,它的子进程就不是数据库进程或者说是我们的业务进程了。因为 baseImage 的存在,它通常在容器启动的时候会顺便启动一些辅助进程,帮我们在容器内部搭建一个较好的运行环境。但是,我们想要观察的日志大部分都来自于业务进程和数据库进程。

解决思路

针对上面的问题,并且结合 Docker 收集容器日志的原理。我们想出了以下的解决方案:

  1. 将容器内部我们想收集的来自标准输出的日志都先塞入到一个容器内部的文件中,如 syslog
  2. 利用容器内部的其他服务,将 syslog 文件内容再重定向到标准输出
解决方案
  1. 使用 GitHub - phusion/baseimage-docker: A minimal Ubuntu base image modified for Docker-friendliness baseImage 中内置的一个名为 syslog-ng 的服务,将 syslog 的内容重定向到标准输出
  2. 通过 logrus 第三方日志库,加入日志输出的 hook。将业务进程内部输出的日志重定向到 syslog
  3. 开启 Redis Server 自身提供的,将服务日志输出到 syslog 的功能

通过对上面关于「标准输出日志」遇到问题的解决方案的了解,我们可以发现,它其实有如下的几个风险:

  1. syslog 文件的内容可能会无限增大,占用容器的存储资源(此问题在 log driver 将日志保存在宿主机文件的情况下,也同样存在)
日志滚动

在宿主机上,Docker 将会帮我们解决问题1。若你使用 json-file 这种 logdriver 的时候,会让你配置一个日志文件的大小。一旦达到这个阈值,Docker 会通过一些日志滚动服务来删除掉一部分旧的日志,从而保持日志文件一个稳定的大小。

而在容器内部我们就得自己想办法了。之前提到过在生产环境中,我们在容器内部使用了 baseImage。其内部内置的一个服务叫:logrotate。他会帮我们针对某个特定的文件做「滚动」操作。

对上述解决方案的一点思考

其实上述方案的本质是:用文件日志作为服务日志和容器标准输出之间的桥梁,虽然容器内部是文件日志,但是是通过标准输出向外传递的。而容器内的文件日志可以通过 baseImage 提供的 syslog-ng 和 logrotate 服务来进行日志的重定向和文件日志的滚动。这是在我们没有成熟的收集容器内部文件日志的机制的前提下,想出来的一种解决方案。它复用了 Docker 提供的一些现有的机制。

再抽象一点——Docker 的日志管理

容器日志管理的本质是「如何处理 stdout」 和「如何将我关心的日志都打入 stdout」。这两个问题的出现,其实是受限于 Docker 对于容器日志的管理方式(通过父子进程 pipe 和 log driver)。

能否对上述方案做一些优化?

上面的方案其实已经能解决容器日志所面临的大部分问题了。但是仍旧有个做的不是很好的点:baseImage 集成了太多了服务(如 logrotate 和 syslog-ng)。其实,对于 baseImage,你可以说这是优点,也可是说这是缺点。优点是方便,缺点就是可能会和业务进程抢占资源。

既然现有方案上有缺点,我们就需要解决它。思路很简单:什么进程可能会抢占业务进程的资源,就把谁拿出这个容器。

  1. 将 logrotate 和 syslog-ng 等服务放入另外一个容器(但是在一个 Pod 内,相当于一个 sidecar container)
  2. 业务容器通过第三方日志库或者其他方式的支持,将 业务进程的日志通过网络通信(UDP) 传递给「日志服务容器」内的服务
  3. 在日志服务容器内部,可重复上述:日志打入 syslog 然后再从定向到标准输出的过程
  4. 集群级别部署一个高可靠的第三方日志收集组件,收集来自 logdriver(即标准输出)的日志内容

这种方案,虽然在资源用量上面没有什么优化。但是却将日志服务和业务进程从一个容器中分开来,两者申请的资源也可以分别控制,互不影响。

如果没有 syslog-ng 和 logrotate

其实大部分容器用户,它们对 baseImage 的要求不是很高。甚至有的时候他们希望 baseImage 是尽量干净,尽量小的。如果 baseImage 中没有我们上面说到的 syslog-ng 和 logrotate 服务的话,容器内的日志收集问题就比较麻烦(在业务进程仍然不是 pid=1的情况下)。

通常情况下,容器内部的业务进程会将日志打入容器内部的一个文件中。所以,我们将面临两个问题:

  1. 日志文件内容的滚动
  2. 如何确认日志文件在宿主机的具体路径

引发这两个问题的根源是相同的:我们由处理标准输出的内容变成了处理文件内容。对于「标准输出」,有 logdriver 负责日志的滚动和最终日志文件在宿主机上的存储位置。而对于「文件」,就必须要我们自己来处理了。

文件日志

解决方案

尝试1——每个容器内部配备日志收集进程

最直接的想法就是在每个业务容器中配备一个用于收集日志的进程,它负责将容器内部的日志压缩文件上传到某个地方。但是这样做的缺点也很明显:

  1. 每个容器都必须要起一个日志收集进程,1w 个容器就是1w 个收集进程,对资源会有浪费
  2. 「上传」操作难以控制,一旦有网络的参与,尤其是内外网的交互,传输可靠性风险极大
  3. 收集日志的进程可能需要做一些自定义的配置,如间隔多久收集一次等等。这些都需要额外的开发工作
尝试2——每个容器集群配备一个日志收集服务(进程)

既然每个容器一个收集程序有诸多缺点,那我们就借助开源社区的力量,找一个强大的日志收集组件,将它部署到集群中。这样一来整个集群只有一个日志收集服务。但是这个方案,也同样需要解决几个和日志路径相关的问题:

  1. Docker 依赖的底层存储方案不同,导致容器运行镜像时候的 top layer 的映射规则五花八门。你不知道容器的 top layer 实际映射到了物理机上的哪个目录。比如 AUFS 这种实现方案,映射关系很复杂,我在了解 AUFS 原理的时候,在这部分花了大量的时间。这种映射关系及时有,估计文档化也比较差,我一直没有找到一个很官方的解释。不了解映射关系就直接导致了:你的日志收集服务不知道到哪里去收集日志
  2. 即使日志路径的映射关系确定了,这部分信息如何通知给集群级别的日志收集服务?

针对于问题1,Docker 为我们提供了原生的解决方案:在容器启动的时候,将 top layer 通过显示声明的方式挂载到一个宿主机目录下。通过-v 参数即可实现。

针对于问题2,Docker 同样也为我们提供了帮助:集群级别的日志收集服务可以通过和宿主机 docker 通信的方式,监听容器的各类事件(创建,删除等)。这些事件内部将会包含我们上面提到的容器的挂载信息。

如果能找到一个比较好的日志收集组件的话,那么此方案会被很好的支持。并且,有了容器的 label以及对容器各类事件的监听,日志收集服务可以很容易的对功能进行扩展。

另外,对于日志文件滚动的的问题。既然是一个单独的日志收集服务来负责容器内文件日志的处理,那么这部分功能理应交由它来实现。该功能要至少满足以下两个需求:

  1. 回滚的策略可配置
  2. 「判定是否满足回滚策略」的信息可以很方便的获取到(如监听容器的各类事件)

总结