The Economist經濟學人是如何使用Go語言構建內容平臺微服務架構的?

本文要點

  • The Economist需要更大的靈活性,將內容提供給日益多樣化的數字渠道。爲了實現這個靈活性的目標並保持高水平的性能和可靠性,其平臺從一個整體架構過渡到了微服務架構。
  • 用Go編寫的服務是其新系統的一個關鍵組件,它將使The Economist能夠提供可伸縮的高性能服務,並快速迭代其新產品。
  • Go的併發性和API支持,以及它作爲靜態編譯語言的設計,使得它適合實現大規模執行的分佈式事件處理系統。其測試支持也很出色。
  • 總的來說,The Economist的團隊在Go的使用方面給我們帶來了積極的經驗,這也是內容平臺得以擴展的關鍵因素之一。
  • Go並不總是正確的工具,沒關係。The Economist擁有一個多語言平臺,可以根據需要使用不同的語言。

我以Drupal開發人員的名義加入了The Economist工程團隊。然而,我真正的任務是參與一個從根本上重塑The Economist內容交付技術的項目。在最初的幾個月裏,我學習Go,與一位外部顧問合作,用幾個月的時間構建了一個最小可行產品,然後重新加入團隊,指導他們的Go之旅。

隨着新聞消費從紙媒轉向數字媒體,The Economist的使命是抵達更廣泛的數字受衆,這是這種技術轉變的推動力。The Economist需要更大的靈活性,將內容提供給日益多樣化的數字渠道。爲了實現這一靈活性的目標並保持高水平的性能和可靠性,平臺從一個整體架構過渡到了微服務架構。用Go編寫的服務是其新系統的一個關鍵組件,它將使The Economist能夠提供可伸縮的高性能服務並快速迭代其新產品。

Go在The Economist的推廣:

  • 使工程師可以快速迭代和開發新特性;
  • 藉助智能錯誤處理實施快速失敗服務的最佳實踐;
  • 在分佈式系統中提供健壯的併發和網絡支持;
  • 在某些需要內容和媒體的方面不夠成熟,缺少一些支持;
  • 簡化了一個大規模數字發佈平臺的實現。

The Economist爲什麼選擇Go?

爲了回答這個問題,有必要重點介紹下新平臺的總體架構。該平臺稱爲內容平臺,是一個基於事件的系統。它響應來自不同內容創作平臺的事件,並觸發在離散工作者微服務中運行的處理流。這些服務執行數據標準化、語義標籤分析、ElasticSearch索引以及將內容推送到蘋果新聞或Facebook等外部平臺等操作。該平臺還有一個RESTful API,它與GraphQL相結合,是前端客戶端和產品的主要入口。

在設計整體架構時,團隊研究了適合平臺需求的語言。將Go與Python、Ruby、Node、PHP和Java做了比較。雖然每種語言都有其優點,但是Go最符合平臺的架構。Go的併發性和API支持,以及它作爲靜態編譯語言的設計,使得它適合實現大規模執行的分佈式事件處理系統。此外,Go語法相對簡單,學習以及開始編寫工作代碼更容易,這讓一個經歷瞭如此多技術轉換的團隊可以實現速贏。總之,可以確定,Go這門語言是兼顧了基於雲的分佈式系統的可用性和效率的最佳設計。

三年之後,Go幫助實現了這些遠大的目標了嗎?

Go語言可以很好地滿足平臺設計的幾個要素。快速失敗是系統的關鍵部分,因爲它是由分佈式的獨立服務組成的。遵循十二要素應用原則,應用程序需要快速啓動和失敗。Go被設計成一種靜態編譯語言,啓動速度快,而且編譯器的性能在不斷提高,對於工程或部署來說從來都不是問題。此外,Go的錯誤處理設計使得應用程序不僅能更快地失敗,而且能更智能地失敗。

錯誤處理

工程師很快就會注意到Go的一個不同之處,它沒有異常,而是有一個Error類型。在Go中,所有錯誤都是值。Error類型是預先聲明的,它是一個接口。Go中的接口本質上是一個命名的方法集合,任何其他自定義類型如果具有相同的方法,都可以滿足該接口的要求。Error類型是一個可以用字符串描述自己的接口。

type error interface {
    Error() string
}

這爲工程師提供了更好的控制和錯誤處理功能。在任何自定義模塊中,通過添加一個返回字符串的Error方法,就可以創建自定義錯誤並生成它們,就像使用如下所示的來自Errors包的新函數一樣。

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

這在實踐中意味着什麼呢?在Go中,函數可以返回多個值,因此,如果函數失敗,它很可能返回一個錯誤值。該語言鼓勵開發人員在錯誤出現的地方顯式地檢查錯誤(而不是拋出和捕獲異常),因此,代碼中通常會有一個“if err != nil”檢查。這種頻繁的錯誤處理一開始似乎是重複的。但是,Error作爲一個值使開發人員能夠使用該錯誤來簡化錯誤處理。例如,在分佈式系統中,可以通過封裝錯誤輕鬆地實現重試。

網絡問題是一個在系統中總會遇到的問題,無論是向其他內部服務發送數據,還是向第三方工具推送數據。下面這個來自Net包的示例重點說明了如何把錯誤作爲一種類型用來區分臨時網絡錯誤和永久錯誤。當向外部API推送內容時,The Economist團隊使用類似的錯誤封裝來實現增量重試。

package net

type Error interface {
    error
    Timeout() bool   // 錯誤是超時嗎?
    Temporary() bool // 錯誤是臨時的嗎?
}

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}

if err != nil {
    log.Fatal(err)
}

Go的作者認爲,並非所有例外都是例外。工程師應該聰明地從錯誤中恢復,而不是讓應用程序失敗。此外,Go的錯誤處理允許開發人員對錯誤有更多的控制,這可以改進諸如調試或錯誤可用性之類的東西。在內容平臺中,Go的這個設計特性使得開發人員能夠圍繞錯誤做出深思熟慮的決策,從而增強了整個系統的可靠性。

一致性

一致性是內容平臺中的一個關鍵因素。在The Economist,內容是業務的核心,而內容平臺的目標就是確保內容可以一次發佈隨處閱讀。因此,每個產品和消費者都必須與內容平臺的API保持一致。其產品主要使用GraphQL來查詢API,這需要一個靜態模式作爲消費者和平臺之間的契約。平臺處理的內容需要和這個模式保持一致。靜態語言有助於實現這一點,並且很容易確保數據一致性。

Go的測試

另一個提高一致性的特性是Go的測試包。Go的快速編譯與作爲一等特性的測試相結合,使團隊能夠在構建管道中將強大的測試實踐嵌入到工程工作流和快速失敗中。Go提供的測試工具使它們非常易於設置和運行。運行“go test”就可以在當前目錄中運行所有測試,測試命令有幾個有用的特性標識。標識“cover”提供了關於代碼覆蓋率的詳細報告。“bench”測試會運行基準測試,測試函數名要以單詞“Bench”而不是“Test”開始。TestMain函數提供了方法,可以用於進行額外的測試設置,例如,模擬身份驗證服務器。

此外,Go還能夠使用匿名結構和模擬接口創建表測試,提高測試覆蓋率。雖然測試並不是什麼新鮮的語言特性,但是,Go使得編寫健壯的測試並無縫地嵌入工作流變得很容易。從一開始,The Economist的工程師就能夠把測試作爲構建管道的一部分來運行,無需進行任何特殊的定製,甚至在將代碼推送到Github之前,還添加了Git Hooks來運行測試。

不過,該項目在實現一致性方面並非沒有困難。該平臺面臨的第一個主要挑戰是管理來自不可預知的後端的動態內容。該平臺主要是通過JSON端點使用來自源CMS系統的內容,而數據結構和類型並沒有保證。也就是說,平臺不能使用Go的標準編碼/json包,該包支持將JSON解組成結構,但是,如果結構字段和傳入數據的字段類型不匹配,就會出現莫名其妙的問題。

爲了克服這一挑戰,需要一個將後端映射到標準格式的自定義方法。在對該方法進行了幾次迭代之後,團隊實現了一個自定義的反編組過程。雖然這種方法感覺有點像重建一個標準庫的包,但它讓工程師可以細粒度地控制如何處理源數據。

網絡支持

可伸縮性是新平臺關注的焦點,而Go的網絡及API標準庫支持可伸縮性。在Go中,你可以在沒有任何框架的情況下快速實現可伸縮的HTTP端點。下面的示例使用標準庫的net/http包創建了接受請求和響應寫入器參數的處理程序。在內容平臺API首次實現時,它使用了一個API框架。最終,標準庫取代了它,因爲團隊認識到,它可以滿足他們的所有網絡需求,而不會帶來額外的代碼膨脹。Go語言的HTTP處理程序是可伸縮的,因爲處理程序上的每個請求都在一個輕量級線程Goroutine中併發運行,不需要自定義。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

併發模型

Go的併發模型提供了多項跨平臺的性能改進。處理分佈式數據意味着要與承諾給消費者的保證作鬥爭。根據CAP定理,在以下三個保證中,要同時提供兩個以上的保證是不可能的:一致性、可用性、分區容錯性。在The Economist的平臺上,最終一致性是可以接受的,也就是說,數據源的讀取最終是一致的,在所有數據源達到一致狀態的過程中,適度的延遲是可以容忍的。縮小這種延遲的其中一個方法就是利用Goroutines。

Goroutines是Go運行時管理的輕量級線程,用於防止線程耗盡。Goroutines支持跨平臺優化異步任務。例如,該平臺的其中一個數據存儲是Elasticsearch。當系統內容更新時,Elasticsearch中指向該項內容的引用將被更新並重新索引。使用Goroutines減少了再處理時間,確保內容項更快地達到一致狀態。這個例子說明了如何在Goroutine中對每個符合再處理條件的內容項進行再處理。

func reprocess(searchResult *http.Response) (int, error) {
	responses := make([]response, len(searchResult.Hits))	
	var wg sync.WaitGroup
	wg.Add(len(responses))
	
	for i, hit := range searchResult.Hits {
		wg.Add(1)
		go func(i int, item elastic.SearchHit) {
			defer wg.Done()
			code, err := reprocessItem(item)
			responses[i].code = code
			responses[i].err = err
		}(i, *hit)
	}
	wg.Wait

	return http.StatusOK, nil
}

設計系統不僅僅是簡單的編程,工程師必須瞭解在什麼地方什麼時候使用什麼工具。雖然Go作爲一個強大的工具滿足了The Economist內容平臺的大部分需求,但其存在的某些侷限性需要其他的解決方案。

依賴管理

Go在發佈時沒有依賴管理系統。社區開發了一些工具來滿足這一需求。The Economist使用了Git Submodules,這在當時是有道理的,因爲社區正在積極推動一個標準的依賴管理工具。到今天爲止,雖然社區距離一種一致的依賴管理方法更近了,但還不夠好。在The Economist,子模塊方法並沒有帶來重大的挑戰,但對於其他Go開發者來說,它一直是一個挑戰,這在轉換到Go時需要加以考慮。

對於這個平臺,還有一些需求是Go的特性或設計不太適合的。由於平臺增加了對音頻處理的支持,當時用於元數據提取的Go工具非常有限,所以,團隊選擇了Python的Exiftool。平臺服務在Docker容器中運行,它支持安裝Exiftool並從Go應用程序中運行。

func runExif(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中實現了一種新的解決方案。JavaScript爲管理HTML驗證和清理過程提供了更大的靈活性和適應性。

JavaScript也是平臺中事件過濾和路由的常見選擇。事件通過AWS Lambda進行過濾。AWS Lambda是僅在調用時運行的輕量級函數。一個用例是將事件過濾到不同的通道,例如快通道和慢通道。該過濾是基於事件封裝器JSON對象中的單個元數據字段完成的。這種過濾實現利用JavaScript JSON指針包獲取JSON對象中的元素。與Go所需的完整JSON反編組相比,這種方法要有效得多。雖然這類功能可以通過Go實現,但是對於工程師來說,使用JavaScript更容易,並且它提供了更簡單的Lambda表達式。

回顧

在實現了內容平臺並投入生產應用之後,如果要我對Go和內容平臺做一次回顧,那麼我會作出如下回復。

什麼做得好?

  • 針對分佈式系統的關鍵語言設計要素
  • 相對易於實現的併發模型
  • 令人愉快的編碼體驗和有趣的社區

什麼還可以改進?

  • 版本控制和廠商標準有待進一步完善
  • 在某些方面不夠成熟
  • 在特定場景下比較繁瑣

總的來說,這是一種積極的體驗,Go是實現可擴展內容平臺的其中一個關鍵因素。Go並不總是正確的工具,沒關係。The Economist擁有一個多語言平臺,可以根據需要使用不同的語言。在處理文本塊和動態內容時,Go可能永遠都不是第一選擇,所以JavaScript會在工具集中。然而,Go的強項構成了系統擴展和發展的基礎。

在考慮Go是否適合自己時,請回顧系統設計的關鍵問題:

  • 你的系統目標是什麼?
  • 你爲客戶提供什麼保證?
  • 什麼架構和模式適合你的系統?
  • 你的系統需要如何擴展?

如果你正在設計一個系統,旨在解決分佈式數據、異步工作流、高性能和可擴展性的挑戰,我建議你考慮Go,它可以大大加速系統目標的實現。

關於作者

image

Kathryn Jonas目前是Teachers Pay Teachers的一名軟件工程師,此前曾在The Economist擔任內容平臺的技術負責人。Jonas爲北京、倫敦和紐約的組織領導項目,將技術應用於各種挑戰,如任務影響評估、社論透明度和信任以及在線學習和協作。她熱衷於參與軟件架構的討論,並與活躍的獲得授權的團隊一起工作。

查看英文原文:Using Golang to Building Microservices at The Economist: A Retrospective

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章