一、前言
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,IPC_CREAT|0666),-1,"shmget failed!");
2、在图1中D上记录最新的共享内存的大小,attach之前把_len设成此值。
说明:
_len = 0 获取已存在的共享内存,不存在则失败
_len > 0 不存在则创建,存在则返回共享内存