章節
- ① 什麼是Cache? Cache的目標?
- ② Caching住哪些內容?
- ③ 咱們想要的Cache產品
- ④ Cache使用方式
- ⑤ 對於整體系統的提升
- ⑥ 關於Sharding
- ⑦ Cache痛點和關注點
- ⑧ 咱們用的Cache的產品
- ⑨ 咱們的一些實踐
① 什麼是Cache? Cache的目標?
- 在說這個以前咱們先看下典型Web 2.0的一些架構演變(這裏不用」演進」). 從簡單的到複雜的通用架構.
- 首先, 誠然說Cache在互聯網公司裏,是一個好東西. Cache化,能夠顯著地提升應用程序的性能和便於提供應用程序的伸縮性(能夠消除沒必要要請求落到外在的不頻繁改變數據的DataSource上). 那麼Cache化目的很是明顯, 就是有且只有一個: 提升應用程序的性能.
- 再者, Cache化, 以in-memory爲組織形式, 做爲外部的持久化系統的數據的副本(可能數據結構不一樣), 僅僅爲了提升性能. 那麼Cache化的數據應當是短暫停留在Distributed Cache中 — 它們可能(能夠)隨時的消失(即便斷電不保證立馬就有數據-這一點相似CPU的L1/L2 Cache), 那麼應用在用到Cache時候僅當Cache系統可用時候使用不該當徹底依賴於Cache數據 — 就是說在Distributed Cache中個別的Cache實例失效,那麼DataSource(持久化)能夠臨時性完成數據被訪問的工做.
- 最後, 咱們能夠假定若是各類DataSource自有的系統性能很是高, 那麼Cache所能解決的領域就變得很是的少.
② Caching住哪些內容?
- 可以提升系統總體命中率+提升性能的一切數據, 均放入Distributed Cache是很是合適的.
③ 咱們想要的Cache產品
從上面的目標和定位推理看一款Cache產品應當知足如下需求(不只僅有):java
- 極致的性能, 表如今極低的延遲, 甚至從ms到us響應
- 極高的吞吐量, 能夠應對大促/大流量業務場景
- 良好的擴展性, 方便擴容, 具有基本的分佈式特色而不是單機
- 在擴容/縮容的時候, 已有的節點影響(發生遷移)的成本儘量低
- 節點的基本的高可用(或者部署上能夠沒有)
- 基本的監控, 進程級別和實例級別等都有關鍵性的指標
④ Cache使用方式
說到Cache使用方式, 必不可少的會與數據庫(甚至是具有ACID的RDBMS)或者普通存儲系統對比.node
- 簡單的而言. 即便Cache有了持久化, 但市面上的Cache產品(Redis仍是其它)都不具有良好的高可靠的持久化特性(不管是RDB仍是AOF, 仍是AOF+RDB), 持久化的可靠性都不如MySQL. 注:這裏不深刻Redis原理和源碼和OS文件存儲內容.
而使用方式有如下三種:web
懶漢式(讀時觸發)
這是比較多的場景會使用到. 就是先查詢第一數據源裏的數據, 而後把相關的數據寫入Cache. 如下部分代碼:算法
Java (Laziness)
Jedis cache = new Jedis();
String inCache = cache.get("100");
if (null == inCache) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
User user = jdbcTemplate.queryForObject("SELECT UserName, Salary FROM User WHERE ID = 100",
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
return null;
}
});
if (null != user) {
cache.set(String.valueOf(user.getId()), user.toString()); // 能夠異步
}
cache.close();
}
好處和壞處:
- 不太好的是: 大多數的數據可能再也不被高頻度訪問. 若是第一次訪問不命中就有另外多餘的反作用.
- 比較好的是: 保證數據在Cache裏. 適用於大多數的場景.
飢餓式(寫時觸發)
Java (Impatience)
User user = new User();
JdbcTemplate jdbcTemplate = new JdbcTemplate();
int affectedRows = jdbcTemplate.update("UPDATE User SET Phone = ? WHERE ID = 100 LIMIT 1",
new Object[] { 198 });
cache.set(String.valueOf(user.getId()), user.toString()); // 能夠異步
好處和壞處:
- 比較好的是: 這種寫比例不高數據, 能保證數據比較新.
分析下重要的幾條, 關於"懶漢式"和"飢餓式":
- 飢餓式總保持數據較新
- 分別存在了寫失誤/讀失誤
- 單一方式的使用都將使Miss機率增長
以上兩種各有優缺點, 所以咱們將兩種結合一下(追加一個TTL):數據庫
Java (Combo : Laziness && Impatience)
cache.setex(String.valueOf(user.getId()), 300, user.toString()); // TTL, 能夠異步
按期刷新
常見場景, 有以下幾點數據結構
- 週期性的跑數據的任務
- 適合Top-N列表數據
- 適合不要求絕對實時性數據
- …
⑤ 對於整體系統的提升
如下看看命中率如何影響整體系統? 爲了簡化公式計算, 如下作一些假定.架構
-
場景一: 咱們假定, HTTP QPS 有 10,000, 沒有使用Cache(變相地假定Miss100%), RDBMS是讀 3 ms/query , Cache是 1 ms/query. 那麼理想下10,000個Query總耗時: 3 ms/query * 10,000query = 30,000 ms 若是咱們用了以上2者結合的方式 假定是 90% 命中率, 那麼理想下10,000個Query總耗時: 3 ms/query * 1,000query + 1 ms/query * 9,000query = 12,000 ms. 假定是 70% 命中率, 那麼理想下10,000個Query總耗時: 3 ms/query * 3,000 query + 1 ms/query * 7,000query = 16,000 ms.app
-
場景二: 咱們假定, HTTP QPS 有 10,000, 沒有使用Cache(變相地假定Miss100%), RDBMS是 讀:寫 是 8 : 2 . 讀 3 ms/query, 寫 5 ms / query, Cache是 1 ms/query. 那麼理想下10,000個Query總耗時: 3 ms / query * 8,000 query + 5 ms / query * 2000 query = 34,000 ms . 若是咱們用了以上2者結合的方式, 假定新數據寫入後纔有讀的操做, 那麼命中率可能爲100%, 那麼理想下10,000個Query總耗時: 1 ms/query * 8,000query + 5 ms/query * 2000 query = 18,000 ms. 差一些命中率可能爲90%, 那麼理想下10,000個Query總耗時: 1 ms/query * ( 8,000query90%) + 3 ms/query * ( 8,000query10%) + 5 ms/query * 2000 query = 19,600 ms. 再差一些命中率可能爲70%, 那麼理想下10,000個Query總耗時: 1 ms/query * ( 8,000query70%) + 3 ms/query * ( 8,000query30%) + 5 ms/query * 2000 query = 22,800 ms.
能夠看到 22,800ms / 19,600ms = 117%, 那麼有17%的性能損失.運維
如下看看Cache高可用下如何影響整體系統? 爲了簡化公式計算. 咱們假定Cache依然是提升性能使用, 就是說數據源不是Cache層的.異步
- 場景一: 如上Web2.0架構裏, 訪問Cache一層, 和訪問MySQL一層. 在壓力可接受的狀況下.
假定Cache集羣可用性是99%, MySQL可用性是99%. 即便集羣裏掛了一個Cache實例, 那麼整體系統的可用性: ( 1 - (1-99%)(1-99%) ) = 99.99% .
假定Cache集羣可用性是99%, 共有10個實例. MySQL可用性是98%, MySQL能夠承受3個Cache實例帶來的壓力, 即便集羣裏掛了兩個Cache實例, 那麼整體系統的可用性: ( 1 - (1-99%)(1-99%)*(1-98%) ) = 99.9998%
- 場景二: 訪問Cache一層, 但由於某種因素再也不訪問MySQL一層. 那麼整體系統的可用性: ( 1 - 1% - 1% ) = 98%
- 場景三: 不算Web2.0的架構裏, 訪問Cache一層, 和訪問MySQL一層, 和不訪問MySQL一層, 那麼整體系統的可用性是多少呢? — 答案留給讀者 對比場景一和場景二, 在增長正常的系統處理下(就是多幾行代碼), 咱們就能夠提升極大的整體系統的可用性.
- (這裏聲明下: 任何一個系統,不可能有100%的可用性, 包括Google也同樣, 咱們能作的就是多作幾個9的可用性)
⑥ 關於Sharding
算法有如下常見的兩種比較:
- Hashing
- Consistent Hashing (using virtual nodes)
- servers = [‘cache-server1.yuozan.com:6379’, ‘cache-server2.youzan.com:6379’];
- serverindex = hash(key) % servers.length; server = servers[serverindex;
算法描述
- 第一種Hashing方式, 一旦須要擴容一個或者下線一個, 那麼會致使大量的keys重分配: = ( oldnodecount/newnodecount ), 就是說3臺server擴充到4臺server時候, 3/4 = 75%的keys都受到影響.
- 第二種Consistent Hashing方式, 一旦須要擴容一個或者下線一個, 那麼僅有將近( 1 - (oldnodecount/newnodecount) ) 比例的keys受到影響, 就是說3臺server擴充到4臺server時候, (1 - 3/4) = 25%的keys都受到影響. 這樣相比上一種受到的影響下降了50%. 這將是更好的方式.
Consistent Hashing簡化算法流程的描述:
- 將keys和servers都進行當作一個ring(常被稱爲 continuum)
- 將keys和servers的hash值分隔成多個的slots
- 將servers的virtual nodes按照順時針順序分別映射到slots上
- 將key進行hash按照順時針順序查找最近的一個virtual node
⑦ Cache痛點和關注點
公司之前業務剛起來, 用的Redis看成Cache, 你們知道Redis是單機版本-沒有Sharding. 因爲業務起來, 單機版本對於某個業務來講, 一旦擴容或是掛了那個業務的全部流量都掛了, 當時只作到了垂直分片(Vertical Partition), 而爲了快速解決這一問題, 咱們必須引入DistributedCache, 但願它簡單的好(由於咱們只用來作Cache), 甚至目標都不想讓Redis作持久化數據.
⑧ 咱們用的Cache的產品
2015年爲了業務技術改造, 並能快速的上線. 咱們調查了Twemproxy Codis. 考慮到咱們技術投入. 同時對Codis作了相應的測試, 最終使用Codis做爲Cache的產品來使用. (性能能夠看看Codis官方的對比) 另外咱們結合本身PHP的業務須要, 作了PHP和本地部署Proxy的方式來基準測試.
Codis提供的擴容時的遷移採用了向新老的Server雙寫的模式, 在遷移數據到達了100%的量時候會有必定的極短的鎖時間(這有優點也有劣勢), 咱們和Redis官方同樣不建議開啓AOF.
從目前一年多的使用和運維經驗看, Codis已經知足咱們當下的業務需求. 對於雙11等相似的大促峯值, 咱們能夠看到Codis單純看成Cache來使用的可靠性是比MySQL高的, 也就是說: 若是假定在高峯值下, 即使是Cache會掛了, 並將流量打到了MySQL集羣上, 那麼對於外網的業務而言系統同樣是不可用的. 那麼只要保證不出現Cache整個集羣掛了-只要保證一兩個實例(極點比例)掛了, 那麼流量分散到MySQL集羣上後大促業務依舊保持可用.
⑨ 咱們的一些實踐
- 着重在Codis-Server上的Redis配置, 在運維上儘量提升Server一側的性能: 綁定單核至每一個Redis進程 + 去除持久化(目標: 無Slave節點) + 每一個Server進程實例的內存大小盡量的小(控制在2.5GiB之內)
- 針對PHP-FPM模式下, 用Codis-Proxy當作PHP的LocalAgent直接部署同機上: 提升穩定性 + 下降延遲(Latency)
- 咱們針對Namespace多業務共用集羣問題: 按照約定取不用的業務/應用名稱. 另外共用集羣在sharding狀況下帶來的實例級別的複用帶來的命中率變化和sharding均衡性變化, 感興趣的讀者能夠自行計算下
原文:
http://tech.youzan.com/cache-background/?utm_source=tuicool&utm_medium=referral
章節
- ① 什麼是Cache? Cache的目標?
- ② Caching住哪些內容?
- ③ 咱們想要的Cache產品
- ④ Cache使用方式
- ⑤ 對於整體系統的提升
- ⑥ 關於Sharding
- ⑦ Cache痛點和關注點
- ⑧ 咱們用的Cache的產品
- ⑨ 咱們的一些實踐
① 什麼是Cache? Cache的目標?
- 在說這個以前咱們先看下典型Web 2.0的一些架構演變(這裏不用」演進」). 從簡單的到複雜的通用架構.
- 首先, 誠然說Cache在互聯網公司裏,是一個好東西. Cache化,能夠顯著地提升應用程序的性能和便於提供應用程序的伸縮性(能夠消除沒必要要請求落到外在的不頻繁改變數據的DataSource上). 那麼Cache化目的很是明顯, 就是有且只有一個: 提升應用程序的性能.
- 再者, Cache化, 以in-memory爲組織形式, 做爲外部的持久化系統的數據的副本(可能數據結構不一樣), 僅僅爲了提升性能. 那麼Cache化的數據應當是短暫停留在Distributed Cache中 — 它們可能(能夠)隨時的消失(即便斷電不保證立馬就有數據-這一點相似CPU的L1/L2 Cache), 那麼應用在用到Cache時候僅當Cache系統可用時候使用不該當徹底依賴於Cache數據 — 就是說在Distributed Cache中個別的Cache實例失效,那麼DataSource(持久化)能夠臨時性完成數據被訪問的工做.
- 最後, 咱們能夠假定若是各類DataSource自有的系統性能很是高, 那麼Cache所能解決的領域就變得很是的少.
② Caching住哪些內容?
- 可以提升系統總體命中率+提升性能的一切數據, 均放入Distributed Cache是很是合適的.
③ 咱們想要的Cache產品
從上面的目標和定位推理看一款Cache產品應當知足如下需求(不只僅有):
- 極致的性能, 表如今極低的延遲, 甚至從ms到us響應
- 極高的吞吐量, 能夠應對大促/大流量業務場景
- 良好的擴展性, 方便擴容, 具有基本的分佈式特色而不是單機
- 在擴容/縮容的時候, 已有的節點影響(發生遷移)的成本儘量低
- 節點的基本的高可用(或者部署上能夠沒有)
- 基本的監控, 進程級別和實例級別等都有關鍵性的指標
④ Cache使用方式
說到Cache使用方式, 必不可少的會與數據庫(甚至是具有ACID的RDBMS)或者普通存儲系統對比.
- 簡單的而言. 即便Cache有了持久化, 但市面上的Cache產品(Redis仍是其它)都不具有良好的高可靠的持久化特性(不管是RDB仍是AOF, 仍是AOF+RDB), 持久化的可靠性都不如MySQL. 注:這裏不深刻Redis原理和源碼和OS文件存儲內容.
而使用方式有如下三種:
懶漢式(讀時觸發)
這是比較多的場景會使用到. 就是先查詢第一數據源裏的數據, 而後把相關的數據寫入Cache. 如下部分代碼:
Java (Laziness)
Jedis cache = new Jedis();
String inCache = cache.get("100");
if (null == inCache) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
User user = jdbcTemplate.queryForObject("SELECT UserName, Salary FROM User WHERE ID = 100",
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
return null;
}
});
if (null != user) {
cache.set(String.valueOf(user.getId()), user.toString()); // 能夠異步
}
cache.close();
}
好處和壞處:
- 不太好的是: 大多數的數據可能再也不被高頻度訪問. 若是第一次訪問不命中就有另外多餘的反作用.
- 比較好的是: 保證數據在Cache裏. 適用於大多數的場景.
飢餓式(寫時觸發)
Java (Impatience)
User user = new User();
JdbcTemplate jdbcTemplate = new JdbcTemplate();
int affectedRows = jdbcTemplate.update("UPDATE User SET Phone = ? WHERE ID = 100 LIMIT 1",
new Object[] { 198 });
cache.set(String.valueOf(user.getId()), user.toString()); // 能夠異步
好處和壞處:
- 比較好的是: 這種寫比例不高數據, 能保證數據比較新.
分析下重要的幾條, 關於"懶漢式"和"飢餓式":
- 飢餓式總保持數據較新
- 分別存在了寫失誤/讀失誤
- 單一方式的使用都將使Miss機率增長
以上兩種各有優缺點, 所以咱們將兩種結合一下(追加一個TTL):
Java (Combo : Laziness && Impatience)
cache.setex(String.valueOf(user.getId()), 300, user.toString()); // TTL, 能夠異步
按期刷新
常見場景, 有以下幾點
- 週期性的跑數據的任務
- 適合Top-N列表數據
- 適合不要求絕對實時性數據
- …
⑤ 對於整體系統的提升
如下看看命中率如何影響整體系統? 爲了簡化公式計算, 如下作一些假定.
-
場景一: 咱們假定, HTTP QPS 有 10,000, 沒有使用Cache(變相地假定Miss100%), RDBMS是讀 3 ms/query , Cache是 1 ms/query. 那麼理想下10,000個Query總耗時: 3 ms/query * 10,000query = 30,000 ms 若是咱們用了以上2者結合的方式 假定是 90% 命中率, 那麼理想下10,000個Query總耗時: 3 ms/query * 1,000query + 1 ms/query * 9,000query = 12,000 ms. 假定是 70% 命中率, 那麼理想下10,000個Query總耗時: 3 ms/query * 3,000 query + 1 ms/query * 7,000query = 16,000 ms.
-
場景二: 咱們假定, HTTP QPS 有 10,000, 沒有使用Cache(變相地假定Miss100%), RDBMS是 讀:寫 是 8 : 2 . 讀 3 ms/query, 寫 5 ms / query, Cache是 1 ms/query. 那麼理想下10,000個Query總耗時: 3 ms / query * 8,000 query + 5 ms / query * 2000 query = 34,000 ms . 若是咱們用了以上2者結合的方式, 假定新數據寫入後纔有讀的操做, 那麼命中率可能爲100%, 那麼理想下10,000個Query總耗時: 1 ms/query * 8,000query + 5 ms/query * 2000 query = 18,000 ms. 差一些命中率可能爲90%, 那麼理想下10,000個Query總耗時: 1 ms/query * ( 8,000query90%) + 3 ms/query * ( 8,000query10%) + 5 ms/query * 2000 query = 19,600 ms. 再差一些命中率可能爲70%, 那麼理想下10,000個Query總耗時: 1 ms/query * ( 8,000query70%) + 3 ms/query * ( 8,000query30%) + 5 ms/query * 2000 query = 22,800 ms.
能夠看到 22,800ms / 19,600ms = 117%, 那麼有17%的性能損失.
如下看看Cache高可用下如何影響整體系統? 爲了簡化公式計算. 咱們假定Cache依然是提升性能使用, 就是說數據源不是Cache層的.
- 場景一: 如上Web2.0架構裏, 訪問Cache一層, 和訪問MySQL一層. 在壓力可接受的狀況下.
假定Cache集羣可用性是99%, MySQL可用性是99%. 即便集羣裏掛了一個Cache實例, 那麼整體系統的可用性: ( 1 - (1-99%)(1-99%) ) = 99.99% .
假定Cache集羣可用性是99%, 共有10個實例. MySQL可用性是98%, MySQL能夠承受3個Cache實例帶來的壓力, 即便集羣裏掛了兩個Cache實例, 那麼整體系統的可用性: ( 1 - (1-99%)(1-99%)*(1-98%) ) = 99.9998%
- 場景二: 訪問Cache一層, 但由於某種因素再也不訪問MySQL一層. 那麼整體系統的可用性: ( 1 - 1% - 1% ) = 98%
- 場景三: 不算Web2.0的架構裏, 訪問Cache一層, 和訪問MySQL一層, 和不訪問MySQL一層, 那麼整體系統的可用性是多少呢? — 答案留給讀者 對比場景一和場景二, 在增長正常的系統處理下(就是多幾行代碼), 咱們就能夠提升極大的整體系統的可用性.
- (這裏聲明下: 任何一個系統,不可能有100%的可用性, 包括Google也同樣, 咱們能作的就是多作幾個9的可用性)
⑥ 關於Sharding
算法有如下常見的兩種比較:
- Hashing
- Consistent Hashing (using virtual nodes)
- servers = [‘cache-server1.yuozan.com:6379’, ‘cache-server2.youzan.com:6379’];
- serverindex = hash(key) % servers.length; server = servers[serverindex;
算法描述
- 第一種Hashing方式, 一旦須要擴容一個或者下線一個, 那麼會致使大量的keys重分配: = ( oldnodecount/newnodecount ), 就是說3臺server擴充到4臺server時候, 3/4 = 75%的keys都受到影響.
- 第二種Consistent Hashing方式, 一旦須要擴容一個或者下線一個, 那麼僅有將近( 1 - (oldnodecount/newnodecount) ) 比例的keys受到影響, 就是說3臺server擴充到4臺server時候, (1 - 3/4) = 25%的keys都受到影響. 這樣相比上一種受到的影響下降了50%. 這將是更好的方式.
Consistent Hashing簡化算法流程的描述:
- 將keys和servers都進行當作一個ring(常被稱爲 continuum)
- 將keys和servers的hash值分隔成多個的slots
- 將servers的virtual nodes按照順時針順序分別映射到slots上
- 將key進行hash按照順時針順序查找最近的一個virtual node
⑦ Cache痛點和關注點
公司之前業務剛起來, 用的Redis看成Cache, 你們知道Redis是單機版本-沒有Sharding. 因爲業務起來, 單機版本對於某個業務來講, 一旦擴容或是掛了那個業務的全部流量都掛了, 當時只作到了垂直分片(Vertical Partition), 而爲了快速解決這一問題, 咱們必須引入DistributedCache, 但願它簡單的好(由於咱們只用來作Cache), 甚至目標都不想讓Redis作持久化數據.
⑧ 咱們用的Cache的產品
2015年爲了業務技術改造, 並能快速的上線. 咱們調查了Twemproxy Codis. 考慮到咱們技術投入. 同時對Codis作了相應的測試, 最終使用Codis做爲Cache的產品來使用. (性能能夠看看Codis官方的對比) 另外咱們結合本身PHP的業務須要, 作了PHP和本地部署Proxy的方式來基準測試.
Codis提供的擴容時的遷移採用了向新老的Server雙寫的模式, 在遷移數據到達了100%的量時候會有必定的極短的鎖時間(這有優點也有劣勢), 咱們和Redis官方同樣不建議開啓AOF.
從目前一年多的使用和運維經驗看, Codis已經知足咱們當下的業務需求. 對於雙11等相似的大促峯值, 咱們能夠看到Codis單純看成Cache來使用的可靠性是比MySQL高的, 也就是說: 若是假定在高峯值下, 即使是Cache會掛了, 並將流量打到了MySQL集羣上, 那麼對於外網的業務而言系統同樣是不可用的. 那麼只要保證不出現Cache整個集羣掛了-只要保證一兩個實例(極點比例)掛了, 那麼流量分散到MySQL集羣上後大促業務依舊保持可用.
⑨ 咱們的一些實踐
- 着重在Codis-Server上的Redis配置, 在運維上儘量提升Server一側的性能: 綁定單核至每一個Redis進程 + 去除持久化(目標: 無Slave節點) + 每一個Server進程實例的內存大小盡量的小(控制在2.5GiB之內)
- 針對PHP-FPM模式下, 用Codis-Proxy當作PHP的LocalAgent直接部署同機上: 提升穩定性 + 下降延遲(Latency)
- 咱們針對Namespace多業務共用集羣問題: 按照約定取不用的業務/應用名稱. 另外共用集羣在sharding狀況下帶來的實例級別的複用帶來的命中率變化和sharding均衡性變化, 感興趣的讀者能夠自行計算下