應用層優化
應用層緩存
- 2.本地共享內存緩存
這種緩存一般是中等大小(幾個GB),快速,難以在多臺機器間同步。它們對小型的半靜態位數據比較合適。例如每個州的城市列表,分片數據存儲的分區函數(映射表),或者使用存活時間(TTL)策略進行失效的數據等。共享內存最大的好處是訪問非常快——通常比其他任何遠程緩存訪問都要快不少 - 3.分布式內存緩存
最常見的分布式內存緩存的例子是memcached.分布式緩存比本地共享內存緩存要大得多,增長也容易。緩存中創建的數據每一個比特都只有一份副本,這樣既不會浪費內存,也不會因為相同的數據存在不同的地方而引入一致性問題。分布式內存非常適合存儲共享對象,例如用戶資料,評論,以及HTML片段。分布式緩存比本地共享緩存的延時要高得多,所以最高效的使用方法是批量進行多個獲取操作(例如,在一次循環中獲取多個對象)。分布式緩存還需要考慮怎么增加更多的節點,以及某個節點崩潰了怎么處理。對于這兩個場景,應用程序必需決定在節點間怎么分布或重分布緩存對象。當緩存集群增加或減少一臺服務器時,一致性緩存對避免性能問題而言是非常重要的。 - 4.磁盤上的緩存
磁盤是很慢的,所以緩存在磁盤上的最好是持久化對象,很難全部裝進內存的對象,或者靜態內容(例如預處理的自定義圖片)。對于磁盤上的緩存和Web服務器,一個非常有用的技巧是使用404錯誤處理機制來捕捉緩存未命中的情況。假設Web應用要在頭部展示一張基于用于名(“歡迎回來,John”)的自定義圖片。并且通過/images/welcomeback/john.jpg這樣的訪問路徑引用此圖片。如果圖片不存在,將會導致一個404錯誤,并且觸發上述錯誤處理。這個錯誤處理可以生成圖片,在磁盤上存儲它,然后發出一個重定向或者將該圖片傳回瀏覽器。后續的請求只需要從文件中直接返回圖片。有很多類型的內容可以使用這種技巧。例如,不用再將最近的標題作為HTML部分進行緩存,可以在Javascript文件中存儲這些東西,然后在網頁頭重引用這個文件:latest_headlines.js.緩存失效很簡單:刪除文件即可。可以通過執行一個刪除N分鐘前所創建的文件的定時任務,來實現TTL失效。如果想要限制緩存大小,也可以通過按最近訪問時間排序來刪除文件,從而實現最近最少使用(LRU)失效算法。如果失效策略是基于最近訪問時間,則必須在文件系統掛載參數中打開訪問時間記錄(忽略noatime選項即可),如果這么做,應該使用內存文件系統來避免大量磁盤操作。
緩存控制策略
緩存也有像反范式化數據庫設計一樣的問題:重復數據,也就是說有多個地方需要更新數據,所以需要想辦法避免讀到臟數據。下面是一些最常見的緩存控制策略:
- 1.TTL(time to live,存活時間)
緩存對象存儲時設置一個過期時間;可以通過清理進程在達到過期時間后刪掉最想,或者先留著直到下次訪問時再清理(清理后需要使用新的版本替換)。對于數據很少變更或者沒有新數據的情況,這是最好的失效策略 - 2.顯式失效
如果不能接受臟數據,那么進程在更新原始數據時需要同時使緩存失效。這種策略有兩個寫——失效和寫——更新。寫——失效策略很簡單:只需要標記緩存數據已經過期(是否清理緩存數據是可選的)。寫——更新策略需要多做一些工作,因為在更新數據時就需要替換掉緩存項。無論如何,這都是非常有益的,特別是當生成緩存數據代價很昂貴時(寫線程也許已經做了)。如果更新緩存數據,后續的請求將不在需要等待應用來生成。如果在后臺做失效處理,例如基于TTL的失效,就可以在一個從用戶請求完全分離出來的進程中生成失效數據的新版本 - 3.讀時失效
在更改舊數據時,為了避免要同時失效派生出來的臟數據,可以在緩存中保存一些信息,當從緩存讀數據時可以利用這些信息判斷數據是否已經失效。和顯式失效策略相比,這樣做有很大的優勢:成本固定且可以分散在不同時間內。假設要失效一個有一百萬緩存對象依賴的對象,如果采用寫時失效,需要一次在緩存中失效一百萬個對象,即使有高效的方法來找到這些對象,也可能需要很長的時間才能完成。如果采用讀時失效,寫操作可以立即完成,但后續這一百萬對象的讀操作可能會有略微的延遲。這樣就把失效一百萬對象的開銷分散了,并且可以幫助避免出現負載沖高和延遲增大的峰值。
一種最簡單的讀時失效的辦法時采用對象版本控制。使用這種方法,在緩存中存儲一個對象時,也可以存儲對象所依賴的數據的當前版本號或者時間戳。例如,假設要緩存用戶博客日志的統計信息,包括用戶所發表的博客數。當緩存blog_stats對象時,也可以同一時間存儲用戶的當前版本號,因為該統計信息是依賴于用戶的。
不管什么時候更新依賴于用戶的數據,都需要更新用戶的版本號,假設用戶的版本號初始為0,并且由你來生成和緩存統計信息。當用戶發表了一篇博客,就增加用戶的版本好到1(當然也要同時存儲這篇博客,盡管在這個例子并沒有用到博客數據)。然后當需要顯示統計數據的時候,可以對緩存中blog_stats對象的版本與緩存的用戶版本進行比較。因為用戶的版本比對象的版本高,所以可以直到緩存的統計信息已經過期了,需要重新計算。
這是一個非常粗糙的內容失效方式,因為它假設依賴于用戶的每一個比特的數據與所有其他數據都有交互。但這個假設并不總是成立的。舉個例子,如果一個用戶對一篇博客做了編輯,你也需要增加用戶的版本號,著就會導致存儲的統計信息失效,而實際上統計信息(發表的博客數)并沒真的改變。這個取舍是很簡單的。一個簡單的緩存失效策略不只是更容易創建,也可能更加高效。
對象版本控制是一種簡單的標記緩存方法,它可以處理更復雜的依賴關系。一個標記的緩存可以識別不同類型的依賴,并且分別跟蹤每個依賴的版本。在前面的圖書俱樂部的例子中,你可以通過下面的版本好標記評論,使緩存的評論依賴于用戶的版本和書的版本:user_ver=1234和book_ver=5678.任一版本號變了,都應該刷新緩存的評論
緩存對象分層
分層緩存對象對檢索、失效和內存利用都有幫助。相對于只緩存對象,也可以緩存對象的ID、對象的ID組等通常需要一起檢索的數據。電子商務網站的搜索結果是這種技術很好的例子。一次搜索可能返回一個匹配產品的列表,包括名稱、描述、縮略圖,以及價格。緩存整個列表的效率很低:其他的搜索也可能會包含一些相同的產品,這就會導致數據重復,并且浪費內存。這種策略也使得當一個產品價格變動時,找出并失效搜索結果變得很困難,因為你必須查看每個列表,找到哪些列表包含了更新過的產品。
可以緩存關于搜索的最小信息,而不必緩存整個列表,例如返回結果的數量以及列表中的產品ID。然后可以再單獨緩存每個產品。這樣做可以解決 兩個問題:不會重復存放任何結果數據,也更容易在失效產品的粒度上去失效緩存。缺點則是,相對于一次性獲得整個搜索結果,必須在緩存中檢索多個對象。然而不管怎么說,為搜索結果緩存產品ID的列表都是更有效的做法。先在一個緩存命中返回ID的列表,再使用這些ID去請求緩存獲得產品信息。如果緩存允許在一次調用里返回多個結果,第二次請求就可以返回多個產品(memcached通過mget()調用來支持)
如果使用不當,這種方法可能會導致奇怪的結果。假設使用TTL策略來失效搜索結果,并且當產品變更時顯式地區失效單個產品。現在想象以下,一個產品地描述發生了變化,不再包含搜索中匹配地關鍵字,但是搜索結果地緩存還沒有過期失效,此時用戶就會看到錯誤地搜索結果,因為緩存的搜索結果將會引用這個變化了的產品,即使它不再包含匹配搜索的關鍵字。
對于大多數應用程序來說,這不是問題。如果應用程序不能容忍這種情況,可以使用基于版本的緩存,并在執行搜索時在結果中存儲產品的版本好。當發現搜索結果在緩存中時,可以將當前搜索結果的版本號和搜索結果中的每個產品的版本號做比較。如果發現任何一個產品的版本數據不一致,可以重新搜索并且重新緩存結果。這對理解遠程緩存訪問的花銷是多么昂貴非常重要。雖然緩存很快,也可以避免很多工作,但在LAN環境下網絡往返緩存服務器通常也需要0.3ms左右。我們見過很多案例,復雜的網頁需要一千次左右的緩存訪問來組合頁面結果,這將會耗費3s左右的網絡延時,意味著你的頁面可能慢得不可接受,即使它甚至不需要訪問數據庫!因此,在這種情況下對緩存使用批量獲取調用是非常重要的。對緩存分層,采用小一些的本地緩存,也可能獲得很大的收益
預生成內容
除了在應用程序幾倍緩存位數據,也可以在后臺預先請求一些頁面,并且將結果存為靜態頁面。如果頁面是動態的,也可以預先生成頁面的部分內容,然后使用像服務端包含(SSI)這樣的技術創建最終頁面。這有助于減小生成預生成內容的大小和開銷,否則可能在將不同部分拼裝到最終頁面的時候,由于微小的變化產生大量的重復內容。幾乎可以對任何類型的緩存使用預生成策略,包括memcached.預生成內容有幾個重要的好處。
- 1.應用代碼沒有復雜的命中和未命中處理路徑
- 2.當未命中的處理路徑慢得不可接受時,這種方案可以很好地工作,因為它保證了未命中的情況永遠不會發生。實際上,在任何時候設計任何類型的緩存系統,總是應該考慮未命中的路徑有多曼。如果平均性能提升很大,但是因為要預生成緩存內容,偶爾有一些請求變得非常緩慢,這時可能比不用緩存還糟糕。性能的持續穩定通常跟高性能一樣重要
- 3.預生成可以避免在緩存未命中時異常的雪崩效應
緩存預生成號的內容可能占用大量空間,并且并不總能預生成所有東西。無論是哪種形式的緩存,需要預生成的內容中最重要的部分是哪些最經常被請求,或者生成的成本最高的,所以可以通過404錯誤處理機制來按需生成。預生成的內容有時候也可以從內存文件系統中獲益,因為可以避免磁盤IO