Docker日志驱动程序探究

1,106 阅读12分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

Docker日志驱动程序探究

背景

环境压测过程中,磁盘占满导致服务宕机。删除挂载的日志文件,磁盘占用还是很高,查看磁盘的文件占用发现/home/docker/data/containers目录下的****-json.log文件占用了大量的磁盘文件。查阅官方文档发现该文件为Docker容器日志。本文就重点介绍下Docker日志的一些基本使用。

Docker日志分类

引擎日志

Docker 引擎日志一般是交给了 Upstart(Ubuntu 14.04) 或者 systemd (CentOS 7, Ubuntu 16.04)。前者一般位于 /var/log/upstart/docker.log 下,后者我们一般 通过 journalctl -u docker 来进行查看。

系统日志位置
Ubuntu(14.04)/var/log/upstart/docker.log
Ubuntu(16.04)journalctl -u docker.service
CentOS 7/RHEL 7/Fedorajournalctl -u docker.service
CoreOSjournalctl -u docker.service
OpenSuSEjournalctl -u docker.service
OSX~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/log/d‌ocker.log
Debian GNU/Linux 7/var/log/daemon.log
Debian GNU/Linux 8journalctl -u docker.service
Boot2Docker/var/log/docker.log

容器日志

查看容器或服务的日志

docker logs命令显示正在运行的容器记录的信息。该 docker service logs命令显示参与服务的所有容器记录的信息。记录的信息和日志的格式几乎完全取决于容器的端点命令。

默认情况下,docker logsdocker service logs显示命令的输出,就像在终端中以交互方式运行命令一样。UNIX和Linux命令通常开在运行时间上三个I/O流,所谓的 STDINSTDOUTSTDERR。STDIN是命令的输入流,它可能包括来自键盘的输入或来自另一个命令的输入。STDOUT通常是命令的正常输出,STDERR通常用于输出错误消息。默认情况下,docker logs显示命令的STDOUT和 STDERR。

在某些情况下,docker logs除非您采取其他步骤,否则可能不会显示有用的信息。

  • 如果您使用将日志发送到文件、外部主机、数据库或其他日志记录后端的日志记录驱动程序,并且禁用了“双日志记录”,则docker logs可能不会显示有用的信息。
  • 如果您的图像运行非交互式进程,例如 Web 服务器或数据库,则该应用程序可能会将其输出发送到日志文件而不是STDOUT 和STDERR。

日志驱动程序

Docker 包含多种日志记录机制,可帮助您从正在运行的容器和服务中获取信息。这些机制称为日志驱动程序。每个 Docker 守护进程都有一个默认的日志驱动程序,除非您将其配置为不使用日志驱动程序,简称为“log-driver”,否则每个容器都会使用该驱动程序。

默认情况下,Docker 使用json-filelogging driver,它在内部将容器日志缓存为 JSON。除了使用 Docker 附带的日志驱动程序,您还可以实现和使用日志驱动程序插件。

提示:使用“本地”日志驱动程序来防止磁盘耗尽

默认情况下,不执行日志轮换。因此,默认json-file日志驱动程序日志驱动程序存储的日志文件可能会导致大量磁盘空间用于生成大量输出的容器,从而导致磁盘空间耗尽。

Docker 将 json-file 日志驱动程序(无日志轮换)作为默认设置,以保持与旧版本 Docker 的向后兼容性,以及在将 Docker 用作 Kubernetes 运行时的情况下。

对于其他情况,建议使用“本地”日志驱动程序,因为它默认执行日志轮换,并使用更高效的文件格式。

配置默认日志驱动

要将 Docker 守护程序配置为默认使用特定的日志驱动程序,请将daemon.json 配置文件中的log-driver值设置为日志驱动程序的名称。

默认的日志驱动程序是json-file. 以下示例将默认日志记录驱动程序设置为local日志驱动程序:

{
  "log-driver": "local"
}

如果日志驱动程序具有可配置的选项,您可以在daemon.json文件中将它们设置 为带有键的 JSON 对象log-opts。以下示例在json-file日志记录驱动程序上设置了两个可配置选项:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

重新启动 Docker 以使更改对新创建的容器生效。现有容器不使用新的日志记录配置。

注意

log-opts配置daemon.json文件中的配置选项必须以字符串形式提供。因此,布尔值和数字值(例如上例中的值 max-file)必须用引号括起来。

如果未指定日志记录驱动程序,则默认为json-file. 要查找 Docker 守护程序的当前默认日志记录驱动程序,请运行 docker info并搜索Logging Driver. 您可以在 Linux、macOS 或 Windows 上的 PowerShell 上使用以下命令:

docker info --format '{{.LoggingDriver}}'

注意

更改守护程序配置中的默认日志驱动程序或日志驱动程序选项只会影响配置更改后创建的容器。现有容器保留创建时使用的日志驱动程序选项。要更新容器的日志驱动程序,必须使用所需的选项重新创建容器。

为容器配置日志驱动程序

当您启动一个容器时,您可以使用--log-driver标志将其配置为使用不同于 Docker 守护程序默认值的日志记录驱动程序。如果日志驱动程序具有可配置选项,您可以使用一个或多个--log-opt =标志实例来设置它们。即使容器使用默认日志驱动程序,它也可以使用不同的可配置选项。

\

要查找正在运行的容器的当前日志记录驱动程序,如果守护程序正在使用json-file日志记录驱动程序,请运行以下docker inspect 命令

docker inspect -f '{{.HostConfig.LogConfig.Type}}' <CONTAINER>

配置日志消息从容器到日志驱动的传递方式

Docker提供了两种将消息从容器传递到日志驱动程序的模式:

  • (默认)direct,阻止从容器到驱动程序的交付
  • non-blocking,将日志消息存储在中间每个容器的环形缓冲区中供驱动程序使用

非阻塞消息传递模式可防止应用程序因日志记录积压而阻塞。当 STDERR 或 STDOUT 流阻塞时,应用程序可能会以意想不到的方式失败。

警告

当缓冲区已满且新消息入队时,内存中最旧的消息将被丢弃。丢弃消息通常比阻止应用程序的日志写入过程更受欢迎。

在mode日志选项控制是否使用blocking(默认)或 non-blocking消息传递。

当模式设置为非阻塞时,最大缓冲区大小日志选项控制用于中间消息存储的环形缓冲区的大小。最大缓冲区大小默认为1 MB。

以下示例以非阻塞模式和 4 MB 缓冲区启动一个具有日志输出的 Alpine 容器:

docker run -it --log-opt mode=non-blocking --log-opt max-buffer-size=4m alpine ping 127.0.0.1

使用环境变量或标签使用日志记录驱动程序

一些日志驱动程序将容器的--env |-e或--label标志的值添加到容器的日志中。本例使用Docker守护进程的默认日志驱动程序(假设为json文件)启动一个容器,但设置环境变量os=ubuntu。

docker run -dit --label production_status=testing -e os=ubuntu alpine sh

如果日志记录驱动程序支持它,则会向日志记录输出中添加其他字段。以下输出由json文件日志记录驱动程序生成:

"attrs":{"production_status":"testing","os":"ubuntu"}

支持的日志驱动程序

驱动类型说明
none容器没有可用的日志,docker日志不会返回任何输出。
local日志以定制格式存储,旨在将开销降至最低。
json-file日志的格式为JSON。Docker的默认日志记录驱动程序。
syslog将日志消息写入syslog工具。syslog守护进程必须在主机上运行。
journald将日志消息写入日志。日志守护程序必须在主机上运行。
gelf将日志消息写入Graylog扩展日志格式(GELF)端点,如Graylog或Logstash。
fluentd将日志消息写入fluentd(正向输入)。fluentd守护程序必须在主机上运行。
awslogs将日志消息写入Amazon CloudWatch日志。
splunk使用HTTP事件收集器将日志消息写入splunk。
etwlogs将日志消息作为Windows(ETW)事件的事件跟踪写入。仅在Windows平台上可用。
gcplogs将日志消息写入Google云平台(GCP)日志。
logentries将日志消息写入Rapid7日志条目。

注意

使用Docker Engine 19.03或更高版本时,Docker logs命令仅对本地、json文件和日志记录驱动程序起作用。Docker 20.10及更高版本引入了“双日志记录”,它使用本地缓冲区,允许您对任何日志记录驱动程序使用Docker logs命令。

日志驱动:local

local日志驱动记录从容器的STDOUT/STDERR输出,并写到主机磁盘上。默认情况下,local日志驱动为每个容器保留100M的日志信息,并启用压缩来保存

local日志驱动存储于/var/lib/docker/containers/容器id/local-logs目录下,以container.log命名,local驱动支持的选项有:

max-size:切割之前日志的最大容量,可取值为k/m/g,默认20m

max-file:可以存在的最大日志文件个数,如果超过最大值,则会删除旧文件,仅在max-size选项设置时有效。默认为5

compress:对应切割日志文件是否启用压缩,默认启用

配置全局日志驱动为local时,需修改/etc/docker/daemon.json文件:

{ "log-driver": "local", "log-opts": { "max-size": "10m" } }

然后重启docker服务即可生效

对于指定容器设置为local驱动时,需在docker run中加--log-driver local选项即可

补充说明:

经验证,默认情况下会生成5个文件,container.log存放最新的日志输出,当container.log到默认20m时会压缩成container.log.1.gz,如已经存在container.log.1.gz则修改原文件container.log.1.gz的文件名称,文件名中的数字+1。例子如下图所示

image.png

日志驱动json-file:

json-file日志路径/var/lib/docker/containers/容器id/容器id-json.log

json-file日志驱动支持的驱动选项:

max-size:切割之前日志的最大大小。取值单位k/m/g,默认为-1(表示无限制)

max-file:存在的最大日志文件数,仅在max-size设置时有效,默认为1

labels:适用于启动docker守护进程时,此守护进程接受的以逗号分隔的与日志记录相关的标签列表

env:适用于启动docker守护进程时,此守护进程接手的以逗号分隔的与日志记录相关的环境变量列表

env-regex:类似并兼容env。用于匹配与日志记录相关的环境变量的正则表达式。

compress:切割的日志是否进行压缩。默认是disabled

日志驱动syslog:

syslog日志驱动将日志路由到syslog服务器,syslog以原始的字符串作为日志消息元数据,接收方可以提取以下消息:

debug、warning、error、info的日志level;timestamp时间戳、hostname事件发生的主机、facility系统模块、进程名和进程id

修改/etc/docker/daemon.json文件配置全局日志驱动为syslog:

{ "log-driver": "syslog", "log-opts": { "syslog-address": "udp://192.168.3.1:1111" } }

日志驱动程序的限制

  • 读取日志信息需要解压缩已旋转的日志文件,这会导致磁盘使用率临时增加(直到读取已旋转文件中的日志条目为止),并在解压缩时增加CPU使用率。
  • Docker数据目录所在的主机存储容量决定了日志文件信息的最大大小。

Docker日志对磁盘占用的比较

新建一个springboot应用,配置logback日志,代码如下:

LogController

@RestController
@RequestMapping("log")
@Slf4j
public class LogController {
    @GetMapping("/addLog")
    public Boolean addLog(){
        log.info("这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志" +
            "这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志" +
            "这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志" +
            "这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志" +
            "这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志这是一条info日志");
        return true;
    }
}

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <property name="FILE_ERROR_PATTERN"
            value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} %file:%line: %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<level>INFO</level>
		</filter>
		<encoder>
			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
			<charset>UTF-8</charset>
		</encoder>
	</appender>

	<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!--如果只是想要 Info 级别的日志,只是过滤 info 还是会输出 Error 日志,因为 Error 的级别高, 所以我们使用下面的策略,可以避免输出 Error 的日志-->
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			<!--过滤 Error-->
			<level>ERROR</level>
			<!--匹配到就禁止-->
			<onMatch>DENY</onMatch>
			<!--没有匹配到就允许-->
			<onMismatch>ACCEPT</onMismatch>
		</filter>
		<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
		<!--<File>logs/info.demo-logback.log</File>-->
		<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
			<FileNamePattern>logs/demo-logback/info.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
			<!--只保留最近90天的日志-->
			<maxHistory>90</maxHistory>
			<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
			<!--<totalSizeCap>1GB</totalSizeCap>-->
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- maxFileSize:这是活动文件的大小,默认值是10MB-->
				<maxFileSize>2MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>
		<!--<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
		<!--<maxFileSize>1KB</maxFileSize>-->
		<!--</triggeringPolicy>-->
		<encoder>
			<pattern>${FILE_LOG_PATTERN}</pattern>
			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
		</encoder>
	</appender>

	<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>Error</level>
		</filter>
		<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
		<!--<File>logs/error.demo-logback.log</File>-->
		<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
			<FileNamePattern>logs/demo-logback/error.created_on_%d{yyyy-MM-dd}.part_%i.log</FileNamePattern>
			<!--只保留最近90天的日志-->
			<maxHistory>90</maxHistory>
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- maxFileSize:这是活动文件的大小,默认值是10MB -->
				<maxFileSize>2MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>
		<encoder>
			<pattern>${FILE_ERROR_PATTERN}</pattern>
			<charset>UTF-8</charset> <!-- 此处设置字符集 -->
		</encoder>
	</appender>

	<root level="info">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE_INFO"/>
		<appender-ref ref="FILE_ERROR"/>
	</root>
</configuration>

将项目打包成docker镜像并部署在服务器上,

默认配置不做任何修改启动(demo-logback-default)

修改日志驱动为local(demo-logback-local)

修改日志驱动为none(demo-logback-none)

image.png 接下来对三个容器进行三分钟压测查看磁盘占用情况,三个容器都未将日志文件挂载至宿主机

demo-logback-default

image.png

宿主机中docker日志占用了1G,进入容器中查看java程序的日志文件大小502.7M

demo-logback-local

image.png 宿主机中docker日志占用了16M,进入容器中查看java程序的日志文件大小501.8M

demo-logback-none

image.png 宿主机中docker日志无,且无法用docker logs命令查看,进入容器中查看java程序的日志文件大小501.7M

容器名称驱动类型是否压缩docker日志大小文件数总大小
demo-logback-defaultjson-file1G默认11G+502.7M
demo-logback-locallocal16M大于116M+501.8M
demo-logback-nonenode无日志文件000+501.7M

总结

默认情况下docker 容器会额外记录一份日志文件,容器长期运行下去会大量占用磁盘空间,造成资源的浪费,建议在docker全局配置文件中修改日志驱动的默认配置