標籤:

攜程圖片服務架構

近些年攜程業務突飛猛進,用戶遍及世界各地。公司對用戶體驗也越來越重視,每一個小的功能改動、頁面改版的背後,都有大量的A/B實驗提供保障。與此同時,與用戶體驗息息相關的媒體文件的應用質量也被放到重要位置,如圖片載入延時、成功率、清晰度等數據。

本文將分享攜程圖片服務架構,包括服務架構的演變過程,以及在生產上實際遇到的一些問題,避免大家重複踩坑。

一、服務架構

1、初始階段

攜程圖片的服務架構主要經歷了三次比較大的調整。早些年為了滿足業務快速上線的需求,我們做了簡單實現,架構如下:

這個架構開發工作量不大,因為當時業務對圖片尺寸的需求單一,也沒有複雜的圖片組合處理需求,因此有大量圖片都被Squid緩存住,緩存命中率很高,取圖速度非常快。

圖片裁剪命令的執行,則由業務發布的時候上傳處理。存儲通過NFS讓整個Nginx服務集群共享。直到移動端流量開始爆發的時候,這個架構有點力不從心。

首先,同一張原圖需要裁剪出大量不同尺寸的小圖片,佔用了大量存儲資源。其次,業務圖片越來越多加上大量不同尺寸的小圖片的出現,導致Squid緩存命中率變差,大量流量穿透到NFS上,I/O迅速變為瓶頸。

從監控看,當時的NFS Read I/O一直處於高水位水平,告警更是24小時不斷,回源流量的上升也導致Squid服務集群開始變得不穩定,經常需要重啟。鑒於這些問題,我們做了下面架構上的調整。

2、發展階段

用Varnish替換了Squid,作為緩存和反向代理服務。

從實際監控情況看,同等壓力下Varnish的表現比Squid更穩定,Varnish虛擬內存swap機制比Squid自己管理的更好,因此性能上更優,並且Varnish配置方便,對運維友好。

當然Squid也有更適合的使用場景,選擇Varnish是因為在當前場景下更符合我們的需求。

為了解決Varnish節點宕機會引發大量緩存數據失效,LB上對URL做了一致性Hash,這樣能盡量減少緩存失效帶來的其他節點數據的遷移,同時也解決了Varnish利用率的問題。

Nginx內嵌Lua腳本用於在圖片訪問的時候直接對圖片進行處理,而不是上傳的時候處理,這樣很多不同尺寸的小圖不用在存儲上保留,存儲上少了大量I/O,並且減少存儲量的同時也會減輕運維的壓力。

從訪問效率看,因為圖片需要實時處理,服務響應延時相比上一個版本有大幅上升,平均延時大概在300毫秒左右。但是這個影響實際對端的影響有限。

首先,國內CDN普遍質量較好,95%以上的圖片資源訪問都會被CDN擋掉,正常情況下回源流量不會太大。其次,我們Varnish集群命中率大概在40~50%之間,所以整體圖片實時處理壓力佔整體流量約1%~2%之間,這些流量訪問延時會上升300毫秒左右是完全能夠接受的。

存儲用FastDFS替換了NFS,當時Ceph還不像現在那麼穩定,FastDFS的特性又能夠滿足我們需求,並且架構簡單,源碼能完全掌控。事實證明,FastDFS集群完全支撐了每天數億次的原圖讀寫操作,並多次在多機房DR演練中完成各項指標。

當時這個架構的核心是Lua的圖片處理模塊,Coroutine的性能非常好,當有大量圖片回源請求的時候,CPU不會浪費在線程的context switch上,開發也很直白,在I/O操作的時候不需要用非同步方式編碼,並且Lua的執行在Nginx里足夠高效。

這裡唯一的缺點是Lua擴展性相對較弱,很多模塊需要自己寫,比如對接我們自己的監控系統的時候就遇到難題。

隨著業務的發展,用戶對圖片的處理要求越來越高,多重濾鏡的應用,需要在Lua里實現很多功能,並且很多基礎數據結構都要自己寫或者依賴第三方,不僅開發工作量大,穩定性和正確性的驗證也需要花費不少的精力。

是不是還有一種技術方案可替代,既能享受協程帶來的簡單,高效。又能兼顧擴展性和完善的功能包,不用重複造輪子。

3、現階段

我們選擇了Golang做為當前版本的開發語言,架構如下:

採用多進程單協程圖片處理模型。圖片庫主要依賴的是GraphicsMagick,和少部分ImageMagick,通過封裝cgo調用實現。

Golang調用cgo會申明一個進入syscall的指令,意味著調度器會創建一個M去執行goroutine。因此當有大量並發調用,並且圖片處理足夠慢,比如一張像素特別大的原圖,就會引發大量線程同時存在,造成不必要context switch,CPU load看上去很高,實際效率很低。

因此我們通常會通過Master進程fork出和CPU相等數量的Worker進程做圖片處理,每個進程只有一個協程來處理圖片,每個進程會創建一個可配置的buffer用於保存原圖的blob, 這樣能最大化利用單協程的利用率。

採用這種架構當時主要還為了規避GM本身的一個問題,參考我們向作者提交的issue:

sourceforge.net/p/graph.

問題描述是setjmp函數和longjmp函數在某些操作系統非線程安全,作者需要一個全局鎖來保證線程安全。因此多線程調用本身是低效的。

這個問題在java或者.net封裝的GM也會存在。上一個版本的Lua不存在這個問題,因為Nginx本身會fork多個Worker進程進行圖片處理,並且只可能存在一個正在運行的協程。事實上Linux執行這兩個函數本身是線程安全的,作者可以通過build的時候來決定是不是需要加上線程安全的flag。在發表本文的時候,作者已經在最新的release中修復了這個bug。

這裡的Nginx不僅僅用來做LB,因為Nginx能提供很豐富的腳本,可以省去很多開發工作量,並且當有獲取原圖的需求,可以通過Nginx sendfile直接從存儲取回,節省不必要的系統開銷。

LB演算法並不是簡單的RR,我們會根據每個進程的CPU消耗,以及原圖像素,buffer消耗等維度動態算出各進程的負載量,如果Nginx RR到一個負載非常大的進程,可以通過返回重定向狀態碼讓Nginx重新跳轉,這裡可能會出現幾次網路跳轉,但是因為是Loopback,網路上的消耗相對圖片處理的消耗可以忽略不計。

Master進程用來管理Worker進程,當有Worker意外Crash,則會重新拉起一個Worker進程,始終保持和CPU數量一致。 Master進程的健康安全會定期Report給監控系統做告警。

二、小結

當前的圖片服務架構,支撐了攜程每天上億次原圖處理,平均圖片處理延時控制在200毫秒以內,圖片處理失敗率小於萬分之一,從發布至今節點沒有出現宕機現象,偶爾Worker進程有性能問題和Crash也通過日誌和分析工具逐一解決。

如上所述,攜程圖片服務架構經歷了三次改版,從一開始沒有設計複雜的架構,只是為了解決碰到實際問題而重構,到後來根據遇到的問題,不斷調整,也說明了沒有完美的架構,只有適合的架構。

當然,要提供穩定圖片服務,架構是一方面,也必須有其他技術上的支持,比如圖片本身質量和尺寸的優化,盜鏈和版權問題,端到端的實時監控和預警機制,不良內容識別,產品圖片管理和編輯功能,以及海外用戶圖片訪問加速問題。這些問題每個都能寫下不少篇幅的文章,有時間再和小夥伴分享。

目前,攜程圖片服務已在github上開源了小部分功能,開源地址:github.com/ctripcorp/ne

後續會逐步完善,歡迎PR。

【作者簡介】胡健,攜程框架高級研發經理,目前負責多媒體服務的構建和研發工作。

更多來自攜程技術人的一手乾貨,歡迎搜索關注「攜程技術中心」微信公號。

推薦閱讀:

戰狼:業務高速增長,如何保證系統高可用
架構師手記 17 如何設計一個實時大數據分析系統(二)
基於函數計算處理數據並分發的實踐操作
架構師手記17 如何設計一個實時大數據用戶行為分析系統 (一)

TAG:架構 |