UDN-企业互联网技术人气社区

板块导航

浏览  : 1370
回复  : 0

[讨论交流] 深入剖析 redis RDB 持久化策略

[复制链接]
王的风范的头像 楼主
发表于 2015-12-6 11:49:14 | 显示全部楼层 |阅读模式
  深入剖析 redis RDB 持久化策略

  简介 redis 持久化 RDB、AOF
  • redis 提供两种持久化方式:RDB 和 AOF。redis 允许两者结合,也允许两者同时关闭。
  • RDB 可以定时备份内存中的数据集。服务器启动的时候,可以从 RDB 文件中回复数据集。
  • AOF 可以记录服务器的所有写操作。在服务器重新启动的时候,会把所有的写操作重新执行一遍,从而实现数据备份。当写操作集过大(比原有的数据集还大),redis 会重写写操作集


  本篇主要讲的是 RDB 持久化,了解 RDB 的数据保存结构和运作机制。redis 主要在 rdb.h 和 rdb.c 两个文件中实现 RDB 的操作。
数据结构 rio

  持久化的 IO 操作在 rio.h 和 rio.c 中实现,核心数据结构是 struct rio。RDB 中的几乎每一个函数都带有 rio 参数。struct rio 既适用于文件,又适用于内存缓存,从 struct rio 的实现可见一斑。
  1. struct _rio {
  2.     // 函数指针,包括读操作,写操作和文件指针移动操作
  3.     /* Backend functions.
  4.      * Since this functions do not tolerate short writes or reads the return
  5.      * value is simplified to: zero on error, non zero on complete success. */
  6.     size_t (*read)(struct _rio *, void *buf, size_t len);
  7.     size_t (*write)(struct _rio *, const void *buf, size_t len);
  8.     off_t (*tell)(struct _rio *);
  9.     // 校验和计算函数
  10.     /* The update_cksum method if not NULL is used to compute the checksum of
  11.      * all the data that was read or written so far. The method should be
  12.      * designed so that can be called with the current checksum, and the buf
  13.      * and len fields pointing to the new block of data to add to the checksum
  14.      * computation. */
  15.     void (*update_cksum)(struct _rio *, const void *buf, size_t len);
  16.     // 校验和
  17.     /* The current checksum */
  18.     uint64_t cksum;
  19.     // 已经读取或者写入的字符数
  20.     /* number of bytes read or written */
  21.     size_t processed_bytes;
  22.     // 每次最多能处理的字符数
  23.     /* maximum single read or write chunk size */
  24.     size_t max_processing_chunk;
  25.     // 可以是一个内存总的字符串,也可以是一个文件描述符
  26.     /* Backend-specific vars. */
  27.     union {
  28.         struct {
  29.             sds ptr;
  30.             // 偏移量
  31.             off_t pos;
  32.         } buffer;
  33.         struct {
  34.             FILE *fp;
  35.             // 偏移量
  36.             off_t buffered; /* Bytes written since last fsync. */
  37.             off_t autosync; /* fsync after 'autosync' bytes written. */
  38.         } file;
  39.     } io;
  40. };
  41. typedef struct _rio rio;
复制代码


  redis 定义两个 struct rio,分别是 rioFileIO 和 rioBufferIO,前者用于内存缓存,后者用于文件 IO:
  1. // 适用于内存缓存
  2. static const rio rioBufferIO = {
  3.     rioBufferRead,
  4.     rioBufferWrite,
  5.     rioBufferTell,
  6.     NULL,           /* update_checksum */
  7.     0,              /* current checksum */
  8.     0,              /* bytes read or written */
  9.     0,              /* read/write chunk size */
  10.     { { NULL, 0 } } /* union for io-specific vars */
  11. };
  12. // 适用于文件 IO
  13. static const rio rioFileIO = {
  14.     rioFileRead,
  15.     rioFileWrite,
  16.     rioFileTell,
  17.     NULL,           /* update_checksum */
  18.     0,              /* current checksum */
  19.     0,              /* bytes read or written */
  20.     0,              /* read/write chunk size */
  21.     { { NULL, 0 } } /* union for io-specific vars */
  22. };
复制代码


  RDB 持久化的运作机制
rdb_datastruct_sample.png

  redis 支持两种方式进行 RDB:当前进程执行和后台执行(BGSAVE)。RDB BGSAVE 策略是 fork 出一个子进程,把内存中的数据集整个 dump 到硬盘上。两个场景举例:
  • redis 服务器初始化过程中,设定了定时事件,每隔一段时间就会触发持久化操作;进入定时事件处理程序中,就会 fork 产生子进程执行持久化操作。
  • redis 服务器预设了 save 指令,客户端可要求服务器进程中断服务,执行持久化操作。


  这里主要展开的内容是 RDB 持久化操作的写文件过程,读过程和写过程相反。子进程的产生发生在 rdbSaveBackground() 中,真正的 RDB 持久化操作是在 rdbSave(),想要直接进行 RDB 持久化,调用 rdbSave() 即可。

  以下主要以代码的方式来展开 RDB 的运作机制:
/
  1. / 备份主程序
  2. /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
  3. int rdbSave(char *filename) {
  4.     dictIterator *di = NULL;
  5.     dictEntry *de;
  6.     char tmpfile[256];
  7.     char magic[10];
  8.     int j;
  9.     long long now = mstime();
  10.     FILE *fp;
  11.     rio rdb;
  12.     uint64_t cksum;
  13.     // 打开文件,准备写
  14.     snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
  15.     fp = fopen(tmpfile,"w");
  16.     if (!fp) {
  17.         redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
  18.             strerror(errno));
  19.         return REDIS_ERR;
  20.     }
  21.     // 初始化 rdb 结构体。rdb 结构体内指定了读写文件的函数,已写/读字符统计等数据
  22.     rioInitWithFile(&rdb,fp);
  23.     if (server.rdb_checksum) // 校验和
  24.         rdb.update_cksum = rioGenericUpdateChecksum;
  25.     // 先写入版本号
  26.     snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
  27.     if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
  28.     for (j = 0; j < server.dbnum; j++) {
  29.         // server 中保存的数据
  30.         redisDb *db = server.db+j;
  31.         // 字典
  32.         dict *d = db->dict;
  33.         if (dictSize(d) == 0) continue;
  34.         // 字典迭代器
  35.         di = dictGetSafeIterator(d);
  36.         if (!di) {
  37.             fclose(fp);
  38.             return REDIS_ERR;
  39.         }
  40.         // 写入 RDB 操作码
  41.         /* Write the SELECT DB opcode */
  42.         if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
  43.         // 写入数据库序号
  44.         if (rdbSaveLen(&rdb,j) == -1) goto werr;
  45.         // 写入数据库中每一个数据项
  46.         /* Iterate this DB writing every entry */
  47.         while((de = dictNext(di)) != NULL) {
  48.             sds keystr = dictGetKey(de);
  49.             robj key,
  50.                 *o = dictGetVal(de);
  51.             long long expire;
  52.             // 将 keystr 封装在 robj 里
  53.             initStaticStringObject(key,keystr);
  54.             // 获取过期时间
  55.             expire = getExpire(db,&key);
  56.             // 开始写入磁盘
  57.             if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
  58.         }
  59.         dictReleaseIterator(di);
  60.     }
  61.     di = NULL; /* So that we don't release it again on error. */
  62.     // RDB 结束码
  63.     /* EOF opcode */
  64.     if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
  65.     // 校验和
  66.     /* CRC64 checksum. It will be zero if checksum computation is disabled, the
  67.      * loading code skips the check in this case. */
  68.     cksum = rdb.cksum;
  69.     memrev64ifbe(&cksum);
  70.     rioWrite(&rdb,&cksum,8);
  71.     // 同步到磁盘
  72.     /* Make sure data will not remain on the OS's output buffers */
  73.     fflush(fp);
  74.     fsync(fileno(fp));
  75.     fclose(fp);
  76.     // 修改临时文件名为指定文件名
  77.     /* Use RENAME to make sure the DB file is changed atomically only
  78.      * if the generate DB file is ok. */
  79.     if (rename(tmpfile,filename) == -1) {
  80.         redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
  81.         unlink(tmpfile);
  82.         return REDIS_ERR;
  83.     }
  84.     redisLog(REDIS_NOTICE,"DB saved on disk");
  85.     server.dirty = 0;
  86.     // 记录成功执行保存的时间
  87.     server.lastsave = time(NULL);
  88.     // 记录执行的结果状态为成功
  89.     server.lastbgsave_status = REDIS_OK;
  90.     return REDIS_OK;
  91. werr:
  92.     // 清理工作,关闭文件描述符等
  93.     fclose(fp);
  94.     unlink(tmpfile);
  95.     redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
  96.     if (di) dictReleaseIterator(di);
  97.     return REDIS_ERR;
  98. }
  99. // bgsaveCommand(),serverCron(),syncCommand(),updateSlavesWaitingBgsave() 会调用 rdbSaveBackground()
  100. int rdbSaveBackground(char *filename) {
  101.     pid_t childpid;
  102.     long long start;
  103.     // 已经有后台程序了,拒绝再次执行
  104.     if (server.rdb_child_pid != -1) return REDIS_ERR;
  105.     server.dirty_before_bgsave = server.dirty;
  106.     // 记录这次尝试执行持久化操作的时间
  107.     server.lastbgsave_try = time(NULL);
  108.     start = ustime();
  109.     if ((childpid = fork()) == 0) {
  110.         int retval;
  111.         // 取消监听
  112.         /* Child */
  113.         closeListeningSockets(0);
  114.         redisSetProcTitle("redis-rdb-bgsave");
  115.         // 执行备份主程序
  116.         retval = rdbSave(filename);
  117.         // 脏数据,其实就是子进程所消耗的内存大小
  118.         if (retval == REDIS_OK) {
  119.             // 获取脏数据大小
  120.             size_t private_dirty = zmalloc_get_private_dirty();
  121.             // 记录脏数据
  122.             if (private_dirty) {
  123.                 redisLog(REDIS_NOTICE,
  124.                     "RDB: %zu MB of memory used by copy-on-write",
  125.                     private_dirty/(1024*1024));
  126.             }
  127.         }
  128.         // 退出子进程
  129.         exitFromChild((retval == REDIS_OK) ? 0 : 1);
  130.     } else {
  131.         /* Parent */
  132.         // 计算 fork 消耗的时间
  133.         server.stat_fork_time = ustime()-start;
  134.         // fork 出错
  135.         if (childpid == -1) {
  136.             // 记录执行的结果状态为失败
  137.             server.lastbgsave_status = REDIS_ERR;
  138.             redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
  139.                 strerror(errno));
  140.             return REDIS_ERR;
  141.         }
  142.         redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
  143.         // 记录保存的起始时间
  144.         server.rdb_save_time_start = time(NULL);
  145.         // 子进程 ID
  146.         server.rdb_child_pid = childpid;
  147.         updateDictResizePolicy();
  148.         return REDIS_OK;
  149.     }
  150.     return REDIS_OK; /* unreached */
  151. }
复制代码


  如果采用 BGSAVE 策略,且内存中的数据集很大,fork() 会因为要为子进程产生一份虚拟空间表而花费较长的时间;如果此时客户端请求数量非常大的话,会导致较多的写时拷贝操作;在 RDB 持久化操作过程中,每一个数据都会导致 write() 系统调用,CPU 资源很紧张。因此,如果在一台物理机上部署多个 redis,应该避免同时持久化操作。

  那如何知道 BGSAVE 占用了多少内存?子进程在结束之前,读取了自身私有脏数据 Private_Dirty 的大小,这样做是为了让用户看到 redis 的持久化进程所占用了有多少的空间。在父进程 fork 产生子进程过后,父子进程虽然有不同的虚拟空间,但物理空间上是共存的,直至父进程或者子进程修改内存数据为止,所以脏数据 Private_Dirty 可以近似的认为是子进程,即持久化进程占用的空间。

  RDB 数据的组织方式

  RDB 的文件组织方式为:数据集序号1:操作码:数据1:结束码:校验和—-数据集序号2:操作码:数据2:结束码:校验和……
其中,数据的组织方式为:过期时间:数据类型:键:值,即 TVL(type,length,value)。

  举两个字符串存储的例子,其他的大概都以至于的形式来组织数据:
rdb_persistence.png


  可见,RDB 持久化的结果是一个非常紧凑的文件,几乎每一位都是有用的信息。如果对 redis RDB 数据组织方式的细则感兴趣,可以参看 rdb.h 和 rdb.c 两个文件的实现。

  对于每一个键值对都会调用 rdbSaveKeyValuePair(),如下:
  1. int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
  2.                         long long expiretime, long long now)
  3. {
  4.     // 过期时间
  5.     /* Save the expire time */
  6.     if (expiretime != -1) {
  7.         /* If this key is already expired skip it */
  8.         if (expiretime < now) return 0;
  9.         if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
  10.         if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
  11.     }
  12.     /* Save type, key, value */
  13.     // 数据类型
  14.     if (rdbSaveObjectType(rdb,val) == -1) return -1;
  15.     // 键
  16.     if (rdbSaveStringObject(rdb,key) == -1) return -1;
  17.     // 值
  18.     if (rdbSaveObject(rdb,val) == -1) return -1;
  19.     return 1;
  20. }
复制代码


  如果对 redis RDB 数据格式细则感兴趣,欢迎访问我的 github & 欢迎讨论。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部