查看原文
其他

技术攻关:从零到精通

铁蕾 张铁蕾 2020-10-29

任何一位工程师都不可能了解所有领域的技术知识;任何一个团队也不可能包含所有类型的专业人才。而一个完整的产品被开发出来,或者一个系统被构建出来,这个过程都会用到种类繁多的技术,一般来说总会有一部分超出当前团队所能掌握的现有经验。这个矛盾怎么解决呢?这就需要工程师来进行技术攻关了。

没错,工程师的真正价值就是把未知变成已知的能力。

现在假设你的leader交给你一件你从来没接触过的任务,比如,它可能涉及到研究若干框架以及系统架构,优化某些算法,设计和实现某一类型的网络协议,对音视频进行处理,研究系统底层,甚至这个过程可能会涉及到一些复杂的数学知识。总之,这项任务对你来说有点复杂,你从来没有接触过,所以完全没有概念。

你之所以领到这样一项任务,首先,是因为项目在当前或者未来需要解决这样的技术问题,而团队中没有人有现成的经验能够应付。另一方面,你肯定是在以前的工作中表现出了过人的学习和研究能力,你的leader才敢把这个工作交给你。

我在上篇文章《马拉松式学习与技术人员的成长性》中提到过,按技术领域来划分,编程可以分为「一般性」和「专业性」两大类。作为一件需要进行技术攻关的任务来说,它有可能会涉及到一些「专业性」的领域了。我相信,每一位执着于技术的工程师,在他成长的过程中,总会碰到类似的经历,去挑战一些自己未知的东西。在这个过程中,既为团队解决了眼前的问题,也为自己打开了一片新的技术天地。这也是技术小白进阶到专业人才的必经之路。

今天,我就根据自己的经历和体验,说一说从零开始进行技术攻关的一系列过程,以及可能碰到怎样的一些问题。希望你看完能有共同的感受。


研究问题本身


有些情况下,你的leader没法告诉你具体应该做什么,他只是告诉你问题是怎样的,比如,视频播放总是卡顿,或者,用户总是说丢消息,再比如,用户反馈说搜东西的时候结果给的不准,打语音或视频电话总是接不通,总是有些图片访问不到,诸如此类。

我们第一步应该做的,就是先研究问题本身是怎么产生的。首先试图从用户的角度去理解问题,然后从技术的角度去了解现有的实现,包括细节。这时候,全局的视角变得非常重要,你如果既能理解服务器的逻辑,也能理解客户端的逻辑,那么解决问题的思路会大大开阔。有些“系统性”的问题,属于设计缺陷,并不是做局部改动就能解决的。这种问题,对于只接触客户端编程或只接触服务器开发的工程师,解决起来就有难度,他们考虑问题的思路容易被见到的东西限制住。所以,适时扩大自己的知识域,永远都有好处。

如果问题足够复杂,我们可能还需要增加跟踪日志,便于在出现特殊的问题时能够分析它发生的过程;并定义性能指标,对现有系统的总体状况有一个量化的度量。它们一方面有助于我们更深入地理解当前系统,另一方面也为后面的优化和重构过程提供了方向。

通过对问题本身的研究,我们知道了系统的瓶颈或问题症结在哪里。如果我们发现只有推翻现有系统完全重构才能根治问题,那么接下来就跟从头开始设计一个全新系统的过程一样了。


对专业领域的Overview


在接触一个新领域之前,对它进行一个总体的概览是很有必要的,这让我们对后面整个的精力投入做到心中有数。

这个阶段的目标是,花最短的时间快速了解相关的各个概念,不求深入理解,只求了解技术概况。所以,这个过程怎么快就怎么来,选择自己熟悉的方式。可以去你自己喜欢的技术社区搜索相关的文章,或者通过百度搜索一些概念。很多人不建议用百度来搜索技术问题,但了解一些概况还是没有问题的,不过要适可而止。等到真正需要系统地研究技术细节的时候,还是应该直接阅读更规范的资料(后面我们还会提到)。


Run起来,获得感性认识


在当今的技术条件下,几乎什么技术领域的问题都有开源的软件可以借鉴。如果针对我们要解决的问题能找到开源项目,那么非常幸运,我们探索的过程会大大缩短。

很多技术领域都存在众多抽象的概念,通过前面Overview的阶段,我们一般只能了解到这一层。而开源项目能帮助我们快速地将抽象的概念具体化,获得感性的认识。下载一份代码,编译通过,然后运行起一个简单的Demo,从API层面去理解它(内部的实现尚不是重点)。

很多情况下,开源的实现不止一个,这就面临一个选择的问题。人们对于开源项目的第一印象一般来源于项目的入门教程(tutorial),可见一份好的文档对于一个开源项目来说多么重要。根据我个人的经验,文档是否健全,也是选择开源项目的重要依据。

当然,在这个阶段,我们要解决的主要还不是选择开源项目的问题,而是要通过快速Run起一个相关的实例来达到对技术获得感性认识的目的。注意这种感性认识是技术层面的,是至少基于API层面的。我们经常看到,对于一些热门的技术领域,很多非技术人员也能略知一二,甚至对一些技术概念有所了解,但是,技术人员与非技术人员的区别,应该说,从这个阶段开始就有所不同了。


同行交流


能够Run起一个实现,并从API层面粗略地了解一项技术之后,在这个认识的基础上,我们就差不多可以找同行交流一下了。如果在我们正要涉足去研究的领域里,我们恰好认识一些这方面的专家,那么无疑是非常幸运的。逻辑清晰的技术高手,一般用不了几句话就能把某项技术的关键问题描述清楚了。从这种交流中,我们受益匪浅。

但要注意,我们一定要在对该项技术有所了解之后,再去找专业人士交流。否则这种交流建立在信息严重不对称的基础上,就是极其低效的。对该项技术的初步了解,也是让我们能问出真正有效的问题的基础条件。


研究Spec


我曾经写过一篇关于如何学习新技术的文章《技术的正宗与野路子》,在文中提到的一个重要的观点,就是一定要找到能称得上Spec的文档去阅读。所谓Spec,是集中体现该项技术的设计思想的东西,是高度抽象的描述,一般也是一份完备的、系统性的描述。它的存在形式有很多种,可能是一份官方文档,也可能是一份公开的技术标准,比如RFC或者W3C的规范,还可能是以论文的形式,甚至与其它技术资料混杂在一起。

总之,你应该设法识别出哪些文档是Spec,然后在需要的时候通读它们。有些涉及到抽象概念的技术,你不读通这么一份Spec,有可能后面是看不懂代码的。这确实是比较费力的一个过程,但也正是这个过程,才真正开启了从门外汉向技术专家迈进的征程。


研究和选择具体实现


假设我们找到了开源代码可供参考。前面我们已经能Run起来一些小的Demo了,并且基本通读了一份大而全的Spec,现在需要研究的就是再深入一层,看看这份Spec中的关键点是如何实现出来的。你前面已经花了很多时间来调研,这中间肯定产生了很多疑问,比如有些抽象的概念以及相似概念之间的联系还是难以理解,有些过程的实现初看起来并不是那么地显而易见,而现在就到了该解决它们的时候了。头脑中的疑问和关键点,要自己总结出来,然后在代码中去找到答案,这是把抽象概念最终落地的一个过程。

如果有多个开源的实现,那么就涉及到如何选择的问题。有很多因素需要考虑:

  • 文档是否健全。

  • 提供的特性能否满足要求。

  • API层面逻辑是否清晰。很多代码在你初步接触了API这一层之后就大概知道自己是不是喜欢它了。

  • 模块化和抽象层次是否足够好。这决定了你把这份开源代码集成到自己项目中的难易程度。

  • 是否仍然有人维护。你当然希望在提issue和pull request的时候有人能够响应。


研究相似产品的实现


有可能我们要实现的东西其他家的线上产品已经提供类似的功能了。我们有必要在实现自己的方案之前研究一下他们的做法(逆向工程),对比之后从而做出一个更优的实现。

具体怎么研究呢?两种常见的方式:一种是反解客户端的包,看看里面引用了什么,是不是在我们调研过的那些技术范围之内;另一种当然就是抓包,从网络通讯上猜测他们用了哪一类技术。


网上浏览最佳实践


经过前面的调研,我们基本上已经在头脑中产生了自己的方案了。但在真正实现它之前 ,我们一般还想做一件事,就是「循证」。

记得胡峰同学在他的微信公众号「瞬息之间」上,发过一篇文章《技术干货的选择性问题》,里面就提到了通过阅读技术文章来「循证」的做法。很多个人博主和团队博客会在网上发表他们自己系统的实现过程,以及系统前后版本的演进过程。如果我们恰好找到相关的类似这样的文章,那么它们就有很大的参考价值。我们从别人分享的技术方案中获得一个印证,确保自己的想法没有走向极端,或者漏掉了什么重要的东西。


结合自己的系统设计方案


对于复杂的系统,即使有开源的代码,通常也不能直接拿来就用。现有系统总有一些特殊性。这涉及到多种选择的可能性:

  1. 找到了一份代码扩展性很好的开源实现。这份代码有清晰的模块化和分层结构,我们不需要改动原来的代码,只需要补充自己的一部分实现,再加上一些胶水代码(),或者在原开源代码的基础上进行封装,就能把整个系统实现出来。这是最好的情况,工作量大大减少。

  2. 必须改动原来的开源代码,重新编译,才能实现自己的需求。这种情况一般来说比较糟糕,主要是日后的维护可能会成问题。一方面,我们产生了一份与原开源项目差别很大的代码分支,而且没法合并回开源项目;另一方面,开源代码通常要考虑更通用的一些应用场景,它涉及到的问题域可能远远大于我们要解决的。简单来说就是,开源代码中有大量的与我们的实际需求无关的代码,如果我们要改动这份代码,我们所需要掌握的信息要远远大于自己系统实际的要求。特别是在以后团队人员变动的时候,这份代码很可能变得没有人敢动。再就是,当原开源代码升级的时候,我们很难跟着升级。所以,如果是决定作出这种对开源代码进行私有方式的改动的话,请慎重,并留下足够的文档说明。

  3. 开源代码与我们的需求相差太远,或者找不到开源的实现,那么只能完全自己实现了。还有一个迫使我们重新实现的现实原因,可能是项目体积。如果我们想在客户端引用的话,一个太重的实现就是不太合适的。我们希望引入的东西尽量简单,体积小。

总之,这里的选择过程是比较痛苦的,因为它对后面的实现工作以及日后的维护影响很大。具体如何选择,除了要考虑开源实现与当前系统的实际需求之间的匹配程度,还要考虑预期收益和项目预算(budget),你有多少时间去完成整个的事情。


系统实现的过程


现在进入很关键的实现阶段了。实际上,对于一个复杂的系统,在真正写代码之前,需要首先进行设计,系统架构的设计和软件接口的设计。这个过程非常重要,花费的时间和精力很可能超过代码编写的过程。这个过程逼迫你在真正实现之前就必须想清楚系统在各个层面上是如何运行的,确保不会实现到一半推翻重做。

首先,系统架构的设计,划分出组成系统的各个组件(各个独立的进程,通过网络进行交互)。有两个问题需要在设计时就重点考虑:一个是可扩展性;一个是容错性。可扩展性说的是,当流量逐渐变大的时候,你的系统如何扩展。系统中有些组件是无状态的,有些是有状态的。无状态的组件一般通过增加节点,应用简单的负载均衡策略就可以扩展;而有状态的组件需要明确扩展的方式。容错性说的是,系统应该主动处理失败情况,在设计中就应该考虑进去。比如,你想做到不丢消息,那么必须把网络丢包和处理异常的情况当做正常情况来考虑,设计重传机制;既然有了重传,就不得不考虑去重机制。容错性还包括,系统应该具有从错误状态中恢复的倾向。当然,系统架构的设计还有很多因素需要考虑,比如高可用、高性能、可维护,等等,我们这里就不展开说了。

其次,在更细的层面,要完成接口设计,也就是俗称的「面向接口编程」。这个过程的重要性怎么强调都不为过。我们都知道「面向接口编程」,在面试的时候也经常讨论设计模式,但实际中真正按这种方式工作的人少之又少。造成这种状况的原因可能是,我们平常做业务开发,在大部分情况下,都不用自己设计接口。比如做客户端开发,各种MVC, MVP模式已经把代码框架都定义好了,我们只用往接口实现里填东西。

但我们应该知道,当为一个新系统编写代码的时候,代码应该从接口设计开始。先用代码定义出各层的接口(包括回调接口),没有实现,只是能够编译通过。有了这些接口,就可以拿它们与同事进行非常细节的讨论了。应该先把接口讨论得足够清楚,再进行下一步的具体实现。这也是一个比较痛苦的过程,我们需要反复抉择,而通常「选择」就意味着痛苦。根据我个人的经验,设计接口代码的过程,一般都要前后改很多遍,才能达到令自己基本满意的程度。

接口设计的时候,要时刻考虑这两个问题:

  • 功能层次。也就是系统包括哪些接口,哪些功能放到哪些接口里面,不同的接口之间的关系如何。我们可能还需要画出类似UML(Unified Modeling Language)那样的类图。

  • 实例运行模型。系统运行起来之后,接口的各个实例的生命周期,以及各个实例之间的交互关系和数量关系,是一对一,还是一对多。虽然在编写接口代码的时候,还不要求写出实现,但是一个不错的实践方式是,写完接口,先生成一个空的实现类,然后把能表明实例引用关系的代码先写出来,再进一步把各个接口的实例创建代码写出来(解决了实现参数的注入问题),这样一个系统的「骨架」就出来了。

在编写和修改接口代码的过程中,还有几个问题值得考虑:

  • 是否引入响应式编程(Reactive Programming)的思想。实践证明,采用响应式编程和传统的回调方式(callback)设计出来的接口形式,存在很大的不同。

  • 给接口起名字非常重要(包括各个类名、方法名、参数名等)。起名字其实是个大问题,就像给自己的小孩起名字一样难!名字起的好不好,直接反映了通过系统抽象划分出来的各个角色是不是合理。

  • 能称得上「接口」的代码,从一开始编写就要有非常详细的代码注释。

  • 考虑线程模型。被上层调用的代码预期在哪些线程上执行,回调的代码又在哪些线程上执行。是多线程的环境,还是单线程的环境,这直接影响后面的实现应该怎么来做。一般来说,多线程的环境是存在一个线程池,这个线程池是外部调用者提供,还是接口内的实现来提供,这个要规定清楚。另外就是考虑能否把多线程问题规避,变成单线程的编程问题。比如,在有些异步编程的情况下,充分利用Java的Executor,或者Android开发中的Looper,或者RxJava之类的框架,就可能达到类似的目的。

最后,就是愉快地实现了。只要前面的过程做得比较细致,编写实现代码基本就是水到渠成了。在实现中有一个非常重要但容易被忽视的问题是——日志(log)。每个人都知道怎么打日志,但打一份好的日志,实际没有几个人能够做到的。一般来说,如果没有足够的重视,工程师打出来的日志,或者过于随意,或者逻辑缺失。一份好的日志其实要花很多精力来调整细节,把程序运行看成一个状态机,每一个关键的状态变化,都要在日志中记录。一份好的日志其实反映了一套好的程序逻辑。总之,打日志的目标是:如果线上发生奇怪的情况,拿过这份日志来就能分析出问题所在。这在客户端上分析线上问题的时候尤其有用。


前面讲到的各个过程并不是要严格按照这样的步骤进行,实际中有些过程要前后交叉,甚至反复进行。中间碰到问题,可能还要退回到前面的步骤,重新进行。

在时间、精力和项目进度各种条件允许的情况下,尽量把事情做到当前认为「最好」的状态。当然,如果由于客观条件,一时没有那么多时间去做到完美,也不要太过沮丧,后面能够持续优化才是最重要的。

进行一项技术攻关,从这个过程中学习新的东西,这是一个利用现有各种资源来学习和解决问题的过程。每一步并非必不可少。你参考的东西越多,做出一个差劲的实现的可能性就越小,但付出的精力也就越多。

关键的关键,当团队中出现现有经验无法解决的问题的时候,你能够站出来,勇于承担。这样,你的技术之路也会越来越宽广。

(完)

其它精选文章:

马拉松式学习与技术人员的成长性
蓄力十年,做一个成就
知识的三个层次
论人生之转折
技术的成长曲线
互联网风雨十年,我所经历的技术变迁
技术的正宗与野路子
程序员的宇宙时间线
基于Redis的分布式锁到底安全吗?

扫码或长按关注微信公众号:张铁蕾。


有时候写点技术干货,有时候写点有趣的文章。

这个公众号有点科幻。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存