今日頭條在2015年中期前,使用的開發語言大量採用了Python和C++以及PHP技術棧。golang
隨着系統複雜度,耦合度不斷提高,開始向SOA服務化架構演進。sql
頭條的內容發佈系統使用了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接口
1、從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的代碼。
問題一:那咱們可否減小這種重複書寫的代碼?
2、基本的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層。加載完全部數據以後,再進行打包。
問題二:那麼咱們可否讓這種套路自動化?
3、自動構建調用樹
再次回顧咱們的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.如何(何種形式)告訴這個組件你須要的數據的路徑?
4、自動並發加載:
問題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語言構建大規模微服務架構在頭條的落地。項超曾就任於小米。