文件服务器冷热数据划分,游戏服务器冷热数据分离方案

冷热数据分离

当前场景:

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(&center.readCount, 1)

if isHit {

atomic.AddInt64(&center.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()来检查旧数据,检查的时候从底往上检查,如果数据为冷数据则移除,直到检查到热数据为止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值