今日頭條在2015年中期前,使用的開發語言大量采用了Python和C++以及PHP技術棧。
隨着系統復雜度,耦合度不斷提升,開始向SOA服務化架構演進。
頭條的內容發布系統使用了Django框架,一部分后端系統還使用了PHP,這些解釋型語言以及相應的服務進程管理存在一些瓶頸,即便通過大家的智慧得到解決,但是整個服務器后端的架構是一個大的單體架構,需要將一部分功能從單體架構中抽取出來。
頭條微服務架構概覽
因此有必要轉移為微服務架構。微服務架構具有如下特點:
-
進程解耦
-
易於管理和理解
-
自我包含
-
部署解耦
-
自動化。
因此,微服務可以與語言層無關,具有較強的接口約束性,高內聚,服務之間的正向相交性。
到目前為止,今日頭條技術棧,包括頭條、段子產品線開始全部或部分轉移到Go語言構建的微服務平台上。目前部署的微服務數量超過幾百個,在最高峰時,QPS超過700萬,日處理用戶請求超過3000億次,形成目前部署規模較大的GO語言應用。
選擇Go語言的原因
-
語法簡單,上手快
-
性能高,編譯快,開發效率也不低
-
原生支持並發,協程模型是非常優秀的服務端模型,同時也適合網絡調用
-
部署方便,編譯包小,幾乎無依賴
因為團隊以前用Go 構建過超大流量的后端服務,對其本身的穩定性有信心。再加上頭條后端整體服務化的架構改造,所以決定使用 Go 語言構建后端的微服務架構。
2015年6月,今日頭條技術團隊開始嘗試使用 Go 重構后端 Feed 流(信息流)服務。期間一邊重構,一邊迭代現有業務,同時還進行服務拆分,直到2016年6月,Feed 流后端服務大部分遷移到 Go架構上。
由於期間業務增長較快,夾雜服務拆分,因此沒有橫向對比重構前后的各項指標。實際上切換到 Go 之后,服務整體穩定性和性能都得以大幅提高。
微服務架構
對於復雜的服務間調用,我們抽象出五元組的概念:(From, FromCluster, To, ToCluster, Method)。每一個五元組唯一定義了一類的RPC調用。以五元組為單元,我們構建了一整套微服務架構。
頭條使用 Go 研發了內部的微服務框架:kite,其完全兼容 Thrift協議。
以五元組為基礎單元,我們在 kite 框架上集成了服務注冊和發現,分布式負載均衡,超時和熔斷管理,服務降級,Method 級別的指標監控,分布式調用鏈追蹤等功能。目前統一使用 kite 框架開發內部 Go 語言的服務,整體架構支持無限制水平擴展。
關於 kite 框架和微服務架構實現細節后續有機會會專門分享,這里主要分享下我們在使用 Go 構建大規模微服務架構中,Go 語言本身給我們帶來了哪些便利以及實踐過程中我們取得的經驗。內容主要包括並發,性能,監控以及對Go語言使用的一些體會。
並發
Go 作為一門新興的編程語言,最大特點就在於它是原生支持並發的。
和傳統基於 OS 線程和進程實現不同,Go 的並發是基於用戶態的並發,這種並發方式就變得非常輕量,能夠輕松運行幾萬甚至是幾十萬的並發邏輯。因此使用 Go 開發的服務端應用采用的就是“協程模型”,每一個請求由獨立的協程處理完成。
比進程線程模型高出幾個數量級的並發能力,而相對基於事件回調的服務端模型,Go 開發思路更加符合人的邏輯處理思維,因此即使使用 Go 開發大型的項目,也很容易維護。
並發模型
Go 的並發屬於 CSP 並發模型的一種實現,CSP 並發模型的核心概念是:“不要通過共享內存來通信,而應該通過通信來共享內存”。這在 Go 語言中的實現就是 Goroutine 和 Channel。
在1978發表的 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以內所有的素數,這里使用的方法是篩法,即從2開始每找到一個素數就標記所有能被該素數整除的所有數。直到沒有可標記的數,剩下的就都是素數。下面以找出10以內所有素數為例,借用 CSP 方式解決這個問題。
從上圖中可以看出,每一行過濾使用獨立的並發處理程序,上下相鄰的並發處理程序傳遞數據實現通信。通過4個並發處理程序得出10以內的素數表,對應的 Go 語言代碼如下:
以上例子體現出 Go 語言開發的兩個特點:
1.Go 語言的並發很簡單,並且通過提高並發可以提高處理效率。
2.協程之間可以通過通信的方式來共享變量。
並發控制
當並發成為語言的原生特性之后,在實踐過程中就會頻繁地使用並發來處理邏輯問題,尤其是涉及到網絡I/O的過程,例如 RPC 調用,數據庫訪問等。下圖是一個微服務處理請求的抽象描述:
當 Request 到達 GW 之后,GW 需要整合下游5個服務的結果來響應本次的請求,假定對下游5個服務的調用不存在互相的數據依賴問題。那么這里會同時發起5個 RPC 請求,然后等待5個請求的返回結果。為避免長時間的等待,這里會引入等待超時的概念。超時事件發生后,為了避免資源泄漏,會發送事件給正在並發處理的請求。在實踐過程中,得出兩種抽象的模型。
·Wait
·Cancel
Wait和Cancel兩種並發控制方式,在使用 Go 開發服務的時候到處都有體現,只要使用了並發就會用到這兩種模式。在上面的例子中,GW 啟動5個協程發起5個並行的 RPC 調用之后,主協程就會進入等待狀態,需要等待這5次 RPC 調用的返回結果,這就是 Wait 模式。另一中 Cancel 模式,在5次 RPC 調用返回之前,已經到達本次請求處理的總超時時間,這時候就需要 Cancel 所有未完成的 RPC 請求,提前結束協程。Wait 模式使用會比較廣泛一些,而對於 Cancel 模式主要體現在超時控制和資源回收。
在 Go 語言中,分別有 sync.WaitGroup 和 context.Context 來實現這兩種模式。
超時控制
合理的超時控制在構建可靠的大規模微服務架構顯得非常重要,不合理的超時設置或者超時設置失效將會引起整個調用鏈上的服務雪崩。
圖中被依賴的服務G由於某種原因導致響應比較慢,因此上游服務的請求都會阻塞在服務G的調用上。如果此時上游服務沒有合理的超時控制,導致請求阻塞在服務G上無法釋放,那么上游服務自身也會受到影響,進一步影響到整個調用鏈上各個服務。
在 Go 語言中,Server 的模型是“協程模型”,即一個協程處理一個請求。如果當前請求處理過程因為依賴服務響應慢阻塞,那么很容易會在短時間內堆積起大量的協程。每個協程都會因為處理邏輯的不同而占用不同大小的內存,當協程數據激增,服務進程很快就會消耗大量的內存。
協程暴漲和內存使用激增會加劇 Go 調度器和運行時 GC 的負擔,進而再次影響服務的處理能力,這種惡性循環會導致整個服務不可用。在使用 Go 開發微服務的過程中,曾多次出現過類似的問題,我們稱之為協程暴漲。
有沒有好的辦法來解決這個問題呢?通常出現這種問題的原因是網絡調用阻塞過長。即使在我們合理設置網絡超時之后,偶爾還是會出現超時限制不住的情況,對 Go 語言中如何使用超時控制進行分析,首先我們來看下一次網絡調用的過程。
第一步,建立 TCP 連接,通常會設置一個連接超時時間來保證建立連接的過程不會被無限阻塞。
第二步,把序列化后的 Request 數據寫入到 Socket 中,為了確保寫數據的過程不會一直阻塞,Go 語言提供了 SetWriteDeadline 的方法,控制數據寫入 Socket 的超時時間。根據 Request 的數據量大小,可能需要多次寫 Socket 的操作,並且為了提高效率會采用邊序列化邊寫入的方式。因此在 Thrift 庫的實現中每次寫 Socket 之前都會重新 Reset 超時時間。
第三步,從 Socket 中讀取返回的結果,和寫入一樣, Go 語言也提供了 SetReadDeadline 接口,由於讀數據也存在讀取多次的情況,因此同樣會在每次讀取數據之前 Reset 超時時間。
分析上面的過程可以發現影響一次 RPC 耗費的總時間的長短由三部分組成:連接超時,寫超時,讀超時。而且讀和寫超時可能存在多次,這就導致超時限制不住情況的發生。為了解決這個問題,在 kite 框架中引入了並發超時控制的概念,並將功能集成到 kite 框架的客戶端調用庫中。
並發超時控制模型如上圖所示,在模型中引入了“Concurrent Ctrl”模塊,這個模塊屬於微服務熔斷功能的一部分,用於控制客戶端能夠發起的最大並發請求數。並發超時控制整體流程是這樣的:
首先,客戶端發起 RPC 請求,經過“Concurrent Ctrl”模塊判斷是否允許當前請求發起。如果被允許發起 RPC 請求,此時啟動一個協程並執行 RPC 調用,同時初始化一個超時定時器。然后在主協程中同時監聽 RPC 完成事件信號以及定時器信號。如果 RPC 完成事件先到達,則表示本次 RPC 成功,否則,當定時器事件發生,表明本次 RPC 調用超時。這種模型確保了無論何種情況下,一次 RPC 都不會超過預定義的時間,實現精准控制超時。
Go 語言在1.7版本的標准庫引入了“context”,這個庫幾乎成為了並發控制和超時控制的標准做法,隨后1.8版本中在多個舊的標准庫中增加對“context”的支持,其中包括“database/sql”包。
性能
Go 相對於傳統 Web 服務端編程語言已經具備非常大的性能優勢。但是很多時候因為使用方式不對,或者服務對延遲要求很高,不得不使用一些性能分析工具去追查問題以及優化服務性能。在 Go 語言工具鏈中自帶了多種性能分析工具,供開發者分析問題。
·CPU 使用分析
·內部使用分析
·查看協程棧
·查看 GC 日志
·Trace 分析工具
下圖是各種分析方法截圖:
在使用 Go 語言開發的過程中,我們總結了一些寫出高性能 Go 服務的方法如下:
1.注重鎖的使用,盡量做到鎖變量而不要鎖過程
2.可以使用 CAS,則使用 CAS 操作
3.針對熱點代碼要做針對性優化
4.不要忽略 GC 的影響,尤其是高性能低延遲的服務
5.合理的對象復用可以取得非常好的優化效果
6.盡量避免反射,在高性能服務中杜絕反射的使用
7.有些情況下可以嘗試調優“GOGC”參數
8.新版本穩定的前提下,盡量升級新的 Go 版本,因為舊版本永遠不會變得更好。
下面描述一個真實的線上服務性能優化例子。
這是一個基礎存儲服務,提供 SetData 和 GetDataByRange 兩個方法,分別實現批量存儲數據和按照時間區間批量獲取數據的功能。為了提高性能,存儲的方式是以用戶 ID 和一段時間作為 key,時間區間內的所有數據作為 value 存儲到 KV 數據庫中。因此,當需要增加新的存儲數據時候就需要先從數據庫中讀取數據,拼接到對應的時間區間內再存到數據庫中。
對於讀取數據的請求,則會根據請求的時間區間計算對應的 key 列表,然后循環從數據庫中讀取數據。
這種情況下,高峰期服務的接口響應時間比較高,嚴重影響服務的整體性能。通過上述性能分析方法對於高峰期服務進行分析之后,得出如下結論:
問題點:
·GC 壓力大,占用 CPU 資源高
·反序列化過程占用 CPU 較高
優化思路:
1.GC 壓力主要是內存的頻繁申請和釋放,因此決定減少內存和對象的申請
2.序列化當時使用的是 Thrift 序列化方式,通過 Benchmark,我們找到相對高效的 Msgpack 序列化方式。
分析服務接口功能可以發現,數據解壓縮,反序列化這個過程是最頻繁的,這也符合性能分析得出來的結論。仔細分析解壓縮和反序列化的過程,發現對於反序列化操作而言,需要一個”io.Reader”的接口,而對於解壓縮,其本身就實現了”io.Reader“接口。在 Go 語言中,“io.Reader”的接口定義如下:
這個接口定義了 Read 方法,任何實現該接口的對象都可以從中讀取一定數量的字節數據。因此只需要一段比較小的內存 Buffer 就可以實現從解壓縮到反序列化的過程,而不需要將所有數據解壓縮之后再進行反序列化,大量節省了內存的使用。
為了避免頻繁的 Buffer 申請和釋放,使用“sync.Pool”實現了一個對象池,達到對象復用的目的。
此外,對於獲取歷史數據接口,從原先的循環讀取多個 key 的數據,優化為從數據庫並發讀取各個 key 的數據。經過這些優化之后,服務的高峰 PCT99 從100ms降低到15ms。
上述是一個比較典型的 Go 語言服務優化案例。概括為兩點:
1.從業務層面上提高並發
2.減少內存和對象的使用
優化的過程中使用了 pprof 工具發現性能瓶頸點,然后發現“io.Reader”接口具備的 Pipeline 的數據處理方式,進而整體優化了整個服務的性能。
服務監控
Go 語言的 runtime 包提供了多個接口供開發者獲取當前進程運行的狀態。在 kite 框架中集成了協程數量,協程狀態,GC 停頓時間,GC 頻率,堆棧內存使用量等監控。實時采集每個當前正在運行的服務的這些指標,分別針對各項指標設置報警閾值,例如針對協程數量和 GC 停頓時間。另一方面,我們也在嘗試做一些運行時服務的堆棧和運行狀態的快照,方便追查一些無法復現的進程重啟的情況。
Go編程思維和工程性
相對於傳統 Web 編程語言,Go 在編程思維上的確帶來了許多的改變。每一個 Go 開發服務都是一個獨立的進程,任何一個請求處理造成 Panic,都會讓整個進程退出,因此當啟動一個協程的時候需要考慮是否需要使用 recover 方法,避免影響其它協程。對於 Web 服務端開發,往往希望將一個請求處理的整個過程能夠串起來,這就非常依賴於 Thread Local 的變量,而在 Go 語言中並沒有這個概念,因此需要在函數調用的時候傳遞 context。
最后,使用 Go 開發的項目中,並發是一種常態,因此就需要格外注意對共享資源的訪問,臨界區代碼邏輯的處理,會增加更多的心智負擔。這些編程思維上的差異,對於習慣了傳統 Web 后端開發的開發者,需要一個轉變的過程。
關於工程性,也是 Go 語言不太所被提起的點。實際上在 Go 官方網站關於為什么要開發 Go 語言里面就提到,目前大多數語言當代碼量變得巨大之后,對代碼本身的管理以及依賴分析變得異常苦難,因此代碼本身成為了最麻煩的點,很多龐大的項目到最后都變得不敢去動它。而 Go 語言不同,其本身設計語法簡單,類C的風格,做一件事情不會有很多種方法,甚至一些代碼風格都被定義到 Go 編譯器的要求之內。而且,Go 語言標准庫自帶了源代碼的分析包,可以方便地將一個項目的代碼轉換成一顆 AST 樹。
下面以一張圖形象地表達下 Go 語言的工程性:
同樣是拼成一個正方形,Go 只有一種方式,每個單元都是一致。而 Python 拼接的方式可能可以多種多樣。
下面我們再結合Go與內涵段子的微服務升級之實錄。
內涵段子Golang DAO
內涵近段時間遷移了部分API代碼到Golang,主要是為了使用Golang中方便的goroutine。但是開發中很多冗余代碼需要重復開發(缺少一個組件能夠收斂各種RPC調用,復用代碼,減少開發量),同時,又不希望組件使用過多的黑魔法,導致結構復雜,開發維護麻煩。
要求
希望開發一個組件:
* 能夠收斂各種RPC調用,復用代碼,減少開發量
* 能夠利用Golang的goroutine優勢,加快數據獲取
* 不要使用太多黑魔法,導致結構復雜難於維護
假設場景:
需要實現一個接口,接受一個段子的Content_id,返回如下數據:
* 數據a. 段子基本內容Content → 調用獲取Conent_Info接口
* 數據b. 段子的作者信息User → 調用獲取User_Info接口
* 數據c. 段子的評論信息Comment → 調用獲取Comment_Info接口
一、從RPC調用開始
假設場景在golang中的調用順序就是:
1.根據段子ID(Content_id),並發調用數據a(基本內容)和數據c(評論信息)
2.根據a(基本內容)中的作者userid調用數據b(作者用戶信息userinfo)
(圖1-1)
單獨看來,這些操作也沒什么,但是我們看看完成這個步驟需要的代碼:
ContentTask = NewContentInfoTask(id=123)
CommentTask = NewCommentsListTask(ContentId=123)
ParallelExec(ContentTask, CommentTask) // 並行調用兩個任務
// 判斷結果是否正確,一堆代碼
user_id = ContentTask.Response.User_id //獲取作者ID
UserResp = NewUserTask(user_id).Load() // 再獲取作者信息
// 判斷結果,一堆代碼
// 用上面獲取的數據打包數據
我們看到,代碼非常的冗余,而且麻煩在於,這種步驟基本每個接口都會需要進行,完全無法重用。 一旦數據有區別,需要多一點或者少一點數據,又需要重新寫一個Load過程。很多Copy的代碼。
問題一:那我們能否減少這種重復書寫的代碼?
二、基本的Dao功能
自然的,我們會想到將RPC調用都收斂到自己所屬的實體(Dao),並建立Dao之間的關聯關系,每個RPC對應一個方法(在方法中將數據填充到自身),即(圖2-1):
此時,我們獲取數據只需要如下代碼:
content = NewContentDao(id=123) // 段子信息
comments = NewCommentList(ContentId=123) // 段子評論信息
// 第一層Load: 獲取Content和comments的信息
ParallelExec(content.Content_Info(), comments.Comment_Info()) # 並行兩個任務
// 第二層Load: 獲取user的屬性
user = NewUser(id=content.UserId)
user.User_Info()
// 使用上面對象的屬性,進行數據的打包。
Python中可以將方法作為property,即使用某個屬性的時候,才進行需要的RPC調用,使用更加的方便。但是也就不方便進行並行處理
顯然的,此時代碼已經省略了很多:將RPC調用收斂到了一個實體中。 更進一步,我們可以利用已經包含在了Dao關聯關系之中的相互關系:即,獲取用戶和評論信息,是可以通過Dao來調用的。
content = NewContentDao(id=123)
ParallelExec(content.Content_Info(),content.Comments.Comment_Info()) // 並發獲取content基本信息和Comment信息
content.User.User_Info() //獲取作者信息
至此,已經實現了基本的Dao功能。即:
·收斂所有RPC、DB、Cache等跨服務調用
·並建立他們之間的關聯關系
·收斂冗余代碼,只要實現一套Dao(收斂屬於該實體的所有調用)
此時要實現新一個接口將是相對輕松的工作!只需要聚合各種Dao之后打包數據即可
但是此時,代碼就會是一個套路:加載第一層、加載第二層、、、、加載第N層。加載完所有數據之后,再進行打包。
問題二:那么我們能否讓這種套路自動化?
三、自動構建調用樹
再次回顧我們的Dao的關聯關系對應的對象圖,可以明顯的看到是一個樹結構(全樹) (圖3-1):
而我們需要的是這些屬性:Content、User、Comment,即樹中的某些節點 (圖3-2):
所以,我們需要有某個組件(稱之為Loader組件),使用DAO的調用方提供需要的屬性(即上圖中的紅色部分),該組件將上圖3-2和圖3-1的全樹進行match,一旦需要,則進行RPC調用,將結果數據放到Dao對象中。最終返回一個已經Load好數據的Dao的struct就可以啦!
問題三:
1.Dao之間有一些復雜的依賴關系,同時一個Dao內的屬性又有依賴關系, 這個組件如何組織調用和對應的先后關系?
2.如何實現調用中的並發獲取?
3.如何(何種形式)告訴這個組件你需要的數據的路徑?
四、自動並發加載:
問題1:
組件如何組織調用和對應的先后關系?
在上一節自動構建調用樹中我們已經知道Dao之間的關系。現在我們再將Dao拆開,以RPC調用為最小的單元,再來看下這個樹(圖4-1):
白圓圈是每個需要RPC調用的代碼塊,白方塊表示屬性(部分表示由RPC調用獲取到的屬性)。
有沒有發現什么?我們單獨拿Content來看,他的屬性結構如下(圖4-2):
再結合圖4-1,可以看到:
1.一些基本屬性如Text、UserId、DiggCount僅僅依賴於主鍵ID;
2.另外一些屬性如:User、Category等,是依賴於1中的基本屬性
此時,將一個DAO中的所有屬性分成兩種:
·Basic:依賴於主鍵ID,即獲取這個屬性,僅僅依賴於主鍵。
·Sub:依賴於Basic中的某個屬性。
如此划分之后,就可以將Dao拆分成兩層調用:
第一層Basic調用基本的;完成之后,Sub依賴的屬性都具備了,再調用第二層;
至此該Dao的數據加載完成
划分之后,每個DAO的屬性如下(圖4-3):
如content則存在一個屬性和RPC調用關聯關系的map:
// 基本屬性和RPC調用方法的映射
BASIC_LOADER_MAP = map[string]Loader{
"basic": {Loader: duanziBasicLoader}, // 獲取段子的基本屬性(依賴於content_id)
"commentStats": {Loader: commentstatsLoader}, // content的評論數據(依賴於content_id)
}
// Sub屬性和RPC調用方法的映射
SUB_LOADER_MAP = map[string]Loader{
"User": {Loader: userLoader,}, // 作者信息(依賴於段子信息中的user_id)
}
再建立他們之間的聯系(圖4-4):
至於下層的Dao的調用,則交給下層的Dao來完成,當前層不再介入,此時,我們的調用結構如下(圖4-5):
問題2:
如何實現調用中的並發獲取?
我們只需要在調用過程中,將同一個層的Basic或者Sub進行並發調用,就可以了,如(圖4-6):
即調用順序如下(每行表示一個RPC調用,並列的行,並行調用):
1. 設置Content_Id
2. 開啟Goroutine,並發調用Content的Basic層:
* a. RPC獲取段子基本信息
* b. RPC獲取段子Stats
* c. 評論Dao,
* 1. 評論Dao調用自身的Basic層
* a. RPC獲取評論基本信息
* d. RPC獲取評論相關數據stats
3. 開啟Goroutine,並發調用Content的Sub層:
* a. CategoryDao
* 2. Basic層:
* a. RPC獲取Category信息
* b. UserDao
* 1. Basic層:
* a. RPC獲取用戶基本信息
* 2. Sub層:
* .......
問題3:
最后,我們討論一下問題三的3:如何告訴這個組件你需要的數據的路徑?
其實上面的結構梳理清楚了,那么這個問題就好解決了, 我們無非是提供一個你需要的屬性的樹,讓Dao的結構一層層遍歷的過程中,將該樹傳遞下去, 如果match,則mark一下這個RPC會進行調用,mark完成當前Dao的basic層和sub層之后,統一並發調用。 而下一級的Dao的Load則在sub層中做,下一級的Dao又去match提供的樹,構建自身的Load任務。如此一層層進行下去,直到Load完所有你需要的屬性!
比如你需要Contentinfo和Content中的UserInfo和Content中的Comment,就構造一棵樹:
Content_Info → User_Info
→ Comment_Info
然后傳遞給該組件,他就能夠只Load這幾個需要的屬性,match和構造以及並發調用的過程如下:
// paramLoaderMap、subLoaderMap是basic和sub屬性和Rpc調用關系的map
func DaoLoad(needParamsTree, daoList, paramLoaderMap, subLoaderMap) error {
var basicParamTaskList // Basic打包任務列表
var subDaoTaskList // Sub打包任務列表
// 遍歷用戶需要的屬性,構造當前Dao的Basic和Sub任務結構
for _, sonTree := range needParamsTree {
if basic屬性需要Load {
// put to basicParamTaskList
} else if sub屬性需要load{
// put to subDaoTaskList
}
}
// 並發執行Basic Load
// 並發執行Sub Load
}
優化:
1.組件來幫助調用方建立needParamsTree,只需要提供幾個個字符串,:[]string{"Content_Info", "Content.User_Info", "Content.Comment_Info"},
2.組件幫你填充Sub依賴的基本屬性,Sub屬性依賴的Basic屬性是確定的,可以交給Dao自己來填充,此部分也可以省略。
此時我們的代碼如下:
dao = LoadDao([1,2,3], []string{"User_Info", "Comment_Info"}).Exec()
// 用dao去打包數據吧!
多個不同類型的Dao的Load就多構建幾個並行執行Exec即可
此時該組件減少了很多冗余的代碼,而且能夠並發加快Load數據的過程。以后還能夠方便的使用!
問題:
問:以上面的模型來說,這樣顯然會帶來更多的rpc調用(比如鏈條a.獲取段子的用戶信息;鏈條b.段子的評論的用戶信息無法進行聚合之后再調用):
答:開始考慮過合並減少RPC調用,但是這種方式有時候是有損的,即可能鏈條a更長,為了等待鏈條b拿到用戶ID,導致了總耗時的增加。所以選擇了兩者區分開。
此時就耗時來說,相對最開始的模型並沒有增長,都是取最長路徑。無非是相同的RPC調用會增多,但是這個是可以容忍的。因為:
1.使用Go,消耗有限,只是多開了一些goruntine而已;
2.根據業務的情況來看,這種增長有限,在忍受范圍內;
至此,整個Go_Dao的Load完成(version 1.0),再回到最開始的背景要求,算是完成基本的要求:
·該組件能夠實現基本的Dao的功能(收斂所有RPC、DB、Cache等跨服務調用,並建立他們之間的關聯關系,收斂冗余代碼,減少開發者的工作量)
·同時能夠利用Golang的並行優勢加快數據獲取
·沒有使用Golang中的一些相對復雜的特性(比如反射等)
就功能來說,這個組件最好的使用場景在於需要很多跨服務調用的地方,能夠極大的節省開發量。當前還只是第一個版本,還需要持續的迭代。
總結
今日頭條使用 Go 語言構建了大規模的微服務架構。在文前闡述了 Go 語言特性,着重講解了並發,超時控制,性能等。Go 不僅在服務性能上表現卓越,而且非常適合容器化部署。
后面我們又分享了內涵段子的Go語言微服務化實踐。頭條內部很大一部分服務已經運行於內部的私有雲平台。結合微服務相關組件,向着 Cloud Native 架構演進。
作者:項超。今日頭條高級研發工程師。
2015年加入今日頭條,負責服務化改造相關工作。在內部推廣Go語言的使用,研發內部微服務框架kite,集成服務治理,負載均衡等多種微服務功能,實現了Go語言構建大規模微服務架構在頭條的落地。項超曾就職於小米。