ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

K8s日志收集实战:log-pilot基于DaemonSet的容器日志采集方案

K8s日志收集实战:log-pilot基于DaemonSet的容器日志采集方案 1. 为什么用log-pilotk8s日志收集的选型思路1.1 先聊聊k8s环境里日志收集的痛点k8s集群跑起来之后日志收集这件事几乎是每个团队都绕不开的坑。POD是动态调度的容器随时可能被重建、迁移到其他节点日志文件散落在各个工作节点的/var/lib/docker/containers目录下你根本不知道某个服务的日志下一秒会在哪台机器上。以前在单机Docker环境里一条docker logs命令就能搞定的事到了k8s里就变成了一个需要专门设计的架构问题。常用的方案无外乎三种一是k8s原生提供的kubectl logs但这个只能看当前存活POD的日志容器一删日志就没了更谈不上聚合检索二是sidecar模式在每个POD里塞一个日志代理容器把日志转发出去缺点是资源占用高而且你得改业务POD的编排定义三是DaemonSet模式在每个节点部署一个日志采集代理统一收集节点上所有容器的日志这也是目前生产环境里最主流的做法。log-pilot正是第三种思路的落地实现。它是阿里开源的一个容器日志采集工具核心思想很朴素以DaemonSet方式跑在每个节点上通过监听Docker守护进程的事件来动态感知容器创建和销毁根据容器上的label自动生成filebeat采集配置把日志直接打到Elasticsearch里。整个过程不需要改业务镜像不需要侵入POD定义只需要在部署业务容器时加上几个label就能完成日志采集配置。1.2 为什么选log-pilot而不是直接上EFK全家桶很多人会问社区里最成熟的方案明明是EFKElasticsearch Filebeat Kibana直接用Filebeat的autodiscover功能也可以实现类似效果为什么还要用log-pilot我得说纯粹从功能角度讲Filebeat的autodiscover确实能做容器日志发现但log-pilot有一个比较实际的优势它把最繁琐的配置细节都包住了。用过Filebeat的人知道写autodiscover的condition匹配规则、处理多行日志、处理容器json-file格式的日志内容每一件事都需要仔细调试。而log-pilot把这一切封装成了简单的label规则比如aliyun.logs.log4jtomcat这样一个label它就自动帮你把容器里的日志文件采集出来放到ES的tomcat索引里全程不需要写复杂的YAML配置。另外一个选型考量是资源占用。log-pilot的采集核心还是filebeat但整个进程只跑一个比在POD里塞sidecar容器要省得多。我实测过一个4C8G的节点上跑20个业务容器log-pilot的CPU占用基本维持在5%以下内存占用稳定在100M左右。对于生产环境来说这个开销完全可以接受。还有一点log-pilot早期就针对Docker的json-file日志格式做了优化。Docker默认把容器标准输出和标准错误写到/var/lib/docker/containers目录下的json文件里每条日志是一行JSON包含time、stream、log三个字段。log-pilot能自动解析这个结构把原始的log字段内容提取出来而不是把整条JSON原样灌进ES这一点在后续做日志检索时特别重要。1.3 这套方案的整体数据链路先把整条链路画个框架方便理解后面的实操内容。业务容器的日志分两种产生方式进程直接往标准输出写或者进程写到文件里。Docker守护进程会把标准输出重定向到宿主机上的json文件同时如果容器内有进程写文件且路径被挂载到宿主机日志也会以文件形式存在。log-pilot通过挂载/var/run/docker.sock和/var/lib/docker/containers目录既能监听到Docker事件又能直接读到底层日志文件然后把日志处理之后通过filebeat的output模块发到Elasticsearch。最终用户通过Kibana检索和分析日志。这个链路的关键点在于log-pilot是唯一一个绑定在宿主机上的代理进程它不依赖k8s API Server来发现容器而是直接读Docker的event所以在容器频繁调度的情况下它感知新容器启动的速度非常快基本在秒级。相比通过k8s API轮询POD状态的方案实时性高了一个量级。2. log-pilot的核心机制与关键配置项2.1 log-pilot是怎么发现容器的log-pilot最巧妙的点在于它绕开了k8s API直接与Docker守护进程通信。它把宿主机的/var/run/docker.sock挂载进自己的容器通过Docker的Remote API监听/events接口实时获取容器创建、销毁的事件流。每当一个新容器启动log-pilot就能拿到这个容器的全部元信息包括容器ID、名称、label、挂载点等。拿到容器信息后log-pilot会检查这个容器是否带有关键label也就是aliyun.logs.*开头的标签。这个设计思路很优雅相当于通过容器自身的定义来自描述日志采集规则而不是让采集端去猜测日志在哪。你在部署应用容器的时候声明好labellog-pilot自动感知并生成filebeat配置应用销毁时自动清理对应的采集配置整个生命周期管理完全自动化。这里需要特别注意的是log-pilot依赖的是Docker Socket意味着你的集群必须是Docker作为运行时。如果你用的是containerd或者CRI-O这套方案就不适用了这个我在后面会再展开讲。2.2 label配置规则详解log-pilot的label规则设计得相当简洁核心就三种用法。第一种是采集标准输出日志label格式为aliyun.logs.自定义索引名stdout。比如你给业务容器加上aliyun.logs.accessstdoutlog-pilot就会把这个容器的标准输出采集到ES的access索引里。这里的自定义索引名后面会作为ES的index前缀Kibana里直接搜这个名字就能看到日志。第二种是采集容器内文件日志label格式为aliyun.logs.自定义索引名容器内日志路径。例如你的应用把日志写到/var/log/app/error.log那label可以写成aliyun.logs.error/var/log/app/error.loglog-pilot会把这个文件的日志采集走并索引为error。第三种是给日志附加taglabel格式为aliyun.logs.自定义索引名.tagstag1,tag2。这个在区分多环境、多版本时特别好用比如同一套服务部署在dev和prod两个集群通过tag就能快速过滤。用一个实际部署例子来说明。假设我要采集一个订单服务的业务日志容器内日志路径是/data/logs/order/order.log希望在Kibana里用order-log索引查看并且打上order-service和v1.2.0两个tag。那部署时的label就写成labels: aliyun.logs.order-log: /data/logs/order/order.log aliyun.logs.order-log.tags: order-service,v1.2.0当log-pilot感知到容器启动后它会在内部的filebeat配置里自动生成一个prospector指定路径指向宿主机上对应的文件位置并把fields设置为包含index: order-log和tags: [order-service, v1.2.0]。2.3 索引与字段映射的底层逻辑log-pilot采集到日志之后写给ES的文档结构是固定的。每个日志条目包含timestamp、host、path、content、fields等字段。content就是原始日志内容host是宿主机IPpath是宿主机上的日志文件路径fields里包含你通过label自定义的索引名和tags。ES索引的命名规则是自定义索引名加上日期后缀。log-pilot默认按天切分索引比如order-log-2025.06.14。这样做的好处是明显的日志数据量大了之后可以直接删除过期索引来释放磁盘空间不需要做复杂的生命周期管理。同时Kibana里创建index pattern的时候要注意应该用通配符方式比如order-log-*这样每天新生成的索引都会自动被匹配到。这里有一个需要提前规划的点log-pilot默认不创建ES索引模板所以ES会自动为第一个文档动态创建mapping。如果你对某些字段有类型要求比如想把content字段改成text类型用于全文检索或者想给request_time字段指定为float类型就需要提前在ES里创建索引模板。我踩过这个坑日志索引起来之后发现content字段被映射成了keyword全文搜索完全失效最后只能重建索引非常痛苦。建议在一开始就配置好模板。3. 手把手实操k8slog-pilot环境搭建与日志接入3.1 环境准备和版本选型先交代一下我这里的实验环境。我手上是一套通过kubeadm部署的k8s集群三个节点都是Rocky Linux 9系统k8s版本是1.36容器运行时用的是Docker。注意这里有一个前提log-pilot必须依赖Docker Socket所以如果集群里用的是containerd后面所有的监听逻辑都无法工作这一点在部署前就要确认清楚。ES和Kibana我直接用docker方式跑在集群外的一台独立服务器上版本选择的是7.17系列。为什么不用8.x因为log-pilot的filebeat内核版本比较旧对ES 8.x的兼容性存在一些坑最明显的是esoutput模块认证方式变了导致日志写不进去。如果你非要用8.x得给log-pilot做额外的配置适配麻烦。7.17目前还在维护期内功能完全够用稳定性最稳妥。官网默认的log-pilot镜像地址是registry.cn-hangzhou.aliyuncs.com/acs/log-pilot:0.1这个镜像已经很久没更新了但胜在稳定我一直在用。如果集群在国内网络环境直接pull这个地址基本没有问题。3.2 部署log-pilot的DaemonSet部署log-pilot只需要创建一个DaemonSet资源不需要额外的RBAC配置因为它根本不访问k8s API只操作Docker Socket。这是它的一个安全优势权限面非常小。直接把下面的YAML保存为log-pilot.yamlapiVersion: apps/v1 kind: DaemonSet metadata: name: log-pilot namespace: kube-system labels: app: log-pilot spec: selector: matchLabels: app: log-pilot template: metadata: labels: app: log-pilot spec: tolerations: - operator: Exists hostNetwork: true hostPID: true containers: - name: log-pilot image: registry.cn-hangzhou.aliyuncs.com/acs/log-pilot:0.1 env: - name: LOGGING_OUTPUT value: elasticsearch - name: ELASTICSEARCH_HOST value: 192.168.1.100 - name: ELASTICSEARCH_PORT value: 9200 - name: ELASTICSEARCH_USER value: elastic - name: ELASTICSEARCH_PASSWORD value: yourpassword resources: limits: cpu: 500m memory: 512Mi requests: cpu: 200m memory: 128Mi volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock - name: docker-logs mountPath: /var/lib/docker/containers - name: var-log mountPath: /var/log volumes: - name: docker-sock hostPath: path: /var/run/docker.sock - name: docker-logs hostPath: path: /var/lib/docker/containers - name: var-log hostPath: path: /var/log有几个细节我展开说一下。tolerations里直接写operator: Exists意思是容忍所有污点。这是必须的否则log-pilot这个DaemonSet只会调度到没有污点的节点上Master节点如果有污点就不会部署采集进程那么Master上跑的系统组件日志就收不到了。生产环境建议保留或者根据实际污点情况精准配置。hostNetwork: true和hostPID: true也是必要的。log-pilot需要和宿主机共享网络命名空间否则它访问Docker API时等于是走容器网络会出现连接问题。hostPID则是因为它在定位日志文件时需要读取宿主机上容器进程的真实路径。环境变量这块LOGGING_OUTPUT指定输出端类型支持elasticsearch、kafka等。如果后续想引入kafka做削峰填谷只需要把这个值改成kafka并配置对应的broker地址就行log-pilot内部的buffer机制会自动适配。本次实验直接打到ES所以只需要关注ES相关的几个变量。如果你启用了ES安全认证别忘了填ELASTICSEARCH_USER和ELASTICSEARCH_PASSWORD。很多人在这一步漏配导致日志pilot启动成功但日志始终写不进去排查半天才发现是认证问题。应用配置kubectl apply -f log-pilot.yaml部署完成后检查一下kubectl -n kube-system get pods -l applog-pilot -o wide如果每个节点都有一个Running状态的POD这一步就算完成了。3.3 部署一个测试应用验证日志采集光部署完log-pilot还看不出效果必须跑一个带label的业务容器来验证整条链路。我这里用一个简单的Nginx测试服务它能同时产生标准输出日志和访问日志文件非常适合验证两类采集场景。先创建一个测试命名空间kubectl create namespace log-test再部署一个带label的Nginx PODapiVersion: v1 kind: Pod metadata: name: nginx-log-test namespace: log-test labels: app: nginx-log-test aliyun.logs.nginx-stdout: stdout aliyun.logs.nginx-access: /var/log/nginx/access.log aliyun.logs.nginx-stdout.tags: nginx,stdout-test spec: containers: - name: nginx image: nginx:1.24 ports: - containerPort: 80这里我同时声明了两个采集任务nginx-stdout采集标准输出nginx-access采集容器内/var/log/nginx/access.log文件。执行kubectl apply -f nginx-log-test.yaml等POD跑起来后我们来模拟一点访问流量让日志有内容可采kubectl -n log-test exec nginx-log-test -- curl http://localhost/连续执行几次让access.log产生几行访问日志。然后稍等片刻让log-pilot完成配置生成和日志采集流转。这个等待时间通常在10到30秒之间具体取决于filebeat的scan频率。3.4 验证ES索引与Kibana输出先确认ES是否收到了数据。用curl直接查ES的索引列表curl -u elastic:yourpassword http://192.168.1.100:9200/_cat/indices?v | grep nginx正常情况下应该能看到类似nginx-access-2025.06.14和nginx-stdout-2025.06.14这样的索引状态为open文档数量大于0。我实测时遇到的一个问题是在ES 7.17里索引名如果是大写开头会被自动改成小写索引名中包含的-也会被保留。所以给label取自定义索引名时尽量用全小写加短横线的方式命名避免后续Kibana检索时的大小写困惑。ES有数据了接下来去Kibana配置index pattern。在Kibana的Management页面选择Index Patterns点击Create index pattern输入nginx-*这样会自动匹配到所有以nginx开头的索引。然后选择时间过滤字段为timestamp。创建完成后到Discover页面选择nginx-*这个pattern就能看到Nginx的标准输出和访问文件日志都在这里了。在过滤栏搜nginx.stdout-test可以快速定位到标准输出的测试标签日志。这里还有一个细节log-pilot默认给每条日志写入的字段里fields.index字段保存的就是你label里定义的自定义索引名所以你可以直接通过fields.index: nginx-access来筛选文件日志非常方便。4. 常见问题排查与实战避坑4.1 k8s初始化时api server not healthy的问题有朋友问过k8s控制节点执行kubeadm init时提示the api server is not healthy after 4m0.00747357s这虽然不是log-pilot本身的问题但在集群搭建阶段很常见我顺带说一嘴排查思路。这个报错的核心意思是kubeadm init在等待API Server健康检查超时了。大多数情况下不是k8s本身的问题而是前置条件没满足。常见的原因有这么几个一是kubelet服务没起来或者没配置好检查systemctl status kubelet有没有报错二是由于control-plane的Podapiserver、etcd等镜像没有提前拉取国内网络环境下拉取超时三是swap分区没有关闭kubelet会直接拒绝启动。我记得之前有过一次初始化失败后看kubectl get pods -n kube-system发现apiserver一直处于ContainerCreating状态describe之后发现镜像拉取失败先把镜像手动拉下来再重启kubelet才解决。如果你用的是Rocky Linux还要额外注意SELinux的状态通常情况下装好k8s组件之后kubelet才能正常工作。总之先看kubelet日志再排查镜像这个顺序基本不会错。4.2 log-pilot已经运行但日志始终采不到这个问题遇到的人最多而且原因五花八门。我按出现的概率排序说一下最常见的三种。第一容器没有声明label。这是最容易被忽略的log-pilot的工作机制决定了它只会采集带aliyun.logs.*label的容器不带label的容器它看都不看。检查方式很简单kubectl get pod pod-name -o yaml | grep aliyun.logs看看label是否存在。第二ES连接认证失败。如果你的ES开了安全认证但log-pilot的环境变量里没配或者配错了账号密码日志就会卡在output阶段。直接看log-pilot的POD日志kubectl -n kube-system logs log-pilot-pod-name --tail50如果看到反复出现unauthorized或者401基本上就是认证问题。第三文件路径没找对。对于采集容器内文件日志的场景label里写的路径是容器内的路径log-pilot会自动把这个路径转换为宿主机上的真实路径。但如果你的业务镜像里日志文件是一个软链接或者日志文件被轮转切分就会导致路径映射失败。解决办法是不要采集软链接路径直接采集真实文件路径对于日志轮转场景log-pilot本身能通过filebeat的backoff机制适应但前提是真实路径保持稳定。4.3 日志里有大量Kubernetes元数据污染了原始日志这是刚开始用log-pilot时最想吐槽的一点。log-pilot在把日志发给ES时会自动附加一坨k8s元数据包括容器名、POD名、镜像名、宿主机名等。初衷是好的方便检索过滤但如果你只想看到干净的业务日志这些字段会挤占文档空间而且和日志内容混在一起非常容易混淆。解决办法有两个方向。一是在ES索引模板里去字段直接在模板里设置dynamic: false只保留你关心的字段或者显式地忽略以kubernetes.开头的字段。二是直接用log-pilot的环境变量控制元数据输出。查了一下log-pilot的配置文档它支持设置LOG_COLLECT_FIELDS之类的环境变量来决定取哪些元数据字段但我建议多花一点时间在ES侧做字段过滤因为要控制log-pilot的附加字段必须修改镜像内的配置文件改动成本高。我自己实际操作时是直接建了一套ES索引模板只保留timestamp、content、host、path和自定义的fields字段其他自动生成的字段全部忽略索引体积立刻缩小了将近一半检索速度也快了不少。4.4 多行日志被拆成多行记录Java应用里经常有带堆栈信息的异常日志比如下面这种2025-06-14 10:15:30.123 ERROR [order-service] - something went wrong java.lang.NullPointerException: null at com.example.OrderService.process(OrderService.java:88) at com.example.OrderController.handle(OrderController.java:25)默认配置下filebeat会把每一行作为一条独立日志记录导致一条异常日志在ES里变成了三四条记录检索的时候上下文完全断开。要解决这个问题需要在filebeat配置里启用multiline匹配但log-pilot默认的配置里没有开启。我的做法是给log-pilot挂载一个自定义的filebeat配置通过ConfigMap覆盖默认配置启用multiline。具体思路是新建一个filebeat.yml配置文件在multiline.pattern里设置匹配规则将不以时间戳开头的行合并到上一条。在log-pilot的DaemonSet中追加一个Volume挂载把它放到/etc/pilot/config路径下即可。不过说实话这个改造需要动log-pilot的yaml如果你的log-pilot是0.1版本filebeat内核比较老multiline的配置语法和现在的7.x版本有差异。我在生产环境里更推荐用独立的filebeat容器来替代log-pilot做带多行日志需求的采集这样配置控制力更强而log-pilot主要用于常规日志场景。4.5 日志时间戳时区不对还有一个高频坑是时区问题。Docker容器默认使用UTC时区如果你的业务应用没有显式设置时区环境变量日志打印的时间戳就是UTC时间。log-pilot采集到的timestamp字段也是UTC时间Kibana默认按浏览器时区展示如果服务器和浏览器时区不一致你看到的日志时间就会比实际慢8个小时。解决办法有两个层面。一是业务侧做调整在部署业务容器时设置TZAsia/Shanghai环境变量这是最彻底的做法。二是在Kibana展示侧调整时区设置在Stack Management里把默认时区改成GMT8但这样只是展示层补偿如果日志本身的时间戳是通过String类型保存的你看到的还是业务自己打的字符串那个不受影响。从收集完整性的角度讲我建议两边都做业务容器显式指定时区Kibana侧也把时区配置正确这样日志时间轴和实际事件发生时间一致排查问题时才能对得上。5. 生产环境落地的一些额外提醒5.1 资源规划与性能调优log-pilot虽然轻量但也不是完全没有资源消耗。在日志量大的节点上filebeat底层的文件监听和ES写入会占用一定的IO和带宽。我之前在一天产生50GB日志的节点上看过log-pilot的CPU使用率能到10%左右网络出口占用直接拉满。这个时候就要考虑在ES端做限流或者在其前面加一层消息队列缓冲。log-pilot本身暴露了一些filebeat的调优参数比如filebeat.spool_size、filebeat.idle_timeout可以通过环境变量直接传进去。我一般把spool_size调大到2048减少ES写入次数对单条日志量小的场景有明显帮助。还有queue.mem.events控制内存队列大小默认是4096在突发流量下可以适当调大但注意别把内存撑爆。5.2 日志数据生命周期管理日志采集只是第一步后续的存储和清理才真正挑战运维能力。ES数据不能无限增长尤其是业务量大的系统几天不清理索引就可能把磁盘塞满。建议配置ES的Index Lifecycle Management策略按天切分的索引7天或30天自动转移到冷节点并最终删除具体保留周期依据业务需求来定。log-pilot生成的索引名天然适合做这项策略日期后缀让归档和清理变得非常直接。我在生产环境就是借助ILM策略设置30天热节点保留、60天冷节点归档、90天删除磁盘占用保持在一个可控范围内。5.3 结合k8s集群其他常见需求来扩展后面其实可以做很多扩展方向。可以把log-pilot的输出从ES换成Kafka流量先进消息队列再由下游的消费端决定是进ES还是做实时告警。对于需要快速检索的业务日志直接ES自然是最快的对于更偏向于监控告警的指标类日志配合日志采集结果接入Prometheus告警规则做统一监控也是一种常见思路。另外如果集群规模大了需要收集多个集群的日志并统一展示可以考虑在每个集群部署一套log-pilot统一将日志写入同一个ES集群在索引级别上做隔离。这比一套集群一套Kibana的运维方式要轻松得多。我个人在实际操作中的体会是log-pilot解决的是如何低成本地把容器日志从业务POD里搬出来这个根本问题它的label机制一旦理解透了配置效率比手写filebeat的prospector快很多。但它毕竟是一个比较早的项目社区的更新频率不高在做技术选型的时候需要结合团队对日志系统的长期规划来做判断。如果是几十个节点的中小规模集群log-pilot完全够用且实现成本极低如果是超大规模集群并且有复杂的流式处理、多行合并、脱敏需求那还是推荐用新一点的技术栈比如Fluent Bit加上Loki或者对应的ES插件取舍之间看具体情况。最后再分享一个小技巧在测试环境验证log-pilot是否正常工作时可以先用一个临时容器带上一组测试label然后到ES里查询索引是否创建成功不用急着把业务容器全部接入。等链路完全验证通了再逐步给业务容器加label灰度放量这样就算出问题影响面也只有一个测试容器而已。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进