8

ASP.NET Core 搭载 Envoy 实现微服务的反向代理

 3 years ago
source link: https://blog.yuanpei.me/posts/3599307335/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

ASP.NET Core 搭载 Envoy 实现微服务的反向代理

2021-07-0139 22 min.

话说,博主第一次接触到Envoy,其实是在微软的示例项目 eShopOnContainers,在这个示例项目中,微软通过它来为Ordering API、Catalog API、Basket API 等多个服务提供网关的功能。当时,博主并没有对它做深入的探索。此刻回想起来,大概是因为那个时候更迷恋领域驱动设计(DDD)的理念。直到最近这段时间,博主需要在一个项目中用到Envoy,终于决定花点时间来学习一下相关内容。所以,接下来这几篇博客,大体上会以记录我学习Envoy的历程为主。考虑到Envoy的配置项特别多,在写作过程中难免会出现纰漏,希望大家谅解。如对具体的配置项存在疑问,请以官方最新的 文档 为准,本文所用的示例代码已经上传至 Github,大家作为参考即可。对于今天这篇博客,我们来聊聊 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 这个话题,或许你曾经接触过 Nginx 或者 Ocelot,这次我们不妨来尝试一点新的东西,譬如,通过Docker-Compose来实现服务编排,如果对我说的这些东西感兴趣的话,请跟随我的脚步,一起来探索这广阔无垠的技术世界吧!

走近 Envoy

Envoy 官网对Envoy的定义是:

Envoy 是一个开源边缘和服务代理,专为原生云应用设计。

而更进一步的定义是:

Envoy 是专为大型现代服务导向架构设计的 L7 代理和通讯总线。

这两个定义依然让你感到云里雾里?没关系,请看下面这张图:

Envoy架构图
Envoy架构图

注:图片来源

相信从这张图中,大家多少能看到反向代理的身影,即下游客户端发起请求,Envoy对请求进行侦听(Listeners),并按照路由转发请求到指定的集群(Clusters)。接下来,每一个集群可以配置多个终结点,Envoy按照指定的负载均衡算法来筛选终结点,而这些终结点则指向了具体的上游服务。例如,我们熟悉的 Nginx ,使用listen关键字来指定侦听的端口,使用location关键字来指定路由,使用proxy_pass关键字来指定上游服务的地址。同样地,Ocelot 使用了类似的上下游(Upstream/Downstream)的概念,唯一的不同是,它的上下游的概念与这里是完全相反的。

你可能会说,这个Envoy看起来“平平无奇”嘛,简直就像是“平平无奇”的古天乐一般。事实上,Envoy强大的地方在于:

  • 非侵入式的架构: 独立进程、对应用透明的Sidecar模式
Envoy 的 Sidecar 模式
Envoy 的 Sidecar 模式
  • L3/L4/L7 架构:Envoy同时支持 OSI 七层模型中的第3层(网络层, IP 协议)、第四层(传输层,TCP / UDP 协议)、第七层(应用层,HTTP 协议)
  • 顶级 HTTP/2 支持: 视 HTTP/2 为一等公民,且可以在 HTTP/2HTTP/1.1间相互转换
  • gRPC 支持:Envoy 支持 HTTP/2,自然支持使用 HTTP/2 作为底层多路复用协议的 gRPC
  • 服务发现和动态配置:与 Nginx 等代理的热加载不同,Envoy 可以通过 API 接口动态更新配置,无需重启代理。
  • 特殊协议支持:Envoy 支持对特殊协议在 L7 进行嗅探和统计,包括:MongoDBDynamoDB 等。
  • 可观测性:Envoy 内置 stats 模块,可以集成诸如 prometheus/statsd 等监控方案。还可以集成分布式追踪系统,对请求进行追踪。

Envoy配置文件

Envoy通过配置文件来实现各种各样的功能,其完整的配置结构如下:

{
"node": "{...}",
"static_resources": "{...}",
"dynamic_resources": "{...}",
"cluster_manager": "{...}",
"hds_config": "{...}",
"flags_path": "...",
"stats_sinks": [],
"stats_config": "{...}",
"stats_flush_interval": "{...}",
"watchdog": "{...}",
"tracing": "{...}",
"runtime": "{...}",
"layered_runtime": "{...}",
"admin": "{...}",
"overload_manager": "{...}",
"enable_dispatcher_stats": "...",
"header_prefix": "...",
"stats_server_version_override": "{...}",
"use_tcp_for_dns_lookups": "..."
}

这里我们主要对侦听器(Listeners)、集群(Clusters) 和 管理(Admin)这三个常用的部分来进行说明。其中,(Listeners)、集群(Clusters) 均位于 static_resources 节点下,而 管理(Admin) 则有一个单独的admin节点。

侦听器(Listeners)

侦听器,顾名思义就是侦听一个或者多个端口:

static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 9090

在这里,它表示的是侦听9090这个端口,这里的listeners是一个数组,所以,你可以同时侦听多个端口。

过滤器(Filters)

我们知道,单单侦听一个或者多个端口,是无法完成一个HTTP请求的,因为它还不具备处理HTTP请求的能力。

Envoy Filters 架构图
Envoy Filters 架构图

Envoy 中,这一工作由一个或者多个过滤器组成的过滤器链(Filter Chains) 来完成:

static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 9090
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: AUTO
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/api/w"
route:
auto_host_rewrite: true
prefix_rewrite: /Weather
cluster: weatherservice
- match:
prefix: "/api/c"
route:
auto_host_rewrite: true
prefix_rewrite: /City
cluster: cityservice
http_filters:
- name: envoy.filters.http.router

在这段配置中,Http Connection Manager表示的是位于 L3(网络层)/L4(传输层) 的过滤器,这个过滤器连接的下一个过滤器是envoy.filters.http.router,表示的是 L7(应用层) 的关于路由的过滤器。个人感觉,这和我们通常所说的中间件相当接近。注意到,我们在 L3/L4 这个层级上做了什么事情呢?其实应该是 TCP/IP 层面上请求转发,这里定义的路由规则如下:

  • 当外部调用者访问/api/w时,请求会被转发到WeatherService
  • 当外部调用者访问/api/c时,请求会被转发到CityService

集群(Clusters)

在过滤器部分,我们定义了路由,那么,请求的最终去向是哪里呢?这里 Envoy 将其称之为 集群::

static_resources:
clusters:
# City Service
- name: cityservice
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN

集群本身,其实只是一个名字,并没有实际的意义,真正工作的其实是指向上游服务的终结点,我们可以为每一个集群指定一个负载均衡策略,让它决定选择哪一个终结点来提供服务。

负载均衡(Load Assignment)

目前,Envoy支持一下负载均衡算法:

  • ROUND_ROBIN:轮询
  • LEAST_REQUEST:最少请求
  • RING_HASH:哈希环
  • RANDOM:随机
  • MAGLEV:磁悬浮
  • CLUSTER_PROVIDED

下面的示例展示了如何为某个集群配置负载均衡:

static_resources:
clusters:
# Weather Service
- name: weatherservice
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: LEAST_REQUEST
load_assignment:
cluster_name: weatherservice
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: weatherservice1
port_value: 80
- endpoint:
address:
socket_address:
address: weatherservice2
port_value: 80

其中,weatherservice1weatherservice2是同一个服务的两个容器实例,当我们使用Docker-Compose进行构建的时候,不需要显式地去指定每一个服务的IP地址,只要对应docker-compose.yaml文件中的服务名称即可。

管理(Admin)

管理(Admin)这块儿相对简单一点,主要用来指定Envoy的的管理接口的端口号、访问日志存储路径等等:

admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9091 }

好了,在正确配置Envoy以后,我们来考虑如何对这些服务进行编排,在本文的 例子 中,我们有两个后端服务,WeatherServiceCityService,它们本质上是两个ASP.NET Core应用,我们希望通过Envoy来实现反向代理功能。这样做的好处是,后端服务的架构不会直接暴露给外部使用者。所以,你会注意到,在微服务架构的设计中,网关经常扮演着重要的角色。那么,如何对服务进行编排呢?这里我们使用Docker-Compose来完成这个工作:

version: '3'
services:
envoygateway:
build: Envoy/
ports:
- "9090:9090"
- "9091:9091"
volumes:
- ./Envoy/envoy.yaml:/etc/envoy/envoy.yaml
cityservice:
build: CityService/
ports:
- "8081:80"
environment:
ASPNETCORE_URLS: "http://+"
ASPNETCORE_ENVIRONMENT: "Development"
weatherservice:
build: WeatherService/
ports:
- "8082:80"
environment:
ASPNETCORE_URLS: "http://+"
ASPNETCORE_ENVIRONMENT: "Development"

可以注意到,这里需要部署3个服务,其中,Envoy负责监听9090端口,即对外的网关。而两个后端服务,WeatherServiceCityService则被分别部署在80828081端口。这里最重要的是envoy.yaml这个配置文件,我们在上一节编写的配置文件会挂在到容器目录:/etc/envoy/envoy.yamlEnvoy将如何使用这个配置文件呢?事实上,这个Dockerfile是这样编写的:

FROM envoyproxy/envoy-alpine:v1.16-latest
COPY ./envoy.yaml /etc/envoy.yaml
RUN chmod go+r /etc/envoy.yaml
CMD ["/usr/local/bin/envoy", "-c", "/etc/envoy.yaml", "--service-cluster", "reverse-proxy"]

除此之外,Envoy通过9091端口提供管理功能,我们可以通过这个端口来获得集群、请求、统计等信息,这一特性我们将会在接下来的文章里用到:

Envoy 管理界面功能一览
Envoy 管理界面功能一览

这里,想分享一个关于Envoy的小技巧,当我们在指定集群的地址时,可以使用docker-compose.yaml中定义的服务的名称,这会比填入一个固定的IP地址要更优雅一点,理由非常简单,我们不希望每次都来维护这个地址。

# Weather Service
- name: weatherservice
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: weatherservice
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# 建议使用 docker-compose.yaml 文件中对应的服务名称来代替IP地址
address: weatherservice
port_value: 80

接下来,如果每一个服务的Dockerfile都编写正确的话,我们就可以通过docker compose up命令启动这一组服务,通过命令行下打印出来的信息,我们可以确认,基于Envoy的网关服务、两个基于ASP.NET Core的后端服务都已经成功运行起来:

在 Docker-Compose 中成功启动服务
在 Docker-Compose 中成功启动服务

还记得我们的路由规则是是如何定义的吗?

  • 当外部调用者访问/api/w时,请求会被转发到WeatherService
  • 当外部调用者访问/api/c时,请求会被转发到CityService

实际的情况如何呢?我们不妨来验证一下:

通过 Envoy 调用 WeatcherService
通过 Envoy 调用 WeatcherService

可以注意到,不管是浏览器返回的结果,还是容器内部输出的日志,都表明请求确实被转发到对应的服务上面去了,这说明我们设计的网关已经生效。至此,我们实现了基于Envoy的反向代理功能,有没有觉得比Nginx要简单一点?重要的是,基于Docker-Compose的服务编排使用起来真的舒适度爆棚,这意味着你会有更多的属于程序员的贤者时间。前段时间热映的电视剧《觉醒时代》,鲁迅先生写完《狂人日记》后那一滴眼泪令人动容。也许,这种如匠人一般反复雕琢、臻于完美的心境是互通的,即使人类的悲欢并不相通,对美和极致的追求竟然出奇的相似。

本文主要介绍了 ASP.NET Core 搭载 Envoy 实现反向代理这一过程。对于 Envoy,有两个重要的定义:第一、Envoy 是一个开源边缘和服务代理,专为原生云应用设计。第二、Envoy 是专为大型现代服务导向架构设计的 L7 代理和通讯总线。相比 NginxOcelot ,Envoy 提供了 L7 级别的代理服务,支持 HTTP/2gRPC,无侵入式的Sidecar模式可以提供与应用进程完全隔离的代理服务。接下来,博主对 Envoy 配置文件的结构及主要的配置项进行了说明,对于常见的 API 网关,我们应该重点关注侦听器(Listeners) 和 集群(Clusters)。最终,我们结合Docker-Compose对服务进行了编排,并由此构建出了一个基本的反向代理的方案。本文的源代码已托管至 Github ,大家可以在此基础上做进一步的探索。好了,以上就是这篇博客的的全部内容啦,欢迎大家就本文中提出的方案、代码等进行讨论,如果大家有任何意见或者建议,欢迎在评论区进行留言,谢谢大家!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK