Software Engineering at Google

谷歌的軟件工程

2017年1月31日

弗格斯·亨德森

<fergus@google.com>(工作)<fergus.henderson@gmail.com>(私人)

摘要

本文記載和描述了谷歌關(guān)鍵的軟件工程實踐。

作者簡介

??弗格斯·亨德森在谷歌從事了超過10年的軟件工程師工作。在1979年還是孩子時他就開始編程,之后進行編程語言設(shè)計和實踐的學(xué)術(shù)研究。他和他的博士學(xué)位導(dǎo)師在墨爾本大學(xué)合作創(chuàng)立的研究小組開發(fā)出了Mercury語言。他是八次國際性會議的程序委員會委員,并且已經(jīng)提交了超過50萬行開源代碼。他曾是Usenet的comp.std.c++新聞組主持人,并被官方認可為ISO C和C++ 委員會的“技術(shù)專家”。弗格斯有超過15年商業(yè)軟件產(chǎn)業(yè)經(jīng)驗,他是谷歌軟件構(gòu)建工具Blaze最初的開發(fā)者之一,如今Blaze已經(jīng)在谷歌內(nèi)部廣泛使用。他參與了服務(wù)器端軟件背后的語音識別、語音操作(早于Siri)和語音合成工作。他目前主管著谷歌的文本到語音轉(zhuǎn)化工程團隊,但他仍在編寫和評審大量的代碼。他寫出的軟件如今安裝在超過10億臺設(shè)備上,每天被使用超過10億次。

目錄

摘要
作者簡介
目錄
1.引言
2.軟件開發(fā)
? 2.1. 源碼倉庫
? 2.2. 構(gòu)建系統(tǒng)
? 2.3. 代碼評審
? 2.4. 測試
? 2.5. 缺陷追蹤
? 2.6. 編程語言
? 2.7. 調(diào)試和分析工具
? 2.8. 發(fā)布工程
? 2.9. 啟動批準
? 2.10. 事后剖析
? 2.11. 頻繁重寫

3.項目管理
? 3.1. 20%時間
? 3.2. 目標和關(guān)鍵成果(OKRs)
? 3.3. 項目批準
? 3.4. 企業(yè)重組

4.人員管理
? 4.1. 角色
? 4.2. 設(shè)施
? 4.3. 培訓(xùn)
? 4.4. 調(diào)動
? 4.5. 績效考核與獎勵

5.結(jié)論

致謝

參考文獻

1.引言

??谷歌是一家取得巨大成功的公司。同它成功的搜索引擎和AdWords一樣,谷歌還開發(fā)了許多其他杰出的產(chǎn)品,包括谷歌地圖、谷歌新聞、谷歌翻譯、谷歌語音識別、Chrome和Android等。谷歌同樣大大提升和擴展了許多通過收購YouTube等小公司得到的產(chǎn)品,而且對許多開源項目做出了顯著的貢獻。谷歌還展示了許多將要發(fā)布的驚人產(chǎn)品,無人駕駛汽車就在其列。

??谷歌成功的原因有很多:開明的領(lǐng)導(dǎo)、偉大的人物、極高的招聘條件,以及在一個極速增長的市場中成功通過早期確立的領(lǐng)先優(yōu)勢帶來的財務(wù)實力。還有一個引領(lǐng)谷歌走向成功的原因是,谷歌開發(fā)出了杰出的軟件工程實踐。基于世界上最有才華的軟件工程師們智慧的積累和提煉,這些實踐隨著時間推移一直在進步。我們想要和全世界分享這些實踐中的知識,以及我們一路上在犯錯中學(xué)習到的東西。

??這篇文章旨在簡要地記載、描述谷歌關(guān)鍵的軟件工程實踐。其他組織或個人可以將其與自己的軟件工程實踐進行比較和對比,考慮是否應(yīng)用其中的一些實踐。

??許多作者(如引用[9]、[10]、[11])都寫了書籍或文章來分析谷歌的成功和歷史,但其中絕大多數(shù)都主要關(guān)注商業(yè)、管理和企業(yè)文化;只有一小部分(如引用[1]、[2]、[3]、[4]、[5]、[6]、[7]、[13]、[14]、[16]、[21])研究過軟件工程方面,這些書籍和文章中的大多數(shù)都只探討一個方面;并且所有書籍和文章都沒有進行關(guān)于谷歌軟件工程實踐的總結(jié),所以本文將提供一個整體的谷歌軟件工程實踐概述。

2.軟件開發(fā)

2.1. 源碼倉庫

??谷歌的絕大多數(shù)代碼都存放在一個統(tǒng)一的源碼倉庫中,所有為谷歌工作的軟件工程師都可以訪問它。有一些值得關(guān)注的例外,特別是兩個大型開源項目Chrome和Android,它們存放在單獨的開源倉庫中,還有一些高價值和涉及重要安全的代碼被嚴格控制其訪問權(quán)限??偟膩碚f,大多數(shù)谷歌的項目都分享使用同一個源碼倉庫,截止2015年1月,這個86TB的倉庫中存儲有10億個文件,包括含有總計20億行代碼的900萬個源碼文件,這些代碼已經(jīng)有了3500萬次的提交歷史,并且提交數(shù)量以每天4萬次的速度不斷增長著[18]。這個倉庫的寫入權(quán)限被以下方式控制:只有在被列出的倉庫子樹所有者才能批準對子樹的變更。但通常情況下,所有軟件工程師都可以訪問任意一部分代碼,他們可以查看、構(gòu)建、做本地修改、測試,還可以將變更發(fā)送給代碼所有者進行評審,如果評審?fù)ㄟ^,這部分變更就會被提交到倉庫中。無論項目的邊界如何,谷歌的企業(yè)文化鼓勵所有工程師學(xué)習如何修復(fù)代碼并且實踐于任何他們覺得存在錯誤的項目中。這增加了工程師的自主權(quán)并使工程師能夠獲得更高質(zhì)量的基礎(chǔ)架構(gòu),從而更好滿足用戶的需求。

??幾乎所有的開發(fā)活動都發(fā)生在倉庫的“頭部”而非分支,這樣有助于盡快識別集成問題所在、極大地減小合并分支的工作量,還可以更快更容易地推出安全修復(fù)程序。

??自動化測試系統(tǒng)頻繁工作,通常在每次測試的傳遞依賴項中任何文件更改后運行,盡管這么做并不總是可行。當某次變更導(dǎo)致測試結(jié)果失敗時,系統(tǒng)會在幾分鐘內(nèi)自動通知變更提交者和評審員。很多團隊通過放置醒目的標志物甚至是掛有彩色編碼燈的雕塑來使自己的構(gòu)建狀態(tài)變得顯眼(綠色表示構(gòu)建成功并且所有測試用例通過,紅色表示有一些測試用例沒有通過,黑色表示構(gòu)建失?。?,這種方法有助于讓軟件工程師追求成功的構(gòu)建。大多數(shù)大型團隊還有一名“構(gòu)建警察”來保證測試持續(xù)通過,他們在提交違規(guī)變更的開發(fā)者身旁工作來快速修復(fù)問題或者回滾違規(guī)變更(這些構(gòu)建警察一般是團隊成員輪流擔任,或者由團隊中有經(jīng)驗的成員擔任)。即使在非常巨大的團隊中,這種專注于成功構(gòu)建的模式也能使得開發(fā)工作變得更加實用。

??代碼所有權(quán)。倉庫中每個子樹中都有一個存儲著子樹“所有者”用戶id的文件,子目錄一般從父目錄那里繼承所有者,也可以選擇抑制繼承。如下文的代碼評審部分所述,每個子樹的所有者控制著子樹的寫入權(quán)限。每個子樹——尤其是那些地理位置上分散的團隊——需要有至少兩名所有者,實際應(yīng)用中人數(shù)會更多。常見情況是團隊中所有成員都會被列為子樹所有者。因為不僅是團隊成員,谷歌的任意工程師都會向子樹提交變更,所以變更必須得到團隊成員的批準。這樣可以確保每次變更都被了解這個軟件的工程師所評審。

??想要了解更多關(guān)于谷歌源碼倉庫的內(nèi)容,可以閱讀引用[17]、[18]和[21];想了解另一個大公司如何應(yīng)對類似的挑戰(zhàn),可以閱讀引用[19]。

2.2. 構(gòu)建系統(tǒng)

??谷歌使用分布式系統(tǒng)Blaze來進行編譯、鏈接軟件以及運行測試用例。Blaze提供用于跨整個倉庫工作的構(gòu)建軟件和測試軟件的標準命令。這些標準命令和高度優(yōu)化的實現(xiàn)意味著任何谷歌的軟件工程師通常都可以十分簡單迅捷地構(gòu)建和測試倉庫中的任何軟件,這種一致性是工程師跨項目邊界進行變更活動的關(guān)鍵推動因素。

??程序員編寫B(tài)laze所使用的如何構(gòu)建目標軟件的“BUILD”文件,例如庫、程序和測試用例等構(gòu)建實體是使用相當高級的聲明性構(gòu)建規(guī)范聲明的,規(guī)范對每個構(gòu)建實體指定名稱、源文件以及它所依賴的庫或其他構(gòu)建實體。這些構(gòu)建規(guī)范由名為“構(gòu)建準則”的聲明組成,每個聲明都指定如“這個C++庫帶有這些源文件,這些源文件依賴于這些其他庫”這樣的高級概念,構(gòu)建系統(tǒng)需要將每個構(gòu)建規(guī)則映射到一組構(gòu)建步驟,例如:編譯、鏈接每個源文件的步驟,以及要確定所使用的編譯器和編譯標志的步驟。

??在某些情況下,特別是Go語言程序,構(gòu)建文件可以被自動生成(并更新),因為BUILD文件中的依賴關(guān)系(通常)是源文件中依賴關(guān)系信息的抽象,但是它們?nèi)匀粫粚懭雮}庫。這樣保證了構(gòu)建系統(tǒng)能夠僅通過分析構(gòu)建文件(而不需要源文件)來確定依賴關(guān)系,避免了構(gòu)建系統(tǒng)與所支持的許多不同語言的編譯器、分析工具間的過度耦合。

??構(gòu)建系統(tǒng)的實現(xiàn)使用到谷歌分布式計算基礎(chǔ)架構(gòu),每個構(gòu)建工作通常分布在成百上千的物理機器中運行,這使得構(gòu)建超大項目或者并行執(zhí)行數(shù)千個測試用例成為可能。

??單個的構(gòu)建步驟必須是“封閉”的,它們僅僅依賴于聲明的輸入。分布構(gòu)建的結(jié)果是強制所有的依賴都要被正確聲明,只有聲明的輸入才可以被送到運行構(gòu)建步驟的機器。因此,可以根據(jù)構(gòu)建系統(tǒng)來了解真正的依賴關(guān)系,甚至是構(gòu)建系統(tǒng)調(diào)用到的編譯器也可以被一并視為輸入。

??單個的構(gòu)建步驟是確定的,因此構(gòu)建系統(tǒng)能緩存構(gòu)建結(jié)果。軟件工程師能從一個舊的變更版本同步自己的工作區(qū),也可以重新構(gòu)建并且將會得到和之前完全一致的二進制文件。另外,這個緩存機制能在不同用戶間安全地共享。(為了讓緩存機制正常工作,我們必須消除構(gòu)建調(diào)用的工具中的非確定因素,例如:清除生成輸出文件中的時間戳。)

??構(gòu)建系統(tǒng)是可靠的。它會追蹤構(gòu)建準則本身變更的依賴性,如果產(chǎn)生目標文件的操作變更時,即便是操作的輸入沒有改變,系統(tǒng)也知道重新構(gòu)建目標文件,如構(gòu)建時編譯器選項的更改就會導(dǎo)致上述構(gòu)建系統(tǒng)行為。構(gòu)建系統(tǒng)還能適當處理以部分方式中斷構(gòu)建的問題,或在構(gòu)建過程中更改源碼文件問題:這種情況下你只需要重新運行構(gòu)建命令。所以永遠沒必要在這個構(gòu)建系統(tǒng)上運行類似“make clean”的命令。

??構(gòu)建結(jié)果,包括中間結(jié)果都緩存在云平臺上。即便是不同用戶的另一個構(gòu)建請求需要相同的結(jié)果,構(gòu)建系統(tǒng)也不會重新構(gòu)建,而是自動復(fù)用結(jié)果。

??增量重新構(gòu)建是很快的。構(gòu)建系統(tǒng)會駐留在內(nèi)存中,所以對于重新構(gòu)建命令,系統(tǒng)會分析出自上次構(gòu)建以來更改過的文件。

??提交前檢查。當初始化一次代碼評審和/或準備提交一次變更時,谷歌有會自動運行的一系列測試工具開始工作。源碼倉庫的每個子樹都有一份配置文件,文件里確定哪些測試需要運行,在代碼評審時運行、在提交前立即運行還是兩種情況都運行。測試可以是同步的,即在發(fā)送變更以供評審之前和/或在提交變更到源碼倉庫前(適用于快速運行的測試);測試也可以是異步的,結(jié)果將通過電子郵件發(fā)送給代碼評審討論主題。(評審主題是進行代碼評審的電子郵件主題,主題內(nèi)的所有信息都會展示在Web端的代碼評審工具中。)

2.3. 代碼評審

??谷歌建立起了優(yōu)秀的Web端代碼評審工具,集成有電子郵件,作者可以請求評審,評審員可以并排查看代碼差異(有漂亮的顏色編碼來體現(xiàn)視覺差異)并對其進行評論。當變更的作者啟動一次代碼評審時,評審員會收到電子郵件通知,電子郵件中包含一條對應(yīng)Web評審工具中這次評審頁面的鏈接。當評審員添加代碼評論時,電子郵件也會發(fā)給作者。此外,自動化工具可以發(fā)送通知,例如自動化測試的結(jié)果或者靜態(tài)分析工具的發(fā)現(xiàn)。

??所有對主源碼倉庫的變更“必須”被至少一名工程師評審。此外,如果不是某個文件所有者的工程師提交了關(guān)于這個文件的更改,那么必須由至少一名文件的所有者評審并且批準變更。

??在特殊情況下,子樹的所有者可以在評審前對子樹進行緊急提交變更,但他仍然必須指定評審員。直到評審?fù)ㄟ^前,變更的作者和評審員肯定會被指責做這種緊急提交。在這種情況下,解決評審意見所需的任何修改必須在新的變更中提交,因為原始的變更已經(jīng)被提交了。

??谷歌有工具通過查看被更改的代碼的所有權(quán)和作者、最近評審員的歷史以及每個潛在評審員的待審代碼審閱次數(shù),自動為給定的更改建議評審員。一次變更影響的每個子樹的至少一個所有者必須審查并批準變更。但除此以外,作者可以自由選擇他們認為合適的評審員。

代碼評審的一個潛在問題是,如果評審員回復(fù)太慢或者極度不愿意贊成變更,這可能潛在地拖慢開發(fā)。事實上讓代碼的編寫者選擇他們的評審員有助于避免這樣的問題,允許工程師避開可能過度占用他們的代碼的評審員,或允許工程師向較不全面的評審員發(fā)送簡單變更的審閱請求,向更有經(jīng)驗的評審員或者多個評審員發(fā)送更加復(fù)雜的變更的審閱請求。
?
??每個項目的代碼評審的討論會自動復(fù)制到項目維護者指定的郵件列表中。任何人都可以在提交變更之前和之后自由評論任何變更,不管他們是否被指定為這個變更的評審員。如果發(fā)現(xiàn)了一個缺陷,追蹤引入它的變更并對原始代碼評審線程進行注釋來指出錯誤是很常見的,以便原始作者和評審員們能夠意識到這個錯誤。

??工程師也可以向幾個評審員發(fā)送代碼評審,然后一旦其中一位評審員批準(當然前提是作者或者第一個響應(yīng)的評審者是所有者),在其他評審員評論前,立即提交更改,并在后續(xù)的更改中處理隨后的任何評審注釋。這可以減少評審的周轉(zhuǎn)時間。

??倉庫除了主要部分之外,還有一個“實驗”部分,在這個部分正常的代碼評審請求不被強制執(zhí)行。然而,在產(chǎn)品中運行的代碼必須存儲在倉庫的主要部分,并且強烈鼓勵工程師在倉庫的主要部分編寫開發(fā)代碼,而不是在實驗區(qū)開發(fā)然后再移動到主要部分。因為相比在代碼開發(fā)之后,代碼評審在代碼編寫中完成是最有效的。在實踐中工程師甚至經(jīng)常對實驗中的代碼請求代碼審查。

??工程師們被鼓勵保持較小的單個變更,并最好把大的變更分解成一系列更小的變更,以便一個評審員能夠一次性更簡單地審閱。這也使編寫者更容易對每個評審中提出的主要更改中做出反應(yīng);非常大的變更經(jīng)常太死板并且反對評審員建議的更改。一種被鼓勵的保持變更較小的方法1 是使用代碼檢查工具,用這次變更的大小標記每次代碼評審。對30-99行代碼的添加/刪除/移除的更改被標記為“中等大小”,對超過300行代碼的變更被標記為越來越貶低的標簽,例如“大”(300-999)、“極其巨大”(1000-1999)等。(然而,以一種典型的谷歌式的方式,在每年的幾天內(nèi),如在每年的“像海盜一樣談話的日子”里,用有趣的替代說法來代替這些熟悉的描述,以此來保持樂趣。:)

1 這在近幾年有所改變。最新版本的代碼檢查工具不再對大型的變更標記使用貶義標簽,但是它們?nèi)匀槐挥么笮擞?,例如“S”、“M”、“L”、“XL”。

2.4. 測試

??單元測試在谷歌得到了強烈支持和廣泛實行。所有產(chǎn)品中使用的代碼都被預(yù)期擁有單元測試,并且如果源文件在沒有對應(yīng)的測試時被添加,代碼檢查工具會突出顯示。代碼評審員通常要求任何增加新功能的更改都應(yīng)該增加新的測試來覆蓋新功能。模擬框架(它允許構(gòu)建輕量級單元測試,甚至對于依賴重量級庫的代碼也是如此)相當流行。

??集成測試和回歸測試也被廣泛應(yīng)用。

??正如上面在“提交前檢查”中討論的,測試可以作為代碼審查和提交過程的一部分自動執(zhí)行。

??谷歌還擁有測量測試覆蓋率的自動化工具。測量結(jié)果也被整合為源代碼瀏覽器中的可選層。

??部署前的負載測試在谷歌也是必不可少的。團隊預(yù)計會生成一個顯示關(guān)鍵度量的表格或圖表,特別是延遲和錯誤率隨著輸入請求的速率如何變化。

2.5. 缺陷追蹤

??谷歌使用一個名為Buganizer的缺陷跟蹤系統(tǒng)追蹤問題:缺陷、特性請求、客戶問題和工序流程(比如發(fā)布或清理工作)。缺陷被分類為分級組件,每個組件可以具有默認的受托人和給次級接收者的默認郵件列表。當發(fā)送源更改以供評審時,工程師會被提示將變更與特定的發(fā)行編號相關(guān)聯(lián)。

??谷歌的團隊(盡管不是所有)經(jīng)常按時掃描他們組件中的未決問題,確定它們的優(yōu)先級,并在適當?shù)臅r候?qū)⑺鼈兎峙浣o特定的工程師。一些團隊有一個人專門負責缺陷的分級,其他的團隊在定期的團隊會議上進行缺陷的分級。谷歌的許多團隊利用缺陷上的標簽來指示是否已經(jīng)對缺陷進行了篩選,以及每個缺陷預(yù)定在哪個版本修復(fù)。

2.6. 編程語言

??在谷歌我們強烈鼓勵軟件工程師使用谷歌官方批準的五種編程語言之一來編寫程序:C++,Java,Python,Go或JavaScript。最小化被使用的不同編程語言的數(shù)量可以減少代碼重用和程序員協(xié)作的障礙。

??谷歌還有針對每種語言的谷歌風格指南,以確保那種語言的代碼在整個公司都以相似的風格、布局、命名規(guī)約等編寫。此外,還有一個全公司的可讀性培訓(xùn)過程,在這個過程中,關(guān)心代碼可讀性的經(jīng)驗豐富的工程師通過回顧重大變化或者一系列變化來訓(xùn)練其他工程師如何用特定語言編寫可讀的、慣用的代碼,直到評審員滿意地認為作者知道如何使用那種語言編寫可讀的代碼。每一項在特定語言中添加不平凡的新代碼的更改都必須得到用該語言通過了“可讀性”培訓(xùn)過程的人的批準。

??除了這五種語言之外,還有許多專門的領(lǐng)域特定語言用于特定的目的(例如,用于指定構(gòu)建目標及其依賴項的構(gòu)建語言)。

??這些不同編程語言之間的互操作主要使用Protocol Buffers來完成。Protocol Buffers是一種以高效但可擴展的方式對結(jié)構(gòu)化數(shù)據(jù)進行編碼的方法。它包括用于指定結(jié)構(gòu)化數(shù)據(jù)的特定于域的語言,以及包含這樣描述的編譯器,并在C++、Java、Python中生成代碼,用于構(gòu)建、訪問、序列化和反序列化這些對象。谷歌的Protocol Buffers版本與谷歌的RPC庫集成,支持簡單的跨語言RPC,請求和響應(yīng)的序列化和反序列化由RPC框架自動處理。

??即使擁有龐大的代碼庫和語言多樣性,過程的通用性仍是使開發(fā)變得容易的關(guān)鍵:只有一組命令來執(zhí)行所有常見的軟件工程任務(wù)(比如檢查、編輯、構(gòu)建、測試、評審、提交、文件缺陷報告等),并且相同的命令可以被無論什么項目或語言使用。開發(fā)人員不需要因為他們正在編輯的代碼碰巧是不同項目的一部分或者用不同語言編寫而學(xué)習新的開發(fā)過程。

2.7. 調(diào)試和分析工具

??谷歌服務(wù)器與提供多個調(diào)試運行服務(wù)器的工具的庫鏈接。萬一服務(wù)器崩潰,信號處理器將自動將堆棧跟蹤轉(zhuǎn)儲到日志文件,并保存核心文件。如果崩潰是由于堆內(nèi)存耗盡造成的,服務(wù)器將轉(zhuǎn)儲活動堆對象的采樣子集的分配站點的堆棧跟蹤。還有用于調(diào)試的Web接口,允許檢查傳入和傳出的RPC(包括定時、錯誤率、速率限制等)、更改命令行標志值(例如增加特定模塊的日志詳細性)、資源消耗、簡要介紹等。這些工具極大地提高了調(diào)試的整體易用性,達到了很少啟動gdb等傳統(tǒng)調(diào)試器的階段。

2.8. 發(fā)布工程

??少數(shù)團隊有專門的發(fā)布工程師,但對于谷歌的大多數(shù)團隊來說,發(fā)布工程工作是由普通的軟件工程師完成的。

??對于大多數(shù)軟件來說,發(fā)布是經(jīng)常進行的。每周或每兩周發(fā)布是常見的目標,有些團隊甚至每天發(fā)布。通過自動化大多數(shù)普通的發(fā)布工程任務(wù)使其成為可能。頻繁發(fā)布有助于使工程師保持積極(為某個要到未來幾個月甚至幾年才會發(fā)布的東西感到激動是更難的事情),并且通過允許更多的迭代來提高總體速度,從而內(nèi)獲得更多的機會來反饋和更多的機會來回應(yīng)反饋。

??一次發(fā)布通常從一個新的工作區(qū)開始,通過同步到最新的“綠色”構(gòu)建的更改編號(所有自動測試都通過的最后一次更改),制作發(fā)布分支。發(fā)布工程師可以選擇其他更改為“挑選”,即從主分支合并到發(fā)布分支。然后軟件將從頭開始重建,并運行測試。如果任何測試失敗,則會進行額外的更改以修復(fù)故障,并將這些額外的更改挑選到發(fā)布分支上,之后將重建軟件并重新運行測試。當測試全部通過時,內(nèi)置的可執(zhí)行文件和數(shù)據(jù)文件被打包。所有這些步驟都是自動的,所以工程師只需要運行一些簡單的命令,甚至只是選擇一個菜單驅(qū)動的用戶界面的一些條目,并選擇其中的變化(如果有的話)來挑選。

??一旦候選構(gòu)建被打包,它通常被加載到“staging”服務(wù)器上,以便由一小組用戶(有時只是開發(fā)團隊)進行進一步的集成測試。

??一種有用的技術(shù)不僅將來自生產(chǎn)流量的請求的副本(的一部分)發(fā)送到臨時服務(wù)器,而且將這些相同的請求發(fā)送到當前的生產(chǎn)服務(wù)器以進行實際處理。臨時服務(wù)器的響應(yīng)被丟棄,來自現(xiàn)場生產(chǎn)服務(wù)器的響應(yīng)被發(fā)送回用戶。這有助于確保在將服務(wù)器投入生產(chǎn)之前能夠檢測到可能導(dǎo)致嚴重問題(例如服務(wù)器崩潰)的任何問題。

??下一步通常是向一個或多個正在處理一部分實時生產(chǎn)流量的“canary”服務(wù)器推送。與“staging”服務(wù)器不同,這些服務(wù)器對真實用戶進行處理和響應(yīng)。

??最后,發(fā)布版本可以被推送到所有數(shù)據(jù)中心的所有服務(wù)器。對于非常高流量、高可靠性的服務(wù),這是通過幾天內(nèi)的逐步推出來完成的,以幫助減少由于新引入的未被前面任何步驟捕獲的錯誤而導(dǎo)致的任何中斷的影響。

??有關(guān)谷歌發(fā)布工程的更多信息,請參閱SRE圖書第8章[7]。也見[15]。

2.9. 啟動批準

??任何用戶可見的更改或重大的設(shè)計更改的啟動都需要執(zhí)行更改的核心工程團隊之外的許多人的批準。特別地,需要批準(通常要經(jīng)過詳細審查)以確保代碼符合法律要求、隱私要求、安全要求、可靠性要求(例如,具有適當?shù)淖詣颖O(jiān)控以檢測服務(wù)器中斷并自動通知適當?shù)墓こ處煟?、業(yè)務(wù)需求等。

??發(fā)布過程還被設(shè)計成確保每當重要的新產(chǎn)品或功能發(fā)布時,公司內(nèi)部合適的人員都會得到通知。

??谷歌有一個內(nèi)部啟動批準工具,用于跟蹤所需的審查和批準,并確保符合每個產(chǎn)品定義的啟動過程。該工具易于定制,因此不同的產(chǎn)品或產(chǎn)品區(qū)域可以擁有不同的所需審查和批準集。

??有關(guān)啟動過程的更多信息,請參閱SRE圖書第27章[7]

2.10. 事后剖析

??每當我們的生產(chǎn)系統(tǒng)發(fā)生重大中斷或類似事故時,相關(guān)人員就需要編寫事后剖析文檔。該文檔描述了該事件,包括標題、摘要、影響、時間線、根本原因、什么生效/什么沒有生效以及行動條目。關(guān)注的是問題以及如何在未來避免它們,而不是針對人員或分配責任。影響部分嘗試根據(jù)中斷持續(xù)時間,丟失查詢數(shù)(或RPC失敗等)和收入來量化事件的影響。時間線部分給出了導(dǎo)致中斷的事件的時間線以及診斷和糾正中斷的步驟。什么生效/什么沒有生效部分描述了所吸取的教訓(xùn)——哪些做法有助于快速檢測和解決問題,出現(xiàn)了什么問題,以及可以采取哪些具體行動(最好歸檔為分配給特定人員的缺陷)來降低未來類似問題的可能性和/或嚴重性。
??有關(guān)谷歌交付后驗收文化的更多信息,請參閱SRE圖書第15章[7]。

2.11. 頻繁重寫

??谷歌的大部分軟件每幾年就會重寫一次。

??這看起來代價高昂。事實上,它確實消耗了谷歌資源的很大一部分。然而,它還具有一些關(guān)鍵優(yōu)勢,這些優(yōu)勢對于Google的敏捷性和長期成功至關(guān)重要。在幾年的時間里,隨著軟件環(huán)境和周圍的其他技術(shù)的變化,以及隨著技術(shù)或市場的變化影響用戶需求、愿望和期望,產(chǎn)品的需求發(fā)生顯著變化是平常的。幾年前的軟件是圍繞著一組較舊的需求而設(shè)計的,通常不是以對當前需求最理想的方式設(shè)計的。此外,它通常積累了大量的復(fù)雜性。重寫代碼減少了處理不再那么重要的需求的所有不必要的累積復(fù)雜性。此外,重寫代碼是將知識和所有權(quán)感轉(zhuǎn)移給更新的團隊成員的一種方式。這種歸屬感對生產(chǎn)率至關(guān)重要:工程師自然會投入更多的精力開發(fā)特性和修復(fù)他們認為屬于他們的代碼中的問題。頻繁的重寫也鼓勵工程師在不同項目之間的流動,這有助于鼓勵思想的交叉?zhèn)魇凇nl繁重寫也有助于確保代碼是使用現(xiàn)代技術(shù)和方法編寫的。

3.項目管理

3.1. 20%時間

??工程師被允許把最多20%的時間花在自己選擇的任何項目上,而不需要經(jīng)理或其他任何人的批準。這種對工程師的信任是非常有價值的,有以下幾個原因。首先,它允許任何有好主意的人,即使別人不會立即認識到這個主意是有價值的,他們也有足夠的時間來開發(fā)一個原型、演示版或報告以展示他們的想法的價值。其次,它提供了對可能隱藏的活動的可視化管理。在其他沒有允許20%時間的官方政策的公司里,工程師有時會在不通知管理層的情況下從事“臭鼬工程”項目。如果工程師能夠?qū)@些項目持開放態(tài)度,在他們的常規(guī)狀態(tài)更新中描述他們在這些項目上的工作,那就更好了,即使他們的管理層可能不同意項目的價值。有一個公司范圍的官方政策和支持它的文化使這成為可能。第三,通過允許工程師們花一小部分時間在更有趣的事情上,使得工程師們對他們所做的事保持積極和興奮,并防止他們精疲力盡,如果他們覺得被強迫花100%的時間來處理更繁瑣的任務(wù),這種精疲力盡的情況很容易發(fā)生。忙碌的、有進取心的工程師和精疲力盡的工程師之間的生產(chǎn)率差別遠遠超過20%。第四,這種政策鼓勵創(chuàng)新文化??吹狡渌こ處熼_展有趣的實驗性20%項目,會鼓勵每個人都這樣做。

3.2. 目標和主要成果(OKRS)

??谷歌的個人和團隊需要明確記錄他們的目標,并評估這些目標的進展情況。團隊制定了季度目標和年度目標,并以可衡量的關(guān)鍵成果顯示這些目標的進展情況。這是在公司的每一個層級完成的,一直到為整個公司定義目標。個人和小團隊的目標應(yīng)該與他們所在的更大團隊的更高級別目標以及公司總體目標相一致。在每個季度結(jié)束時,記錄可衡量的關(guān)鍵結(jié)果的進展情況,并給每個目標一個分數(shù) ,從0.0(無進展)到1.0(100%完成)。OKRs和OKR得分在谷歌中通常都是可見的(偶爾也會有一些特別敏感的信息,比如高度機密的項目),但它們并沒有直接用于個人的績效評估。

??OKRs應(yīng)該要設(shè)置得高:理想的總體平均得分是65%,這意味著鼓勵團隊將目標設(shè)定為比實際完成的任務(wù)多50%左右。如果一個團隊得分明顯高于這個水平,他們就會被鼓勵為下一季度設(shè)置更有野心的OKRs(反之,如果得分明顯低于這個水平,他們就會被鼓勵在下一季度設(shè)置更保守的OKRs)。

??OKRs提供了一個關(guān)鍵機制,用于溝通公司各個部門正在開展的工作,并通過社群激勵措施鼓勵員工提供良好的績效......工程師們知道他們的團隊將會召開OKRs會議,并且盡管OKRs對績效評估或薪酬沒有直接影響,但他們有一種自然的動力去爭取好成績。 確定客觀和可衡量的關(guān)鍵結(jié)果有助于確保這種表現(xiàn)良好的人力驅(qū)動能夠引導(dǎo)到對實現(xiàn)共同目標的進展產(chǎn)生實際具體可衡量影響的事物上。

3.3. 項目批準

??雖然有一個定義良好的啟動審批流程,但谷歌沒有一個定義良好的項目審批或取消流程。盡管在谷歌工作了10多年,現(xiàn)在我自己也成為了一名經(jīng)理,但我仍然不完全明白這些決定是如何做出的。在一定程度上,這是因為在整個公司,解決這個

??問題的方法并不統(tǒng)一。每個級別的經(jīng)理都要對他們的團隊所從事的項目負責,并在他們認為合適的時候行使他們的判斷力。在某些情況下,這意味著這些決策是以一種自下而上的方式做出的,工程師可以在他們的團隊范圍內(nèi)自由地選擇要從事的項目。在其他情況下,這些決策是以一種自上而下的方式做出的,主管或經(jīng)理決定哪些項目將繼續(xù)進行,哪些項目將獲得額外資源,哪些項目將被取消。

3.4. 公司重組

??有時會做出取消一個大型項目的執(zhí)行決定,然后許多從事該項目的工程師可能不得不在新的團隊中尋找新的項目。類似地,也有偶爾的“重組”工作將跨多個地點的項目合并成更少的地點,某些地點的工程師需要改變團隊和/或項目來實現(xiàn)這一點。在這種情況下,工程師通??梢宰杂傻貜钠涞攸c的可用職位中選擇新的團隊和角色,或者在碎片整理的情況下,他們也可以通過變動職位來選擇留在同一團隊和項目中。

??此外,其他類型的公司重組,如合并或拆分團隊以及報告鏈的變化,似乎是相當頻繁的事情,盡管我不知道谷歌在這方面與其他大公司相比如何。在大型的、技術(shù)驅(qū)動的組織中,當技術(shù)和需求發(fā)生變化時,為了避免組織的低效,可能需要頻繁的重組。

4.人事管理

4.1. 角色

??正如我們將在下面更詳細地解釋的那樣,谷歌將工程和管理職業(yè)發(fā)展階梯分開,將技術(shù)主管角色從管理中分離出來,將研究嵌入到工程中,并為工程師提供產(chǎn)品經(jīng)理、項目經(jīng)理和現(xiàn)場可靠性工程師(SREs)的支持。似乎至少有一些做法對維持谷歌的創(chuàng)新文化很重要。

??谷歌在工程中有少量不同的角色。在每個角色中,都可能有職業(yè)發(fā)展,有一系列的級別,以及晉升(與薪酬相關(guān)的改進,如薪水)的可能性,以表彰下一級別的表現(xiàn)。

??主要角色如下:

  • 工程經(jīng)理

    這是列表中唯一的人員管理角色。軟件工程師等其他角色的個人可以管理人員,但工程經(jīng)理總是管理人員。工程經(jīng)理通常是以前的軟件工程師,并且總是擁有相當多的技術(shù)專長和人際技巧。

    技術(shù)領(lǐng)導(dǎo)和人員管理是有區(qū)別的。工程經(jīng)理不一定領(lǐng)導(dǎo)一個項目;項目由技術(shù)主管領(lǐng)導(dǎo),他可能是工程經(jīng)理,但經(jīng)常是軟件工程師。項目的技術(shù)主管對該項目的技術(shù)決策有最終決定權(quán)。

    經(jīng)理們負責選擇技術(shù)主管,以及對他們團隊的表現(xiàn)負責。他們對職業(yè)發(fā)展進行指導(dǎo)和協(xié)助,進行績效評估(使用來自同行反饋的意見,見下文),并負責薪酬的某些方面。他們還負責招聘流程的某些部分。

    工程經(jīng)理通常直接管理3至30人,但最常見的是8至12人。

  • 軟件工程師(SWE)

    大多數(shù)從事軟件開發(fā)工作的人都有這個角色。谷歌招聘軟件工程師的門檻很高;通過雇傭非常優(yōu)秀的軟件工程師,能避免或最小化許多困擾組織的軟件問題。

    Google在工程和管理方面有不同的職業(yè)發(fā)展順序。 雖然軟件工程師可以管理人員,或轉(zhuǎn)換到工程經(jīng)理的角色,但是管理人員并不是晉升的必要條件,即使是在最高級別。 在更高層次上,展示領(lǐng)導(dǎo)力是必要的,但這可以有多種形式。 例如,創(chuàng)建具有巨大影響力或被許多其他工程師使用的優(yōu)秀軟件就足夠了。 這很重要,因為這意味著那些擁有出色技術(shù)技能但缺乏管理人員的愿望或技能的人仍然擁有良好的職業(yè)發(fā)展道路,而不需要他們走上管理的道路。 這避免了一些組織所面臨的問題,即人們最終因為職業(yè)發(fā)展的原因而進入管理崗位,而忽視了團隊中人員的人事管理。

  • 研究科學(xué)家

    這個角色的招聘標準是非常嚴格的,并且門檻非常高,要求展示出卓越的研究能力,這可以通過出色的出版記錄和編寫代碼的能力得到證明。學(xué)術(shù)界許多能夠獲得軟件工程師資格的非常有才能的人不具備在谷歌擔任研究科學(xué)家職位的資格;大多數(shù)擁有博士學(xué)位的人都是軟件工程師,而不是研究科學(xué)家。研究科學(xué)家對他們包括出版物在內(nèi)的研究貢獻進行了評估,但除以上所訴和不同的頭銜外,軟件工程師和研究科學(xué)家在谷歌的角色之間并沒有太大的區(qū)別。兩者都可以做原創(chuàng)研究和發(fā)表論文,既可以開發(fā)新產(chǎn)品理念和新技術(shù),也可以編寫代碼和開發(fā)產(chǎn)品。 谷歌的研究科學(xué)家通常與軟件工程師一起在相同的團隊中工作,并從事相同的產(chǎn)品或相同的研究。這種將研究嵌入工程中的做法有助于將新的研究輕松地結(jié)合到到運輸產(chǎn)品中。

  • 網(wǎng)站可靠性工程師(SRE)

    操作系統(tǒng)的維護由軟件工程團隊完成,而不是傳統(tǒng)的系統(tǒng)管理員,但SRE的招聘要求與軟件工師職位的要求略有不同(如果有其他專業(yè)知識技能,如網(wǎng)絡(luò)或unix系統(tǒng)作為彌補,軟件工程技能要求可能略低)。SRE角色的性質(zhì)和目的在SRE書中解釋的非常詳細[7],因此我們不在此進一步討論。

  • 產(chǎn)品經(jīng)理

    產(chǎn)品經(jīng)理負責產(chǎn)品的管理;作為產(chǎn)品用戶的倡導(dǎo)者,他們協(xié)調(diào)軟件工程師的工作,向用戶宣傳重要的特性,與其他團隊協(xié)調(diào),跟蹤bug和時間表,確保生產(chǎn)高質(zhì)量產(chǎn)品所需的一切。產(chǎn)品經(jīng)理通常不會自己編寫代碼,而是與軟件工程師合作以確保編寫正確的代碼。

  • 項目集經(jīng)理/技術(shù)項目經(jīng)理

    項目集經(jīng)理的角色和產(chǎn)品經(jīng)理大致相似,但他們管理的不是產(chǎn)品,而是項目、過程或操作(例如數(shù)據(jù)收集)。技術(shù)項目經(jīng)理是類似的,但也需要與他們的工作相關(guān)的專門技術(shù)知識,例如用于處理語音的語言學(xué)。

    軟件工程師與產(chǎn)品經(jīng)理和項目集經(jīng)理的比例因組織而異,但通常很高,例如在4:1到30:1的范圍內(nèi)。

4.2 設(shè)施

??谷歌以其有趣的設(shè)施而聞名,其中包括滑梯,球坑和游戲室。 這有助于吸引和留住優(yōu)秀人才。谷歌很棒的咖啡館對員工免費提供,辦公室也提供這個功能(即免費咖啡)從而巧妙地鼓勵員工留在辦公室;饑餓絕不是離開的理由。隨處可見得“微型廚房”也有相同的作用,員工可以在那獲取零食和飲料,它們也充當了非正式想法交流的一個重要來源,因為很多對話都是從那里開始的。健身房、運動和現(xiàn)場按摩有助于員工保持舒適、健康和快樂,從而提高生工作效率和記憶力。

??谷歌的座位是開放式的,通常相當密集。雖然存在爭議[20],有時以犧牲個人注意力為代價,但它鼓勵交流而且經(jīng)濟。

??員工被分配了一個單獨的座位,但是會經(jīng)常換座(例如,每6-12個月,通常是組織擴展的結(jié)果),由經(jīng)理選擇座位使得促進和鼓勵相鄰或接近相鄰的個人之間的交流更加容易。

??谷歌配備了最先進視頻會議設(shè)施的會議室,只需輕輕一按即可連接到另一方進行預(yù)先安排的議事日程邀請。

4.3. 培訓(xùn)

??谷歌通過多種方式鼓勵員工接受教育:

  • 新員工(“Nooglers”)有一個強制性的初始培訓(xùn)課程。
  • 技術(shù)人員(SWEs和研究科學(xué)家)從做“Codelabs”開始:短期的個人技術(shù)在線培訓(xùn)課程,以及編碼練習。
  • 谷歌提供員工各種網(wǎng)絡(luò)和面對面的培訓(xùn)課程。
  • 谷歌也為在外部機構(gòu)學(xué)習提供支持。

??此外,每個Noogler通常都被指定一個官方的“導(dǎo)師”和一個單獨的“伙伴”來幫助他們跟上進度。非正式的指導(dǎo)還通過與經(jīng)理的定期會議、團隊會議、代碼評審、設(shè)計評審和非正式過程來進行。

4.4. 調(diào)動

??鼓勵公司不同部門之間的人員調(diào)動,以幫助在整個組織內(nèi)傳播知識和技術(shù),并改善跨組織的溝通。在一個職位上工作了12個月的員工可以在項目和/或辦公室之間進行調(diào)動。軟件工程師也被鼓勵在組織的其他部分做臨時任務(wù),例如 在SRE(現(xiàn)場可靠性工程)中進行為期六個月的“輪換”(臨時任務(wù))。

4.5. 績效考核和獎勵

??谷歌非常鼓勵反饋。工程師可以通過“同行獎金”和“榮譽”給予彼此明確的積極反饋。 任何員工都可以提名任何其他員工獲得“同行獎金” ——現(xiàn)金獎勵100美元——每年最多兩次,獎勵超出正常的任務(wù)范圍的工作,只需填寫網(wǎng)絡(luò)表格來描述理由。當獲得同行獎勵時,通常也會通知團隊成員。員工也可以給予“榮譽”,即正式的表揚聲明,為良好的工作提供明確的社群認可,但沒有資金獎勵; 對于“榮譽”而言,并不要求工作超出正常的任務(wù)范圍,也不限制他們可以獲得的次數(shù)。

??經(jīng)理還可以發(fā)放獎金,包括項目完成時的獎金。和許多公司一樣,谷歌的員工會根據(jù)自己的表現(xiàn)獲得年度績效獎金和股權(quán)獎勵。

??谷歌有一個非常細致的晉升過程,包括自我提名或經(jīng)理提名,自我審查,同行審查,經(jīng)理評估;隨后,晉升委員會根據(jù)這些意見做出實際決定,其結(jié)果可由晉升審查委員會進一步審查。確保合適的人員獲得晉升對于維持對員工的正確激勵至關(guān)重要。

??另一方面,績效不佳則由經(jīng)理反饋處理,必要時還會采取績效改進計劃,其中包括制定非常明確的具體績效目標和評估實現(xiàn)這些目標的進展。如果失敗,員工可能會因表現(xiàn)不佳而被解約,但實際上這在谷歌極其罕見。

??通過反饋調(diào)查評估經(jīng)理的績效; 每位員工被要求每年填寫兩次關(guān)于其經(jīng)理績效的匿名調(diào)查,然后把調(diào)查結(jié)果匯總給經(jīng)理。這種向上反饋對于維護和提高整個組織的管理質(zhì)量是非常重要的。

5.結(jié)論

??我們簡要介紹了谷歌過去的大多數(shù)關(guān)鍵軟件工程實踐。當然,谷歌現(xiàn)在是一個龐大而多樣化的組織,組織的某些部分有不同的做法。但是谷歌的大多數(shù)團隊通常會遵循此處描述的做法。

??由于涉及許多不同的軟件工程實踐,以及與我們的軟件工程實踐無關(guān)的谷歌成功的其他許多原因,因此很難提供任何定量或客觀證據(jù)來將個別實踐與改進的結(jié)果聯(lián)系起來。 然而,這些實踐在谷歌經(jīng)受了時間的考驗,受到數(shù)千名優(yōu)秀軟件工程師的集體主觀判斷。

??對于其他組織中那些主張使用本文中描述的特定實踐的人來說,也許會說“這對谷歌來說已經(jīng)足夠好了”。

致謝

??特別感謝Alan Donovan的非常詳細和建設(shè)性的反饋,并感謝Y aroslav Volovich,UrsH?lzle,Brian Strope,Alexander Gutkin,Alex Gruenstein和Hameed Husaini對本文早期草稿的非常有益的評論。

參考文獻

[1] Build in the Cloud: Accessing Source Code , Nathan York,
http://google-engtools.blogspot.com/2011/06/build-in-cloud-accessing-source-code.html
[2] Build in the Cloud: How the Build System works , Christian Kemper,
http://google-engtools.blogspot.com/2011/08/build-in-cloud-how-build-system-works.htm
[3] Build in the Cloud: Distributing Build Steps, Nathan York
http://google-engtools.blogspot.com/2011/09/build-in-cloud-distributing-build-steps.html
[4] Build in the Cloud: Distributing Build Outputs, Milos Besta, Yevgeniy Miretskiy and Jeff Cox
http://google-engtools.blogspot.com/2011/10/build-in-cloud-distributing-build.html
[5] Testing at the speed and scale of Google , Pooja Gupta, Mark Ivey, and John Penix, Google
engineering tools blog, June 2011.
http://google-engtools.blogspot.com/2011/06/testing-at-speed-and-scale-of-google.html
[6] Building Software at Google Scale Tech Talk, Michael Barnathan, Greg Estren, Pepper
Lebeck-Jone, Google tech talk.
http://www.youtube.com/watch?v=2qv3fcXW1mg
[7] Site Reliability Engineering , Betsy Beyer, Chris Jones, Jennifer Petoff, Niall Richard Murphy,
O'Reilly Media, April 2016, ISBN 978-1-4919-2909-4.
https://landing.google.com/sre/book.html
[8] How Google Works, Eric Schmidt, Jonathan Rosenberg.
http://www.howgoogleworks.net
[9] What would Google Do?: Reverse-Engineering the Fastest Growing Company in the History
of the World , Jeff Jarvis, Harper Business, 2011.
https://books.google.co.uk/books/about/What_Would_Google_Do.html?id=GvkEcAAACAAJ&redir_esc=y
[10] The Search: How Google and Its Rivals Rewrote the Rules of Business and Transformed
Our Culture , John Battelle, 8 September 2005.
https://books.google.co.uk/books/about/The_Search.html?id=4MY8PgAACAAJ&redir_esc=y
[11] The Google Story , David A. Vise, Pan Books, 2008.
http://www.thegooglestory.com/
[12] Searching for Build Debt: Experiences Managing Technical Debt at Google, J. David
Morgenthaler, Misha Gridnev, Raluca Sauciuc, and Sanjay Bhansali.
http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37755.pdf
[13] Development at the speed and scale of Google , A. Kumar, December 2010, presentation,
QCon.
http: //www.infoq.com/presentations/Development-at-Google
[14] How Google Tests Software , J. A. Whittaker, J. Arbon, and J. Carollo, Addison-Wesley,
?2012.
[15] Release Engineering Practices and Pitfalls , H. K. Wright and D. E. Perry, in Proceedings of
the 34th International Conference on Software Engineering (ICSE ’12) , IEEE, 2012, pp.
1281–1284.
http://www.hyrumwright.org/papers/icse2012.pdf
[16] Large-Scale Automated Refactoring Using ClangMR , H. K. Wright, D. Jasper, M. Klimek, C.
Carruth, Z. Wan, in Proceedings of the 29th International Conference on Software Maintenance
(ICSM ’13) , IEEE, 2013, pp. 548–551.
[17] Why Google Stores Billions of Lines of Code in a Single Repository , Rachel Potvin,
presentation.
https://www.youtube.com/watch?v=W71BTkUbdqE
[18] The Motivation for a Monolithic Codebase , Rachel Potvin, Josh Levenberg, to be published
in Communications of the ACM, July 2016.
[19] Scaling Mercurial at Facebook, Durham Goode, Siddharth P. Agarwa, Facebook blog post,
January 7th, 2014.
https://code.facebook.com/posts/218678814984400/scaling-mercurial-at-facebook/
[20] Why We (Still) Believe In Private Offices , David Fullerton, Stack Overflow blog post,
January 16th, 2015.
https://blog.stackoverflow.com/2015/01/why-we-still-believe-in-private-offices/
[21] Continuous Integration at Google Scale , John Micco, presentation, EclipseCon, 2013.
http://eclipsecon.org/2013/sites/eclipsecon.org.2013/files/2013-03-24%20Continuous%20Integration%20at%20Google%20Scale.pdf

翻譯:安昕瑜 蔡新宇 孔慶振

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容