關鍵點前端
經濟學人內容分發系統須要更大的靈活性,將內容傳遞給日益多樣化的數字渠道。爲了實現這一靈活性目標並保持高水平的性能和可靠性,平臺從一個單體結構過渡到微服務體系結構。golang
用Go編寫的服務是新系統的一個關鍵組件,它使得團隊可以交付可伸縮的、高性能的服務並快速迭代新產品。docker
Go的併發性和對API的支持以及它做爲靜態編譯語言的設計,使得分佈式事件系統可以大規模執行。與此同時,Go對於測試的支持也很是出色。編程
總的來講,團隊在Go上的使用經驗是積極的,這也是內容平臺得以擴展的關鍵因素之一。json
隨着新聞消費從紙媒轉向數字媒體,《經濟學人》的使命是讓更普遍的數字受衆看到這種技術轉變。所以須要更大的靈活性,將內容傳遞給日益多樣化的數字渠道。爲了實現這一靈活性目標並保持高水平的性能和可靠性,平臺從一個單體結構走向微服務體系結構。用Go編寫的服務是新系統的一個關鍵組件,它將使團隊可以交付可伸縮的、高性能的服務並快速迭代新產品。後端
如下是基於Go的一些實踐與問題:api
容許工程師快速迭代產品並開發新特性服務器
強化智能錯誤處理並針對服務快速失敗的實踐網絡
爲分佈式系統中的高併發和網絡提供有力支持數據結構
在內容和媒體所需的領域缺少成熟度和支持
經過平臺實現規模化的數字內容出版發行
爲何使用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=