一、前言
ipquery是一个用于根据ip查询对应信息(地址、天气等)的php模块,基于共享内存实现,为了做到更新数据时不重启php,我们引入了数据动态加载概念。如下图1设计:
(图1)
在调用查询接口时,php进程会首先访问共享内存D,取出存储在D中shmkey,然后再去访问shmkey表示的内容,热加载的过程就是当数据有更新时,重新申请一块共享内存,把数据加载到这块内存中,然后把D中的内容改成New data的shmkey,当IPQuery接口被调用时,如果取出的shmkey跟旧的shmkey不同,php进程就会dattach Old data, attach New data, 之后就可以访问到新的数据了。
二、问题
上线一段时间后出现了致命bug(期间应该使用了热加载程序),apache错误日志分析,报如下错误为:
terminate called after throwing an instance of 'std::runtime_error'
what(): appinfo: shmget failed!,errno:22 errmsg:Invalid argument
[Fri Feb 01 20:11:30 2013] [notice] child pid 10507 exit signal Aborted (6)
分析代码发现此错误出自源代码 :“_shmid = result::not_val<int>(shmget(_s_shm_key,_len,IPC_CREAT|0666),-1,"shmget failed!")”,错误码errno 为22,Invalid argument(非法参数),可以确定是attach 共享内存时报错。
man shmget :
shmget函数返回错误码22有两种原因:
a、创建size<SHMMIN or size>SHMMAX的共享内容;
b、指定key的共享内存存在,但是size大于已存在共享内存的大小。SHMMIN默认值为1,_len肯定是大于1的;
执行命令:
cat /proc/sys/kernel/shmmax
可以看到SHMMAX值远大于所申请的共享内存大小,所以错误只可能是最后一种:共享内存存在,但_len大于存在的共享内存的size。
三、调试分析
经过配合测试,客户端用siege一直打压,执行数据热加载数分钟后,问题重现了:
图中的nattch是共享内存当前被引用的次数。以下图2、3、4是连续几次执行ipcs的结果。
(图2)
(图3)
(图4)
图中key为0x00924660是每个httpd进程都要attach的共享内存,对应图1中的D,key为0x7c000237的是httpd子进程第一次处理请求时需要attach的共享内存。从图2和图3可以看出,key为0x00924660和key为0x7c000237的nattch在减少,但都不为0,图4中key为0x00924660的nattch值回升。但是key为0x7c000237的nattch值为0.
整个过程中,数据热加载执行的时间是[Fri Feb 01 20:06:38 2013],但error_log总最早出现错误时间为[Fri Feb 01 20:11:28 2013],结合图2-4也可以说明,数据热加载之前已存在的httpd子进程可以正常服务,也就是说数据热加载之前已存在的httpd子进程的数据源已成功切换到新的共享内存区,可以排除crash由数据源切换导致的疑虑,确定是由动态创建的httpd子进程造成的。但是不能确定是在子进程的创建过程中还是创建完之后处理请求过程中。
图4中key为0x7c000237的nattch值为0,而key为0x00924660的nattch值回升到522,结合apache错误日志可以知道:在出错过程中,动态创建httpd子进程一直在crash,httpd父进程也在不停地创建子进程,但赶不上crash的速度,直到全crash掉,客户端连不上服务器,siege退出,httpd子进程数量才回升至稳定,如果继续siege发请求,又会crash。由此确定crash发生在接口attach新共享内存时。
以上确定crash发生在httpd动态创建的子进程处理第一次请求过程,希望观察在处理请求过程中_len的变化,找出真正的真凶!于是用gdb在线上调试httpd,观察_len的变化。
# : sudo gdb httpd
# : (gdb) attach pid
# : (gdb) b _Z16space_ptr_updatev(attach和切换数据源的函数)
# : (gdb) c
客户端启动siege,当此进程运行到断点处时,会停在端点上
# : (gdb) p idx->shmkey
# : (gdb) $3 = 3472884279(0xcf000237) 可以看到idx->shmkey是正确的。
# ::(gdb) p_len
# : (gdb) $5 = 927305456
927305456是旧的共享内存的大小,找到crash的真正原因了:请求的数据大小比存在的共享内存大。
因为crash是由数据热加载引起的,所以在apache启动之后,多次执行热加载命令,加载不同大小的ip数据文件,然后gdb attach到未处理过请求的httpd子进程中观察_len值。多次测试后发现未处理过请求的httpd子进程中_len始终为apache启动时加载数据的大小。验证了apache动态创建子进程机制为: apache启动时,由父进程加载模块,以后动态创建子进程时fork自己,复制地址空间到子进程空间。
四、解决方案
1、在attach共享内存时把_len设置问题0:
_len = 0;
_shmid = result::not_val<int>(shmget(_s_shm_key_len, _len, IPC_CREAT|0666), -1, "shmget failed!");
2、在图1中D上记录最新的共享内存的大小,attach之前把_len设成此值。
说明:
_len = 0 获取已存在的共享内存,不存在则失败
_len > 0 不存在则创建,存在则返回共享内存