從零到生產:Go在Google的歷程
2007年Go誕生于Google,2009年Google正式對外宣布了Go語言的開源!時至今日,距離Go開源已經過去了近15個年頭了[1]!Go在Google公司內部究竟是怎樣的一個狀態呢?前Google員工Yves Junqueira近期撰文從其個人所見所聞談了Go在Google的歷程[2]!這里簡單翻譯,供大家參考!
最近,Jeremy Mason[3]和Sameer Ajmani[4]撰寫了有關使Go成為Google內部語言之一的傳奇故事。Go目前是世界上第八大最受歡迎的編程語言(譯者注:2024.4,Go已經攀升到第7位,見下圖),并且仍在增長,因此人們有興趣了解Go早期以及它是如何走到這一步的。
我想我應該從SRE、框架開發人員和早期采用者的角度來寫。我分享的所有信息都與谷歌已經公開記錄的系統相關,所以我不認為我泄露了任何秘密。這個故事有一些重要的部分(例如:envelopei(譯者注:不知道是什么鬼))我在其他地方沒有看到提到過,所以我不會討論它們。
破冰:我在Google的Go編程簡介
在Go公開發布之前我就開始關注它,當它發布時,我立即成為了它的粉絲和Google內部的早期用戶。我喜歡它的簡單性。
我在核心庫上做了一些工作,并且在社區中很活躍,早期經常幫助go-nuts郵件列表中的用戶,并編寫開源庫。后來,我幫助組織了西雅圖的Go Meetup[5],并與他人共同組織了備受喜愛的會議Go Northwest[6]。
據我所知,我在Google編寫了第一個生產關鍵型工具,后來又用Go編寫了第一個面向用戶的服務。
第一個是用于監控Google+[7] Bigtable[8]服務器運行狀況的服務。這是我作為SRE的工作之一。Bigtable擁有有關每個tablet性能的詳細內部統計數據,但有時我們需要了解為什么某個tablet如此過載以及系統其他地方發生了什么,以便我們能夠了解根本原因。我們需要隨著時間的推移收集這些數據并進行分析。因此,我構建了一個爬蟲,可以檢查數千臺服務器并在全局儀表板中顯示詳細的統計數據。
2011 年,Andrew Gerrand在接受The Register采訪[9]時提到了這項工作。他當時向我證實,這指的是我的項目。我很興奮!他在采訪中這樣說道:
谷歌有管理應用程序和服務的人員,他們需要編寫工具來抓取幾千臺機器的狀態并聚合數據,”他說。“以前,這些操作人員會用Python編寫這些內容,但他們發現Go在性能和實際編寫代碼的時間方面要快得多。”
Go的運行速度和編寫速度確實更快。最重要的是,使用起來很有趣。它讓我更有效率,所以我迷上了Go!
低級庫:節點身份驗證和RPC
當Go啟動時,它無法與Google的內部基礎設施通信。
首先,團隊必須構建一個基于proto buffer的stubby RPC 系統[10]。這需要實現LOAS[11]來加密和驗證與遠程節點的通信,并使用Chubby[12] 進行名稱解析(類似于kubernetes中使用的etcd[13])。
Stubby和Chubby是出了名的復雜。Stubby需要一個復雜的狀態機來管理連接,但大部分繁重的工作都是由Chubby完成的,即使Borg[14] 節點耗盡CPU,或者因為有人正在運行map reduce而占用了所有機架的交換機帶寬而導致暫時的網絡斷開連接,Chubby也需要提供一致的world view,這很容易陷入死鎖或可靠性問題。
根據海勒姆定律[15],系統的所有可觀察行為都將取決于某人,因此團隊必須確保與現有生產網絡預期的行為完全匹配,并注意極端情況。例如,眾所周知,健康檢查很容易出錯,不應該太嚴格,否則當網絡的一部分暫時過載或與另一部分斷開連接時,它們會為級聯故障敞開大門。必須實現的其他的分布式系統功能,例如backend subsetting和負載均衡。我們需要診斷何時出現問題,因此很早就添加了日志記錄和指標庫。
為了找到要通信的host:port,服務使用Chubby進行名稱解析(name resolution)。它作為少量數據的完全一致的存儲系統,其最常用的功能是解析BNS[16] 地址 - 類似于你今天在kubernetes中使用etcd看到的功能。
系統使用Stubby協議向其他服務發送數據并從其他服務接收數據。在Stubby(如gRPC[17])中,消息使用proto buffer wire format進行編碼。使用反射在運行時創建proto buffer有效負載會太慢并且占用大量資源。工程師還會錯過來自強類型系統的反饋。出于這些原因,谷歌為所有語言使用了生成代碼庫。幸運的是,proto buffer與語言無關。團隊只需為現有構建系統邏輯編寫Blaze[18] 擴展,瞧,我們就為所有內部RPC服務提供了高質量的客戶端庫代碼。
奇怪的是,為另一種語言生成代碼會產生少量的增量構建時間成本,而Google擁有成千上萬的RPC服務。因此,我們決定每個RPC服務的所有者必須選擇允許構建系統為其特定服務生成Go代碼。雖然有點官僚主義,但隨著時間的推移,我們看到數千個CL(谷歌的等效Pull請求)飛來飛去,將Go添加到每個服務的生成代碼集中。這對于我們的社區來說是一個愚蠢但有趣的進度衡量標準,因為我們可以計算代碼庫中“啟用 Go”標志的實例數量。
影響全局Master選擇和Bigtable引流執行
作為這些早期庫的早期采用者和專注于生產系統的工程師,我能夠了解內部系統的工作原理。我幫助調試并解決了許多奇怪的問題。隨著時間的推移,我獲得了構建系統來自動化運維SRE工作的信心。注意到我們的服務中大多數面向用戶的中斷發生在存儲層(Bigtable 或 Colossus),我產生了創建一個控制系統的想法,該系統可以監視Bigtable分區的運行狀況,并在檢測到問題時在GSLB[19]中小心地清空它們。當時,當發生中斷時,SRE會進行分頁,在確認這是存儲問題后,他們會簡單地清空集群并返回睡眠狀態。
我想用適當的控制系統取代這個手動whackamole。抽取流量可能會導致級聯故障,因此這是一項危險的操作。當時,大多數SRE不想在自動化系統中冒這種風險。幸運的是,我有一個很好的團隊。他們仔細審查了我的提案,提供了有關潛在故障模式的大量反饋,我們最終提出了一個我們有足夠信心的設計。我們需要仔細聚合來自不同監控系統的信息(這可能會失敗或提供不正確的數據),使用全局負載均衡器安全地離開集群,然后最終在Buganizer[20] 中開具ticket,以便待命的SRE在工作期間進行處理。
系統需要多個副本始終處于運行狀態以對中斷做出反應,但一次只有一個副本保持活動狀態至關重要。為了支持這一點,我為Go編寫了一個全局“主選舉(master election)”庫,它將確保系統的單個副本一次處于活動狀態。它使用全局Chubby鎖服務來提供一個高級庫來告訴應用程序開始運行或在無法證明我們持有“全局鎖”時自行關閉。
為了支持這項工作,我還到處編寫了一些小實用程序,并與Go團隊合作修復錯誤。我報告了我發現的問題,他們修復了這些問題。
當時,Go團隊的重點是外部用戶。他們所有的注意力都集中在發布Go 1.0上。這是一個資源很少的小團隊,但他們的“秘密武器”是他們是杰出的工程師,而且團隊非常高效。不知何故,盡管針對內部用戶的支持時間非常有限,但他們還是很好地完成了支持工作。內部郵件列表非常活躍,谷歌員工大多在業余項目中使用Go,但Go團隊采用了非常強大的內部流程來使事情順利運行。他們仔細審查了每個人的代碼,并幫助建立了強大的內部代碼質量文化。每當他們發布新的Go候選版本時,他們都會使用新版本重建所有內部項目并重新運行我們的測試以確保一切正常。他們總是以正確的方式做事。
生產中JID代理部署的最初洞察
幾個月后,我在Google用Go編寫了第一個面向用戶的服務。我所說的面向用戶的意思是,如果它停止工作,許多面向用戶的產品將停止工作。這是一個簡單的RPC服務,但所有Google消息服務都使用它。
該服務根據從另一個RPC服務獲取的內部用戶ID將數據與[JID格式](https://en.wikipedia.org/wiki/JID_(Jabber "JID格式")) 相互轉換。該服務很簡單,但規模很大,當時每秒執行數十萬個請求。它對于為Android、Hangouts和其他產品提供支持的Google消息服務核心至關重要。
這次遷移是Google Go的一個非常重要的測試平臺。重要的是,它為我們提供了一個令人難以置信的基礎來比較Go與其他生產語言(特別是 Java)的性能。該服務正在取代難以維護的基于Java的服務(不是因為Java,而是因為其他原因),因此我們使用實際生產流量同時運行這兩個服務,并密切比較它們的性能。
我們從第一個大規模實驗中吸取了重要的教訓:Go使用比Java更多的CPU內核來服務相同的流量,但垃圾收集(GC) 暫停非常短。作為一個努力減少GC暫停以改善面向用戶的服務的尾部延遲的SRE,這是非常有希望的。Go團隊對這個結果很滿意,但他們并不感到驚訝:Go只是在做它設計的事情!
事實上,幾年后,當SRE領導層正式審查Go的生產就緒情況[21]并要求Go團隊確保Go具有良好的GC性能時,我認為這很大程度上只是形式上的。Go很早就證明了Go具有出色的GC性能,并且多年來它不斷變得更好。
遇到內部庫缺失的情況
在早期,在Flywheel[22]之前,在dl.google.com[23] 服務之前,在Vitess[24]之前,Go被Google的大多數工程師忽視了。如果有人想向用戶交付產品,他們首先必須編寫基本構建塊,讓他們連接到谷歌的其他服務。對于大多數人來說,這是不可能的。
鎖服務(chubby)和RPC系統(stubby)的底層庫相對較快地出現(同樣,Go團隊非常優秀),Google最重要的庫是與我們存儲系統的接口:Bigtable、 Megastore、Spanner、Colossus。如果你想讀取或寫入數據,你基本上還不能使用Go。但是,慢慢地,Go團隊(有時與核心基礎設施團隊合作)開始應對這一挑戰。
他們最終一一為Bigtable、Colossus甚至Spanner 創建了庫(不是Megastore,因為它很大程度上是一個被Spanner 取代的庫)。這是一項重大成就。
Google的Go 使用量仍然有限,但我們的社區正在不斷壯大。我在Google開設了第一門官方的Go編程簡介課程,并幫助位于蘇黎世的Google員工找到了可以使用Go進行工作的有趣項目。大約在這個時候我終于獲得了Go的“可讀性”(譯者注:這似乎是Go團隊對代碼review者資格的一種認可),后來加入了Go可讀性團隊。
需要站點可靠性工程師來指導應用程序功能
Go中缺少的另一件事是與生產相關的功能,我們多年來了解到這些功能對于生產團隊來說是必需的。也就是說,如果你想運行大型系統而不需要一直處于運維和救火模式。
每當發生中斷并診斷根本原因時,隨著時間的推移,我們會了解到系統中應該改進的弱點。目標是減少停機和運維開銷。很多時候,為了使系統更加可靠,我們必須對應用程序運行時進行更改。我們很難理解我們需要觀察和控制系統以使其真正可靠的細節深度。
例如,我們需要確保,除了記錄傳入請求之外,應用程序還應該記錄有關該操作中涉及的傳出請求的詳細信息。這樣,我們就可以確定地指出,比如說,我們的“CallBob”服務在上午 11:34 變慢是因為“FindAddress”調用的延遲增加。當我們操作大型系統時,我們不能滿足于猜測工作和弱相關性。有太多的轉移注意力和根本原因查找工作需要處理。我們需要對原因有更高的確定性:我們希望看到失敗的特定請求確實經歷了高延遲,并排除其他解釋(即:未觸發緩慢的 FindAddress 調用的傳入請求不應失敗)。
同樣,多年來我們注意到SRE的大部分時間都花在團隊之間的協調上,以確定一個服務每秒應發送到另一個服務的確切連接數和請求數,以及如何準確建立這些連接。例如,如果多個服務想要連接到后端,我們希望清楚哪些節點正在連接到哪些其他節點。這稱為后端子集化(backend subsetting)。需要仔細調整,考慮整個系統的健康狀況,而不僅僅是一個節點或一對節點的健康狀況,而是整個網絡的健康狀況。太大的子集會導致資源占用過多,太小的子集會導致負載不平衡。因此,隨著時間的推移,SRE團隊開始幫助維護用于與其服務通信的客戶端庫,以便他們可以檢測正在發生的情況,并保留對其他節點與其系統通信方式的一些控制。
揭開魔法:Go服務器工具包
SRE共同擁有客戶端庫的模型在實踐中運行得非常好,隨著時間的推移,我們了解到向這些庫添加流量和負載管理是一個好主意。
- 當你的系統開始過載時,你會如何處理傳入的RPC?
- 你應該將這些請求保留在隊列中,還是立即拒絕它們?
- 你應該使用哪些指標來確定你的系統是否過載?
- 當系統的太多部分認為它們過載時,如何避免進入級聯故障?
Alejo Forero Cuervo 在SRE書籍章節“處理過載”[25]中寫了一些經驗教訓,值得一讀。我們一一向庫中添加了謹慎的邏輯,以根據經驗和內部傳感器自動設置這些參數。
在《不斷發展的SRE參與模型[26]》中,我的前同事 Ashish Bhambhani和我的前老板Acacio Cruz解釋說,我們最終發展了SRE參與模型,以包括服務器框架(server framework)的工作和采用。該模型使SRE能夠直接影響系統在細微差別領域的行為,這得益于我們豐富的現場經驗。
我和我的SRE團隊希望將這些功能引入Go,但它們對于Go團隊來說太過奇特和專業,無法處理。我設立了一個20%的項目(后來變成了一個全職項目),并招募了一群愿意做出貢獻的經驗豐富的工程師。我飛往紐約,會見了一位非常出色的Go團隊成員,我們共同努力為Go中的“服務器框架”構建了路線圖。
Go團隊一開始不太愿意接受我們的方法。整個“框架”概念對他們來說有點危險。這可能會成為一場宗教戰爭,但Go團隊花時間詳細解釋了他們擔心的原因。Sameer尤其具有一種不可思議的能力,能夠用技術術語反思和解釋為什么他認為某件事以某種方式比另一種方式效果更好。
Sameer強烈認為,Go不應該有不一致的開發人員體驗,無論是內部還是外部,無論是否有“框架”。如果Google有不同的方法來構建Go應用程序,那將對內部Go社區造成損害。與他的擔憂一致,我們的20%人組成的烏合之眾團隊竭盡全力確保我們的“框架”感覺更像是另一個庫,而不是一個框架,并且它不會為Go引入不同的編程模型。目標是通過簡單的庫導入來引入我們的可靠性功能。如果你使用我們的庫包裝你的Go HTTP或Stubby服務器,所有內容在代碼中看起來都一樣,但你神奇地獲得了開箱即用的日志記錄、檢測、負載卸載、流量管理,甚至每請求級別的實驗性支持。
為了創建這個讓服務變得更好的神奇庫,我們必須對Google的內部RPC庫甚至構建系統進行重大更改 - 以使我們的框架團隊能夠為RPC系統創建任意“擴展”,從而無需任何操作即可無縫運行,并避免接收和發送請求時產生顯著的性能開銷。
結果是值得的。效果非常好。我們的項目使服務變得更容易管理,而無需強加與Go團隊想要的不同的編程風格。為了避免混淆,我們將其稱為服務器“工具包”,它成為在Google構建生產就緒系統的正確方法。人們經常在他們的LinkedIn個人資料中引用我們的內部服務器框架:)。它被稱為Goa,不要與不相關的外部Goa 框架[27]混淆。以下是某人LinkedIn個人資料中的示例:
圖片
憑借其生產就緒功能,我們的Go工具包消除了Go內部增長的主要障礙。工程師現在可以確信他們的Go項目的性能與舊的Java和C++項目一樣好,并且可調試。也就是說,增長還沒有完全發生。Go需要一個殺手級用例才能在Google流行起來。
Go在多個SRE團隊中的采用
當時,我所在的SRE團隊在Google具有特殊地位,即社交SRE團隊。我們在SWE和SRE都有出色的工程師和出色的管理人員。所以我們能夠以正確的方式做事。一些SRE團隊正在追尾救火,但我們有幸能夠正確地進行工程設計。這創造了一個良性循環,我們在問題變得嚴重之前不斷解決問題,這意味著我們有時間進一步優化運維,等等。
結果,我們的SRE團隊編寫了很多有用的代碼。像我的高級工程師同事一樣,我幫助人們找到要做的事情,因此我幫助啟動了許多早期的Go中與生產相關的工具。如果其中一個工具發現有問題,它會自動、安全地從整個Bigtable集群中刪除流量。
還有其他與流量和負載管理相關的Java和C++項目,由其他高級工程師領導。這種創新環境吸引了人才,我們不斷取得良好的成果,因此我們的SRE團隊不斷壯大。
我們的工程總監Acacio Cruz[28](負責我們團隊以及山景城的同事所發生的許多積極的事情)非常關注工程效率:我們是否將工程時間用于最有影響力的事情?他明白標準化可以提高效率,而且他看到我們的工程師很高興并且富有成效。他的想法是推動Go成為我們團隊中任何自動化的首選工具。該建議是避免使用Python并使用Go來編寫生產工具。令我驚訝的是,我的隊友沒有人反對。這加速了Go在我們的社交SRE團隊中的使用,很快我們區域之外的人們就注意到了。
核心庫、服務器框架、成功的生產工具和圍繞Go的社交SRE標準化——它們都促成了人們對Go正在成為Google的一種嚴肅語言的看法的改變。
與此同時,SRE已經看到了幾代用Python編寫的工具,這些工具運行得非常好,但隨著時間的推移變得非常難以維護。Google SRE喜歡Python,我們編寫了大量的Python代碼。不幸的是,當時缺乏類型和編譯時語法錯誤檢查導致了許多難以修復的問題:
- 當你從事其他人啟動的項目時,該項目可能有也可能沒有良好的測試覆蓋率。為不是你編寫的代碼添加測試是很困難的。你并不真正知道正在使用什么以及如何使用。所以你最終會測試太多的東西或測試太少的東西。在生產關鍵型工具中,我們在進行更改時不能冒險。
- 當時,人們通常一會兒編寫代碼,一會兒運行測試。如果你在運行測試時才意識到有語法錯誤,也許你已經將上下文切換到執行其他操作,所以現在你必須返回并修復它。這會浪費時間并增加不確定性。
隨著越來越多的SRE開始用Go編寫自動化,很明顯這些團隊很高興并且富有成效,并且不太可能陷入難以維護的代碼中。人們開始意識到,Go項目更容易發展和維護,而這不僅僅是這些項目更新、更干凈或設計得更好的結果。
SRE領導層注意到了這種影響,并決定采取行動并在組織內進行廣泛的溝通:SRE團隊最好使用Go進行與生產相關的項目[29],并避免使用Python。我不知道這在谷歌現在是否被視為獨裁,但當時我認為這感覺像是整個組織范圍內良好的溝通和決策。
Go生產平臺和爆炸式增長
此后事情進展得很快。我們創建了一個從早期就對Go提供強大支持的生產平臺,并用高級抽象取代了許多樣板配置和重復過程。該平臺出現了強勁增長,最終其他平臺也出現了。Go和我們的服務器框架變得無處不在。我最終離開了谷歌,但我仍然快樂地記得那些日子。
雖然我只是該語言的用戶,但觀看一個項目從零到成為前10名的編程語言的經歷教會了我很多東西。我親眼看到,一個強大的團隊,周圍有一個強大的社區,真的可以做出大事。
觀察Go的崛起
我在Google從事Go編程工作改變了游戲規則,讓我對項目的技術方面以及世界著名團隊的運作方式有了深入的了解。隨著項目的進行,我可以清楚地看到Go如何使項目和團隊擴展變得更容易。
Go對簡約設計的強調促進了統一編碼,使新程序員可以輕松地集成到項目中,這一功能在時間緊迫的項目中特別有用。隨著項目的發展,新的庫和工具包也出現了,提高了它的受歡迎程度,并促進了包括Apple、Facebook和Docker在內的幾家大型科技公司的采用。
盡管Rust具有更為廣泛和豐富的功能特性,但Go在各個行業的廣泛接受表明,強大的軟件不一定需要復雜。
回顧過去,很明顯,雖然我們的旅程充滿了挑戰,但每一次的曲折、每一次的調整和進步,都是塑造今天Go的關鍵。隨著社區不斷向前發展,我很高興看到我們下一步的發展方向。
Go gopher由Renee French設計,并根據 Creative Commons 3.0 屬性許可證獲得許可。