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用戶所屬,可以在配置中設置切換用戶
/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_metadata、add_kubernetes_metadata、add_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集群里面。

浙公網安備 33010602011771號