从技术难题中学习

之前写过一篇如何学习一门技术的文章,介绍了我在学习新技术的一些经验。不过这种按计划的学习方式效率可能并不高,真正高效的学习方式是在解决问题中实践所学的知识。作为工程技术人员,在项目中遇到问题的概率是高的,这给了我们机会去不断提升自己的能力边界。

某天项目上生产系统突然出现了一个性能问题,监控不断的产生某个API响应时间超出阈值的告警。经过短暂的排查,发现是我们这个API在处理时请求的另外一个服务下线导致的响应时间大幅增长。这个API在处理时会发起多个异步的请求去获取需要的资源,看起来是这个HTTP客户端在异步请求无响应时会让整个API响应的时间大幅度增加。

难题的成因

一般的问题可能需要我们半天的时间就能得到解决,但难题的出场总让人一脸困惑。为什么会这样?一般有以下的原因:

  • 有限的时间;
  • 复杂的环境;
  • 知识的盲区;

大多难题是伴随上述三个原因而出现的。如果时间是无限的,我自然可以慢慢去学习研究,消灭知识盲区,把复杂的环境搞清楚,问题自然也会得到解决;如果环境很简单,定位问题就很容易,那么问题也就不难;如果我对问题的解集不存在知识盲区,我当然知道问题的产生的技术原因,自然也能解决掉它。

而解决难题的方法就诞生于对这三个原因的逐个攻破。

初步分析后,我们发现这个问题发生的原因很奇怪。因为对HTTP客户端做超时时间的限制了,也就是所有的异步请求都不应该超出这个时间限制,但实际上API的响应时间增长了很多。这个问题的复杂之处在于:整个API在跑在一个Nginx Worker进程上的JVM的一个线程里,这个线程又会调用一个线程池做I/O任务的处理,整个代码又是Clojure语言开发的。因为我们的服务是公共服务,这个问题是因集群中某个服务下线触发的一个性能问题,而服务的下线又是一个很可能发生的事情,所以时间也有限制。整个难题的出现完全满足环境复杂、知识盲区与时间限制。

设置目标

难题之所以难,是因为需在对此知识有盲区的前提下,在有限的时间内解决。在对难题解决的第一步是确定时间的限制,如果问题很紧急,那我们需要先找到一个方法把紧急的时间限制转变为一个较宽松的。比如可以先把线上出问题的版本回滚,或采取更容易驾驭的技术方案,或制定新的发布计划等。

在将难题的时间限制放宽松后,难题依旧可能有复杂的环境和知识的盲区,而这两个要素会使问题难以解决,最终影响整个项目的正常交付开发。所以在这一步要给自己或团队设置一个阶段性的目标,比如花费一定的时间将难题所处复杂的环境转变为在简单的环境可复现问题,如果在这个时间段依旧解决不了问题,就转而去寻求外部的帮助。这样可以将问题的影响限制到预期范围内。

由于是线上出现的问题,我们的代码并没有做过修改,是因为所依赖的某个服务下线导致的突发问题。这时候只能先把这个下线的服务临时再次上线让线上环境先正常运行。这时候我们有了充足的时间来解决这个问题了。

确定问题边界

确定难题的边界是解决复杂环境要素的第一步。因为难题的根因和表象距离可能很远,从表象去分析问题很可能会浪费时间。这时候需要通过一些方法去找到问题的边界。

由于是线上出现的问题,第一步要做的就是在线下或非生产环境重现这个问题。幸运的是这个问题在线下得到了重现,如果不能重现那就是一个更难的问题了。但无法解释的还是为什么HTTP客户端异步请求的超时时间没有生效。难道是最终阻塞等待的函数有问题?也可能是底层多线程池的问题?或者是这个库在某种特定的场景下会出现这个Bug?

分析难题

对难题初步的分析可能会引发多个方向的猜测,而这些猜测很多都是错误的,如果一个个试错,成本太高。这时候需要利用已有的经验去分析问题,无论是个人或团队都可以通过头脑风暴去分析难题,寻找矛盾。在这个过程中要大胆假设,小心求证,也需要对习以为常的经验抱有怀疑态度,因为一不小心就掉入知识盲区的陷阱里了。

在经过一段时间的讨论分析之后,我们尝试过一些方法。比如我们测试发现HTTP客户端在同步请求模式下超时时间设置是正确的,无论请求的Socket链接是否能正常建立,都可以在规定的超时时间生效,那就可以在外部通过多线程的方式用这个HTTP客户端的同步请求模式。因为Clojure的core.async库支持类似Go语言的CSP并发模型,于是我们测试了这种方案,甚至上线了一个版本。最终又在线上发现这个版本会导致底层Nginx的Worker进程异常退出,看起来陷入了另一个知识盲区里了。经过搜索后发现core.async库使用了协程的方式,但它并不会像Go语言的运行时一样将阻塞的I/O请求使用I/O多路复用来处理,所以无法在它的Go任务块里执行阻塞I/O请求。最终经历了一圈尝试,发现问题依旧没有得到解决。

找出矛盾

找出问题的矛盾点是解决知识盲区这个要素有力的武器。任何问题都有矛盾点,这个矛盾点就是问题的根因。如何找出矛盾?在复杂环境中寻找矛盾会受到很多干扰因素的影响,所以需要通过简化环境的办法寻找矛盾。比如通过在本地搭建一个最简的环境来模拟问题产生的环境,也可以将复杂的功能或组件通过删除或注销的方式停止。

既然core.async的方案行不通,甚至陷入了新的知识盲区,那就是我们对业务系统所使用的Nginx-Clojure框架并不了解,看起来要解决这个问题,不仅要考虑业务代码使用的库,还需要考虑其所运行的Nginx环境。在查阅了其官方文档后,发现Nginx-Clojure已经考虑到这个问题了,甚至提供了多种解决方案,我们选择了其中的协程的方案。并在本地环境配置好了,但更奇怪的问题出现了,按照官方的指导配置,实际压测的结果却非常的差。这个结果一度让我们很迷茫,看着这个star不多的库,想到在配置的过程中遇到很多按文档操作不成功的例子,只能通过issue和源码的方式去找可用的方式。难道只能推倒重来,重写解决问题?

为了进一步排除可能是知识盲区导致的问题,我们又用Go语言重写了一个Goroutine的版本做压测的基准对比,也与业务系统的其他API做对比,结果显示这个协程版本的压测结果都差到难以接受。

看来只能寻求外部帮助了。

寻求帮助

以上都是用已有的经验去诊断未知的问题,如果运气不好或问题无法被上面的方法所锁定,那我们只能去寻求外部帮助了。外部帮助是解决知识盲区有效的办法,前提是能找的正确的知识或人。

从文档中找答案

最基本的知识盲区是我们不了解所用的技术,解决这类盲区可以通过查阅官方文档,最起码要了解所用技术最基本的用法,这样就算找人问也可以清楚的描述问题。如果文档极其的多,那可以利用一定的搜索技巧,比如尝试关联的关键词去搜索相关的文档。

从源码中找答案

当文档不准确或者过于简单,只能通过阅读相关的源码来了解一些底层的实现。阅读源码是一种很好的提高自己技术能力的方式,这也是难题给我们创造了一种可能不得不读源码来解决问题的场景。从这个角度看,难题是能提高我们对底层框架实现的了解。如何更好的读代码,可以阅读这篇文章:

为了确定是否是HTTP客户端的影响,我走读了clj-http的源码,发现它在异步请求模式下底层封装了HttpAsyncClients。于是写了一个Java版本做对比,发现存在相同的问题,很可能是因为异步模式下需要做很多重量级的初始化工作导致的问题。于是给提了一个issue,但没有得到回复。

提问的智慧

当文档和源码都无法告诉我们答案时,唯一的办法是去咨询更厉害的人。当然找人帮助,需要一些好的提问技巧。

从周围人寻求帮助

当周围人有相关的经验时,最简单是找这些人帮忙。但在找之前,准备好必要的上下文(如果是公司项目,需考虑脱敏),必要时可以画一些流程图或技术图,这样可以更高效的获取有效的帮助。

从互联网中寻求帮助

在互联网上寻求帮助,要想免费得到答案,需要一些提问的智慧。至少也需要在问题中详细的描述所做的尝试及相关的环境、测试等信息,这些都能帮助其他人快速的了解到我们的问题。

最终我在做了很多测试后,不得不给Nginx-Clojure提了一个issue。这个issue也得到了作者的回复及最终解决问题的办法。这个难题得到了最终的答案。

记录并分享

在解决难题的过程中,我们学习了很多未知的知识,甚至做了很多技术尝试。这些过程如果被记录下来,那会是不错的笔记。如果能写成文章,不仅能帮助他人,也能收获影响力。这种学习方式被称之为Learn In Public

从解决难题中成长

解决难题是一种非常高效的主动学习方式。难题是一面镜子,能反映我们的知识盲区。在解决难题的过程中,不仅可以解决知识盲区,还能提升解决问题的能力,甚至能得到更厉害的人的帮助,也有可能收获影响力。

因解决某个“难题”而给K8S容器镜像构建工具Kaniko提的PR

因解决某个“难题”而给K8S容器镜像构建工具Kaniko提的PR

更新时间: 21个月前 版本: 8171f612c