2011年李彥宏在百度聯盟峯會上就提到過互聯網的讀圖時代已經到來1,圖片服務早已成爲一個互聯網應用中佔比很大的部分,對圖片的處理能力也相應地變成企業和開發者的一項基本技能。須要處理海量圖片的典型應用有:
1. 圖片類應用,如百度相冊。
2. 導購類應用,如Guang.com。
3. 電商類應用,如淘寶。
4. 雲存儲服務,如七牛雲存儲。
除此以外幾乎全部的網站都須要考慮本身圖片處理的解決方案,以避免在流量變大以後顯得手足無措。
本文將從做者本身設計完成的圖片服務程序zimg的設計思路出發,探討高性能圖片服務器的特色、難點和應對辦法。php
要想處理好圖片,須要面對的三個主要問題是:大流量,高併發,海量存儲。下面將逐一進行討論。html
除了那些擁有本身數據中心的大型企業,中小型企業都須要考慮到流量問題,由於流量就是成本,圖片相對於文原本說流量增長了一個數量級,省下的每個字節都是白花花的銀子。我曾經在一篇博客2裏看到,做者在業務邏輯中引入PHP的imagick模塊進行壓縮,短短几行代碼就作到了每月爲公司節省2萬人民幣的效果,可見凡是涉及到圖片的互聯網應用,都應該統籌規劃,下降流量節約開支。前端
高併發的問題在用戶量較低時幾乎不會出現,可是一旦用戶攀升,或者遇到熱點事件,好比淘寶的雙十一,或者網站被人上傳了一張爆炸性的新聞圖片,短期內將會涌入大量的瀏覽請求,若是架構設計得很差,又沒有緊急應對方案,極可能致使大量的等待、更多的頁面刷新和更多請求的死循環。總的來講,就是要把圖片服務的性能作得足夠好。node
在2012年的介紹Facebook圖片存儲的文章3裏提到,當時Facebook用戶上傳圖片15億張,總容量超過了1.5PB,這樣的數量級是通常企業沒法承受的。雖然咱們很難作出一個能夠跟Facebook比肩的應用,可是從架構設計的角度來講,良好的拓展方案仍是要有的。咱們須要提早設計出最合適的海量圖片數據存儲方案和操做方便的拓容方案,以應對未來不斷增加的業務需求。git
以上三個問題,其實也是相互制約和鉗制的,好比要想下降流量,就須要大量的計算,致使請求處理時間延長,系統單位時間內的處理能力降低;再好比爲了存儲更多的圖片,必然要在查找上消耗資源,一樣也會下降處理能力。因此,圖片服務雖然看起來業務簡單,實際作起來也不是一件小事。github
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分支中,也算是一個記念吧。