Kubernetes: Task: Application Logs

5th June 2021 at 1:49pm

应用日志,是指 pod 中业务程序打出来的日志。

这是实现采集容器 stdout 日志功能时的调研,重点在于:

  • pod 日志打到了哪里
  • 如何配置 filebeat 来做采集

查看日志

如果日志打到 stdout,那么 kubectl logs 可以看到。如果 pod 经历过重启,kubectl logs --previous 可以看之前的日志。如果 pod 被销毁,这些日志会丢失。

如果日志打到容器内的文件中,可以运行 kubectl exec <pod> cat <logfile>,或者 tail -n 等命令查看。

kubectl cp 提供了在容器内外复制文件的能力:

  • kubectl cp foo-pod:/var/log/foo.log foo.log
  • kubectl cp localfile foo-pod:/etc/remotefile

Kubernetes 的 pod 日志收集机制

K8S 利用了 Docker 提供的 logging driver 机制来采集容器日志(来源)。Docker 默认将各容器的 stdout 和 stderr 重定向到磁盘文件中,最终变成一个 JSON 文件。在 node 节点上的一个例子(裁剪了 ID 长度以提升可读性):

[root@cluster1node1 pods]# tree /var/log/pods
/var/log/pods
|-- 510cb18b-3b2c-11e9-85ab-525400b56f21
|   `-- ake-add-node
|       `-- 0.log -> /var/lib/docker/containers/36ce55/36ce55-json.log
|-- c68f45d9-3b09-11e9-85ab-525400b56f21
|   `-- coredns
|       `-- 0.log -> /var/lib/docker/containers/3336f5/3336f5-json.log
`-- fcbc3dd0-3b2a-11e9-85ab-525400b56f21
    `-- ake-add-node
        `-- 0.log -> /var/lib/docker/containers/2b77c9/2b77c9-json.log

JSON 日志内容节选,带 log, streamtime 三个参数:

{"log":"      \"capabilities\":{\n","stream":"stderr","time":"2019-02-28T03:34:58.383595946Z"}
{"log":"        \"por{"log":"      }\n","stream":"stderr","time":"2019-02-28T03:34:58.383600746Z"}

Pod 重启时,K8S 默认保留一份已停止的容器的日志。

K8S 通过 Docker Engine 接口去创建容器,在 HostConfig 中指定了 json-file 的文件格式(猜测如此,代码中没找到),因此 /var/log/pods 目录可以保证存在。/var/log/pods 下的具体日志内容是由 Docker 写入。

注意 1.10 及以下版本可能没有这个目录:

如果您有特殊需求,那么可以在集群中设置您自己的日志记录解决方案。在运行 Kubernetes V1.11 或更高版本的集群中,可以从 /var/log/pods/ 路径中收集容器日志。在运行 Kubernetes V1.10 或更低版本的集群中,可以从 /var/lib/docker/containers/ 路径中收集容器日志。
- 来自 IBM Cloud Kubernetes 文档

从这个 issue 中的信息能看到,1.10 起 /var/log/pods 下的路径发生变化:

# 1.9:            /var/log/pods/<UID>/<container>_<0-based-restart-count>.log
# 1.10 and newer: /var/log/pods/<UID>/<container>/<0-based-restart-count>.log

这个 issue 体现出 1.6 版本的 K8S 已经有 /var/log/pods 日志了。

1.9.6 版本的 K8S 的一例 log 文件路径:

# Symlink: /var/log/pods/<pod-uid>/<container-name>_<0-based-restart-count>.log => /var/log/containers/<pod-name>_<namespace>_<container-name-container-id>.log
/var/log/pods/37f5169f-3e63-11e9-be9d-525400dec72d/onlyice-consumer_5.log
              └ pod uid                            |                |
                                                   └ container name |
                                                                    └ restart count (6th run)

其中 pod uid 可以通过这几种方式取得:

  • 通过 kubectl get pods/<pod-name> -n <namespace> -o yaml,观察 metadata.uid 字段
  • 通过 Downward API 通过 fieldRefmetadata.uid 中获得

metadata.uid 需要 Kubernetes 1.8.0 及以上版本,参考这个 commit 所在的 tag。

如果想通过 Kubernetes 来获得 pod 中容器的 stdout / stderr,可以使用 /var/log/pods 中的日志,但是官方并没有文档描述这个路径的 pattern,也没有承诺这个路径不变。而且直接采集 /var/log/pods 中的日志有可能丢数据(因为日志滚动)。

可能的更好方式是使用 kubectl logs 或者 kubeapi-server 的 read log 接口。这两者不依赖 /var/log/pods 下的日志。kubectl logs 最终调用的是 kubeapi-server,而 kubeapi-server 又会调用 Docker Engine API 中的 /containers/(id or name)/logs 接口。这个接口使你不用关心 /var/log/pods 的路径变化。但是有 issue (1, 2) 反馈,docker logs -f 在 JSON 日志发生 rotation 时会有异常。我实测是 docker logs -fkubectl logs -f 都会在那一刻退出,不再显示日志。这就使得这个方法也变得鸡肋。

filebeat 采集 /var/log/pods 下日志

观察到 log-opts 仅配置单文件的情况,一旦日志达到限制的大小,会 close_writeopen,没有删除及新建过程发生,因此 inode 也没有发生变化:

$ inotifywatch ./53f5db77572eca11f6af33f89e3d0972298e1ab1743ebc7f1ce5d1e807d90a04-json.log 
Establishing watches...
Finished establishing watches, now collecting statistics.
^C
 total  modify  close_write  open  filename
108588  108580  4            4     ./53f5db77572eca11f6af33f89e3d0972298e1ab1743ebc7f1ce5d1e807d90a04-json.log

根据这个 帖子,对于快速被 truncate 的文件,filebeat 可能会漏读日志。可以设置 backoff 参数来让 filebeat 更激进地去检测文件内容变化,来减少漏读的概率。但是本质上无法避免。

参考:

不同容器平台的 Docker 日志配置(不影响 k8s 拉起的 pod)

视 K8S 集群部署工具的不同,可能会带上不同的 --log-opt 参数来控制日志文件的大小和数量,如 Google 的 Container-VM Image 配置 了 10MiB 及最多 5 个文件,并最终作为环境变量写入 /etc/default/docker 文件中。

部分容器平台实现调研:

  • 灵雀 2.x 版本,在节点上配置了(/etc/docker/daemon.json)单文件最多 20MiB 的配置
  • TKE 公有云版本,在节点上配置了(/etc/docker/dockerd)单文件上限 100MiB,最多 10 个文件

docker inspect -f '{{.HostConfig.LogConfig.Type}}' <container-id> 可以看到相应的容器运行在哪种 logging driver 上。

参考资源