Shopify的Docker實戰經驗(二)如何用容器支持10萬的在線商店
Shopify是一個電子商務平臺,提供專業的網上店面。目前的客戶超過12萬,包括GE、特斯拉汽車、GitHub等。作為首家市值超過10億美元的加 拿大網絡公司,Shopify在歐美市場的影響力也與日俱增。Shopify是一個大型的Ruby on Rails應用,其產品服務器能通過給1700個處理核心和6TB RAM分配任務來完成每秒處理8000多個請求。Shopify在其博客上分享了系列內容來介紹他們的Docker使用經驗。這是系列文章的第二篇,主要 講述Shopify如何使用Docker支撐的容器化數據中心。

這是系列文章的第二篇,講述Shopify如何使用Docker支撐的容器化數據中心。這篇文章重點介紹當用戶訪問Shopify商店門戶的時候我們底層的生產環境是如何創建出容器的。
系列文章的第一篇在 這里( 中文翻譯)。
為什么選擇容器化?
在深入討論構建容器的原理之前,先討論下我們的動機。容器對于數據中心的作用可能類似于控制臺對于游戲的作用。在PC游戲發展的早期階段,游戲在玩之前一般都會要求顯卡和聲卡驅動massaging。然而,游戲控制臺提供了不一樣的方式:- 可預測:cartridge是自包含的:隨時可用,無需下載或更新。
- 快速:cartridge使用只讀內存,所以可以非常快。
- 簡易:cartridge健壯并且被大范圍證明 - 僅需插上即可游戲。 </ul>
- 日志索引:日志對于診斷生產環境問題至關重要,在容器化的世界里更為重要,因為文件系統在容器退出后 就消失了。我們盡量不要改變應用自己的日志行為(比如強制應用日志重定向到syslog中),而是應該允許應用繼續記錄日志到文件系統里。運行100個日 志傳遞代理看上去并不合理,因此我們構建了一個后臺程序來處理一些核心任務:
- 在宿主機器上運行,并訂閱Docker事件
- 當容器啟動時,配置日志索引器監控容器
- 當容器銷毀時,移除索引指令 </ul> </li> </ul>
- 統計:Shopify進行多個層級的運行時統計:系統,中間件和應用層面。統計數據通常是由代理轉發或者從應用代碼里直接發出。
- 我們大部分統計數據是由StatsD收集的,也很幸運地能配置主機端的Datadog收集器來接受容器的消息(比如,網絡流量和代理配置)。因為有這些配置,只需要將StatsD的地址轉發到容器里就可以了。
- 主機端系統監控代理能夠跨越容器界限,因為容器歸根到底就是個進程樹。因此可以共享一個系統監控器。
- 從容器為中心的視角看,要考慮Datadog的Docker集成,這樣可以將Docker矩陣加入到主機端的監控代理上。
- 應用級別上,大部分情況都可以工作,因為它們要么想要發送事件給StatsD,要么直接和其他服務通信。定義容器的名字很重要,這樣日志里才會記錄下有效名字。 </ul> </li>
- Kafka: 我們使用Kafka作為事件總線將Shopify的實時事件傳送到感興趣的組件里。構建消息并將其放置到SysV消息隊列,這樣可以將Ruby on Rails里的Kafka事件發布出去。一個簡單的用Go寫的后臺程序會清空隊列并且發送消息給Kafka。這樣的架構減少了Ruby處理時間,幫助我們 很好地解決了Kafka服務器過載的問題。不幸的是,SysV消息隊列是IPC命名空間的一部分,所以我們無法為容器使用隊列:主機連接。我們通過給主機 添加了一個socket接入點,使用其將消息放到SysV隊列里來解決了這個問題。當然,我們需要將這個接入點的地址通過環境變量傳遞給容器。另外一篇文章詳細介紹了這個問題。 </ul>
- 根目錄始終在容器/app目錄下的應用
- 服務暴露在單一端口的應用 </ul>
- /container/files 包含的文件樹是在容器構建時直接拷貝過來的。比如,請求某個應用日志的Splunk索引 /container/files/etc/splunk.d/inputs.conf 文件指向git倉庫。注意:這是一個微妙且重要的職責轉換 - 開發人員現在控制日志索引,而以前這是屬于運維領域的事情。
- /container/complie是編譯應用的shell腳本,它輸出隨時可運行的容器。構建這個文件并且適應自己的應用是很復雜的。
- /container/roles.json包含執行工作的命令行,以機器語言存在。很多應用用多個角色運行同一段代碼 - 一些在處理后臺任務時處理web消息。這一部分是受Heroku的procfile啟發。這里有個roles.json示例。 </ul>
- 使用Capistrano將代碼部署到機器上,組件編譯已經在部署過程中完成了。將組件編譯挪到容器構建過程中進行,使得部署新的代碼也就需要幾分鐘,簡單快速。
- Unicorn master啟動時需要詢問數據庫得到表類型。這不僅僅很慢,而且很小的容器空間意味著需要更多的數據庫連接。所以,可以考慮在容器構建時間來做這個從而降低啟動時間。 </ul>
- bundle安裝
- 組件編譯
- 數據庫啟動 </ul>
100法則的使用需要一定的靈活性。有些情況下,只需要給組件間編寫些“黏合器”。而另外一些情況下,通過配置就可以實現,也有些時候,需要重新設計。最終,你應該獲得一個容器,內含你的應用程序運行所需的所有東西,以及一個提供了Docker托管和共享服務的主機環境。
容器化應用
環境搭好后,接下來需要關注容器化應用本身。
在Shopify里,我們傾向于只做一件事情的“纖薄”容器,比如unicorn master和響應web請求的worker,或者響應某個特定隊列的Resque worker。“纖薄”容器允許細粒度按需擴展。比如,可以增加Resque worker的數量來檢查蠕蟲從而應對攻擊。
我們總結了一些將代碼放到容器里的標準原則:
我們也發現一些git倉庫適合被容器化:
我們用一個也能在本地運行的簡單的Makefile構建build。Dockerfile看上去像:
記住編譯階段的目的是輸出一個 即刻可用的容器。Docker的重要優點之一就是極快的啟動時間,盡量不要在容器啟動時做額外的工作從而延長啟動時間。為了達到這個目標,我們需要理解整個部署流程。一些例子說明:
我們的編譯環節包括以下邏輯步驟:
注意:為了控制這篇文章的篇幅,我們簡化了一些細節,沒有在此討論管理訣竅,沒有涉及源碼管理的訣竅。可以查看 這里的代碼。很快會有一片文章重點討論這個問題。
調試及其細節
在大多數情況下,應用運行在容器里時的行為和它運行在非容器化環境下沒有什么不同。因此,大多數我們標準的調試技術(strace、gdb、/proc文件系統)在Docker宿主機上也都適用。
一個需要額外注意的工具是: nsenter或者nsinit,它讓大家可以連接到一個正在運行的容器里去調試。Docker 1.3里有一個新的docer exec工具,可以將一個進程注入到正在運行的容器里。不幸的是,如果你想要注入的進程有root權限,還是需要nsenter。
還有些領域沒能像預期那樣工作,包括:
進程層次結構
盡管我們運行的是瘦容器,也始終會有一個init進程(pid=1),它允許和監控工具,秘密管控,服務發現緊密集成,允許我們進行細粒度的健康檢查。
除了init進程,我們增加了ppidshim進程,在每個容器里這個進程pid=2,它只是簡單去啟動應用進程(pid=3)。ppidshim是為了讓應用進程不直接繼承于init(也就是說,ppid!=1),因為那樣會讓它們認為自己是后臺程序而出錯。
最后進程層次結構是:
Signals
如果你要容器化,很可能需要改變現有的腳本,或者重寫一個新腳本,里面包含調用docker run。默認地,docer run會發出signal到你的應用,也就意味著你需要理解應用是如何處理signal的。
標準UNIX規范是發送SIGTERM請求來正常關閉進程。確保你的應用是遵守這個規范的,因為我們發現不止一個應用,比如Resque,使用 SIGQUIT來正常關閉,而使用SIGTERM來緊急關閉。幸運的是,Resque(>1.22)能夠配置其使用SIGTERM來觸發正常關閉。
主機名
使用容器名來描述容器的工作內容(比如,unicorn-1,resque-2),并且將這個名字和機器的主機名結合使用,方便問題的跟蹤。最后的結果類似:runicorn-1.server2.shopify.com。
使用Docker的--hostname標志將主機名傳進容器,這使得大部分應用能使用正確的主機名。一些程序(Ruby)會使用短名(unicorn-1)而不是FQDN。
因為Docker管理/etc/resolv.conf,而現在的版本不允許隨意改動,我們使用LD_PRELOAD來注入庫,這個庫攔截并且重載gethostname()和uname()函數。最終結果是監控工具發布我們需要的主機名而不需要更改應用代碼。
注冊和部署
我們發現構建能夠復制“純物理機”行為的容器的流程其實就是一個調試的過程。一旦你構建了穩健的容器之后就自然想要自動化整個構建過程。
我們使用github commit hook為每次主推觸發容器的構建,并且 commit statuses來標志構建是否成功。使用git commit SHA 來給容器起標簽:因此可以一目了然容器里包含的是哪個版本的代碼。我們也將SHA放進容器里的文件(/app/REVISION),以便調試和腳本的調用。
一旦構建出穩定的build,就需要將這個容器放到一個中央注冊表里。我們選擇搭建自己的注冊表以便提高部署速度,同時減少對外部的依賴。我們使用nginx反向代理來緩存GET請求,在Nginx之后,使用多個相同的標準python注冊表。拓撲看上去如圖:
我們發現龐大的網絡接口(10 Gbps)和反向代理可以有效對抗當多個Docker主機申請同一個鏡像時會引起的“驚群效應”。這種采用代理的解決方法允許我們同時運行多個注冊表,并且在某個注冊表崩潰的時候提供自動故障轉移。
如果按照我們的方法,那么你會得到自動構建的容器,它們安全地存儲在中央注冊表里,隨時可以被用來部署。
我們的下一篇文章會講述如何管理應用,并深入探討如何自定義構建流程來創建盡可能小的容器。
原文鏈接:Docker at Shopify: How we built containers that power over 100,000 online shops (翻譯:崔婧雯 )
===========================
譯者介紹
崔婧雯,現就職于VMware,高級軟件工程師,負責桌面虛擬化產品的質量保證工作。曾在IBM WebSphere業務流程管理軟件擔任多年系統測試工作。對虛擬化,中間件技術有濃厚的興趣。
來自:http://dockerone.com/article/172
注意有時在容器退出后需要延遲容器的銷毀從而確保所有的日志都建立了索引。
可預測、快速、簡易都是閃亮的優點。Docker容器提供了構建模塊,可以將應用放到自包含,隨時可運行的單元里,這樣使得運行數據中心更為簡單,也更加靈活,就像cartridge帶給控制臺游戲的改進一樣。
Bootstrapping
要完成容器化的轉變需要開發和運維的雙方面配合。首先,需要和運維團隊溝通,確保容器能夠完全復制現在的生產環境。如果你在OSX(或者Windows)上運行,部署到Linux上,需要使用虛擬機,比如Vagrant作為本地的測試環境。首先需要得到操作系 統信息和其上需要安裝的支持包。選擇符合生產環境(我們用的是Ubuntu 14.04)的基礎鏡像,拒絕任何非緊急的系統升級請求,誰都不想同時既進行容器化改進又要升級操作系統/包。
選擇容器封裝格式
Docker提供封裝格式的選擇,從“纖薄”單進程容器到更像傳統意義虛擬機的“胖”容器(比如, Phusion)。我們選擇了“纖薄”容器路徑,并盡量隔絕外部影響。在這兩種封裝格式之間很難做決定,但是更小、更簡單的容器消耗的CPU和內存更少。 Docker官方博客上有這種解決方案的更為詳細的介紹。
環境搭建
我們使用Chef管理生產節點。雖然可以簡單地在容器內部運行Chef,但是這會帶進一些不想復制到每個容器里的服務(比如,日志索引和stats收集)。與其忍受重復,倒不如給每個Docker主機共享一個單獨的這些服務的拷貝。構建“纖薄”容器的路徑要求把Chef請求轉換成Dockerfile(后來我們更換成了自定義的構建流程 - 不過這在另一篇文章里討論)。這樣的轉換也給了我們很好的機會去審查生產環境并記錄下真正需要的東西(可能需要些考古學知識)。盡可能得刪除不需要的東 西,并且在這一階段安排盡可能多的代碼審查。
這個過程其實沒有聽上去那么痛苦。我們最后得到了一個125行,包含很多注釋的Dockerfile,它定義了Shopify所有容器共享的基礎 鏡像。這個基礎鏡像包含25個包,涉及各種編程語言的運行時環境(Ruby、Python、Node),開發工具(Git、Vim、Build- essential、Go)和一些常用庫。它還包括完成一些任務的實用腳本,比如,用優化的參數啟動Ruby,或者給Datadog發送事件。
應用程序也可以向這個基礎鏡像添加特殊的需求。即使這樣,我們最大的應用程序也只是另外添加了兩個操作系統包,因此我們的基礎鏡像是非常精簡的。
100法則
當選擇將什么服務放到容器里時,先假定在一臺主機上運行了100個小容器,然后,問自己是否真的需要運行100個這樣的服務,還是最好只共享一臺主機的服務。以下是些實際例子,展示100法則如何影響我們的容器:
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!