真相只有一个
这几天看美剧 Criminal Minds,主角引用了福尔摩斯的一句话:
When you have eliminated the impossible, whatever remains, however improbable, must be the truth?
引子
柯南道尔藉福尔摩斯一遍又一遍地陈述这句话。
让我们先来质疑下这句话:
- 假设消除了不可能,没有任何留下呢?
对于这种情况,需要我们对初始的假设池足够大。 大到包含了最终的可能性。
- 假设完全无法消除不可能呢?
这需要我们有足够的证据和推理能力。 经验也许很重要:经验教给我们一些判定的原则。
- 消除错了呢?
福尔摩斯自负地通过 I never make exceptions. An exception disproves the rule.
来排除这种可能性。
然而现实中时常会有推导出错的情况。
另外,这要求我们证据足够精准。
问题
终上,这句话必须在以下约束存在时才能成立:
- 有足够的,大量的推测
- 有足够的, 精准的证据
- 有足够的,正确的推理能力
否则,我们将无法导出最后的结论。
现实生活中有大量的无法导出最后结论的例子:
- 因为没有想到并发场景,找不到bug
- 因为记不清一些细节,找不到钥匙
- 因为瞎猜,像个无头苍蝇乱撞,这里调调,那里调调,一直没有找到错的地方
示例
找筷子
早上,家鹅说为什么我们家的筷子都不见了? 我快速列出了原因:
- 丢了,所以不见了
- 在别处,她没看到
考虑到昨晚吃饭看running man后直接离开了桌子,猜测筷子仍然在桌上。
查看了下,果然在。问题解决。
301 跳转
早上,项目上线,开放首页。发现首页老是跳到后台,而非想要的页面。 推测原因:
- 如果是 301 Permanent Redirection. 那肯定是 Nginx 配置了跳转,因为我们的应用里面目前没有写过 301的跳转。
- 如果是 302,那肯定是应用代码在某个节点配置了跳转。
打开 Chrome Inspector,查看是哪种跳转。发现是 301 跳转。 去服务器看下 Nginx 的配置,果然发现了 location / 的配置。去除后解决问题。
supervisor 未重载
应用上线,但是还是在执行老代码的逻辑。 推测原因:
- 应用上线失败
- 应用上线成功,但发布了老代码
- 应用上线成功,发布了新代码,但应用未重启
- 应用上线成功,发布了新代码,应用也重启了,但是重启的命令有问题
上述原因需要我们依次做排查
- 看上线日志是否有问题?
- 看最终部署目录的代码是否正确?
- 看应用日志打点是否正确
- 开应用Shell排查是否正确
- 看supervisor配置是否正确
一步一步排查下来,竟然发现都对。 可能性都被排除没了,那就只剩一种可能性,还有更多原因不在推测原因里面。
将部署流程分解地更细,分析从敲下命令到最终成功应该成功的事件,逐个排除不可能,最后发现:
- supervisor 配置变更后应该 reload,否则无法应用变更。
解决:运行 supervisorctl myservice reload
,解决问题。
以此为引子,还有更多部署细节需要调整。
并发 worker
celery worker 老是报错 MySQL connection gone away.
在寻找通用方案无果的情况下,静下心,仔细推敲从 worker 启动到 worker 死亡期间会发生的每件事情。
流程分析:
- supervisor 加载配置
- celery worker 启动
- 找到 celery application 所在模块
- 执行整个模块,并得到 celery application
- celery prefork 出几个子进程
- 子进程获得父进程的所有数据
- celery worker subprocess 等待 queue job
- celery worker subprocess 得到 queue job
- celery worker subprocess 从连接池获取数据库连接
- celery worker subprocess 执行业务代码
- celery worker subprocess 将数据库连接丢回连接池并设为 job.done
- celery worker subprocess 连接池周期性地关闭数据库连接
证据:
- 总共开了4个worker,一段时间后后同时报错3个。
- …
证据总是很多的,在注意到上述证据后,我们假定是 celery worker subprocess 数据库连接池是会相互影响的。这可以解释一个 subprocess 回收了数据库连接,另外三个 subprocess 也被回收了。
而相互影响说明他们很可能共享了一个连接池。
在流程分析中,他们共享的时机应该是子进程获得父进程的所有数据
。
这说明了执行整个模块时,数据库连接池已经创建。
这个假定通过阅读代码得到了证实。那么对应的解决方案就是在子进程 fork 之后在执行应用的初始化。
而celery 提供了 worker_process_init
这个钩子注册 fork 之后的逻辑,问题迎刃而解。
在熟悉代码仓库的情况下,整个思考过程在阅读代码前,都可以使用纸和笔。在进入这个思考过程后,只花了大约1个番茄的时间找到问题。而未进入这个思考过程前,各种乱调试,既无任何斩获,还浪费了整整8颗番茄的时间。
总结
在问题发生时,对证据掌握的越全,对推测越精准,找到问题也就越快。
所以我们解决问题时,首先应该做的事情就是做 Profiling,然后找证据,一个一个排除不可能,找到剩下唯一的一种可能性,那几乎就是问题所在。