Kubernetes 日志收集的原理,看这一篇就够了

Kubernetes 日志收集的原理,看这一篇就够了-腾讯云开发者社区-腾讯云

准备

关于容器日志

Docker的日志分为两类,一类是 Docker引擎日志;另一类是容器日志。引擎日志一般都交给了系统日志,不同的操作系统会放在不同的位置。本文主要介绍容器日志,容器日志可以理解是运行在容器内部的应用输出的日志,默认情况下,docker logs 显示当前运行的容器的日志信息,内容包含 STOUT(标准输出) 和 STDERR(标准错误输出)。日志都会以 json-file 的格式存储于/var/lib/docker/containers/<容器id>/<容器id>-json.log,不过这种方式并不适合放到生产环境中。

  • 默认方式下容器日志并不会限制日志文件的大小,容器会一直写日志,导致磁盘爆满,影响系统应用。(docker log-driver 支持log文件的rotate)
  • Docker Daemon 收集容器的标准输出,当日志量过大时会导致Docker Daemon 成为日志收集的瓶颈,日志的收集速度受限。
  • 日志文件量过大时,利用docker logs -f 查看时会直接将Docker Daemon阻塞住,造成docker ps等命令也不响应。

Docker提供了logging drivers配置,用户可以根据自己的需求去配置不同的log-driver,可参考官网 Configure logging drivers[1] 。但是上述配置的日志收集也是通过Docker Daemon收集,收集日志的速度依然是瓶颈。

log-driver 日志收集速度 syslog 14.9 MB/s json-file 37.9 MB/s

能不能找到不通过Docker Daemon收集日志直接将日志内容重定向到文件并自动 rotate的工具呢?答案是肯定的采用S6[2]基底镜像。

S6-log 将 CMD 的标准输出重定向到/…/default/current,而不是发送到 Docker Daemon,这样就避免了 Docker Daemon 收集日志的性能瓶颈。本文就是采用S6[3]基底镜像构建应用镜像形成统一日志收集方案。

关于k8s日志

k8s日志收集方案分成三个级别:

1、应用(Pod)级别 2、节点级别 3、集群级别

  • 应用(Pod)级别

Pod级别的日志 , 默认是输出到标准输出和标志输入,实际上跟docker 容器的一致。使用 kubectl logs pod-name -n namespace 查看,具体参考[4]。

  • 节点级别

Node级别的日志 , 通过配置容器的log-driver[5]来进行管理 , 这种需要配合logrotare[6]来进行 , 日志超过最大限制 , 自动进行rotate操作。

  • 集群级别

集群级别的日志收集 , 有三种

  • 节点代理方式,在node级别进行日志收集。一般使用DaemonSet部署在每个node中。这种方式优点是耗费资源少,因为只需部署在节点,且对应用无侵入。缺点是只适合容器内应用日志必须都是标准输出。
  • 使用sidecar container作为容器日志代理,也就是在pod中跟随应用容器起一个日志处理容器,有两种形式:

一种是直接将应用容器的日志收集并输出到标准输出(叫做Streaming sidecar container),但需要注意的是,这时候,宿主机上实际上会存在两份相同的日志文件:一份是应用自己写入的;另一份则是 sidecar 的 stdout 和 stderr 对应的 JSON 文件。这对磁盘是很大的浪费 , 所以说,除非万不得已或者应用容器完全不可能被修改。

另一种是每一个pod中都起一个日志收集agent(比如logstash或fluebtd)也就是相当于把方案一里的 logging agent放在了pod里。但是这种方案资源消耗(cpu,内存)较大,并且日志不会输出到标准输出,kubectl logs 会看不到日志内容。

  • 应用容器中直接将日志推到存储后端,这种方式就比较简单了,直接在应用里面将日志内容发送到日志收集服务后端。

日志架构

通过上文对k8s日志收集方案的介绍,要想设计一个统一的日志收集系统,可以采用节点代理方式收集每个节点上容器的日志,日志的整体架构如图所示。

解释如下:

  • 所有应用容器都是基于s6基底镜像的,容器应用日志都会重定向到宿主机的某个目录文件下比如/data/logs/namespace/appname/podname/log/xxxx.log
  • log-agent 内部 包含 filebeat[7] ,logrotate 等工具,其中filebeat是作为日志文件收集的agent
  • 通过filebeat将收集的日志发送到kafka
  • kafka在讲日志发送的es日志存储/kibana检索层
  • logstash 作为中间工具主要用来在es中创建index和消费kafka 的消息

整个流程很好理解,但是需要解决的是

  • 用户部署的新应用,如何动态更新filebeat配置,
  • 如何保证每个日志文件都被正常的rotate,
  • 如果需要更多的功能则需要二次开发filebeat,使filebeat 支持更多的自定义配置。

付诸实践

解决上述问题,就需要开发一个log-agent应用以daemonset形式运行在k8s集群的每个节点上,应用内部包含filebeat,logrotate,和需要开发的功能组件。

第一个问题,如何动态更新filebeat配置,可以利用http://github.com/fsnotify/fsnotify[8] 工具包监听日志目录变化create、delete事件,利用模板渲染的方法更新filebeat配置文件

第二个问题,利用http://github.com/robfig/cron[9] 工具包 创建cronJob,定期rotate日志文件,注意应用日志文件所属用户,如果不是root用户所属,可以在配置中设置切换用户

代码语言:javascript
代码运行次数:0
运行
AI代码解释
 
/var/log/xxxx/xxxxx.log {
      su www-data www-data
      missingok
      notifempty
      size 1G
      copytruncate
    }

第三个问题,关于二次开发filebeat,可以参考博文 https://www.jianshu.com/p/fe3ac68f4a7a[10]

总结

本文只是对k8s日志收集提供了一个简单的思路,关于日志收集可以根据公司的需求,因地制宜。

基于filebeat二次开发Kubernetes日志采集 - 简书

目前最为主流的容器编排工具主要有kubernetes、mesos、swarm,个人不评价谁好谁坏因为每个东西都有自己的优势。不过个人认为目前关注度最高的应该当属kubernetes,现在越来越多的公司采用kubernetes作为底层编排工具开发自己的容器调度平台。既然是一个PAAS平台那么就应该提供一个计算监控等一体的服务,因为是在kubernetes运行上面的容器大多数都是无状态服务,所以统一的日志管理又是其中必不可少的一部分。下面我们就讲一下如何基于filebeat开发属于自己的日志采集。

目前用的最多的日志管理技术应该是ELK,E应该没有太多的疑问基本上很多公司都是采用的这个作为存储索引引擎。L及logstash是一个日志采集工具支持文件采集等多种方式,但是基于容器的日志采集又跟传统的文件采方式略有不同,虽然docker本身提供了一些log driver但是还是无法很好的满足我们的需求。现在kubernetes官方有一个日志解决方案是基于fluentd的。至于为什么最后选择采用filebeat而没有用fluentd主要有一下几点:

  • 首先filebeat是go写的,我本身是go开发,fluentd是ruby写的很抱歉我看不太懂
  • filbeat比较轻量,filbeat现在功能虽然比较简单但是已经基本上够用,而且打出来镜像只有几十M
  • filbeat性能比较好,没有具体跟fluentd对比过,之前跟logstash对比过确实比logstash好不少,logtash也是ruby写的我想应该会比fluentd好不少
  • filbeat虽然功能简单,但是代码结构非常易于进行定制开发
  • 还有就是虽然用了很久fluentd但是fluentd的配置文件实在是让我很难懂

filebeat如何采集kubernetes日志

所以基于以上几点决定采用filebeat开发了自己的日志采集。
filebeat的Github地址是https://github.com/elastic/beats里面囊括了好几个项目其中就包括filebeat。

和其他的日志采集处理一样filebeat也有几个部分分别是input、processors、output,不过filebeat提供的能力还比较少,不过无所谓够用就好。

filebeat提供了一个add_kubernetes_metadata的processor,文件的采集路径就要配成/var/lib/docker/containers/*/*-json.log主要是监听kubernetes的apiserver把容器对应的pod的信息存到内存里面,从文件日志source里面(就是上面的那个路径)里面获取容器id匹配得到pod的信息。
因为json.log文件里面的日志都是json格式的所以需要对日志进行json格式化,filebeat有一个processor叫decode_json_fields这些processor都支持条件判断,可以通过条件判断来绝对是否要对某一条日志进行处理。filebeat默认的日志字段是message但是*-json.log解析出来以后的日志字段是log,如果同时配置了其他的日志采集这个时候所用的存储日志的字段就不一样了,所以需要对它们进行处理让它们使用同一个字段,但是filebeat并没有提供这个功能所以自己写了一个add_fields的功能。

整理后的配置文件如下:

filebeat.prospectors:
- type: log
  paths:
    - /var/lib/docker/containers/*/*-json.log
    - /var/log/containers/applogs/*

processors:
- add_kubernetes_metadata:
    in_cluster: false
    host: "127.0.0.1"
    kube_config: /root/.kube/config
- add_fields:
    fields:
      log: '{message}'
- decode_json_fields:
    when:
       regexp:
         log: "{*}"
    fields: ["log"]
    overwrite_keys: true
    target: ""
- drop_fields:
     fields: ["source", "beat.version", "beat.name", "message"]
- parse_level:
     levels: ["fatal", "error", "warn", "info", "debug"]
     field: "log"

logging.level: info
setup.template.enabled: true
setup.template.name: "filebeat-%{+yyyy.MM.dd}"
setup.template.pattern: "filebeat-*"
#setup.template.fields: "${path.config}/fields.yml"
setup.template.fields: "/fields.yml"
setup.template.overwrite: true
setup.template.settings:
   index:
     analysis:
       analyzer:
         enncloud_analyzer:
           filter: ["standard", "lowercase", "stop"]
           char_filter: ["my_filter"]
           type: custom
           tokenizer: standard
       char_filter:
         my_filter:
           type: mapping
           mappings: ["-=>_"]

output:
  elasticsearch:
    hosts: ["127.0.0.1:9200"]
    index: "filebeat-%{+yyyy.MM.dd}"

如果线上环境filebeat也是以daemonset的方式运行在kubernetes集群里面,所以in_cluster就需要设置成true,对应的kube_config则不需要配置了,host参数则是监听的某一个节点的pod,所以这个值应该是filebeat运行所在节点的pod的名称,当然也可以不写,那样的话就是监听全局的pod,不过这个对于filebeat来说是没必要的也是不好的。

add_fieldsprocessor可以添加自己想要的字段,值可以是字符串也可以是{message}格式,如果是这种格式则会从已有的字段里面取值进行填充。

parse_levelprocessor是用于一个匹配日志格式的功能,如果日志文件最前面出现的那个日志级别则这个日志加一个相应级别的字段。

filebeat还有对于template处理的功能的功能可以指定所用的mapping。

开发filebeat processor

使用的过程中主要是针对一些不满足的processor进行了开发,filebeat的代码结构非常清晰抽象也很好,可以很简单的进行开发。
filebeat的processor功能主要放在libbeat和filbeat同级的目录下,在这个目录下就叫processors。可以看到里面有actions,add_cloud_metadataadd_kubernetes_metadataadd_docker_metadata所以filebeat也只支持直接docker的processor的,比较普通的processor都是放在actions下面的所以如果我们需要开发一些简单的processor的话可以直接放到下面,包括decode_json和drop_event等也是放在下面的。以add_field为例:

package actions

import (
    "fmt"
    "regexp"
    "strings"

    "github.com/elastic/beats/libbeat/beat"
    "github.com/elastic/beats/libbeat/common"
    "github.com/elastic/beats/libbeat/processors"
)

type addFields struct {
    Fields map[string]string
    reg    *regexp.Regexp
}

func init() {
    processors.RegisterPlugin("add_fields",
        configChecked(newAddFields,
            requireFields("fields"),
            allowedFields("fields", "when")))
}

func newAddFields(c *common.Config) (processors.Processor, error) {
    config := struct {
        Fields map[string]string `config:"fields"`
    }{}
    err := c.Unpack(&config)
    if err != nil {
        return nil, fmt.Errorf("fail to unpack the add_fields configuration: %s", err)
    }

    f := &addFields{Fields: config.Fields, reg: regexp.MustCompile("{(.*)}")}
    return f, nil
}

func (f *addFields) Run(event *beat.Event) (*beat.Event, error) {
    var errors []string
    for field, value := range f.Fields {
        matchers := f.reg.FindAllStringSubmatch(value, -1)
        if len(matchers) == 0 {
            event.PutValue(field, value)
        } else {
            if len(matchers[0]) >= 2 {
                val, err := event.GetValue(strings.Trim(matchers[0][1], " "))
                if err != nil {
                    errors = append(errors, err.Error())
                } else {
                    event.PutValue(field, val)
                }
            }
        }
    }
    return event, nil
}

func (f *addFields) String() string {
    var fields []string
    for field, _ := range f.Fields {
        fields = append(fields, field)
    }
    return "add_fields=" + strings.Join(fields, ", ")
}

需要定义自己的struct, newAddFields方法通过配置文件初始化自己的struct。并在init里面通过RegisterPlugin把自己的processor注册进去。这个struct主要是要实现Run方法,这个方法就是对于每一条日志event的具体处理。

到这就基本上实现了对接kubernetes的对接改造就基本上完成了,当然还有其他很多工作可以做,比如golang本身的regex和encoding/json性能比较差,这些都是可以优化的地方。

我自己fork出来的地址是https://github.com/yiqinguo/beats增加了Makefile直接编译打镜像,和filebeat-ds.yml直接发到kubernetes集群里面。

 

posted @ 2025-08-15 17:26  CharyGao  阅读(14)  评论(0)    收藏  举报