[原創]分佈式系統之緩存的微觀應用經驗談(一) 【設計基礎細節篇】

分佈式系統之緩存的微觀應用經驗談(一) 【基礎細節篇】html

 

前言 數據庫

 

  近幾個月一直在忙些雜事,幾乎年後都沒怎麼閒過。忙忙碌碌中就進入了2018年的秋天了,不得不感嘆時間老是如白駒過隙,也不知道收穫了什麼和失去了什麼。最近稍微休息,買了兩本與技術無關的書,其一是Yann Martel 寫的《The High Mountains of Portugal》(葡萄牙的高山),發現閱讀此書是須要一些耐心的,對人生暗喻很深,也有足夠的留白,有興趣的朋友能夠細品下。好了,下面迴歸正題,嘗試寫寫工做中緩存技術相關的一些實戰經驗和思考。
緩存


正文服務器

 

  在分佈式Web程序設計中,解決高併發以及內部解耦的關鍵技術離不開緩存和隊列,而緩存角色相似計算機硬件中CPU的各級緩存。現在的業務規模稍大的互聯網項目,即便在最初beta版的開發上,都會進行預留設計。可是在諸多應用場景裏,也帶來了某些高成本的技術問題,須要細緻權衡。本系列主要圍繞分佈式系統中服務端緩存相關技術,也會結合朋友間的探討說起本身的思考細節。文中如有不妥之處,懇請指正。

  第一篇這裏嘗試儘量詳細的談談緩存自身的基礎設計應用,以及相關的操做細節等(具體應用主要以Redis 舉例)。網絡

 

  1、稍微說明基本特色和技術成本(本文主要指服務端數據緩存)
數據結構


    1.1 一種區分架構

 

      緩存基於不一樣的條件有不少種劃分方式,本地緩存(Local cache)和分佈式緩存(Distributed cache)是一種常見分類,二者自身又包含不少細類。併發

      本地並非指程序所在本地服務器(從嚴格概念來講),而是更細粒度的指位於程序自身的內部存儲空間,而分佈式更多強調的是存儲在進程以外的一個或者多個服務器上,彼此交互通訊,在具體軟件項目的設計和應用中,多數時候是混合一體。運維

      (固然,我的認爲對緩存本質的理解纔是最重要的,至於概念上的分類只是一個不一樣理解下的劃分而已)dom

 

    1.2 一些技術成本


      在具體項目架構設計時,單純使用前者(本地緩存)的開發成本毋庸置疑是極低的,主要考慮的是本機的內存負載或者極少許的磁盤I/O影響。然後者的設計初心是爲了利於分佈式程序之間緩存數據的高效共享和管理,除了考慮緩存所在服務器自身的內存負載,設計時更須要充分考慮網絡I/O、CPU的負載,以及某些場景下的磁盤I/O的代價,同時還在具體設計時儘量規避和權衡總體穩定性和效率,這些不只僅只是做爲緩存服務器的硬件成本和技術維護。 須要謹慎考慮的底層問題包括緩存間通訊、網絡負載和延遲等各類須要權衡的細節。

      其實若是理解了緩存本質就該知道,任何存儲介質在適當的場景下均可以充當一個高效的緩存角色並進行項目集成和緩存間集羣。常見主流的Memcached和Redis等均是屬於後者範疇,甚至能夠包括如基於NoSql設計的MongoDB這類文檔數據庫(但這是從角色角度講,而狹義劃分上這是基於磁盤的存儲庫,須要注意,各有專攻)。這些第三方緩存在進行項目集成和緩存間集羣,也須要解決一些問題。甚至項目迭代到了後期階段,每每還須要具有較高專業知識的運維同時參與,而且在開發中的邏輯設計和代碼實現也會增長必定的工做量。因此有時候在具體項目的設計上,一方面要儘量預留,一方面還得根據實際狀況儘量精簡。

 

      額外說下其餘體會:在我的有限的技術學習和實踐裏,關於節點數據交互,尤爲是服務間通訊,是不存在完美的閉環的,理論上也都是在「當前階段」面向「高一致」的權衡罷了(大概跟生活是同樣的吧,呵,寫偏了)。

 

  2、緩存數據庫結構的一些設計細節

  

    (因爲目前我的工做中大多數狀況應用的是Redis 3.x,如下如有特性關聯,均是以此做爲參照說明。)


    2.1 實例(Instance)

 

      根據業務場景,公共數據和業務耦合數據,必定分別使用不一樣的實例。若是是單實例,才能夠考慮以DB劃分。當你使用的是Redis,那麼DB在Redis裏是有數據隔離,但沒有嚴格權限限制,因此劃庫只是一種選擇。在Cluster集羣裏則是保持默認單個庫,不過實際中我會嘗試根據項目大小來調整,至於在哪一個開發階段則是做爲預留設計。

      額外須要注意的是,做爲重度依賴服務器內存的緩存產品,若是開啓了持久化(後面會提到),而且在爲併發量極大的服務提供支持時,服務器硬件資源會出現大量搶佔,請結合持久策略配置,考慮實例是否進行分盤存儲。持久化本質是將內存數據同步寫入硬盤(刷盤),而磁盤I/O實在有限,被迫的寫入阻塞除了形成線程阻塞和服務超時,還會致使額外異常甚至波及其餘底層依賴服務。固然,個人建議是,若是條件容許,最好是在項目初期設計時就進行規劃並肯定。

 

    2.2 緩存「表」(Table)

 

      通常緩存中並無傳統RDBMS中直觀的表概念(每每以鍵值對「KV」形式存在),但從結構上來說,鍵值對自己就能夠組裝爲各類表結構。通常我會先生成數據庫表關係圖,而後分析何時存儲字符串,何時存儲對象,而後使用緩存鍵(KEY)進行表和字段(列)分割。

      假定須要存儲一個登陸服務器表數據,包含字段(列):name、sign、addr,那麼能夠考慮將數據結構拆分爲如下形式:
        { key : "server:name" , value : "xxxx" }
        { key : "server:sign" , value : "yyyy" }
        { key : "server:addr" , value : "zzzz" }

      須要注意的是,每每在分佈式緩存產品中,例如Redis,存在多種數據結構(如String、Hash等),還須要根據數據關聯性和列的數量,來選擇對應緩存的存儲數據結構,相關存儲空間和時間複雜度是徹底不一樣的,而這個在初期階段是很難感覺到的。

      同時,就算緩存的內存設置的足夠大,剩餘也不少,也一樣須要考慮相似RDBMS中的單表容量問題,控制條目數量不能無限增加(好比預知到存儲條目能夠輕鬆達到百萬級),「分庫分表」的設計思路都是相通的。

 

    2.3 緩存鍵(Key)

 

      上面提到了基於緩存鍵來設計表,這裏再單獨說明一下鍵相關的我的規範。在鍵長度足夠簡短的前提下,若是關聯相同業務模塊,則必須設計爲以同一個標識(代號)開頭,目的是方便查找和統計管理。
如用戶登陸服務器列表:
        { key : "ul:server:a" , value : "xxxx" }
        { key : "ul:server:b" , value : "yyyy" }

      另外,每一個獨立業務系統可考慮配置一個惟一的通用前綴標識,固然,這裏不是必需,若實際工做中,若是使用的是不一樣庫,則能夠忽略。

 

    2.4 緩存值(value)

 

      緩存中的值(這裏指單一條目)的大小沒有平均標準,但Size天然是越小越好(若使用的是Redis,一次操做的value較大會直接影響整個Redis的響應時間,不只僅是指網絡I/O)。若是存儲佔用空間直達10M+,建議考慮關聯的業務場景是否能夠拆分爲熱點和非熱點數據。

 

    2.5 持久化(Permanence)

 

      上面也簡單提了下,通常來講,持久和緩存自己是沒有直接關係的,能夠粗略想象爲一個面向硬盤一個面向內存。但現在的Web項目裏,有些業務場景是高度依賴緩存的,持久化能夠一方面幫助提升緩存服務重啓後的快速恢復,一方面提供特定場景下的存儲特性。固然,因爲持久化必然須要犧牲一些性能,包括CPU的搶佔和硬盤I/O影響。不過大多數時候是利大於弊,建議在應用緩存的時候,沒有特別狀況的話,儘可能搭配持久化,不管是使用自身機制仍是第三方來實現。

      若是是使用的Redis,其自身就具有相關持久策略,包含AOF和RDB,我在大多數狀況下是二者同時配置的(固然,最新官方版本自己也提供了混合模式)。若是在一些非高併發的場景下,或者說在一些中小項目的管理模塊裏,僅僅只是做爲優化手段,肯定了不需持久,也能夠直接設置關閉,節約性能開銷損耗,但建議在程序中將該實例作好標註,確保該實例的公共使用範圍。

 

    2.6 淘汰(Eliminate)

 

      緩存若是無限制的增加,即便設置了較短的過時(Expiration ),在一些時間點上,高併發的一批大數據會在較短期內就達到了可以使用內存的峯頂,此時程序中與緩存服務器的交互會出現大量延遲和錯誤,甚至給服務器自身都帶來了嚴重的不穩定性。因此在生產環境裏儘可能給緩存配置最大內存限制,以及適當的淘汰策略。

      若是使用的是Redis,自身淘汰策略選擇比較靈活。我的的設計是,在數據呈現相似冪律分佈狀況下,總有大量數據訪問較低,我會選擇配置allkeys-lru、volatile-lru,將最少訪問的數據進行淘汰。再好比緩存是做爲日誌應用的,那麼我通常是項目前期是配置no-enviction,後期會配置爲volatile-ttl。固然,我也見過一種特殊業務下的設計,緩存直接用來做爲輕量的持久數據庫使用,並且是終端,開始以爲有些新奇,後來發現是很是符合業務設計的(好比幾乎沒有任何複雜邏輯和強事務)。因此合情合理,確實不該該禁錮在傳統設計裏,畢竟架構老是基於業務去實時組合和改變的。

 


  3、緩存的基礎CURD和其餘相關(在這裏我主要討論一級緩存)

 

    3.1 新增(Create)

 

      若是沒有特殊業務需求(如上面提到的),插入必須設置過時時間。同時,儘可能保證過時隨機性。若是是進行批量緩存,則我的的作法是保證設置的過時時間上至少是分散的,目的是爲了下降緩存雪崩等風險和影響(關於這些我會在之後的擴展篇裏嘗試闡述)。

      如,批量緩存的對象是一個結果集,條目有10萬條,緩存時間基礎爲 60*60*2(sec),如今須要同時進行緩存。個人作法是默認生成一個隨機數,如random(範圍 0 - 1000),過時時間則設置爲( 60*60*2 + random ) 。

 

    3.2 修改(Update)

 

      更新一條緩存的數據,注意是否須要從新調整過時時間。同時在不少場合,如多個緩存間同步時,建議直接刪除該緩存,而不是更新緩存。修改操做不少時候是關聯到DB間的同步操做的,相對考究的多一些,須要權衡分佈式事務上的問題,後續文章裏會寫到。

 

    3.3 讀取(Read)

 

      查找緩存時,若是存在多條,並肯定數據量不大,務必使用嚴格匹配key的模式,而儘可能不要使用通配符方式。雖然發送指令的key數據變長了,但卻避免了沒必要要的緩存內的搜索性能損耗。
      例如單純相信Redis裏自身的存儲優化,無限制的使用 keys pattern而不考慮時間複雜度,同時形成大量線程阻塞(這裏與主從複製無關)。若是折中使用scan分頁替代,也並不是一種「無憂」的實現,一是須要在程序代碼的封裝裏設置較低的容量,二是請務必在程序邏輯裏對數據幻讀等潛在問題作相關的管控處理。

      另外能夠額外類比一種場景,操做DB中的大表,命中的熱點數據分佈靠後。

 

    3.4 刪除 / 清空(Delete / Clear)

 

      刪除緩存,通常有直接移除和設置時間過時(並非任什麼時候候都是滑動增長過時)兩種方式,沒什麼細節上的說明。(卻是聽過一種特殊業務場合,批量請求同類數據,而且即時性沒有很高要求,設置過時時間並將時間稍做分散。)

      清空緩存,我在項目裏目前並未應用,甚至也不提倡直接使用。可是假如在應用時,須要慎重考慮兩個地方。一是清理時機,二是清理時效(若在Redis裏,不管是flushdb或者flushall,都會造成必定阻塞)


    3.5 鎖/信號(Locking)

 

      自己無關緩存,屬於一些併發特性實現,有必定的適用場景。這在Redis中有一些基於原子的實現,但與本系列討論無關。本人去年寫過一篇與之相關的分享,詳見:商城系統下單庫存管控系列雜記(二)http://www.javashuo.com/article/p-altodoee-n.html),但這裏不贅述。

 

    3.6 發佈-訂閱(Publish-Subscribe)

 

      爲何提到這個跟生產消費(Produce-Consume)相關的動做呢?這個機制自己是不屬於緩存自身的範疇的,而是更相關於消息隊列(Message Queue)。之因此提到,是由於現在主流的緩存產品都自帶這一特性,不少場景使用起來較方便,配置也簡單,效率也夠快。只是,每每會形成濫用。最關鍵是沒必要要的強耦合也下降了總體靈活性和性能,擴展性也實在有限。固然,這是我目前的見解。

      個人建議是:若是沒有特殊的場景應用,儘可能不使用。至少本人是不會優先推薦使用緩存自身的發佈訂閱的,甚至在緩存集羣系統中,須要考究的細節更多。而推薦的方式是,使用其餘專業中間件解決,如基於MQ的產品替代方案。具體的候選有優秀的開源做品如RabbitMQ、Kafka等,包括有朋友提到的近兩年國內阿里研發的RocketMQ等等,可是我的目前使用較多的依然是RabbitMQ。固然,這裏不去過多贅述了,根據場景選擇,合適的場景選用最合適的技術方案便可吧。

 

 

結語

 

  本篇先寫到這裏,下一篇會圍繞相關主題嘗試擴展闡述。

  PS:因爲我的能力和經驗均有限,本身也在持續學習和實踐,文中如有不妥之處,懇請指正。

 

【已更新:分佈式系統之緩存的微觀應用經驗談(二) 【主從和主備高可用篇】 http://www.javashuo.com/article/p-mxxagmfd-eu.html

 

 

End.

相關文章
相關標籤/搜索