前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Micro 构建弹性与容错的应用程序

使用 Micro 构建弹性与容错的应用程序

作者头像
StoneDemo
发布2018-06-28 16:07:01
1.2K0
发布2018-06-28 16:07:01
举报
文章被收录于专栏:小石不识月小石不识月

自上一篇博客发布以来,已有一段时日了,此间我们一直在努力研究 Micro,并且已经初见成效。现在让我们一起深入探讨吧!

如果您想先仔细研究 Micro 工具包,可点击此处查看之前的博文,或者如果您想了解更多关于微服务的概念,请看这里

大家都懂,构建分布式系统是具有挑战性的。尽管我们一路上已经解决了很多问题,但我们仍然要经历许多重建构建模块(Building block)的周期。无论是由于迁移到下一个抽象层次,虚拟机到容器,采用新的开发语言,利用基于云的服务,还是即将转向的微服务。似乎总有些什么东西需要我们重新学习如何为下一波技术构建高性能和容错系统。

迭代和创新之间的斗争永远不会结束,但我们需要做一些事情来帮助缓解向云、容器以及微服务的转变而带来的诸多痛苦。

动机

那么,为什么我们要这么做?为什么我们不断地重建构建模块,为什么我们一直试图解决同规模、容错和分布式的系统问题?

我所想到的术语是“更大,更强,更快”,甚至是“速度,规模,敏捷”。您将能从 C 级高管那里听到很多这样的词语,但其关键的结论是,我们需要不断地构建性能更高且更具弹性的系统。

在互联网初期,只有数千甚至数十万人上网。随着时间的推移,我们看到了增长,现在我们已经达到了数十亿的数量级。数十亿人,数十亿设备。我们必须学习如何为此构建系统。

对于老一辈来说,您可能还会记得 C10K 问题。我不确定我们处于哪个位置,但我认为我们正在讨论解决不亚于数百万并发连接的问题。世界上最大的科技公司在十年前真正解决了这个问题,并且具有大规模构建系统的模式,但我们其他人仍在学习。

亚马逊,谷歌和微软等公司如今给我们提供了云计算平台,以充分发挥其规模效应,但我们仍在努力研究如何编写能够有效利用这些平台的应用程序。如今您经常能听到这些术语:容器编排、微服务,以及云本地化。这项工作正在多个层面上展开,而且在我们作为一个行业真正敲定需要向前发展的模式和解决方案之前,还需要一些时间。

许多公司现在正在帮助解决“我如何以可扩展且容错的方式运行我的应用程序?”,但仍然很少有人帮助解决更重要的问题......

我如何能真正地以可扩展和容错的方式 编写 应用程序?

Micro 则着眼于解决这些问题,它重点关注微服务的关键软件开发需求。我们将从客户端开始,介绍一些可以帮助您构建弹性和容错应用程序的方法。

客户端

客户端是一个构建模块,它用于在 Go-Micro 中提出请求。如果您在此之前构建过微服务或 SOA 架构,那么您就知道,很重要的一部分时间与执行都花费在调用其他服务以获取相关信息上了。

而在整体式应用程序中,主要侧重在于提供内容,但在微服务领域,它更多地涉及检索或发布内容。

以下是包含三种最重要的方法的 Go-Micro 客户端接口的简化版本:调用(Call),发布(Publish)和流(Stream)。

代码语言:javascript
复制
type Client interface {
	Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
	Publish(ctx context.Context, p Publication, opts ...PublishOption) error
	Stream(ctx context.Context, req Request, opts ...CallOption) (Streamer, error)
}
type Request interface {
	Service() string
	Method() string
	ContentType() string 
	Request() interface{}
	Stream() bool
}

调用与流用于发出同步请求。其中调用返回单个结果,而流则是与另一个服务一起维护的双向流(Bidirectional streaming)连接,消息可以在连接中来回流动。发布则用于通过代理发布异步消息,但我们今天不会讨论这一点。

客户端是如何在幕后进行工作的,我已在之前的几篇博文中提到,您可以在这里这里找到这些文章。如果您想了解详细信息,请点击相应链接以查看具体内容。

我们将简要地提及一些重要的内部细节。

客户端处理 RPC 层,同时利用代理、编解码器、注册表、选择器,以及传输包(Transport package)来实现各种功能。分层架构非常重要,因为我们将每个组件的关注点分离,从而降低复杂性并提供可插拔性(Pluggability)。

为什么客户端很重要?

客户端从本质上抽象出在服务之间提供弹性和容错通信的细节。对另一个服务进行调用似乎相当直接,但它有多种可能的失败方式。

让我们开始介绍其中的一些功能,以及它对我们有什么帮助。

服务发现

在分布式系统中,服务的实例可以因为各种各样的理由来来去去。网络分区、机器故障、重调度(Rescheduling)等。我们不需要关心这个。

在调用另一个服务时,我们按名称进行,并允许客户端使用服务发现将名称解析为具有其地址和端口的实例列表。服务在启动时注册发现,在关闭时则取消注册。

正如我们所提到的那样,分布式系统中可能会发生各式各样的问题,服务发现也不例外。所以我们依靠通过经过验证(Battle Tested)的分布式服务发现系统(例如 consul,etcd 和 zookeeper)来存储关于服务的信息。

这些系统都使用 Paxos 的 Raft 网络共识算法,这使得我们可以从 CAP 定理中获得一致性和分区容忍度。通过运行 3 或 5 个节点的集群,我们可以容忍大部分系统故障,并为客户端获得可靠的服务发现。

节点选择

所以现在我们可以可靠地将服务名称解析到一个地址列表。我们实际要如何选择要调用哪一个?这就是 Go-Micro Selector 发挥作用的地方了。它建立在注册表之上,并提供诸如轮流调度或随机哈希这样的负载平衡策略,同时还提供过滤,缓存和将失败节点列入黑名单的方法。

以下是一个简化的接口。

代码语言:javascript
复制
type Selector interface {
	Select(service string, opts ...SelectOption) (Next, error)
	Mark(service string, node *registry.Node, err error)
	Reset(service string)
}
type Next func() (*registry.Node, error)
type Filter func([]*registry.Service) []*registry.Service
type Strategy func([]*registry.Service) Next

平衡策略

目前的策略非常直截了当。当 Select 被调用时,Selector 将从注册表中检索服务,并创建一个 Next 函数,该函数使用默认策略或者作为选项传入(如果被重写)的节点池来封装节点池。

客户端将调用 Next 函数根据负载平衡策略检索列表中的下一个节点,并发出请求。如果请求失败并且重试数设置在 1 以上,则它将重复同样的过程,以检索要调用的下一个节点。

此处我们可以使用各种各样的策略,例如循环法、随机哈希、最少连接(leastconn)、加权法等。负载平衡策略对于在服务之间均匀地分布请求是必不可少的。

缓存选择

尽管拥有健壮的服务发现系统非常重要,但对每个请求进行查找可能会效率低下,并且开销很大。您试着想象一个大型的系统,其中每个服务都在这样做,那么发现系统可能相当容易过载。在某些情况下,系统可能会变得完全不可用。

为了避免这种情况,我们可以使用缓存。大多数发现系统提供了监听更新的方法,这通常称为 Watcher。我们不会主动轮询发现,而是等待事件发送给我们。Go-Micro Registry 为此提供了一个 Watch 的抽象(Abstraction)。

我们编写了一个缓存选择器,用它维护一个内存缓存的服务。在缓存未命中时,它会查找信息的发现,并将其缓存,然后将其用于后续请求。如果收到了关于我们所知的服务的 Watch 事件,则缓存将相应地进行更新。

首先,这通过删除服务查找来大幅提高性能。在服务发现失败的情况下,它还提供了一些容错功能。虽然我们有些偏执,但由于某些故障情况,缓存可能会过时,因此节点应适当地进行 TTL 处理。

列入黑名单的节点

下一个是黑名单。注意 Selector 接口有 Mark 和 Reset 方法。我们永远无法真正保证健康的节点是在发现过程中进行注册的,因此需要对其进行一些处理。

无论何时提出请求,我们都会跟踪结果。如果一个服务实例多次失败,我们实际上可以将该节点列入黑名单,并在下次发出选择请求时将其过滤掉。

节点在被放回池中之前被会列入黑名单一段时间。如果某个服务的某个节点出现故障,我们将其从列表中删除,以便继续为成功的请求提供服务,这一点至关重要。

超时与重试

Adrian Cockcroft 最近开始讨论微服务架构中缺失的组件。其中一个非常有趣的事情,就是传统的超时和重试策略导致了连锁故障。我恳请点击这里阅览他的幻灯片。我这里直接贴上了它开始介绍超时和重试的位置。感谢 Adrian 让我使用他的幻灯片。

这张幻灯片很好地总结了这个问题。

上面 Adrian 所描述的是一种常见的情况,即缓慢的响应可能会导致超时,然后导致客户端重试。由于请求实际上是顺流而下的一系列请求,这将通过系统创建一组全新的请求,而旧的工作可能仍在继续。错误的配置可能会导致调用链中的服务过载,并造成难以恢复的故障情况。

在微服务领域,我们需要重新思考处理超时和重试的策略。Adrian 继续讨论了这个问题可能的解决方案。其中一个方案是超时预算(Timeout budget)并针对新节点进行重试。

在重试方面,我们已经在 Micro 中做了一段时间。可以将重试次数配置为客户端的选项。如果调用失败,客户端将检索新节点并尝试再次发出请求。

超时是经过深思熟虑的,但实际上这是从经典的静态超时设置开始的。直到 Adrian 提出他的想法后,策略才变得清晰起来。

超时预算方法现在已经嵌入到 Micro 中。让我们来看看它是如何工作的。

第一个调用者设置超时,这通常发生在边缘。在链上的每个请求中,超时都被减少,以说明其传递过程中已消耗的时间。当剩下的时间为 0 时,我们将停止处理任何进一步的请求或重试并返回调用堆栈。

正如 Adrian 所述,这是一种提供动态超时预算的好方法,可以消除下游发生的任何不必要的工作。

除此之外,下一步则应该是除掉任何类型的静态超时。服务如何进行响应,这将根据环境、请求负载等而有所不同。这应该是一个动态的 SLA,根据它的当前状态而变化,但是要留到以后再说。

连接池怎么样?

连接池是构建可伸缩系统(Scalable system)的重要组成部分。我们很快就会看到没有它时的局限性。通常这会导致文件描述符受到限制和端口耗尽。

目前有一个 PR 正在将连接池添加到 Go-Micro。考虑到 Micro 的插件扩展特性,在 Transport 的上层解决这个问题非常重要,如此一来任何实现(无论是 HTTP,NATS 还是 RabbitMQ 等)都将因此受益。

您可能会想,这是特定的实现,某些传输协议可能已经支持它了。虽然确实如此,但并不总能保证在每个运输工具上都以相同的方式工作。通过解决这个特定的问题,我们减少了运输本身的复杂性和需求。

还有什么?

以上这些都是 Go-Micro 中内置的一些非常有用的东西,但还有其它什么吗?

我很高兴你发问了......或者说,我假设你问了......管它呢~

服务版本 Canarying?

我们的项目中有这个特性!实际上,这个内容在之前的一篇关于微服务的架构和设计模式的博客文章中我已经讨论过了,您可以点击这里查看

在服务发现中,服务包含了名称(Name)和版本(Version),并作为一对。当从注册表中检索服务时,它的节点按照版本进行分组。然后就可以利用选择器使用各种负载平衡策略在每个版本的节点之间分配流量。

为什么 Canarying 很重要?

在发布新版本服务,并确保所有的功能都能正常运行之前,它是非常有用的。新版本可以部署到一个小的节点池中,客户端会自动将一定比例的流量分配给新服务。结合 Kubernetes 等编排系统,您可以放心地使用 canary 进行部署,并在出现任何问题时进行回滚。

过滤(Filtering)怎么样?

我们有这个特性!选择器功能非常强大,它具有在选择时通过滤波器来过滤节点的功能。在提出请求时,这些可以作为调用选项(Call Option)传递给客户端。一些现有的用于元数据(Metadata)、终端(Endpoint)或版本过滤的过滤器可以在这里找到。

为什么说过滤是重要的?

您可能有一些只存在于一组服务版本中的功能。将服务之间的请求流固定到这些特定版本可以确保您总是命中正确的服务。在系统中有多个版本同时运行的情况下,这是非常棒的。

另一个有用的用例是,你想要基于位置的服务路由。通过在每个服务上设置数据中心标签,您可以应用只会返回本地节点的过滤器。基于元数据的过滤功能非常强大,并且有着更广泛的应用,我们希望从自然使用中了解更多。

可插件扩展的体系结构

你会一遍又一遍地听到的事情之一,即是 Micro 的可插拔性(Pluggable,即可加入插件进行扩展)。这是设计之初就做好了的。相对于一个完整的系统,Micro 所提供的构建模块是非常重要的。这是一些拿到即用但可以增强 Micro 能力的东西。

为什么可拔插性是重要的?

每个人对于 “构建分布式系统意味着什么” 这个问题,都会有不同的想法,我们真的希望为人们提供一种设计他们想要使用的解决方案的方法。不仅如此,我们还可以使用具有鲁棒性的 Battle 测试工具,而不是从头开始编写所有的东西。

技术不断在更新,每天都会涌现新的更好的工具。我们如何避免锁定(Lock in)?可插拔的架构意味着我们今天可以使用组件,并且未来某时能以最小的努力将它们切换出来。

插件

Go-Micro 的每个功能都是作为 Go 的接口创建的。通过这样做并且只引用接口,我们实际上可以用最少到零的代码更改交换底层的实现。在大多数情况下,就是在命令行上指定一个简单的导入语句和标志。

GitHub 上的 Go-Plugins 仓库中有许多插件。

虽然 Go-Micro 提供了一些默认设置,例如服务发现的 consul 和传输协议是 http,但您可能希望在架构中使用不同的东西,甚至实现自己的插件。我们现在已经在 PR 模式下为社区贡献了 Kubernetes 注册表插件和Zookeeper 注册表。

我要如何使用插件?

大多数情况下,这事儿这很简单。

代码语言:javascript
复制
# Import the plugin
import _ "github.com/micro/go-plugins/registry/etcd"
go run main.go --registry=etcd --registry_address=10.0.0.1:2379

如果你想看到更多相关操作,请查看关于 Micro 上的 NAT 的文章。

封装

更重要的是,客户端和服务端还支持中间件(Middleware)的概念,即封装器(Wrapper)。通过支持中间件,我们可以在请求-响应(Request-response)处理的基础上添加额外功能的前后钩子(Hook)。

中间件是一个很好理解的概念,迄今为止在数千个库中已有使用。您马上就能看到诸如断路器(Circuit breaking),限速(Rate limiting),认证(Authentication),记录(Logging),追溯(Tracing)等用例的好处。

代码语言:javascript
复制
# Client Wrappers
type Wrapper func(Client) Client
type StreamWrapper func(Streamer) Streamer
# Server Wrappers
type HandlerWrapper func(HandlerFunc) HandlerFunc
type SubscriberWrapper func(SubscriberFunc) SubscriberFunc
type StreamerWrapper func(Streamer) Streamer

我要如何使用封装器?

这和插件一样简单。

代码语言:javascript
复制
import (
	"github.com/micro/go-micro"
	"github.com/micro/go-plugins/wrapper/breaker/hystrix"
)
func main() {
	service := micro.NewService(
		micro.Name("myservice"),
		micro.WrapClient(hystrix.NewClientWrapper()),
	)
}

很简单,不是吗?我们发现许多公司在 Micro 顶上创建了自己的层,以初始化大部分他们正找寻的默认封装器,所以如果需要添加任何新的封装器,它们都可以在同一个位置完成。

现在让我们看看下面几种封装器的弹性和容错能力。

断路器

在 SOA 或微服务的领域中,单个请求实际上可能会导致对多个服务进行调用,并且在很多情况下可能需要几十个或更多的服务来收集必要的信息才能返回给调用者。对于成功的情况,这种方法可以很好地运行,但如果发生了问题,它会迅速下降为级联故障(Cascading failure),如果不把整个系统重置,则难以恢复。

我们通过请求重试和将多次失败节点加入黑名单,从而部分地解决了客户端中的一些,但在某些时候可能需要让客户端停止尝试发出请求。

此处便是断路器发挥作用的所在。

断路器的概念很简单。函数的执行被封装起来,或与某种追踪故障的监视器相关联。当故障次数超过特定阈值时,断路器将跳闸,并且之后的任何调用的尝试都会返回错误而不执行封装的函数。超时之后,电路进入半开放状态(Half open state)。如果在这种状态下,单个调用失败,则断路器会再次跳闸,而如果成功调用,我们则将恢复到正常状态(闭路)。

虽然 Micro 客户端内置了一些容错功能,但我们不应期望这能够解决所有问题。但结合现有的断路器实现并使用封装器,我们将受益匪浅。

限速

如果我们能够毫不费力地满足世界各地的所有请求,这不是很好吗?简直如梦如幻。然而现实并非如此。处理查询需要一段时间,并且由于资源有限,我们实际能服务的请求量是由资源的多少决定的。

在某些时候,我们需要考虑限制并行完成或服务的请求数量。此时限速就该发挥作用了。如果没有限速措施,它可能非常容易陷入资源枯竭或导致系统完全瘫痪,并令其停止服务于任何进一步的请求。这通常是进行大规模 DDOS 攻击的基础。

每个人都听说过,甚至实现过某种形式的限速。目前有许多不同的限速算法,其中之一是 Leaky Bucket 算法。我们不打算详细介绍算法的细节,但它值得一读。

我们可以再次使用 Micro Wrappers 和现有的库来执行此功能。现成的实现可以在这里找到。

我们真正感兴趣的系统是隶属于 YouTube 的 Doorman,这是一种全球分布式客户端限速器。我们正在为它寻找社区贡献者,如有意愿请联系我们!

服务端

至此我们介绍了很多关于客户端的功能或用例。那么服务端又如何呢?首先我们需要注意的是,Micro 利用 Go-Micro 客户端来实现 API,CLI,Sidecar 等等特性。这些优势将整个架构从边缘转变为最后端的服务。不过,我们仍然需要去解决一些服务器的基本问题。

在客户端中,注册表用于查找服务,而服务端则是实际注册的地方。当一个服务实例出现时,它会 “优雅地” 通过服务发现机制将自己注册,并在它正常退出时自我注销。请注意这个词 —— “优雅地”。

故障处理

在分布式系统中,我们必须对故障进行处理,我们需要具有容错能力。注册表支持 TTL 以将基于任何底层服务发现机制(例如 consul,etcd)的节点终止(Expire)或标记为不健康的(Unhealthy)。而服务本身也支持重新注册。这两者的组合意味着,当服务节点是健康的,它将在一个设定的时间间隔内进行重新注册,并且如果未刷新,则注册表将把节点终止。如果节点因任何原因失败同时不重新进行注册,则它将被从注册表中删除。

这种容错行为最初并未作为 Go-Micro 的一部分,但我们很快从实际使用中看到,由于应急(Panic)以及其它导致服务无法正常退出的故障,此时很容易出现过期的节点填满注册表的情况。

敲击(Knock on)的效果是,如果不是数百个过期的记录,客户端就会被留下处理几十个。虽然客户端也需要具有容错能力,但我们认为该功能可以预先消除很多问题。

增加更多功能

另外需要注意的是,如上所述,服务器还提供了使用封装器或中间件的功能,因为它更为人熟知。这意味着我们可以在此层使用断路器,限速以及其他功能来控制请求流,并发性等。

服务端的功能有意保持简单但可拔插,如此功能就可以根据需要而置于顶层。

客户端与 Sidecars 的对比

本文讨论的大部分内容都存在于核心 Go-Micro 库中。虽然这对所有 Go 程序员来说很棒,但其他人可能会疑惑:我如何获得所有这些优势?

Micro 最初就已经包含了一个 Sidecar(挎斗)的概念,它是一个 HTTP 代理,具有内置 Go-Micro 的所有功能。因此,无论您使用何种语言构建应用程序,您都可以通过使用 Micro Sidecar 从我们讨论过的所有内容中受益。

挎斗模式不是什么新鲜事物。NetflixOSS 有一个名为 Prana 的平台,它利用的是基于 JVM 的 NetflixOSS 堆栈。Buoyant(一家云服务公司) 最近已经加入了一个令人难以置信的、功能丰富的系统(它被称为 Linkerd),这是一个RPC代理层,它在 Twitter 的 Finagle 库的顶层。

Micro Sidecar 使用默认的 Go-Micro 客户端。所以如果你想添加其他功能,你可以很容易地增加并重建。我们将来会着眼于更简化这一过程,并提供一个预构建所有有用容错功能的版本。

等等,还有一些话要唠叨

本篇博文涵盖了很多关于核心 Go-Micro 库和周边工具包的内容。这些工具是一个很好的开始,但还不够。当你想要大规模运行时,当你想要用数百个微服务服务于数百万个请求时,此时还有很多亟待解决的问题。

平台

这是 Go 平台以及平台发挥作用的所在。在 Micro 的基础构建模块中,平台更进一步地解决了在规模上运行的需求。身份验证,分布式追溯,同步,健康检查监控(Healthcheck monitoring)等等。

分布式系统需要一套与众不同的工具来实现可观察性、一致性,以及协调容错能力,Micro 平台可以帮助满足这些需求。通过提供分层架构,我们可以在核心工具定义的基元(Primitive)上进行构建,并在有需要时增强其功能。

现在还处于早期阶段,但我们希望 Micro 平台能够解决组织在构建分布式系统平台时遇到的许多问题。

我如何使用这些工具?

正如你可以从博客文章中看到的,这些功能大部分都内置在Micro工具箱中。您可以在 GitHub 上查看相应项目,并立即开始编写可容错的 Micro 服务。

如果您需要帮助或有任何疑问,请通过 Slack 加入社区。这一社区非常活跃,并且发展迅速,它有着广泛的用户,从黑客到今天已经在使用 Micro 的公司,各式各样的用户都有。

总结

科技正在迅速发展,如今云计算使我们能够获得几乎无限的规模。试图跟上变化的步伐可能会很困难,为新世界构建可扩展的容错系统仍然是具有挑战性的。

但不一定要这样。作为一个社区,我们可以互相帮助以适应这种新的环境,并建立能够满足我们日益增长的需求的产品。

Micro 通过提供简化构建和管理分布式系统的工具来帮助开发者实现这一目标。希望这篇博文能够帮助我们展示一些我们正在找寻的方法。

如果您想了解更多有关我们提供的服务或微服务的信息,请查看博客、网站 micro.mu 或 Github 仓库

请关注我们的 Twitter 账户 @MicroHQ,或点击此处加入 Slack 社区。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 动机
  • 客户端
    • 为什么客户端很重要?
    • 服务发现
    • 节点选择
      • 平衡策略
        • 缓存选择
          • 列入黑名单的节点
          • 超时与重试
          • 连接池怎么样?
          • 还有什么?
            • 服务版本 Canarying?
              • 为什么 Canarying 很重要?
                • 过滤(Filtering)怎么样?
                  • 为什么说过滤是重要的?
                  • 可插件扩展的体系结构
                    • 为什么可拔插性是重要的?
                      • 插件
                        • 我要如何使用插件?
                          • 封装
                            • 我要如何使用封装器?
                              • 断路器
                                • 限速
                                • 服务端
                                  • 故障处理
                                    • 增加更多功能
                                    • 客户端与 Sidecars 的对比
                                    • 等等,还有一些话要唠叨
                                      • 平台
                                      • 我如何使用这些工具?
                                      • 总结
                                      相关产品与服务
                                      消息队列 TDMQ
                                      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档