滴滴出行技術總監:關于技術選型的那些事兒

jopen 7年前發布 | 35K 次閱讀 滴滴

滴滴出行技術總監:關于技術選型的那些事兒

編者按:本文來自微信公眾號“InfoQ”(ID:infoqchina),作者 杜歡。微信公眾號回復“選型” ,獲取完整視頻回顧。

杜歡,滴滴出行技術總監,負責滴滴小巴業務的技術管理工作。在互聯網領域已經有十年工作經驗,曾就職于微軟、百度,也曾自主創業兩次,來到滴滴之后也經歷過很多項目和業務的變化,是一個“什么都懂”工程師,對前端、客戶端、服務端、運維等方面都有不少實戰經驗。平時是一個 ACG 宅,也喜歡閱讀各種技術和非技術的文章擴大視野,不愿主動交談,但一旦放松了就聊到停不下來。

技術選型案例

今天會聊技術選型這個話題,主要就是因為我經歷相對比較豐富,親歷過不少項目選型的過程,自己也做過不少靠譜或者不靠譜的決策,在這個方面也有些自己的思考。我想先從幾個案例開始,像講故事一樣聊聊選型背后的事,作為話題的開始。

在我剛開始工作時就經歷過一次很大的選型事件,我是這件事情的旁觀者。當時公司希望做一個非常酷炫的手機界面系統,恰逢 Windows Vista 一系列新技術的發布,包括 WPF、Silverlight、C# 這些技術非常火,公司對它們抱有極高的期望,所以就想第一時間用在新一代 Windows Mobile 上面。確實界面開發和各種效果可以做的很酷炫也節省了界面開發時間,但是很尷尬的遇到了另外一個問題,性能問題。

這些東西都是跑在移動設備上面,當年的移動設備內存能有 32MB,CPU 能到 1GHz 就很不錯了,根本不能很好的支撐這一整套界面系統對性能的要求。后來,當公司發現確實在當時的硬件環境下突破性能問題,就對所有界面做了一次重寫,回到了用 C++ 和各種 API 傳統寫界面方式上才解決問題,這里面涉及到將近一千名工程師一年多的時間,可以說是個很大的人力和時間的損失。

當時我還不是很理解,為什么公司不能更早一點止損,后來我慢慢發現,這真的是當局者迷,當一個決策作出之后大家就天然的希望能通過努力來解決眼前的問題,結果反而越陷越深。這也意味著最初選型的時候得十分謹慎,特別是選型影響面巨大時保守點會更好。

后來加入了真正的互聯網公司,我看到了技術選型是穩定壓倒一切。比如 gcc、linux 內核這些非常底層和關鍵的東西,在互聯網公司里基本不會去追最新版,只是保持了解和跟進,非常克制的將一些 patch 和功能引入到線上環境,真正上線也會經歷相當久的灰度驗證過程。

我印象挺深的是當年(2009 年)對 lighttpd 和 apache 的選型,當時 lighttpd 單機性能明顯優于 apache,同時也支持 php 擴展,能夠以 mod 形式運行 php,看起來使用 lighttpd 全面替換 apache 就好了,但實際上為了業務穩定性,真正的用法是將 lighttpd 做反向代理,后面還是使用 apache + mod_php 來提供服務。這里面的思考就是對于一個新技術的天然不信任,在技術接受程度還不夠高且公司內沒有人能吃透這個技術的情況下,不愿意讓自己的業務做第一個吃螃蟹的人。

謹慎確實是個美德,不過如果在一個非常追求速度的業務里,這可能也意味著過于保守,會延誤時機。

我在自己創業的過程中選型就比較激進,也玩的比較 high。

比如我會積極的使用 MongoDB,我對它靈活的數據結構、強大的查詢語句和內置的高可用機制等非常認可,當它剛剛 1.0 的時候就將它用在一些不重要的數據上,后來等到 2.x 發布后就開始嘗試用在新業務上作為核心數據庫。我也曾經遇到一些嚴重的坑,比如數據損壞、擴容不及時造成停機等,但是由于業務對這些問題容忍度較高,同時也有一些兜底方案,所以還不至于成為業務瓶頸,總體來說利大于弊,可以節省業務開發人員的寶貴時間。

我也曾決策使用 Node.js 作為主力服務器開發工具,當時(2013 年)因為客戶端要使用 Javascript 作為主力語言,服務端和客戶端會有不少能夠復用的代碼,所以挺想使用 Node.js 來提升開發效率。

為了驗證 Node.js 是否靠譜,我自己通讀了源碼、閱讀了不少相關文章、看了下官方 release note 及社區活躍程度(github issues、stackoverflow 討論等)、還做了一些基本的壓測,最后的結論是,它的性能可以滿足要求,在穩定性方面基本合格,考慮到只是用它做無狀態服務,且單臺服務器上都會跑多個實例(當時使用 supervisord 管理),簡單的崩潰不會對系統有明顯影響,再加上當時確實也有些公司將它作為主力服務,所以最終選擇了它。

后來加入滴滴后,我在技術選型方面綜合了以前所有的經驗,有做得好的,也有犯錯的時候。

2015 年滴滴有一個很大的內部代碼重構項目,涉及到服務端和客戶端大量代碼。客戶端的技術選型做的相對較好,針對當時代碼庫多業務耦合嚴重,大家開發時候模塊間沖突頻繁的問題,評估并引入了 cocoapods 和 maven/gradle 作為 iOS 和 Android 的項目拆分工具,并且通過代碼重構,將客戶端項目分成幾個獨立的倉庫,可以讓業務獨立開發的同時,也能通過構建腳本輕松的整合成一個完成的 app。

服務端的選型則比較錯誤,當時考慮到滴滴的業務模式非常類似于 erlang 的 actor 模型,一個叫車流程會涉及到非常多可復用的 actor,如果我們直接實現一個分布式的 actor 模型和數據流管理機制,那么很多問題就迎刃而解了。可是當時并不存在一套這樣的機制,我們自己在實現的時候采用 Go + kafka 分別實現 actor 和數據流存儲,過程中遇到了 kafka 消息丟失不好定位、actor 模型過于抽象不容易在整個團隊貫徹執行等問題,最終放棄了整個方案。

技術選型方法論

技術選型關鍵需要思考三個角度:技術、業務和人。

角度之一:技術

技術選型首先考慮的當然是技術本身,這里提到的技術包括語言、框架、工具、設計模式、開發模式等。

在選擇技術時有兩個大原則。第一,要取其長避其短;第二,要關注技術的發展前景。

每種技術都是有它特定的適用場景的,“沒有銀彈”。開發者經常犯的錯誤就是盲目追新,當一個新語言、框架、工具出現后,特別是開發者自己學會了這種新技術后,就會有種“拿著錘子找釘子”的感覺,將新技術濫用于各種項目。

比如最近幾年 Go 在國內很火,我自己也非常使用它開發項目,但絕對不應該將它用于所有項目。Go 的優點是上手快、運行時性能高、方便的使用多核運算能力等,經常被提起的特性是超輕線程 goroutine、內置的內存隊列 chan、極快的編譯速度,非常適合于編寫各種無狀態應用服務,無需使用任何的第三方框架都能輕松寫出一個高性能的 http 服務。

但它的缺點也非常明顯,最痛的一點是 gc。Go 在設計之初就號稱要實現一個世界上最優秀的 gc,可惜直到今天也還差的較遠,最近一年才實現了 jvm 幾年前就做到的并發 gc,并且沒有很好的方法解決內存碎片和對象過多帶來的性能問題。這些缺陷使得 Go 不太適合做有狀態服務,特別不適合做內存管理相關的服務,在這些場景里面還是 C/C++ 更加可靠。

技術的發展前景也是一個重要考慮因素。有些技術設計的很好,比如我個人挺喜歡一個叫做 Io 的語言,但我不會把它用于真實項目,因為這個語言缺乏社區和長期支持,就算設計理念寫的再好,里面也必然有各種 bug 和不足,如果沒人能夠解決就會帶來嚴重的問題。技術的“前景”可以從幾個維度來判斷,有沒有長期規劃、有沒有持續投入的人或者社區、問題解決的速度如何、業界使用案例及口碑、源碼質量。

選擇一個技術最低限的標準是,技術的生命周期必須顯著長于項目的生命周期。想象一下,如果項目還沒做完這個技術就不被維護了,那將是怎樣一種窘境。拿去年很火的 Vue.js 來說,尤大在規劃、投入和解決問題速度方面都沒有問題,這是這個技術能火起來的基本保障,再加上設計優雅、源碼確實寫的不錯,它的成功并不偶然。可以預見,隨著尤大全職開發這個框架并且社區貢獻者越來越多,Vue.js 能持續幾年應該問題不大。

滴滴的 web app,比如微信錢包里面的滴滴入口,就在去年年底全面改用 Vue.js 重構了一版,我們就是看中了 Vue.js 在移動應用開發中的優勢再加上對它的前景有信心。在重構前,我們為了確認 Vue.js 真的能承擔如此大任,公共前端團隊在 2016 年花了半年的時間整體梳理和評估了 Vue.js 1.0 和 2.0 的全部源碼,為此還出了一本書,在公司大規模使用前也在滴滴小巴業務和行程分享功能里做了試點,效果非常不錯,最終才真正下定決心廣泛推廣。

技術的發展前景是動態變化的,當一個技術走向了末路,我們也應該勇敢的揚棄。拿 jQuery 為例,最開始它是前端開發的必需品,當時很多前端同學離開了 $ 函數就不會寫代碼了,它在簡化 DOM 操作、抹平瀏覽器間差異做出了極其重要的貢獻。但是隨著瀏覽器越來越標準和趨同,jQuery 的亮點已經不再吸引人,它的插件開發模式逐步被模塊化開發給取代,再加上各種歷史包袱,它所適用的項目也會變得越來越少,新項目在選型的時候就不推薦優先考慮 jQuery 了。

對于一家大型公司來說,其核心業務的技術選型更需謹慎,看前景時甚至需要考慮技術的獨立性。依然把 Go 當做一個例子,當前核心 Go Authors 基本都受雇于 Google,也沒有一個獨立運作的基金會來負責語言的長期維護,更沒有一個公開透明的決策機制來決定語言的未來,假如 Google 出于某種原因停止投入或者改變語言的發展方向,那么這對一家大型公司來說可能會是毀滅性打擊。立志于成為一家千億美元規模的公司,或者是 Google 的潛在競爭對手,在選擇使用 Go 時就應該更加謹慎,不要盲從。

角度之二:業務

技術選型必須貼著業務來選擇,不同業務階段會有不同的選型方式。

處于初創期的業務,選型的關鍵詞是“靈活”。只要一個技術夠用且開發效率足夠高,那么就可以選擇它。初創的業務往往帶有風險性和不確定性,朝令夕改、反復試錯是常態,技術必須適應業務的節奏,然后才是其他方面。MongoDB 是一個很好的例子,相比 MySQL,它的數據結構靈活多變,相比一般的 KV 存儲,它又具有類似 SQL 的復雜查詢能力,再加上它內置的傻瓜式高可用和水平擴展機制,讓它能夠很好的適應初創業務對效率的追求。

等業務進入穩定期,選型的關鍵詞是“可靠”。技術始終是業務的基石,當業務穩定了技術不穩,那就會成為業務的一塊短板,就必須要修正。當年 推ter 放棄 RoR 選擇 Java 系框架,這就是個很好的例子。RoR 以快速開發著稱,但同時 ruby 的性能非常有限,推ter 工程團隊針對 ruby 虛擬機做了非常多性能優化可是依然不能達到預期,再加上當時的 推ter 為了提升前端體驗,全面使用模塊化和異步化的方法加載頁面,服務端已經基本不怎么負責渲染頁面,而專注于提供各種 RESTful API,RoR 的優勢也不太明顯了。

當業務步入維護期,選型的關鍵詞是“妥協”。代碼永遠有變亂的趨勢,一般經過一兩年就有必要對代碼來一次大一點的重構。在這種時候,必須得正視各種遺留代碼的遷移成本,如果改變技術選型會帶來遺留代碼重寫,這背后帶來的代價業務無法承受,那么我們就不得不考慮在現有技術選型之上做一些小修小補或者螺旋式上升的重構。

正因為技術選型和業務相關,我們能夠觀察到一些很明顯的現象:新技術往往被早期創業團隊或大公司的新興業務使用;中大型公司的核心業務則更傾向于用一些穩定了幾年的技術;一個公司如果長期使用一種技術,就會傾向于一直使用下去,甚至連版本都不更新的使用下去。這現象背后都是有道理的。

角度之三:人

技術選型過程中最終影響決策的還是人本身,這里要強調一下,我說的“人”是指的個人,而不是團隊。

技術選型的決策流程一定得專制。決策者可以在調研的時候體恤民情,并把團隊現狀當做一個因素考慮進來,但絕對不能采用類似“少數服從多數”、“按著大家習慣來”的方式選型。專制可以使技術選型更加的客觀,考慮的更加全面,并且使得權責統一。

并不是每個人都懂得怎么為項目負責,一個基層的開發人員思考的更多的可能是技術是否有挑戰、能否做出彩、甚至未來好不好找工作,這些主觀因素可能會給選型帶來災難性的后果。專制也使得“螺旋式上升”成為可能,很多時候我們沒法一蹴而就的使用某種技術,這時候需要有一個領路人,帶著大家堅定的朝一條曲折的路線前進才能獲得成功。

技術選型也非常依賴于人的能力。選型是一件很難被標準化的過程,選型的決策質量跟人的眼界、經驗、業務敏感度、邏輯性等息息相關。就我自己來說,我在面臨一個選型問題時首先考慮的是去學習,看看公司內外類似的問題如何解決的,避免自己閉門造車,然后思考所有的可能性,列舉最核心需要考慮的因素,心里列一個方案優劣對比,最后將這些邏輯整理清楚,落地成一個決策。

滴滴在決策客戶端動態化方向時就是以這樣的方式來進行的,我們將業界所有可能的方案都拿出來,理解他們的優缺點,然后在某次會議上幾個核心同學在白板上列了一張表格,以考慮的因素為行,可能的方案為列,分別評估各個方案在每種因素里的優劣勢,最終確定了一個結論。我們選擇的路是偏向于客戶端開發的動態化方案,在保留所有代碼和工具鏈的前提下做到對開發者透明的動態化,這樣能讓整體遷移和維護代價變得最小,當然,這條路開發難度也相當大,幸好我們當時也找到了最合適的人,我們依然可以在能接受的時間里實現整個方案。

培養技術選型的能力

可以看到,要想做好技術選型還是挺難的,要想做好得有足夠的知識積累和實際踩坑的經歷才行。如果一個不太懂得如何選型的新人想學著做好這件事,那可以先從小項目開始做嘗試,慢慢積累經驗。技術選型對人來說最重要的還是“邏輯性”,每一個決策背后都藏著許多假設和事實,我們通過不斷挑戰這些背后的東西來逐步成長。

比如在需要使用緩存來加快數據訪問速度的場景中,我們可能會很自然的選擇 redis 作為緩存服務。這看似“直覺”的決策,背后也是由一系列假設和事實組成。可以問自己一連串問題,看看在具體的場景下這個決策是不是真的正確,例如,緩存服務有沒有 redis 之外的選項、是否可以在內存里直接緩存、redis 是否穩定、redis 性能是否滿足需求、數據庫訪問速度瓶頸究竟在哪等等問題,很可能最終結果還是“ 使用 redis 做緩存”這個直觀方案,但正因為有分析的過程,讓我們在下一次做決策可以更迅速、更自信。

如何保持敏感性和廣度

技術選型是個很需要經驗的活,得有大量的信息積累和輸入,再根據具體現實情況輸出一個結果。我們在選型的時候最忌諱的是臨時抱佛腳、用網上收集一些碎片知識來決策,這是非常危險的,我們得確保自己所有思考都是基于以前的事實,還要弄清楚這些事實背后的假設,這都需要讓知識內化形成經驗。

我一直在想,“經驗”的本質是什么,有什么方法能夠確定自己的經驗增長了,而不是不斷在重復一些很熟悉的東西。我現在的結論是,經驗等于“知識索引”的完備程度。

我們一生中會積累很多的知識,如果把我們的大腦比作數據庫的話,那我們一定有一部分腦存儲貢獻給了內容的索引,它能幫助我們將關聯知識更快的取出來,并且輔助決策。經驗增長等同于我們知識索引的增長,意味著我們能輕易的調動更多的關聯知識來做更全面的決策。

要想建立好這個知識索引,我們得保持技術敏感性和廣度,也就是要做到持續的信息輸入、內化,并發現信息之間的關聯性,建立索引,記下來。說起來容易,做起來還是挺有難度的。

首先難在信息輸入量大,忘記了怎么辦。我們的大腦不是磁盤,不常用的知識就會忘記,忘記了就跟沒看過是一回事。我的經驗是一定要對知識進行壓縮,記住的是最關鍵的細節,并且反復的去回味這個細節。

比如我學習各種語言的時候就會非常留意一些最有特色的語法特性和應用場景,像 C++,我一直記得很早以前看過的細節,像編譯器默認會生成哪些類方法,默認析構、拷貝構造、operator = 等,默認生成的類方法有哪些場景需要顯示禁用,什么時候要在構造函數用 explicit 等,我看這些細節已經超過十五年的時間了,依然記憶尤新。

看起來好像有點難度,實際上不難,大家想想自己學過的英文單詞,再怎么樣最常見的幾百個英文單詞還是能清楚的記得含義的,而技術的知識點其實壓縮之后會遠小于英文單詞的個數,記憶負擔不會有想象中那么大。

然后難在信息更新速度太快,跟不上技術發展怎么辦。我學習了非常多技術之后就會發現這確實是個難解的問題,像前端開發,每年都會有新的框架和開發方式出現,ES7 的語法如果不去提前了解,過兩年可能連 Javascript 語法都看不懂了。

我在這個問題上也是有些焦慮的,不過多少還是有應對的方式,就是堅持碎片化學習,增量更新過時的內容,只要形成習慣也還是能夠慢慢的找到自己的節奏。如果有些技術實在細節太多,比如 Node.js 這種,我以前曾經通讀過源碼,仔細研究過內部設計,但隨著它不斷發展現在我也不太敢說對它內部有多熟悉,那我會考慮大膽的放棄追新,等著我可能需要用它的時候再統一更新到最新的知識。

最后難在信息究竟如何存入知識索引,知識太零散形成不了體系,建不了索引怎么辦。最入門的做法是看書,看別人是怎么將知識變成一個個章節的信息。要想掌握建立索引背后的方法論,我的經驗是先從兩個相近的技術開始,找到建索引的感覺,然后再鋪開去學習更多知識。有這樣困惑的開發者往往在學習方面有些貪心,覺得自己記性好可以囫圇吞棗式的將知識強行內化,這樣做短期可以,長期還是會遺忘,也形成不了經驗。

其實技術知識之間非常像,有很多共性的點可以挖掘。比如客戶端和前端開發,各個框架在 View 生命周期管理、消息派發機制等方面非常像,后端開發則更加的套路化,無論用那種語言,最基本的分布式服務原理、緩存、隊列、數據庫等基礎組件原理,都萬變不離其宗。

如果我們更宏觀的看每個領域,甚至于都能發現領域之間的知識體系劃分也很類似。作為表現層的前端和客戶端,知識體系都可以分為語言、API、工程化、框架和設計模式。比如前端的語言包括 HTML、CSS、Javascript 和一些稍小眾的 TypeScript、CoffeeScript 等,API 就是各種標準、接口的使用、能夠實現的效果、平臺限制等,工程化就是各種打包工具、代碼轉化工具、輔助開發工具等,框架就是像 Vue、React 等,設計模式就是像 PWA、redux 等。

相應的,剛剛說的這些知識都能找到在 iOS 或 Android 里幾乎對應的知識,無非換了一些細節,這里我就不繼續展開了。服務端也是這樣,知識體系最頂層的部分也很少,具體到細節,只是要了解每一個實現背后的優劣。

總結一下,技術選型依賴于經驗,經驗又來源于知識索引的建設,這依賴于平時的總結和不斷的新知識輸入,技術是一輩子的事,必須得投入大量時間維持狀態。學無止盡,大家一起共勉。

來自: InfoQ

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!