Thoughtworks 的 Ruby

Thoughtworks 從 2006 年開始將 Ruby 用於生產專案,從那時到 2008 年底,我們已經完成了 41 個 Ruby 專案。為了準備在 QCon 的演講,我調查了這些專案,以檢視我們可以從經驗中汲取哪些教訓。我描述了我們迄今為止對 Ruby 生產力、速度和可維護性的常見問題的想法。到目前為止,我們的結論是 Ruby 是可行的平台,應該認真考慮用於許多形式的應用程式,特別是使用 Ruby on Rails 的網路應用程式。我也會探討一些技術課程,包括一些關於使用 Active Record 進行測試的想法。

2009 年 6 月 11 日



我的雇主 Thoughtworks 主要是軟體交付公司。我們為人們打造軟體,包括為自己打造的產品。我們哲學的一個重要部分是對不同開發平台的開放性,因此我們可以為廣泛的客戶選擇適當的平台。當我在 2000 年加入 Thoughtworks 時,Java 是我們壓倒性的主要平台。不久之後,我們開始使用 .NET,這兩個平台在十年中期主導了我們的業務。

然而,少數人已開始嘗試使用 LAMP 腳本語言,特別是 Ruby。Ruby on Rails 網路框架的出現讓 Ruby 大受歡迎,足夠讓我們在 2006 年開始使用 Ruby 平台進行一些嚴肅的專案工作。當我在 2009 年撰寫本文時,Ruby 平台在我們的業務中佔有穩定的份額,雖然不如 Java 和 C#,但仍佔有相當大的比例。

在這三年中,我們在實務中學到了很多關於 Ruby 的知識。2009 年初,我被要求在 QCon 會議上發表關於我們使用 Ruby 經驗的演講。為了準備這場演講,我對我們的 Ruby 專案進行了廣泛的調查,並詢問我們的 Ruby 領導者他們的想法和經驗。我花了比預期更長的時間才寫出這篇文章,但它終於完成了。

我將本文分為三個部分。首先,我將檢視我們的 Ruby 專案經驗概況,讓您了解多年來我們一直在處理哪些類型的專案。接下來,我將探討幾個關於 Ruby 的常見問題,以及我們的經驗如何回答這些問題。最後,我將深入探討我們從使用 Ruby 學到的教訓。

專案的形狀

在 2006-8 年間,Thoughtworks 參與了約 41 個 Ruby 專案。我將 Ruby 專案定義為以 Ruby 為主要開發語言的專案。Ruby 也出現在其他專案中,最近有許多使用 Ruby 進行建置自動化或 Java 專案功能測試的開發。幾乎所有這些專案都涉及 Rails,而且大多數都是網站專案,其中 Rails 的重要性至少與 Ruby 相同。

圖 1:2006-8 年間 Thoughtworks Ruby 專案的參與人數與參與時間的散佈圖。

圖 1 讓我們了解我們參與專案的規模。這裡的人數是所有參與人員(Thoughtworks、客戶和其他人員;開發人員、專案經理、分析師等)的高峰人數。時間是我們參與專案的期間。

Ruby 專案通常被視為比其他專案短且小。遺憾的是,我沒有其他平台專案的比較資料,無法更深入了解此說法是否正確。我們當然可以看出,大多數專案的人數少於 20 人,時間少於一年。

有幾個專案很突出。到目前為止,我們最大的專案是我稱為亞特蘭大專案的專案,高峰人數超過 40 人。另一個大型且長期的專案是 Jersey 專案。這兩個專案有關係,因為兩者之間有相當程度的人員輪調,因此我們許多較有經驗的 Ruby 人員都參與過這兩個專案。

我這裡提到的第三個專案是 Mingle,這是 Thoughtworks Studios 的產品,因此是一個特別有趣的案例,而且我們可以公開討論它,而不是我們為客戶執行的專案。這是一個長期專案,也是一個國際專案:從澳洲開始,移到北京,現在在北京和舊金山設有多個據點。

圖 2:顯示專案每年工作量的條形圖。

圖 2 以不同的方式檢視形狀,檢視我們參與的各個專案在各個年度所投入的心力。條狀圖上的每個點代表該年度一個專案的總心力(所有人員)。此圖表可清楚看出過去三年來 Ruby 專案的成長幅度。

圖 3:顯示每個主辦國專案心力的條狀圖

圖 3 檢視各個主辦國的專案。由於我尚未妥善處理少數幾個跨站點專案或已搬遷的專案(例如,我將 Mingle 分類為中國,儘管其歷史較為多元),因此此圖表較為粗略。

國家分布顯示,美國對 Ruby 工作最感興趣。印度也表現得相當不錯,我們的首個 Ruby 專案確實是在班加羅爾執行。英國的採用率較低。這可能反映出我們早期的 Ruby 倡導者大多來自美國,而英國對 Ruby 抱持相當大的懷疑。印度的參與程度令人鼓舞,傳統上印度被視為採用新技術的落後者,但我們似乎在讓印度辦公室變得截然不同方面做得相當不錯。

我們銷售 Ruby 工作的經驗是,使用 Ruby 等動態語言非常符合我們的整體吸引力。我們的優勢在於我們聘用難以吸引到一般 IT 組織的高素質人員。Ruby 的哲學是提供一個能讓有才華的開發人員發揮更大影響力的環境,而不是試圖保護較無才華的開發人員免於錯誤。因此,像 Ruby 這樣的環境讓我們的開發人員更有能力發揮其真正的價值。

Ruby 也符合我們偏好使用敏捷軟體開發流程。敏捷哲學是透過建置軟體並定期與客戶檢視,來快速回饋。開發環境的生產力越高,您就能更頻繁地檢視進度,而敏捷的「檢查和調整」流程就能發揮得越好。

關於 Ruby 的問題

Ruby 是正確的選擇嗎?

回顧我們的 41 個專案,或許最重要的問題是 Ruby 平台是否為正確的選擇。探討此問題的方法之一是詢問專案中的技術負責人,他們是否認為事後看來這個選擇是正確的。

圖 4:Ruby 是否為此專案的正確平台選擇?

圖 4 所示,投票結果非常正面,36 票支持、5 票反對。作為一個團隊,我們的技術主管通常不會害羞地表示他們對技術選擇不滿意。因此,我將此視為 Ruby 平台作為合理選擇的可行性之明確聲明。

我進一步深入探討了五個令人遺憾的專案。首先顯而易見的是,在五個案例中的四個案例中,主管認為使用 Ruby 並不比其他選擇更差。Ruby 的相對罕見性意味著我們認為使用 Ruby 必須比其他選擇帶來好處,如果 Ruby 與使用更廣泛的選項相同,那麼引入不尋常的技術就沒有價值。五個案例中的四個案例也回報了由於與 Ruby 不太適合的其他技術整合而產生的問題。例如,.NET 工具往往與 .NET 技術整合得更好。兩個專案回報的另一個主題是社會問題,即客戶組織中的人反對 Ruby 或其他動態語言。表現最差的專案顯示了這些社會問題,一個 IT 組織極力抵制 Ruby(此案例中的業務贊助商是 Ruby 愛好者)。

的確,當我進一步詢問在軟體專案中使用 Ruby 的警訊時,唯一的明確答案與社會問題有關。Ruby 通常被接受或鼓勵用於我們的軟體開發工作,但避免它的最大徵兆是客戶的社會阻力。

Ruby 的生產力比較高嗎?

當詢問人們為什麼應該在專案中使用 Ruby 時,最常見的答案是提高生產力。一個早期的指標是對一個專案的評估,該評估表明 Ruby 將帶來生產力大幅提升。

因此,很明顯要調查專案技術主管並詢問他們有關生產力的問題,即 Ruby 是否提高了生產力,如果是,提高了多少。我請他們將此與他們所知的最具生產力的方式完成的主流(Java 或 .NET)專案進行比較。

圖 5:Ruby 為此專案提高了多少生產力?(與您所知的最佳主流工具相比。)

您應該對這些結果持保留態度。畢竟,我們無法客觀衡量軟體生產力。這些只是每個專案技術主管的主觀、定性評估。(我並未收到所有專案的回應。)然而,它們仍然表明確實存在生產力大幅提升的情況。

此建議進一步獲得人事考量的支持。管理我們亞特蘭大辦公室的 Scott Conley 報告說,一旦一個 Ruby 專案開始進行,他預計他們需要比其他技術多 50% 的人員專注於需求準備。

我們看到的一件事是,你不應該期望這些生產力提升會立即出現。我聽過好幾次,人們在 Ruby 新團隊進展緩慢的初期感到驚慌,這是因為我所謂的 進步鴻溝。Ruby 團隊需要時間才能掌握平台運作方式,在那段時間內,他們的進度會比你預期的慢。

進步鴻溝是一種常見現象,通常的緩解方法是確保團隊中有一些經驗豐富的人員。然而,我們的歷史經驗是,這裡最重要的經驗是支援 Ruby 所具備的各種元程式設計功能的動態語言,而不是特別的 Ruby 經驗。正如 Scott Conley 所說:差別在於效率風險和交付風險。具備動態語言經驗但 Ruby 經驗較少的團隊一開始會較慢(效率風險),但沒有任何動態語言經驗的團隊可能會產生難解的程式碼庫,這可能會危及整體交付。

Ruby 速度慢嗎?

簡而言之,「是的」。在網路上搜尋基準,你會發現許多調查顯示,即使以腳本語言的標準來看,Ruby 也是一隻烏龜。

然而,整體而言,這與我們無關。我們大多數使用 Ruby 的地方是在建立資料庫後端的網站。在過去的幾十年中,我參觀過許多像這樣的專案,使用 Ruby 和其他技術,幾乎每個專案都花時間處理效能問題,而且在幾乎所有情況下,這些效能問題都是資料庫存取。人們花時間調整 SQL,而不是調整他們的處理程式碼。因此,由於大多數應用程式都是 I/O 繫結的,使用較慢的語言進行處理並不會對系統的整體效能產生顯著影響。

你會注意到我在上一個段落中使用了常見的專家模棱兩可的用語。儘管幾乎每個專案都是 I/O 繫結的,但你確實會遇到偶爾的例外,而一個有趣的例外是 Mingle。Mingle 在許多方面都不尋常。它非常動態的顯示方式表示它無法使用任何頁面快取來提升效能,這立即使其與大多數網路應用程式不同。因此,它不是 I/O 繫結的,而且為了獲得良好的效能,需要比許多人預期的更多硬體(一個四核心盒子,配備 2GB 記憶體,以支援 20-40 人的團隊)。

但 Mingle 團隊仍然認為他們在使用 Ruby 時做出了正確的選擇。Mingle 團隊非常快速地建構了許多功能,他們認為從 Ruby 獲得的生產力提升值得最終產品更高的硬體需求。與許多事情一樣,這是一個硬體與生產力的權衡取捨,這是電腦中最古老的權衡取捨之一。每個團隊都需要思考哪個更重要。這裡的好消息是,Mingle 具有良好的橫向擴充性(向它投入更多處理器,你會獲得成比例的良好效能)。在這些情況下,硬體擴充性通常是你能擁有的最有價值的東西,因為硬體成本持續下降。

我應該再次強調。對於大多數專案,Ruby 的速度不相關,因為它們幾乎都受 I/O 限制。Mingle 是例外,不是一般情況。

Ruby 程式碼庫難以理解嗎?

我們經常聽到的關於 Ruby 的疑慮是,它的動態型別、對元程式設計的支援,以及缺乏工具,使得它容易留下難以追蹤的程式碼庫。一般來說,這在實務上對我們來說並非問題。我聽到的說法是,你可以為相同功能撰寫更少的程式碼,這表示讓程式碼保持乾淨比主流語言更容易。

話雖如此,記住我們的背景很重要。Thoughtworks 開發人員在能力方面往往遠高於平均水準,而且也非常熱衷於高度有紀律的方法,例如極限程式設計。我們非常重視測試(這是 Ruby 社群普遍存在的現象),而這些測試有助於讓程式碼庫保持清晰。因此我無法說我們的經驗是否會延續到能力較差且缺乏紀律的開發人員身上。(即使其他語言的工具和相對控制也無法阻止我們看到一些非常糟糕的程式碼,因此值得懷疑一個糟糕的 Ruby 程式碼庫是否會更糟糕。)

我們已經看到對元程式設計的一系列常見態度。

圖 6:對元程式設計的感受進程

  • 可怕且糟糕:人們對元程式設計感到戒慎恐懼,而且不太使用它
  • 可怕且良好:人們開始看到元程式設計的價值,但仍然不習慣使用它。
  • 容易且良好:隨著人們感到自在,他們開始過度使用它,這可能會使程式碼庫複雜化。
  • 容易且糟糕:人們對元程式設計感到戒慎恐懼,並意識到它在小劑量時非常有用。

最後,我最喜歡的這些技術的類比是,它們就像處方藥。它們在少量時非常有價值,但你需要確保不要過量服用。

與許多事物一樣,經驗在此扮演著重要的幫手角色,因為它能讓你更快速地度過這條曲線。特別重要的是預期這條採用曲線,特別是過度使用。在學習新事物時,過度使用是很常見的階段,因為不跨越界線,很難知道界線在哪裡。建立沙盒也很有幫助,也就是程式碼庫中一個相對封閉的區域,供人們過度使用元程式設計。有了適當的沙盒,以後要取消過度使用就容易多了。

Ruby 是可行的平台嗎?

所有這些問題都歸結到對我們來說的關鍵問題:Ruby(和 Rails)對我們和我們的客戶來說是否是一個可行的平台。到目前為止,答案是響亮的「是」。它提供了顯著的生產力提升,讓我們能夠更靈活地回應,並為我們的客戶更快速地製作出更好的軟體。這並不是說它在所有情況下都是正確的選擇。選擇開發平台永遠不是一個簡單的選擇,特別是因為它通常更像是社會選擇,而不是技術選擇。但標題結論是,Ruby 是值得考慮的選擇,值得我們將此工具保留在工具箱中。

這裡一個有趣的附帶問題是其他較不常見語言的角色。我們應該使用 Groovy、F#、Python、Smalltalk 和其他語言嗎?對於 Ruby 來說,我們看到許多相同的權衡,對於這些其他語言來說也是如此,這一點並不讓我感到意外。我希望未來我們能看到其中一些語言加入我們的工具箱。

我還應該強調,在使用這些語言和主流 Java/C# 選項時,並不是非此即彼的情況。我一直主張使用 Java/C# 等語言的開發團隊也應該使用指令碼語言來執行各種支援任務。Ruby 是個絕佳的選擇,而且我們看到這種組合在我們的專案中越來越多。隨著這些語言在 JVM 和 CLR 上獲得支援,我們看到更多機會將不同強項的不同語言混合在一起,尼爾·福特將這種方法稱為多語程式設計

一些開發秘訣

在最後一節中,我將快速瀏覽一下我們從使用 Ruby 中學到的經驗。

使用 Active Record 進行測試

在我們開始使用 Ruby 之初,就有一場辯論,討論在 Rails 中存在 Active Record 資料庫層的情況下,如何最好地組織測試。基本問題在於,大多數時候,企業應用程式的效能是由資料庫存取決定的。我們發現,透過使用測試替身,我們可以大幅加快我們的測試速度。對於我們測試密集的開發流程來說,擁有快速的測試至關重要。肯特·貝克建議基本提交建置時間在十分鐘以內。我們大多數專案現在都能做到這一點,而使用資料庫替身是實現此目標的重要部分。

Active Record 的問題在於將資料庫存取程式碼與商業邏輯結合,因此要建立資料庫替身會比較困難。Mingle 團隊對此的反應是接受 Rails 緊密繫結資料庫,因此針對真實資料庫執行所有提交測試。

相反的觀點是由亞特蘭大和 Jersey 團隊最堅定地主張。Ruby 有一個強大的功能,允許您在執行時重新定義方法。您可以使用此功能取得一個 active record 類別,並重新定義該類別中的資料庫存取方法作為存根。該團隊啟動了寶石 unitrecord 來協助這項工作。

三年來,我們尚未看到這場辯論中普遍接受的勝利者。Mingle 團隊針對一個真實的 postgres 資料庫執行數千個測試,約需 8 分鐘。(他們平行化測試以使用多個核心。)亞特蘭大和 Jersey 團隊認為,他們的提交測試使用存根在 2 分鐘內執行完成,而不用存根則需 8 分鐘,這一點很有價值。取捨在於直接資料庫測試的簡潔性與存根測試的更快速提交建置之間。

雖然兩支團隊都對他們在這次辯論中的立場大致滿意,但存根的使用已為亞特蘭大/Jersey 團隊帶來另一個問題。隨著團隊熟悉使用方法存根,他們使用得越來越多 - 陷入不可避免的過度使用,其中單元測試會存根除了正在測試的方法之外的所有方法。這裡的問題,就像使用替身時常發生的,是測試脆弱。當您變更應用程式的行為時,您還必須變更許多模擬舊行為的替身。這種過度使用已導致兩支團隊遠離存根單元測試,並使用更多具有直接資料庫存取權的 rails 風格功能測試。

Active Record 記憶體外洩

人們回報的常見情況是花費時間在 SQL 上。Active Record 在隱藏許多資料庫存取方面做得很好,但無法全部隱藏 - 抽象基本上會外洩。因此,人們必須花費合理的時間直接使用 SQL。

這種外洩是物件/關聯性對映架構的常見特徵。我幾乎每次與專案人員交談時,他們都會說 O/R 對映架構在 80-90% 的時間有效地隱藏 SQL,但您確實需要花一些時間處理 SQL 才能獲得良好的效能。因此,在這方面,Active Record 真的與任何其他 O/R 對映器沒有不同。

的確,我確實聽過一個評論,即使用 Active Record,抽象會乾淨地中斷。與 DHH 聊天時,他總是強調,他相信使用關聯式資料庫的開發人員應該知道如何使用 SQL。Active Record 簡化了常見的情況,但一旦您開始進入更複雜的場景,它便希望您直接使用 SQL。

我不認為 O/R 抽象的滲漏性是這些架構的譴責。這些架構的重點是透過讓常見事務更容易執行來提升生產力。它讓團隊可以將精力集中在少數真正重要的案例上。問題只會出現在團隊相信抽象是水密時,而且不花任何精力在使用 SQL 上。這是一個常見的缺點,但並不是在正確使用時放棄 O/R 架構的真正優點的理由。

長時間執行要求

我們看過一個常見問題,即應用程式在執行需要花費一些時間的任務時會陷入糾纏。如果天真地執行,這可能會導致網路請求處理常式在處理請求時長時間處於黑暗狀態,令人擔憂。

這是任何人類介面都很常見的問題,而且有一個常見的解決方案,即將任務交給背景處理程序或執行緒。任何使用豐富用戶端 GUI 應用程式進行程式設計的人都會認出執行類似這樣的事情。然而,如果交接和通訊執行不當,人們就會陷入困境。

我比較偏好的路線,而且幸運的是,大多數 ThoughtWorkers 都同意,就是使用執行者。在此模型中,網路請求處理常式會採用任何長時間執行的任務,將其包裝在命令中,並將其放入佇列中。然後,背景執行者會監控佇列,從佇列中取出命令並執行它們,在完成每個命令時向人類互動執行者發出訊號。佇列通常從資料庫中的表格開始,然後在需要時遷移到真正的訊息佇列系統。

與 Active Record 的滲漏性一樣,我指出這一點並不是因為這是 Rails 應用程式的特定問題,我們在各種應用程式中都看過這個問題。這一點值得指出,因為對於許多使用 Rails 的人來說,似乎很容易忘記這種事情會發生,因此他們需要使用這種模式。我們發現 Rails 使得網路應用程式的許多重複部分更容易、更快速地執行,但更複雜的部分仍然存在。

部署

Rails 應用程式很容易建置,但遺憾的是,部署非常困難。使用多個 mongrel 網路伺服器的常見情況充其量只是設定起來相當繁瑣。由於與 Ruby 體驗的許多其他部分的順暢性形成鮮明對比,這一點相當突出。

目前的共識是,Phusion Passenger 讓這整件事變得非常簡單,現在是使用 MRI 的建議部署方法。

我們也一直是使用 JRuby 進行部署的忠實愛好者。JRuby 讓人們可以使用標準的 Java Web 應用程式堆疊,這讓許多企業環境的處理變得容易許多。Mingle 也使用這種方法,讓客戶可以更輕鬆地安裝。事實上,Mingle 團隊使用 MRI 進行所有開發,但部署到 JRuby。他們這樣做的原因是,MRI 的啟動時間較快,讓開發更快速。(JRuby 需要 JVM 啟動,這會明顯猶豫不決。)

控制 Gem

Ruby 包含一個套件管理系統,Ruby Gems,讓安裝和升級第三方程式庫變得容易。Rails 也有外掛程式,對 Rails 執行類似的任務。這些都是好工具,但如果不同的機器設定了不同版本的不同程式庫,團隊很容易陷入糾纏。

有幾種方法可以處理這個問題。第一種方法涉及取得所有第三方程式庫的原始程式碼副本,並將其檢查到原始碼控制中。這樣一來,簡單的簽出即可取得所有程式庫的所有正確版本。第二種方法是使用一個腳本,下載並啟用所有程式庫的正確版本。這個腳本需要保留在原始碼控制中。

沿著類似的路線,大多數團隊也會取得 Rails 原始程式碼本身的副本。這讓他們可以直接對 Rails 應用程式補丁,以修正任何錯誤或其他重要問題。這些補丁然後可以傳送給核心團隊。使用分散式版本控制系統,例如 git,讓這變得更容易管理。這肯定比我們過去必須反編譯和修補 Java 應用程式伺服器的記憶容易得多。

安排更新時間

Ruby 一般來說,特別是 Rails,移動得很快。Rails 系統有頻繁的更新,其中包含我們想要使用的功能。我們發現,我們需要確保排定時間來處理 Rails 更新,並將這些更新納入規劃程序。它們比其他平台更重要,但好消息是,有源源不絕的新功能。

在 Windows 上開發

Ruby 誕生於 Unix 世界,而大多數湧向這個平台的人使用正斜線作為目錄路徑。可以在 Windows 平台上執行、部署和開發 Ruby,但這也更棘手。我們的建議是,對所有開發都使用 Unix 平台。Mac 通常是首選,但也有很多人使用其他 FOSS Unix。

我們希望隨著 Iron Ruby 的發展,這種情況將會改變。能夠在基本 Unix、JVM 或 CLR 上部署 Ruby 應用程式會是一件很棒的事。這的確會讓 Ruby 成為跨多個平台執行支援的特別靈活選擇。這也有助於我們的 .NET 專案將 Ruby 作為腳本語言,與主線 .NET 語言結合使用。


致謝

比平常更甚的是,我無法在沒有許多同事的合作下,將所有這些整合在一起。儘管我多年來一直將 Ruby 用於許多個人工作,但一個人拼湊他的個人網站與我們與客戶合作的應用程式類型之間有很大的差異。我很感謝有這麼多同事花時間給我資訊,讓我真正評估 Ruby 的價值。

而且就像任何 Ruby 使用者一樣,我們要感謝更廣泛的 Ruby 和 Rails 社群。對於任何開放原始碼的努力,社群的角色至關重要,因此 Thoughtworks 對所有 Ruby 駭客和 Rubyist 說 ありがとうございました。

重大修訂

2009 年 6 月 11 日:首次發佈於 martinfowler.com

2009 年 6 月 03 日:TW 內部審查草稿