
聊一个老生常谈但又绕不开的话题Kubernetes环境里的日志收集。我在生产集群里折腾过filebeat、fluentd、也写过一批sidecar容器最后真正稳定跑起来的反而是阿里开源的log-pilot。这篇文章不打算讲什么高大上的架构就说说我为什么最终选了它、怎么在k8s里把ESKibana这条日志链路完整搭起来以及那些文档里不会写、只有踩过坑才知道的细节。如果你正处在把业务从docker迁移到k8s的阶段或者刚部署完集群不知道该用哪套日志方案这篇应该能帮你省下不少弯路。1. 日志收集方案选型从痛点出发为什么是log-pilot1.1 容器日志收集的难点在哪单机环境下想看日志就登录服务器tail -f /var/log/app.log简单粗暴。到了k8s里这套思路彻底行不通了——Pod是随时会被重建的实例在节点之间漂移IP、容器ID天天变。日志文件分散在每个节点的宿主机上如果你还沿用人肉登录的方式查问题等于开着几十台机器挨个翻文件效率低到离谱。更深一层的痛点是“动态发现”。k8s里的Pod生命周期极短扩容、缩容、滚动更新是常态。一套日志收集方案如果不能在Pod创建的一瞬间就知道该收哪些日志、从哪个路径收、收完打上什么标签那它就是个残废。更别说日志还要跟namespace、Pod名称、容器名这类元数据关联起来否则你在Kibana里搜一段异常日志根本不知道是哪个服务哪个实例打出来的。1.2 log-pilot的核心机制环境变量驱动的动态采集log-pilot解决这个问题的思路很有意思它不依赖你在每个Pod里塞一个日志代理而是利用k8s和Docker的运行时事件来自动发现。它运行在每个节点上作为一个DaemonSet常驻。节点上只要有新容器创建log-pilot就能感知到然后去读取这个容器的环境变量——只要环境变量满足aliyun_logs_前缀的约定它就自动创建一条采集任务。采集到的日志统一走管道进入后端落Kafka、落ES都可以。核心优势说白了就是八个字约定优于配置。开发同学不需要懂日志采集原理只需要在自己应用的Deployment里加上几个环境变量日志就自动开始收集不用新增sidecar容器不用改动业务代码。采集任务随容器创建而生成、随容器销毁而回收天然适配k8s的调度特性。1.3 和其他方案的对比我为什么没选它们市面上主流的容器日志方案有这么几类DaemonSetfilebeat、DaemonSetfluentd、sidecar模式加上log-pilot。我列个表对比下实际体验方案动态发现能力元数据关联资源开销配置复杂度适用场景filebeat DaemonSet弱需要额外配置一般低中采集规则要自己维护日志格式高度统一的场景fluentd DaemonSet中强偏高高配置语法复杂需要复杂过滤/转换的场景sidecar日志容器无手动声明强高每Pod额外一个容器中日志格式差异极大且数量少log-pilot强容器事件驱动强低低环境变量声明即可多业务共集群、格式多样的场景用sidecar最直观的问题就是资源浪费。每个Pod多起一个日志容器CPU和内存都翻倍百十个Pod下来成本感人。filebeat按DaemonSet部署是轻量但每个业务的日志路径、格式、索引命名都不一样时你要维护一堆prospector配置新业务接入还得改配置重启采集器。log-pilot把“采集什么”下沉到了业务Pod的环境变量里对于多团队共用一个集群的场景特别友好。每个团队自己管自己的日志规则运维不用天天替别人改采集配置。2. 部署前准备这些坑越早处理越省心2.1 先解决集群初始化阶段的健康检查报错很多人在k8s集群还没完全健康的时候就急着往上堆日志组件结果全链路排查时到处是坑。最典型的现象就是执行kubeadm init时明明等了好几分钟最后一行却提示类似the api server is not healthy after 4m0.00747357s之类的信息。这个报错看起来吓人本质就是初始化超时apiserver在指定时间内没通过健康检查。我处理过的案例里九成是下面几种原因apiserver容器本身起不来最常见的是kubelet和容器运行时的cgroup driver配置不一致节点时间不同步证书校验直接失败6443端口被占用或者防火墙拦截健康检查连不上swap分区没关kubelet启动参数和初始化检查冲突pause镜像拉不下来导致容器无法正常拉起遇到这个报错别急着重新init。先看apiserver容器状态确定pause镜像没问题再检查kubelet日志里的具体报错。确定集群本身干干净净、所有节点都是Ready状态再考虑部署日志方案。否则日志组件进来了元数据采集乱成一团排查问题的时候更疼。2.2 运行时差异决定log-pilot能不能用这是选择log-pilot之前最需要确认的一点。log-pilot通过监听Docker事件、读取/var/run/docker.sock来感知容器创建并对接Docker日志驱动收集标准输出。如果你的集群还在用Docker作为运行时一切顺畅但如果你的集群已经换成了containerd宿主上根本没有docker.sock这个文件log-pilot就直接失去了容器发现能力。新版本的k8s默认基本都是containerd了所以这里要提前确认清楚用containerd的集群就别硬套log-pilot了改选fluent-bit或者vector这类原生支持CRI接口的采集器省得DaemonSet跑起来一个节点一个报错。还在用docker运行时的话log-pilot仍然是一个性价比很高的选择社区里大量老集群一直用它跑得很好。2.3 磁盘轮转、资源预留与权限准备部署前还有三件小事每件都能在关键时刻救你一命。第一宿主机上Docker的日志轮转必须提前配好。Docker默认的json-file日志驱动如果不限制大小一个高并发的业务容器能把节点磁盘写满到时候不仅日志丢其他Pod也会跟着遭殃。建议在Docker daemon配置里加上max-size: 100m、max-file: 5这样的轮转参数先把日志分片上限卡住。第二给log-pilot预留足够的系统资源。它虽然是轻量级采集器但高流量场景下依然吃CPU和内存建议按每节点至少100m CPU、256Mi内存起步来规划给ES和业务Pod之间留出缓冲空间。第三RBAC权限提前建好。log-pilot至少要能读取集群中的Pod和namespace信息才能给日志打上元数据标签。没有权限的话日志能收到但全是裸数据Kibana里上哪儿查归属去。3. 实操在k8s里完整部署log-pilot并接入ES3.1 部署YAML拆解RBAC DaemonSet我用的是最常见的log-pilot镜像registry.cn-hangzhou.aliyuncs.com/acs/log-pilot:latest。整套部署分两部分先是ServiceAccount和RBAC权限然后是DaemonSet本体。apiVersion: v1 kind: ServiceAccount metadata: name: log-pilot namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: log-pilot rules: - apiGroups: [, apps, extensions] resources: [namespaces, pods, services, endpoints, nodes, events] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: log-pilot roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: log-pilot subjects: - kind: ServiceAccount name: log-pilot namespace: kube-system权限这块我见过不少人忽略。没有ClusterRole权限log-pilot也能收到日志但日志里不会带namespace、Pod名这些关键元数据等于给你一堆盲盒日志查问题时毫无头绪。DaemonSet部分我贴一段核心配置apiVersion: apps/v1 kind: DaemonSet metadata: name: log-pilot namespace: kube-system labels: k8s-app: log-pilot spec: selector: matchLabels: k8s-app: log-pilot template: metadata: labels: k8s-app: log-pilot spec: serviceAccountName: log-pilot tolerations: - operator: Exists hostNetwork: true containers: - name: log-pilot image: registry.cn-hangzhou.aliyuncs.com/acs/log-pilot:latest imagePullPolicy: Always env: - name: LOGGING_OUTPUT value: elasticsearch - name: ELASTICSEARCH_HOST value: elasticsearch.default.svc.cluster.local - name: ELASTICSEARCH_PORT value: 9200 - name: ELASTICSEARCH_INDEX value: log-pilot - name: ELASTICSEARCH_TYPE value: log volumeMounts: - name: docker-socket mountPath: /var/run/docker.sock - name: container-logs mountPath: /var/log/containers - name: docker-logs mountPath: /var/lib/docker/containers - name: pilot mountPath: /var/lib/log-pilot resources: requests: cpu: 100m memory: 256Mi limits: memory: 512Mi volumes: - name: docker-socket hostPath: path: /var/run/docker.sock - name: container-logs hostPath: path: /var/log/containers - name: docker-logs hostPath: path: /var/lib/docker/containers - name: pilot hostPath: path: /var/lib/log-pilot type: DirectoryOrCreate两个配置点值得展开说说。第一tolerations: - operator: Exists。这个容忍度让log-pilot可以调度到带有污点的节点上包括master节点。如果不加master节点上的Pod日志就永远收不到而很多系统组件恰恰跑在master上。第二环境变量里的三个ES参数就是采集器的后端出口。ELASTICSEARCH_HOST我用的是k8s集群内的Service域名你只需要把elasticsearch.default.svc.cluster.local换成自己ES服务对应的地址端口一般就是9200。3.2 应用侧声明采集规则环境变量是灵魂log-pilot之所以让开发同学接受度高就是因为接入方式极其简单在自己应用的Pod环境变量里以aliyun_logs_为前缀命名变量即可。变量名的后半部分就是日志库名称变量值则是你要采集的日志来源。我实际用过的几种姿势env: - name: aliyun_logs_access value: stdout - name: aliyun_logs_error value: stderr - name: aliyun_logs_app_log value: /var/log/app/*.log第一行含义是采集这个容器的标准输出在ES里的索引逻辑归到access这个logstore下。第二行同理采集标准错误输出。第三行更灵活采集容器内/var/log/app/目录下所有.log文件支持通配符。这里有个细节如果你不想为每条日志单独起一个环境变量可以直接用通配符aliyun_logs_*stdout意思是将容器所有标准输出都采集进去。但我不建议一上来就这么干因为后面你想在Kibana里按业务类型区分日志时全都糊在一个logstore里会非常痛苦。建议宁可多写几个环境变量也别图省事一把梭。3.3 完整示例跑一个Nginx验证链路空谈配置太虛直接拿Nginx跑一遍。我创建一个测试DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: nginx-log-test namespace: default spec: replicas: 1 selector: matchLabels: app: nginx-log template: metadata: labels: app: nginx-log spec: containers: - name: nginx image: nginx:1.24 ports: - containerPort: 80 env: - name: aliyun_logs_nginx_access value: stdout创建完这个Deployment之后log-pilot会立刻感知到新容器启动根据环境变量aliyun_logs_nginx_access开始采集Nginx的标准输出。验证链路是否通了我的习惯做法是先看log-pilot采集端日志kubectl logs -n kube-system -l k8s-applog-pilot如果采集正常正常情况下是持续输出INFO级别日志包含created、start这类关键词。然后在Kibana里创建一个log-pilot-*的索引Pattern按时间倒序查看能看到Nginx访问日志每一条都带着kubernetes.namespace_name: default、kubernetes.pod_name: nginx-log-test-xxx这类标签。3.4 有状态应用的日志采集以Redis集群为例再多说一种场景就是有状态应用。很多人觉得日志收集只关心Deployment这类无状态工作负载实际上Redis集群、数据库这类有状态服务更需要日志。我维护过一套Redis集群Pod数量多、IP漂移频繁手动去节点上翻日志完全不可行。log-pilot处理这类场景的优势在于自动关联元数据。它会把每个Pod的namespace、pod_name、container_name这些信息都打进日志记录里。Redis Pod无论怎么重建、无论漂到哪个节点你在Kibana里按kubernetes.pod_name过滤永远能定位到某一次重启前后的完整日志。这比在宿主机上根据容器ID找日志要优雅得多。Redis的日志也建议直接打到stdout环境变量一行aliyun_logs_redis_logstdout就搞定了。如果你非要采Redis的慢日志文件那就需要把日志目录用hostPath或emptyDir方式暴露出来然后在环境变量中配置对应的宿主机路径。不过能用stdout解决的问题不建议引入额外存储逻辑。4. 生产环境调优把log-pilot用稳的几点经验4.1 多行日志与业务时间字段最典型的问题是Java应用抛异常时堆栈信息在stdout里是几十行连在一起的。log-pilot默认按行采集一个堆栈会被拆成几十条日志记录在Kibana里看起来碎片化严重特别影响排查效率。我的建议是在业务侧从源头解决让代码把异常堆栈拼成一条整体的字符串再输出文件日志同理尽量输出单行JSON格式。这类日志对采集器最友好后续做字段解析、告警分析都省事。如果实在改不动代码就在下游Logstash或ES ingest pipeline里用multiline正则把多行日志重新合并不要指望log-pilot在采集端帮你做这个事。另外一个高频需求是用业务自己的时间字段作为日志时间而不是采集时间。尤其处理历史数据补录的时候采集时间和业务时间可能相差很久。实践中我会在Logstash配置里用date插件解析业务日志里的timestamp字段替换掉ES里的timestamp这样Kibana排序、筛选才能对齐业务实际发生时间。4.2 时区与索引生命周期时区坑几乎人人踩。log-pilot输出的日志时间默认是UTC如果你的业务在国内直接看Kibana会发现所有日志都比本地时间慢8小时。看起来是小事但线上排查故障时容易造成误判。处理方式我一般分成两层如果只是展示问题Kibana的高级设置里可以配置时区偏移如果下游有告警和数据分析系统建议在消费端统一加上时区转换规则避免有的系统用了Asia/Shanghai、有的还停留在UTC最后对不上账。索引生命周期同样要提前规划。log-pilot默认按天滚动索引这本身是合理的但要注意控制索引分片数量。我见过有人图省事把所有日志灌进一个索引分片数还开得特别大ES集群查询性能直接崩掉。比较稳妥的做法是按业务场景划分索引前缀比如log-pilot-nginx-*、log-pilot-redis-*然后用ILM策略设置保留时长超过7天或30天的自动删除。这样既能控制存储成本也方便Kibana里隔离查询。4.3 高流量下的资源与缓冲区调优大流量场景下log-pilot最脆弱的环节不在采集而在缓冲区。当ES出现短暂不可用或者网络抖动时采集器本地会积压日志内存和磁盘占用瞬间飙升。如果节点的/var/lib/log-pilot目录空间不足甚至可能丢日志。我经历过一次ES集群磁盘告警后大量日志滞留在log-pilot的缓冲目录里等ES恢复后积压的数据一下子全灌进去又给ES造成二次压力。从那以后我就学乖了给log-pilot的内存limit留足了余量同时监控采集器本身的日志和缓冲目录大小。ES集群扩容、索引重建这类运维操作前先把日志量预期评估好避免一边恢复一边涌入。还有一点容易被忽略业务容器日志量的不均匀会导致节点负载不均。比如某个Node上压着一个打印狂魔应用日志走量特别大这个节点上的log-pilot CPU使用率明显比其他节点高。遇到这种情况要么给业务容器加max-size限制要么结合Pod反亲和性把高日志量Pod尽量分散到不同节点。4.4 日志安全与多团队隔离一个集群多个团队共用时日志归属和权限边界必须事先定义好。log-pilot采集时会自动把namespace作为元数据打上所以最自然的安全边界就是按namespace隔离。如果你的团队结构比较复杂建议在Kibana里按namespace配置只读权限谁只能看自己团队的日志。不要为了图方便给所有人都开全量日志查询权限一旦某个同事误操作或误导出敏感日志责任很难划分。这在金融机构、政务类项目里尤其重要虽然k8s本身不背这个锅但日志平台作为数据汇聚点权限治理早晚要跟上。5. 故障排查实录从现象到原因的速查手册日志收集链路长了问题五花八门。我把这半年多遇到的典型故障整理成一张速查表方便你对照排查现象可能原因排查思路处理办法Pod日志完全收不到容器环境变量没写对检查Pod yaml里的aliyun_logs_前缀是否拼写正确修正后重建Pod只有部分节点有日志DaemonSet被污点排斥kubectl get pods -n kube-system -o wide看log-pilot分布增加tolerations容忍全部污点日志到了ES但无元数据RBAC权限不足查看采集器日志是否有403错误补全ClusterRole的list/watch权限Nginx访问日志有了业务日志没有业务日志路径与配置不符用kubectl exec进容器确认日志文件真实路径修改环境变量路径Kibana查询很慢ES分片规划不合理查看节点分片数量是否过多调整索引分片数启用ILM采集器内存持续走高ES不可用导致缓冲区积压查看采集器缓冲目录大小和ES集群状态先恢复ES再观察积压数据排空日志时间比本机慢8小时UTC和本地时区不一致查看timestamp字段实际值在下游转换时区或Kibana设置里偏移除了表格里的这些再分享一个我经常用来验证整个链路的小办法。起一个临时Pod让它每隔一秒打一条有规律的日志然后去Kibana里按这个Pod的namespace和Pod名过滤看日志是否实时到达。这个办法能快速区分问题出在采集端、传输端还是ES端避免在错误的方向上浪费时间。另一个实用技巧是查看log-pilot容器自身的日志。如果采集任务正常创建log-pilot日志里会出现对应的采集目标如果环境变量写得不规范它会直接跳过并在日志里输出告警信息。所以每次排查日志丢失第一站永远是kubectl logs -n kube-system log-pilot-xxx别一上来就怀疑ES丢数据。还有个冷门但很实际的坑宿主机磁盘写满后Docker容器可能直接挂掉log-pilot连自身日志都写不出去。这时候排查要先把磁盘空间清理出来再谈其他。日志系统本身也是需要被监控的我后来专门给log-pilot的日志加了一条Kafka告警采集器持续无输出就立刻报警防止日志服务挂了自己却不知道。最后再分享一个我个人的使用体会log-pilot优点突出但对运行时的依赖也限制死了它的适用边界。如果你正处在containerd时代的新集群里不用对它念念不忘fluent-bit完全能接替这个位置但如果你手上还有一批docker运行时下的老集群log-pilot用起来是真的省心尤其是让开发同学自己管理采集规则这个体验我用过一轮之后再也不想回到那种“每个人都来提需求让你改采集配置”的日子了。