背景
在k8s环境下,业务日志使用emptyDir或者hostPath存储,因业务日志逐渐增加导致的node磁盘空间不足。
ONCALL值守的同学深有体会,值班时收到较多的是磁盘不足的告警,这类告警的处理技术含量吗你懂的,但确实需要人工干预,SRE宝贵的时间啊。
除了人工成本,还要考虑服务器资源成本,node机器是标准的机型,磁盘空间也是有限。面对各种日志的日益增加,磁盘空间终究会面不足。
既要控制每台机器的服务器成本,也要解放运维生产力,又要日志集中存储满足安全合规要求,还要优雅的解决。
在云原生这个时代我们能否有低成本、安全、高效、优雅的办法解决因日志导致的磁盘空间不足的问题,尽可能少的运维干预,解放生产力呢?
答案是肯定的,有。
下面让我们一起来了解一下。
日志处理共识
在开始解决上述问题前,我们先达成六大共识:
首先业务日志需要保留,最好保留180天以上满足等保三级要求;
其次所有有按天分隔的日志按T+2压缩,即压缩一天前的日志;
第三是业务日志在容器和node上保留是的时间要适当短一些,比如保留一周日志;
第四有特殊需要的可以调整日志处理脚本保留日志时长;
第五各种日志都保存到目录/data/logs/下,比如nginx日志/data/logs/nginx/,业务日志/data/logs/modulename;
第六业务日志通过日志收集工具实时打到远端存储(保留时长180天以上)。
下面我们来看看解决磁盘不足的痛点用到哪些技术。
用到技术
- 1. 运维标准化,这里主要指日志统一目录
- 2. kubelet配置优化
- 3. openkruise 的 AdvancedCronJob+BroadcastJob sidecarset 使用
- 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 #保留一天方便定位错误