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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 最近我给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
相关文章
|
17天前
|
缓存 监控 网络安全
因服务器时间不同步引起的异常
因服务器时间不同步引起的异常
56 1
|
16天前
|
弹性计算 监控 容灾
阿里云ECS提供强大的云上灾备解决方案,通过高可用基础设施、多样的数据备份方式及异地灾备服务,帮助企业实现业务的持续稳定运行
在数字化时代,企业对信息技术的依赖加深,确保业务连续性至关重要。阿里云ECS提供强大的云上灾备解决方案,通过高可用基础设施、多样的数据备份方式及异地灾备服务,帮助企业实现业务的持续稳定运行。无论是小型企业还是大型企业,都能从中受益,确保在面对各种风险时保持业务稳定。
34 4
|
28天前
|
自然语言处理 编译器 应用服务中间件
PHP在服务器上的运行过程
PHP在服务器上的运行过程
38 7
|
1月前
|
监控 Ubuntu Linux
使用VSCode通过SSH远程登录阿里云Linux服务器异常崩溃
通过 VSCode 的 Remote - SSH 插件远程连接阿里云 Ubuntu 22 服务器时,会因高 CPU 使用率导致连接断开。经排查发现,VSCode 连接根目录 ".." 时会频繁调用"rg"(ripgrep)进行文件搜索,导致 CPU 负载过高。解决方法是将连接目录改为"root"(或其他具体的路径),避免不必要的文件检索,从而恢复正常连接。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
SQL 分布式计算 NoSQL
大数据-170 Elasticsearch 云服务器三节点集群搭建 测试运行
大数据-170 Elasticsearch 云服务器三节点集群搭建 测试运行
46 4
|
2月前
|
SQL 分布式计算 大数据
大数据-168 Elasticsearch 单机云服务器部署运行 详细流程
大数据-168 Elasticsearch 单机云服务器部署运行 详细流程
60 2
|
1月前
|
Ubuntu 关系型数据库 MySQL
如何选择适合CMS运行的服务器?
在数字互联网时代,企业与单位都需要搭建企业官网在互联网上展示自己的品牌和产品宣传。除去了传统建设公司开发网站外,使用CMS就成为常用的网站创建方式。而成功的网站除了选对CMS外,还需要考虑到搭建完CMS的服务器。今天的文章给大家介绍:如何选择CMS和服务器: 很多客户都不清楚是选择CMS还是先选择服务器?
|
2月前
|
网络安全 Docker 容器
【Bug修复】秒杀服务器异常,轻松恢复网站访问--从防火墙到Docker服务的全面解析
【Bug修复】秒杀服务器异常,轻松恢复网站访问--从防火墙到Docker服务的全面解析
29 0
|
2月前
|
前端开发 Java Shell
后端项目打包上传服务器部署运行记录
后端项目打包上传服务器部署运行记录
43 0