文件服务器冷热数据划分,游戏服务器冷热数据分离方案
冷熱數據分離
當前場景:
gamserver啟動時,會將所有數據加載到內存中,提高讀取數據的性能。但是有很多數據很可能是不常用甚至再也用不到的數據,將這些數據加載到內存中需要占用更多的內存,極大的浪費了內存的使用。
目標:
對冷熱數據進行分離,減少非必要數據對內存的占用,節約內存資源。
主要工作:
數據監控
冷熱數據識別
數據遷移
1.數據監控:
監控與統計數據的使用,為冷熱數據識別服務
監控數據讀取的命中率和數據存儲的百分比
統計每次數據庫讀取和寫入的命中,定期收集數據讀取的命中率和數據存儲比例,以便更加直觀了解使用冷熱數據分離后,節約多少內存,包括百分比和大小,數據讀取的命中率,為以后優化冷熱數據識別算法提供對比數據。
2.冷數據與熱數據:
定義冷數據與熱數據
按玩家區分/按數據的引用區分
目前主要用的還是LRU算法來識別冷熱數據,知網上有看到基于溫度模型的緩存替換策略TCR(Temperature Cache Replacement)論文,可能會有更高的緩存命中率。
或者我們也可根據玩家活躍行為來定義冷熱數據,但是非活躍玩家的數據也可能會被讀取,所以效果不能保證。
3.數據遷移:
主要是對冷熱數據的數據遷移處理。
冷數據處理:
冷數據壓縮/冷數據逐出
大部分都是采用冷數據逐出的方法,把冷數據放到存儲系統中的低性能層級,節約高性能存儲空間。
也有少部分采用冷數據壓縮的方法,把冷數據壓縮存儲,還是放在內存中,等需要用的時候解壓就行,可以節約一點內存,并且讀取冷數據時不需要等待IO。具體壓縮效率需要試驗,相關文檔也比較少,網上看到的基本上是基于gzip壓縮數據。但是我覺得對于我們的架構模型還蠻適用,因為我們的全局唯一鎖的架構,當讀取冷數據時,需要等待IO,這時候釋放鎖和不釋放鎖都比較尷尬。
LRU:
LRU全稱是Least Recently Used,即最近最久未使用的意思。
LRU算法的設計原則是:如果一個數據在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。
LRU的變形算法:
LRU-K
LRU-K中的K代表最近使用的次數,因此LRU可以認為是LRU-1。LRU-K的主要目的是為了解決LRU算法“緩存污染”的問題,其核心思想是將“最近使用過1次”的判斷標準擴展為“最近使用過K次”。
相比LRU,LRU-K需要多維護一個隊列,用于記錄所有緩存數據被訪問的歷史。只有當數據的訪問次數達到K次的時候,才將數據放入緩存。當需要淘汰數據時,LRU-K會淘汰第K次訪問時間距當前時間最大的數據。
two queue
Two queues(以下使用2Q代替)算法類似于LRU-2,不同點在于2Q將LRU-2算法中的訪問歷史隊列(注意這不是緩存數據的)改為一個FIFO緩存隊列,即:2Q算法有兩個緩存隊列,一個是FIFO隊列,一個是LRU隊列。當數據第一次訪問時,2Q算法將數據緩存在FIFO隊列里面,當數據第二次被訪問時,則將數據從FIFO隊列移到LRU隊列里面,兩個隊列各自按照自己的方法淘汰數據。
Multi Queue(MQ)
MQ算法根據訪問頻率將數據劃分為多個隊列,不同的隊列具有不同的訪問優先級,其核心思想是:優先緩存訪問次數多的數據。詳細的算法結構圖如下,Q0,Q1....Qk代表不同的優先級隊列,Q-history代表從緩存中淘汰數據,但記錄了數據的索引和引用次數的隊列。
其他
LRU算法有很多變種,innoDB也是采用LRU算法來提高緩存命中率,innoDB的LRU把表分為兩個部分,old和yount,中間用modpoint隔開,modpoint值可以自己設置,默認值為37,距離表尾端37%的位置。數據第一次讀區的時候會被放在midpoint的位置,這個位置是old表的頭部,但是如果再次被讀取,下次就會直接放在young表的頭部。另外一個數據頁里面可能會有多條記錄,在做全表掃描的時候,一個數據頁可能會一瞬間被訪問多次,這時候可能剛放入列表的時候就再次被訪問,導致直接挪到young區頭部但是其實數據只訪問過一次,這時候有個innodb_old_blocks_time的值來控制,innodb_old_blocks_time設置一個時間,當數據第一次放入列表中后,只有經過一段時間后再次讀取才能觸發把數據移動到young表頭部的行為。
go語言Demo實現
LRUCache.go
package main
import (
"container/list"
"sync"
"time"
)
type LRUCache struct {
list *list.List
cacheMap map[NodeKey]*list.Element
}
var RWLock sync.RWMutex
//數據在內存中存活時間 單位秒
const DATA_LIVE_TIME = 5
type Node struct {
key NodeKey
time int64
}
func NewLRUCache() (*LRUCache) {
return &LRUCache{
list: list.New(),
cacheMap: make(map[NodeKey]*list.Element)}
}
//返回LRU的存儲數據長度
func (lru *LRUCache) Size() int {
return lru.list.Len()
}
func (lru *LRUCache) Set(key NodeKey) {
RWLock.Lock()
defer RWLock.Unlock()
//節點已存在
if element, ok := lru.cacheMap[key]; ok {
lru.list.MoveToFront(element)
element.Value.(*Node).time = time.Now().Unix()
} else {
// 節點不存在,生成新節點
newElement := lru.list.PushFront(&Node{key, time.Now().Unix()})
lru.cacheMap[key] = newElement
}
lru.CheckList()
}
// 獲取數據是否在LRU中,如果存在則更新時間
func (lru *LRUCache) Get(key NodeKey) bool {
/**
由于在RLock中,其他線程也能讀數據,并且WLock需要等待才能將數據移除,所以此處使用RLock就足夠
如果修改時間時用WLock,需要先釋放RLock,反而可能出現釋放RLock后被其他線程搶占WLock的情況
*/
RWLock.RLock()
defer RWLock.RUnlock()
if element, ok := lru.cacheMap[key]; ok {
lru.list.MoveToFront(element)
element.Value.(*Node).time = time.Now().Unix()
return true
} else {
return false
}
}
func (lru *LRUCache) Remove(key NodeKey) {
if element, ok := lru.cacheMap[key]; ok {
delete(lru.cacheMap, key)
lru.list.Remove(element)
//TODO:將數據在內存中移除
println("將數據從內存中移除:", key)
}
}
// 從列表尾端開始檢查數據,將過期的冷數據從內存移除
func (lru *LRUCache) CheckList() {
dList := lru.list
if dList.Len() == 0 {
return
}
node := dList.Back()
for {
if CheckData(node.Value.(*Node)) {
break
} else {
lru.Remove(node.Value.(*Node).key)
if node.Prev() != nil {
node = node.Prev()
} else {
break
}
}
}
}
// 判斷數據是否應該存留, 該存留返回true
func CheckData(node *Node) bool {
if node.time < time.Now().Unix()-DATA_LIVE_TIME {
return false
} else {
return true
}
}
DataCenter.go
package main
import "sync/atomic"
type CacheDataCenter struct {
readCount int64 //總讀取數據次數
hitCount int64 //熱數據讀取命中次數
lruCache ILRU
}
type NodeKey int64
type ILRU interface {
Set(key NodeKey)
Get(key NodeKey) (bool)
Remove(key NodeKey)
Size() int
}
func NewCacheDataCenter() *CacheDataCenter {
return &CacheDataCenter{
readCount: 0,
hitCount: 0,
lruCache: NewLRUCache(),
}
}
func (center *CacheDataCenter) AddReadNum(isHit bool) {
atomic.AddInt64(¢er.readCount, 1)
if isHit {
atomic.AddInt64(¢er.hitCount, 1)
}
}
// 總數據量
func (center *CacheDataCenter) GetTotalCount() int {
return 0
}
// 內存中數據量
func (center *CacheDataCenter) GetCacheCount() int {
return center.lruCache.Size()
}
func (center *CacheDataCenter) GetData(key NodeKey) {
if center.lruCache.Get(key) {
center.AddReadNum(true)
//TODO: 從內存中取數據
println("從內存讀取數據:", key)
} else {
center.AddReadNum(false)
center.lruCache.Set(key)
//TODO: 操作數據庫取數據
println("從數據庫讀取數據:", key)
}
}
main.go
package main
import (
"math/rand"
"sync"
"time"
)
func main() {
cacheDataCenter := NewCacheDataCenter()
wg := sync.WaitGroup{}
wg.Add(3)
for i := 0; i < 3; i++ {
go func() {
readData(cacheDataCenter)
wg.Done()
}()
}
wg.Wait()
println("test結束,總共讀取次數:", cacheDataCenter.readCount, "命中次數:", cacheDataCenter.hitCount, "內存中最終剩余數據:", cacheDataCenter.GetCacheCount())
}
func readData(center *CacheDataCenter) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
r := rand.Int63n(30)
center.GetData(NodeKey(r))
}
}
demo中三個線程每一秒會隨機生成一個數據,并且嘗試在LRUCache中讀取,假如未命中,則插入,假如命中,則更新時間。每次寫數據的時候,會執行一次Check()來檢查舊數據,檢查的時候從底往上檢查,如果數據為冷數據則移除,直到檢查到熱數據為止。
總結
以上是生活随笔為你收集整理的文件服务器冷热数据划分,游戏服务器冷热数据分离方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器接收消息写日志,在Ubuntu 1
- 下一篇: 关于异常的再思考