1. 程式人生 > >在Docker中執行PHP專案的探索之旅

在Docker中執行PHP專案的探索之旅

Docker出現後,容器技術在網際網路領域得到了空前的普及,無論是大公司還是屌絲創業公司的碼農基本上都會在各種技術社群或者各種演講會議上了解到過相關技術,我們作為一家屌絲創業公司也不例外,去年對Docker做了一番瞭解,並在年前測試了一些方案,今天在這裡總結一下遇到的各種坑以及踩坑過程中的一些思考,希望能對了解過Docker並且躍躍欲試的同學有點啟發,當然更歡迎在這方面有豐富經驗的同學給一些建議或者指點。

專案環境
我們專案用的標準的PHP技術棧:PHP-FPM + Nginx,執行在阿里雲的ECS上,資料庫也是阿里雲的服務。

緣起
對於一個普通的屌絲創業公司的屌絲專案來說,理論上來說是沒必要用太複雜的技術的,對新技術的剋制也是碼農的一個職業操守。然而我之所以在專案上做這個嘗試也並非是想嘗試新技術,而是源自於專案上線以來遇到的各種問題,比如前面文章裡有提到過的laravel的效能問題,而HHVM或者PHP7這些新技術都大大提到了效能,一下子全部切換風險太大,所以可以用容器來執行某幾個服務測試一下;再比如之前有出現過一個BUG,某個接口出現問題佔用記憶體太高導致整個系統響應超時;再比如,看了各種技術大會上別人分享的經驗確實想自己試一下,哈哈。

牛刀小試
根據Docker的理念,每個容器都是一個獨立的服務,服務之間通過介面相互協作。我們採用的方案是PHP-FPM和Nginx分別跑在不同的容器中,PHP-FPM容器暴露埠給Nginx容器。
容器之間的網路通訊最初用的是Docker自帶的link,link雖然很簡單,但是不太好用,link是通過在容器啟動時修改/etc/hosts檔案來實現的,當時用的時候遇到的一個問題是被link的容器重啟後,這個容器IP變了,但是link的容器裡並沒有更新(1.10版本對link機制做了修改,不知道是否解決了這個問題,還沒具體看),還有一個問題就是當同一個服務需要開多個容器時就搞不定了,當然如果是用叢集的方式來跑就更搞不定了,所以link只能在本地開發時玩玩,在生產環境沒啥用。最後這部分用的方案是用Consul來做服務發現,關於consul就不在這裡展開介紹了,有興趣的同學可以看下這篇文章:Service Discovery for Docker Containers using Consul and Registrator(

https://www.livewyer.com/blog/2015/02/05/service-discovery-docker-containers-using-consul-and-registrator)。
然後程式碼是放在host上,在容器啟動時掛載一下程式碼的目錄。根據介面使用的場景將介面分到了幾個不同的容器裡面去跑,對資源隔離做了下壓力測試,符合預期,很滿意!然後選了兩個容器跑HHVM測試了下,確實性能提高了很多。整個結構如下圖所示:在這裡插入圖片描述這樣可以正常使用了,然後就是日誌的處理,最開始掛載了一個host上的目錄用來存放日誌,但是這樣也不太符合Docker的理念,於是就又弄了個logstash容器,將各種容器的日誌寫到logstash容器裡面去。
然後就是對容器的監控,我們用了OneAPM提供的服務,還不錯,也有其他一些開源工具的選擇,但是用OneAPM的服務更方便一些。
一個完整的技術棧就這樣搞定了,跑了幾天也沒出現什麼問題。作為一個有追求的碼農,當然是要有更高的要求的,後來想到這樣一個問題:Docker的理念是容器即是一個完整的服務,但是我們的用法是把在容器內把PHP-FPM跑起來了,程式碼並不在裡面,而是通過目錄掛載的方式來實現的,這樣就需要在host上面部署好程式碼,沒有完美的實現容器即服務的理念。好,我們追求下完美!完美之路
其實把程式碼放進容器並不只是為了去追求理念上的完美,還有另外一個用意,那就是基於Docker的CI。基本思路是每次釋出都會build一個新版本的映象,build過程中需要把程式碼更新到最新版本,然後安裝依賴的第三方庫以及其他一些業務相關的預處理,build完成後再push到registry,然後在各個節點pull下來,重啟容器。整個流程

如下圖所示:在這裡插入圖片描述
怎麼樣?這下完美了吧!如果現實也是那麼完美就好了,但是偏偏現實總是會有各種各樣的坑等著你!
首先,在沒有用這個機制之前,釋出一次程式碼也就幾秒鐘的事情,merge下分支,然後各個節點pull下程式碼就可以了,但是用了Docker後,merge分支後就需要build一下映象,build後再push到registry,然後再到各個節點把映象pull下來,少說也是要花分鐘級別的。這時候有的同學可能會說Docker映象的原理不是layer機制麼,對,確實是layer,每次build只需要更新需要更新的layer就可以,但是還是沒辦法做到像Git那樣只更新那幾個檔案,至少要更新整個專案的程式碼目錄。
其次,之前只需要更新一下程式碼就可以了,現在卻需要把容器重啟一下!本來重啟容器是很快的,但是在容器裡PHP-FPM跑起來後卻很慢,我猜測是在執行docker stop時會發一個SIGTERM的訊號到容器裡的程序,PHP-FPM在處理SIGTERM訊號時會做一些清理工作。一個節點至少有幾十個容器,每個容器都重啟一下非常耗時!
這樣本來很簡單的一個釋出,用了這個「完美」的機制後卻需要好幾分鐘。雖然整個流程完全自動化,但是確實沒必要為了完美的理念做出這樣一個妥協。
此外還遇到了另外一個非常奇葩的問題:升級到1.10後,容器啟動後經常會遇到埠不通的情況,這時候如果進入到容器裡發起一次網路請求,比如ping以下某個IP,埠就會恢復正常。用各種工具除錯了很久依然沒找出來問題在哪,屌絲創業公司也沒精力研究這麼高深的問題,只好暫時擱置了。
掙扎了很久,最後又回到了老路子。

面對現實
雖然沒能理念上完美的方案,但是也要讓整套架構更方便的擴充套件,比如在流量高峰期快速上架節點。得益於雲端計算近幾年的發展,這個問題也比較容易解決。前面提到程式碼是放在host的一個目錄,那麼基本上每個host只需要三個條件就能夠上線:
專案程式碼目錄

安裝Docker
配置好salt-minion(用來更新配置檔案)
只需要在購買一臺ECS後,做完上面的操作,生成一個系統映象,需要上線新節點時用這個系統映象來直接配置伺服器即可。節點上線後,啟動各個容器,通過前面提到的用Consul實現的服務發現機制,自動加入叢集。

繼續探索
後面打算用Docker Swarm來簡單做下叢集管理,未來對排程需求比較大時可能會嘗試下Mesos或者Kubernetes。不過關於容器的生態鏈都發展的非常快,可能等需要的時候又有更好的方案了吧。
簡單總結下整個過程的感悟吧,從架構上來說,永遠都沒有完美的架構,只有最適合自己的架構。從碼農角度來說,在做技術選型時最重要的是權衡當前專案需求、團隊對各種技術瞭解的程度以及該技術選型帶來的複雜度。切勿看了幾個大牛的技術架構分享就在自己專案上躍躍欲試,最後弄的連自己都搞不定還給別人留下一個爛攤子收拾。最後,在專案迭代過程中保持專案的簡單的能力永遠是衡量一個碼農技術的水平的重要標準。