緣起:來自於我在近期一個項目上遇到的問題,在Segmentfault上發表了提問redis
知識背景:mongodb
對不是很熟悉MongoDB和Redis的同窗作一下介紹。數據庫
1.MongoDB數組查詢:MongoDB自帶List,能夠存放相似這樣的結構 List = [1, 2, 3, 4, 5, 6, 7, 8, 9].數組
若是咱們有一個 l = [2, 3, 8], 則能夠進行這樣的查詢:spce = { 'List' : { '$in' : l }, 這裏spce就是一個查詢條件,表明 l 是 List的一個子集。緩存
2.Redis隊列: Redis提供基本的List(普通鏈表),set(集合),Zset(有序集合) 類型的結構,將List的 lpush, rpop操做運用起來,能夠作一個普通的隊列,運用Zset 能夠作一個帶權值的最小堆排序的隊列(能夠看作優先級)。服務器
總體架構以下圖所示:架構
生產者產生任務,經過LVS與RPC服務器將任務記錄到MongoDB,消費者一樣經過RPC服務獲取任務,這是個很簡單的架構,通常服務可能去掉集羣都是這樣的。併發
整個業務架構須要一個前提,任務不能丟失,也就是說任務即便失敗也須要從新加入到隊列,至少若干次後任然失敗也要知道爲何失敗(非記錄日誌形式)。異步
不少人問爲何不直接用RabbitMQ或者Redis,由於這類消息隊列沒法作到管理任務超時等狀況,由於業務須要,也須要作一些簡單的查詢,這類隊列是不支持某些稍複雜的查詢的,並且一開始咱們的任務量估計在5KW/Day這樣,擔憂Redis扛不住,後來我發現這是個錯誤的假設。高併發
問題內容以下:
問題背景: 近期在重構公司內部一個重要的任務系統,因爲原來的任務系統使用了MongoDB來保存任務,客戶端從MongoDB來取,至於爲何用MongoDB,是一個歷史問題,也是由於若是使用到MongoDB的數組查詢能夠減小任務數量不少次,假設這樣的狀況,一個md5(看作一條記錄的惟一標識)須要針對N種狀況作任務處理,若是用到MongoDB的數組,只須要將一個md5做爲一條任務,其中包含一個長度爲N的待處理任務列表,可使用到MongoDB的數組(只有N個子任務都處理完後整個任務纔算處理完畢),這樣整個任務系統的數量級就變爲原來的 1/N(若是須要用到普通的關係型數據庫,可能須要建立 m*n 個任務,這樣算下來咱們的任務數量將可能達到一個很大的值,主要是由於處理任務的進程因爲某些不肯定因素沒法控制,因此比較慢)
細節描述: 1.當MongoDB的任務數量增多的時候,數組查詢至關的慢(已經作索引),任務數達到5K就已經不能容忍了,和咱們天天的任務數不在一個數量級。
2.任務處理每一個md5對應的N個子任務必需要所有完成才從MongoDB中刪除
3.任務有相應的優先級(保證高優先級優先處理),任務在超時後能夠重置。
改進方案以下: 因爲原有代碼的耦合,不能徹底拋棄MongoDB,因此決定加一個Redis緩存。一個md5對應的N個子任務分發到N個Redis隊列中(拆分子任務)。一個單獨的進程從MongoDB中向Redis中將任務同步,客戶端再也不從MongoDB取任務。這樣作的好處是拋棄了原有的MongoDB的數組查詢,同步進程從MongoDB中取任務是按照任務的優先級偏移(已作索引)來取,因此速度比數組查詢要快。這樣客戶端向Redis的N個隊列中取子任務,把任務結果返回原來的MongoDB任務記錄中(根據md5返回子任務)。
改進過程遇到的問題: 因爲任務處理端向MongoDB返回時候會有一個update操做,若是N個子任務都完成,就將任務從MongoDB中刪除。這樣的一個問題就是,通過測試後發現MongoDB在高併發寫的狀況下性能很低下,整個任務系統任務處理速度最大爲200/s(16核, 16G, CentOS, 內核2.6.32-358.6.3.el6.x86_64),緣由大體爲在頻繁寫狀況下,MongoDB的性能會因爲鎖表操做急劇降低(鎖表時間能夠達到60%-70%,熟悉MongoDB的人都知道這是多麼恐怖的數字)。
具體問題: (Think out of the Box)可否提出一個好的解決方案,可以保存任務狀態(子任務狀態),速度至少超過MongoDB的?
提出這個問題後,很感謝官方將問題發到微博首頁,有一個回答我以爲能夠採納:
初步的思考了一下,僅供參考:
首先,提一下索引,相信這個你應該加了索引。
有個問題確認一下,mongodb最新版本中的鎖粒度仍是Database級別吧,不知道你用的哪一個版本,還沒到鎖表(Collection)這個粒度,因此寫併發大的狀況下比較糟糕,不過應該性能也不至於糟到像你描述的那樣啊?不解,建議考慮任務分庫的可能性?
可否考慮把子任務的狀態和主任務的狀態分開保存。子任務的狀態,能夠放到redis,主任務只負責本身自己的狀態,這樣每一個主任務更新頻率降爲1/N,可大大減小mongodb中主任務表的壓力。
子任務完成或超時後,能否考慮後臺異步單線程順序同步mongodb的主任務狀態?
上面這個Answer能夠考慮,可是在作同步過程當中發現不少問題。
在開發過程當中發現,由單一進程從MongoDB向Redis同步數據,能夠採起兩種可參考的方案:
1.模擬MongoDB replication機制,一個進程模擬slave向master請求oplog,而後本身解析數據格式存放到Redis.
2.一個進程從MongoDB中按照優先級取數據而後同步到Redis.
兩種參考方案各有優劣,我最終選擇了第二種。
第一種方案
優勢:
1.主MongoDB查詢壓力變小
2.之後業務擴展很方便(能夠運用到查詢緩存啊,讀寫分離什麼的)
缺點:
1.可參考文檔較少,須要模擬MongoDB replication的機制較爲複雜
2.同步實時性沒法估計確切時間
第二種方案:
優勢:
1.編碼相對簡單,按照優先級作索引後查詢不影響原有邏輯
2.開發較爲靈活(彷佛和第一點是同樣的)
缺點:
1.(項目完成後測試不理想,具體緣由會作說明)
2.同步進程單點,若是進程卡死或者機器崩潰會形成系統卡死
方案肯定:由單一進程從MongoDB同步任務到Redis.
架構變遷到這樣:
加上Redis,作到MongoDB的讀寫分離,單一進程從MongoDB及時把任務同步到Redis中。
看起來很完美,可是上線後出現了各類各樣的問題,列舉一下:
1.Redis隊列長度爲多少合適?
2.同步進程根據優先級從MongoDB向Redis同步過程當中,一次取多少任務合適?太大致使不少無謂的開銷,過小又會頻繁操做MongoDB
3.當某一個子任務處理較慢的時候,會致使MongoDB的前面優先級較高的任務沒有結束,而優先級較低的確得不處處理,形成消費者空閒
最終方案:
在生產者產生一個任務的同時,向Redis同步任務,Redis sort set(有序集合,保證優先級順序不變),消費者經過RPC調用時候,RPC服務器從Redis中取出任務,而後結束任務後從MongoDB中刪除。
測試結果,Redis插入效率。Redis-benchmark 併發150,32byte一個任務,一共100W個,插入效率7.3W(不使用持久化)
在這以前咱們的擔憂都是不必的,Redis的性能很是的好。
目前此套系統能夠勝任天天5KW量的任務,我相信能夠更多。後面有文章可能會講到Redis的事務操做