K8S同一个deployment多个pod向pvc输出logback日志到不同文件

1.使用内置变量HOSTNAME

    <property name="LOG_PATH" value="/data"/>
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_PATH}/servicelog-rt-${HOSTNAME}.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/servicelog-%d{yyyyMMdd}-${HOSTNAME}.log.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>100</maxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</Pattern>
        </layout>
    </appender>

2.使用自定义hostname变量

第一种方式k8s主机名过长,若想只截取后缀部分字符串可以参考以下代码

package com.example;
import ch.qos.logback.core.PropertyDefinerBase;
import java.net.InetAddress;
import java.net.UnknownHostException;
 * <p> 功能描述:自定义主机名变量 </p>
 * 参考:https://logback.qos.ch/manual/configuration.html#definingPropsOnTheFly
 * @author ouruyi
 * @version 1.0
 * @date Created in 2022/9/24 23:08
public class CustomHostNamePropertyDefiner extends PropertyDefinerBase {
     * 最大长度
    private Integer maxLength = 5;
     * 默认值
    private String defaultValue = "localhost";
    public Integer getMaxLength() {
        return maxLength;
    public void setMaxLength(Integer maxLength) {
        this.maxLength = maxLength;
    public String getDefaultValue() {
        return defaultValue;
    public void setDefaultValue(String defaultValue) {
        this.defaultValue = defaultValue;
    @Override
    public String getPropertyValue() {
        InetAddress ia;
        try {
            ia = InetAddress.getLocalHost();
            String host = ia.getHostName();
            final int length = host.length();
            if (length > maxLength) {
                return host.substring(length - maxLength, length);
            } else {
                return host;
        } catch (UnknownHostException e) {
            e.printStackTrace();
        return defaultValue;
 

以下截取部分logback.xml配置文件,其中maxLength建议填写为5或15,其中5仅使用k8s pod name最后5位,15使用pod-template-hash + 最后5位

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <define name="custom.hostname" scope="system" class="com.example.CustomHostNamePropertyDefiner">
        <!-- k8s pod name 可写5或15 -->
        <maxLength>5</maxLength>
        <defaultValue>localhost</defaultValue>
    </define>
	<!-- 系统日志输出 -->
	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
	    <file>${log.path}/sys-info-${custom.hostname}.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
			<fileNamePattern>${log.path}/sys-info-%d{yyyy-MM-dd}-${custom.hostname}.log</fileNamePattern>
			<!-- 日志最大的历史 60天 -->
			<maxHistory>60</maxHistory>
		</rollingPolicy>
		<encoder>
			<pattern>${log.pattern}</pattern>
		</encoder>
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
	</appender>
</configuration> 	

3.自动清理日志

  • META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.CustomLogbackConfiguration
  • CustomLogbackConfiguration.java
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 * @author ryou
 * @since 2022/10/14 13:58
@Configuration
public class CustomLogbackConfiguration {
    @Bean
    public CustomHistoryLogCleaner customHistoryLogCleaner() {
        return new CustomHistoryLogCleaner();
  • CustomHistoryLogCleaner.java
package com.example;
import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.RollingPolicy;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import ch.qos.logback.core.util.OptionHelper;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.StopWatch;
import java.io.File;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 * @author ryou
 * @since 2022/10/13 18:54
public class CustomHistoryLogCleaner implements InitializingBean {
    private static final String CUSTOM_HOSTNAME = "custom.hostname";
    public static final Pattern PATTERN = Pattern.compile("%d\\{.+}");
    public static final Logger log = LoggerFactory.getLogger(CustomHistoryLogCleaner.class);
    @Override
    public void afterPropertiesSet() {
        final ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.execute(this::handle);
        executorService.shutdown();
     * 遍历ROOT关联的所有RollingFileAppender
    public void handle() {
        final StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        final ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
        if (loggerFactory instanceof LoggerContext) {
            final Logger root = loggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
            if (root instanceof ch.qos.logback.classic.Logger) {
                final ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) root;
                final Iterator<Appender<ILoggingEvent>> it = logger.iteratorForAppenders();
                while (it.hasNext()) {
                    final Appender<ILoggingEvent> appender = it.next();
                    if (appender instanceof RollingFileAppender) {
                        final RollingFileAppender<?> fileAppender = (RollingFileAppender<?>) appender;
                        handleFileAppender(fileAppender);
                    } else if (appender instanceof AsyncAppender) {
                        final AsyncAppender asyncAppender = (AsyncAppender) appender;
                        final Iterator<Appender<ILoggingEvent>> attachIt = asyncAppender.iteratorForAppenders();
                        while (attachIt.hasNext()) {
                            final Appender<ILoggingEvent> attachAppender = attachIt.next();
                            if (attachAppender instanceof RollingFileAppender) {
                                final RollingFileAppender<?> attachFileAppender = (RollingFileAppender<?>) attachAppender;
                                handleFileAppender(attachFileAppender);
        stopWatch.stop();
        log.info("历史日志清理耗时{}毫秒", stopWatch.getTotalTimeMillis());
     * 获取RollingFileAppender滚动策略
    public void handleFileAppender(RollingFileAppender<?> appender) {
        int orphanLogSavePeriod = -1;
        // 清理历史日志
        final RollingPolicy rollingPolicy = appender.getRollingPolicy();
        if (rollingPolicy instanceof TimeBasedRollingPolicy) {
            final TimeBasedRollingPolicy<?> timeBasedRollingPolicy = (TimeBasedRollingPolicy<?>) rollingPolicy;
            final String fileNamePattern = timeBasedRollingPolicy.getFileNamePattern();
            final int maxHistory = timeBasedRollingPolicy.getMaxHistory();
            orphanLogSavePeriod = maxHistory;
            // 启动时清理日志
            if (timeBasedRollingPolicy.isCleanHistoryOnStart()) {
                cleanHistoryLogExpired(fileNamePattern, maxHistory);
        // 清理孤儿日志
        if (orphanLogSavePeriod > 0) {
            final String fileNameInUse = appender.getFile();
            cleanOrphanLogExpired(fileNameInUse, orphanLogSavePeriod);
     * 清理过期日志
     * @param fileNamePattern 文件匹配模式 /data/interview-realtime/application-%d{yyyyMMdd}-86kx2.log.%i
     * @param maxHistory      最大保存历史天数 200
    private void cleanHistoryLogExpired(String fileNamePattern, int maxHistory) {
        log.info("logback日志自动清理:fileNamePattern=>[{}], maxHistory=>[{}]", fileNamePattern, maxHistory);
        final int index = fileNamePattern.lastIndexOf("/");
        String dir = fileNamePattern.substring(0, index);
        String fileName = fileNamePattern.substring(index + 1);
        Matcher matcher = PATTERN.matcher(fileName);
        if (matcher.find()) {
            String pattern = matcher.group(0);
            int start = matcher.start();
            String fileNamePrefix = fileName.substring(0, start);
            String datePattern = pattern.substring(3, pattern.length() - 1);
            final LocalDateTime now = LocalDateTime.now();
            final LocalDateTime expiredDay = now.minusDays(maxHistory);
            final LocalDateTime min = LocalDateTime.of(1970, 1, 1, 12, 0);
            final String maxDay = expiredDay.format(DateTimeFormatter.ofPattern(datePattern));
            final String minDay = min.format(DateTimeFormatter.ofPattern(datePattern));
            final String maxFileName = fileNamePrefix + maxDay;
            final String minFileName = fileNamePrefix + minDay;
            log.info("logback日志自动清理:minFileName=>[{}], maxFileName=>[{}]", minFileName, maxFileName);
            final File file = new File(dir);
            final File[] files = file.listFiles(e -> e.isFile() && e.getName().startsWith(fileNamePrefix));
            for (File f : files) {
                final String name = f.getName();
                log.info("logback日志自动清理:历史日志文件name=>{}", name);
                if (name.compareTo(minFileName) > 0 && name.compareTo(maxFileName) < 0) {
                    log.info("logback日志自动清理:正在删除历史日志文件{}...", name);
                    f.delete();
     * 清理孤儿日志
     * 注意自定义变量作用域为system
     * @param fileNameInUse 正在写入的日志文件 /data/interview-realtime/interview-realtime-rt-86kx2.log
     * @param maxHistory    最大保存历史天数 200
     * @see ch.qos.logback.core.joran.action.ActionUtil#setProperty
    private void cleanOrphanLogExpired(String fileNameInUse, int maxHistory) {
        log.info("logback日志自动清理:正在写入的日志文件=>[{}], 最大保存周期=>[{}]", fileNameInUse, maxHistory);
        final LocalDateTime now = LocalDateTime.now();
        final LocalDateTime endDateTime = now.minusDays(maxHistory);
        final long maxExpired = endDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        final String customHostName = OptionHelper.getSystemProperty(CUSTOM_HOSTNAME);
        if (Objects.isNull(customHostName)) {
            log.info("logback日志自动清理:找不到自定义变量[{}], 已跳过!", CUSTOM_HOSTNAME);
            return;
        final int index = fileNameInUse.lastIndexOf("/");
        String dir = fileNameInUse.substring(0, index);
        String fileName = fileNameInUse.substring(index + 1);
        final int end = fileName.lastIndexOf(customHostName);
        String fileNamePrefix = fileName.substring(0, end);
        final File file = new File(dir);
        final File[] files = file.listFiles(e -> e.isFile() && e.getName().startsWith(fileNamePrefix));
        for (File f : files) {
            final String name = f.getName();
            if (fileName.equals(name)) {
                continue;
            log.info("logback日志自动清理:孤儿日志文件name=>{}", name);
            final long lastModified = f.lastModified();
            if (lastModified < maxExpired) {
                log.info("logback日志自动清理:正在删除孤儿日志文件{}...", name);
                f.delete();

参考:
GlusterFS集群文件系统研究
多个spring boot实例输出logback日志到一个文件导致日志混乱问题
多项目写入同一Logback日志文件导致的滚动混乱问题(修改Logback源码)

提供了一个kubernetes部署描述符,如果需要修改VERSION属性Makefile然后运行以下命令 kubectl create -f k8s-sample-app.yml watch -n 0.2 -d http --timeout 1 `minikube service k8s-sample-app --url`/foo 部署kubectl命令 kubectl create -f k8s-sample-app.yml 更改k8s-sample-app.yml后更新部署 kubectl rep from kubernetes import client, config # kubernetes生成时/root目录下.kube目录下的认证配置文件 config.kube_config.load_kube_config(config_file=/root/.kube/config) # config.load_kube_config(kube_conf) api_instance = client.AppsV1Api() 查看deployment内容 def read_deployment(name, namespace, up 命令: yo k8s 这将启动资源生成器,该资源生成器询问您要配置的部分。 回答完所有问题后,将所有部分连接到一个文件中。 使用此生成器,您可以生成包含服务,应用程序,入口和其他内容的完整部署。 命令: yo k8s:config 这将为k8s生成一个ConfigMap 。 在提问期间,如果愿意,可以使用配置的$EDITOR输入配置映射值。 关闭编辑器后,空行和以#开头的#被忽略。 命令: yo k8s:deployment 这将为k8s生成一个Deployment 。 您必须输入一些值才能生成yml结构。 如果您配置了PersistentVolumeClaim则将其添加到底部。 命令: yo k8s:ingress 这会为k8s生成一个Ingress 。 您
###一、传统日志解决方案 在以前我们的应用日志一般由log4j输入到不同文件中,比如info.log warn.log error.log。 然后当我们需要查看日志的时候,就需要登录服务器使用命令tail -fn 500 error.log进行查看。 ###二、微服务日志解决方案 近年来微服务越来越火爆,微服务虽然带来一些好处,但是也引入了日志收集的问题。一般来说,我们可能将服务部署在容器中...
manifests/service.yml 使用Kubernetes服务器Dryrun进行Lint 服务器dryrun将与kuberenetes服务器通信,因此请确保KUBECONFIG在上下文中可用。 这仅适用于Kubernetes版本> = 1.12 - uses : azure/k8s-lint@v1 with : lintType : dryrun manifests : | manifests/deployment.yml manifests/
Oratos Kubernetes部署 Oratos是一个基于希腊语的名称,与Istio和Prometheus等其他名称一样,属于Kubernetes社区。 ορατός•(oratós)m(女性,中性) visible, in sight, seen 此仓库包含在kubernetes上运行loggregator所需的对象。 注意:该项目目前处于实验阶段,可能会发生重大变化。 一个k8s集群启动并运行。 kubectl在本地安装,并配置为以您的k8s集群为目标(kubernetes API和CLI的最低支持版本: 1.10 )。 docker本地安装并且被配置为靶向dockerd用于运行证书生成容器。 生成TLS证书和密钥 要为您的loggregator部署生成TLS证书和密钥,您需要运行一个容器。 要简化此操作,您可以简单地: pushd secrets ./gen
编写deployment和service的yaml文件,在kubernates集群中添加服务。 1、docker镜像已存在,名为mydemoapp,tag为0.0.1。如果不清楚如何制作自己的docker镜像,可以参考《构建一个Java应用的Docker镜像》 2、k8s集群已搭建成功。 3、应用:端口为999,有个可访问的接口名称为hello 下面就可以开始手写yaml文件 ,向k8s集群中添加自己的服务了。 # mydemoapp-0.0.1.yaml apiVersion: apps/v1 kind: Deployment metadata: name: mydemoapp
1、统一日志管理的整体方案 通过应用和系统日志可以了解Kubernetes集群内所发生的事情,对于调试问题和监视集群活动来说日志非常有用。对于大部分的应用来说,都会具有某种日志机制。因此,大多数容器引擎同样被设计成支持某种日志机制。对于容器化应用程序来说,最简单和最易接受的日志记录方法是将日志内容写入到标准输出和标准错误流。 但是,容器引擎或运行时提供的本地功能通常不足以支撑完整的日志记录解决方...
[code=java] org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.mongo.autoconfigure.MongoMultiTenantAutoConfiguration [/code] MongoDB多租户方案设计 he_sk: spring.factories配置的是MongoMultiTenantAutoConfiguration.java吗?