redis服务的一些注意事项
- server.client_max_querybuf_len redis:写入时,如果数据长度大于该配置,则redis会关闭并释放链接,该参数默认为1G。
- server.maxidletime:在redis的定时器时间中,会对每个客户端的最近一次请求时间和当前时间进行比较,如果超过了该参数,则认为idle过长时间,redis会关闭并释放链接。该参数的限制会使一些执行时间过长的命令出现问题,特别像multi、exec命令,有可能执行时间过长,出现相关问题。
- server.client_obuf_limits:在redis给client发送数据的过程中,会根据该参数判断发送的数据量是否过大,如果满足如下条件,redis会关闭并释放链接。
1)hard_limit_bytes不为0,且发送的数据大于hard_limit_bytes。
2) soft_limit_bytes不为0,且发送的数据量大于soft_limit_bytes,同时,持续时间超过soft_limit_seconds。 - redis 3.x的bug:迁移时会将不存在过期时间的记录设置过期时间,导致数据丢失。
void migrateCommand(client *c) {
... ...
long long ttl, expireat;
for (j = 0; j < num_keys; j++) {
expireat = getExpire(c->db,kv[j]);
if (expireat != -1) {
ttl = expireat-mstime();
if (ttl < 1) ttl = 1;
}
... ...
}
}
如上所示,ttl在for循环外设置,如果某个key没有设置ttl,则在循环中迁移该key的时候会使用上一个获得的ttl。
- hiredis客户端中的限制:redisAppendCommandArgv函数实现中存在2G限制的限制。
int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) {
int totlen, j;
totlen = 1+intlen(argc)+2;
for (j = 0; j < argc; j++) {
len = argvlen ? argvlen[j] : strlen(argv[j]);
totlen += bulklen(len);
}
/* Build the command at protocol level */
cmd = malloc(totlen+1);
... ...
}
超过2G的长度的时候,totlen可能为负数,而malloc函数参数为uint32_t,因此可能会分配一个超大的内存导致客户端程序异常。
- redis cluster集群的异常恢复
备节点挂掉然后重启的场景下,如果待同步的数据超过master节点环形缓冲区管理的buffer后,slave节点会启动全量同步,但是svale节点启动加入集群后,集群马上对外暴露slave节点可读,如果客户端读取slave节点,则容易出现大量数据读取异常,如果启动后slave节点为数据增量同步,则也会有数据读取异常的影响,只是影响比全量同步的影响小。
基于以上的分析,线上运营时一般会配置slave节点为冷备节点,平常不承载读,只会承载master的同步写,这样,相同的并发下,redis cluster需要的节点数会需要更多,带来集群规模的成倍增加,相应的,资源成本也会成倍增加。
redis cluster数据同步
数据全同步
数据全同步分析:
- 通过replicationSetMaster函数设置了自己的master节点后,在定时执行的replicationCron函数中会判断server.repl_state是否为REPL_STATE_CONNECT,如果是,则会调用connectWithMaster函数, 在该函数中为该fd添加事件处理函数syncWithMaster。
- 当步骤1添加事件后,当有事件发生时,会触发syncWithMaster回调函数,在syncWithMaster函数中,会依次发送一系列的命令,如PING、AUTH、REPLCONF。
上述命令发送ok,并且master节点正常响应后,会调用slaveTryPartialResynchronization函数,在该函数中会发送PSYNC命令,同时,会在发送的PSYNC命令中带上psync_runid跟psync_offset参数。
如果有server.cached_master(slave节点调用freeClient函数释放链接时,会判断释放的是否为跟master节点的链接,如果是,则会调用replicationCacheMaster函数保存server.cached_master,并且重置server.repl_state为REPL_STATE_CONNECT),则会将psync_runid和psync_offset分别设置为server.cached_master中的replrunid跟reploff+1,如果没有server.cached_master(比如第一次连上master),则会将psync_runid和psync_offset分别设置为”?”跟”-1”。
当master节点收到slave节点发过来的PSYNC命令后,会调用syncCommand函数,在syncCommand函数中,判断命令为psync时,会调用函数masterTryPartialResynchronization,在函数masterTryPartialResynchronization中,会判断slave发送的psync命令中带的runid是否跟master节点本身的runid一致,如果一致,则会判断slave发送过来的psync_offset是否在当前master节点的repl_backlog范围之内,如果不在当前repl_backlog范围之内,则会进入全同步流程。
master节点在启动的时候就会调用getRandomHexChars函数得到一个随机字符串设置为自己的server.runid,master节点判断slave节点需要FULLRESYNC时,master节点会将自己的server.runid和server.master_repl_offset发送给slave,slave收到FULLRESYNC响应时,会将server.repl_master_runid和server.repl_master_initial_offset分别设置为master发送过来的runid和offset,slave节点在调用replicationCreateMasterClient函数时,又会将server.master的reploff和replrunid分别设置为server.repl_master_initial_offset和server.repl_master_runid。
在master节点的全量同步的处理中,如果当前没有在dump rdb文件,则会调用startBgsaveForReplication函数fork一个进程去dump rdb文件,在serverCron函数中会判断dump rdb文件是否完成,如果完成了dump rdb文件,则会调用updateSlavesWaitingBgsave函数为每个slave注册AE_WRITABLE的网络事件,如果AE_WRITABLE事件触发,则调用sendBulkToSlave函数将rdb文件发送给slave。当发送完成后,会删除原有注册的AE_WRITABLE网络事件,重新为slave注册AE_WRITABLE网络事件和sendReplyToClient回调函数,注册该回调函数后,会将master的修改操作异步发送给slave节点。
数据部分同步
数据部分同步分析:
- 进入部分同步之前的流程与全同步流程步骤1 ~ 4类似,不同的是在步骤4中,master节点收到slave节点发过来的命令后,如果判断slave发送过来的命令中的psync_offset在当前master节点的repl_backlog范围之内,那么则会进入部分同步流程。
进入部分同步流程后,会调用addReplyReplicationBacklog函数将repl_backlog中从psync_offset开始的后续所有修改操作的数据发送给slave节点。
- repl_backlog_size: repl_backlog的大小。
- master_repl_offset: repl_backlog全局范围的last pos。
- repl_backlog_off:repl_backlog在全局pos中的start pos,repl_backlog_off = master_repl_offset - repl_backlog_histlen + 1
- repl_backlog_idx:repl_backlog在repl_backlog_size范围的last pos,这个是循环的,范围在0 ~ repl_backlog_size - 1。
- repl_backlog_histlen: repl_backlog的数据已写入大小,范围在0 ~ repl_backlog_size。
addReplyReplicationBacklog执行时,会先通过psync_offset - repl_backlog_off计算得到repl_backlog中要跳过同步的数据大小skip。
- 计算要同步的内容在repl_backlog_size范围的start pos的变量j, j = ( repl_backlog_idx + (repl_backlog_size - repl_backlog_histlen) )% repl_backlog_size。
- 对j进行取余处理, j = (j + skip) % repl_backlog_size,然后,计算要同步的数据长度len = repl_backlog_histlen - skip。
- 最后,将repl_backlog中从j的pos处开始同步后续所有数据到slave。
while(len) { long long thislen = ((server.repl_backlog_size - j) < len) ? (server.repl_backlog_size - j) : len; serverLog(LL_DEBUG, "[PSYNC] addReply() length: %lld", thislen); addReplySds(c,sdsnewlen(server.repl_backlog + j, thislen)); len -= thislen; j = 0; }
主备复制
redis cluster采用的是异步复制,尽可能保证最终一致性,一致性较弱,因此,有可能出现一些极端异常场景,比如master接受到请求处理成功,返回给客户端执行成功,但是master未将要同步的数据发送给slave就挂了,然后slave主备切换为master,但是该条记录已经丢失,给业务层的感知即为数据丢失。
主备复制分析:
- 主节点在accept slave的连接后,会为该连接注册AE_WRITABLE网络写事件和对应的回调函数sendReplyToClient,当slave需要全同步时,会先删除之前注册的事件和回调函数,然后重新注册AE_WRITABLE写事件和对应的回调函数sendBulkToSlave,sendBulkToSlave回调函数将rdb文件同步到slave后,会删除注册的AE_WRITABLE网络事和对应的回调函数,然后调用putSlaveOnline重新为该slave的连接注册AE_WRITABLE事件和对应的回调函数sendReplyToClient。
- 客户端发送更新请求到master节点后, 会触发master节点的网络读事件,进而调用回调函数readQueryFromClient,读取请求后,会调用processCommand函数执行对应的命令更新mater节点的内存数据。
- master节点更新完本机的内存后,会调用propagate函数,并在函数中调用feedAppendOnlyFile函数将更新请求写入到aof文件和调用replicationFeedSlaves函数将更新请求写入到repl_backlog,同时,replicationFeedSlaves还会将更新请求同步到slave。
- 这里需要注意的是,实际主备同步并不是拿的repl_backlog中的数据进行同步的,而是在master节点中直接将更新操作拷贝到slave。
- slave节点收到master节点的更新操作的同步请求后,会调用readQueryFromClient函数读取请求,读取请求后,会将该slave与master的连接对应的reploffset增加读取的请求长度。
if (c->flags & CLIENT_MASTER) c->reploff += nread;
集群管理
Failover
- 集群所有节点启动后,同时通过自带ruby脚本给每节点都发送了meet节点消息中第一个节点的消息后,所有节点的server.cluster->nodes包含两个节点:节点本身、meet消息带过来的节点。
- clusterCron函数定期执行,与本节点已知的其他所有节点握手、发送心跳消息,每一次握手、心跳消息,都会附带已知的10%节点的状态信息。其他节点收到消息后,会更新或新增节点的状态信息。
- clusterCron函数除了定期与其他节点握手或心跳之外,还会根据本节点已知的状态信息处理备节点强制Failover、本节点Failover、设置集群分布。
redis cluster的Failover采用的是raft算法的多数派投票策略。
1. 投票请求只会发送给master节点,如果是slave节点或本节点本身没有承载slots,则节点不会发送投票响应消息。
2. 发起投票的节点的epoch号必要要大于投票节点,如果小于投票节点的epoch号,则投票节点不会发送投票响应。
3. 如果本节点已经投过票,则不会再给其他节点投票。
4. 如果发起投票节点为master,或者发起投票节点为slave,但是投票节点发现它的master为NULL,或者发现它的master节点没有fail,且force_ack为0,则不会发送响应。
5. 投票节点两次投票的时间间隔必须要大于2 * cluster_node_timeout。
6. 发起投票节点的epoch号必须要大于投票节点上承载的每一个slot的epoch号。如果投票节点判断满足上述所有条件,则会发送跳票响应消息给发起投票节点,发起投票节点判断满足大多数条件,则会投票通过,执行Failover。
持久化
rdb持久化
触发条件
initServerConfig()初始化触发条件
appendServerSaveParams(60*60,1); /* save after 1 hour and 1 change */ appendServerSaveParams(300,100); /* save after 5 minutes and 100 changes */ appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... ... for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) { rdbSaveBackground(server.rdb_filename); break; } } ... ... }
flushall命令调用flushallCommand,清空整个数据库的数据,同时,将rdb文件也清空。
void flushallCommand(client *c) {
signalFlushedDb(-1);
server.dirty += emptyDb(NULL);
addReply(c,shared.ok);
if (server.rdb_child_pid != -1) {
kill(server.rdb_child_pid,SIGUSR1);
rdbRemoveTempFile(server.rdb_child_pid);
}
if (server.saveparamslen > 0) {
int saved_dirty = server.dirty;
rdbSave(server.rdb_filename);
server.dirty = saved_dirty;
}
server.dirty++;
}
手动执行save命令,调用saveCommand()函数,此时服务端会阻塞, 直到rdb文件save完成。
void saveCommand(client *c) {
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
if (rdbSave(server.rdb_filename) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
prepareForShutdown触发
int prepareForShutdown(int flags) {
... ...
if ((server.saveparamslen > 0 && !nosave) || save) {
if (rdbSave(server.rdb_filename) != C_OK) {
... ...
return C_ERR;
}
}
... ...
}
手动执行bgsave命令,调用bgsaveCommand函数触发。
void bgsaveCommand(client *c) {
... ...
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
if (schedule) {
server.rdb_bgsave_scheduled = 1;
} else {
... ...
}
} else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
serverCron定时检查
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.rdb_bgsave_scheduled &&
(server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
if (rdbSaveBackground(server.rdb_filename) == C_OK)
server.rdb_bgsave_scheduled = 0;
}
slave全量同步master节点的数据,触发执行startBgsaveForReplication()函数。
void syncCommand(client *c) { ... ... startBgsaveForReplication(c->slave_capa); ... ... }
void updateSlavesWaitingBgsave(int bgsaveerr, int type) { ... ... if (startbgsave) startBgsaveForReplication(mincapa); }
void replicationCron(void) { ... ... startBgsaveForReplication(mincapa); }
aof持久化
propagate函数调用feedAppendOnlyFile函数,将更新操作命令添加到aof_buf中,后续处理中,会调用flushAppendOnlyFile函数将aof_buf中内容写入到aof文件中。
flushAppendOnlyFile调用时机
stopAppendOnly触发
void stopAppendOnly(void) {
... ...
flushAppendOnlyFile(1);
... ...
}
serverCron定时处理
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... ... if (server.aof_flush_postponed_start) flushAppendOnlyFile(0); run_with_period(1000) { if (server.aof_last_write_status == C_ERR) flushAppendOnlyFile(0); } ... ... }
如果是AOF_FSYNC_EVERYSEC刷盘方式,则会创建后台任务,由后台线程bioProcessBackgroundJobs执行。
void flushAppendOnlyFile(int force) { ... ... if (server.aof_fsync == AOF_FSYNC_ALWAYS) { aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */ server.aof_last_fsync = server.unixtime; } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC && server.unixtime > server.aof_last_fsync)) { if (!sync_in_progress) aof_background_fsync(server.aof_fd); server.aof_last_fsync = server.unixtime; } } void aof_background_fsync(int fd) { bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL); }
在flushAppendOnlyFile函数写入aof_buf中数据时,发现AOF_FSYNC_EVERYSEC类型的fsync未将上次需要fsync的操作执行,则会设置aof_flush_postponed_start,并且,不是强制write数据到aof的话,如果持续未fsync的时间少于2s的情况下,不会写入数据到aof文件。持续时间超过2s,为避免过多数据堆积,会write数据到aof文件。
void flushAppendOnlyFile(int force) { if (server.aof_fsync == AOF_FSYNC_EVERYSEC) sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0; if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) { if (sync_in_progress) { if (server.aof_flush_postponed_start == 0) { server.aof_flush_postponed_start = server.unixtime; return; } else if (server.unixtime - server.aof_flush_postponed_start < 2) { return; } server.aof_delayed_fsync++; } } ... ... }
beforesleep中定期触发
aeSetBeforeSleepProc(server.el,beforeSleep);
void beforeSleep(struct aeEventLoop *eventLoop) {
... ...
flushAppendOnlyFile(0);
... ...
}
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
rewrite机制
rewrite触发条件
bgrewriteaof命令
void bgrewriteaofCommand(client *c) { if (server.aof_child_pid != -1) { addReplyError(c,"Background append only file rewriting already in progress"); } else if (server.rdb_child_pid != -1) { server.aof_rewrite_scheduled = 1; addReplyStatus(c,"Background append only file rewriting scheduled"); } else if (rewriteAppendOnlyFileBackground() == C_OK) { addReplyStatus(c,"Background append only file rewriting started"); } else { addReply(c,shared.err); } }
- 如果aof_child_pid != -1,即rewrite进程正在进行aof rewrite,则直接返回。
- 如果rdb_child_pid != -1,即rdb进程正在进行rdb dump,则设置aof_rewrite_scheduled = 1,后续在serverCron中定时触发。
- 如果rewrite进程、rewrite进程均没有,则直接进行rewrite,并且设置aof_child_pid。
serverCron定时判断,aof_rewrite_scheduled触发
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
serverCron中定时判断,容量触发
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_perc && server.aof_current_size > server.aof_rewrite_min_size) { long long base = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1; long long growth = (server.aof_current_size*100/base) - 100; if (growth >= server.aof_rewrite_perc) { ... ... rewriteAppendOnlyFileBackground(); } }
void flushAppendOnlyFile(int force) { ... ... server.aof_current_size += nwritten; ... ... }
startAppendOnly
void configSetCommand(client *c) { ... ... config_set_special_field("appendonly") { int enable = yesnotoi(o->ptr); if (enable == -1) goto badfmt; if (enable == 0 && server.aof_state != AOF_OFF) { stopAppendOnly(); } else if (enable && server.aof_state == AOF_OFF) { if (startAppendOnly() == C_ERR) { return; } } } ... ... }
void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) { ... ... if (eof_reached) { if (server.aof_state != AOF_OFF) { int retry = 10; stopAppendOnly(); while (retry-- && startAppendOnly() == C_ERR) { sleep(1); } if (!retry) { exit(1); } } } ... ... }
startAppendOnly()
一般调用前会调用stopAppendOnly()函数,startAppendOnly()函数主要的处理是调用rewriteAppendOnlyFileBackground()函数将内存中全量数据以命令方式写入到aof文件,并且会将此过程中的其他变更命令也写入到aof文件,最后,后续所有的变更操作全部追加写入到aof文件中。
打开aof_filename文件,作为rewriteAppendOnlyFileBackground()函数启动子进程后,父进程写入更新命令的文件。
server.aof_last_fsync = server.unixtime; server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
调用函数rewriteAppendOnlyFileBackground()。
if (server.rdb_child_pid != -1) { server.aof_rewrite_scheduled = 1; } else if (rewriteAppendOnlyFileBackground() == C_ERR) { close(server.aof_fd); return C_ERR; }
rewriteAppendOnlyFileBackgroud()函数启动子进程dump命令文件
int rewriteAppendOnlyFileBackground(void) { ... ... if (aofCreatePipes() != C_OK) return C_ERR; if ((childpid = fork()) == 0) { ... ... snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid()); if (rewriteAppendOnlyFile(tmpfile) == C_OK) { ... ... exitFromChild(0); } else { exitFromChild(1); } } else { /* Parent */ ... ... server.aof_child_pid = childpid; ... ... return C_OK; } return C_OK; /* unreached */ }
rewriteAppendOnlyFileBackgroud()启动子进程dump命令文件时,此时,父进程如果有更新操作,会通过管道的方式发送给子进程,子进程收到管道中的命令后,会先读取,当连续读取超过1s时或者连续20ms管道中没有数据时,子进程会发送”!”给父进程,父进程收到后会停止写入管道,此时,父进程会将更新命令放在aof_rewrite_buf_blocks里,当父进程发现子进程结束时,会调用aofRewriteBufferWrite()函数将aof_rewrite_buf_blocks中的数据写入到命令文件中,并且发送消息通知子进程,子进程收到通知后,会将父进程最后写入管道的数据写入命令文件。