ZIMG -- 高性能圖片服務器淺談

(偶然的在網上看到這篇文章, 覺得ZIMG很優秀, 只是目前版本是1.0還不支持分佈式, 也期望後續版本如作者所說會支持集羣.

項目代碼可以去github: https://github.com/buaazp/zimg/)


綜述

2011年李彥宏在百度聯盟峯會上就提到過互聯網的讀圖時代已經到來1,圖片服務早已成爲一個互聯網應用中佔比很大的部分,對圖片的處理能力也相應地變成企業和開發者的一項基本技能。需要處理海量圖片的典型應用有:
1. 圖片類應用,如百度相冊。
2. 導購類應用,如Guang.com。
3. 電商類應用,如淘寶。
4. 雲存儲服務,如七牛雲存儲。
除此之外幾乎所有的網站都需要考慮自己圖片處理的解決方案,以免在流量變大之後顯得手足無措。
本文將從作者自己設計完成的圖片服務程序zimg的設計思路出發,探討高性能圖片服務器的特點、難點和應對辦法。

主要問題

要想處理好圖片,需要面對的三個主要問題是:大流量,高併發,海量存儲。下面將逐一進行討論。

大流量

除了那些擁有自己數據中心的大型企業,中小型企業都需要考慮到流量問題,因爲流量就是成本,圖片相對於文本來說流量增加了一個數量級,省下的每一個字節都是白花花的銀子。我曾經在一篇博客2裏看到,作者在業務邏輯中引入PHP的imagick模塊進行壓縮,短短几行代碼就做到了每個月爲公司節省2萬人民幣的效果,可見凡是涉及到圖片的互聯網應用,都應該統籌規劃,降低流量節約開支。

高併發

高併發的問題在用戶量較低時幾乎不會出現,但是一旦用戶攀升,或者遇到熱點事件,比如淘寶的雙十一,或者網站被人上傳了一張爆炸性的新聞圖片,短時間內將會涌入大量的瀏覽請求,如果架構設計得不好,又沒有緊急應對方案,很可能導致大量的等待、更多的頁面刷新和更多請求的死循環。總的來說,就是要把圖片服務的性能做得足夠好。

海量存儲

在2012年的介紹Facebook圖片存儲的文章3裏提到,當時Facebook用戶上傳圖片15億張,總容量超過了1.5PB,這樣的數量級是一般企業無法承受的。雖然我們很難做出一個可以跟Facebook比肩的應用,但是從架構設計的角度來說,良好的拓展方案還是要有的。我們需要提前設計出最合適的海量圖片數據存儲方案和操作方便的拓容方案,以應對將來不斷增長的業務需求。

以上三個問題,其實也是相互制約和鉗制的,比如要想降低流量,就需要大量的計算,導致請求處理時間延長,系統單位時間內的處理能力下降;再比如爲了存儲更多的圖片,必然要在查找上消耗資源,同樣也會降低處理能力。所以,圖片服務雖然看起來業務簡單,實際做起來也不是一件小事。

設計方案

zimg是作者針對圖片處理服務器而設計開發的開源程序,它擁有很高的性能,也滿足了應用在圖片方面最基本的處理需求,下面將從架構設計、代碼邏輯和性能測試等方面進行介紹。

總體思路

想要在展現圖片這件事情上有最好的表現,首先需要從整體業務中將圖片服務部分分離出來。使用單獨的域名和建立獨立的圖片服務器有很多好處,比如:
1. CDN分流。如果你有注意的話,熱門網站的圖片地址都有特殊的域名,比如微博的是ww1.sinaimg.cn,人人的是fmn.xnpic.com等等,域名不同可以在CDN解析的層面就做到非常明顯的優化效果。
2. 瀏覽器併發連接數限制。一般來說,瀏覽器加載HTML資源時會建立很多的連接,並行地下載資源。不同的瀏覽器對同一主機的併發連接數限制是不同的,比如IE8是10個,Firefox是30個。如果把圖片服務器獨立出來,就不會佔用掉對主站連接數的名額,一定程度上提升了網站的性能。
3. 瀏覽器緩存。現在的瀏覽器都具有緩存功能,但是由於cookie的存在,大部分瀏覽器不會緩存帶有cookie的請求,導致的結果是大量的圖片請求無法命中,只能重新下載。獨立域名的圖片服務器,可以很大程度上緩解此問題。

圖片服務器被獨立出來之後,會面臨兩個選擇,主流的方案是前端採用Nginx,中間是PHP或者自己開發的模塊,後端是物理存儲;比較特別一些的,比如Facebook,他們把圖片的請求處理和存儲合併成一體,叫做haystack,這樣做的好處是,haystack只會處理與圖片相關的請求,剝離了普通http服務器繁雜的功能,更加輕量高效,同時也使部署和運維難度降低。
zimg採用的是與Facebook相似的策略,將圖片處理的大權收歸自己所有,絕大部分事情都由自己處理,除非特別必要,最小程度地引入第三方模塊。
注:zimg的1.0版本,設計面向圖片量在TB級別的中小型服務,物理存儲暫時不支持分佈式集羣,分佈式功能將在2.0版本中完成。

架構設計

爲了極致的性能表現,zimg全部採用C語言開發,總體上分爲三個層次,前端http處理層,中間圖片處理層和後端的存儲層。下圖爲zimg架構設計圖:
總體架構

http處理層引入基於libevent的libevhtp庫,libevhtp是一款專門處理基本http請求的庫,它太適合zimg的業務場景了,在性能和功能之間找到了很好的平衡點。圖片處理層採用imagemagick庫,imagemagick是現在公認功能最強,性能最好的圖片處理函數庫。存儲層採用memcached緩存加直接讀寫硬盤的方案,更加深入的優化將在後續進行,比如引入TFS4等。爲了避免數據庫帶來的性能瓶頸,zimg不引入結構化數據庫,圖片的查找全部採用哈希來解決。
事實上圖片服務器的設計,是一個在I/O與CPU運算之間的博弈過程,最好的策略當然是繼續拆:CPU敏感的http和圖片處理層部署於運算能力更強的機器上,內存敏感的cache層部署於內存更大的機器上,I/O敏感的物理存儲層則放在配備SSD的機器上,但並不是所有人都能負擔得起這麼奢侈的配置。zimg折中成本和業務需求,目前只需要部署在一臺服務器上。由於不同服務器硬件不同,I/O和CPU運算速度差異很大,很難一棒子定死。zimg所選擇的思路是,儘量減少I/O,將壓力放在CPU上,事實證明這樣的思路基本沒錯,在硬盤性能很差的機器上效果更加明顯;即使以後SSD全面普及,CPU的運算能力也會相應提升,總體來說zimg的方案也不會太失衡。

代碼層面

雖然zimg在二進制實體上沒有分模塊,上面已經提到了原因,現階段面向中小型的服務,單機部署即可,但是代碼上是分離的,下面介紹主要部分的功能和實現,更詳細的內容可以從github上拉下來研究。熱烈歡迎大家fork和contribute。

main.c是程序的入口,主要功能是處理啓動參數,部分參數功能如下:

-p [port] 監聽端口號,默認4869
-t [thread_num] 線程數,默認4,請調整爲具體服務器的CPU核心數
-k [max_keepalive_num] 最高保持連接數,默認1,不啓用長連接,0爲啓用
-l 啓用log,會帶來很大的性能損失,自行斟酌是否開啓
-M [memcached_ip] 啓用緩存的連接IP
-m [memcached_port] 啓用緩存的連接端口
-b [backlog_num] 每個線程的最大連接數,默認1024,酌情設置

zhttpd.c是解析http請求的部分,分爲GET和POST兩大部分,GET請求會根據請求的URL參數去尋找圖片並轉給圖片處理層處理,最後將結果返回給用戶;POST接收上傳請求然後將圖片存入計算好的路徑中。
爲了實現zimg的總體設計願景,zhttpd承擔了很大部分的工作,也有一些關鍵點,下面撿重點的說一下:

在zimg中圖片的唯一Key值就是該圖片的MD5,這樣既可以隱藏路徑,又能減少前端(指zimg前面的部分,可能是你的應用服務器)和zimg本身的存儲壓力,是避免引入結構化存儲部分的關鍵,所以所有GET請求都是基於MD5拼接而成的。
大家設想一下,假如你的網站某個地方需要展示一張圖片,這個圖片原圖的大小是1000*1000,但是你想要展示的地方只有300*300,你會怎麼做呢?一般還是依靠CSS來進行控制,但是這樣的話就會造成很多流量的浪費。爲此,zimg提供了圖片裁剪功能,你所需要做的就是在圖片URL後面加上w=300&h=300(width和height)即可。
另一個情景是圖片灰白化,比如某天遇到重大自然災害,想要網站所有圖片變成灰白的,那麼只需在圖片URL後面再加上g=1(gray)即可。
當然,依託於imagemagick所提供的完善的圖片處理函數,zimg將在後續版本中逐步增加功能,比如加水印等。

在圖片上傳部分,其實能玩的花樣很少,但是編寫代碼所消耗的時間最多。現在我們再假設一種情景,如果我們的圖片服務器前端採用Nginx,上傳功能用PHP實現,需要寫的代碼很少,但是性能如何呢,答案是很差。首先PHP接收到Nginx傳過來的請求後,會根據http協議(RFC1867)分離出其中的二進制文件,存儲在一個臨時目錄裏,等我們在PHP代碼裏使用$_FILES["upfile"][tmp_name]獲取到文件後計算MD5再存儲到指定目錄,在這個過程中有一次讀文件一次寫文件是多餘的,其實最好的情況是我們拿到http請求中的二進制文件(最好在內存裏),直接計算MD5然後存儲。
於是我去閱讀了PHP的源代碼,自己實現了POST文件的解析,讓http層直接和存儲層連在了一起,提高了上傳圖片的性能。關於RFC1867的內容和PHP是如何處理的,感興趣的讀者可以去搜索瞭解下,這裏推薦@Laruence的文章《PHP文件上傳源碼分析(RFC1867) 》
除了POST請求這個例子,zimg代碼中有多處都體現了這種“減少磁盤I/O,儘量在內存中讀寫”和“避免內存複製”的思想,一點點的積累,最終將會帶來優秀的表現。

zimg.c是調用imagemagick處理圖片的部分,這裏先解釋一下在zimg中圖片存儲路徑的規劃方案。
上文曾經提到,現階段zimg服務於存儲量在TB級別的單機圖片服務器,所以存儲路徑採用2級子目錄的方案。由於Linux同目錄下的子目錄數最好不要超過2000個,再加上MD5的值本身就是32位十六進制數,zimg就採取了一種非常取巧的方式:根據MD5的前六位進行哈希,1-3位轉換爲十六進制數後除以4,範圍正好落在1024以內,以這個數作爲第一級子目錄;4-6位同樣處理,作爲第二級子目錄;二級子目錄下是以MD5命名的文件夾,每個MD5文件夾內存儲圖片的原圖和其他根據需要存儲的版本,假設一個圖片平均佔用空間200KB,一臺zimg服務器支持的總容量就可以計算出來了:

1024 * 1024 * 1024 * 200KB = 200TB

這樣的數量應該已經算很大了,在200TB的範圍內可以採用加硬盤的方式來拓容,當然如果有更大的需求,請期待zimg後續版本的分佈式集羣存儲支持。
除了路徑規劃,zimg另一大功能就是壓縮圖片。從用戶角度來說,zimg返回來的圖片只要看起來跟原圖差不多就行了,如果確實需要原圖,也可以通過將所有參數置空的方式來獲得。基於這樣的條件,zimg.c對於所有轉換的圖片都進行了壓縮,壓縮之後肉眼幾乎無法分辨,但是體積將減少67.05%。具體的處理方式爲:

圖片裁剪時使用LanczosFilter濾鏡;
以75%的壓縮率進行壓縮;
去除圖片的Exif信息;
轉換爲JPEG格式。

經過這樣的處理之後可以很大程度的減少流量,實現設計目標。

zcache.c是引入memcached緩存的部分,引入緩存是很重要的,尤其是圖片量級上升之後。在zimg中緩存被作爲一個很重要的功能,幾乎所有zimg.c中的查找部分都會先去檢查緩存是否存在。比如:
我想要a(代表某MD5)圖片裁剪爲100*100之後再灰白化的版本,那麼過程是先去找a&w=100&h=100&g=1的緩存是否存在,不存在的話去找這個文件是否存在(這個請求所對應的文件名爲 a/100*100pg),還不存在就去找這個分辨率的彩色圖緩存是否存在,若依然不存在就去找彩色圖文件是否存在(對應的文件名爲 a/100*100p),若還是沒有,那就去查詢原圖的緩,原圖緩存依然未命中的話,只能打開原圖文件了,然後開始裁剪,灰白化,然後返回給用戶並存入緩存中。
可以看出,上面過程中如果某個環節命中緩存,就會相應地減少I/O或圖片處理的運算次數。衆所周知內存和硬盤的讀寫速度差距是巨大的,那麼這樣的設計對於熱點圖片抗壓將會十分重要。

除了上述核心代碼以外就是一些支持性的代碼了,比如log部分,md5計算部分,util部分等。

性能測試

爲了橫向對比zimg的性能,我用PHP寫了一個功能一模一樣的後端,僅用時一下午,這充分證明了“PHP是世界上最好的語言”,也同時說明了用C語言來進行開發是多麼的辛苦,不過,我喜歡性能測試結果出來之後的那份成就感,這樣的付出我覺得是值得的。

測試方案

採用Apache自帶的測試程序ab對指定請求進行測試,在特定併發數100的情況下進行10w個請求的測試,結果依據該併發下每秒處理請求數來定性,對比的方案是未啓用緩存的zimg,啓用緩存的zimg和Nginx+PHP,其中zimg端口爲4868,Nginx端口爲80。

測試命令分別爲:

ab2 -c 100 -n 100000 http://127.0.0.1:4869/5f189d8ec57f5a5a0d3dcba47fa797e2
ab2 -c 100 -n 100000 http://127.0.0.1:80/zimg.php?md5=5f189d8ec57f5a5a0d3dcba47fa797e2
ab2 -c 100 -n 100000 http://127.0.0.1:4869/5f189d8ec57f5a5a0d3dcba47fa797e2?w=100&h=100&g=1
ab2 -c 100 -n 100000 http://127.0.0.1:80/zimg.php?md5=5f189d8ec57f5a5a0d3dcba47fa797e2&w=100&h=100&g=1

注:以下測試數據單位皆爲rps(request per second)。

測試環境

操作系統:openSUSE 12.3
CPU:Intel Xeon E3-1230 V2
內存:8GB DDR3 1333MHz
硬盤:西部數據 1TB 7200轉

軟件版本

zimg:1.0.0
Nginx:1.2.9
PHP:5.3.17

測試結果

測試項目 zimg zimg+memcached Nginx+PHP
靜態圖片 2857.80 4995.95 426.56
動態裁剪圖片 2799.34 4658.35 58.61

總的來說測試結果符合預期,純C寫成並且專門爲圖片而做了大量優化的zimg表現遠遠優於採用PHP的方案,性能有6-79倍的提升。

高壓測試

在測試過程中由於php-fpm的性能瓶頸,導致併發壓力根本壓不上去,爲了充分展現zimg面對超高併發的抗壓能力,我又做了另一項對比測試,即單純的echo測試。測試方法是在逐漸升高的併發壓力下完成20w個echo請求,記錄每種併發壓力下的處理能力。硬件環境不變,這次所要對比的是業界以性能著稱的Nginx,Nginx和zimg都是接收echo請求後返回簡單的“It works!”頁面,不做任何複雜的業務。

測試命令分別爲:

ab2 -c 5000 -n 200000 http://127.0.0.1:4869/
ab2 -c 5000 -n 200000 http://127.0.0.1:80/

測試結果如下:

Concurrency zimg Nginx
100 32765.35 33412.12
300 32991.86 32063.05
500 31364.29 30599.07
1000 28936.67 28163.63
2000 27939.02 25124.51
3000 28168.56 22053.22
4000 28463.45 21464.88
5000 27947.37 13536.93
6000 27533.83 14430.21
7000 27502.03 14623.62
8000 26505.07 13389.28
9000 27124.89 13650.01
10000 27446.23 10901.13
11000 26335.22 10585.73
12000 27068.68 10461.54
13000 26798.55 8530.11
14000 26741.93 7628.09
15000 26556.54 9832.16
16000 26815.70 8018.44
17000 27811.33 7951.21
18000 25722.97 6246.00
19000 26730.02 8134.93
20000 27678.67 6106.95

這是一份有趣的數據,其實測試過程中,Nginx在併發1000開始已經出現了部分失敗,在併發9000以後就無法完成20w個請求,通過不斷降低請求數才勉強完成了測試。而強大的zimg毫無壓力地完成了20000併發以內的所有測試,沒有一個失敗返回。爲了直觀地顯示測試結果請參考下圖:

測試結果

由於去掉了不需要的複雜功能,zimg在http處理層面要遠比Nginx輕量,同時測試數據也說明了它的高併發抗壓能力。能有這樣的成績則完全要歸功於libevhtp項目,它比libevent自帶的http庫要優秀得多。在我設計zimg的早期版本時,選用了libevent自帶的evhttp庫,然後採用線程池的方式來實現多線程處理,結果發現在高壓力之下問題頻出,最後無奈放棄。該版本封存在github上的zimg_workqueue分支中,也算是一個紀念吧。

最後

圖片服務器的設計方案多種多樣,zimg也只是提供了其中的一種思路而已,它纔剛剛誕生,以後還有很長的路要走,共同學習,共同進步。
在孤獨而漫長的開發過程中,經常會遇到思維枯竭毫無頭緒的時候,感謝好基友@Xscape給予大量建設性指導性意見;還有@喀啦喀拉在存儲路徑規劃問題上提供的思路。
那麼就用這樣一句經典而充滿力量的話作爲結尾吧。

We stand on the shoulders of giants.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章