关键点
-
经济学人内容分发系统需要更大的灵活性,将内容传递给日益多样化的数字渠道。为了实现这一灵活性目标并保持高水平的性能和可靠性,平台从一个单体结构过渡到微服务体系结构。
-
用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=
微信公众号: 架构师日常笔记 欢迎关注!