Lyft Envoy入门教程


【编者的话】Envoy是一款由Lyft开源的7层代理和通信总线,本文作者就Envoy的背景、主打功能特性以及一些配置细节做了简单介绍。

使用微服务来解决现实世界中遇到的问题常常会比简单地编写代码更加深入。你需要测试你的服务。你需要弄清楚如何进行持续部署。你需要找出一个服务之间干净,优雅,弹性的交互方式。

Lyft公司出品的Envoy是一款非常有趣的工具,它可以帮助服务之间“互相交谈”。

Lyft Envoy概览

Envoy Proxy是一款现代化的,高性能,小体积的边缘及服务代理。Enovy为用户的服务加入了弹性和监测能力,它是通过一种对服务透明的方式做到这一点的。你可能会觉得很奇怪,我们为什么要给一个自称为代理的家伙加油打气呢 —— 毕竟,在此之前业内已经出现了无数款代理软件,其中还包括一些重量级的明星产品,像NGINXHAProxy,是吧?然而,这也正是Enovy的有趣之处:
  • 它能够代理任何TCP协议
  • 它支持双向SSL
  • 它将HTTP/2视为一等公民,并且可以在HTTP/2和HTTP/1.1之间相互转换(双向)
  • 它在服务发现和负载均衡方面很灵活

  • 它被设计用于提高系统的可见性
    • 尤其是,Enovy可以生成许多流量方面的统计数据,这是其它代理软件很难取代的地方
    • 在某些情况下(如MongoDB和Amazon RDS),Enovy可以很确切地知道如何查看压缩协议(wire protocol)并进行透明监控

  • 相比于其它代理软件,Envoy更容易搭建
  • Envoy是一个sidecar进程,因此它对服务的实现语言完全无感知


Enovy可以通过一些相对高端——而复杂——的方式扩展,我们会在之后深入了解这块——可能要很久以后。现在我们先简单了解一下。(如果你感兴趣,想了解更多关于Envoy的细节,Matt Klein2017年微服务实践者峰会上有过一次很棒的演讲)。

Envoy支持任意TCP协议的代理,包括SSL,做到这一点是相当了不起的。想要代理Websockets?Postgres?Raw TCP?都没问题。另外,Envoy支持同时接收和发起SSL连接,这在某些时候很方便:你可以让Enovy做客户端证书验证,而与此同时仍然在Envoy和你的服务之间维持一个SSL连接。

当然,HAProxy也支持任意协议的TCP和SSL代理,但在HTTP/2方面,它只支持将整个流转发到一个支持HTTP/2的后端服务器。NGINX不支持任意协议的代理(公平地讲,Enovy也不支持像FastCGI这样的协议,因为Envoy不是一个Web服务器)。无论是开源的NGINX还是HAProxy都不能很好的处理服务发现(尽管NGINX Plus有提供一些参数),并且它们也无法提供和一个配置好的Envoy代理所给出的统计数据等同的功能。

总的来说,我们认为Envoy前景良好,它通过一款单一的软件满足了我们的众多需求,而不需要我们去搭配一些工具混合使用。

Envoy架构

虽然我说过,相比一些曾经使用过的其他产品,Envoy的配置并不是那么繁琐。但是你可能会注意到,我并没有说过它一定很容易。Envoy的学习曲线刚开始是有些陡峭的,这是有原因的,而且也是值得的。

Envoy和它的网络栈

假设你要编写一款HTTP网络代理。有两种显而易见的方式:在HTTP层工作,或者在TCP层工作。

在HTTP层的话,你将会从传输线路上读取整个HTTP请求的数据,对它做解析,查看HTTP头部和URL,并决定接下来要做什么。随后,你将从后端读取整个响应的数据,并将其发送给客户端。这即是一款OSI 7层(应用层)代理:代理完全明白用户想要做什么,并且可以利用这些认知做出非常聪明的决策。

这种做法的缺点就是非常复杂和缓慢 —— 试想在做出任何决定之前因为读取和解析整个请求而引入的延迟!更糟糕的是,有时候最高级别的协议根本没有决策所需的信息。一个很好的例子便是SSL:在添加SRI扩展之前,SSL客户端永远不会说明它在尝试连接哪台主机 —— 因此尽管HTTP服务器能够很好地处理虚拟主机(使用HTTP/1.1协议中定义的Host头部),一旦涉及到SSL,你就必须给你的服务器专门分配一个IP地址,因为一个7层代理根本没有正确代理SSL所需的信息。

因此,也许更好的选择是下沉到TCP层操作:只读取和写入字节,并使用IP地址,TCP端口号等来决定如何处理事务。这即是OSI3层(网络)或4层(传输)代理,具体取决于用户要交互的对象。我们将借用Envoy里的术语,将其称之为3/4层代理。

在这个模型中,代理处理事务的速度很快,某些事情变得更优雅和简单(参见上面的SSL示例)。另一方面,假设用户要根据不同的URL代理到不同的后端呢?对于经典的L3 / 4代理,这是不可能的:在这些层上无法访问更高层的应用程序信息。

Envoy支持同时在3,4层和7层操作,以此应对这两种方法各自都有其实际限制的现实。这很强大,并且也非常高效...但是用户通常付出的代价便是配置的复杂性。

我们面临的挑战是让简单的事务维持它的简便,同时允许复杂的事务成为可能,而Envoy在HTTP代理等方面做得相当不错。

Envoy网格

关于Envoy,接下来这一点可能会让人感到多少有些惊讶,那便是大多数应用涉及到的是两层Envoy,而不是一层:

  • 首先,会有一个“边缘Envoy”在某个地方单独运行。边缘Envoy的工作是给其他地方提供一个入口。来自外部的传入连接请求到这里,边缘Envoy将会决定他们在内部的转发路径。

  • 其次,服务的每个实例都有自己的Envoy与它一起运行,这是一个运行在服务一侧的独立进程。这些“服务Envoy”会密切关注他们的服务,并且记住哪些是正在运行的,哪些不是。

  • 所有的Envoy形成一个网格,然后在他们之间共享路由信息。

  • 如果需要的话(通常都是这样),服务间调用也可以通过Envoy网格。我们稍后会讨论到这个问题。


注意,你当然也可以只使用边缘Envoy,然后去掉Envoy服务这一层。 但是,使用完整网格的话,服务Envoy可以对应用服务进行健康监控等,让网格知道尝试联系一个挂掉的服务是否是毫无意义的。此外,Envoy的统计数据收集最适合用在全网格上(尽管这块更多内容会放到一篇单独的文章里)。

网格中的所有Envoy使用同一份代码运行,但是它们的配置显然是不同的……这就来到了下一节,关于Envoy的配置文件。

Envoy配置概览

Envoy的配置看起来很简单:它主要由监听器(listener)和集群(cluster)组成。

一个监听器告诉Envoy它应该监听的TCP端口,以及决定Envoy应该收听处理的一组过滤器(filter)。集群会告诉Envoy,它可以代理传入请求的一个或多个后端主机。到目前为止一切还挺顺利。但是,这里有两个方法可以让事情变得更简单:

  • 过滤器可以——通常也是必须的——拥有他们自己的配置,而且通常他们会比监听器的配置更加复杂!

  • 集群和负载平衡可以放在一起,并且可以用到像DNS之类的一些外部事物。


由于我们一直在谈论HTTP层的代理,我们不妨继续看一下http_connection_manager这个过滤器。该过滤器工作在3/4层,因此它可以访问来自IP和TCP的信息(如连接两端的主机和端口号),但是它也可以很好地理解HTTP协议,获取HTTP URL、标题等,这均适用于HTTP/1.1和HTTP/2。每当新连接抵达时,http_connection_manager都会利用上述的所有这些信息来决定哪个Envoy集群最适合处理该连接。然后,Envoy集群将会使用其负载平衡算法挑选出单个成员来处理该HTTP连接。

http_connection_manager的过滤器配置是一个包含很多选项的字典,但是目前最重要的一个参数是virtual_hosts数组,它定义了过滤器该如何做出路由决策。数组中的每个元素都是包含如下属性的字典:
  • name:一个服务的可读名称
  • domains:一组DNS风格的域名,每个必须和该虚拟主机的URL中的域名匹配才能匹配
  • routes:一组路由字典


每个路由字典至少需要包含:
  • prefix:此路由的URL路径前缀
  • cluster:处理此请求的Envoy集群
  • timeout_ms:如果出错放弃的超时时间


所有这一切即代表着一套最简单的HTTP代理 - 在HTTP的指定端口上侦听,然后根据URL路由到不同的主机 - 实际上在Envoy中配置起来也非常简单。

例如:将以/service1开头的URL代理到名为service1的集群,将以/service2开头的URL代理到名为service2的集群,你可以使用:
“virtual_hosts”: [
              {
                “name”: “service”,
                “domains”: [“*”],
                “routes”: [
                  {
                    “timeout_ms”: 0,
                    “prefix”: “/service1”,
                    “cluster”: “service1”
                  },
                  {
                    “timeout_ms”: 0,
                    “prefix”: “/service2”,
                    “cluster”: “service2”
                  }
                ]
              }
            ]

就是这样。请注意,我们使用domains [“*”]表明我们不太关心请求哪个主机,并且值得一提的是我们可以按需添加多条路由。最后,边缘Envoy和服务Envoy之间的这个监听器配置基本相同:主要区别在于服务Envoy可能只有一个路由,它只会代理localhost上的服务而不是包含多个主机的一个集群。

当然,我们仍然需要定义上面virtual_hosts部分中引用的service1service2集群。 我们可以在cluster_manager配置部分实现这一点,它也是一个字典,并且还有一个称为clusters的关键组件。它的值,又是一组词典:
  • name:集群的一个可读名称
  • type:此集群将如何知道哪些主机已启动?
  • lb_type:这个集群将如何处理负载均衡?
  • hosts:定义集群所属主机的一个URL数组(实际上通常是tcp:// URLs)。


type的可选值有:
  • static:在集群中列出所有可代理的主机
  • strict_dns:Envoy将会监控DNS,而每个匹配的A记录都将被认为是有效的
  • logical_dns:Envoy一般会使用DNS来添加主机,但是如果DNS不再返回它们时,也不会丢弃它们(想想有数百台主机的round-robin DNS - 我们将在后续文章中详细介绍)
  • sds:Envoy将会去调用一个外部的REST服务以查找集群成员


lb_type的可选值有:
  • round_robin:按顺序循环遍历所有健康的主机
  • weighted_least_request:选择两个随机健康的主机并选择请求最少的主机(这是O(1),其中扫描所有健康的主机将是O(n)。Lyft声称研究表明O(1)算法"差不多"和全扫描的效果“一样好”。
  • random:随机挑选一台主机


关于负载均衡还有一个有趣的注意事项:集群还可以定义一个恐慌阈值(panic threshold),其中,如果集群中健康主机的数量低于恐慌阈值,集群将判定健康检查算法可能存在问题,并且假定所有主机在集群中是健康的。这可能会导致一些意外情况发生,因此这一点最好留意一下!

一个边缘Envoy的简单用例可能是这样的:
“clusters”: [
              {
                “name”: “service1”,
                “type”: “strict_dns”,
                “lb_type”: “round_robin”,
                “hosts”: [
                  {
                    “url”: “tcp://service1:80”
                  }
                ]
              },
              {
                “name”: “service2”,
                “type”: “strict_dns”,
                “lb_type”: “round_robin”,
                “hosts”: [
                  {
                    “url”: “tcp://service2:80”
                  }
                ]
              }
            ]

由于我们将这个集群标记为strict_dns类型,它将依赖于在DNS中查找service1service2,我们假定任何新起的服务实例将会被添加到DNS中 ———— 这可能适用于像使用docker-compose配置的类似情况。针对服务Envoy(比如service1),我们可能会采取一个更直接的路由方式:
“clusters”: [
              {
                “name”: “service1”,
                “type”: “static”,
                “lb_type”: “round_robin”,
                “hosts”: [
                  {
                    “url”: “tcp://127.0.0.1:5000”
                  }
                ]
              }
            ]

想法类似,只是目标不太一样:我们总是在本地主机上转发到我们的服务,而不是重定向到其他主机。

接下来

这就是Envoy千里之行的一小步,我们还介绍了一些深入内容,关于Envoy的背景和配置。接下来,我们将使用Kubernetes,Postgres,Flask和Envoy实际部署一个简单的应用程序,并在这个过程中观察我们扩容和缩容时会发生什么。敬请关注。

原文链接:Part 1: Getting started with Lyft Envoy for microservices resilience(译者:吴佳兴)

0 个评论

要回复文章请先登录注册