容器集群的日志要如何治理

本文我们主要来思考一下,关于使用了容器集群去部署服务之后,这些服务的日志应该如何进行治理?

日志种类的变化

早在十几年前的时候,很多业务服务为了记录程序运作过程中的变化,可能会采用非常简单的输出流去记录,例如下方所示的代码:

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的功能能够简化互联网中大量日志的管理问题,但是其运维成本是比较高的,需要企业有足够的技术能力去支撑才可以运行。

相关推荐
毅航18 分钟前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题33 分钟前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油42 分钟前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug2 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕3 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A13 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈
蜗牛快跑1233 小时前
github 源码阅读神器 deepwiki,自动生成源码架构图和知识库
前端·后端
嘻嘻嘻嘻嘻嘻ys3 小时前
《Vue 3.4响应式超级工厂:Script Setup工程化实战与性能跃迁》
前端·后端
橘猫云计算机设计3 小时前
net+MySQL中小民营企业安全生产管理系统(源码+lw+部署文档+讲解),源码可白嫖!
数据库·后端·爬虫·python·mysql·django·毕业设计
执念3653 小时前
MySQL基础
后端