1. 程式人生 > >日誌系統之基於flume收集docker容器日誌

日誌系統之基於flume收集docker容器日誌

http://blog.csdn.net/yanghua_kobe/article/details/50642601

最近我在日誌收集的功能中加入了對docker容器日誌的支援。這篇文章簡單談談策略選擇和處理方式。

關於docker的容器日誌

docker 我就不多說了,這兩年火得發燙。最近我也正在把日誌系統的一些元件往docker裡部署。很顯然,元件跑在容器裡之後很多東西都會受到容器的制約,比如日誌檔案就是其中之一。

當一個元件部署到docker中時,你可以通過如下命令在標準輸出流(命令列)中檢視這個元件的日誌:

<code class="hljs bash has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;">docker logs <span class="hljs-variable" style="color: rgb(102, 0, 102); box-sizing: border-box;">${containerName}</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

日誌形如:

終端檢視docker容器日誌

但這種方式並不能讓你實時獲取日誌並對它們進行收集。但是docker還是比較友好的,它把這些日誌檔案都儲存在以容器ID為檔名的檔案系統中。如果你是標準安裝的話,那麼它應該在檔案系統的如下位置:

<code class="hljs perl has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;">/var/lib/docker/containers/<span class="hljs-variable" style="color: rgb(102, 0, 102); box-sizing: border-box;">${fullContainerId}</span>/<span class="hljs-variable" style="color: rgb(102, 0, 102); box-sizing: border-box;">${fullContainerId}</span>-json.<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">log</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

這個fullContainerId應該如何獲得呢?簡單一點,你可以通過如下命令來檢視full container-id:

<code class="hljs brainfuck has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">docker</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">ps</span> <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">-</span><span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">-</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">no</span><span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">-</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">trunc</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

然後通過vi 命令來檢視日誌檔案。但基於檔案的日誌和基於標準輸出流的日誌是有區別的,區別是基於檔案的日誌是json形式的,並且以標準輸出流的一行作為日誌的間隔。形如:

檔案檢視docker容器日誌

這相當於兩層日誌格式,外面這一層是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產生的:

<code class="hljs ini has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;"><span class="hljs-setting" style="box-sizing: border-box;">wrappedByDocker = <span class="hljs-value" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span></span></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

接著,我們根據docker的json格式定義其對應的Java Bean:

<code class="hljs cs has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;">    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> DockerLog {

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> String log;
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> String stream;
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> String time;

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-title" style="box-sizing: border-box;">DockerLog</span>() {
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">getLog</span>() {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> log;
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setLog</span>(String log) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.log = log;
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">getStream</span>() {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> stream;
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setStream</span>(String stream) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.stream = stream;
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> String <span class="hljs-title" style="box-sizing: border-box;">getTime</span>() {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> time;
        }

        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">setTime</span>(String time) {
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>.time = time;
        }
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li></ul>

然後,當我們讀取一行之後,如果日誌是docker產生的,那麼先用gson將其反序列化為java物件,然後取出我們關心的log欄位拿到原始日誌文字,接下來的處理就跟原來一樣了。

<code class="hljs javascript has-numbering" style="display: block; padding: 0px; background: transparent; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal;">readBeforeOffset = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span>.tell();
<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">String</span> preReadLine = readSingleLine();

<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (preReadLine == <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">null</span>) <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">null</span>;

    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//if the log is wrapped by docker log format,</span>
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//should extract origin log firstly</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (wrappedByDocker) {
        DockerLog dockerLog = GSON.fromJson(preReadLine, DockerLog.class);
        preReadLine = dockerLog.getLog();
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; background-color: rgb(238, 238, 238); top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right;"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li></ul>

這樣agent採集到的日誌就都是原始日誌了,也就保證了後續一致的解析邏輯。