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-06-18 分类: 默认分类,PHP 评论 ### 背景 公司的外网线上项目连续宕机将近2周时间,后来通过查找日志,发现是某度蜘蛛大量爬取造成。因为公司花钱进行了SEO推广,所以也不能直接禁掉蜘蛛,所以一度造成网站打不开的情况。期间增加带宽,扩容cpu,增强搜索引擎集群等方案,都得不到好的效果。 ### 分析 开始优化代码,记录慢日志,在云平台看慢查询,渐渐优化慢查询,但是问题依旧。慢慢通过分析慢日志注意到如下细节:  这里我摘取了两个日志片段,其实类似日志片段还有非常多。可以看到将近相同的时间不同的许多进程都进行了GC,而这里的所谓GC是什么? 在StartSession.php上看到了如下代码: ```php /** * Remove the garbage from the session if necessary. * * @param \Illuminate\Session\SessionInterface $session * @return void */ protected function collectGarbage(SessionInterface $session) { $config = $this->manager->getSessionConfig(); // Here we will see if this request hits the garbage collection lottery by hitting // the odds needed to perform garbage collection on any given request. If we do // hit it, we'll call this handler to let it delete all the expired sessions. if ($this->configHitsLottery($config)) { $session->getHandler()->gc($this->getSessionLifetimeInSeconds()); } } ``` 这里注释的意思是有一定概率进行GC,并不是每次都进行GC,先来看下GC做了什么事情。 因为配置的是文件session,所以我们找到文件Session的实现文件FileSessionHandler.php,摘取GC函数代码如下: ```php /** * {@inheritdoc} */ public function gc($lifetime) { $files = Finder::create() ->in($this->path) ->files() ->ignoreDotFiles(true) ->date('<= now - '.$lifetime.' seconds'); foreach ($files as $file) { $this->files->delete($file->getRealPath()); } } ``` 代码的意思很简单,就是通过搜索文件,获取到过期的session文件然后进行删除,这里的过期时间在session.php的配置里定义: ```php /* |-------------------------------------------------------------------------- | Session Lifetime |-------------------------------------------------------------------------- | | Here you may specify the number of minutes that you wish the session | to be allowed to remain idle before it expires. If you want them | to immediately expire on the browser closing, set that option. | */ 'lifetime' => 120, 'expire_on_close' => true, ``` 可以看到默认是2个小时,下面一个参数的意义是是否要在浏览器关闭的时候立即过期!刚才说到了,有一定概率执行这个GC,那这个概率多大呢? ```php /** * Determine if the configuration odds hit the lottery. * * @param array $config * @return bool */ protected function configHitsLottery(array $config) { return random_int(1, 100) <= 2; } ``` 上面代码是我把默认配置替换后的结果,也就是会有2%的可能会触发一次GC!而一次GC我们要回收两个小时之前的过期session文件。如果在并发量稍微有点大的情况下,要删除的文件其实是非常可观的,删除这个IO操作就是直接导致了cpu负荷直接超载的罪魁祸首。 ### 解决方案 方法很简单,就是换driver,比如集中式的redis session方案。 ### 总结 虽然这次项目的主要负责人不是我,不过通过这次抓取日志,跟踪代码,分析代码学到了很多优化的一手资料。Laravel其实是一个相当灵活的框架,文件session方式只能在测试环境进行应用,而在线上环境必须上集中式session方式,不然流量起来的时候,就会吃苦头。Laravel如不经过优化的情况下,他的性能是不高的,好在他也提供了很多可以优化的扩展接口,算的上是目前PHP里相当成熟的框架了。