Doordash經過各種語言評估後決定從Python遷移到Kotlin

banq發表於2021-05-07

當DoorDash達到了我們基於Django的整體程式碼庫所能支援的極限時,我們需要設計一個新的堆疊,這將為我們的物流服務提供堅實的基礎。這個新平臺將需要支援我們的未來發展,並使我們的團隊能夠使用更好的模式進行構建。 
在我們的舊系統下,需要更新的節點數量增加了大量釋出時間。由於每個部署擁有的提交數量,分析不良部署(找出導致某個問題的提交或提交)變得越來越困難。最重要的是,我們的整體構建於舊版本的Python 2Django之上,這些版本正迅速進入生命週期以尋求安全支援。 
我們需要打破整體/單體架構,使我們的系統更好地擴充套件,並決定我們希望新服務的外觀和行為方式。尋找一個可以支援這項工作的技術堆疊是該過程的第一步。在調查了多種不同的語言之後,我們選擇Kotlin是因為其豐富的生態系統,與Java的互操作性以及對開發人員的友好性。但是,我們需要進行一些更改以應對其不斷增長的痛苦。
 

為DoorDash找到合適的堆疊
構建伺服器軟體的可能性很多,但是由於多種原因,我們只想使用一種語言。有一種語言: 

  • 幫助集中我們的團隊,並促進在整個工程組織中共享開發最佳實踐。
  • 使我們能夠構建適合於我們的環境的通用庫,並選擇預設值以在我們的規模和持續增長中發揮最佳作用。 
  • 允許工程師以最小的摩擦來更換團隊,從而促進協作。 

考慮到這些特徵,對我們來說,問題不是我們是否應該學習一種語言,而是我們應該學習哪種語言。 
 

選擇正確的程式語言 
我們從提出編碼語言選擇開始,提出了關於我們希望我們的服務如何相互看起來和相互操作的要求。我們很快同意使用Apache Kafka作為訊息佇列,將gRPC作為同步服務到服務通訊的機制。我們已經在PostgresApache Cassandra上擁有豐富的經驗和專業知識,因此這些仍將保留在我們的資料儲存中。這些都是相當成熟的技術,在所有現代語言中均具有廣泛的支援,因此我們必須弄清楚要考慮的其他因素。
我們選擇的任何技術都必須是: 

  • CPU效率高且可擴充套件到多個核心
  • 易於監控 
  • 在強大的庫生態系統的支援下,我們可以專注於業務問題
  • 能夠確保良好的開發人員生產力 
  • 規模可靠
  • 面向未來,能夠支援我們的業務增長 

我們將語言與這些要求進行了比較。我們丟棄了主要的語言,包括  C ++RubyPHPScala,這些語言不支援每秒查詢(QPS)和規模的增長。
儘管這些都是很好的語言,但是它們缺少我們在未來的語言堆疊中尋找的一個或多個核心原則。考慮到這些因素,這種情況僅限於KotlinJavaGoRustPython 3。在這些產品作為競爭對手的情況下,我們建立了以下圖表,以幫助我們比較和對比每種選擇的優勢和劣勢。
 

比較我們的語言選擇

  • Kotlin

–提供強大的庫生態系統
–為gRPC,HTTP,Kafka,Cassandr和SQL提供一流的支援
–繼承Java生態系統。
–快速且可擴充套件
–具有併發的本地原語
–簡化了Java的冗長性,並消除了對複雜的Builder / Factory模式的需求
– Java代理以很少的程式碼即可對元件進行強大的自動內省,自動定義並匯出度量和跟蹤以監控解決方案
–在伺服器端不常用,這意味著我們的開發人員可以使用的示例和示例更少
–併發並不像Go那樣瑣碎,它在語言的基本層及其標準庫中整合了gothreads的核心思想
–缺少REPL
 
  • Java

–提供強大的庫生態系統
–提供對GRPC,HTTP,Kafka,Cassandra和SQL的一流支援
–快速且可擴充套件
– Java代理只需少量程式碼即可對元件進行強大的自動自檢,自動定義並匯出度量和跟蹤以監控解決方案
–併發性比Kotlin或Go(回撥地獄)更難
–可能非常冗長,使得編寫乾淨的程式碼更加困難
–缺少REPL
 
  • Go

–提供強大的庫生態系統
–為GRPC,HTTP,Kafka,Cassandra和SQL提供一流的支援
–是快速且可擴充套件的選項
–具有用於併發的本機基元,這使得編寫併發程式碼更加簡單
–許多伺服器端示例和文件可用
–對於不熟悉該語言的人來說,配置資料模型可能會很困難
–沒有泛型(但最終會出現!)意味著某些類庫很難在Go中構建
–缺少REPL
 
  • Rust語言

–執行速度非常快
–沒有垃圾收集,但仍然具有記憶體和併發安全性
–隨著大公司開始採用該語言,大量的投資和令人振奮的發展
–強大的型別系統可以比其他語言更輕鬆地表達複雜的思想和模式
–相對較新,這意味著更少的樣本,庫或具有構建模式和除錯經驗的開發人員 
–當時的生態系統不像其他非同步/等待系統那樣強大
–缺少REPL
–記憶體模型需要時間來學習
 
  • Python3

–提供強大的庫生態系統
–易於使用
–團隊中已經有很多經驗
–通常容易被僱用
–具有對GRPC,HTTP,Cassandra和SQL的一流支援
–具有REPL,便於進行測試和除錯實時應用程式
-執行相比,大多數慢 ,全域性解釋鎖使其難以有效充分地利用我們的多核機器
-沒有一個強型別檢查功能
-卡夫卡的支援有時可能參差不齊且有滯後的特點
 
進行此比較後,我們決定開發經過測試和擴充套件的Kotlin元件黃金標準,從本質上為我們提供了更好的Java版本,同時減輕了痛苦。因此,科特林是我們的選擇;我們只需要解決一些成長的煩惱。
 

Kotlin相對於Java的好處
與Java相比,Kotlin的最大好處之一就是null安全。必須顯式宣告可為空的物件,以及迫使我們以安全的方式處理它們的語言,消除了我們可能不得不處理的許多潛在的執行時異常。我們還將獲得null合併運算子:?.,該運算子允許單行安全地訪問可為null的子欄位。
In Java:

int subLength = 0;
if (obj != null) {
  if (obj.subObj != null) {
    subLenth = obj.subObj.length();
  }
}


Kotlin:

val subLength = obj?.subObj?.length() ?: 0


儘管上面是一個非常簡單的示例,但此運算子的強大功能極大地減少了程式碼中條件語句的數量,並使其更易於閱讀。
與Kotlin相比,使用Kotlin遷移到事件監控系統Prometheus時,使用指標對我們的服務進行測試變得更加容易。我們開發了一種註釋處理器,該處理器可以自動生成按度量的功能,以正確的順序確保正確數量的標籤。 
標準的Prometheus庫整合如下所示:

// to declare
val SuccessfulRequests = Counter.build( 
    "successful_requests",
    "successful proxying of requests",
)
.labelNames("handler", "method", "regex", "downstream")
.register()

// to use
SuccessfulRequests.label("handlerName", "GET", ".*", "www.google.com").inc()


我們可以使用以下程式碼將其更改為不那麼容易出錯的API:

// to declare
@PromMetric(
  PromMetricType.Counter, 
  "successful_requests", 
  "successful proxying of requests", 
  ["handler", "method", "regex", "downstream"])
object SuccessfulRequests

// to use
SuccessfulRequests("handlerName", "GET", ".*", "www.google.com").inc()


透過這種整合,我們不需要記住指標的標籤順序或數量,因為編譯器和我們的IDE確保正確的數量並讓我們知道每個標籤的名稱。當我們採用分散式跟蹤時,整合就像在執行時新增Java代理一樣簡單。這使我們的可觀察性和基礎架構團隊能夠快速將分散式跟蹤推廣到新服務,而無需擁有團隊的程式碼更改。

協程對於我們來說也變得非常強大。這種模式使開發人員可以編寫程式碼使其更接近他們習慣的命令式樣式,而不會陷入回撥地獄。協程也很容易合併,並在必要時並行執行。我們的一位Kafka消費者的例子是

val awaiting = msgs.partitions().map { topicPartition ->
   async {
       val records = msgs.records(topicPartition)
       val processor = processors[topicPartition.topic()]
       if (processor == null) {
           logger.withValues(
               Pair("topic", topicPartition.topic()),
           ).error("No processor configured for topic for which we have received messages")
       } else {
           try {
               processRecords(records, processor)
           } catch (e: Exception) {
               logger.withValues(
                   Pair("topic", topicPartition.topic()),
                   Pair("partition", topicPartition.partition()),
               ).error("Failed to process and commit a batch of records")
           }
       }
   }
}
awaiting.awaitAll()


Kotlin的協程使我們可以按分割槽快速拆分訊息,並按分割槽釋放協程以處理訊息,而不會違反將訊息插入佇列時的順序。之後,我們將在訊息代理偏移灰checkpointing時,加入所有futures。
這些只是Kotlin允許我們以可靠且可擴充套件的方式快速移動的便捷性的一些示例。
 

Kotlin的成長之痛
為了充分利用Kotlin,我們必須克服以下問題: 

  • 教育我們的團隊如何有效使用這種語言
  • 開發使用協程的最佳實踐 
  • 解決Java互操作性的痛點
  • 使依賴管理更容易

我們將在以下各節中詳細介紹如何處理這些問題。
  • 向我們的團隊教授Kotlin

採用Kotlin的最大問題之一是確保我們可以使我們的團隊加快使用它的速度。我們大多數人都具有Python的深厚背景,並且在後端團隊中有一些Java和Ruby的經驗。Kotlin並不經常用於後端開發,因此我們不得不想出好的指導方針來教我們的後端開發人員如何使用該語言。 
儘管可以從網上找到許多此類學習內容,但Kotlin周圍的許多線上社群都是特定於Android開發的。高階工程人員撰寫了“如何在Kotlin中程式設計”指南,其中包含建議和程式碼段。我們舉辦了午餐和學習課程,教給開發人員如何避免常見的陷阱並有效地使用IntelliJ IDE來完成工作。 
我們教了我們的工程師Kotlin的一些功能性方面的知識,以及如何使用模式匹配,並且預設情況下更喜歡不變性。我們還建立了Slack渠道,人們可以在那裡提出問題並獲得建議,並建立Kotlin工程指導社群。透過所有這些努力,我們能夠在Kotlin建立起一支精通流利的工程師的強大基礎,隨著我們增加員工人數,可以幫助教授新員工,建立自我維持的週期,從而不斷改善我們的組織。
  • 避免協程陷阱

gRPC是我們用於服務到服務通訊的首選方法,但是當時缺乏協程,需要對其進行糾正才能充分利用Kotlin的優勢。gRPC-Java是Kotlin gRPC服務的唯一選擇,但是它缺乏對協程的支援,因為協程不存在於Java中。兩個開源專案Kroto-plusProtokruft正在努力幫助解決這種情況。我們最終都使用了兩者來設計我們的服務,並建立了一個更加原生的感覺解決方案。最近,gRPC-Kotlin變得普遍可用,我們已經在很好地進行遷移服務,以使用官方繫結獲得Kotlin的最佳體驗構建系統。
進行此轉換的Android開發人員會熟悉其他帶有協程的陷阱。不要在請求之間重用CoroutineContext。取消或異常會使CoroutineContext進入取消狀態,這意味著在該上下文上啟動協程的任何進一步嘗試都將失敗。這樣,對於伺服器正在處理的每個請求,都應建立一個新的CoroutineContext。不再可以依賴ThreadLocal變數,因為可以將協程交換進出,從而導致資料不正確或被覆蓋。要注意的另一個陷阱是避免使用GlobalScope啟動協程,因為它是不受限制的,因此可能導致資源問題。
  • 解決Java的幻影NIO問題

選擇Kotlin之後,我們發現許多聲稱實現現代Java非阻塞I / O(NIO)標準的庫(因此可以很好地與Kotlin協程進行互操作)以不可擴充套件的方式實現了。他們不是使用基於NIO原語的底層協議和標準,而是使用執行緒池來包裝阻塞的I / O。 
這種策略的副作用是在協程環境中執行緒池很容易耗盡,由於它們的阻塞性質,這導致了很高的峰值延遲。這些幻影NIO庫中的大多數將公開其執行緒池的調整,因此可以確保它們足夠大以滿足團隊的要求,但是這增加了開發人員進行適當調整以節省資源的負擔。使用真實的NIO或Kotlin本機庫通常可以提高效能,更容易擴充套件和更好的開發人員工作流程。
  • 依賴管理:使用Gradle具有挑戰性

對於新手和Java / JVM生態系統領域的新手來說,構建系統和依賴項管理要比Rust的Cargo或Go的模組等較新的解決方案直觀得多。特別是,我們擁有的某些直接或間接依賴項對版本升級特別敏感。諸如Kafka和Scala之類的專案不遵循語義版本控制,這可能會導致編譯成功的問題,但應用程式啟動時會失敗,並帶有奇怪的,看似無關的回溯。
 隨著時間的流逝,我們瞭解到哪些專案最容易導致這些問題,並提供瞭如何捕獲和繞過這些問題的示例。特別是Gradle上有一些有用的頁面,介紹如何檢視依賴關係樹,在這些情況下這總是有用的。學習多專案儲存庫的來龍去脈可能會花費一些時間,而且很容易遇到衝突的需求和迴圈依賴關係。
從長遠來看,提前計劃多專案儲存庫的佈局將大大有利於專案。始終嘗試使依賴關係成為一棵簡單的樹。具有一個不依賴於任何子專案的基礎(並且永不依賴),然後以遞迴方式建立在其基礎上,可以防止難以除錯或糾纏的依賴鏈。DoorDash還大量使用了Artifactory,從而使我們可以輕鬆地跨儲存庫共享庫。
 

DoorDash上Kotlin的未來
我們繼續將Kotlin視作DoorDash服務的標準。我們的Kotlin平臺團隊一直在努力構建下一代服務標準(在GuiceArmeria的基礎上構建),以透過預先連線工具和實用程式(包括監視,分散式跟蹤,異常跟蹤,與我們的執行時配置管理整合)來幫助簡化開發。工具和安全性整合。
這些努力將幫助我們開發更具共享性的程式碼,並減輕開發人員查詢可一起工作的依賴項並使它們保持最新狀態的負擔。建立這樣一個系統的投資已經顯示出在需要時我們能夠以多快的速度推出新服務的好處。Kotlin使我們的開發人員可以專注於他們的業務用例,而花費更少的時間編寫最終將在純Java生態系統中使用的樣板程式碼。總體而言,我們對選擇Kotlin感到非常滿意,並期待繼續改進語言和生態系統。
根據我們的經驗,我們強烈建議後端工程師將Kotlin作為主要語言。Kotlin作為更好的Java的想法對DoorDash來說是正確的,因為它可以提高開發人員的工作效率並減少執行時發現的錯誤。這些優勢使我們的團隊能夠專注於解決業務需求,提高敏捷性和速度。我們將繼續投資Kotlin作為我們的未來,並希望繼續與更大的生態系統合作,為Kotlin開發更強大的案例作為伺服器開發的主要語言。
 

相關文章