現象:
同事負責的項目轉到我部門,整理服務過程中發現了隊列的積壓問題。
為了搞清楚積壓的嚴重程度, 對隊列任務數每分鍾進行一次采樣,生成一個走勢圖, 隊列積壓情況一目了然,非常嚴重。
分析:
聽了同事對系統的介紹,猜測是mongo性能影響了處理效率,於是針對mongo進行分析
1. 使用mongotop /usr/local/mongodb/bin/mongotop --host 127.0.0.1:10000
odds_easy.basic_odds表的操作一直排第一,寫操作占大部分時間
2. 看mongo shard日志
大量超過1s的操作,集中在odds_easy.basic_odds寫操作, 看日志lock數量很多
查詢某一個文檔的更新,在同一秒中居然有15個更新操作,這樣的操作產生什么樣的結果: 大量的寫鎖,並且影響讀;而且還是最影響性能的數組的$push, $set操作
看看文檔的結構,數組的數量之大,而且里面還是對象嵌套; 對這樣一個文檔不停的更新, 性能可想而知
看看 db.serverStats()的lock情況
看看odds_easy的db.basic_odds.stats()情況,大量的更新沖突
3. 看看sharding情況
使用腳本,查看sharding情況,重定向到文件中查看。
sql='db.printShardingStatus({verbose:true})'
echo $sql|/usr/local/mongodb/bin/mongo --host 192.168.1.48:30000 admin --shell
basic_odds的sharding信息:
shard key: { "event_id" : 1, "betting_type_id" : 1 } event_id為mysql自增字段,betting_type_id為玩法id(意味着幾個固定的值,區別度不大)
shard 分布情況, 從圖里面可以看到mongo主要根據event_id這個自增字段的范圍進行數據拆分, 意味着相鄰比賽的數據大概率會被分配到同一個shard分區上(這就是為什么01機器上的日志大小遠大於其他機器的原因吧,目前的數據都集中在shard1上)
下圖為數據庫讀寫情況, 更新操作是查詢操作的4倍。 對一個寫多讀少的數據庫, 本該將寫操作分布到不同的分區上,結果由於sharding key的錯誤選擇造成了寫熱點,將寫集中到了同一個分區,進一步加劇了寫的阻塞
結論
- 文檔結構不合理,數組過大、更新過於頻繁,特別是對同一文檔。 對數組頻繁的更新操作是mongo最不推薦的,不僅影響本機的性能,還影響oplog的數據同步
- sharding key不合理造成了寫熱點, 在第一點不合理的基礎上,更加劇了性能的急劇下降, 還會造成頻繁的mongo數據遷移
(由於odds_easy.basic_odds的更新量大,目前問題在這個表上,但是其他表也有同樣的問題)
【可以看到前期合理的架構設計是多么的重要】
解決思路
- 減少同一時刻對同一文檔的更新操作,將一定時間內的多次更新改為一次更新。
- 將更新最頻繁的process字段從文檔中移出,寫到新的表中。 在新表中,同一event_id,betting_type_id, 賠率公司的變化在同一條記錄中
- 文檔結構中加入時間字段,方便數據遷移,定期將歷史數據進行遷移,進行冷熱數據分離
- 修改sharding key為hash或者其他字段,將寫操作分布到不同的分區上
解決方案
分兩個階段:
第一階段 結構優化
- odds_easy庫中basic_odds, main_odds不再存儲最近10條的變化,去掉process字段。
- 數據直接 mongo push到odds_change中對應的記錄rows字段中
- 單獨提供接口,數據變化從odds_change中讀取, 使用 mongo的$slice讀出最近n條信息,然后程序排序截取即可
- odds_easy庫和odds_change庫中的表都使用 event_id 作 hash sharding key
- odds_easy, odds_change, odds_bet007, odds_betbrain_bb, odds_betbrain_v5, odds_txodds這些庫中的記錄都加入match_time字段。 新增的記錄直接加入;歷史的記錄補全
- 加入分布式鎖,解決並發問題,提高系統橫向擴張能力
第二階段 冷熱分離
目的
- 解決積壓問題
- 提高訪問速度
- 防止用戶對大量歷史的訪問從而影響熱數據的訪問。(可以在配置中加入開關, 出現問題時關閉歷史數據的訪問)
系統中加入redis做熱數據緩存, zookeeper/etcd作為配置服務中心以及熱數據導入的流程控制中心
架構圖
2. 相關流程
update_betting_offer隊列的GermanWorker啟動新增流程
- 從服務配置中心讀取熱點比賽列表
- 需要在服務配置中心注冊節點,節點內容:“本機ip進程號_update_setting” (去掉ip中的點號)
-
在german注冊一個任務名稱,名字為第一步中的節點內容
"本機IP進程號_update_setting"任務處理流程:
- 在服務配置中心注冊新節點
- watch 服務配置中心的 “導入數據OK節點”
- 收到watch變化后,更新程序的熱點event_id列表
- 刪除在服務配置中心注冊的節點
定時任務流程:
- 找出最近2天內未結束比賽的event_id列表
- watch服務配置中心 “導入數據OK節點”,內容為0
- 從服務配置中心獲取所有update_betting_offer的GermanWorker節點,並根據節點內容的發送任務(任務名稱=節點內容,任務內容=event_id列表)
- 等待watch的節點數==german worker數后, 從服務配置中心讀取現在的event_id列表,與新的列表進行對比。將新增的event_id數據從mongo導入redis,過期時間3天
- 導入完畢后, 改變 “導入數據OK節點”,內容為1
3. redis結構
初賠結構, key值: “event_id:betting_id:start” , value值為hash類型,hash_key:provider_id;hash_value:跟mongo中的結構一致,json格式;如{"i":{"t0":{"h":4.27,"d":3.24,"a":1.88},"t1":0.9305,"t2":{"h":0.2179,"d":0.2872,"a":0.4949},"t3":{"h":0.93,"d":0.93,"a":0.93}},"s":1,"t":"2017-04-03 13:39:28","b":0,'p':744 }
終賠結構,key值:"event_id:betting_id:end" value值同初賠
平均結構,key值:"event_id:betting_id:avg" value值同初賠
變化過程,key值:"event_id:betting_id:provider_id:boundary", value值為sorted set, member為賠率信息,跟mongo中的結構一致,json格式;如{"i":{"t0":{"h":4.27,"d":3.24,"a":1.88},"t1":0.9305,"t2":{"h":0.2179,"d":0.2872,"a":0.4949},"t3":{"h":0.93,"d":0.93,"a":0.93}},"s":1,"t":"2017-04-03 13:39:28","b":0,'p':744 }。 score為時間,如20170403133928
如果按照上面的結構進行存儲, 進行了大概的預估。
對2789場比賽進行了歐賠統計,平均一場比賽2006個初賠,2006個終賠;583個boudary值,每個boundary結構中存23個賠率變化;這樣計算一場比賽需要大概 5.5m, 盤口數據大概為歐賠的一半。總8M
如果放入所有未開賽的比賽,大概1個半月的比賽,1w場比賽,所需內存80G,這個量太大了。
所以只放入熱數據,2天內未開賽的比賽,保存3天,3天比賽450場左右。 需要 3.6G
----------------后續:第一階段優化完后高峰期最高120左右