1. 程式人生 > >技術攻關:從零到精通

技術攻關:從零到精通

任何一位工程師都不可能瞭解所有領域的技術知識;任何一個團隊也不可能包含所有型別的專業人才。而一個完整的產品被開發出來,或者一個系統被構建出來,這個過程都會用到種類繁多的技術,一般來說總會有一部分超出當前團隊所能掌握的現有經驗。這個矛盾怎麼解決呢?這就需要工程師來進行技術攻關了。

沒錯,工程師的真正價值就是把未知變成已知的能力。

現在假設你的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. 找到了一份程式碼擴充套件性很好的開源實現。這份程式碼有清晰的模組化和分層結構,我們不需要改動原來的程式碼,只需要補充自己的一部分實現,再加上一些膠水程式碼(glue code),或者在原開原始碼的基礎上進行封裝,就能把整個系統實現出來。這是最好的情況,工作量大大減少。
  2. 必須改動原來的開原始碼,重新編譯,才能實現自己的需求。這種情況一般來說比較糟糕,主要是日後的維護可能會成問題。一方面,我們產生了一份與原開源專案差別很大的程式碼分支,而且沒法合併回開源專案;另一方面,開原始碼通常要考慮更通用的一些應用場景,它涉及到的問題域可能遠遠大於我們要解決的。簡單來說就是,開原始碼中有大量的與我們的實際需求無關的程式碼,如果我們要改動這份程式碼,我們所需要掌握的資訊要遠遠大於自己系統實際的要求。特別是在以後團隊人員變動的時候,這份程式碼很可能變得沒有人敢動。再就是,當原開原始碼升級的時候,我們很難跟著升級。所以,如果是決定作出這種對開原始碼進行私有方式的改動的話,請慎重,並留下足夠的文件說明。
  3. 開原始碼與我們的需求相差太遠,或者找不到開源的實現,那麼只能完全自己實現了。還有一個迫使我們重新實現的現實原因,可能是專案體積。如果我們想在客戶端引用的話,一個太重的實現就是不太合適的。我們希望引入的東西儘量簡單,體積小。

總之,這裡的選擇過程是比較痛苦的,因為它對後面的實現工作以及日後的維護影響很大。具體如何選擇,除了要考慮開源實現與當前系統的實際需求之間的匹配程度,還要考慮預期收益和專案預算(budget),你有多少時間去完成整個的事情。

系統實現的過程

現在進入很關鍵的實現階段了。實際上,對於一個複雜的系統,在真正寫程式碼之前,需要首先進行設計,系統架構的設計和軟體介面的設計。這個過程非常重要,花費的時間和精力很可能超過程式碼編寫的過程。這個過程逼迫你在真正實現之前就必須想清楚系統在各個層面上是如何執行的,確保不會實現到一半推翻重做。

首先,系統架構的設計,劃分出組成系統的各個元件(各個獨立的程序,通過網路進行互動)。有兩個問題需要在設計時就重點考慮:一個是可擴充套件性;一個是容錯性。可擴充套件性說的是,當流量逐漸變大的時候,你的系統如何擴充套件。系統中有些元件是無狀態的,有些是有狀態的。無狀態的元件一般通過增加節點,應用簡單的負載均衡策略就可以擴充套件;而有狀態的元件需要明確擴充套件的方式。容錯性說的是,系統應該主動處理失敗情況,在設計中就應該考慮進去。比如,你想做到不丟訊息,那麼必須把網路丟包和處理異常的情況當做正常情況來考慮,設計重傳機制;既然有了重傳,就不得不考慮去重機制。容錯性還包括,系統應該具有從錯誤狀態中恢復的傾向。當然,系統架構的設計還有很多因素需要考慮,比如高可用、高效能、可維護,等等,我們這裡就不展開說了。

其次,在更細的層面,要完成介面設計,也就是俗稱的「面向介面程式設計」。這個過程的重要性怎麼強調都不為過。我們都知道「面向介面程式設計」,在面試的時候也經常討論設計模式,但實際中真正按這種方式工作的人少之又少。造成這種狀況的原因可能是,我們平常做業務開發,在大部分情況下,都不用自己設計介面。比如做客戶端開發,各種MVC, MVP模式已經把程式碼框架都定義好了,我們只用往介面實現裡填東西。

但我們應該知道,當為一個新系統編寫程式碼的時候,程式碼應該從介面設計開始。先用程式碼定義出各層的介面(包括回撥介面),沒有實現,只是能夠編譯通過。有了這些介面,就可以拿它們與同事進行非常細節的討論了。應該先把介面討論得足夠清楚,再進行下一步的具體實現。這也是一個比較痛苦的過程,我們需要反覆抉擇,而通常「選擇」就意味著痛苦。根據我個人的經驗,設計介面程式碼的過程,一般都要前後改很多遍,才能達到令自己基本滿意的程度。

介面設計的時候,要時刻考慮這兩個問題:

  • 功能層次。也就是系統包括哪些介面,哪些功能放到哪些接口裡面,不同的介面之間的關係如何。我們可能還需要畫出類似UML(Unified Modeling Language)那樣的類圖。
  • 例項執行模型。系統執行起來之後,介面的各個例項的生命週期,以及各個例項之間的互動關係和數量關係,是一對一,還是一對多。雖然在編寫介面程式碼的時候,還不要求寫出實現,但是一個不錯的實踐方式是,寫完介面,先生成一個空的實現類,然後把能表明例項引用關係的程式碼先寫出來,再進一步把各個介面的例項建立程式碼寫出來(解決了實現引數的注入問題),這樣一個系統的「骨架」就出來了。

在編寫和修改介面程式碼的過程中,還有幾個問題值得考慮:

  • 是否引入響應式程式設計(Reactive Programming)的思想。實踐證明,採用響應式程式設計和傳統的回撥方式(callback)設計出來的介面形式,存在很大的不同。
  • 給介面起名字非常重要(包括各個類名、方法名、引數名等)。起名字其實是個大問題,就像給自己的小孩起名字一樣難!名字起的好不好,直接反映了通過系統抽象劃分出來的各個角色是不是合理。
  • 能稱得上「介面」的程式碼,從一開始編寫就要有非常詳細的程式碼註釋。
  • 考慮執行緒模型。被上層呼叫的程式碼預期在哪些執行緒上執行,回撥的程式碼又在哪些執行緒上執行。是多執行緒的環境,還是單執行緒的環境,這直接影響後面的實現應該怎麼來做。一般來說,多執行緒的環境是存在一個執行緒池,這個執行緒池是外部呼叫者提供,還是介面內的實現來提供,這個要規定清楚。另外就是考慮能否把多執行緒問題規避,變成單執行緒的程式設計問題。比如,在有些非同步程式設計的情況下,充分利用Java的Executor,或者Android開發中的Looper,或者RxJava之類的框架,就可能達到類似的目的。

最後,就是愉快地實現了。只要前面的過程做得比較細緻,編寫實現程式碼基本就是水到渠成了。在實現中有一個非常重要但容易被忽視的問題是——日誌(log)。每個人都知道怎麼打日誌,但打一份好的日誌,實際沒有幾個人能夠做到的。一般來說,如果沒有足夠的重視,工程師打出來的日誌,或者過於隨意,或者邏輯缺失。一份好的日誌其實要花很多精力來調整細節,把程式執行看成一個狀態機,每一個關鍵的狀態變化,都要在日誌中記錄。一份好的日誌其實反映了一套好的程式邏輯。總之,打日誌的目標是:如果線上發生奇怪的情況,拿過這份日誌來就能分析出問題所在。這在客戶端上分析線上問題的時候尤其有用。

前面講到的各個過程並不是要嚴格按照這樣的步驟進行,實際中有些過程要前後交叉,甚至反覆進行。中間碰到問題,可能還要退回到前面的步驟,重新進行。

在時間、精力和專案進度各種條件允許的情況下,儘量把事情做到當前認為「最好」的狀態。當然,如果由於客觀條件,一時沒有那麼多時間去做到完美,也不要太過沮喪,後面能夠持續優化才是最重要的。

進行一項技術攻關,從這個過程中學習新的東西,這是一個利用現有各種資源來學習和解決問題的過程。每一步並非必不可少。你參考的東西越多,做出一個差勁的實現的可能性就越小,但付出的精力也就越多。

關鍵的關鍵,當團隊中出現現有經驗無法解決的問題的時候,你能夠站出來,勇於承擔。這樣,你的技術之路也會越來越寬廣。

(完)

原文來自微信公眾號:張鐵蕾