背景
对ssdb进行性能测试,当跑以下测试用例时,惊奇发现set的qps跌到了百位数!
get-ssdb-bench没结束之前,切入set-ssdb-bench
对get命令测试,并发连接数是200
./get-ssdb-bench 127.0.0.1 8888 4000000 200
qps: 41877
对set命令测试,并发连接数是200
./set-ssdb-bench 127.0.0.1 8888 4000000 200
qps: 220.02
单独对set命令测试,set命令请求能达到40000+的qps。
分析
ssdb的代码优美,结构清晰。容易找到ssdb处理请求的核心逻辑(src/net/server.cpp)。
//只保留核心代码
void NetworkServer::serve(){
//ssdb 根据命令的读写属性,将其放到对应的处理队列。
//命令与队列的对应关系可查serv.cpp的reg_procs函数
//REG_PROC(get, "r");get命令是在此线程处理
//REG_PROC(set, "wt"); set命令异步处理处理
writer = new ProcWorkerPool("writer");
writer->start(num_writers);
reader = new ProcWorkerPool("reader");
reader->start(num_readers);
ready_list_t ready_list;
ready_list_t ready_list_2;
ready_list_t::iterator it;
const Fdevents::events_t *events;
...
while(!quit){
...
ready_list.swap(ready_list_2);
ready_list_2.clear();
if(!ready_list.empty()){
// ready_list not empty, so we should return immediately
events = fdes->wait(0);
}else{
events = fdes->wait(50);
}
if(events == NULL){
log_fatal("events.wait error: %s", strerror(errno));
break;
}
for(int i=0; i<(int)events->size(); i++){
const Fdevent *fde = events->at(i);
...
}else if(fde->data.ptr == this->reader || fde->data.ptr == this->writer){
//此逻辑很关键。
//当发送队列有消息待发送时,就会触发这个逻辑
ProcWorkerPool *worker = (ProcWorkerPool *)fde->data.ptr;
ProcJob job;
//注意!
//每个epoll loop从发送队列取出一条消息,发送响应。
if(worker->pop(&job) == 0){
log_fatal("reading result from workers error!");
exit(0);
}
if(proc_result(&job, &ready_list) == PROC_ERROR){
//
}
}else{
proc_client_event(fde, &ready_list);
}
}
for(it = ready_list.begin(); it != ready_list.end(); it ++){
...
ProcJob job;
job.link = link;
this->proc(&job);
if(job.result == PROC_THREAD){
fdes->del(link->fd());
continue;
}
if(job.result == PROC_BACKEND){
fdes->del(link->fd());
this->link_count --;
continue;
}
if(proc_result(&job, &ready_list_2) == PROC_ERROR){
//
}
} // end foreach ready link
}
}
//请求处理函数
void NetworkServer::proc(ProcJob *job){
...
if(cmd->flags & Command::FLAG_THREAD){ //如果命令需要放到线程中执行
if(cmd->flags & Command::FLAG_WRITE){ //如果命令是写命令
job->result = PROC_THREAD;
writer->push(*job); //set请求会放到这个处理队列
}else{ //如果命令是读命令。
job->result = PROC_THREAD;
reader->push(*job);//get命令的处理逻辑不走这里
}
return;
}
proc_t p = cmd->proc;
job->time_wait = 1000 * (millitime() - job->stime);
job->result = (*p)(this, job->link, *req, &resp);//get请求会就地处理
job->time_proc = 1000 * (millitime() - job->stime) - job->time_wait;
...
}
从上述代码可以获得几个重要信息:
- get请求在epoll loop就地处理,set请求异步处理。
- 每次epoll loop 从set处理队列取出一条响应消息,发送。
那么当有400个get并发连接时,会出现什么情况?
一个set请求过来,放到异步队列,接下来整个线程要处理400个get请求。就算set请求处理非常快,也要等下一个epoll loop才有机会把消息发送出去。
因此,上面bench出现的低qps是因为,处理完set请求-> 发送消息,这之间有较大的延迟。
解决方法
最简单有效的方法就是让get不要在epoll线程处理。
方法很简单,在(src/serv.cpp) 把 REG_PROC(get, “r”) 改为 REG_PROC(get, “rt”)。
再跑bench
./get-ssdb-bench 127.0.0.1 8888 4000000 200
qps: 41877
./set-ssdb-bench 127.0.0.1 8888 4000000 200
qps: 23454
PS,给ssdb的作者提issue,希望在项目代码中做这个修改。(Edit in 2015-05-19, 作者已采纳修改)
Q&A
- 增加set请求的并发连接数能改善吗?
不能。因为每次epoll loop 都只会从set处理队列取一条响应消息,并且ssdb默认的处理写请求的线程只有一条。因此无补于事,甚至会造成更大的延迟。
此问题在一般的bench里面暴露不出来,希望这篇文章能够帮助大家。