在实际工作中,我需要使用redis的客户端去连接redis,于是选择了hiredis客户端(公司强推)。hiRedis是Redis官方指定的C语言客户端开发包,支持Redis完整的命令集、管线以及事件驱动编程。
1、情景描述
1.1使用场景
一个epool模型的服务器不断接受外界请求,这个服务器框架给用户预留一个回调函数(多线程),回调函数为用户自己去实现的业务逻辑,其中redis的使用就需要在这个回调函数内部实现。
1.2初步实现方案
在程序启动的时候,我就初始化redis的连接,获得hiredis句柄。然后把hiredis句柄传入到线程函数里面。让其做相应的业务逻辑。
1.3结果
很不幸,一次请求都没问题,做压力测试,同时开20个线程访问,程序立即出core。
线上出core如下:
02 | #00x000000302af2e2edinraise()from/lib64/tls/libc.so.6 |
03 | #10x000000302af2fa3einabort()from/lib64/tls/libc.so.6 |
04 | #20x000000302af62db1in__libc_message()from/lib64/tls/libc.so.6 |
05 | #30x000000302af6888ein_int_free()from/lib64/tls/libc.so.6 |
06 | #40x000000302af6a12din_int_realloc()from/lib64/tls/libc.so.6 |
07 | #50x000000302af6b39cinrealloc()from/lib64/tls/libc.so.6 |
08 | #60x0000000000dc2269insdscatlen(s=Variable"s"isnotavailable. |
10 | #70x0000000000dc1d40in__redisAppendCommand(c=0x16fa1d0,cmd=Variable"cmd"isnotavailable. |
12 | #80x0000000000dc1d97inredisvAppendCommand(c=0x16fa1d0,format=Variable"format"isnotavailable. |
14 | #90x0000000000dc1eedinredisvCommand(c=0x16fa1d0,format=Variable"format"isnotavailable. |
16 | #100x0000000000dc1fb6inredisCommand(c=Variable"c"isnotavailable. |
18 | #110x0000002b1a8e6310inDefault_Handler::get_batch_redis(this=0x1ff41f0,redis_ins=0x175a7d0,dataid=6202,buf_num=12,res_num=6,key_sign=0x2bd67cb3c8, |
19 | res_lens=0x2bd5f54208,res_buf=0x2bd5f54398"")atdefault_handler.cpp:659 |
20 | #120x0000002b1a9134dfinDefault_Ms_Handler::get_digest(this=0x1ff41f0)atdefault_ms_handler.cpp:646 |
21 | #130x000000000092910cindo_proc()atgss_work.cpp:1107 |
22 | #140x000000000091c91finthread_main()atgss_net.cpp:188 |
23 | #150x0000000000bc10e9indefault_native()atubserver_app.cpp:283 |
24 | #160x0000000000bbc676ineppool_consume(pool=0x2230b90,data=0x22188f0)ateppool.cpp:649 |
25 | #170x0000000000bbc4d1in_eppool_workers(param=0x22188f0)ateppool.cpp:604 |
26 | #180x000000302b80610ainstart_thread()from/lib64/tls/libpthread.so.0 |
27 | #190x000000302afc6003inclone()from/lib64/tls/libc.so.6 |
28 | #200x0000000000000000in??() |
当时经过多次尝试。把连接放入到了每个线程中。那么就不会出core了。
2、线下复现
因为不方便公开公司代码,所以我写一个类似的代码来复现这个case。
2.1代码
代码主要有testredis.cpp和Makefile(自己指定hiredis目录)。用法是./redis-n[num]-h[host]-p[port],n为host数目,多个host用"-"进行分割。
testredis.cpp
001 | /*************************************************************************** |
003 | *Copyright(c)2014Baidu.com,Inc.AllRightsReserved |
005 | **************************************************************************/ |
011 | *@authorliujun05(com@baidu.com) |
012 | *@date2014/02/2510:28:44 |
026 | #defineuint32unsignedint |
029 | #defineMAX_REDIS_SERVER_CNT10 |
030 | #defineMAX_REDIS_IPS1024 |
032 | typedefstruct_redis_conf_t |
035 | charredis_ips[MAX_REDIS_IPS]; |
036 | charredis_ip_array[MAX_REDIS_SERVER_CNT][MAX_REDIS_IPS]; |
041 | typedefstruct_redis_data_t |
044 | redisContext*rc[MAX_REDIS_SERVER_CNT]; |
052 | printf("usage:./redis-n[num]-h[host]-p[port]\n"); |
056 | intmain_parse_option(intargc,char**argv) |
059 | //reset获取参数的位置,多次调用时这个会出现问题 |
060 | while((c=getopt(argc,argv,"h:p:n:"))!=-1) |
065 | sprintf(g_cfg.redis_ips,optarg); |
068 | g_cfg.redis_port=atoi(optarg); |
071 | g_cfg.redis_num=atoi(optarg); |
082 | void*test_thread1(void*data) |
084 | redis_data*redis_ins=(redis_data*)data; |
086 | for(inti=0;i<redis_ins->redis_num;i++) |
088 | reply=(redisReply*)redisCommand(redis_ins->rc[i],"SET%s%s","foo","helloworld"); |
089 | freeReplyObject(reply); |
096 | structtimevaltimeout={1,500000};//1.5seconds |
099 | char*part=strtok_r(g_cfg.redis_ips,"-",&ptok); |
103 | strcpy(g_cfg.redis_ip_array[num++],part); |
104 | part=strtok_r(NULL,"-",&ptok); |
107 | if(num!=g_cfg.redis_num||num>MAX_REDIS_SERVER_CNT) |
109 | printf("ipnum[%d]notequalredis_num[%d]ornotvaild\n",num,g_cfg.redis_num); |
112 | g_data.redis_num=(num>MAX_REDIS_SERVER_CNT)?MAX_REDIS_SERVER_CNT:num; |
115 | for(i=0;i<g_data.redis_num;i++) |
117 | g_data.rc[i]=redisConnectWithTimeout(g_cfg.redis_ip_array[i],g_cfg.redis_port,timeout); |
118 | if(g_data.rc[i]==NULL||g_data.rc[i]->err) |
120 | printf("contenttoredisserver[%s:%u],error[%s]\n", |
121 | g_cfg.redis_ip_array[i],g_cfg.redis_port,g_data.rc[i]->errstr |
131 | if(g_data.rc[j]!=NULL) |
133 | redisFree(g_data.rc[j]); |
142 | for(intj=0;j<g_data.redis_num;j++) |
144 | if(g_data.rc[j]!=NULL) |
146 | redisFree(g_data.rc[j]); |
151 | intmain(intargc,char**argv) |
153 | g_cfg.redis_ips[0]='\0'; |
154 | g_cfg.redis_port=6379; |
156 | if(0!=main_parse_option(argc,argv)) |
162 | if(0==g_cfg.redis_num||g_cfg.redis_num>MAX_REDIS_SERVER_CNT) |
164 | printf("thereidsnum[%u]isnotvaild\n",g_cfg.redis_num); |
172 | printf("initnumfail\n"); |
180 | pthread_create(&t[i],NULL,test_thread1,&g_data); |
185 | pthread_join(t[i],NULL); |
194 | /*vim:setexpandtabts=4sw=4sts=4tw=100:*/ |
Makefile
2 | g++-gtestredis.cpp-I./hiredis-L./hiredis-lhiredis-lpthread-oredis |
2.2编译执行
1 | liujun05@cq01-rdqa-dev012.cq01:~/test/hiredis$./redis-n2-h10.48.46.26-10.46.175.102 |
2 | ***glibcdetected***doublefreeorcorruption(!prev):0x000000000050aa80*** |
可以看到出core了
02 | #00x000000302af2e2edinraise()from/lib64/tls/libc.so.6 |
03 | #10x000000302af2fa3einabort()from/lib64/tls/libc.so.6 |
04 | #20x000000302af62db1in__libc_message()from/lib64/tls/libc.so.6 |
05 | #30x000000302af6888ein_int_free()from/lib64/tls/libc.so.6 |
06 | #40x000000302af68bd6infree()from/lib64/tls/libc.so.6 |
07 | #50x0000000000403c75inredisBufferWrite(c=0x50a010,done=0x571c008c)athiredis.c:1162 |
08 | #60x0000000000403d3einredisGetReply(c=0x50a010,reply=0x571c00b8)athiredis.c:1195 |
09 | #70x0000000000403f62inredisvCommand(c=0x50a010,format=Variable"format"isnotavailable. |
11 | #80x0000000000404006inredisCommand(c=Variable"c"isnotavailable. |
13 | #90x00000000004013e7intest_thread1(data=0x509ba0)attestredis.cpp:88 |
14 | #100x000000302b80610ainstart_thread()from/lib64/tls/libpthread.so.0 |
15 | #110x000000302afc6003inclone()from/lib64/tls/libc.so.6 |
16 | #120x0000000000000000in??() |
虽然出core位置不一致,但是经过查看代码,出core的原因应该是一致的。
2.3原因分析
从堆栈5可以看到hiredis.c的1162行出的core,打开hiredis.c
1 | 1160}elseif(nwritten>0){ |
2 | 1161if(nwritten==(signed)sdslen(c->obuf)){ |
6 | 1165c->obuf=sdsrange(c->obuf,nwritten,-1); |
可以看到的确在1152行对c->obuf进行了一次free导致出core。
我们分析下调用关系,首先调用redisCommand.
1 | 1309void*redisCommand(redisContext*c,constchar*format,...){ |
4 | 1312va_start(ap,format); |
5 | 1313reply=redisvCommand(c,format,ap); |
然后调用redisvCommand
1 | 1303void*redisvCommand(redisContext*c,constchar*format,va_listap){ |
2 | 1304if(redisvAppendCommand(c,format,ap)!=REDIS_OK) |
4 | 1306return__redisBlockForReply(c); |
接着调用redisvAppendCommand
01 | <span></span>1233intredisvAppendCommand(redisContext*c,constchar*format,va_listap){ |
05 | 1237len=redisvFormatCommand(&cmd,format,ap); |
07 | 1239__redisSetError(c,REDIS_ERR_OOM,"Outofmemory"); |
11 | 1243if(__redisAppendCommand(c,cmd,len)!=REDIS_OK){ |
这里,我们需要care调用__redisAppendCommand.
01 | 1220int__redisAppendCommand(redisContext*c,char*cmd,size_tlen){ |
04 | 1223newbuf=sdscatlen(c->obuf,cmd,len); |
06 | 1225__redisSetError(c,REDIS_ERR_OOM,"Outofmemory"); |
问题出现了。
对于任意一个多线程,他传入的redisContext*c都是一个,那么他们也公用同一个c->obuf,这里很明显,线程数据是耦合的。
当一个线程调用sdsfreec->obuf,其他任意一个线程使用c->obuf都会导致出core.这也是我所谓的hiredis对多线程支持的不好的地方。
3.终极解决方案
那么,如果我一定要在多线程中通过hiredis客户端调用redis呢。有没有方案了,答案肯定是有,只不过性能稍差。
原先的做法是先获得hiredis连接句柄,然后把句柄传入到多线程中,让多线程使用。现在改成在线程里面连接获得hiredis句柄,然后再进行使用。当然,代价是对于每个请求,都需要去连接redis服务器,加大了网络开销的同时还加大了redis的请求。
redis是单线程异步模型,hiredis这个客户端看来也只支持单线程。希望后续有redis的相关程序猿来改进相应问题,在hiredis使用多线程需要慎重。