变形记---容灾恢复 ,异常崩溃引发服务器丢档或无法正常运行

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 最近我给M部门面试服务器主程序开发的职位,我只问他们的架构设计经验,我发现相当一部分5-12年“本应该有足够开发经验”的开发组长,或开发主程序缺乏设计,缺乏容错,缺乏创新,比如一些服务器宕机如何崩溃拉起恢复玩家数据,数据库的异步线程读写如何避免被其他线程写回呢,至少目前能听到合理方案的面试者的回答不多,这也是我想写这篇文章的出发点,以此来分享给大家, 不仅仅是为了应付面试,更是解决实际问题的一种思路。 如题,举例说明:游戏服务器(或者其他业务服务器)正常运行中出现了异常崩溃,可能是异常断电引发,可能是云服务商的软硬件问题引发,这种情况下,你们的服务器架构有没有做灾难恢复处理? 使得

      接着上篇文章 变形记---抽象接口,屎山烂代码如何改造成优质漂亮的代码 ,我一直想写一些对年轻人有帮助的文档来,刚好最近有空就零零碎碎写了一些,罗列了一些提纲然后改再删,花了一个礼拜的时间。

      写这一系列的 “变形记”,也是因为最近我给M部门面试服务器主程序开发的职位,我只问他们的架构设计经验,我发现相当一部分5-12年“本应该有足够开发经验”的开发组长,或开发主程序缺乏设计,缺乏容错,缺乏创新,比如一些服务器宕机如何崩溃拉起恢复玩家数据,数据库的异步线程读写如何避免被其他线程写回呢,至少目前能听到合理方案的面试者的回答不多,这也是我想写这篇文章的出发点,以此来分享给大家, 不仅仅是为了应付面试,更是解决实际问题的一种思路。

     如题,举例说明:游戏服务器(或者其他业务服务器)正常运行中出现了异常崩溃,可能是异常断电引发,可能是云服务商的软硬件问题引发,这种情况下,你们的服务器架构有没有做灾难恢复处理? 使得用户/玩家的数据不丢或尽可能的减少丢失。

     如果这个问题放在10年前,大家不一定会考虑这么远,服务器宕机了,可能会存在-10分钟左右的玩家数据丢档,大不了重启后给玩家补偿邮件而已,现在不敢这样了,现在大家都在逐步地做到完美,即使服务器异常宕机,也使得玩家的数据能及时存档和恢复,对于玩家几乎无感。

     所以我们先聊一聊常见的服务器的数据保存策略:

数据保存策略

       以前常见的长连接服务器的保存策略是

               1.玩家和排行等其他表的数据从数据库加载到内存中

               2.所有玩家的一些升级,加资源等影响其属性变化的都直接修改内存,但并不会立即写数据库(有些重要的数据可能会立即写库,玩家的或者其他模块的数据是定时写的)

               3.服务器有定时器,每隔3-10分钟触发一次数据库落盘写的逻辑,需要遍历有变化的玩家列表,然后依次保存到数据库中。

image.gif编辑

大家发现了没有,如果服务器在本次触发定时器需要将所有的玩家数据保存到数据库中之后,又有一些玩家的等级,货币资源,技能等发生变化了,且在下次触发定时器保存到数据库的逻辑之前发生了宕机或非正常停服,那么这种场景就会造成短暂的玩家丢档问题。

缓存策略

        因此我们需要借助缓存来将玩家内存里的数据及时同步到这里,即使发生宕机,你仍然可以从缓存里将缓存的数据加载到内存中。

image.gif编辑

           这个缓存,可以是映射到一块硬盘的共享内存,也可以是memcache,redis等这种内存数据库,whatever,各有千秋优缺点。

           我以前做RPG的卡牌策略游戏的时候,做过改造,那个时候使用redis的团队还不多,相关的用法并不熟悉,所以我们采用共享内存映射到硬盘上的一块大文件来实现,我们来看下Shamem的声明和实现:

class ShareMem
{
public:
  /*创建ShareMem 内存区
   *  
   *  key   创建ShareMem 的关键值
   *
   *  Size  创建大小
   *
   *  返回 对应ShareMem保持值
   */
  static HANDLE createShareMem(const char* szFile, uint64_t Size, bool bClear);
  /*映射ShareMem 内存区
   *  
   *  handle 映射ShareMem 的保持值
   *
   *  返回 ShareMem 的数据指针
   */
  static char* mapShareMem(HANDLE memHandle, uint32_t dataSize, uint64_t offset);
  /*关闭映射 ShareMem 内存区
   *
   *  MemoryPtr     ShareMem 的数据指针
   *  
   *  
   */ 
  static void unMapShareMem(char* MemoryPtr, uint32_t dataSize);
  /*  关闭ShareMem
   *  handle  需要关闭的ShareMem 保持值
   */
  static void closeShareMem(HANDLE memHandle);
  static void flushShareMem(char* pBaseAddr, uint32_t dataSize);
};

image.gif

HANDLE ShareMem::createShareMem(const char* szFile, uint64_t Size, bool bClear)
{
#ifndef WIN32
  HANDLE fd = open(szFile, bClear ? O_CREAT|O_RDWR|O_TRUNC|O_LARGEFILE : O_CREAT|O_RDWR|O_LARGEFILE, 00777);
  if (fd < 0)
  {
    LOG(ERROR)("open %s failed.errno %d", szFile, errno);
    abort();
  }
  lseek(fd, Size - 1, SEEK_SET);
  write(fd, "", 1);
  return fd;
#else
  HANDLE hFile = CreateFile(szFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, bClear ? CREATE_ALWAYS : OPEN_ALWAYS, 0, NULL);
  HANDLE hMem = CreateFileMapping(hFile, NULL, PAGE_READWRITE, HIUINT32(Size), LOUINT32(Size), NULL);
  CloseHandle(hFile);
  return hMem;
#endif
}
char* ShareMem::mapShareMem(HANDLE memHandle, uint32_t dataSize, uint64_t offset)
{
#ifndef WIN32
  return (char*)mmap(NULL, dataSize, PROT_READ | PROT_WRITE, MAP_SHARED, memHandle, offset);
#else
  return (char*)MapViewOfFile(memHandle, FILE_MAP_READ | FILE_MAP_WRITE, HIUINT32(offset), LOUINT32(offset), dataSize);
#endif
  return 0;
}
void ShareMem::unMapShareMem(char* MemoryPtr, uint32_t dataSize)
{
#ifndef WIN32
  munmap(MemoryPtr, dataSize);
#else
  UnmapViewOfFile(MemoryPtr);
#endif
}
void ShareMem::closeShareMem(HANDLE memHandle)
{
#ifndef WIN32
  close(memHandle);
#else
  CloseHandle(memHandle);
#endif
}
void ShareMem::flushShareMem(char* pBaseAddr, uint32_t dataSize)
{
#ifndef WIN32
  //msync(pBaseAddr, dataSize, MS_ASYNC);
#else
  bool bRet = /*FlushViewOfFile(pBaseAddr, dataSize)*/true;
  if (!bRet)
  {
    printf("%d", GetLastError());
  }
#endif
}

image.gif

因此,当我们将玩家player对象的数据序列化到二进制数据中,保存到一个叫PlayerData的结构中,

struct PlayerData
{
    enum {MAX_DATA_SIZE = 40960 - sizeof(uint64_t) - sizeof(uint32_t)};
    uint64_t playerId;
    uint32_t dataLen;
    char payload[MAX_DATA_SIZE];
};

image.gif

那么我们将玩家PlayerData数据的更新,快速查找,缓存的初始化封装成SmData,

typedef PlayerData TData;
class SmData
{
public:
  SmData()
  {
    m_smHandle = NULL;
  }
  void init(const char* szFile, uint64_t Size, uint32_t trunckSize, bool bClear)
  {   
    if (m_smHandle != NULL)
    {
       ShareMem::closeShareMem(m_smHandle);
    }
    m_smHandle = ShareMem::createShareMem(szFile, Size, bClear);
    m_smSize = 0;
    m_maxSize = Size;
    m_trunkSize = trunckSize;
    m_playerSmMap.clear();
    playerDataMap.clear();
  }
  void finit()
  {
    if (m_smHandle != NULL)
    {
      ShareMem::closeShareMem(m_smHandle);
    }
  }
public:
  void append(uint64_t key, TData* pData)
  {
    m_playerSmMap.insert(make_pair(key, m_smSize));
    //根据文件位置,计算出映射的位置,然后再计算出真正的位置来
    uint32_t baseMapSize = (m_smSize / m_trunkSize) * m_trunkSize;
    uint32_t mapSize = ((m_smSize + sizeof(TData)) / m_trunkSize - m_smSize / m_trunkSize + 1) * m_trunkSize;
    char* pBaseAddr = ShareMem::mapShareMem(m_smHandle, mapSize, baseMapSize);
    uint32_t offsetSize = m_smSize - baseMapSize;
    char* pPlayerData = pBaseAddr + offsetSize;
    memcpy(pPlayerData, pData, sizeof(TData));
    m_smSize += sizeof(TData);
    ShareMem::unMapShareMem(pBaseAddr, mapSize);
  }
  bool getData(uint64_t key, TData* pData)
  {
    bool bFound = false;
    hash_map<uint64_t, PlayerData>::iterator itePlayerData = playerDataMap.find(key);
    if (itePlayerData != playerDataMap.end())
    {
      pData = &itePlayerData->second;
      bFound = true;
    }
    else
    {
      //根据文件位置,计算出映射的位置,然后再计算出真正的位置来
      hash_map<uint64_t, uint64_t>::iterator itePlayerDataPos = m_playerSmMap.find(key);
      if (itePlayerDataPos != m_playerSmMap.end())
      {
        uint32_t baseMapSize = (uint32_t)((itePlayerDataPos->second / m_trunkSize) * m_trunkSize);
        uint32_t mapSize = ((itePlayerDataPos->second + sizeof(TData)) / m_trunkSize - itePlayerDataPos->second / m_trunkSize + 1) * m_trunkSize;
        char* pBaseAddr = ShareMem::mapShareMem(m_smHandle, mapSize, baseMapSize);
        uint32_t offsetSize = itePlayerDataPos->second - baseMapSize;
        char* pPlayerData = pBaseAddr + offsetSize;
        memcpy(pData, pPlayerData, sizeof(TData));
        ShareMem::unMapShareMem(pBaseAddr, mapSize);
        bFound = true;
      }
      else
      {
        LOG(ERROR)("can not found %llu player data", key);
      }
    }
    return bFound;
  }
  void update(uint64_t key, TData* pData)
  {
    hash_map<uint64_t, uint64_t>::iterator iteSm = m_playerSmMap.find(key);
    if (iteSm != m_playerSmMap.end())
    {
      uint32_t baseMapSize = (iteSm->second / m_trunkSize) * m_trunkSize;
      uint32_t mapSize = ((iteSm->second + sizeof(TData)) / m_trunkSize - iteSm->second / m_trunkSize + 1) * m_trunkSize;
      char* pBaseAddr = ShareMem::mapShareMem(m_smHandle, mapSize, baseMapSize);
      uint32_t offsetSize = iteSm->second - baseMapSize;
      char* pPlayerData = pBaseAddr + offsetSize;
      memcpy(pPlayerData, pData, sizeof(TData));
      ShareMem::flushShareMem(pPlayerData, sizeof(TData));
      ShareMem::unMapShareMem(pBaseAddr, mapSize);
      LOG(DEBUG)("player %llu serialize", key);
    }
    else
    {
      if (m_smSize + sizeof(TData) <= m_maxSize)
      {
        m_playerSmMap.insert(make_pair(key, m_smSize));
        //根据文件位置,计算出映射的位置,然后再计算出真正的位置来
        uint32_t baseMapSize = (m_smSize / m_trunkSize) * m_trunkSize;
        uint32_t mapSize = ((m_smSize + sizeof(TData)) / m_trunkSize - m_smSize / m_trunkSize + 1) * m_trunkSize;
        char* pBaseAddr = ShareMem::mapShareMem(m_smHandle, mapSize, baseMapSize);
        uint32_t offsetSize = m_smSize - baseMapSize;
        char* pPlayerData = pBaseAddr + offsetSize;
        memcpy(pPlayerData, pData, sizeof(TData));
        m_smSize += sizeof(TData);
        ShareMem::flushShareMem(pPlayerData, sizeof(TData));
        ShareMem::unMapShareMem(pBaseAddr, mapSize);
        LOG(DEBUG)("player %llu serialize new", key);
      }
      else
      {
        playerDataMap[key] = *pData;
      }
    }
  }
public:
  uint64_t getSmSize() const
  {
    return m_smSize;
  }
private:
  hash_map<uint64_t, uint64_t> m_playerSmMap; //玩家基础数据到共享内存的映射 playerid,数据在文件中的位置
  HANDLE m_smHandle;
  uint64_t m_smSize;
  uint64_t m_maxSize;
  uint32_t m_trunkSize;
  hash_map<uint64_t, TData> playerDataMap; //玩家基础数据
};

image.gif

如何使用呢      

SmData  smPlayerData;
smPlayerData.init("playerdata.cache", SMFILE_MAX_SIZE, g_gs.m_sysPageSize, true);

image.gif

如果有新增的玩家数据,那么你需要将player数据序列化到PlayerData对象playerData中

smPlayerData.append(playerId, &playerData);

image.gif

如果需要更新缓存则使用

smPlayerData.update(pPlayer->m_playerId, &playerData);

image.gif

一般系统分配4G的共享内存即可满足 10W人的缓存记录,不过共享内存有个缺陷就是如果当前服务器的硬盘坏了或磁盘满,那么你映射到当前硬盘的共享内存就会出现异常,可能造成一些新的数据无法被添加进来,也无法及时更新,另外由于缓存块是按照固定大小来分配和查找的,当缓存块的大小超过限制就无法添加新的缓存块进来,也会造成可能丢档的情况。

       随着硬件和技术越来越成熟,redis,memcache提供了更方便的操作api来给开发者提供服务,就拿redis来说,无论是哨兵模式还是集群模式的数据库服务商,他们都可以提供良好的容灾,主从备份服务,大大减少开发者对缓存容灾的过度考虑和设计。

崩溃拉起

       上面的图只是说了如何借助缓存存数据,没有说一旦崩溃如何拉起数据,所以这里我做个补充: 即时数据都通过内存来修改,并同步至缓存中,内存数据约=缓存数据,因此缓存里存在的是热数据,需要保存的数据都有脏数据标志,定时器触发后,将有脏数据标志的玩家数据保存到数据库中(冷数据),如果发生宕机,可按照下列流程来处理:

image.gif编辑

这个图只是个例,并不是所有的服务器都是这样的。

       因为有一些游戏服务器他是启动的时候将所有玩家的数据都加载到内存中,这个时候如果缓存里有数据,就要以缓存的数据为准,因为数据是先内存-》缓存-》数据库,缓存里没有了再以数据库中的为准。

      有一些服务器却是只加载一部分活跃玩家的数据,那么加载的时候也需要以缓存的为准,当内存里找不到,则继续从缓存中找,缓存未命中,则从数据库中查找。

     实际上这种崩溃拉起的思路和方案不仅仅适用于游戏服务器这种行业,在直播,云会议等高可用,可容灾的服务行业也有成熟的方案。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
Linux 网络安全 Python
如何在服务器上运行python文件
如何在服务器上运行python文件
|
24天前
|
SQL 关系型数据库 MySQL
服务器运行一段时间后
【4月更文挑战第1天】服务器运行一段时间后 需要清除日志
284 10
|
3天前
|
存储 运维 安全
服务器数据恢复—异常断电导致RAID5阵列信息丢失的数据恢复案例
服务器数据恢复环境: 某品牌ProLiant DL380系列服务器,服务器中有一组由6块SAS硬盘组建的RAID5阵列,WINDOWS SERVER操作系统,作为企业内部文件服务器使用。 服务器故障: 机房供电几次意外中断,服务器出现故障前最后一次异常断电重启后RAID报错,提示无法找到存储设备,进入RAID管理模块做任何操作都死机,重启服务器后问题依旧,用户联系北亚企安数据恢复中心寻求帮助。
|
4天前
|
存储 安全 网络协议
游戏服务器:构建与运行的艺术
游戏服务器:构建与运行的艺术
17 1
|
13天前
|
存储 监控 安全
什么情况下物理服务器会运行出错?
物理服务器,也称为裸机服务器,一般可以提供高性能计算水平和巨大的存储容量。然而,它们也难免会遇到一些问题。运行出错时,可能会导致停机和数据丢失。
30 15
|
17天前
|
安全 Java 网络安全
对象存储oss使用问题之使用oss上服务器后显示服务异常如何解决
《对象存储OSS操作报错合集》精选了用户在使用阿里云对象存储服务(OSS)过程中出现的各种常见及疑难报错情况,包括但不限于权限问题、上传下载异常、Bucket配置错误、网络连接问题、跨域资源共享(CORS)设定错误、数据一致性问题以及API调用失败等场景。为用户降低故障排查时间,确保OSS服务的稳定运行与高效利用。
18 0
|
1月前
|
安全 关系型数据库 MySQL
卸载宝塔后,如何检查服务器运行状态?
通过上述方法,您可以全面检查服务器在卸载宝塔面板后的运行状态。如果发现问题,您可以根据错误信息或日志进行相应的故障排除。
|
1月前
|
监控 数据安全/隐私保护 iOS开发
服务器监控新利器:ServerBee带你看透服务器运行状态
服务器监控新利器:ServerBee带你看透服务器运行状态
35 0
|
1月前
|
监控 Java Linux
使用jvisualVM监控远程linux服务器上运行的jar程序
使用jvisualVM监控远程linux服务器上运行的jar程序
15 5
|
2月前
|
存储 数据挖掘 Windows
服务器数据恢复—异常断电导致raid信息丢失的数据恢复案例
由于机房多次断电导致一台服务器中raid阵列信息丢失。该阵列中存放的是文档,上层安装的是Windows server操作系统,没有配置ups。 因为服务器异常断电重启后,raid阵列可以正常使用,所以未引起管理员的注意。后续出现的多次异常断电导致raid报错,服务器无法找到存储设备,进入raid管理模块进行任何操作都会导致操作系统死机。管理员尝试多次重启服务器,故障依旧。