引子
“啊,哇去!”,这是来自今天大胖司机的一声惊叹。
“呵,你都6年的老司机了,怎么还不知道避开井盖坑呢?”副驾驶座的小马调侃道。
“哎呀,没想到这井盖这么深……”,大胖心痛不已。
原来是马路井盖遭到损坏导致下陷,比平常的深度要高出好多。大胖想当然以为就是普通井盖,高速通过划了底盘,车损。这一幕人让小马想起了上次线上活动开发中踩到的致命坑,“摔”得现在膝盖都疼。
这一切还得从那天下午说起……
壹号坑:swoole协程的禁令
那日下午气温稍沉,闷不透风。小马刷着自己开发的活动页面,正悠然地摇着扇子,抿着温茶,忽觉惠风和畅,信可乐也。老大告诉他,这次的活动颇为重要,要充分重视,而且PV500万级。但小马毕竟是5年的PHP老程序员了,多多少少见过场面。此次用上了比PHP性能大约高出4倍的swoole协程,语法遵循PHP语法,而且功能测试又是一把过的,自然胸有成竹。
有了它PHP号称全宇宙最好的语言
窗外一声雷鸣打破了宁静。工作群里闪烁着活动代码上线后的第一次消息。
“@小马同学,前端反馈部分用户已陆续出现请求失败……”。
“@小马同学,报警日志触发了,coredump……”。
天哪,本次可是重要的活动啊。小马倒吸一口冷气,扔下手中的枸杞茶,打开了日志,迅速锁定了罪魁祸首。可在那一瞬间他却一脸愕然。
出现问题的代码是:在redis.class.php的类文件中有这样一个函数__call,如果调用的是本类中未定义的方法直接调用PHP的redis的方法。正是call_user_func_array函数导致coredump。如图。
使用PHP函数call_user_func_array
有经验的攻城狮都能看出,这个处理看起来并没什么不妥呀,而且call_user_func_array函数在很多框架代码中出场率蛮高的。案情简直扑朔迷离,小马一头雾水,各方求教,“翻山越岭”,最终在wiki官方文档找到了答案……
官方如是说:
swoole wiki文档
原来在swoole 中调用call_user_func_array,是有可能发生协程中数据错乱的。小马自然不会放过任何蛛丝马迹,文中提到在4.0版本后已解决此问题,排查线上版本,正是4.0版本,却仍然存在问题。小马眉头紧锁。
于是在类文件将所有用到的redis方法函数手动重写,以避开调用call_user_func_array,问题得以解决。如图。
手动重写redis方法
但后面的实践证明,有些函数自动调用call_user_func_array却并没有导致coredump。其实小马不仅使用了redis的setTimeout()方法还同时使用了setnx(),但并没有对setnx()方法手动重写,也就意味着其调用了call_user_func_array函数,奇怪的是并没有导致coredump。
总结陈词
编程就像驾驶一样,要仔细观察谨慎驾驶,不能想当然,否则轻则车损重则致命伤害。
总算解决完问题,工作群里渐渐恢复平静,静得能听得清窗外的雨声。小马默默地打开了笔记本飞快敲打着文字……
官方文档如是说:在ZendVM中魔术方法、反射函数、call_user_func、call_user_func_array是由C函数实现的,并未opcode,这些操作可能会与Swoole底层的协程调度发生冲突。因此严禁在这些地方使用协程的API。请使用PHP提供的动态函数调用语法来实现相同的功能。
想起老大的那句话:“我认为其实swoole就是底层C,然后套上PHP的壳,让PHP能够用上协程等特性。”目前来看果然善存缺陷,而且这无疑是个大坑,因为使用swoole编程的友军潜意识里认为语法就是和PHP一致的,自然就像写PHP一样写swoole。但我认为这不是导致这次掉坑的主要因素,如果自己没有疏忽于压测,估计问题早已能提前暴露。一定要吸取这次教训。
沉思
小马搁笔,桌上的枸杞茶依旧冒着热气,他擦了擦额头的汗水,抬头望向了远方,眼神里若有所思……