關鍵點
-
經濟學人內容分發系統需要更大的靈活性,將內容傳遞給日益多樣化的數字渠道。為了實現這一靈活性目標並保持高水平的性能和可靠性,平台從一個單體結構過渡到微服務體系結構。
-
用Go編寫的服務是新系統的一個關鍵組件,它使得團隊能夠交付可伸縮的、高性能的服務並快速迭代新產品。
-
Go的並發性和對API的支持以及它作為靜態編譯語言的設計,使得分布式事件系統能夠大規模執行。與此同時,Go對於測試的支持也非常出色。
-
總的來說,團隊在Go上的使用經驗是積極的,這也是內容平台得以擴展的關鍵因素之一。
隨着新聞消費從紙媒轉向數字媒體,《經濟學人》的使命是讓更廣泛的數字受眾看到這種技術轉變。因此需要更大的靈活性,將內容傳遞給日益多樣化的數字渠道。為了實現這一靈活性目標並保持高水平的性能和可靠性,平台從一個單體結構走向微服務體系結構。用Go編寫的服務是新系統的一個關鍵組件,它將使團隊能夠交付可伸縮的、高性能的服務並快速迭代新產品。
以下是基於Go的一些實踐與問題:
-
允許工程師快速迭代產品並開發新特性
-
強化智能錯誤處理並針對服務快速失敗的實踐
-
為分布式系統中的高並發和網絡提供有力支持
-
在內容和媒體所需的領域缺乏成熟度和支持
-
通過平台實現規模化的數字內容出版發行
為什么使用Go?
為了回答這個問題,先看看新平台的總體架構是很有幫助的。這個平台稱為內容平台,是一個基於事件的系統。它響應來自不同內容創作平台的事件,並觸發獨立的微服務處理這些流程。這些服務的功能包括數據標准化、語義標簽分析、ES索引,以及將內容推送到蘋果新聞或Facebook等外部平台。該平台還有一個RESTful API,它與GraphQL相結合,是前端客戶端和產品的主要入口。
在設計總體架構時,團隊研究了哪些語言適合平台的需求。將Go與Python、Ruby、Node、PHP和Java進行比較。雖然每種語言都有其優點,但最好與平台的體系結構保持一致。Go的並發性和API支持以及它作為靜態編譯語言的設計將使分布式事件系統能夠大規模執行。此外,Go相對簡單的語法使學習和開始編寫工作代碼變得很容易,這對於一個經歷了如此多技術轉換的團隊來說是一個好消息。總的來說,Go被認為是分布式雲系統中可用性和效率的最佳設計語言。
Go的目標
平台設計的幾個元素與Go語言很好地結合在一起。快速失敗是系統的關鍵部分,因為系統本身是由分布式的、獨立的服務組成的。按照應用程序的12個因素原則,應用程序需要快速啟動和失敗。Go作為一種靜態編譯語言的先天在快速啟動上有優勢,並且隨着編譯器的性能不斷提高,對於工程或部署來說從來都不是問題。此外,Go的錯誤處理從設計上不僅允許應用程序快速失敗,還允許應用程序更智能地失敗。
錯誤處理
Go與其他語言相比有一個明顯的區別,它沒有異常,而是用一個錯誤類型代替。在Go中,所有錯誤都是值。錯誤類型是預先聲明的,是一個接口。Go中的接口本質上是一個命名的方法集合,如果它具有相同的方法,那么任何其他自定義類型都可以滿足該接口。錯誤類型是一個可以用字符串描述自身的接口。
typeerror interface {
Error() string
}
Go為工程師提供了更好的控制錯誤處理功能。通過在定制模塊中添加返回字符串的Error方法,可以創建定制錯誤,如下面的函數所示,該函數來自errors包。
typeerrorString struct {
s string
}
func(e *errorString) Error() string {
return e.s
}
在Go中,函數允許多個返回值,因此如果函數可能失敗,它很可能返回一個錯誤值。這種語言鼓勵開發人員顯式地檢查錯誤發生的地方(而不是拋出和捕獲異常),因此代碼通常會有一個“if err != nil”檢查。在分布式系統中,可以通過包裝錯誤輕松地啟用重試。
網絡問題總是會在系統中遇到,無論是向其他內部服務發送數據,還是向第三方工具推送數據。這個來自Net包的示例強調了如何利用錯誤作為一種類型來區分臨時網絡錯誤和永久網絡錯誤。當將內容推送到外部api時,團隊使用類似的錯誤包裝來構建增量重試。
package net
typeError interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
ifnerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
iferr != nil {
log.Fatal(err)
}
Go的作者認為並非所有異常都是例外。鼓勵工程師明智地從錯誤中恢復,而不是讓應用程序失敗。此外,Go錯誤處理允許您對錯誤進行更多的控制。在內容平台中,Go的這個設計特性使開發人員能夠圍繞錯誤做出深思熟慮的決策,從而增強了整個系統的可靠性。
一致性
一致性是內容平台中的一個關鍵因素。內容是業務的核心,而內容平台的目標是確保內容可以發布一次並可以到處閱讀。因此,每個產品和消費者都必須具有內容平台API的一致性。產品主要使用GraphQL查詢API,這需要一個靜態模式作為消費者和平台之間的契約。平台處理的內容需要符合這一模式。靜態語言有助於實現這一點,並在確保數據一致性方面能輕松取勝。
Go與測試
另一個提高一致性的特性是Go的測試包。Go的快速編譯能力和易於測試的特性相結合,使團隊能夠將強大的測試實踐嵌入到工作流和構建Pipeline中。Go的測試工具使它們易於安裝和運行。運行“go test”將在當前目錄中運行所有測試,測試命令有幾個有用的特性標志。“Cover”標志提供關於代碼覆蓋率的詳細報告。“bench”測試運行基准測試。TestMain函數為額外的測試提供便利的功能,例如模擬身份驗證服務器。
此外,Go能夠使用匿名結構創建表測試,並使用接口創建模擬,從而提高測試覆蓋率。盡管就語言特性而言,測試並不是什么新鮮事,但是Go使得編寫健壯的測試並將其無縫地嵌入工作流變得很容易。從一開始,工程師們就能夠在構建Pipeline的過程中運行測試,而不需要進行特殊的定制。
然而,該項目在實現一致性方面並非沒有困難。該平台面臨的第一個主要挑戰是從不可預知的后端管理動態內容。該平台主要通過JSON端點(Endpoint)使用來自CMS系統的內容,而JSON端點不能保證數據結構和類型。這意味着平台不能使用Go的標准編碼json包,該包支持將json解組到結構(Struct)中,但是如果結構字段和傳入的數據字段類型不匹配,就會出現異常。
為了克服這個挑戰,需要一種將后端映射到標准格式的自定義方法。在對該方法進行了幾次迭代之后,團隊實現了一個自定義的數據編出流程。雖然這種方法感覺有點像重新構建標准的lib包,但它為工程師提供了處理源數據的細粒度控制。
網絡支持
可伸縮性是新平台關注的焦點,Go的網絡和api標准庫支持可伸縮性。在Go中,可以在不需要框架的情況下快速實現可伸縮的HTTP端點入口。在下面的示例中,標准庫net/http包用於接受用戶請求並響應。當內容平台實施時,首先嘗試使用了一個API框架。隨着團隊認識到標准庫能夠滿足所有的網絡需求而又不增加額外的負擔,最終標准庫取代了該框架。Golang HTTP處理程序是可伸縮的,因為處理程序上的每個請求都在一個輕量級線程Goroutine中並發運行。
package main
import(
"fmt"
"log"
"net/http"
)
funchandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
funcmain() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
並發模型
Go的並發模型提供了跨平台的性能改進。處理分布式數據意味着要與向消費者承諾的保證作斗爭。根據CAP定理,不可能同時提供以下三個保證中的兩個以上:一致性、可用性、分區容忍。在經濟學人的平台上,最終的一致性是可以接受的,這意味着來自數據源的讀取最終是一致的,所有數據源達到一致狀態的適度延遲是可以容忍的。縮小這種差距的方法之一是利用Goroutines。
Goroutines是Go運行時管理的輕量級線程,用於防止線程耗盡。Goroutines支持跨平台優化異步任務。例如,該平台的數據存儲之一是Elasticsearch。當內容在系統中更新時,在Elasticsearch中引用該項目的內容將被更新並重新索引。通過實現Goroutines,減少了再處理時間,確保項目的一致性更快。這個示例演示了在Goroutine中如何對每個符合再處理條件的項進行再處理。
funcreprocess(searchResult *http.Response) (int, error) {
responses := make([]response,len(searchResult.Hits))
var wg sync.WaitGroup
wg.Add(len(responses))
for i, hit := rangesearchResult.Hits {
wg.Add(1)
go func(i int,item elastic.SearchHit) {
deferwg.Done()
code,err := reprocessItem(item)
responses[i].code= code
responses[i].err= err
}(i, *hit)
}
wg.Wait
return http.StatusOK, nil
}
設計系統不僅僅是簡單的編程,工程師必須了解在何時何地使用哪些工具。雖然Go對於內容平台的大多數需求來說是一個強大的工具,但某些局限性需要其他解決方案。
依賴管理
Go發布時沒有依賴管理系統,社區內有一些工具來滿足這種需求。經濟學人使用Git子模塊,整個社區與此同時也正在積極推動一個標准的依賴管理工具。雖然社區更建議采用一致的方法進行依賴關系管理,但仍有許多分歧。在經濟學人內部使用Git子模塊進行依賴管理並沒有帶來重大的挑戰,但對其他Go開發者來說,它是一個需要加以考慮的因素。
還有一些平台需求是Go的功能或設計不太適合的。由於平台增加了對音頻處理的支持,Go的元數據提取工具在當時是有限的,因此團隊選擇了Python的Exiftool。平台服務在docker容器中運行,這也允許了安裝Exiftool並從Go應用程序調用它。
funcrunExif(args []string) ([]byte, error) {
cmdOut, err :=exec.Command("exiftool", args...).Output()
if err != nil {
return nil, err
}
return cmdOut, nil
}
該平台的另一個常見場景是從源CMS系統接收損壞的HTML,將HTML解析為有效的,並對HTML進行清洗。Go最初用於此過程,但由於Go標准HTML庫期望得到有效的HTML輸入,因此需要大量定制代碼來解析HTML輸入。這段代碼很快變得不堪重負,對於邊緣例外情況無法有效處理。Javascript實現了一種新的解決方案,為管理HTML驗證和清洗提供了更大的靈活性和適應性。
Javascript也是平台中事件過濾和路由的常見選擇。事件使用AWS Lambdas進行過濾,AWS Lambdas是輕量級函數。一個用例是將事件過濾到不同的通道中,例如快速通道和慢通道。此篩選基於事件包裝器JSON對象中的單個元數據字段完成。過濾實現利用Javascript JSON指針包抓取JSON對象中的元素。與Go所需的完整JSON解組相比,這種方法要有效得多。雖然這種類型的功能可以通過Go實現,但是對於工程師來說,使用Javascript更容易,並且提供了更簡單的Lambdas。
回顧
在實現了內容平台的實施並生產上線運行之后,對於這一歷程回顧如下:
好的地方?
-
分布式系統的關鍵語言設計元素
-
並發模型,相對容易實現
-
愉快的編碼和有趣的社區
不足的地方?
-
版本控制和標准方面的需要進一步提升
-
在某些領域缺乏成熟的解決方案
總的來說,使用Go來快速構建系統是一種積極的體驗,Go是內容平台擴展項目成功的關鍵元素之一。經濟學人是一個多語言的平台(注:此處指編程語言),在合適的地方使用不同的語言來解決特定問題。例如,在處理文本和動態內容時,Go可能永遠不會是首選,所以團隊將Javascript納入工具集中。然而,Go在支持系統擴展和發展起到主要作用。
在考慮是否要使用Go時,可以考慮一下系統設計的關鍵問題:
-
系統目標是什么?
-
你為你的消費者提供了什么保證與承諾?
-
什么樣的架構體系和模式適合你的系統?
-
系統需要如何擴展?
如果你正在設計一個旨在解決分布式數據、異步工作流、高性能和可伸縮性挑戰的系統,可以考慮使用Go來加速構建並達成系統目標。
原文作者:KathrynJonas 譯者:江瑋
英文原文:https://www.infoq.com/articles/golang-the-economist?utm_source=infoq&utm_medium=popular_widget&utm_campaign=popular_content_list&utm_content=