(接上文《架構設計:系統存儲(19)——圖片服務器:需求和技術選型(1)》)nginx
關於持久化存儲的數據庫技術要注意一點,實際上它並非圖片服務的必要組件。例如,咱們在進行設計時能夠將圖片訪問的URL地址直接對應圖片文件在服務器上的存儲地址,並按照必定的規則將圖片文件重命名成一個系統中惟一的文件名,最後再刪除Redis和Nginx Proxy Cache中可能存在的歷史文件數據。這樣就算沒有數據庫技術,也能夠保證圖片服務正常工做。web
可是在上文描述的圖片服務需求中,產品團隊還明確要求須要對用戶上傳的規律、活躍度等狀態信息進行統計,須要對圖片的物理磁盤讀操做頻度進行統計分析,因此一些結構化的數據仍是須要作持久化存儲的。就拿圖片的每日訪問狀況來講,當咱們爲經過Flmue收集了Nginx的訪問日誌,並送入到一個獨立的日誌分析系統中進行處理後,相似單個用戶每一天對圖片數據的訪問數量、每張圖片在每一天的訪問數量這樣的分析結果仍是要存入到數據庫中以備後續的統計分析——不管是選用關係型數據庫MySQL、SQL Server或者非關係型數據庫Mongodb、Apache Cassandra。redis
也就是說,實際上要實現完整的圖片服務系統的話仍是離不開使用數據庫技術的,可是這個基本上屬於一個邊緣選型,徹底能夠根據您所在公司某種默認規定的數據庫技術做爲依據。算法
關於Nginx Proxy Cachespring
Nginx的Proxy Cache緩存採用內存索引 + 物理磁盤存儲的工做方式,因此爲了進一步提升Proxy Cache的工做性能,在爲Proxy Cache指定工做目錄時,最好指定到一個獨立掛載點上,而且這個掛載點的底層物理介質最好爲SSD 固態磁盤 + RIAD 5磁盤陣列。另外在後文咱們介紹Proxy Cache配置時,還會討論Proxy Cache的一些注意細節。數據庫
關於Image_Filter模塊
有的讀者可能會問,什麼咱們不直接基於Nginx提供的第三方模塊Image_Filter做爲圖片處理的基礎呢?這個模塊也能夠實現圖片的縮放、裁剪、翻轉、特效等操做。是的,若是您的系統對圖片處理的需求不高,徹底可使用Image_Filter來動態處理圖片請求。Image_Filter使用C/C++語言完成,在完成單張圖片一樣的特效要求的前提下,處理性能也比使用JAVA原生的Image I/O API要高。但Image_Filter能夠提供的圖片效果也是有限,例如Image_Filter提供的特效方面只有透明度、銳化、旋轉、變動圖片質量等操做,但若是系統中有諸如效果加強、背景虛化等這樣的圖片特效要求,那仍是隻能有開發人員自行編程解決;Image_Filter雖然能夠爲圖片加水印效果,可是要求水印圖片背景必須透明(有Alpha通道,後文會講到);最後,這個圖片服務系統是一個對本專題和其餘幾個專題所講解的架構知識的綜合應用,固然最好介紹一下本身編程作作圖片處理的相關知識。編程
在圖片處理系統的首個版本中,咱們計劃先提供諸如圖片等比例縮放、圖片中心點裁剪、圖片白化、圖片文字水印等基本功能,可是爲了保證軟件設計部分可以在後續版本方便進行功能擴展,咱們須要找到一種符合功能特色的行爲模式,做爲基本的設計模式。設計模式
首先,咱們在首期提供的這些功能並不能要求使用者(客戶端)按照某種操做順序執行,而應該由使用者自行肯定操做順序。什麼意思呢?咱們不能規定使用者必須先縮放圖片才能爲圖片添加水印,也不能規定要進行圖片白化,就不能進行圖片裁剪。而應該讓使用者像使用PS軟件同樣——能夠首先進行圖片裁剪,而後再進行白化,最後再添加水印;也能夠先向圖片增長文字水印,而後再進行圖片等比例縮放操做。咱們能夠用如下的一張概要圖表示這裏文字描述的內容:緩存
從緩存中取得圖片這一步的注意事項咱們將在下一節中進行介紹,這裏咱們先看讀取原始圖片後的處理過程。從上圖咱們能夠看到讀取完成後的圖片各類處理組合,有一點相似於生產線的概念,每個圖片處理器接收到上一個處理器的產品後,再按照本身的處理邏輯進行處理,最後輸出到下一個處理器,這種表象性的處理特色符合一個典型的責任鏈模式所適應的處理場景。在責任鏈模式裏,若干實際的處理過程被串成一個鏈式結構,數據在上一個處理器中被處理後傳遞到下一個處理器,每一個處理器都按照本身的業務邏輯規則處理數據。若是責任鏈中某個處理器處理失敗能夠經過返回null或者拋出異常等方式通知整個責任鏈中止處理。服務器
咱們在Http Servlet中使用的Filter就是一種責任鏈模式,在Netty中的ChannelHandler也是基於責任鏈模式進行構造的。另外,責任鏈模式還有不少變種/結構相似的設計模式,例如命令模式和裝飾器模式。在本示例的圖片服務系統中,咱們能夠參照客戶端傳遞的參數,來構造相應的執行順序。例如:當客戶端傳遞zoom|0.8->cut|400|640參數時,表示先按照原始圖片的80%縮小整張圖片,再按照寬400像素高640像素以圖片中心爲基點進行圖片裁剪;當客戶單傳遞mark|aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWlud2Vuamll->cut|800|640參數時,表示先在圖片上加文字水印,文字信息爲編碼前的值,而後在按照寬800像素高640像素以圖片中心爲基點進行圖片裁剪。
上篇文章中咱們介紹到,圖片服務系統將使用Redis Clusters做爲圖片數據遠離客戶端的最後一層緩存系統,那麼存儲什麼樣的數據,以及選用Redis支持的哪一種數據結構進行存儲就須要思考清楚。
在「存儲什麼樣的數據」方面,初步來看有兩種選擇,一種是存儲原始圖片數據另外一種是存儲通過各類圖片處理器通過處理後的用戶最終須要的圖片數據。顯而後者在客戶體驗性、需求契合度和存儲效率上更爲合適,而若是存儲原始圖片不但存儲單張圖片須要的緩存容量更大,更關鍵的是這樣的原始圖片大多數狀況下客戶端並不須要。最後雖然緩存原始圖片能夠下降減輕物理磁盤的I/O壓力,但並不能減輕圖片服務器上CPU的計算壓力,這是由於圖片服務器在從緩存系統中取出原始圖片數據後,大多數狀況下都會再根據客戶端的要求進行各類圖片特效運算,而這一部分操做很是消耗CPU資源。
那麼根據以上的分析,咱們能夠在「存儲什麼樣的數據」方面造成討論結果了。那「存成哪一種數據結構」方面又是怎樣一個思考呢?最直觀的判斷是,既然圖片服務器向Redis Clusters中讀取的是通過各類特效處理後的圖片效果,而一張原始圖片根據不一樣的特效組合處理後,獲得的效果也不同。因此應該使用Redis中的簡單K-V結構進行存儲,其中的Key應該是原始圖片的路徑 + 客戶端給定的特效處理參數,而Value則應該是通過處理後的圖片bytes數據。
但實際狀況真是這樣嗎?實際狀況是以上的內容描述並無考慮太多性能方面的細節,這裏咱們至少還須要討論一個重要性能點:數據文件的大小。雖然一個128 * 128 像素大小24KB的文件數據,相對於物理介質上的存儲來講算是一個小文件,可是它在單個Redis上的存儲卻屬於一個大文件——咱們通常在Redis上存儲的緩存數據也不過是1KB(例如一個通過序列化的用戶信息)。而不少技術資料也代表當單個Redis Value的大小大於10KB時,Redis對於這個Value的讀寫性能會大大下降,甚至還給出了具體的數據寫操做的性能測試結果。另外Redis Cluster保證性能的一個辦法是在客戶端將Key作一次CRC16運算,並根據計算結果將不一樣的Key送入不一樣的Redis Cluster Master節點,這樣多個Redis Cluster Client就能夠在同一時間完成多個Key的操做(Redis Server節點自己是單線程的,其性能徹底依靠epoll、自身實現的事件分離器和全內存態數據存儲來保證)。Java版本的Jedis Client,其CRC16算法工具類的類名是redis.clients.util.JedisClusterCRC16。
根據以上的分析,在存儲一個24KB的圖片文件時,咱們不能直接將這個文件使用一個K-V結構存儲到Redis Cluster的某一個節點上,而是應該將這個較大的數據文件分紅若干byte數據段並對應不一樣的Key,Key的命名的原則是可以經過CRC16算法,計算出不一樣的Slot目標結果。而且還應該將這個圖片的size進行緩存,以便讀取時使用。什麼叫作讓CRC16算法呈現不一樣結果呢?請看以下測試代碼:
public static void main(String[] args) throws Exception {
......
// 如下模擬某個圖片在Redis Cluster存儲的3個分段
System.out.println(JedisClusterCRC16.getCRC16("/path/imagename.png|1"));
System.out.println(JedisClusterCRC16.getCRC16("/path/imagename.png|2"));
System.out.println(JedisClusterCRC16.getCRC16("/path/imagename.png|3"));
......
}
// 如下是可能的計算結果
6450
10577
14704
以上計算結果當Redis Clusters中master的節點個數大於12時,圖片的3個分段就會被存儲到兩個master節點中(6450 slot和10577 slot在一個節點上,14704在另外一個節點上)。在這個圖片服務系統的示例中,咱們將固定5KB爲一個文件分片,也就是說以上的舉例的24KB圖片文件數據會有5個文件分段。
當讀取緩存文件時,客戶端會首先去Redis Cluster上讀取緩存圖片的大小,以便從新肯定文件有幾個分片,而後再到Redis Cluster中讀取每一個數據分片。注意,雖然每一個圖片數據分片都設置了一樣的過時時間,但因爲每一個節點的實際工做狀態不一樣,因此仍是可能出現某些分片數據讀取失敗,這個時候若是任何一個分片讀取失敗就認爲整個讀取過程失敗。若是出現這樣的狀況,圖片系統就會到最下層的分佈式文件系統上讀取數據。
另外在前文中咱們介紹過的Redis單節點的性能配置注意事項,都須要應用到Redis Clusters的配置中,例如中止Redis節點的主動快照和AOF記錄功能,調高操做系統最大文件副本數量,調整redis的backlog參數項等等(詳細討論請參見《架構設計:系統存儲(15)——Redis基本概念和安裝使用》、《架構設計:系統存儲(16)——Redis事件訂閱和持久化存儲》)。最後,因爲咱們預計會使用Redis最基本的K-V結構存儲數據,因此配置信息中關於「緊湊型數據結構」的配置項就不須要多作調整了。
在後續的文章中,咱們將對圖片服務工程中重要的代碼片斷進行演示,例如多個Jedis客戶端同時配合去不一樣的Redis Cluster Slot中讀取數據,並相互等待所有完成後進行byte的合併;再例如使用責任鏈模式按照外部請求方的要求自由組合各類圖片處理器對圖片進行流水線形式的處理,等等。但爲了讀者對整個工程有一個全面的瞭解以便提出本身的改進建議,咱們將在CSDN的下載區上傳整個工程。
這個版本的圖片服務工程將基於Spring Boot進行構建。Spring Boot由Pivotal團隊提供,它是基於Spring Core 4.X版本構建的一套組件庫,它的既定目標是大幅度減小Spring工程在初始化搭建時的配置工做量。舉個例子來講,咱們在使用Spring(3.X版本尤其突出)時,會產生大量的xml配置信息,咱們至少須要在配置信息中設定ScanBase的包路徑、設定若干ApplicationListener、配置數據庫數據源、配置各類對象池和鏈接池。此外若是沒有部署持續化集成服務,咱們還須要自行管理多套配置信息,以便將工程應用到不一樣的部署環境。
使用Spring Boot後,最直觀的現象就是之前工程中的全部配置所需的Spring XML文件都消失了,Spring Boot會按照「約定優於配置」的原則,自動對工程進行掃描並提煉出須要在工程啓動是加載的Bean、ApplicationListener、啓動線程、預處理數據等等資源。爲了方便讀者閱讀源代碼下圖給出了整個工程結構,後續的文章還會給出工程中重要的代碼片斷:
您還能夠直接基於Spring Boot爲圖片服務系統集成一套服務治理框架Spring Cloud,但通過這個實例以前的文章內容討論,您會發現這樣作涉嫌過分設計——除非您的團隊已經搭建了Spring Cloud基礎應用環境,而圖片服務只是做爲一個服務提供節點註冊到現有頂層系統中。
針對大多數TO C端的互聯網系統,圖片服務系統雖然屬於一種頂級子系統(但也不必定,例如某些LBS系統就不怎麼依賴於圖片),但它必須依附於頂層系統的全局規劃。並且圖片服務系統對外暴露的服務接口太少,按照目前的功能規劃,它對外暴露的服務接口就只包括三個:單一圖片上傳、批量上傳和圖片顯示/下載。將爲最頂層全局規劃服務的Spring Cloud Eureka Server(服務發現/註冊)放置到圖片服務系統中既沒有必要也不合理。
在以前的文章中,咱們還從技術功能的角度討論了爲何圖片服務的路由層採用Nginx而不是Spring Cloud Zuul的緣由。這裏咱們再從系統結構層面再進行如下補充:Spring Cloud Zuul着眼於系統服務和系統服務間的調用,其做用在於系統服務間的服務治理,而非單一系統內部的調用;另外,Spring Cloud Zuul最好配合Spring Cloud的其它組件進行使用,例如Spring Cloud Security、Spring Cloud Netfix、Spring Cloud Eureka,而在單一系統內部單獨使用Spring Cloud Zuul不能發揮它的效用。
最後,基於Spring Boot構建圖片服務工程,也是爲了後續的工程部署過程可以靈活選用運行環境和集成環境。例如Spring Boot Web比起傳統的Spring Web工程更能簡便的在微服務組件上運行,如在Docker微服務容器上運行;再例如,若是您所在公司之後決定向Spring Cloud服務治理架構作技術轉型,那麼圖片服務也能夠方便的進行升級,直接將本身提供的服務向Spring Cloud Eureka註冊便可供其它的子系統服務使用。
以上技術特性都是圖片服務的第一個版本須要實現的,但在後續爲了完善圖片服務功能能夠爲圖片服務增長一些新的技術特性。
上文中咱們介紹技術選型時提到,將選用一款分佈式文件系統做爲對原始圖片文件進行持久存儲的技術選型,而且使用高性能的SSD固態磁盤 + 磁盤陣列做物分佈式文件系統的物理層支持。這樣一來系統I/O的吞吐量,特別是讀操做的吞吐量比起簡單的本地塊存儲方案會獲得很高的提高。那麼咱們還有沒有其它方法繼續提升持久化存儲層的數據讀性能呢?咱們首先來分析一下圖片系統中數據讀取和緩存的一些特色:
面向C端的系統有一些共同的特色,就拿圖片服務來講吧,一個商品頁面上會有不少圖片,通常來講它們都不是原始圖片(就是存放在圖片上1MB到2MB的原圖),都是按照必定的要求被縮放、被加印甚至被旋轉的;它們使用的特效模式也基本上是不變的,例如A圖片在界面上被以0.8的比例縮放,那麼同一頁面上的B圖片確定就是以0.7的比例被縮放;最後,因爲它們會在物理磁盤上被同時讀取,因此它們在各級緩存中存在的時間基本上也是一致的,當某張圖片過時時其它圖片也會過時;
那麼咱們可使用預讀的概念,將這些有讀取關係圖片從文件系統上一次性讀取出來,這樣對文件系統的讀操做效率優於一次只讀取一個文件的操做效率。預讀的概念咱們在本專題介紹塊存儲的文章中介紹過一次(讀者能夠參考《架構設計:系統存儲(1)——塊存儲方案(1)》)。預讀技術基於局部性原理,這是說計算機上某些相關部分的資源,都會存在於一個集中的區域,CPU寄存器、內存地址、磁盤數據等等,當某個資源X被處理時,和它臨近的若干資源也即將被處理。這個概念能夠被運用到圖片處理上:若是某張頁面上的圖片A被讀取時,這張圖片上的其它圖片也將同時被讀取。
因爲咱們的圖片系統並無集成持久化數據庫技術,因此沒法記錄某一個文件和哪些文件存在讀取聯繫。並且即便可以記錄這些原始文件的上傳關係,也不能做爲文件預讀的依據——由於客戶端請求圖片信息時並非請求原始圖片,而是請求通過特效處理後的圖片,也就是說圖片C通過特效處理後的圖片C1,和圖片D通過特效處理後的圖片D1才存在讀取關聯。
這樣的圖片讀取關係顯然只能經過對圖片讀取請求的持續分析才能得出,而這個分析源頭能夠基於Nginx層access.log日誌,而分析的手段能夠基於相似Hadoop MapReduce這樣的離線/延遲分析手段。分析過程也很好理解,即按照10毫秒爲單位以某一個訪問路徑爲參照(帶特效參數的),對後續又再次出現了這個訪問路徑的毫秒範圍內的全部訪問路徑取交集,交集運算次數越多,獲得的圖片讀取關係就越準確:
這樣系統就能夠得出,當圖片A通過特效X處理後,緊接着最有可能會讀取的其它文件和須要加載的特效,這樣依賴圖片系統就能夠對後續的圖片進行預讀並在完成特效處理存儲到緩存系統中。這個圖片關聯關係的分析工做計算量比較大,以上只是計算的某一個文件的關聯狀況,試想一下全部的圖片都要進行相似的分析過程,而後還要過濾出重複的分析數據,因此只有依靠大數據分析手段完成。
咱們好像一直沒有討論過圖片的刪除問題,實際上並非全部的圖片系統都須要圖片刪除功能,甚至有些系統還會特別說明全部的原始圖片都要進行永久保存。但若是圖片系統的存儲容量確實有限,而且團隊暫時沒有太多資金進行存儲擴容,那麼刪除一些再也不使用的圖片就是一個節約存儲容量的好辦法。但關鍵問題是,怎樣判斷圖片再也不使用呢?
最直觀的思路是,按照圖片上傳時間向後推導3至6個月的時間到一個固定的時間點,若是超過這個固定時間點就將這張圖片刪除。但這樣作的話圖片系統並不能肯定這些圖片在後續的時間不會被請求者訪問,例如一些暢銷商品甚至會保持1年以上的銷售熱度。還有一種刪除思路,是由客戶端自行進行刪除操做,例如當一個商品下架時同時刪除商品圖片。但這樣作也有問題,由於後續運營團隊可能還會在進行後期銷售總結是訪問這個商品的快照信息,這時也會同時查看這個商品的圖片。
那麼怎樣刪除纔是較合理的呢?首先是刪除時機的問題,顯然給定一個固定的時間長度做爲刪除依據是不知足要求的,時間長度的選擇應該是動態的:利用數據處理工具分析出當前某張圖片最後一次訪問時間,若是當前時間離該圖片最後一次訪問時間大於規定的閾值(例如3個月、6個月等值),就啓動刪除過程。另外從刪除策略上來講,一張圖片的刪除不能不留餘地的直接刪除原始圖片自己,一張原始圖片的大小在1MB——2MB左右(可能還會更大,這徹底取決於系統提供的圖片上傳功能中對圖片大小的限制問題),而一張通過特效處理後的圖片大小在100KB——300KB左右(可能還會更小,這徹底看特效處理的狀況),因此這裏能夠採用漸進式刪除的方式。固然若是發現這樣原始圖片存在多種特效處理規則,而且通過這些規則處理後的圖片大小總和已經大於原始圖片的大小了,則能夠跳過漸進式刪除過程:
經過刪除原始圖片替換保留特效結果文件,能夠有效防止原始圖片刪除後用戶零星訪問的空窗期。待到一個更長的,再無任何圖片訪問請求的時間期後,最終將圖片全部的存儲痕跡所有抹去,這時若是用戶再進行訪問就會出現圖片已過時的提示。經過漸進式刪除過程,通常能夠在刪除的第一階段騰出20%——30%左右的存儲容量,並且不會對用戶後續的零星訪問形成任何影響(但不在容許用戶設定新的特效了),最後在保證用戶有90%以上的概率沒有再次訪問該圖片的可能後,在對圖片進行正式刪除。漸進式刪除不適合全部的圖片服務,本文仍是建議在存儲容量充足、集羣服務性能足夠的狀況下對原始圖片進行永久保存(至少3——5年)。
=================== (下文咱們將對圖片工程中重要的代碼片斷進行講解)