通过Sidecar实现通用ACS应用日志轮转

由于容器计算服务 ACS(Container Compute Service)集群采用Serverless形式管理集群资源,无法通过部署DaemonSet来管理日志的轮转。日志文件若不轮转,可能持续增长至占用全部磁盘空间。因此,ACS提供了一种通过Sidecar来实现通用的容器日志轮转的方案。本文介绍如何在Sidecar中通过配置Cron定时触发logrotate来实现容器日志文件的轮转。

原理说明

重要

本文提供的是通用的日志轮转方案, 不推荐用于以下两个场景:

  • 只有一个文件输出的场景。此场景推荐使用标准输出而不是文件日志,因为容器对标准输出有原生的轮转机制,并且也更方便在容器出现异常时观察日志。

  • Java、Python等自带日志轮转能力的场景,推荐直接使用日志库的日志轮转功能。

本文提供的方案设计如下:

image
  • 工作负载数量较少时(上图①部分),可以采用单独为应用配置logrotate Sidecar的方式来实现日志轮转。

  • 工作负载数量较多时(上图②部分),采用SidecarSet的方式,批量为指定范围内(集群维度或命名空间维度)的工作负载注入Logrotate Sidecar

为应用配置logrotate Sidecar

以下示例采用原生的Sidecar机制(即配置restartPolicy: Alwaysinit容器)来部署logrotate容器。 对于K8s 1.28及以下的ACS集群,可以用ACS增强的环境变量__IS_SIDECAR=true来标记普通容器,实现和原生Sidecar一样的容器生命周期管理能力。更多信息,请参见配置Sidecar容器启停顺序

重要

Logrotate容器需与业务容器共享数据卷,并额外挂载/var/lib目录保存轮转状态数据,防止logrotate容器重启后丢失状态,从而做出错误的日志轮转动作。同时,需要确保应用输出的日志文件存放在共享数据卷中。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-logrotate
  labels:
    app: test
spec:
  replicas: 1 
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      initContainers:
        - name: logrotate
          env:
            - name: CRON_EXPR
              value: "5 * * * *"
            - name: LOGROTATE_LOGFILES
              value: "/var/log/*/*.log"
            - name: LOGROTATE_FILENUM
              value: "5"
            - name: LOGROTATE_FILESIZE
              value: "10M"
            - name: __IGNORE_RESOURCE__
              value: "true"
            - name: __IGNORE_READY__
              value: "true"
          command: [ "sh", "/start.sh" ]
          image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/logrotate:v1.1
          volumeMounts:
            - mountPath: /var/log
              name: shared-log
            - mountPath: /var/lib
              name: logrotate-state
          restartPolicy: Always
          resources:
            limits:
               cpu: 0.25
               memory: 0.5Gi
      containers:
        - name: busybox
          image: mirrors-ssl.aliyuncs.com/busybox:latest
          command: [ "sh", "-c" ]
          args:
            - |
              mkdir /var/log/busybox
              while true; do
                TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
                LOG_LEVEL=$(shuf -n1 -e INFO WARNING ERROR)
                APP_NAME="busybox"
                HOSTNAME=$(hostname)
                MESSAGE=$(shuf -n1 -e "Success" "Timeout" "Database error")
                echo "$TIMESTAMP $HOSTNAME [$LOG_LEVEL] $APP_NAME: $MESSAGE" >> /var/log/busybox/busybox.log
                sleep 1
              done
          volumeMounts:
            - mountPath: /var/log
              name: shared-log
      volumes:
        ## 采集标准输出日志
        - emptyDir:
          name: shared-log
        - emptyDir:
          name: logrotate-state

主要环境变量说明:

环境变量名

说明

示例

CRON_EXPR

执行logrotateCron表达式。

5 * * * *表示每小时的第5分钟执行logrotate。日志轮转一般需要避免在每个小时的整点进行, 用于避免影响整点开始的业务活动。

LOGROTATE_LOGFILES

需要轮转的日志路径。

/var/log/*/*.log,扫描所有/var/log下的子目录的以.log结尾的文件。

LOGROTATE_FILENUM

每一个日志文件需要保留的历史文件数。

5

LOGROTATE_FILESIZE

每个日志文件的最大空间占用量。超过LOGROTATE_FILESIZE后调用logrotate来轮转日志。

10M

说明

LOGROTATE_FILESIZE * LOGROTATE_FILENUM 决定了所有日志文件要占用的最大磁盘空间,您可以根据实例申请的存储空间大小进行合理的设置。 

__IGNORE_RESOURCE__

忽略logrotate容器的资源声明,避免额外带来资源花费。同时可以确保logrotate容器与业务容器共享CPU和内存资源,且仍然受配置的resource.limits的限制。更多信息,请参见配置调度忽略特定容器资源

"true"

__IGNORE_READY__

忽略logrotate容器的Ready状态,避免由于logrotate容器处于NotReady状态而影响整个Pod运行。更多信息,请参见忽略Sidecar容器的NotReady状态

"true"

共享数据卷及轮转效果

  • 共享数据卷验证:

    分别查看busyboxlogrotate容器的/var/log/busybox目录。

    image

    可以看到,两个容器的相同目录下都存在busybox.log的日志,说明共享数据卷挂载成功。

  • 日志轮转效果:

    说明

    为了快速演示轮转效果,以下演示结果通过调整CronLOGROTATE_FILESIZE值加速轮转周期。

    image

批量为应用配置logrotate Sidecar

重要

ACS集群中创建和使用SidecarSet需要先安装ack-kruise组件。具体操作,请参见管理组件

OpenKruiseSidecarSet提供了一种对Sidecar容器的管理能力,能够自动注入和独立升级Sidecar容器。本示例的SidecarSet会对所有带有kruise.io/inject-logrotate: "true"标签的Pod注入logrotate Sidecar。同时,此SidecarSet开启了shareVolume策略shareVolumePolicy.type=enabled,开启shareVolumePolicy后,Pod的所有volumeMounts会自动挂载到Sidecar中。 若所有待注入的Pod有约定好的数据卷名称,也可以在SidecarSet中进行声明。

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: logrotate-sidecarset
spec:
  selector:
    matchLabels:
      kruise.io/inject-logrotate: "true"
  shareVolumePolicy:
    type: enabled
  updateStrategy: 
    type: NotUpdate
  initContainers:
    - name: logrotate
      env:
        - name: CRON_EXPR
          value: "5 * * * *"
        - name: LOGROTATE_LOGFILES
          value: "/var/log/*/*.log"
        - name: LOGROTATE_FILENUM
          value: "5"
        - name: LOGROTATE_FILESIZE
          value: "10M"
        - name: __IGNORE_RESOURCE__
          value: "true"
      command: [ "sh", "/start.sh" ]
      image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/logrotate:v1.1
      volumeMounts:
        - mountPath: /var/lib/
          name: logrotate-state
      restartPolicy: Always
      resources:
        limits:
           cpu: 0.25
           memory: 0.5Gi
  volumes:
    - name: logrotate-state
      emptyDir: {}

相关信息

以下提供用于构建本文中logrotate镜像的Dockerfile和相关脚本文件。您可以根据实际需求对Dockerfile或脚本文件进行修改。

  • Dockerfile文件:

    FROM registry-cn-hangzhou.ack.aliyuncs.com/dev/alpine:3.20-update
    
    RUN apk --update add bash logrotate
    
    ADD start.sh /start.sh
    
    CMD ["/start.sh"]
  • start.sh文件:

    #!/bin/sh
    
    LOGROTATE_LOGFILES="${LOGROTATE_LOGFILES:?Files for rotating must be given}"
    LOGROTATE_FILESIZE="${LOGROTATE_FILESIZE:-10M}"
    LOGROTATE_FILENUM="${LOGROTATE_FILENUM:-5}"
    
    cat > /etc/logrotate.conf << EOF
    ${LOGROTATE_LOGFILES}
    {
      size ${LOGROTATE_FILESIZE}
      missingok
      notifempty
      copytruncate
      rotate ${LOGROTATE_FILENUM}
    }
    EOF
    
    if [ -z "$CRON_EXPR" ]; then
      CRON_EXPR="0 6 * * *"
      echo "CRON_EXPR environment variable is not set. Set to default: $CRON_EXPR"
    else
      echo "CRON_EXPR environment variable set to $CRON_EXPR"
    fi
    
    echo "$CRON_EXPR	/usr/sbin/logrotate -v /etc/logrotate.conf" >> /etc/crontabs/root
    
    (crond -f) & CRONPID=$!
    trap "kill $CRONPID; wait $CRONPID" SIGINT SIGTERM
    wait $CRONPID