容器日志采集问题解决方案-全面分析
容器日志采集的挑战
在传统的应用部署方式下,应用运行在物理机或虚拟机上,我们可以通过SSH等方式直接访问到机器上的日志文件。而在容器化部署中,每个容器都是一个独立的进程空间,相互之间隔离,无法直接访问其他容器内部的数据。因此,在进行容器日志采集时需要考虑以下几个问题:
- 如何获取到容器内部的日志信息?
- 如何区分不同容器之间的日志信息?
- 如何将这些日志信息聚合起来进行统一管理?
容器日志采集方式
针对上述问题,我们可以使用以下几种方式进行容器日志采集:
- 容器日志文件直接输出到宿主机上的文件系统中,再使用传统的日志采集工具进行采集。
- 容器日志文件输出到标准输出(stdout)或标准错误输出(stderr)中,再使用Docker提供的日志驱动将其转发到指定的目的地。
- 使用第三方容器日志采集工具进行采集,如Fluentd、Logstash等。
Docker 日志都在哪里?怎么收集?
日志分两类,一类是 Docker 引擎日志;另一类是 容器日志。
Docker 引擎日志
Docker 引擎日志 一般是交给了 Upstart(Ubuntu 14.04) 或者 systemd (CentOS 7, Ubuntu 16.04)。前者一般位于 /var/log/upstart/docker.log 下,后者一般通过 jounarlctl -u docker 来读取。不同系统的位置都不一样,SO上有人总结了一份列表,我修正了一下,可以参考:
系统 日志位置 Ubuntu(14.04) /var/log/upstart/docker.log Ubuntu(16.04) journalctl -u docker.service CentOS 7/RHEL 7/Fedora journalctl -u docker.service CoreOS journalctl -u docker.service OpenSuSE journalctl -u docker.service OSX ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/log/docker.log Debian GNU/Linux 7 /var/log/daemon.log Debian GNU/Linux 8 journalctl -u docker.service Boot2Docker /var/log/docker.log
容器日志
容器的日志 则可以通过 docker logs 命令来访问,而且可以像 tail -f 一样,使用 docker logs -f 来实时查看。如果使用 Docker Compose,则可以通过 docker-compose logs <服务名> 来查看。
如果深究其日志位置,每个容器的日志默认都会以 json-file 的格式存储于 /var/lib/docker/containers/<容器id>/<容器id>-json.log 下,不过并不建议去这里直接读取内容,因为 Docker 提供了更完善地日志收集方式 - Docker 日志收集驱动。
关于日志收集,Docker 内置了很多日志驱动,可以通过类似于 fluentd, syslog 这类服务收集日志。无论是 Docker 引擎,还是容器,都可以使用日志驱动。比如,如果打算用 fluentd 收集某个容器日志,可以这样启动容器:
$ docker run -d \ --log-driver=fluentd \ --log-opt fluentd-address=10.2.3.4:24224 \ --log-opt tag="docker.{{.Name}}" \ nginx
其中 10.2.3.4:24224 是 fluentd 服务地址,实际环境中应该换成真实的地址。
具体使用 fluentd 的方法,请参考我写的一组 fluentd 日志收集的例子:
https://coding.net/u/twang2218/p/docker-example/git/tree/master/fluentd
不同容器的日志汇聚到 fluentd 后如何区分?
有两种概念的区分,一种是区分开不同容器的日志,另一种是区分开来不同服务的日志。
区分不同容器的日志是很直观的想法。运行了几个不同的容器,日志都送向日志收集,那么显然不希望 nginx 容器的日志和 MySQL 容器的日志混杂在一起看。
但是在 Swarm 集群环境中,区分容器就已经不再是合理的做法了。因为同一个服务可能有许多副本,而又有很多个服务,如果一个个的容器区分去分析,很难看到一个整体上某个服务的服务状态是什么样子的。而且,容器是短生存周期的,在维护期间容器生存死亡是很常见的事情。如果是像传统虚拟机那样子以容器为单元去分析日志,其结果很难具有价值。因此更多的时候是对某一个服务的日志整体分析,无需区别日志具体来自于哪个容器,不需要关心容器是什么时间产生以及是否消亡,只需要以服务为单元去区分日志即可。
这两类的区分日志的办法,Docker 都可以做到,这里我们以 fluentd 为例说明。
version: '2' services: web: image: nginx:1.11-alpine ports: - "3000:80" labels: section: frontend group: alpha service: web image: nginx base_os: alpine logging: driver: fluentd options: fluentd-address: "localhost:24224" tag: "frontend.web.nginx.{{.Name}}" labels: "section,group,service,image,base_os"
这里我们运行了一个 nginx:alpine 的容器,服务名为 web。容器的日志使用 fluentd 进行收集,并且附上标签 frontend.web.nginx.<容器名>。除此以外,我们还定义了一组 labels,并且在 logging 的 options 中的 labels 中指明希望哪些标签随日志记录。这些信息中很多一部分都会出现在所收集的日志里。
让我们来看一下 fluentd 收到的信息什么样子的。
{ "frontend.web.nginx.service_web_1": { "image": "nginx", "base_os": "alpine", "container_id": "f7212f7108de033045ddc22858569d0ac50921b043b97a2c8bf83b1b1ee50e34", "section": "frontend", "service": "web", "log": "172.20.0.1 - - [09/Dec/2016:15:02:45 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.49.1\" \"-\"", "group": "alpha", "container_name": "/service_web_1", "source": "stdout", "remote": "172.20.0.1", "host": "-", "user": "-", "method": "GET", "path": "/", "code": "200", "size": "612", "referer": "-", "agent": "curl/7.49.1", "forward": "-" } }
如果去除 nginx 正常的访问日志项目外,我们就可以更清晰的看到有哪些元数据信息可以利用了。
{ "frontend.web.nginx.service_web_1": { "image": "nginx", "base_os": "alpine", "container_id": "f7212f7108de033045ddc22858569d0ac50921b043b97a2c8bf83b1b1ee50e34", "section": "frontend", "service": "web", "group": "alpha", "container_name": "/service_web_1", "source": "stdout", } }
可以看到,我们在 logging 下所有指定的 labels 都在。我们完全可以对每个服务设定不同的标签,通过标签来区分服务。比如这里,我们对 web 服务指定了 service=web 的标签,我们同样可以对数据库的服务设定标签为 service=mysql,这样在汇总后,只需要对 service 标签分组过滤即可,分离聚合不同服务的日志。
此外,我们可以设置不止一个标签,比如上面的例子,我们设置了多组不同颗粒度的标签,在后期分组的时候,可以很灵活的进行组合,以满足不同需求。
此外,注意 frontend.web.nginx.service_web_1,这是我们之前利用 --log-opt tag=frontend.web.nginx.<容器名> 进行设定的,其中 <容器名> 我们使用的是 Go 模板表达式 {{.Name}}。Go 模板很强大,我们可以用它实现非常复杂的标签。在 fluentd 中,<match> 项可以根据标签来进行筛选。
这里可以唯一表示容器的,有容器 ID container_id,而容器名 container_name 也从某种程度上可以用来区分不同容器。因此进行容器区分日志的时候,可以使用这两项。
还有一个 source,这表示了日志是从标准输出还是标准错误输出得到的,由此可以区分正常日志和错误日志。
现在我们可以知道,除了容器自身输出的信息外,Docker 还可以为每一个容器的日志添加很多元数据,以帮助后期的日志处理中应对不同需求的搜索和过滤。
在后期处理中,fluentd 中可以利用 <match> 或者 <filter> 插件根据 tag 或者其它元数据进行分别处理。而日志到了 ElasticSearch 这类系统后,则可以用更丰富的查询语言进行过滤、聚合。
如何去收集当下比较常用的runtime?
docker和containerd的容器日志及相关参数
对比项 |
Docker |
Containerd |
存储路径 |
docker作为k8s容器运行时的情况下,容器日志的落盘由docker来完成, 默认保存在/var/lib/docker/containers/$CONTAINERID目录下。kubelet会在/var/log/pods和/var/log/containers下面建立软链接,指向/var/lib/docker/containers/$CONTAINERID目录下的容器日志文件 |
containerd作为k8s容器运行时的情况下, 容器日志的落盘由kubelet来完成,保存到/var/log/pods/$CONTAINER_NAME目录下,同时在/var/log/containers目录下创建软链接,指向日志文件 |
配置参数 |
在docker配置文件中指定: "log-driver": "json-file", "log-opts": {"max-size": "100m","max-file": "5"} |
方法一:在kubelet参数中指定: --container-log-max-files=5 --container-log-max-size="100Mi" 方法二:在KubeletConfiguration中指定: "containerLogMaxSize": "100Mi", "containerLogMaxFiles": 5 |
把容器日志保存到数据盘 |
把数据盘挂载到"data-root"(缺省是/data/var/lib/docker)即可 |
创建一个软链接/var/log/pods指向数据盘挂载点下的某个目录(ln -s /data/var/log/pods /var/log/) |
无论k8s使用哪种容器运行时,最终的日志都是读取的xxx-json.log,是由容器以json格式输出。