日志系統之基于flume收集docker容器日志
來自: http://blog.csdn.net/yanghua_kobe/article/details/50642601
最近我在日志收集的功能中加入了對docker容器日志的支持。這篇文章簡單談談策略選擇和處理方式。
關于docker的容器日志
docker 我就不多說了,這兩年火得發燙。最近我也正在把日志系統的一些組件往docker里部署。很顯然,組件跑在容器里之后很多東西都會受到容器的制約,比如日志文件就是其中之一。
當一個組件部署到docker中時,你可以通過如下命令在標準輸出流(命令行)中查看這個組件的日志:
docker logs ${containerName}
日志形如:
但這種方式并不能讓你實時獲取日志并對它們進行收集。但是docker還是比較友好的,它把這些日志文件都保存在以容器ID為文件名的文件系統中。如果你是標準安裝的話,那么它應該在文件系統的如下位置:
/var/lib/docker/containers/${fullContainerId}/${fullContainerId}-json.log
這個 fullContainerId 應該如何獲得呢?簡單一點,你可以通過如下命令來查看full container-id:
docker ps --no-trunc
然后通過vi 命令來查看日志文件。但基于文件的日志和基于標準輸出流的日志是有區別的,區別是基于文件的日志是json形式的,并且以標準輸出流的一行作為日志的間隔。形如:
這相當于兩層日志格式,外面這一層是docker封裝的,格式是固定的;而內層則是因具體的組件而不同的。外面的格式其實對我們而言是無用的,但還是要先解析完外層日志之后,才能回到我們收集組件格式的上下文中來。
如果這是docker給我們日志收集帶來的麻煩之一,那么下面還有一個更棘手的問題就是: 多行日志的關聯性問題 。比較常見的一個例子就是程序的異常堆棧(stacktrace)。因為在標準輸出流中,這些異常堆棧是分多行輸出的,所以在docker日志中一個異常堆棧被以多條日志拆開記錄就像上面的示例日志一樣。
其實在基于非docker日志文件的日志收集中,我們已經針對以異常堆棧為主的多行關聯性日志的收集進行了支持,但現在的一個問題是docker不但把關聯性日志拆成多條,而且在外面包裹了自己的格式,導致我們在不解析的情況下根本拿不到真正的日志分隔符,日志分隔符用于區分多行日志內容中真正的日志分隔界限。比如上圖示例的log4j日志,我們通過判斷行首前綴是否有 [ ,來判斷某一行是一條日志的起點還是應該被追加到上一條日志中。
處理方案
客戶端不解析
在沒有遇到docker容器日志之前,我們遵循的規則是: agent只負責采集,不作任何解析 ,解析在storm里進行。針對上面這種docker容器的多行關聯性日志,在客戶端不解析自然沒辦法識別關聯性,那么就只能作逐行收集,然后在服務端解析。如果在服務端解析,就要保證同一個日志文件中日志的順序性。
- 基于隊列的順序性
我說的這種隊列是日志收集之后暫存在消息中間件中的消息隊列。這可以確保日志在解析之前一直保證順序性,但這樣的代價顯然是很高的,為了一個節點上的一種日志就要單開一個隊列,那么多節點上的多日志類型將會使得消息中間件中的隊列快速增多,而性能開銷也非常大。并且還有個問題是,單純保證在消息隊列里有序還不夠,還必須讓消費者(比如storm)的處理邏輯針對這個隊列是單一的,如果一個消費者負責多個不同的日志隊列,那么還是無法識別單一文件的日志順序性。但是如果消費者跟日志隊列一對一處理,那么像storm這種消費者應對新日志類型的擴展性就會降低。因為storm的實時處理是基于topology的,一個topology既包含輸入(spout)也包含輸出邏輯。這種情況下每次新增一個日志列隊,topology就必須重啟一次(為了識別新的spout)。
- 基于自增序列排序的順序性
如果不通過外部的數據結構來維持單一日志文件中日志的順序性,那就只能通過為每個日志添加序列號來標識日志的順序性。這種方式可以允許日志在消息中間件中無序、混合存儲。但它同樣存在弊端:
(1)單一的序列號還不足夠,還需要額外的標識才能區分同類、不同主機的日志(集群環境)
(2)為了得到前后有關聯的日志,日志必須先落數據庫,然后借助于排序機制還原原先的順序,然后按順序進行合并或者單一處理
上面這兩點都比較棘手。
客戶端解析docker日志格式
上面分析了客戶端不解析存在的問題,另一種做法是客戶端解析。因為docker的格式是固定的,這相對省了點事,我們可以選擇只做外層解析,也就是對docker容器日志的格式做解析,以此來還原原始日志(注意這里原始日志還是純文本),而拿到原始日志之后,就可以根據原先的日志分隔符解析多行關聯性日志,其他問題也就不存在了。但毫無疑問,這需要對日志采集器進行定制。
flume的定制
flume對日志的讀取邏輯組件稱之為 EventDeserializer ,這里我們使用的 MultiLineDeserializer 是基于 LineDeserializer 定制的。
首先我們定義一個配置項來標識日志是否是docker產生的:
wrappedByDocker = true
接著,我們根據docker的json格式定義其對應的Java Bean:
public static class DockerLog {private String log; private String stream; private String time; public DockerLog() { } public String getLog() { return log; } public void setLog(String log) { this.log = log; } public String getStream() { return stream; } public void setStream(String stream) { this.stream = stream; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } }</pre>
然后,當我們讀取一行之后,如果日志是docker產生的,那么先用gson將其反序列化為java對象,然后取出我們關心的log字段拿到原始日志文本,接下來的處理就跟原來一樣了。
readBeforeOffset = in.tell(); String preReadLine = readSingleLine();if (preReadLine == null) return null;
//if the log is wrapped by docker log format, //should extract origin log firstly if (wrappedByDocker) { DockerLog dockerLog = GSON.fromJson(preReadLine, DockerLog.class); preReadLine = dockerLog.getLog(); }</pre>
這樣agent采集到的日志就都是原始日志了,也就保證了后續一致的解析邏輯。
針對flume的完整定制開源在 github/flume-customized .