redis 多线程初体验 作者: nbboy 时间: 2020-08-25 分类: 软件架构,软件工程,设计模式 评论 ###测试环境 mbp本机测试,测试机子配置: 2.9 GHz Intel Core i5 8 GB 1867 MHz DDR3 ###比较版本 redis都开启rdb和aof持久化,比较的版本redis-5.0.7和redis-6.0.6(开启多线程支持),使用测试工具就是作者提供的redis-benchmark,测试命令如下: ```shell redis-benchmark -t set,get -n 1000000 -r 100000000 -h 192.168.1.197 -d {dataSize} -c 200 --threads 4 ``` ###图表 用图表导出后得到: Redis Set命令压测结果 Redis Get命令压测结果 ###结论 我们从上面图表中得出结论,在开启多线程模式下,性能提高确实不少,特别是在包越大的情况下,效果更加明显。其实这是和作者的实现方式有关,在redis中,真正执行命令还是在主线程中,而是把网络数据收发和命令解析单独在i/o线程中去完成而已,作者说他不想让实现变得复杂,而效果也没那么明显。具体可以看下他的文章:http://antirez.com/news/126
redis rdb机制浅析 作者: nbboy 时间: 2020-08-24 分类: 软件架构,软件工程,设计模式 评论 ###dump rdb文件的流程: 1.redis fork()调用,创建子进程 2.利用copy-on-write技术,对内存dump出来到一个临时文件 3.完成dump文件后,替换该临时文件为dump.rdb文件 ###需要注意的几个问题: 1.dump的是执行这条命令时候的数据 2.从上面步骤可以看到,任何时候dump.rdb文件其实都是完整的 3.可以执行save,bgsave来手动执行dump操作,或者配置save规则让redis自动执行,其实也是执行bgsave 4.redis重启后,读取dump.rdb文件开始从磁盘到内存的数据加载过程,这个过程是阻塞的,直到完成加载 5.rdb方式会丢失数据,这部分数据就是从上一次dump到redis挂掉为止修改的数据 通过调试跟踪,bgsave命令最终的实现的文件在rdb.c里的bgsaveCommand ```c /* BGSAVE [SCHEDULE] bgsave执行的命令 */ void bgsaveCommand(client *c) { int schedule = 0; /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite * is in progress. Instead of returning an error a BGSAVE gets scheduled. */ if (c->argc > 1) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) { schedule = 1; } else { addReply(c,shared.syntaxerr); return; } } rdbSaveInfo rsi, *rsiptr; //初始化rdb集群相关信息 rsiptr = rdbPopulateSaveInfo(&rsi); //如果bgsave进程还在执行中,则返回错误 if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); //这里也一样,如果系统还在执行gsave进程,或者执行aof重写进程,或者执行模块进程则选择不执行 } else if (hasActiveChildProcess()) { if (schedule) { server.rdb_bgsave_scheduled = 1; addReplyStatus(c,"Background saving scheduled"); } else { addReplyError(c, "Another child process is active (AOF?): can't BGSAVE right now. " "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever " "possible."); } //执行真正的bgsave } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { addReplyStatus(c,"Background saving started"); } else { addReply(c,shared.err); } } ``` 通过上面的代码,我们可以看到,aof重写进程在工作的时候,bgsave不会真正工作!在rdbSaveBackground里看下做了什么事情? ```c int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid; //再一次检测工作进程执行情况 if (hasActiveChildProcess()) return C_ERR; server.dirty_before_bgsave = server.dirty; server.lastbgsave_try = time(NULL); //建立父子进程通信之间的管道 openChildInfoPipe(); //执行包装的fork if ((childpid = redisFork()) == 0) { int retval; /* Child */ redisSetProcTitle("redis-rdb-bgsave"); redisSetCpuAffinity(server.bgsave_cpulist); //执行真正的保存rdb文件逻辑 retval = rdbSave(filename,rsi); if (retval == C_OK) { //用建立的管道,通知父进程,子进程已经结束的通知 sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB"); } exitFromChild((retval == C_OK) ? 0 : 1); } else { /* Parent */ if (childpid == -1) { closeChildInfoPipe(); server.lastbgsave_status = C_ERR; serverLog(LL_WARNING,"Can't save in background: fork: %s", strerror(errno)); return C_ERR; } serverLog(LL_NOTICE,"Background saving started by pid %d",childpid); server.rdb_save_time_start = time(NULL); server.rdb_child_pid = childpid; server.rdb_child_type = RDB_CHILD_TYPE_DISK; return C_OK; } return C_OK; /* unreached */ } ``` 代码注释非常详细了,可以看到用fork创建了子进程,真正的dump工作都是在子进程中完成的。因为操作系统实现的copy-on-write机制,所以fork后其实子进程地址空间和父进程地址空间还是同一个,所以数据完全可以dump出来,关于copy-on-write可以看下,这篇文章的理论知识https://wingsxdu.com/post/linux/concurrency-oriented-programming/fork-and-cow/ 进一步阅读rdbSave函数,具体的dump工作就是在这个函数里进行的。 ```c /* Save the DB on disk. Return C_ERR on error, C_OK on success. */ int rdbSave(char *filename, rdbSaveInfo *rsi) { char tmpfile[256]; char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */ FILE *fp; rio rdb; int error = 0; //建立临时文件,后续的rdb内容都先写到这个临时文件中 snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Failed opening the RDB file %s (in server root dir %s) " "for saving: %s", filename, cwdp ? cwdp : "unknown", strerror(errno)); return C_ERR; } //初始化文件,这里的rio是作者抽象的流式文件对象 rioInitWithFile(&rdb,fp); //通知模块触发持久化的事件 startSaving(RDBFLAGS_NONE); if (server.rdb_save_incremental_fsync) rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES); //这里执行真正的dump逻辑,涉及到很多rdb文件格式细节 if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) { errno = error; goto werr; } //下面的几个操作,都是保证让buffer中的数据都写入到磁盘 //关于这方面的讨论具体可以看下作者的讨论:http://oldblog.antirez.com/post/redis-persistence-demystified.html /* Make sure data will not remain on the OS's output buffers */ if (fflush(fp) == EOF) goto werr; if (fsync(fileno(fp)) == -1) goto werr; if (fclose(fp) == EOF) goto werr; /* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ //把临时文件重新命名为dump.rdb(可以配置)文件 if (rename(tmpfile,filename) == -1) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Error moving temp DB file %s on the final " "destination %s (in server root dir %s): %s", tmpfile, filename, cwdp ? cwdp : "unknown", strerror(errno)); unlink(tmpfile); stopSaving(0); return C_ERR; } serverLog(LL_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = C_OK; //通知模块持久化结束的事件 stopSaving(1); return C_OK; werr: serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno)); fclose(fp); //执行错误的话,需要删除临时文件 unlink(tmpfile); stopSaving(0); return C_ERR; } ``` ###从上面代码代码可以得出结论: 1)都是先写到临时文件temp-dump.rdb中,然后再重命名为正式文件dump.rdb 2) 在持久化之前和之后,模块会收到开始和结束事件 3)rdbSaveRio根据rdb特有格式写到文件中,其实rdb也用在集群复制中 4)写到文件后,数据都会从用户缓存区和内核缓冲区强制写到磁盘驱动中去,也就是实现落盘,这样即使redis服务崩溃,或者系统崩溃这两个级别错误,都可以应对。 ###总结: rdbSaveRio中是具体的dump逻辑,根据rdb格式dump出来,这个文件格式的具体描述可以看https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format ,因为这个逻辑不是本篇文章的重点,所以不再叙述。 参考: https://blog.csdn.net/aitangyong/article/details/52045251
redis6.0 客户端缓存学习笔记 作者: nbboy 时间: 2020-08-12 分类: 软件架构,软件工程,设计模式 评论 这个功能的原因是在redis server 前面加入L1缓存,也就是进程内缓存,进程内的缓存好处是减少网络io开销和序列化、反序列化的开销,需要解决的核心问题是数据一致性问题。 Redis 同时在server side和client side改动来支持客户端缓存,这里有两种模式: ###默认模式: 该模式需要server side记录key的信息,维护一个**Invalidation Table**来记录所有的track key,客户端获取key后,记录在该表中,以后一旦有其他客户端改动该key,就向源客户端推送失效信息。需要注意的是1.这种模式是链接会话级别的,断开链接后,不在track key.2.发送失效推送后,该key不在track,除非再次get key.该种模式可以看到占用了一定的server端内存,而且也只能收到一次变更信息。 ###广播模式: 广播模式更好理解,采用Pub/Sub模式,给所有关心该key 的客户端都推送变更通知,这种方式不用server 端记录状态信息,但是推送的端稍微更多一些,但是可以指定前缀来过滤推送的信息,在指定该前缀的情况下,server端显然需要存储**Prefixes Table**,所以在文档中指出,尽量要让这个prefix key 设计得小一点,不然将很损耗cpu 通过配置项tracking-table-max-keys可以设置**Invalidation Table**的大小,默认是1M,如果达到这个上限就开始删除之前没有修改过的key 参考: https://redis.io/topics/client-side-caching#two-connections-mode https://www.slideshare.net/RedisLabs/redisconf18-techniques-for-synchronizing-inmemory-caches-with-redis http://remcarpediem.net/article/e3e7a535/
内存泄露检测神器 作者: nbboy 时间: 2020-08-05 分类: 软件架构,软件工程 评论 ###Summary 用非GC语言比如c/c++写程序的时候,内存泄露是非常让人头疼的,一个服务运行一段时间后莫名下线。在测试环境,我们有很多检测内存的方法,valgrind就是一个很犀利的工具。今天花了一点时间学习,最常用的就是检测内存泄露,命令也很简单: ```shell valgrind --tool=memcheck --leak-check=full ./program_name ``` ###Refs [Linux下几款C++程序中的内存泄露检查工具](https://blog.csdn.net/u012662731/article/details/78652651 "Linux下几款C++程序中的内存泄露检查工具") [内存泄漏检测工具valgrind神器](https://zhuanlan.zhihu.com/p/75416381 "内存泄漏检测工具valgrind神器")
snowflake算法学习 作者: nbboy 时间: 2020-08-03 分类: 默认分类 评论 ###起源 内部IM项目需要标识不同消息的唯一性,我设计了一个ID值用来作为标识,关键在于这个值如何生成。该ID值需要满足以下需求: - 在高并发的时候也要保证多机唯一性 - 生成速度足够快,因为我们是IM系统,对实时性有需求 - 最好是数字的,这样排序可能更快一些 ###方案 查了下资料,Twitter 开源出一种ID生成算法,叫snowflake算法。学习了一下,其实也没什么新鲜东西,关键点也是可以想到的,就是时间戳+序列进行唯一性的保证。 SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序。 SnowFlake的算法是产生一个int64整型的ID,这段ID一共分为4部分,通过标示部分、时间戳、worker的Id、生成ID的序号来组合成为一个自增的ID,并且是全局唯一的。 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000  1. 1位标识部分 由于最高位是符号位,正数是0,负数是1,所以一般为0 1. 41位时间戳部分,这个是毫秒级别的的时间,但是使用的是当前时间减去系统开始时的差值,因为如果使用现在的值,会减少生成ID的唯一性, 1. 10位节点部分,前5位作为数据中心的标示,后5位作为机器标示,可以部分1024个节点 1. 12位序号,支持同一毫秒内同一个节点可以生成4096个ID; ###坑 网上也说了,这个算法就像其他基于time的算法一样,都不可避免的有一个漏洞,就是时间回拨问题,这个如何保证?其实在我这个项目中,因为简单原则,我们IM部署机器也不多,就几台,发号只需要集中在1,2台进行发号就可以。对于1,2台还是可以通过人工周期性同步或者脚本检测的方法来预防的。 最后,源码我放在GITHUB上了[Snowflake](https://github.com/x-debug/xfsnowflake.git "Snowflake"),欢迎读者来喷我!