真相只有一个

这几天看美剧 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,然后找证据,一个一个排除不可能,找到剩下唯一的一种可能性,那几乎就是问题所在。