最近在Tini的倉庫下看到作者對Tini優勢的精彩回復,搬運過來,粗糙翻譯,獻給擁有同樣疑惑的你。

寫在前面

我們在查看一些大項目的Dockerfile時經常發現,它們的ENTRYPOINT中往往都有tini的身影:

Rancher官方鏡像
Jenkins官方鏡像

那Tini到底是什麼?為什麼大家都喜歡在鏡像中使用它呢?

開發者的疑問

我注意到Jenkins的官方鏡像中使用了Tini,所以我很好奇它是什麼。它看起來一定很有用,可能解決了一些我不知道的問題。你能用「說人話」的方式簡單解釋一下Tini相對於直接以CMD運行shell腳本的優勢嗎?

我的幾個容器的ENTRYPOINT都設置了一個 docker-entrypoint.sh 腳本,裡面基本上都是以「exec "$@"」的方式在運行,我應該使用Tini來代替嗎?

來自作者的回復

問得好!但這解釋可能有點長,所以請耐心聽我說(我知道你要求簡短,但我真的做不到,捂臉~)。

首先,我們先簡單聊聊Jenkins。當您運行Docker容器時,Docker會將它與系統的其他部分隔離開來。這種隔離發生在不同的級別(例如網路、文件系統、進程)。

但Tini並不真正關注網路或文件系統,所以讓我們把注意力放在Tini的一個重要概念上:進程。

每個Docker容器都是一個PID命名空間,這意味著容器中的進程與主機上的其他進程是隔離的。PID命名空間是一棵樹,從PID 1開始,通常稱為init。

注意:當你運行一個Docker容器時,鏡像的ENTRYPOINT就是你的根進程,即PID 1(如果你沒有ENTRYPOINT,那麼CMD就會作為根進程,你可能配置了一個shell腳本,或其他的可執行程序,容器的根進程具體是什麼,完全取決於你的配置)。

與其他進程不同的是,PID 1有一個獨特的職責,那就是收割「殭屍進程」。

那何為「殭屍進程」呢?

「殭屍進程」是指:

  • 已經退出。
  • 沒有被其父進程wait(wait是指syscall父進程用於檢索其子進程的退出代碼)。
  • 父進程已丟失(也就是說,它們的父進程已經不存在了),這意味著他們永遠不會被其父進程處理。

當「殭屍進程」被創建時(也就是說,一旦它的父進程非正常退出了,它也就跟著無法正常退出了),它會繼承成為PID 1的子級,最後PID 1會負責關閉它。

換句話說,有人必須在「不負責任」的父進程離開後,對這些「孤兒」進行清理,這是PID 1的作用。

請注意,創建「殭屍進程」通常是不被允許的(也就是說,理想情況下,您應該修復代碼,這樣就不會創建「殭屍進程」),但是對於像Jenkins這種應用來說,它們是不可避免的:因為Jenkins通常運行的代碼不是由Jenkins維護者編寫的(也就是您的Jenkins構建腳本),所以他們也無法「修復代碼」。

這就是Jenkins使用Tini的原因:在構建了創建「殭屍進程」的腳本後進行清理。


但其實Bash實際上也做同樣的事情(收割「殭屍進程」),所以你可能會想:為什麼不把Bash當作PID 1呢?

第一個問題是,如果您將Bash作為PID 1運行,那麼您發送到Docker容器的所有信號(例如,使用docker stop或docker kill)最終都會發送到Bash,Bash默認不會將它們轉發到任何地方(除非您自己編寫代碼實現)。換句話說,如果你使用Bash來運行Jenkins,那麼當你運行docker stop的時候,Jenkins將永遠收不到停止信號!

而Tini通過「信號轉發」解決了這個問題:如果你向Tini發送信號,那麼它也會向你的子進程發送同樣的信號(在你的例子中是Jenkins)。

第二個問題是,一旦您的進程退出,Bash也會繼續退出。如果您不小心,Bash可能會退出,退出代碼為0,而您的進程實際上崩潰了(但0表示「一切正常」;這將導致Docker重啟策略不符合您的預期)。因為您真正想要的可能是Bash返回與您的進程相同的退出代碼。

請注意,您可以通過在Bash中創建信號處理程序來實際執行轉發,並返回適當的退出代碼來解決這個問題。另一方面,這需要做更多的工作,而添加Tini只是文檔中的幾行。


其實還有另一個解決方案可以將Jenkins作為PID 1運行,即在Jenkins中添加另一個線程來負責收割「殭屍進程」。

但這也不理想,原因有二:

首先,如果將Jenkins以PID 1的身份運行,那麼很難區分繼承給Jenkins的進程(應該被收割)和Jenkins自己產生的進程(不應該被收割,因為還有其他代碼已經在等待它們執行)。我相信你可以用代碼來解決這個問題,但還是要問一遍:當你可以把Tini放進去的時候,為什麼還要寫呢?

其次,如果Jenkins以PID 1運行,那麼它可能不會接收到您發送的信號!

這是PID 1進程中的微妙之處。與其他進程不同的是,PID 1沒有默認的信號處理程序,這意味著如果Jenkins沒有明確地為SIGTERM安裝信號處理程序,那麼該信號在發送時將被丟棄(而默認行為是終止該過程)。

Tini確實安裝了顯式信號處理程序(順便說一下,是為了轉發信號),所以這些信號不再被丟棄。相反,它們被發送到Jenkins,Jenkins並不像PID 1(Tini )那樣運行,因此有默認的信號處理程序(注意:這不是Jenkins使用Tini的原因,Jenkins使用它來獲取信號,但在RabbitMQ的鏡像中是這個作用)。


請注意,Tini中還有一些額外的功能,在Bash或Java中很難實現(例如,Tini可以註冊為「子收割者」,因此它實際上不需要作為PID 1運行來完成「殭屍進程」收割工作),但是這些功能對於一些高級應用場景來說非常有用。

希望以上內容對你有所幫助!

如果您有興趣瞭解更多,以下是一些可供參考的資料:

  • 殭屍進程詳解: blog.phusion.nl/2015/01
  • 更簡潔的解釋:github.com/docker-libra

最後,請注意Tini還有更多的選擇(比如Phusion的基礎鏡像)。

Tini的主要特性是:

  • 做PID 1需要做的一切,而不做其他任何事情。像讀取環境文件、改變用戶、過程監控等事情不在Tini的範圍內(還有其他更好的工具);
  • 零配置就能上手(如果運行不正常,Tini >= 0.6也會警告您);
  • 它有豐富的測試。

乾杯!

原文鏈接

What is advantage of Tini?


推薦閱讀:
相關文章