緩存服務器設計與實現(一)

    這裏我們nginx的cache系統爲線索,來探討一個緩存服務器的設計和相關細節,我儘量站在設計和框架的角度來分析,限於篇幅這裏不再去擼代碼了,相關的細節,歡迎大家一起參與討論。

    一個cache服務器中從後端取得文件之後,要麼直接發送給客戶端(學名叫透傳),要麼緩存在本地,後續相同的請求訪問到cache服務器時,就可以直接拿本地的拷貝來用了,如果可以用的話。如果本地緩存的文件被後續的請求訪問到,在cache中叫做命中(即Hit)。如果本地還沒有文件的緩存拷貝,那麼cache服務器需要根據配置或者做解析域名,去後端獲取文件,這時稱爲緩存miss,即未命中。關於cache服務器更多的知識,我們在分析nginx的緩存系統時再深入討論。

    nginx的存儲系統分兩類,一類是通過proxy_store開啓的,存儲方式是按照url中的文件路徑,存儲在本地。比如/file/2013/0001/en/test.html,那麼nginx就會在指定的存儲目錄下依次建立各個目錄和文件。另一類是通過proxy_cache開啓,這種方式存儲的文件不是按照url路徑來組織的,而是使用一些特殊方式來管理的(這裏稱爲自定義方式),自定義方式就是我們要重點分析的。那麼這兩種方式各有什麼優勢呢?

    按url路徑存儲文件的方式,程序處理起來比較簡單,但是性能不行。首先有的url巨長,我們要在本地文件系統上建立如此深的目錄,那麼文件的打開和查找都很會很慢(回想kernel中通過路徑名查找inode的過程吧)。如果使用自定義方式來處理模式,儘管也離不開文件和路徑,但是它不會因url長度而產生複雜性增加和性能的降低。從某種意義上說這是一種用戶態文件系統,最典型的應該算是squid中的CFS。nginx使用的方式相對簡單,主要依靠url的md5值來管理,後面我們會分析。

    緩存離不開從後端取內容,然後發送給客戶端。具體的處理方式大家很容易就會想到,肯定是一邊接收一邊發送,其他的方式都太低效了,如讀完再發等等。這裏提一下nginx邊收邊發,使用的結構是ngx_event_pipe_t,它是溝通後端和客戶端的媒介。由於該結構是一個通用組件,所以需要一些特殊的標記來處理涉及存儲的相關功能,那麼成員cacheable就擔當了這份重任。
p->cacheable = u->cacheable || u->store;
即cacheable爲1,則需要存儲,否則不存儲。那麼u->cacheable跟u->store代表什麼?他們分別代表前面說的兩種方式,即proxy_cache和proxy_store。 

(補充一些知識,nginx在取後端數據時,它的行爲受proxy_buffering控制,作用是爲後端的服務器啓用應答緩衝。如果啓用緩衝,nginx假設被代理服務器能夠非常快的傳遞應答,並將其放入緩衝區,可以使用proxy_buffer_size和proxy_buffers設置相關參數。如果響應無法全部放入內存,則將其寫入硬盤。如果禁用緩衝,從後端傳來的應答將立即被傳送到客戶端。)

    這裏都是一些擦邊球,我們還沒有接觸nginx cache功能的核心。從實現上看,在nginx upstream結構中有個成員叫cache,它的類型是ngx_shm_zone_t。如果我們開啓cache功能,cache成員用來管理共享內存(爲什麼用到了共享內存?),而其他方式的存儲該成員都爲NULL。另外有一點需要說明一下,在cache系統中一個文件通常被稱爲store object,即緩存對象,所以進行cache之前必然需要先創建一個store object。一個重要的問題就是如何選擇創建的時機,這點大家有什麼看法?首先我們需要檢查一個文件是否是需要緩存,很明顯GET方法請求的文件一般需要緩存,所以我們在請求處理的前期,看到了GET方法,就可以先創建一個對象。但是很多時候,即使是一個GET方法請求的文件也不能緩存,那麼你過早的創建對象,不僅浪費時間也浪費了空間,到頭來還要將它銷燬。那麼什麼會影響GET請求的存儲呢?那就是響應頭中的Cache-control字段,這個字段就告訴代理或者瀏覽器,該文件能否被緩存。一般的cache服務器面對響應頭中沒有Cache-control字段的請求,默認都是要緩存的。

    基於這一點的考慮,我們開發的cache服務器就是在響應頭解析完成,拿到可緩存的足夠證據之後,纔會創建緩存對象。遺憾的是,nginx沒有這麼去做。
nginx在ngx_http_upstream_init_request函數中完成緩存對象的創建,這個函數處在http處理的什麼階段呢?在跟後端建立連接之前。這個地方,我個人認爲不太合適。。。大家認爲呢?

    關於創建過程,大家可以去讀函數ngx_http_upstream_cache。這裏我拿我們的cache跟nginx對比來分析吧。我們的request中使用一個名叫store的成員,來跟緩存對象建立聯繫。nginx也差不多,它的request結構體中有個cache成員來做同樣的事情。區別在於我們的store成員對應的空間在共享內存中,而nginx則是在r->pool裏申請的(我們爲什麼這麼做?)。

    下一步,nginx需要根據配置來生成緩存對象的key,此處一般都是用md5來算的。這個key作爲一個緩存對象在系統中的唯一標識,很多人可能擔心md5碰撞的問題。這個我認爲要求如果不是特別苛刻,這裏完全可以接受的,而且處理也相對簡單。

後面要處理的是,文件到底應該已怎樣的形式存儲在磁盤?
    我們拿前面用過的一個例子:/file/2013/0001/en/test.html,它對應的md5值是8ef9229f02c5672c747dc7a324d658d0,實際上nginx就用它當做文件名。這樣就可以了?如果我們找一個目錄來存放文件,裏面都是一堆這樣的文件,那麼會怎樣?我們知道,大多數文件系統下,都對單個目錄下的文件數量有限制,所以這樣簡單粗暴的處理是不行的。那怎麼辦?nginx通過配置可以讓你使用多級目錄,來解決這個問題。簡單來說,nginx通過levels這個指令指定目錄層數(冒號分隔)和每個目錄名字的字符個數,在我們的例子中,假設配置levels=1:2,意思是說使用兩級目錄,第一級目錄名是一個字符,第二級用兩個字符。但是nginx最大支持3級目錄,即levels=xxx:xxx:xxx。

    那麼構成目錄名字的字符哪來的呢?假設我們的存儲目錄爲/cache,levels=1:2,那麼對於上面的文件 就是這樣存儲的:
  /cache/0/8d/8ef9229f02c5672c747dc7a324d658d0
    看到0和8d這兩個目錄名怎麼來的了吧,不用解釋了。

    對象創建完成之後,就需要緩存對象管理結構中去了,這個ngx_http_file_cache_exists去處理的。

    如果在創建這個文件時,當前目錄及文件已經存在,那如何處理?大家可以去翻翻代碼,看nginx怎麼處理的。

    討論先告一個段落,其實現在都是一些準備工作,下次討論後端內容到來的處理。

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