一 序
前一阵,阿里云的专家来公司分享阿里云的自研数据库polarDB,号称性能是MySQL 的 6 倍,当然抛去一开介绍的硬件、网络的顶配外,主要是数据底层是分布式存储的,天然的适合多读。这些都没大规模商用,好吧,听到阿里云的专家提了一句,包括redis都有自研的版本,做了很多底层的优化,举例子是短连接优化跟AOF的类似mysql的binlog化。正好网上看到一篇介绍Redis短连接性能优化的文章。原文链接:https://yq.aliyun.com/articles/62593
读了觉得还是挺好的,补充一下相关的源码。
二 问题
对于Redis服务,通常我们推荐用户使用长连接来访问Redis,由于短连接每次需要新建链接所以短连接在tcp协议层面性能就比长连接低效,但是由于某些用户在连接池失效的时候还是会建立大量的短连接或者用户由于客户端限制还是只能使用短连接来访问Redis,而原生的Redis在频繁建立短连接的时候有一定性能缺陷。通过历史监控我们可以发现用户在频繁使用短连接的时候Redis的cpu使用率有显著的上升。
先说下监控,原文:“从扁鹊我们可以看到Redis在freeClient的时候会频繁调用listSearchKey,并且该函数占用了百分30左右的调用量,如果我们可以优化降低该调用,短连接性能将得到具体提升”。不知道怎么做的这个监控,但是这个监控效果还是很好的,能快速定位 到问题。不知道对于我们开发来说,阿里云是否能暴露更多的内部监控给外部用户。
网络连接库剖析(client的创建/释放、命令接收/回复等)之前的 文章整理过redis的networking.c的部分,Redis在释放链接的时候频繁调用了listSearchKey,这里查看Redis关闭客户端源码如下:
//释放客户端
void freeClient(client *c) {
listNode *ln;
/* If it is our master that's beging disconnected we should make sure
* to cache the state to try a partial resynchronization later.
*
* Note that before doing this we make sure that the client is not in
* some unexpected state, by checking its flags. */
// 如果client,已经连接主机,可能需要保存主机状态信息,以便进行 PSYNC
if (server.master && c->flags & CLIENT_MASTER) {
serverLog(LL_WARNING,"Connection with master lost.");
if (!(c->flags & (CLIENT_CLOSE_AFTER_REPLY|
CLIENT_CLOSE_ASAP|
CLIENT_BLOCKED|
CLIENT_UNBLOCKED)))
{ //处理master主机断开连接(要缓存client,可以迅速重新启用恢复,不用整体从头建立连接)
replicationCacheMaster(c);
return;
}
}
/* Log link disconnection with slave */
//打印log 与slave 链接断开
if ((c->flags & CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)) {
serverLog(LL_WARNING,"Connection with slave %s lost.",
replicationGetSlaveName(c));
}
/* Free the query buffer */
//清空输入缓冲区
sdsfree(c->querybuf);
c->querybuf = NULL;
/* Deallocate structures used to block on blocking ops. */
if (c->flags & CLIENT_BLOCKED) unblockClient(c);//解开阻塞
dictRelease(c->bpop.keys);//释放关于阻塞的字典空间
/* UNWATCH all the keys */
// 清空 WATCH 信息
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);//释放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. */
//移除所有的引用(会关闭socket,从事件循环中移除对该client的监听)
unlinkClient(c);
/* Master/slave cleanup Case 1:
* we lost the connection with a slave. */
//从服务器的客户端断开连接
if (c->flags & CLIENT_SLAVE) {
// 如果当前服务器的复制状态为:正在发送RDB文件给从节点
if (c->replstate == SLAVE_STATE_SEND_BULK) {
// 关闭用于保存主服务器发送RDB文件的文件描述符
if (c->repldbfd != -1) close(c->repldbfd);
// 释放RDB文件的字符串形式的大小
if (c->replpreamble) sdsfree(c->replpreamble);
}
// 获取保存当前client的链表地址,监控器链表或从节点链表
list *l = (c->flags & CLIENT_MONITOR) ? server.monitors : server.slaves;
// 取出保存client的节点
ln = listSearchKey(l,c);
serverAssert(ln != NULL);
// 删除该client
listDelNode(l,ln);
/* We need to remember the time when we started to have zero
* attached slaves, as after some time we'll free the replication
* backlog. */
// 服务器从节点链表为空,要保存当前时间
if (c->flags & CLIENT_SLAVE && listLength(server.slaves) == 0)
server.repl_no_slaves_since = server.unixtime;
refreshGoodSlavesCount();//更新存活的slave数目
}
/* Master/slave cleanup Case 2:
* we lost the connection with the master. */
//如果是一个主服务器的客户端断开连接
if (c->flags & CLIENT_MASTER) replicationHandleMasterDisconnection();
/* If this client was scheduled for async freeing we need to remove it
* from the queue. */
// 如果client即将关闭,则从clients_to_close中找到并删除
if (c->flags & CLIENT_CLOSE_ASAP) {
ln = listSearchKey(server.clients_to_close,c);
serverAssert(ln != NULL);
listDelNode(server.clients_to_close,ln);
}
/* Release other dynamically allocated client structure fields,
* and finally release the client structure itself. */
// 如果client有名字,则释放
if (c->name) decrRefCount(c->name);
zfree(c->argv);// 释放参数列表
freeClientMultiState(c);// 清除事物状态信息
sdsfree(c->peerid);
zfree(c);// 释放客户端 redisClient 结构本身
}
看到Redis在释放链接的时候遍历server.clients查找到对应的redisClient对象然后调用listDelNode把该redisClient对象从server.clients删除。查看server.clients为List结构,而redis定义的List为双端链表。
优化方法:我们可以在createClient的时候将redisClient的指针地址保留再freeClient的时候直接删除对应的listNode即可,无需再次遍历server.clients
三 优化代码对比
3.1 createClient修改
原来:源码在networking.c
//创建一个客户端
client *createClient(int fd) {
...
if (fd != -1) listAddNodeTail(server.clients,c);
// 初始化客户端的事务状态
initClientMultiState(c);
return c;
}
修改为:
...
if (fd != -1) c->client_list_node = listAddNodeTailReturnNode(server.clients,c);
// 初始化客户端的事务状态
initClientMultiState(c);
return c;
}
只是改了一个方法,看下这个方法,原文没有贴代码,去github找了下:在adlist.c
/* Add a new node to the list, to tail, containing the specified 'value'
* pointer as value.
*
* On error, NULL is returned and no operation is performed (i.e. the
* list remains unaltered).
* On success the 'listNode' pointer point to the value you pass to the function is returned. */
listNode *listAddNodeTailReturnNode(list *list, void *value)
{
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
node->prev = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
list->len++;
return node;
}
替代了listAddNodeTail方法。
list *listAddNodeTail(list *list, void *value)
{
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
node->prev = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
list->len++;
return list;
}
对比了下,就是插在最后一行。实际上新函数就是在往 server.clients 这个链表里塞当前的 client 的时候,把位置指针也返回回来,并在当前的 client struct 里增加了新字段,用于存储这个返回的,在 server.clients 中的位置
server.h 里面
typedef struct client {
...
listNode *client_list_node; /* list node in client list */
...
}
3.2 freeClient修改
原来
void unlinkClient(client *c) {
* fd is already set to -1. */
if (c->fd != -1) {
/* Remove from the list of active clients. */
ln = listSearchKey(server.clients,c);
serverAssert(ln != NULL);
listDelNode(server.clients,ln);
/* Unregister async I/O handlers and close the socket. */
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
....
}
修改为:
void unlinkClient(client *c) {
* fd is already set to -1. */
if (c->fd != -1) {
/* Remove from the list of clients */
if (c->client_list_node) {
listDelNode(server.clients,c->client_list_node);
c->client_list_node = NULL;
}
/* Unregister async I/O handlers and close the socket. */
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
...
}
逻辑很简单:那我 client struct 里只要创建时存储了它在 clients 链表中的位置,删除时就不用去做这个链表遍历啦。这样在 unlinkClient 的时候,少了一步 listSearchKey(server.clients,c) 的操作。
再看看listSearchKey咋实现的,源码在adlist.c
listNode *listSearchKey(list *list, void *key)
{
listIter iter;
listNode *node;
listRewind(list, &iter);
while((node = listNext(&iter)) != NULL) {
if (list->match) {
if (list->match(node->value, key)) {
return node;
}
} else {
if (key == node->value) {
return node;
}
}
}
return NULL;
}
就是常见的线性查找。为啥阿里云说:“”我们可以看到优化之后的Redis-server性能在短连接多的场景下提升了百分30%以上。“ 我猜测是大量短连接,或者就是这个list有点长,找起来慢。
四 总结
看完了觉得改动不大,但是效果很好。阿里云还是很牛的。
1. 监控很好,能找到问题,找到问题才能进一步做改进优化啊。貌似是废话。
2. 虽然改动不大,但是不要小看了,因为我们大多数是单纯的get\set.看代码的不一定看明白,就算我之前看过这里,也不知道这里有问题,更别说去优化了,根本不是一个层次的。
大厂就厉害。膜拜下。
参考:
https://github.com/alibaba/ApsaraCache
https://yq.aliyun.com/articles/62593
http://xargin.com/apsaracache-diff-analysis/