本文我们主要来思考一下,关于使用了容器集群去部署服务之后,这些服务的日志应该如何进行治理?
日志种类的变化
早在十几年前的时候,很多业务服务为了记录程序运作过程中的变化,可能会采用非常简单的输出流去记录,例如下方所示的代码:
java
System.out.println("system print");
但是,这种记录会随着越来越多的输出,导致之前的输出流被覆盖,而且不可持久化,导致内容丢失。于是日志组件开始渐渐出现了。
-
Log4j:是最早的日志框架,是apach旗下的,可以单独使用,也可配合日志框架JCL使用;
-
Log4j2:apach旗下的关于log4j的升级版;
-
Logback:是基于slf4j接口实现的一套日志框架组件;(Logback是由log4j创始人设计的又一个开源日志组件。)
-
JUL(java utillog):仿log4j实现的日志框架,是sun旗下的,(也就是在我们普遍使用的jdk中);
-
Commons logging:是一套日志接口(apache);
-
Slf4j:也是一套日志接口;
这些日志组件虽然名字很多,但是其实主要是分为两大类别:日志接口(Slf4j,Commons logging),日志底层实现(Log4j,Logback,JUL)。
关于日志接口这块,其实大家听说比较多的可能是Slf4j,而实际上Commons logging在大数据相关的依赖中出现地比较多,例如es,hadoop,hive的依赖都用它比较多。
容器日志的打印管理
下边我们以springboot服务类型为案例,梳理下日志打印的配置文件应该如何设计,首先我们来看以下日志配置内容:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty name="APP_NAME" scope="context" source="spring.application.name" defaultValue="undefined"/>
<!-- 用于生成一个标识,防止多个Docker容器映射到同一台宿主机上出现目录名重复问题-->
<define name="index" class="org.qiyu.live.common.interfaces.utils.IpLogConversionRule"/>
<property name="LOG_HOME" value="/tmp/logs/${APP_NAME}/${index}"/>
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS} -%5p] %-40.40logger{39} :%msg%n"/>
<!-- 控制台标准继续输出内容 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志输出的格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
</appender>
<!-- info级别的日志,记录到对应的文件内 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}.log</file>
<!-- 滚动策略,日志生成的时候会按照时间来进行分类,例如2023-05-11日的日志,后缀就会有2023-05-11,每天的日志归档后的名字都不一样 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}</fileNamePattern>
<!-- 日志只保留1个月 -->
<maxHistory>1</maxHistory>
</rollingPolicy>
<!-- 日志输出的格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
</appender>
<!-- error级别的日志,记录到对应的文件内 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}_error.log</file>
<!-- 滚动策略,日志生成的时候会按照时间来进行分类,例如2023-05-11日的日志,后缀就会有2023-05-11,每天的日志归档后的名字都不一样 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}_error.log.%d{yyyy-MM-dd}</fileNamePattern>
<!-- 日志只保留1个月 -->
<maxHistory>1</maxHistory>
</rollingPolicy>
<!-- 日志输出的格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<!-- 值记录error级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 根输出级别为INFO,控制台中将出现包含info及以上级别的日志-->
<!-- 日志输出级别 -->
<root level="INFO">
<!-- ref值与上面的appender标签的name相对应 -->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
这份配置中有几个关键的点需要注意下:
- 如何保证服务输出日志的内容,同时也会在控制台上输出?
这里我们需要配置一个 ch.qos.logback.core.ConsoleAppender ,这个类底层会通过System.out.println类型的方式来将日志的内容同样输出到控制台,确保我们启动服务的时候观测起来更加快捷。
- 如何避免日志文件体积过大?
通常来说,日志文件体积太过庞大,会导致磁盘空间不足,所以一般的做法是按照日期分割,只保留最近一段时间的日志内容。
关于这块的配置,大家可以看上方的 ch.qos.logback.core.rolling.RollingFileAppender 。
- 容器内的日志路径,如何挂载到宿主机?
这个问题我们需要分场景来区别,如果只是单机服务,那么在启动脚本中,做好磁盘路径映射即可,例如在docker-compose.yml文件中设置好volumes参数,做好容器内的路径映射到宿主机的指定路径。
但是如果是集群部署的话,这样还会存在一个问题,就是各个容器的名称都是一样的,如何区别日志的来源。
所以这里我们可以在创建log文件的时候,注入一个ip的因子,用于标识当前日志是哪个容器产生的。
这里我们可以在Java中自定义一个对象,用于返还当前docker容器的ip地址,具体代码如下:
java
package org.qiyu.live.common.interfaces.utils;
import ch.qos.logback.core.PropertyDefinerBase;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ThreadLocalRandom;
/**
* 保证每个docker容器的日志挂载目录唯一性
*
* @Author idea
* @Date: Created in 15:57 2023/6/3
* @Description
*/
public class IpLogConversionRule extends PropertyDefinerBase {
@Override
public String getPropertyValue() {
return this.getLogIndex();
}
private String getLogIndex() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return String.valueOf(ThreadLocalRandom.current().nextInt(100000));
}
}
每次查看日志都要到宿主机操作,如何简化
上边我们介绍的日志管理方式,还是一种很粗暴的方式,但是其实如今很多中小型公司都会这么做,但是随着服务种类的增加,假设有上百种微服务,不同服务的日志散落在不通的机器上,如果我们需要查看一条长链路请求的日志行为,那么将会是一件非常痛苦的事情。
于是日志记录的治理方案也开始逐渐产生。如果需要将日志治理做成系统化,那么这套系统需要具备哪些功能特性呢?我总结了下,具体特点可以分为以下几类:
- 收集:能够采集多种来源的日志数据
- 传输:能够稳定的把日志数据解析过滤并传输到存储系统
- 存储:存储日志数据
- 分析:支持UI分析
- 警告:能够提供错误报告,监控机制
相信看到这里,你应该能想到ELK这个名词了吧。是的,ELK在日志采集和日志搜索,以及日志监控这块都做的比较成熟,因此该架构一直被各大互联网公司所采用。下边这张图是一张ELK的整体架构图:
ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat,它是一个轻量级的日志收集处理工具(Agent),Filebeat占用资源少,适合于在各个服务器上搜集日志后传输给Logstash。最后由Logstash将日志以规定的格式输出写入到ES中,写入完成后,用户就可以在Kibana平台上搜索到新泻入的日志了。
当然 ELK也并不是万能的宝物,如果遇到一些超高并发的场景,它的支撑成本和支撑能力也是有限的,不过了解了它底层的运作原理之后,我们可以用换插件的思路去进行优化这块,例如es换成clickhouse,logstash前做一层kafka缓冲等等。
看起来,elk的功能能够简化互联网中大量日志的管理问题,但是其运维成本是比较高的,需要企业有足够的技术能力去支撑才可以运行。