《今日頭條Go建千億級(jí)微服務(wù)的實(shí)踐》要點(diǎn):
本文介紹了今日頭條Go建千億級(jí)微服務(wù)的實(shí)踐,希望對(duì)您有用。如果有疑問,可以聯(lián)系我們。
作者:項(xiàng)超
編輯:小智
今日頭條當(dāng)前后端服務(wù)超過80%的流量是跑在 Go 構(gòu)建的服務(wù)上.微服務(wù)數(shù)量超過100個(gè),高峰 QPS 超過700萬,日處理請(qǐng)求量超過3000億,是業(yè)內(nèi)最大規(guī)模的 Go 應(yīng)用.
在2015年之前,頭條的主要編程語言是 Python 以及部分 C++.隨著業(yè)務(wù)和流量的快速增長(zhǎng),服務(wù)端的壓力越來越大,隨之而來問題頻出.Python 的解釋性語言特性以及其落后的多進(jìn)程服務(wù)模型受到了巨大的挑戰(zhàn).此外,當(dāng)時(shí)的服務(wù)端架構(gòu)是一個(gè)典型的單體架構(gòu),耦合嚴(yán)重,部分獨(dú)立功能也急需從單體架構(gòu)中拆出來.
Go 語言相對(duì)其它語言具有幾點(diǎn)天然的優(yōu)勢(shì):
當(dāng)時(shí) Go 的1.4版本已經(jīng)發(fā)布,我曾在 Go 處于1.1版本的時(shí)候,開始使用 Go 語言開發(fā)后端組件,并且使用 Go 構(gòu)建過超大流量的后端服務(wù),因此對(duì) Go 語言本身的穩(wěn)定性比較有信心.再加上頭條后端整體服務(wù)化的架構(gòu)改造,所以決定使用 Go 語言構(gòu)建今日頭條后端的微服務(wù)架構(gòu).
2015年6月,今日頭條開始使用 Go 語言重構(gòu)后端的 Feed 流服務(wù),期間一邊重構(gòu),一邊迭代現(xiàn)有業(yè)務(wù),同時(shí)還進(jìn)行服務(wù)拆分,直到2016年6月,Feed 流后端服務(wù)幾乎全部遷移到 Go.由于期間業(yè)務(wù)增長(zhǎng)較快,夾雜服務(wù)拆分,因此沒有橫向?qū)Ρ戎貥?gòu)前后的各項(xiàng)指標(biāo).但實(shí)際上切換到 Go 語言之后,服務(wù)整體的穩(wěn)定性和性能都大幅提高.
對(duì)于復(fù)雜的服務(wù)間調(diào)用,我們抽象出五元組的概念:(From, FromCluster, To, ToCluster, ?Method).每一個(gè)五元組唯一定義了一類的RPC調(diào)用.以五元組為單元,我們構(gòu)建了一整套微服務(wù)架構(gòu).
我們使用 Go 語言研發(fā)了內(nèi)部的微服務(wù)框架 kite,協(xié)議上完全兼容 Thrift.以五元組為基礎(chǔ)單元,我們?cè)?kite 框架上集成了服務(wù)注冊(cè)和發(fā)現(xiàn),分布式負(fù)載均衡,超時(shí)和熔斷管理,服務(wù)降級(jí),Method 級(jí)別的指標(biāo)監(jiān)控,分布式調(diào)用鏈追蹤等功能.目前統(tǒng)一使用 kite 框架開發(fā)內(nèi)部 Go 語言的服務(wù),整體架構(gòu)支持無限制水平擴(kuò)展.
關(guān)于 kite 框架和微服務(wù)架構(gòu)實(shí)現(xiàn)細(xì)節(jié)后續(xù)有機(jī)會(huì)會(huì)專門分享,這里主要分享下我們?cè)谑褂?Go 構(gòu)建大規(guī)模微服務(wù)架構(gòu)中,Go 語言本身給我們帶來了哪些便利以及實(shí)踐過程中我們?nèi)〉玫慕?jīng)驗(yàn).內(nèi)容主要包括并發(fā),性能,監(jiān)控以及對(duì)Go語言使用的一些體會(huì).
Go 作為一門新興的編程語言,最大特點(diǎn)就在于它是原生支持并發(fā)的.和傳統(tǒng)基于 OS 線程和進(jìn)程實(shí)現(xiàn)不同,Go 語言的并發(fā)是基于用戶態(tài)的并發(fā),這種并發(fā)方式就變得非常輕量,能夠輕松運(yùn)行幾萬甚至是幾十萬的并發(fā)邏輯.因此使用 Go 開發(fā)的服務(wù)端應(yīng)用采用的就是“協(xié)程模型”,每一個(gè)請(qǐng)求由獨(dú)立的協(xié)程處理完成.
比進(jìn)程線程模型高出幾個(gè)數(shù)量級(jí)的并發(fā)能力,而相對(duì)基于事件回調(diào)的服務(wù)端模型,Go 開發(fā)思路更加符合人的邏輯處理思維,因此即使使用 Go 開發(fā)大型的項(xiàng)目,也很容易維護(hù).
Go 的并發(fā)屬于 CSP 并發(fā)模型的一種實(shí)現(xiàn),CSP 并發(fā)模型的核心概念是:“不要通過共享內(nèi)存來通信,而應(yīng)該通過通信來共享內(nèi)存”.這在 Go 語言中的實(shí)現(xiàn)就是 Goroutine 和 Channel.在1978發(fā)表的 CSP 論文中有一段使用 CSP 思路解決問題的描述.
“Problem: To print in ascending order all primes less than 10000. Use an array of processes, SIEVE, in which each process inputs a prime from its predecessor and prints it. The process then inputs an ascending stream of numbers from its predecessor and passes them on to its successor, suppressing any that are multiples of the original prime.”
要找出10000以內(nèi)所有的素?cái)?shù),這里使用的方法是篩法,即從2開始每找到一個(gè)素?cái)?shù)就標(biāo)記所有能被該素?cái)?shù)整除的所有數(shù).直到?jīng)]有可標(biāo)記的數(shù),剩下的就都是素?cái)?shù).下面以找出10以內(nèi)所有素?cái)?shù)為例,借用 CSP 方式解決這個(gè)問題.
從上圖中可以看出,每一行過濾使用獨(dú)立的并發(fā)處理程序,上下相鄰的并發(fā)處理程序傳遞數(shù)據(jù)實(shí)現(xiàn)通信.通過4個(gè)并發(fā)處理程序得出10以內(nèi)的素?cái)?shù)表,對(duì)應(yīng)的 Go 實(shí)現(xiàn)代碼如下:
這個(gè)例子體現(xiàn)使用 Go 語言開發(fā)的兩個(gè)特點(diǎn):
1.Go 語言的并發(fā)很簡(jiǎn)單,并且通過提高并發(fā)可以提高處理效率.
2.協(xié)程之間可以通過通信的方式來共享變量.
當(dāng)并發(fā)成為語言的原生特性之后,在實(shí)踐過程中就會(huì)頻繁地使用并發(fā)來處理邏輯問題,尤其是涉及到網(wǎng)絡(luò)I/O的過程,例如 RPC 調(diào)用,數(shù)據(jù)庫(kù)訪問等.下圖是一個(gè)微服務(wù)處理請(qǐng)求的抽象描述:
當(dāng) Request 到達(dá) GW 之后,GW 需要整合下游5個(gè)服務(wù)的結(jié)果來響應(yīng)本次的請(qǐng)求,假定對(duì)下游5個(gè)服務(wù)的調(diào)用不存在互相的數(shù)據(jù)依賴問題.那么這里會(huì)同時(shí)發(fā)起5個(gè) RPC 請(qǐng)求,然后等待5個(gè)請(qǐng)求的返回結(jié)果.為避免長(zhǎng)時(shí)間的等待,這里會(huì)引入等待超時(shí)的概念.超時(shí)事件發(fā)生后,為了避免資源泄漏,會(huì)發(fā)送事件給正在并發(fā)處理的請(qǐng)求.在實(shí)踐過程中,得出兩種抽象的模型.
Wait和Cancel兩種并發(fā)控制方式,在使用 Go 開發(fā)服務(wù)的時(shí)候到處都有體現(xiàn),只要使用了并發(fā)就會(huì)用到這兩種模式.在上面的例子中,GW 啟動(dòng)5個(gè)協(xié)程發(fā)起5個(gè)并行的 RPC 調(diào)用之后,主協(xié)程就會(huì)進(jìn)入等待狀態(tài),需要等待這5次 RPC 調(diào)用的返回結(jié)果,這就是 Wait 模式.另一中 Cancel 模式,在5次 RPC 調(diào)用返回之前,已經(jīng)到達(dá)本次請(qǐng)求處理的總超時(shí)時(shí)間,這時(shí)候就需要 Cancel 所有未完成的 RPC 請(qǐng)求,提前結(jié)束協(xié)程.Wait 模式使用會(huì)比較廣泛一些,而對(duì)于 Cancel 模式主要體現(xiàn)在超時(shí)控制和資源回收.
在 Go 語言中,分別有 sync.WaitGroup 和 context.Context 來實(shí)現(xiàn)這兩種模式.
合理的超時(shí)控制在構(gòu)建可靠的大規(guī)模微服務(wù)架構(gòu)顯得非常重要,不合理的超時(shí)設(shè)置或者超時(shí)設(shè)置失效將會(huì)引起整個(gè)調(diào)用鏈上的服務(wù)雪崩.
圖中被依賴的服務(wù)G由于某種原因?qū)е马憫?yīng)比較慢,因此上游服務(wù)的請(qǐng)求都會(huì)阻塞在服務(wù)G的調(diào)用上.如果此時(shí)上游服務(wù)沒有合理的超時(shí)控制,導(dǎo)致請(qǐng)求阻塞在服務(wù)G上無法釋放,那么上游服務(wù)自身也會(huì)受到影響,進(jìn)一步影響到整個(gè)調(diào)用鏈上各個(gè)服務(wù).
在 Go 語言中,Server 的模型是“協(xié)程模型”,即一個(gè)協(xié)程處理一個(gè)請(qǐng)求.如果當(dāng)前請(qǐng)求處理過程因?yàn)橐蕾嚪?wù)響應(yīng)慢阻塞,那么很容易會(huì)在短時(shí)間內(nèi)堆積起大量的協(xié)程.每個(gè)協(xié)程都會(huì)因?yàn)樘幚磉壿嫷牟煌加貌煌笮〉膬?nèi)存,當(dāng)協(xié)程數(shù)據(jù)激增,服務(wù)進(jìn)程很快就會(huì)消耗大量的內(nèi)存.
協(xié)程暴漲和內(nèi)存使用激增會(huì)加劇 Go 調(diào)度器和運(yùn)行時(shí) GC 的負(fù)擔(dān),進(jìn)而再次影響服務(wù)的處理能力,這種惡性循環(huán)會(huì)導(dǎo)致整個(gè)服務(wù)不可用.在使用 Go 開發(fā)微服務(wù)的過程中,曾多次出現(xiàn)過類似的問題,我們稱之為協(xié)程暴漲.
有沒有好的辦法來解決這個(gè)問題呢?通常出現(xiàn)這種問題的原因是網(wǎng)絡(luò)調(diào)用阻塞過長(zhǎng).即使在我們合理設(shè)置網(wǎng)絡(luò)超時(shí)之后,偶爾還是會(huì)出現(xiàn)超時(shí)限制不住的情況,對(duì) Go 語言中如何使用超時(shí)控制進(jìn)行分析,首先我們來看下一次網(wǎng)絡(luò)調(diào)用的過程.
第一步,建立 TCP 連接,通常會(huì)設(shè)置一個(gè)連接超時(shí)時(shí)間來保證建立連接的過程不會(huì)被無限阻塞.
第二步,把序列化后的 Request 數(shù)據(jù)寫入到 Socket 中,為了確保寫數(shù)據(jù)的過程不會(huì)一直阻塞,Go 語言提供了 SetWriteDeadline 的方法,控制數(shù)據(jù)寫入 Socket 的超時(shí)時(shí)間.根據(jù) Request 的數(shù)據(jù)量大小,可能需要多次寫 Socket 的操作,并且為了提高效率會(huì)采用邊序列化邊寫入的方式.因此在 Thrift 庫(kù)的實(shí)現(xiàn)中每次寫 Socket 之前都會(huì)重新 Reset 超時(shí)時(shí)間.
第三步,從 Socket 中讀取返回的結(jié)果,和寫入一樣, Go 語言也提供了 SetReadDeadline 接口,由于讀數(shù)據(jù)也存在讀取多次的情況,因此同樣會(huì)在每次讀取數(shù)據(jù)之前 Reset 超時(shí)時(shí)間.
分析上面的過程可以發(fā)現(xiàn)影響一次 RPC 耗費(fèi)的總時(shí)間的長(zhǎng)短由三部分組成:連接超時(shí),寫超時(shí),讀超時(shí).而且讀和寫超時(shí)可能存在多次,這就導(dǎo)致超時(shí)限制不住情況的發(fā)生.為了解決這個(gè)問題,在 kite 框架中引入了并發(fā)超時(shí)控制的概念,并將功能集成到 kite 框架的客戶端調(diào)用庫(kù)中.
并發(fā)超時(shí)控制模型如上圖所示,在模型中引入了“Concurrent Ctrl”模塊,這個(gè)模塊屬于微服務(wù)熔斷功能的一部分,用于控制客戶端能夠發(fā)起的最大并發(fā)請(qǐng)求數(shù).并發(fā)超時(shí)控制整體流程是這樣的
首先,客戶端發(fā)起 RPC 請(qǐng)求,經(jīng)過“Concurrent Ctrl”模塊判斷是否允許當(dāng)前請(qǐng)求發(fā)起.如果被允許發(fā)起 RPC 請(qǐng)求,此時(shí)啟動(dòng)一個(gè)協(xié)程并執(zhí)行 RPC 調(diào)用,同時(shí)初始化一個(gè)超時(shí)定時(shí)器.然后在主協(xié)程中同時(shí)監(jiān)聽 RPC 完成事件信號(hào)以及定時(shí)器信號(hào).如果 RPC 完成事件先到達(dá),則表示本次 RPC 成功,否則,當(dāng)定時(shí)器事件發(fā)生,表明本次 RPC 調(diào)用超時(shí).這種模型確保了無論何種情況下,一次 RPC 都不會(huì)超過預(yù)定義的時(shí)間,實(shí)現(xiàn)精準(zhǔn)控制超時(shí).
Go 語言在1.7版本的標(biāo)準(zhǔn)庫(kù)引入了“context”,這個(gè)庫(kù)幾乎成為了并發(fā)控制和超時(shí)控制的標(biāo)準(zhǔn)做法,隨后1.8版本中在多個(gè)舊的標(biāo)準(zhǔn)庫(kù)中增加對(duì)“context”的支持,其中包括“database/sql”包.
Go 相對(duì)于傳統(tǒng) Web 服務(wù)端編程語言已經(jīng)具備非常大的性能優(yōu)勢(shì).但是很多時(shí)候因?yàn)槭褂梅绞讲粚?duì),或者服務(wù)對(duì)延遲要求很高,不得不使用一些性能分析工具去追查問題以及優(yōu)化服務(wù)性能.在 Go 語言工具鏈中自帶了多種性能分析工具,供開發(fā)者分析問題.
下圖是各種分析方法截圖
在使用 Go 語言開發(fā)的過程中,我們總結(jié)了一些寫出高性能 Go 服務(wù)的方法
下面描述一個(gè)真實(shí)的線上服務(wù)性能優(yōu)化例子.
這是一個(gè)基礎(chǔ)存儲(chǔ)服務(wù),提供 SetData 和 GetDataByRange 兩個(gè)方法,分別實(shí)現(xiàn)批量存儲(chǔ)數(shù)據(jù)和按照時(shí)間區(qū)間批量獲取數(shù)據(jù)的功能.為了提高性能,存儲(chǔ)的方式是以用戶 ID 和一段時(shí)間作為 key,時(shí)間區(qū)間內(nèi)的所有數(shù)據(jù)作為 value 存儲(chǔ)到 KV 數(shù)據(jù)庫(kù)中.因此,當(dāng)需要增加新的存儲(chǔ)數(shù)據(jù)時(shí)候就需要先從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),拼接到對(duì)應(yīng)的時(shí)間區(qū)間內(nèi)再存到數(shù)據(jù)庫(kù)中.
對(duì)于讀取數(shù)據(jù)的請(qǐng)求,則會(huì)根據(jù)請(qǐng)求的時(shí)間區(qū)間計(jì)算對(duì)應(yīng)的 key 列表,然后循環(huán)從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù).
這種情況下,高峰期服務(wù)的接口響應(yīng)時(shí)間比較高,嚴(yán)重影響服務(wù)的整體性能.通過上述性能分析方法對(duì)于高峰期服務(wù)進(jìn)行分析之后,得出如下結(jié)論:
問題點(diǎn):
優(yōu)化思路:
分析服務(wù)接口功能可以發(fā)現(xiàn),數(shù)據(jù)解壓縮,反序列化這個(gè)過程是最頻繁的,這也符合性能分析得出來的結(jié)論.仔細(xì)分析解壓縮和反序列化的過程,發(fā)現(xiàn)對(duì)于反序列化操作而言,需要一個(gè)”io.Reader”的接口,而對(duì)于解壓縮,其本身就實(shí)現(xiàn)了”io.Reader“接口.在 Go 語言中,“io.Reader”的接口定義如下:
這個(gè)接口定義了 Read 方法,任何實(shí)現(xiàn)該接口的對(duì)象都可以從中讀取一定數(shù)量的字節(jié)數(shù)據(jù).因此只需要一段比較小的內(nèi)存 Buffer 就可以實(shí)現(xiàn)從解壓縮到反序列化的過程,而不需要將所有數(shù)據(jù)解壓縮之后再進(jìn)行反序列化,大量節(jié)省了內(nèi)存的使用.
為了避免頻繁的 Buffer 申請(qǐng)和釋放,使用“sync.Pool”實(shí)現(xiàn)了一個(gè)對(duì)象池,達(dá)到對(duì)象復(fù)用的目的.
此外,對(duì)于獲取歷史數(shù)據(jù)接口,從原先的循環(huán)讀取多個(gè) key 的數(shù)據(jù),優(yōu)化為從數(shù)據(jù)庫(kù)并發(fā)讀取各個(gè) key 的數(shù)據(jù).經(jīng)過這些優(yōu)化之后,服務(wù)的高峰 PCT99 從100ms降低到15ms.
上述是一個(gè)比較典型的 Go 語言服務(wù)優(yōu)化案例.概括為兩點(diǎn):
優(yōu)化的過程中使用了 pprof 工具發(fā)現(xiàn)性能瓶頸點(diǎn),然后發(fā)現(xiàn)“io.Reader”接口具備的 Pipeline 的數(shù)據(jù)處理方式,進(jìn)而整體優(yōu)化了整個(gè)服務(wù)的性能.
Go 語言的 runtime 包提供了多個(gè)接口供開發(fā)者獲取當(dāng)前進(jìn)程運(yùn)行的狀態(tài).在 kite 框架中集成了協(xié)程數(shù)量,協(xié)程狀態(tài),GC 停頓時(shí)間,GC 頻率,堆棧內(nèi)存使用量等監(jiān)控.實(shí)時(shí)采集每個(gè)當(dāng)前正在運(yùn)行的服務(wù)的這些指標(biāo),分別針對(duì)各項(xiàng)指標(biāo)設(shè)置報(bào)警閾值,例如針對(duì)協(xié)程數(shù)量和 GC 停頓時(shí)間.另一方面,我們也在嘗試做一些運(yùn)行時(shí)服務(wù)的堆棧和運(yùn)行狀態(tài)的快照,方便追查一些無法復(fù)現(xiàn)的進(jìn)程重啟的情況.
相對(duì)于傳統(tǒng) Web 編程語言,Go 在編程思維上的確帶來了許多的改變.每一個(gè) Go 開發(fā)服務(wù)都是一個(gè)獨(dú)立的進(jìn)程,任何一個(gè)請(qǐng)求處理造成 Panic,都會(huì)讓整個(gè)進(jìn)程退出,因此當(dāng)啟動(dòng)一個(gè)協(xié)程的時(shí)候需要考慮是否需要使用 recover 方法,避免影響其它協(xié)程.對(duì)于 Web 服務(wù)端開發(fā),往往希望將一個(gè)請(qǐng)求處理的整個(gè)過程能夠串起來,這就非常依賴于 Thread Local 的變量,而在 Go 語言中并沒有這個(gè)概念,因此需要在函數(shù)調(diào)用的時(shí)候傳遞 context.
最后,使用 Go 開發(fā)的項(xiàng)目中,并發(fā)是一種常態(tài),因此就需要格外注意對(duì)共享資源的訪問,臨界區(qū)代碼邏輯的處理,會(huì)增加更多的心智負(fù)擔(dān).這些編程思維上的差異,對(duì)于習(xí)慣了傳統(tǒng) Web 后端開發(fā)的開發(fā)者,需要一個(gè)轉(zhuǎn)變的過程.
關(guān)于工程性,也是 Go 語言不太所被提起的點(diǎn).實(shí)際上在 Go 官方網(wǎng)站關(guān)于為什么要開發(fā) Go 語言里面就提到,目前大多數(shù)語言當(dāng)代碼量變得巨大之后,對(duì)代碼本身的管理以及依賴分析變得異常苦難,因此代碼本身成為了最麻煩的點(diǎn),很多龐大的項(xiàng)目到最后都變得不敢去動(dòng)它.而 Go 語言不同,其本身設(shè)計(jì)語法簡(jiǎn)單,類C的風(fēng)格,做一件事情不會(huì)有很多種方法,甚至一些代碼風(fēng)格都被定義到 Go 編譯器的要求之內(nèi).而且,Go 語言標(biāo)準(zhǔn)庫(kù)自帶了源代碼的分析包,可以方便地將一個(gè)項(xiàng)目的代碼轉(zhuǎn)換成一顆 AST 樹.
下面以一張圖形象地表達(dá)下 Go 語言的工程性:
同樣是拼成一個(gè)正方形,Go 只有一種方式,每個(gè)單元都是一致.而 Python 拼接的方式可能可以多種多樣.
今日頭條使用 Go 語言構(gòu)建了大規(guī)模的微服務(wù)架構(gòu),本文結(jié)合 Go 語言特性著重講解了并發(fā),超時(shí)控制,性能等在構(gòu)建微服務(wù)中的實(shí)踐.事實(shí)上,Go 語言不僅在服務(wù)性能上表現(xiàn)卓越,而且非常適合容器化部署,我們很大一部分服務(wù)已經(jīng)運(yùn)行于內(nèi)部的私有云平臺(tái).結(jié)合微服務(wù)相關(guān)組件,我們正朝著 Cloud Native 架構(gòu)演進(jìn).
更多技術(shù)實(shí)踐內(nèi)容可以關(guān)注今日頭條技術(shù)博客:techblog.toutiao.com
項(xiàng)超,今日頭條高級(jí)研發(fā)工程師.2015年加入今日頭條,負(fù)責(zé)服務(wù)化改造相關(guān)工作,在內(nèi)部推廣Go語言的使用,研發(fā)內(nèi)部微服務(wù)框架kite,集成服務(wù)治理,負(fù)載均衡等多種微服務(wù)功能,實(shí)現(xiàn)了Go語言構(gòu)建大規(guī)模微服務(wù)架構(gòu)在頭條的落地.曾就職于小米.
文章來自微信公眾號(hào):InfoQ
轉(zhuǎn)載請(qǐng)注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/4193.html