作者介绍
张鹏义,腾讯云数据库高级工程师,曾参与华为Taurus分布式数据研发及腾讯CynosDB for pg研发工作,现从事腾讯云Redis数据库研发工作。
我们在使用Redis时,总会碰到一些redis-server端CPU及内存占用比较高的问题。下面以几个实际案例为例,来讨论一下在使用Redis时容易忽视的几种情形。
一、短连接导致CPU高
某用户反映QPS不高,从监控看CPU确实偏高。既然QPS不高,那么redis-server自身很可能在做某些清理工作或者用户在执行复杂度较高的命令,经排查无没有进行key过期删除操作,没有执行复杂度高的命令。
上机器对redis-server进行perf分析,发现函数listSearchKey占用CPU比较高,分析调用栈发现在释放连接时会频繁调用listSearchKey,且用户反馈说是使用的短连接,所以推断是频繁释放连接导致CPU占用有所升高。
1、对比实验
下面使用redis-benchmark工具分别使用长连接和短连接做一个对比实验,redis-server为社区版4.0.10。
1)长连接测试
使用10000个长连接向redis-server发送50w次ping命令:
./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 1(k=1表示使用长连接,k=0表示使用短连接)
最终QPS:
PING_INLINE: 92902.27 requests per second
PING_BULK: 93580.38 requests per second
对redis-server分析,发现占用CPU最高的是readQueryFromClient,即主要是在处理来自用户端的请求。
2)短连接测试
使用10000个短连接向redis-server发送50w次ping命令:
./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 0
最终QPS:
PING_INLINE: 15187.18 requests per second
PING_BULK: 16471.75 requests per second
对redis-server分析,发现占用CPU最高的确实是listSearchKey,而readQueryFromClient所占CPU的比例比listSearchKey要低得多,也就是说CPU有点“不务正业”了,处理用户请求变成了副业,而搜索list却成为了主业。所以在同样的业务请求量下,使用短连接会增加CPU的负担。
从QPS上看,短连接与长连接差距比较大,原因来自两方面:
- 每次重新建连接引入的网络开销。
- 释放连接时,redis-server需消耗额外的CPU周期做清理工作。(这一点可以尝试从redis-server端做优化)
2、Redis连接释放
我们从代码层面来看下redis-server在用户端发起连接释放后都会做哪些事情,redis-server在收到用户端的断连请求时会直接进入到freeClient。
void freeClient(client *c) {
listNode *ln;
/* .........*/
/* Free the query buffer */
sdsfree(c->querybuf);
sdsfree(c->pending_querybuf);
c->querybuf = ;
/* Deallocate structures used to block on blocking ops. */
if (c->flags & CLIENT_BLOCKED) unblockClient(c);
dictRelease(c->bpop.keys);
/* UNWATCH all the keys */
unwatchAllKeys(c);
listRelease(c->watched_keys);
/* Unsubscribe from all the pubsub channels */
pubsubUnsubscribeAllChannels(c,0);
pubsubUnsubscribeAllPatterns(c,0);
dictRelease(c->pubsub_channels);
listRelease(c->pubsub_patterns);
/* Free data structures. */
listRelease(c->reply);
freeClientArgv(c);
/* Unlink the client: this will close the socket, remove the I/O
* handlers, and remove references of the client from different
* places where active clients may be referenced. */
/* redis-server维护了一个server.clients链表,当用户端建立连接后,新建一个client对象并追加到server.clients上,
当连接释放时,需求从server.clients上删除client对象 */
unlinkClient(c);
/* ...........*/
}
void unlinkClient(client *c) {
listNode *ln;
/* If this is marked as current client unset it. */
if (server.current_client == c) server.current_client = ;
/* Certain operations must be done only if the client has an active socket.
* If the client was already unlinked or if it's a "fake client" the
* fd is already set to -1. */
if (c->fd != -1) {
/* 搜索server.clients链表,然后删除client节点对象,这里复杂为O(N) */
ln = listSearchKey(server.clients,c);
serverAssert(ln != );
listDelNode(server.clients,ln);
/* Unregister async I/O handlers and close the socket. */
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
close(c->fd);
c->fd = -1;
}
/* ......... */
所以在每次连接断开时,都存在一个O(N)的运算。对于redis这样的内存数据库,我们应该尽量避开O(N)运算,特别是在连接数比较大的场景下,对性能影响比较明显。虽然用户只要不使用短连接就能避免,但在实际的场景中,用户端连接池被打满后,用户也可能会建立一些短连接。
3、优化
从上面的分析看,每次连接释放时都会进行O(N)的运算,那能不能降复杂度降到O(1)呢?
这个问题非常简单,server.clients是个双向链表,只要当client对象在创建时记住自己的内存地址,释放时就不需要遍历server.clients。接下来尝试优化下:
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
/* ........ */
listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
if (fd != -1) {
/* client记录自身所在list的listNode地址 */
c->client_list_node = listAddNodeTailEx(server.clients,c);
}
initClientMultiState(c);
return c;
}
void unlinkClient(client *c) {
listNode *ln;
/* If this is marked as current client unset it. */
if (server.current_client == c) server.current_client = ;
/* Certain operations must be done only if the client has an active socket.
* If the client was already unlinked or if it's a "fake client" the
* fd is already set to -1. */
if (c->fd != -1) {
/* 这时不再需求搜索server.clients链表 */
//ln = listSearchKey(server.clients,c);
//serverAssert(ln != );
//listDelNode(server.clients,ln);
listDelNode(server.clients, c->client_list_node);
/* Unregister async I/O handlers and close the socket. */
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
close(c->fd);
c->fd = -1;
}
/* ......... */
优化后短连接测试
使用10000个短连接向redis-server发送50w次ping命令:
./redis-benchmark -h host -p port -t ping -c 10000 -n 500000 -k 0
最终QPS:
PING_INLINE: 21884.23 requests per second
PING_BULK: 21454.62 requests per second
与优化前相比,短连接性能能够提升30+%,所以能够保证存在短连接的情况下,性能不至于太差。
二、info命令导致CPU高
有用户通过定期执行info命令监视redis的状态,这会在一定程度上导致CPU占用偏高。频繁执行info时通过perf分析发现getClientsMaxBuffers、getClientOutputBufferMemoryUsage及getMemoryOverheadData这几个函数占用CPU比较高。
通过Info命令,可以拉取到redis-server端的如下一些状态信息(未列全):
client
connected_clients:1
client_longest_output_list:0 // redis-server端最长的outputbuffer列表长度
client_biggest_input_buf:0. // redis-server端最长的inputbuffer字节长度
blocked_clients:0
Memory
used_memory:848392
used_memory_human:828.51K
used_memory_rss:3620864
used_memory_rss_human:3.45M
used_memory_peak:619108296
used_memory_peak_human:590.43M
used_memory_peak_perc:0.14%
used_memory_overhead:836182 // 除dataset外,redis-server为维护自身结构所额外占用的内存量
used_memory_startup:786552
used_memory_dataset:12210
used_memory_dataset_perc:19.74%
为了得到client_longest_output_list、client_longest_output_list状态,需要遍历redis-server端所有的client, 如getClientsMaxBuffers所示,可能看到这里也是存在同样的O(N)运算。
void getClientsMaxBuffers(unsigned long *longest_output_list,
unsigned long *biggest_input_buffer) {
client *c;
listNode *ln;
listIter li;
unsigned long lol = 0, bib = 0;
/* 遍历所有client, 复杂度O(N) */
listRewind(server.clients,&li);
while ((ln = listNext(&li)) != ) {
c = listNodeValue(ln);
if (listLength(c->reply) > lol) lol = listLength(c->reply);
if (sdslen(c->querybuf) > bib) bib = sdslen(c->querybuf);
}
*longest_output_list = lol;
*biggest_input_buffer = bib;
}
为了得到used_memory_overhead状态,同样也需要遍历所有client计算所有client的outputBuffer所占用的内存总量,如getMemoryOverheadData所示:
struct redisMemOverhead *getMemoryOverheadData(void) {
/* ......... */
mem = 0;
if (server.repl_backlog)
mem += zmalloc_size(server.repl_backlog);
mh->repl_backlog = mem;
mem_total += mem;
/* ...............*/
mem = 0;
if (listLength(server.clients)) {
listIter li;
listNode *ln;
/* 遍历所有的client, 计算所有client outputBuffer占用的内存总和,复杂度为O(N) */
listRewind(server.clients,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (c->flags & CLIENT_SLAVE)
continue;
mem += getClientOutputBufferMemoryUsage(c);
mem += sdsAllocSize(c->querybuf);
mem += sizeof(client);
}
}
mh->clients_normal = mem;
mem_total+=mem;
mem = 0;
if (server.aof_state != AOF_OFF) {
mem += sdslen(server.aof_buf);
mem += aofRewriteBufferSize;
}
mh->aof_buffer = mem;
mem_total+=mem;
/* ......... */
return mh;
}
实验
从上面的分析知道,当连接数较高时(O(N)的N大),如果频率执行info命令,会占用较多CPU。
1)建立一个连接,不断执行info命令
func main {
c, err := redis.Dial("tcp