乾貨 | 攜程圖片服務架構

Java高階開發發表於2018-04-23

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

近些年攜程業務突飛猛進,使用者遍及世界各地。公司對使用者體驗也越來越重視,每一個小的功能改動、頁面改版的背後,都有大量的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裡實現很多功能,並且很多基礎資料結構都要自己寫或者依賴第三方,不僅開發工作量大,穩定性和正確性的驗證也需要花費不少的精力。

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

1、具有1-5工作經驗的,面對目前流行的技術不知從何下手,

需要突破技術瓶頸的。

2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,用設計思想,常用java開發框架掌握熟練的。

4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的。

  1. 群號:高階架構群 ⑥⑨⑦⑤⑦⑨⑦⑤① 備註好資訊!

6.阿里Java高階大牛直播講解知識點,分享知識,

3、現階段

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

這裡寫圖片描述

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

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

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

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

https://sourceforge.net/p/graphicsmagick/mailman/graphicsmagick-help/?viewmonth=201708 .

問題描述是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上開源了小部分功能,開源地址: https://github.com/ctripcorp/nephele

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

高階架構群:697579751(大牛交流群)沒有開發經驗勿擾!
這裡寫圖片描述

相關文章