用Micro构建有弹性和容错的应用程序

构建分布式系统是很有挑战性的,这已经不是什么秘密了。虽然我们已经解决了很多问题,作为一个行业,我们仍然经历了重建许多建筑街区的周期。是否因为迁移到下一个抽象级别,虚拟机到容器,采用新的语言,利用基于云的服务,甚至是这种向微服务的转变。总有一些东西似乎需要我们重新学习如何为下一波技术构建性能和容错系统。

这是一次迭代和创新之间的无休止的战斗,但是我们需要做一些事情来帮助减轻很多痛苦,因为向云、容器和微服务的转移仍在继续。

动机

我们为什么要这么做?为什么我们要不断地重建构建块,为什么还要继续尝试解决相同的规模、容错和分布式系统问题?

“更大、更强、更快”,甚至可能是“速度、规模、敏捷”。你会从c级高管那里听到很多,但关键的结论是,我们总是需要建立更多的业绩和弹性系统。

在互联网的早期,只有成千上万的人上网。随着时间的推移,我们看到了加速,我们现在进入了数十亿的数量级。数十亿人,数十亿的设备。我们必须学习如何建立这样的系统。

对于老一代,你可能还记得C10K问题。我不确定我们现在在哪里,但我认为我们讨论的是解决数百万并发连接的问题,如果不是更多的话。世界上最大的科技公司在十年前就真正解决了这个问题,并且在规模上有了构建系统的模式,但是我们其余的人仍然在学习。

亚马逊(Amazon)、谷歌和微软(Microsoft)等公司现在为我们提供了大规模的云计算平台,但我们仍在努力研究如何编写能够有效利用它的应用程序。您现在听到的术语是容器编排、微服务和云本地。这项工作正在进行中,将会有一段时间,在我们作为一个行业真正敲定需要向前发展的模式和解决方案之前。

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

如何以可伸缩和容错的方式编写应用程序?

通过关注微服务的关键软件开发需求来解决这些问题。现在,我们将介绍一些可以帮助您构建弹性和容错应用程序的方法,从客户端开始。

Client

客户端是在go-micro中提出请求的构建块。如果您之前已经构建了微服务或SOA架构,那么您就会知道,大部分时间和执行都花费在调用其他服务来获取相关信息上。

而在一个单一的应用程序中,主要关注的是服务内容,在一个微服务的世界中,它更多的是检索或发布内容。

这里有一个精简版的go-micro客户端接口,有三个最重要的方法; Call, Publish和Stream。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

Call和Stream用于同步请求。Call返回一个单独的结果,而Stream则是与另一个服务保持的双向流连接,可以来回发送消息。发布用于通过代理发布异步消息,但我们今天不打算讨论这个问题。

客户端是如何在幕后工作的,在前几篇博客文章中,你可以在这里和这里找到。如果你想了解细节,可以去看看。

我们将简要地介绍一些重要的内部细节。

客户端处理RPC层,同时利用代理、编解码器、注册表、选择器和传输包进行各种功能。分层架构很重要,因为我们分离了每个组件的关注点,降低了复杂性并提供了可插拔性。

为什么Client很重要?

客户端本质上是抽象了提供服务之间的弹性和容错通信的细节。call给另一个服务似乎是相当直接的,但有可能会失败的各种方法。

让我们开始了解其中的一些功能以及它的帮助。

服务发现

在分布式系统中,服务的实例可以根据各种原因来进行。网络分区,机器故障,重新调度等等,我们并不是真的想要关心它。

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

正如我们所提到的,任何数量的问题都可能出现在分布式系统中,服务发现也不例外。因此,我们依靠作战测试分布式服务发现系统,如consul、etcd和zookeeper来存储有关服务的信息。

每一种方法都使用Paxos网络一致性算法,这使我们能够从CAP定理得到一致性和分区的容忍度。通过运行一个3或5个节点的集群,我们可以容忍大多数系统故障,并为客户机提供可靠的服务发现。

节点选择

因此,现在我们可以可靠地将服务名称解析为地址列表。我们如何选择调用哪一个呢?这就是go-micro选择器发挥作用的地方。它构建在注册表上,并提供负载平衡策略,如轮询或随机散列,同时提供过滤、缓存和黑名单失败节点的方法。

这是一个裁剪的接口。

1
2
3
4
5
6
7
8
9
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

平衡策略

目前的策略相当直截了当。当选择被调用时,选择器将从注册表中检索服务,并创建一个下一个函数,该函数使用默认策略封装节点池,或者在被重写时作为一个选项传入。

客户端将根据负载均衡策略调用下一个函数来检索列表中的下一个节点,并提出请求。如果请求失败并且重试设置在1之上,它将经历相同的过程,检索下一个要调用的节点。

在这里可以使用多种策略,例如轮询、随机散列、最小conn、加权等。负载均衡策略对于在服务中均匀分布请求至关重要。

选择缓存

虽然有一个健壮的服务发现系统很好,但是对每个请求进行查找都是低效和昂贵的。如果您想象一个大型的系统,每个服务都在这样做,那么就很容易使发现系统过载。可能会出现完全不可用的情况。

为了避免这种情况,我们可以使用缓存。大多数发现系统提供了一种监听更新的方法,通常被称为观察者。而不是轮询发现,我们等待事件被发送给我们。go-micro注册表为这个提供了一个表抽象。

我们编写了一个缓存选择器,它在服务的内存缓存中维护一个。在缓存缺失时,它会查找信息的发现,缓存它,然后在随后的请求中使用它。如果接收到我们所知道的服务的监视事件,那么缓存将相应地更新。

首先,通过删除服务查找,极大地提高了性能。它还提供了在服务发现失败的情况下的一些容错。虽然我们有点偏执,但由于一些失败的场景,缓存可能会变得陈旧,因此节点会被适当地处理。

黑名单的节点

接下来是黑名单。注意选择器接口有标记和复位方法。我们永远不能真正保证健康的节点是通过发现注册的,所以我们需要做些什么。

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

在将节点放到池中之前,该节点将被列入一段时间的黑名单。如果服务的某个节点失败了,我们将它从列表中删除,这样我们就可以继续为成功的请求提供服务,这一点非常重要。

超时和重试

Adrian Cockroft最近开始讨论微服务架构中缺少的组件。其中一个非常有趣的事情是经典的超时和重试策略导致级联失败。我恳求你去看看他的幻灯片。我直接链接到它开始覆盖超时和重试的地方。感谢艾德里安让我用幻灯片。

这张幻灯片确实很好地概括了这个问题。

Adrian所描述的是一种常见的情况,在这种情况下,缓慢的响应会导致超时,然后导致客户机重试。由于请求实际上是下游的请求链,因此通过系统创建了一整套新的请求,而旧的工作可能仍在进行。错误配置可能导致在调用链中超载服务,并导致难以恢复的失败场景。

在微服务领域,我们需要重新考虑处理超时和重试的策略。Adrian接着讨论了这个问题的潜在解决方案。其中一个是超时预算和对新节点的重试。

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

超时是一些经过深思熟虑的事情,但实际上是从经典的静态超时设置开始的。直到艾德里安提出他的想法后,才明白该策略应该是什么。

预算的暂停现在被建立在Micro上。让我们来看看它是如何工作的。

第一个调用者设置超时,这通常发生在边缘。在链上的每个请求中,超时都被减少,以解释已经通过的时间量。当零时间结束时,我们停止处理任何进一步的请求或重试并返回调用堆栈。

正如Adrian所提到的,这是一种提供动态超时预算并删除在下游发生的不必要工作的好方法。

进一步来说,下一步应该是删除任何类型的静态超时。服务响应的方式会因环境、请求负载等而有所不同。这应该是一个动态的SLA,它会根据当前的状态而变化,但又会有一些东西需要保留一天。

连接池呢?

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

目前正在进行的一项工作是将连接池添加到go-micro。考虑到微的可插拔性,在传输层上处理这一层非常重要,因此任何实现(无论是HTTP、NATS、RabbitMQ等)都将受益。

你可能会想,这是具体的实现,有些传输可能已经支持了。虽然这是事实,但并不总是保证在每个传输过程中都使用相同的方法。通过解决这个特定的问题,我们降低了传输本身的复杂性和需求。

还有什么?

这些都是一些非常有用的东西,可以用来做go-micro,但还有什么呢?

我很高兴你问我…或者,假设你问…。

服务版本Canarying

我们有它!它实际上是在之前的一篇关于微服务的架构和设计模式的博客文章中讨论过的,您可以在这里查看。

服务包含名称和版本作为服务发现中的一对。当从注册中心检索服务时,它的节点按版本分组。然后,可以利用各种负载均衡策略,利用选择器来在每个版本的节点之间分配流量。

Canarying为什么重要?

在发布新版本的服务并确保所有的功能都能正常运行之前,这是非常有用的。新版本可以部署到一个小的节点池中,客户端会自动分配一定比例的流量给新服务。结合像Kubernetes这样的业务流程系统,如果有任何问题,您可以使用信任和回滚来增强部署。

过滤呢?

我们有它!选择器非常强大,它包括在选择时通过过滤器来过滤节点的能力。在发出请求时,这些可以作为调用选项传递给客户端。一些现有的过滤器可以在这里找到元数据、端点或版本过滤。

过滤为什么重要?

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

另一个有用的用例是您希望基于位置的服务路由。通过在每个服务上设置一个数据中心标签,您可以应用一个只返回本地节点的过滤器。基于元数据的过滤功能非常强大,并且有更广泛的应用程序,我们希望能在野外听到更多的应用。

可插拔的体系结构

你会不断听到的一件事是Micro的可插拔性。这是从第一天开始设计的。与一个完整的系统相比,Micro提供构建块非常重要。可以在盒子里工作但可以增强的东西。

为什么是可插入的问题?

每个人对于构建分布式系统都有不同的想法,我们真的想提供一种方式让人们设计他们想要使用的解决方案。不仅如此,还有强大的战斗测试工具,我们可以利用这些工具,而不是从头开始编写任何东西。

技术总是在进化,新的和更好的工具每天都在出现。如何避免锁定?一个可插拔的架构意味着我们可以在今天使用组件,并在明天用最少的努力将它们转换出来。

插件

Go -micro的每个特性都被创建为Go接口。通过这样做并且只引用接口,我们实际上可以将底层实现用最少的代码替换为零。在大多数情况下,在命令行上指定一个简单的import语句和标志。

在GitHub上的go-plugin repo中有许多插件。

虽然go-micro提供了一些默认值,比如发现和http的传输,但是您可能希望在您的体系结构中使用一些不同的东西,甚至可以实现自己的插件。我们现在已经在公共关系模式中使用了Kubernetes注册插件和Zookeeper注册表。

如何使用插件?

大多数时候都是这么简单。

1
2
# Import the plugin
import _ "github.com/micro/go-plugins/registry/etcd"
1
go run main.go --registry=etcd --registry_address=10.0.0.1:2379

包装器

更重要的是,客户端和服务器支持中间件的概念,即所谓的包装器。通过支持中间件,我们可以在请求-响应处理周围添加附加功能的pre和post挂钩。

中间件是一个很好理解的概念,并且在数千个库中使用。您可以立即看到在用例中所带来的好处,如电路中断、速率限制、身份验证、日志记录、跟踪等。

1
2
3
4
5
6
7
8
# 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

我如何使用包装器?

这和插件一样直接。

1
2
3
4
5
6
7
8
9
10
11
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()),
)
}

简单的对吧?我们发现,许多公司在微观上创建自己的层,以初始化他们正在寻找的大多数默认包装器,因此,如果需要添加新的包装器,那么它们都可以在一个地方完成。

现在让我们来看看一些弹性和容错的包装器。

断路器

在SOA或微服务世界中,单个请求实际上可以导致对多个服务的调用,在许多情况下,可能会导致数十个或更多的请求收集必要的信息,以返回给调用者。在成功的情况下,这很好,但是如果出现问题,它会很快下降到级联故障,这很难在不重置整个系统的情况下恢复。

我们在客户端部分解决了这些问题,请求重试,并将多次失败的黑名单节点解决,但在某个时刻,可能需要阻止客户端尝试发出请求。

这就是断路器发挥作用的地方。

断路器的概念是直接的。函数的执行被包装或与跟踪故障的某种监视器关联。当故障数量超过某个阈值时,断路器就会被绊倒,任何进一步的调用尝试都会返回一个错误,而不执行包装的函数。在超时时间后,电路被放入半开放状态。如果在这个状态下一个调用失败,那么这个断路器就会再次被绊倒,但是如果它成功了,我们就会恢复到一个闭合电路的正常状态。

虽然Micro客户机的内部结构具有一些内置的容错功能,但我们不应该期望能够解决所有问题。在现有的断路器实现中使用包装器可以大大受益。

速度限制

如果我们能毫不费力地满足世界上所有的要求,那不是很好吗?啊梦。现实世界并不是这样的。处理一个查询需要一定的时间,并且由于资源的限制,我们实际上可以提供很多请求。

在某种程度上,我们需要考虑限制我们可以并行地创建或服务的请求的数量。这就是速率限制发挥作用的地方。如果没有速率限制,就很容易导致资源耗尽或完全瘫痪系统,并阻止它继续提供任何进一步的请求。这通常是DDOS攻击的基础。

每个人都听说过,使用过或者甚至可能实施过某种形式的限速。有很多不同的速率限制算法,其中之一是漏桶算法。我们不打算讨论算法的细节,但值得一读。

再一次,我们可以利用微包装器和现有的库来执行这个功能。在这里可以找到一个现有的实现。

我们真正感兴趣的一个系统是YouTube的门卫,一个全球分布式客户端速率限制器。我们正在寻找一个社区的贡献,所以请联系!

服务器端

所有这些都涉及到很多客户端特性或用例。服务器端呢?首先要注意的是,Micro杠杆利用了API、CLI、Sidecar等的go-micro客户端。这些好处将整个体系结构从边缘转化为最后的后端服务。不过,我们仍然需要为服务器解决一些基本问题。

在客户端,注册中心用于查找服务,而服务器端则是注册实际发生的地方。当一个服务的一个实例出现时,当它优雅地退出时,它会使用服务发现机制和注销器注册自己。关键词是优雅地

处理失败

在分布式系统中,我们必须处理故障,我们需要容错。注册表支持TTLs过期或标记节点为不健康的,基于任何基础的服务发现机制是e.g consul,etcd。而服务本身也支持重新注册。这两种方法的组合意味着服务节点将在一个集合间隔上重新注册,而它是健康的,并且注册中心将在没有刷新的情况下终止该节点。如果节点因任何原因失败,且不重新注册,则将从注册表中删除该节点。

这种容错行为最初并不是作为go-micro的一部分,但是我们很快从现实世界中看到,由于恐慌和其他导致服务不优雅地退出的故障,很容易在注册表中填充陈旧的节点。

这样做的效果是,如果没有几百个不新鲜的条目,客户就会被留下来处理几十个。虽然客户端也需要容错,但我们认为这种功能可以避免很多问题。

添加更多的功能

另外要注意的一点是,正如上面提到的,服务器还提供了使用包装器或中间件的能力,因为它更广为人知。这意味着我们可以在这一层使用电路中断、速率限制和其他特性来控制请求流、并发性等。

服务器的功能是故意保持简单但可插入的,这样功能就可以按要求分层排列。

Clients vs Sidecars

这里讨论的大部分内容都存在于核心的go-micro库中。虽然这对所有的程序员来说都很好,但其他人可能会想,我该如何获得这些好处呢?

从一开始,Micro就包含了Sidecar的概念,它是一个HTTP代理,它包含了go-micro内置的所有功能。因此,无论您使用哪种语言构建应用程序,您都可以通过使用Micro Sidecar从以上讨论中获益。

sidecar模式并不是什么新鲜事。NetflixOSS有一个名为Prana的系统,它利用基于JVM的NetflixOSS栈。最近,有一个叫Linkerd的功能丰富的系统,它是一个在Twitter的Finagle库之上的RPC代理。

Micro Sidecar使用默认的go-micro客户端。所以如果你想添加其他功能,你可以很容易地增加它和重建。我们将在将来进一步简化这个过程,并提供一个具有所有漂亮容错功能的版本。

等等,还有更多

这篇博客文章涵盖了许多核心的go-micro库和周围的工具包。这些工具是一个很好的开始,但它们还不够。当你想要大规模运行时,当你想要数百个服务数以百万计的微服务时,还有很多需要解决的问题。

Platform

这就是go-platformplatform发挥作用的地方。在micro基础构建块的地方,platform更进一步地解决了在规模上运行的需求。认证、分布式跟踪、同步、健康检查监控等。

分布式系统需要一套不同的工具来观察、协商一致和协调容错,微平台可以帮助满足这些需求。通过提供分层架构,我们可以在核心工具定义的原语基础上构建,并在需要时增强其功能。

现在还为时尚早,但人们希望micro platform能解决许多组织在构建分布式系统平台时遇到的问题。

我如何使用所有这些工具?

正如您可以从博客文章中收集到的,这些特性中的大部分都是内置在Micro工具包中。您可以在GitHub上查看项目,并且几乎可以立即开始编写容错Micro服务。

如果你需要帮助或有问题,请加入我们的社区。它非常活跃,而且发展迅速,用户广泛,从黑客的侧面项目到现在已经在使用Micro生产的公司。

总结

技术正在迅速发展,云计算现在给我们提供了几乎无限的规模。试图跟上变革的步伐是困难的,构建可伸缩的新世界的容错系统仍然具有挑战性。

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

通过提供简化构建和管理分布式系统的工具,Micro可以帮助您实现这一过程。希望这篇博客能帮助我们展示一些我们正在寻找的方法。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :