天天看点

解决运维痛点磁盘空间不足

作者:DecOpSec

背景

在k8s环境下,业务日志使用emptyDir或者hostPath存储,因业务日志逐渐增加导致的node磁盘空间不足。

ONCALL值守的同学深有体会,值班时收到较多的是磁盘不足的告警,这类告警的处理技术含量吗你懂的,但确实需要人工干预,SRE宝贵的时间啊。

除了人工成本,还要考虑服务器资源成本,node机器是标准的机型,磁盘空间也是有限。面对各种日志的日益增加,磁盘空间终究会面不足。

既要控制每台机器的服务器成本,也要解放运维生产力,又要日志集中存储满足安全合规要求,还要优雅的解决。

在云原生这个时代我们能否有低成本、安全、高效、优雅的办法解决因日志导致的磁盘空间不足的问题,尽可能少的运维干预,解放生产力呢?

答案是肯定的,有。

下面让我们一起来了解一下。

日志处理共识

在开始解决上述问题前,我们先达成六大共识:

首先业务日志需要保留,最好保留180天以上满足等保三级要求;

其次所有有按天分隔的日志按T+2压缩,即压缩一天前的日志;

第三是业务日志在容器和node上保留是的时间要适当短一些,比如保留一周日志;

第四有特殊需要的可以调整日志处理脚本保留日志时长;

第五各种日志都保存到目录/data/logs/下,比如nginx日志/data/logs/nginx/,业务日志/data/logs/modulename;

第六业务日志通过日志收集工具实时打到远端存储(保留时长180天以上)。

下面我们来看看解决磁盘不足的痛点用到哪些技术。

用到技术

  1. 1. 运维标准化,这里主要指日志统一目录
  2. 2. kubelet配置优化
  3. 3. openkruise 的 AdvancedCronJob+BroadcastJob sidecarset 使用
  4. 4. filebeat日志收集到远端

需求拆解

需求:node磁盘空间不足,有没有办法尽可能自动的解决这个问题,而非人工干预。

我们把需求进行拆解,然后逐一进行解决。

需求1:node上镜像导致的磁盘空间不足

需求2:pod日志使用磁盘空间大导致node磁盘空间不足,业务日志收集到远端存储

需求3:node上运行的非容器日志导致的磁盘空间不足

实战-需求1:node上镜像导致的磁盘空间不足

一般k8s集群node机器nodefs和imagefs是一个磁盘分区

node磁盘不足超过一定阈值会触发node上pod被动驱逐,严重可能影响业务稳定性。我们需要调整kubelet imagefs 、nodefs参数避免发生pod驱逐。

node上的镜像文件是常见导致node磁盘空间不足,针对node上镜像清理kubelet会针对磁盘空间剩余情况定期触发磁盘镜像的清理(image GC)工作。

生产中的最佳实践:image GC阈值 < 监控告警阈值 < pod被驱逐阈值

#监控告警阈值:磁盘使用率超过85% 。这个阈值根据你生产情况而定,同时注意联动调整image GC阈值 和 pod被驱逐阈值

下面我们来看一下kubelet的配置

#image GC阈值
imageMinimumGCAge: 2m0s         #是对未使用镜像进行垃圾搜集之前允许其存在的时长
imageGCHighThresholdPercent: 80 #镜像的磁盘用量百分数,一旦镜像用量超80%阈值,则镜像垃圾收集会运行
imageGCLowThresholdPercent: 70 #镜像的磁盘用量百分数,镜像用量低于此阈值70%时不会执行镜像垃圾收集操作

#pod被驱逐阈值
evictionHard:
  imagefs.available: 10%  #镜像的磁盘空间剩余10%,触发pod驱逐
  memory.available: 5%    #node内存剩余5%,触发pod驱逐
  nodefs.available: 10%   #node磁盘剩余10%,触发pod驱逐
  nodefs.inodesFree: 5%   #node磁盘inode剩余5%,触发pod驱逐           

有了上面的配置,当镜像盘空间剩余20%也即使用超过80%时会触发镜像的回收,不会触发我们的告警阈值和引起pod被驱逐。

注:镜像回收pod部署或漂移到该节点需要重新拉镜像,建议使用自己的镜像仓库保留镜像,这样镜像可以快速pull下来。

完整的kubelet.conf配置,供参考

#cat /etc/kubernetes/kubelet.conf

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
syncFrequency: 30s
fileCheckFrequency: 20s
httpCheckFrequency: 20s
address: 0.0.0.0
port: 10250
readOnlyPort: 10255
authentication:
  x509: {}
  webhook:
    enabled: false
    cacheTTL: 2m0s
  anonymous:
    enabled: true
authorization:
  mode: AlwaysAllow
  webhook:
    cacheAuthorizedTTL: 5m0s
    cacheUnauthorizedTTL: 30s
registryPullQPS: 5
registryBurst: 10
eventRecordQPS: 5
eventBurst: 10
enableDebuggingHandlers: true
healthzPort: 10248
healthzBindAddress: 0.0.0.0
oomScoreAdj: -999
clusterDomain: cluster.local
clusterDNS:
- 192.168.9.2
streamingConnectionIdleTimeout: 4h0m0s
nodeStatusUpdateFrequency: 10s
nodeStatusReportFrequency: 1m0s
nodeLeaseDurationSeconds: 40
imageMinimumGCAge: 2m0s
imageGCHighThresholdPercent: 80
imageGCLowThresholdPercent: 70
volumeStatsAggPeriod: 1m0s
kubeletCgroups: "/kube.slice"
cgroupsPerQOS: true
cgroupDriver: cgroupfs
cpuManagerPolicy: none
cpuManagerReconcilePeriod: 10s
runtimeRequestTimeout: 2m0s
hairpinMode: promiscuous-bridge
maxPods: 110
podPidsLimit: 10000
resolvConf: "/etc/resolv.conf"
cpuCFSQuota: true
cpuCFSQuotaPeriod: 100ms
maxOpenFiles: 1000000
kubeAPIQPS: 5
kubeAPIBurst: 10
serializeImagePulls: false
evictionHard:
  imagefs.available: 10%
  memory.available: 5%
  nodefs.available: 10%
  nodefs.inodesFree: 5%
evictionPressureTransitionPeriod: 30s
evictionMinimumReclaim:
  memory.available: 200Mi
enableControllerAttachDetach: true
makeIPTablesUtilChains: true
iptablesMasqueradeBit: 14
iptablesDropBit: 15
failSwapOn: true
containerLogMaxSize: 10Mi
containerLogMaxFiles: 5
configMapAndSecretChangeDetectionStrategy: Watch
systemReserved:
  cpu: 100m
  memory: 500Mi
kubeReserved:
  cpu: 100m
  memory: 500Mi
enforceNodeAllocatable:
- pods           

实战-需求2:pod日志(业务日志)使用磁盘空间大导致node磁盘空间不足,业务日志收集到远端存储

统一的日志目录 业务日志目录:/data/logs 挂载到pod的形式是emptyDir

日志压缩、保留、收集到远端,所有的业务pod都需要,用sidecar的形式实现是最合理的。

首先看解决磁盘空间不足,日志压缩、保留sidecarset配置

业务容器部分yaml配置

...

template:
  metadata:
    labels:
      kruise.io/inject-log-deal: "true"  #日志压缩、保留,此标签被sidecarset匹配上后注入log-deal sidecar
      kruise.io/inject-log-remote: "true" #日志打到远端,此标签被sidecarset匹配上后注入log-remote sidecar
...

volumes:
  - name: app-log-volume
    emptyDir: {}
...
volumeMounts:
  - name: app-log-volume
    mountPath: /data/logs           

业务模块多,每个模块都需要sidecar注入,这里选择开源的openKruise组件的sidecarset解决我们的sidecar统一管理和注入。

业务容器打上kruise.io/inject-log-deal: "true" 标签,重启业务pod即可注入 log-deal sidecar

log-deal sidecar作用是,每天凌晨2:02执行日志压缩和保留日志动作

log-deal sidecarset configMap配置如下

cat cm.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: sidecarset-cm
  namespace: log
  labels:
    app.kubernetes.io/name: sidecarset-cm
    app.kubernetes.io/part-of: log
data:
  crontab.txt: |
    2 2 * * * sh /root/compress-log.sh
  
  compress-log.sh: |
    #!/bin/bash
    set -x
    # 每天压缩昨天前的日志
    for i in `find /data/logs/ -mtime +0 -type f -name "*log*"  ! -name "*.tar.gz"`
    do
            filename=`echo $i|awk -F'/' '{print $NF}'`
            bdir=`dirname $i`
            echo $filename  $bdir
            cd $bdir
            tar zcvf $filename".tar.gz" $filename
            rm $filename
    done

    #日志保留7天,注意日志要通过filebeat打到远程,以免丢失
    keepDay=7
    ##modulename保留30天
    hostname | grep "modulename" && keepDay=30

    find /data/logs/ -mtime +$keepDay -type f -name "*log*.tar.gz" | xargs -I {} rm -rf {} \; &> /dev/null
  
  filebeat.yml: |
    filebeat.inputs:
    - type: log
      enabled: true
      paths:
      - /data/logs/**/*
      encoding: utf-8
      fields:
        module_name: "${MODULE_NAME}"
        module_namespace: "${POD_NAMESPACE}"
    #=========================== 是否开启多行合并================================
    # multiline.type: pattern
    multiline.pattern: '(^\S|^\s)' #任何以非空白字符开头的行,都合并到上一行
    multiline.negate: true
    multiline.match: after
    multiline.max_lines: 1000
    multiline.timeout: 5s

    #==========================输出日志到kafka==============================

    output.kafka:
      enabled: true
      hosts: ["yourkafka.domain.local:9092"]
      partition.round_robin:
        reachable_only: true
      version: 2.0.0
      topic: 'module_logs'           

sidecarset配置如下,SidecarSet资源是集群级别

cat logset.yaml

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: log-deal
spec:
  selector:
    matchLabels:
      kruise.io/inject-log-deal: "true"  #业务容器里设置这个标签,重启就会注入该sidecar
  updateStrategy:
    type: RollingUpdate
    maxUnavailable: 1
  containers:
  - name: log-deal
    image: youharbor.domain.com/alpine/3.14.3/alpine  #注意这里是你自己的harbor地址
    command:
      - /bin/sh
      - -c
    args:
      -  /usr/bin/crontab /root/crontab.txt ; /usr/sbin/crond -f -l 8
    volumeMounts:
    - name: compress-log
      mountPath: /root/compress-log.sh
      subPath: compress-log.sh
    - name: crontab
      mountPath: /root/crontab.txt
      subPath: crontab.txt
    podInjectPolicy: AfterAppContainer   #注册到主容器后
    shareVolumePolicy:   #共享pod的挂载卷,注入该容器后该容器继承了pod的卷到自己的namespace里,这样才能执行压缩、保留日志操作
      type: enabled
    resources:
      limits:
        memory: 1000Mi
        cpu: 1
      requests:
        memory: 10Mi
        cpu: 10m
  volumes: # this field will be merged into pod.spec.volumes
  - name: compress-log
    configMap:
      name: sidecarset-cm
      key: compress-log.sh
  - name: crontab
    configMap:
      name: sidecarset-cm
      key: crontab.txt           

其次看日志收集远端sidecarset配置,符合等保要求保留180天以上

configMap复用上文的,sidecarset配置如下

cat remote-log-set.yaml

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: log-remote
spec:
  selector:
    matchLabels:
      kruise.io/inject-log-remote: "true"  #业务容器里设置这个标签,重启就会注入该sidecar
  updateStrategy:
    type: RollingUpdate
    maxUnavailable: 1
  containers:
  - name: log-remote
    image: youharbor.domain.com/filebeat:8.5.0  #注意这里是你自己的harbor地址
    command:
      - /bin/sh
      - -c
    args:
      -  /usr/share/filebeat/filebeat -e -c /usr/share/filebeat/filebeat.yml
    volumeMounts:
    - name: filebeat-config
      mountPath: /usr/share/filebeat/filebeat.yml
      subPath: filebeat.yml
    podInjectPolicy: AfterAppContainer   #注册到主容器后
    shareVolumePolicy:   #共享pod的挂载卷,注入该容器后该容器继承了pod的卷到自己的namespace里,这样才能执行压缩、保留日志操作
      type: enabled
    env: #注入环境变量远程存储需要区分环境模块和pod名等
      - name: POD_NAME
        valueFrom:
          fieldRef:
            fieldPath: metadata.name
      - name: POD_NAMESPACE
        valueFrom:
          fieldRef:
            fieldPath: metadata.namespace
      - name: MODULE_NAME
        valueFrom:
          fieldRef:
            fieldPath: metadata.labels['app.kubernetes.io/name']
      - name: TZ
        value: "Asia/Shanghai"
    resources:
      limits:
        memory: 1000Mi
        cpu: 1
      requests:
        memory: 10Mi
        cpu: 10m
  volumes: # this field will be merged into pod.spec.volumes
  - name: filebeat-config
    configMap:
      name: filebeat-cm
      key: filebeat.yml           

至此,需求2:pod日志(业务日志)使用磁盘空间大导致node磁盘空间不足,业务日志收集到远端存储,通过两个sidecar注入业务pod就解决了。

实战-需求3:node上运行的非容器日志导致的磁盘空间不足

解决思路:通过类似于daemonset形式,把node上的/data/logs 目录挂到容器里进行日志的压缩、保留操作

首先,统一的日志目录 node机器非业务日志目录:/data/logs

其次,通过openKruise 的 AdvancedCronJob+BroadcastJob 实现类daemonset 的Job,定时运行一次后容器就退出,不占用k8s集群资源。

configMap配置 cat cm.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: compress-log-cm
  namespace: log
  labels:
    app.kubernetes.io/name: compress-log
    app.kubernetes.io/part-of: log
data:
  compress_logfile.sh: |
    #!/bin/bash
    set -x
    # 每天压缩昨天前的日志 
    for i in `find /data/logs/ -mtime +0 -type f -name "*log*"  ! -name "*.tar.gz"`
    do
            filename=`echo $i|awk -F'/' '{print $NF}'`
            bdir=`dirname $i`
            echo $filename  $bdir
            cd $bdir
            tar zcvf $filename".tar.gz" $filename
            rm $filename
    done

    #日志保留30天
    find /data/logs/ -mtime +30 -type f -name "*log*.tar.gz" | xargs -I {} rm -rf {} \; &> /dev/null           

AdvancedCronJob+BroadcastJob 的定义如下,每天1:1在所有的k8s集群节点上执行日志的压缩、保留工作。

cat conjobset.yaml

apiVersion: apps.kruise.io/v1alpha1
kind: AdvancedCronJob
metadata:
  namespace: op
  name: compress-log
spec:
  timeZone: "Asia/Shanghai"  ##按上海时间执行定时任务,默认openKruise 控制器时区比上海差8小时
  schedule: "1 1 * * *"
  template:
    broadcastJobTemplate:
      spec:
        template:
          spec:
            tolerations:  #容忍污点,目的是要在所有机器上执行日志压缩,保留
              - key: "your-key"
                operator: "Equal"
                value: "your-value"
                effect: "NoSchedule"
            volumes:
              - name: compress-log-cm-conf
                configMap:
                  name: compress-log-cm
                  items:
                    - key: compress_logfile.sh
                      path: compress_logfile.sh
              - name: data-logs-hostpath
                hostPath:
                  path: /data/logs
            imagePullSecrets:
              - name: your-docker-registry
            containers:
              - name: compress-log
                image: yourharobr.domain.com/alpine/3.14.3/alpine
                command:
                - /bin/sh
                args:
                  - /root/compress_logfile.sh
                volumeMounts:
                  - name: data-logs-hostpath
                    mountPath: /data/logs
                  - name: compress-log-cm-conf
                    mountPath: /root/compress_logfile.sh
                    subPath: compress_logfile.sh
            restartPolicy: Never
            
        completionPolicy:
          type: Always
          ttlSecondsAfterFinished: 86400  #保留一天方便定位错误           

继续阅读