更好的閱讀體驗請移步我的 blog:

http://littledriver.net/post/2018/12/02/detect-logging-of-docker/?

littledriver.net

寫在前面

運行在容器內部的應用會在運行期間產生大量的日誌,這些日誌將作為我們 Debug,分析應用行為的重要依據。本文將帶大家瞭解一下 Docker 面對的和「容器日誌」有關的問題以及它的解決方案。

日誌種類

通常來講,容器內運行的應用會以兩種方式輸出日誌:

  1. 標準輸出
  2. 日誌文件

日誌需求

我們對於容器內應用輸出的日誌的需求比較簡單,基本上可以歸結為以下幾點:

  1. 易使用:可以方便的通過 docker logs 或者 kubectl logs 等命令行查看容器內部的日誌信息
  2. 易收集:可以很容易的找到容器內部日誌最終持久化的位置,如某個文件或者說某個存儲服務
  3. 自處理:當日誌容量達到一定額度的時候,可以通過一些「滾動操作」將舊的日誌清除掉一部分,維持一個固定大小的日誌文件
  4. 可分析:可以方便的將所有的日誌(包括被滾動掉的)內容送入第三方的日誌分析服務,以便能夠以更加有效的方式通過對日誌的分析而瞭解服務的運行情況

Docker 的容器日誌處理方案

標準輸出日誌

實質

回想一下 Docker 的實現原理,無非是「多進程」+「隔離機制」。其中所有容器(子進程)都是由 dockerdaemon(父進程)來創建的。父進程可以收集子進程 PID Namespace 內 PID=1 進程的和標準輸出的內容。因為只有 PID = 1的進程纔是父進程創建的。如果該 PID = 1的子進程再創建孫子進程的話,父進程是無法收集到孫子進程內的標準輸出的內容的。

父進程是通過 Pipe 來獲取子進程的和標準輸出的。所以,在容器進程被創建的時候,會通過一個 Pipe 將輸出到標準輸出的日誌信息傳遞給父進程。而這一切對容器內部應用都是透明的。

Docker Log Driver

當 dockerdaemon 進程從 Pipe 中拿到日誌信息之後,會將它交給一個特殊的模塊來進行處理—— Docker Log Driver。Log Driver 的職責也很簡單,既然收集到了日誌信息,那肯定需要將它寫入到一個位置,這個位置可以是一個 JSON 格式的文件(默認行為),也可以是一個有特殊含義的文件,如 syslog,甚至是一個第三方日誌的收集服務。

Log Driver 寫入的位置,可以通過很靈活的方式進行配置。可以在 dockerdaemon 啟動的時候通過 —log-driver 參數配置,也可以在每個容器啟動的時候用同樣的參數配置。容器級別的 LogDriver 配置會覆蓋全局的。

生產環境中的問題——真正打日誌的服務不是 dockerdaemon 的子進程

問題

在基於 Kubernetes 平臺開發資料庫應用的時候,為了能夠實現一些「自運維」的功能,通常在一個容器內,我們會以一個baseImage 為基礎,再啟動我們的「業務進程」(負責自運維)和資料庫進程(redisd,mysqld 等等)。此時,以 dockerdaemon 進程的視角來看的話,它的子進程就不是資料庫進程或者說是我們的業務進程了。因為 baseImage 的存在,它通常在容器啟動的時候會順便啟動一些輔助進程,幫我們在容器內部搭建一個較好的運行環境。但是,我們想要觀察的日誌大部分都來自於業務進程和資料庫進程。

解決思路

針對上面的問題,並且結合 Docker 收集容器日誌的原理。我們想出了以下的解決方案:

  1. 將容器內部我們想收集的來自標準輸出的日誌都先塞入到一個容器內部的文件中,如 syslog
  2. 利用容器內部的其他服務,將 syslog 文件內容再重定向到標準輸出

解決方案

  1. 使用 GitHub - phusion/baseimage-docker: A minimal Ubuntu base image modified for Docker-friendliness baseImage 中內置的一個名為 syslog-ng 的服務,將 syslog 的內容重定向到標準輸出
  2. 通過 logrus 第三方日誌庫,加入日誌輸出的 hook。將業務進程內部輸出的日誌重定向到 syslog
  3. 開啟 Redis Server 自身提供的,將服務日誌輸出到 syslog 的功能

通過對上面關於「標準輸出日誌」遇到問題的解決方案的瞭解,我們可以發現,它其實有如下的幾個風險:

  1. syslog 文件的內容可能會無限增大,佔用容器的存儲資源(此問題在 log driver 將日誌保存在宿主機文件的情況下,也同樣存在)

日誌滾動

在宿主機上,Docker 將會幫我們解決問題1。若你使用 json-file 這種 logdriver 的時候,會讓你配置一個日誌文件的大小。一旦達到這個閾值,Docker 會通過一些日誌滾動服務來刪除掉一部分舊的日誌,從而保持日誌文件一個穩定的大小。

而在容器內部我們就得自己想辦法了。之前提到過在生產環境中,我們在容器內部使用了 baseImage。其內部內置的一個服務叫:logrotate。他會幫我們針對某個特定的文件做「滾動」操作。

對上述解決方案的一點思考

其實上述方案的本質是:用文件日誌作為服務日誌和容器標準輸出之間的橋樑,雖然容器內部是文件日誌,但是是通過標準輸出向外傳遞的。而容器內的文件日誌可以通過 baseImage 提供的 syslog-ng 和 logrotate 服務來進行日誌的重定向和文件日誌的滾動。這是在我們沒有成熟的收集容器內部文件日誌的機制的前提下,想出來的一種解決方案。它復用了 Docker 提供的一些現有的機制。

再抽象一點——Docker 的日誌管理

容器日誌管理的本質是「如何處理 stdout」 和「如何將我關心的日誌都打入 stdout」。這兩個問題的出現,其實是受限於 Docker 對於容器日誌的管理方式(通過父子進程 pipe 和 log driver)。

能否對上述方案做一些優化?

上面的方案其實已經能解決容器日誌所面臨的大部分問題了。但是仍舊有個做的不是很好的點:baseImage 集成了太多了服務(如 logrotate 和 syslog-ng)。其實,對於 baseImage,你可以說這是優點,也可是說這是缺點。優點是方便,缺點就是可能會和業務進程搶佔資源。

既然現有方案上有缺點,我們就需要解決它。思路很簡單:什麼進程可能會搶佔業務進程的資源,就把誰拿出這個容器。

  1. 將 logrotate 和 syslog-ng 等服務放入另外一個容器(但是在一個 Pod 內,相當於一個 sidecar container)
  2. 業務容器通過第三方日誌庫或者其他方式的支持,將 業務進程的日誌通過網路通信(UDP) 傳遞給「日誌服務容器」內的服務
  3. 在日誌服務容器內部,可重複上述:日誌打入 syslog 然後再從定向到標準輸出的過程
  4. 集羣級別部署一個高可靠的第三方日誌收集組件,收集來自 logdriver(即標準輸出)的日誌內容

這種方案,雖然在資源用量上面沒有什麼優化。但是卻將日誌服務和業務進程從一個容器中分開來,兩者申請的資源也可以分別控制,互不影響。

如果沒有 syslog-ng 和 logrotate

其實大部分容器用戶,它們對 baseImage 的要求不是很高。甚至有的時候他們希望 baseImage 是盡量乾淨,盡量小的。如果 baseImage 中沒有我們上面說到的 syslog-ng 和 logrotate 服務的話,容器內的日誌收集問題就比較麻煩(在業務進程仍然不是 pid=1的情況下)。

通常情況下,容器內部的業務進程會將日誌打入容器內部的一個文件中。所以,我們將面臨兩個問題:

  1. 日誌文件內容的滾動
  2. 如何確認日誌文件在宿主機的具體路徑

引發這兩個問題的根源是相同的:我們由處理標準輸出的內容變成了處理文件內容。對於「標準輸出」,有 logdriver 負責日誌的滾動和最終日誌文件在宿主機上的存儲位置。而對於「文件」,就必須要我們自己來處理了。

文件日誌

解決方案

嘗試1——每個容器內部配備日誌收集進程

最直接的想法就是在每個業務容器中配備一個用於收集日誌的進程,它負責將容器內部的日誌壓縮文件上傳到某個地方。但是這樣做的缺點也很明顯:

  1. 每個容器都必須要起一個日誌收集進程,1w 個容器就是1w 個收集進程,對資源會有浪費
  2. 「上傳」操作難以控制,一旦有網路的參與,尤其是內外網的交互,傳輸可靠性風險極大
  3. 收集日誌的進程可能需要做一些自定義的配置,如間隔多久收集一次等等。這些都需要額外的開發工作

嘗試2——每個容器集羣配備一個日誌收集服務(進程)

既然每個容器一個收集程序有諸多缺點,那我們就藉助開源社區的力量,找一個強大的日誌收集組件,將它部署到集羣中。這樣一來整個集羣只有一個日誌收集服務。但是這個方案,也同樣需要解決幾個和日誌路徑相關的問題:

  1. Docker 依賴的底層存儲方案不同,導致容器運行鏡像時候的 top layer 的映射規則五花八門。你不知道容器的 top layer 實際映射到了物理機上的哪個目錄。比如 AUFS 這種實現方案,映射關係很複雜,我在瞭解 AUFS 原理的時候,在這部分花了大量的時間。這種映射關係及時有,估計文檔化也比較差,我一直沒有找到一個很官方的解釋。不瞭解映射關係就直接導致了:你的日誌收集服務不知道到哪裡去收集日誌
  2. 即使日誌路徑的映射關係確定了,這部分信息如何通知給集羣級別的日誌收集服務?

針對於問題1,Docker 為我們提供了原生的解決方案:在容器啟動的時候,將 top layer 通過顯示聲明的方式掛載到一個宿主機目錄下。通過-v 參數即可實現。

針對於問題2,Docker 同樣也為我們提供了幫助:集羣級別的日誌收集服務可以通過和宿主機 docker 通信的方式,監聽容器的各類事件(創建,刪除等)。這些事件內部將會包含我們上面提到的容器的掛載信息。

如果能找到一個比較好的日誌收集組件的話,那麼此方案會被很好的支持。並且,有了容器的 label以及對容器各類事件的監聽,日誌收集服務可以很容易的對功能進行擴展。

另外,對於日誌文件滾動的的問題。既然是一個單獨的日誌收集服務來負責容器內文件日誌的處理,那麼這部分功能理應交由它來實現。該功能要至少滿足以下兩個需求:

  1. 回滾的策略可配置
  2. 「判定是否滿足回滾策略」的信息可以很方便的獲取到(如監聽容器的各類事件)

總結


推薦閱讀:
相關文章