从 Docker 到 Kubernetes 日志管理机制详解
<h2>1、概述</h2><p>在容器化时代,容器应用的日志管理和传统应用存在很大的区别,为了顺应容器化应用,Docker 和 Kubernetes 提供了一套完美的日志解决方案。本文从 Docker 到 Kubernetes 逐步介绍在容器化时代日志的管理机制,以及在 Kubernetes 平台下有哪些最佳的日志收集方案。涉及到的话题有 Docker 日志管理机制、Kubernetes 日志管理机制、Kubernetes 集群日志收集方案。</p>
<h2>2、Docker 日志管理机制</h2>
<h3>2.1 Docker 的日志种类</h3>
<p>在 Docker 中日志分为两大类:</p>
<ul>
<li>Docker 引擎日志;</li>
<li>容器日志;</li>
</ul>
<h4>2.1.1 Docker 引擎日志</h4>
<p>Docker 引擎日志就是 docker 服务的日志,即 dockerd 守护进程的日志,在支持 Systemd 的系统中可以通过 journalctl -u docker --no-pager 查看日志。</p>
<p>常用命令:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">查看最新十条日志:journalctl -f -u docker
查看所有日志:journalctl -u docker --no-pager
查看最近200条日志【分页】:journalctl -u docker -n 200
查看最近200条日志【不分页】:journalctl -u docker -n 200 --no-pager
</pre>
</div>
<h4>2.1.2 容器日志</h4>
<p>容器日志指的是每个容器打到 stdout 和 stderr 的日志,而不是容器内部的日志文件。docker 管理所有容器打到 stdout 和 stderr 的日志,其他来源的日志不归 docker 管理。我们通过 <strong><code>docker logs</code></strong> 命令查看容器日志都是读取容器打到 stdout 和 stderr 的日志。 </p>
<h3 id="基于日志驱动(loging-driver)的日志管理机制">2.2 基于日志驱动(loging driver)的日志管理机制</h3>
<p>Docker 提供了一套通用、灵活的日志管理机制,Docker 将所有容器打到 stdout 和 stderr 的日志都统一通过日志驱动重定向到某个地方。</p>
<p>Docker 支持的日志驱动有很多,比如 local、json-file、syslog、journald 等等,类似插件一样,不同的日志驱动可以将日志重定向到不同的地方,这体现了 Docker 日志管理的灵活性,以热插拔的方式实现日志不同目的地的输出。</p>
<p>Dokcer 默认的日志日志驱动是 <strong><code>json-file</code></strong>,该驱动将将来自容器的 stdout 和 stderr 日志都统一以 <code>json</code> 的形式存储到本地磁盘。日志存储路径格式为:<strong><code>/var/lib/docker/containers/<容器 id>/<容器 id>-json.log</code></strong>。所以可以看出在 <code>json-file</code> 日志驱动下,Docker 将所有容器日志都统一重定向到了 <code>/var/lib/docker/containers/</code> 目录下,这为日志收集提供了很大的便利。</p>
<blockquote>
<p>注意:只有日志驱动为:local、json-file 或者 journald 时,docker logs 命令才能查看到容器打到 stdout/stderr 的日志。</p>
</blockquote>
<p><img src="https://img2022.cnblogs.com/blog/624219/202207/624219-20220707075045586-1154388666.png"></p>
<blockquote>
<p>注意:以上内容都是使用的docker默认数据盘目录(/var/lib/docker),如果修改数据盘目录的话,那么容器日志都统一重定向到"docker数据盘/containers/"目录下。</p>
</blockquote>
<p>下面为官方支持的日志驱动列表:</p>
<table>
<thead>
<tr><th>驱动</th><th>描述</th></tr>
</thead>
<tbody>
<tr>
<td>none</td>
<td>运行的容器没有日志,<code>docker logs</code> 也不返回任何输出。</td>
</tr>
<tr>
<td>local</td>
<td>日志以自定义格式存储,旨在实现最小开销。</td>
</tr>
<tr>
<td>json-file</td>
<td>日志格式为JSON。Docker的默认日志记录驱动程序。</td>
</tr>
<tr>
<td>syslog</td>
<td>将日志消息写入syslog。该 syslog 守护程序必须在主机上运行。</td>
</tr>
<tr>
<td>journald</td>
<td>将日志消息写入journald。该journald守护程序必须在主机上运行。</td>
</tr>
<tr>
<td>gelf</td>
<td>将日志消息写入Graylog扩展日志格式(GELF)端点,例如Graylog或Logstash。</td>
</tr>
<tr>
<td>fluentd</td>
<td>将日志消息写入fluentd(转发输入)。该fluentd守护程序必须在主机上运行。</td>
</tr>
<tr>
<td>awslogs</td>
<td>将日志消息写入Amazon CloudWatch Logs。</td>
</tr>
<tr>
<td>splunk</td>
<td>使 用HTTP 事件收集器将日志消息写入 splunk。</td>
</tr>
<tr>
<td>etwlogs</td>
<td>将日志消息写为 Windows 事件跟踪(ETW)事件。仅适用于Windows平台。</td>
</tr>
<tr>
<td>gcplogs</td>
<td>将日志消息写入 Google Cloud Platform(GCP)Logging。</td>
</tr>
<tr>
<td>logentries</td>
<td>将日志消息写入 Rapid7 Logentries。</td>
</tr>
</tbody>
</table>
<h4>2.2.1 Docker 日志驱动(loging driver)配置</h4>
<p>上面我们已经知道 Docker 支持多种日志驱动类型,我们可以修改默认的日志驱动配置。日志驱动可以全局配置,也可以给特定容器配置。</p>
<ul>
<li>
<p>查看 Docker 当前的日志驱动配置</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># dockerinfo |grep"Logging Driver"</pre>
</div>
</li>
<li>
<p>查看单个容器的设置的日志驱动</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">docker inspect-f '{{.HostConfig.LogConfig.Type}}' 容器id</pre>
</div>
</li>
<li>
<p>Docker 日志驱动全局配置<br>全局配置意味所有容器都生效,编辑 /etc/docker/daemon.json 文件(如果文件不存在新建一个),添加日志驱动配置。<br>示例:配置 Docker 引擎日志驱动为 syslog(一般都是使用默认的日志驱动json-file,以下只是示例如何配置全局的日志驱动)</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">{
"log-driver": "syslog"
}</pre>
</div>
</li>
<li>
<p>给特定容器配置日志驱动</p>
<p>在启动容器时指定日志驱动 --log-driver 参数。<br>示例:启动 nginx 容器,日志驱动指定为 journald</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">dockerrun --name nginx -d --log-driver journald nginx </pre>
</div>
</li>
</ul>
<h4>2.2.2 Docker 默认的日志驱动 json-file</h4>
<p>json-file 日志驱动记录所有容器的 STOUT/STDERR 的输出 ,用 JSON 的格式写到文件中,每一条 json 日志中默认包含 <code>log</code>, <code>stream</code>, <code>time</code> 三个字段,示例日志如下:<br>文件路径为(此机器docker数据盘目录为/data/docker-data):</p>
<p>/data/docker-data/containers/80efcd729d409346c0af1cc346d6cf44c31a823d0a771a4f834ee580cb7b7ff/80efcd729d409346c0af1cc346d6cf44c3</p>
<p>1a823d0a771a4f834ee580cb7b7ffe-json.log </p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">{"log":"10.233.64.72 - - \"GET /plugins/variant/1.3/variant.hpi HTTP/1.1\" 200 10252 \"-\" \"curl/7.52.1\" \"-\"\n","stream":"stdout","time":"2022-06-09T06:36:19.606097555Z"}
</pre>
</div>
<p>那么打到磁盘的 json 文件该如何配置轮转,防止撑满磁盘呢?每种 Docker 日志驱动都有相应的配置项日志轮转,比如根据单个文件大小和日志文件数量配置轮转。json-file 日志驱动支持的配置选项如下:</p>
<table>
<thead>
<tr><th>选项</th><th>描述</th><th>示例值</th></tr>
</thead>
<tbody>
<tr>
<td>max-size</td>
<td>切割之前日志的最大大小。可取值单位为(k,m,g), 默认为-1(表示无限制)。</td>
<td><code>--log-opt max-size=10m</code></td>
</tr>
<tr>
<td>max-file</td>
<td>可以存在的最大日志文件数。如果切割日志会创建超过阈值的文件数,则会删除最旧的文件。仅在max-size设置时有效。正整数。默认为1。</td>
<td><code>--log-opt max-file=3</code></td>
</tr>
<tr>
<td>labels</td>
<td>适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的标签列表。</td>
<td><code>--log-opt labels=production_status,geo</code></td>
</tr>
<tr>
<td>env</td>
<td>适用于启动Docker守护程序时。此守护程序接受的以逗号分隔的与日志记录相关的环境变量列表。</td>
<td><code>--log-opt env=os,customer</code></td>
</tr>
<tr>
<td>compress</td>
<td>切割的日志是否进行压缩。默认是disabled。</td>
<td><code>--log-opt compress=true</code></td>
</tr>
</tbody>
</table>
<p>配置示例:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;">{
"log-driver": "json-file",
"log-level": "warn",
"log-opts": {
"max-size": "10m", //意味着一个容器日志大小上限是10M,
"max-file": "3" //意味着一个容器有三个日志,分别是id+.json、id+1.json、id+2.json。可以存在的最大日志文件数。如果超过最大值,则会删除最旧的文件。
},
"graph": "/data/docker-data"
} </pre>
</div>
<p>配置后效果示例:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># pwd
/data/docker-data/containers/c9922e8b3fb5b5a757f10e22a183e64add9700d14af76daec684de89cd3fdf9c
# du -sh *
8.4M c9922e8b3fb5b5a757f10e22a183e64add9700d14af76daec684de89cd3fdf9c-json.log
9.6M c9922e8b3fb5b5a757f10e22a183e64add9700d14af76daec684de89cd3fdf9c-json.log.1
9.6M c9922e8b3fb5b5a757f10e22a183e64add9700d14af76daec684de89cd3fdf9c-json.log.2
4.0K checkpoints
8.0K config.v2.json
4.0K hostconfig.json
4.0K mounts </pre>
</div>
<h2>3、Kubernetes 日志管理机制</h2>
<h3>3.1 Kubernetes 的日志种类</h3>
<p>在 Kubernetes 中日志也主要有两大类:</p>
<ul>
<li>Kuberntes 集群组件日志;</li>
<li>应用 Pod 日志;</li>
</ul>
<h4>3.1.1 Kuberntes 集群组件日志</h4>
<p>Kuberntes 集群组件日志分为两类:</p>
<ul>
<li>运行在容器中的 Kubernetes scheduler、kube-proxy、kube-apiserver等。</li>
<li>未运行在容器中的 kubelet 和容器 runtime,比如 Docker。</li>
</ul>
<p>在使用 systemd 机制的服务器上,kubelet 和容器 runtime 写入日志到 journald(常用的centos7正是使用 systemd 机制)。如果没有 systemd,他们写入日志到 /var/log 目录的 .log 文件。</p>
<h4>3.1.2 应用 Pod 日志</h4>
<p>Kubernetes Pod 的日志管理是基于 Docker 引擎的,Kubernetes 并不管理日志的轮转策略,日志的存储都是基于 Docker 的日志管理策略。k8s 集群调度的基本单位就是 Pod,而 Pod 是一组容器,所以 k8s 日志管理基于 Docker 引擎这一说法也就不难理解了,最终日志还是要落到一个个容器上面。 </p>
<p> <img src="https://img2022.cnblogs.com/blog/624219/202207/624219-20220707083221662-803126758.png"></p>
<p> </p>
<p>假设 Docker 日志驱动为 json-file,那么在 k8s 每个节点上,kubelet 会为每个容器的日志创建一个软链接,软连接存储路径为:/var/log/containers/,软连接会链接到 /var/log/pods/ 目录下相应 pod 目录的容器日志,被链接的日志文件也是软链接,最终链接到 Docker 容器引擎的日志存储目录:/docker数据盘目录/containers 下相应容器的日志。另外这些软链接文件名称含有 k8s 相关信息:Pod uid、Pod Name、Namespace、容器 ID 、容器名(这块内容详细分析参见下文示例),这就为日志收集提供了很大的便利。</p>
<p> </p>
<p><img src="https://img2022.cnblogs.com/blog/624219/202207/624219-20220707083514906-134125932.png"></p>
<blockquote>
<p> 注意:以上图片使用的是docker默认数据盘目录(/var/lib/docker),如果修改数据盘目录的话,那么容器日志都统一重定向到"docker数据盘/containers/"目录下。</p>
</blockquote>
<p>示例:我们跟踪一个容器日志文件,证明上述的说明,跟踪一个istio-system命名空间下jaeger-query-7cff7c84f4-9jq5s这个Pod的日志,注意此Pod内有两个容器。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># kubectl get pods -n=istio-system |grep jaeger-query
jaeger-query-7cff7c84f4-9jq5s 2/2 Running 0 7m11s</pre>
</div>
<blockquote>
<p>注意:如果容器日志没有达到docker引擎配置的日志切割文件大小的话,那么Pod内的一个容器在/var/log/containers/目录下软链接一个日志文件,一个Pod在/var/log/containers/下最多软链接数目 = Pod容器数量 * docker引擎配置配置的max-file参数值。</p>
</blockquote>
<p>此示例Pod容器内日志都没超过日志切割文件大小,那么在/var/log/containers/目录下jaeger-query-7cff7c84f4-9jq5s这个Pod对应有两个日志文件,日志文件命名格式为: <strong>Pod Name_Namespace_容器名-容器编号.log</strong></p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># ls -la /var/log/containers/ | grep jaeger-query
lrwxrwxrwx 1 root root 112 Jul 7 09:04 jaeger-query-7cff7c84f4-9jq5s_istio-system_jaeger-agent-7efd82050929dbf4b0581fefe938c9e446cf05a668ab52fa49de
0010a0723fb7.log
-> /var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/jaeger-agent/0.log
lrwxrwxrwx 1 root root 112 Jul 7 09:04 jaeger-query-7cff7c84f4-9jq5s_istio-system_jaeger-query-114d77a9af91cfb33b827eb3cf09b3033edbf0b0bf90713b6ef6
e5ba54e4efc4.log
-> /var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/jaeger-query/0.log</pre>
</div>
<p>可以看到jaeger-query-7cff7c84f4-9jq5s这个Pod在/var/log/containers/目录下的两个日志文件都软链接到了/var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/目录下,其中istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3文件命名格式为:<strong>Namespace_Pod Name_Pod uid,</strong>这里我们看下/var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/目录下文件内容:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># ls /var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/
jaeger-agentjaeger-query</pre>
</div>
<p>可以看到这个路径下正好对应当前Pod里面的两个容器名,每个容器名下的日志文件(日志文件以0.log、1.log...这样命名,日志文件数量等于其分割文件数量,最多产生docker引擎配置配置的max-file参数值个日志文件)对应软链接到"/docker数据盘目录/containers"下相应容器名下的日志文件,下面以jaeger-agent这个容器下的日志文件为例看下其软链接情况:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:html;gutter:true;"># ls -la /var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/jaeger-agent/0.log
lrwxrwxrwx 1 root root 167 Jul7 09:04 /var/log/pods/istio-system_jaeger-query-7cff7c84f4-9jq5s_85811292-a89a-4a38-896d-d6f436fc26c3/jaeger-agent/0.log
-> /data/docker-data/containers/7efd82050929dbf4b0581fefe938c9e446cf05a668ab52fa49de0010a0723fb7/7efd82050929dbf4b0581fefe938c9e446cf05a668ab52fa49de0010
a0723fb7-json.log</pre>
</div>
<div>可以看到日志文件最终链接到 Docker 容器引擎的日志存储目录。</div>
<div>另外需要注意:为什么要软链接两次,这是为了日志方便采集,/var/log/containers/目录下软链接了当前节点所有Pod下所有容器的所有日志文件,这样采集工具只需要采集/var/log/containers/目录就能采集到当前节点下的所有容器日志文件,如果只软链接一次的话,直接从/var/log/pods/下软链接到"/docker数据盘目录/containers"下的话基于Pod元数据创建的目录不一致(<strong>Namespace_Pod Name_Pod uid)</strong>,并且基于Pod下的容器名创建的目录名也不一致,这样会导致日志采集困难,因此kubelet进行了2次软链接。</div>
<h3>3.2 Kubernetes 集群日志收集方案</h3>
<p>Kubernetes 本身并未提供集群日志收集方案,k8s 官方文档给了三种日志收集的建议方案:</p>
<ul>
<li>使用运行在每个节点上的节点级的日志代理</li>
<li>在应用程序的 pod 中包含专门记录日志 sidecar 容器</li>
<li>应用程序直接将日志传输到日志平台</li>
</ul>
<h4 id="节点级日志代理方案">3.2.1 节点级日志代理方案</h4>
<p>从前面的介绍我们已经了解到,k8s 每个节点都将容器日志统一存储到了 <code>/var/log/containers/</code> 目录下,因此可以在每个节点安装一个日志代理,将该目录下的日志实时传输到日志存储平台。</p>
<p>由于需要每个节点运行一个日志代理,因此日志代理推荐以 DaemonSet 的方式运行在每个节点。官方推荐的日志代理是 fluentd,当然也可以使用其他日志代理,比如 filebeat,logstash 等。</p>
<p><img src="https://qhh0205.github.io/images/k8s-logging-arch1.png"></p>
<h4 id="sidecar-容器方案">3.2.2 sidecar 容器方案</h4>
<p>有两种使用 sidecar 容器的方式:</p>
<ul>
<li>sidecar 容器重定向日志流</li>
<li>sidecar 容器作为日志代理</li>
</ul>
<p id="sidecar-容器重定向日志流"><strong>sidecar 容器重定向日志流(日志落盘)</strong></p>
<p><strong>这种方式基于节点级日志代理方案</strong>,sidecar 容器和应用容器在同一个 Pod 运行,这个容器的作用就是读取应用容器的日志文件,然后将读取的日志内容重定向到 stdout 和 stderr,然后通过节点级日志代理统一收集。这种方式不推荐使用,缺点就是日志重复存储了,导致磁盘使用会成倍增加。比如应用容器的日志本身打到文件存储了一份,sidecar 容器重定向又存储了一份(存储到了 /var/lib/docker/containers/ 目录下)。这种方式的应用场景是应用本身不支持将日志打到 stdout 和 stderr,所以才需要 sidecar 容器重定向下。</p>
<p><img src="https://qhh0205.github.io/images/k8s-logging-arch2.png"></p>
<p id="sidecar-容器作为日志代理"><strong>sidecar 容器作为日志代理</strong></p>
<p>这种方式不需要节点级日志代理,和应用容器在一起的 sidecar 容器直接作为日志代理方式运行在 Pod 中,sidecar 容器读取应用容器的日志,然后直接实时传输到日志存储平台。很显然这种方式也存在一个缺点,就是每个应用 Pod 都需要有个 sidecar 容器作为日志代理,而日志代理对系统 CPU、和内存都有一定的消耗,在节点 Pod 数很多的时候这个资源消耗其实是不小的。另外还有个问题就是在这种方式下由于应用容器日志不直接打到 stdout 和 stderr,所以是无法使用 <code>kubectl logs</code> 命令查看 Pod 中容器日志。</p>
<p><img src="https://qhh0205.github.io/images/k8s-logging-arch3.png"></p>
<h4 id="应用程序直接将日志传输到日志平台">3.2.3 应用程序直接将日志传输到日志平台</h4>
<p>这种方式就是应用程序本身直接将日志打到统一的日志收集平台,比如 Java 应用可以配置日志的 appender,打到不同的地方,很显然这种方式对应用程序有一定的侵入性,而且还要保证日志系统的健壮性,从这个角度看应用和日志系统还有一定的耦合性,所以个人不是很推荐这种方式。</p>
<h4>3.2.4 总结</h4>
<ul>
<li>综合对比上述三种日志收集方案优缺点,更推荐使用节点级日志代理方案,这种方式对应用没有侵入性,而且对系统资源没有额外的消耗,也不影响 kubelet 工具查看 Pod 容器日志。</li>
<li>sidecar 容器重定向日志流方式常用于日志落盘使用,这种方式的应用场景是应用本身不支持将日志打到 stdout 和 stderr,所以才需要 sidecar 容器重定向下 stdout 和 stderr。</li>
</ul>
<p>参考:从 Docker 到 Kubernetes 日志管理机制详解</p>
<p>参考:Kubernetes 日志架构</p><br><br>
来源:https://www.cnblogs.com/zhangmingcheng/p/16452365.html
頁:
[1]