跳到主要内容

在Proxyless Mesh上的探索

· 阅读需 24 分钟

经过过去几年的发展,Service Mesh已再是一个新兴的概念,其从一经推出就受到来自全世界的主流技术公司关注和追捧。Proxyless Mesh全称是Proxyless Service Mesh,其是近几年在Service Mesh基础上发展而来的一种新型软件架构。Service Mesh理想很丰满,但现实很骨感!通过一层代理虽然做到了对应用无侵入,但增加的网络代理开销对很多性能要求很高的互联网业务落地存在不少挑战。因此Proxyless Mesh作为一种在传统侵入式微服务框架与Service Mesh之间的折中方案,通过取众家之所长,为大量的非Service Mesh应用在云原生时代,拥抱云原生基础设施,解决流量治理等痛点提供了一种有效的解决方案。本文将介绍Spring Cloud Alibaba在Proxyless Mesh上的探索。

Service Mesh

站在2023年的今天,Service Mesh 早已不是一个新兴的概念, 回顾过去6年多的发展历程,Service Mesh 从一经推出就受到来自全世界的主流技术公司关注和追捧。

  • 2016 年作为 Service Mesh 的元年,Buoyant 公司 CEO William Morgan 率先发布 Linkerd[1] ,成为业界首个 Service Mesh 项目,同年 Lyft 发布 Envoy[2] ,成为第二个 Service Mesh 项目。
  • 2017年,Google、IBM、Lyft 联手发布了 Istio[3],它与 Linkerd / Envoy 等项目相比,它首次给大家增加了控制平面的概念,提供了强大的流量控制能力。经过多年的发展 Istio,已经逐步成为服务网格领域控制平面的事实标准。
  • 2018年7月,Istio 1.0版本发布[4],标志着其进入了可以生产可用的时代,逐渐也有越来越多的企业开始考虑和尝试将服务网格应用于生产中。

Istio 作为当前最流行的开源服务网格技术,它由控制平面和数据平面两部分构成。 image.png 在 Istio Mesh 架构中,其控制平面是一个名为 Istiod 的进程,网络代理是 Envoy 。Istiod 作为控制面的统一组件,负责对接服务注册发现、路由规则管理、证书管理等能力,Envoy 则是作为数据面通过 Sidecar 方式代理业务流量,Istio 和 Envoy 之间通过 xDS 协议接口完成服务发现、路由规则等数据的传递。Istiod 通过监听 K8s 资源例如 Service、Endpoint 等,获取服务信息,并将这些资源统一通过 xDS 协议下发给位于数据平面的网络代理。Envoy 则是独立于应用之外的一个进程,以 Sidecar 的方式(一般是以 Container 方式)伴随业务应用 Pod 运行,它与应用进程共用同一个主机网络,通过修改路由表的方式劫持业务应用的网络流量从而达到为应用无侵入地提供如服务鉴权、标签路由等能力。

Proxyless Mesh

Proxyless Mesh 全称是 Proxyless Service Mesh,其是近几年在 Service Mesh 基础上发展而来的一种新型软件架构。Service Mesh 理想很丰满,但现实很骨感!通过一层代理虽然做到了对应用无侵入,但增加的网络代理开销对很多性能要求很高的互联网业务落地存在不少挑战。因此Proxyless Mesh作为一种在传统侵入式微服务框架与Service Mesh之间的折中方案,通过取众家之所长,为大量的非Service Mesh应用在云原生时代,拥抱云原生基础设施,解决流量治理等痛点提供了一种有效的解决方案。 Service Mesh 和 Proxyless Mesh 架构区别如下图所示: image.png 过去几年,国内外的知名软件开源社区也都在相关领域进行了大量探索,例如在2021年10月,gRPC社区为用户提供如下架构形式[5],通过对接Istio控制平面,遵循 VirtualService & DestinationRule CRD规范为gRPC应用提供流量治理能力。 image.png

Spring Cloud Alibaba Mesh 化方案

Spring Cloud Alibaba作为一种侵入式的微服务解决方案,通过基于Spring Cloud微服务标准为用户提供了微服务应用构建过程中的如服务注册与发现、限流降级、分布式事务与分布式消息等在内的一站式微服务解决方案。过去几年被国内大量中小企业所采用,帮助大量企业更加方便地拥抱微服务。 但随着企业应用微服化的不断深入,微服务给应用带来系统解耦、高可扩展性等诸多优势的同时,也让应用变得更加复杂。如何管理好微服务?成为了很多企业逐渐开始关注和重视的一个新的问题。Spring Cloud Alibaba社区也注意到很多用户有微服务治理方面的诉求,于是从2022年初,就开始了在该方面的探索,社区觉得相比于Service Mesh,Proxyless Mesh是一种对广大中小企业更合适的技术方案,其不仅不会有额外Sidecar代理所带来的较大性能损耗,而且更重要的是对企业来说,其落地成本很低! 要通过Mesh化方案解决微服务治理需求,一个能给应用动态下发规则的控制面不可或缺,社区本着不重复造轮子,拥抱业界主流解决方案的原则,通过支持xDS协议不仅为用户提供通过主流的Istio控制面来对Spring Cloud Alibaba应用进行服务治理以外,用户也可以使用阿里巴巴开源的OpenSergo微服务治理控制面所提供的差异化治理能力进行应用治理。相关提供Mesh技术方案社区在最近发布的 2.2.10-RC版本[6]中进行了提供。做了提供微服治理能力的第一个版本,社区当前已经部分兼容了Istio VirtualService & DestinationRule的标签路由和服务鉴权能力,用户可以通过Istio控制面给应用下发相关规则,对应用进行流量治理。 image.png

准备工作

Proxyless Mesh的方案首先需要准备好一个能给应用动态下发规则的控制面,本次Spring Cloud Alibaba 2.2.10-RC1 版本支持了2种当前市面上的主流控制面来更好的满足各类用户诉求:

1. Istio控制面

为了使用Istio控制面下发治理规则,首先需要在K8s环境中安装Istio控制面,您可以使用Spring Cloud Alibaba社区提供的测试用的Istio环境,也可以选择自己尝试安装一套Istio控制面,安装Istio控制面的流程如下:

  1. 安装K8s环境,请参考K8s的安装工具小节
  2. 在K8s上安装并启用Istio,请参考Istio官方文档的安装小节

2. OpenSergo控制面

OpenSergo 是开放通用的,覆盖微服务及上下游关联组件的微服务治理项目。OpenSergo 从微服务的角度出发,涵盖流量治理、服务容错、服务元信息治理、安全治理等关键治理领域,提供一系列的治理能力与标准、生态适配与最佳实践,支持 Java, Go, Rust 等多语言生态。 OpenSergo 控制平面 (Control Plane) 作为 OpenSergo CRD 的统一管控组件,承载服务治理配置转换与下发的职责。

  1. 安装K8s环境,请参考K8s的安装工具小节
  2. 在K8s上安装并启用 OpenSergo Control Plane,请参考 OpenSergo 官方提供的 OpenSergo 控制面安装文档

标签路由

应用背景

在现在的微服务架构中,服务的数量十分庞大,为了更好的管理这些微服务应用,可能需要给这些应用打上标签,并且将一个或多个服务的提供者划分到同一个分组,从而约束流量只在指定分组中流转,实现流量隔离的目的。标签路由可以作为蓝绿发布、灰度发布等场景的能力基础,它可以被应用在以下场景中:

  • 多版本开发测试

多个版本并行开发时,需要为每个版本准备一套开发环境。如果版本较多,开发环境成本会非常大。流量隔离方案可以在多版本开发测试时大幅度降低资源成本。使用基于标签路由的全链路流量隔离机制,可以将特定的流量路由到指定的开发环境。例如在开发环境1中只修改应用B和应用D,则为这两个应用在开发环境1中的版本创建Tag1标签,并配置对应的路由规则。入口应用A调用B时,会判断流量是否满足路由规则。如果满足,路由到开发环境1中应用B的V1.1版本;如果不满足,路由到基线环境中的应用B的V1版本。应用C调用D的时候同样根据流量决定路由到D的V1版本或V1.1版本。 image.png

  • 应用流量隔离

如果一个应用有多个版本在线上同时运行,部署在不同环境中,如日常环境和特殊环境,则可以使用标签路由对不同环境中的不同版本进行流量隔离,将秒杀订单流量或不同渠道订单流量路由到特殊环境,将正常的流量路由到日常环境。即使特殊环境异常,本应进入特殊环境的流量也不会进入日常环境,不影响日常环境的使用。 image.png

  • A/B Testing

线上有多个应用版本同时运行,期望对不同版本的应用进行A/B Testing,则可以使用标签路由的全链路流量控制将地域A(如杭州)的客户流量路由到V1版本,地域B(如上海)的客户流量路由到V1.1版本,对不同版本进行验证,从而降低新产品或新特性的发布风险,为产品创新提供保障。 image.png 目前,Spring Cloud Alibaba Mesh提供的标签路由能力支持根据请求路径、请求头和HTTP请求参数等请求元信息对请求做标签路由,让应用发出的请求根据Istio控制面下发的规则发送至指定版本的上游服务。

使用方式

1. 导入依赖并配置应用

首先,修改pom.xml 文件,导入Spring Cloud Alibaba 2.2.10-RC1版本下的标签路由以及Istio资源转换模块的相关依赖(推荐通过云原生应用脚手架 start.aliyun.com 进行项目构建试用):

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.10-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-routing</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
</dependencies>

application.yml配置文件给消费者配置Istio控制面以及Nacos注册中心的相关信息:

server:
port: 18084
spring:
main:
allow-bean-definition-overriding: true
application:
name: service-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
fail-fast: true
username: nacos
password: nacos
istio:
config:
# 是否开启Istio配置转换
enabled: ${ISTIO_CONFIG_ENABLE:true}
# Istiod IP
host: ${ISTIOD_ADDR:127.0.0.1}
# Istiod 端口
port: ${ISTIOD_PORT:15010}
# 轮询Istio线程池大小
polling-pool-size: ${POLLING_POOL_SIZE:10}
# 轮询Istio时间间隔
polling-time: ${POLLING_TIME:10}
# Istiod鉴权token(访问Istiod 15012端口时可用)
istiod-token: ${ISTIOD_TOKEN:}
# 是否打印xds相关日志
log-xds: ${LOG_XDS:true}

application.yml配置文件给生产者应用配置元信息:

# 第一个生产者,版本为v1
spring.cloud.nacos.discovery.metadata.version=v1
# 第二个生产者,版本为v2
spring.cloud.nacos.discovery.metadata.version=v2

如果是需要对接 OpenSergo 控制面的,则需要给消费者应用加上 spring-cloud-starter-alibaba-governance-routingspring-cloud-starter-opensergo-adapter相关依赖,并配置OpenSergo所需的配置即可。

2. 运行应用程序

启动两个生产者应用和一个消费者应用,并将这些应用都注册到本地的Nacos注册中心里,消费者在调用生产者时,会根据控制面下发的标签路由规则来调用不同的生产者实例。启动消费者和两个生产者后,可以在Nacos注册中心里看到这几个已注册的服务: image.png 控制台上会打印出以下信息,说明此应用正在监听Istio控制面下发的配置: image.png

3. 通过Istio控制面下发标签路由规则

通过Istio控制面下发标签路由规则,首先下发DestinationRule规则:

kubectl apply -f - << EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-destination-rule
spec:
host: sca-virtual-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
EOF

此规则将后端服务拆分为两个版本,label为v1的pod被分到v1版本,label为v2的pod被分到v2版本 之后,下发VirtualService规则:

kubectl apply -f - << EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: sca-virtual-service
spec:
hosts:
- service-provider
http:
- match:
- headers:
tag:
exact: gray
uri:
exact: /istio-label-routing

route:
- destination:
host: service-provider
subset: v2
- route:
- destination:
host: service-provider
subset: v1
EOF

这条VirtualService指定了一条最简单的标签路由规则,将请求头tag为gray,请求路径为/istio-label-routing的HTTP请求路由到v2版本,其余的流量都路由到v1版本 发送若干条不带请求头的HTTP请求至IstioConsumerApplication

while true;
do curl localhost:18084/istio-label-routing;
sleep 0.1;
echo "";
done;

因为请求头不为gray,所以请求将会被路由到v1版本,返回如下 image.png 之后发送一条请求头tag为gray,且请求路径为/istio-label-routing的HTTP请求

while true;
do curl localhost:18084/istio-label-routing -H "tag: gray";
sleep 0.1;
echo "";
done;

因为满足路由规则,所以请求会被路由至v2版本 image.png

4. 通过 OpenSergo 控制面下发标签路由规则

通过OpenSergo控制面也定义了特定的流量路由规则TrafficRouter,如下是一个OpenSergo控制面对应的流量路由规则:

kubectl apply -f - << EOF
apiVersion: traffic.opensergo.io/v1alpha1
kind: TrafficRouter
metadata:
name: service-provider
namespace: default
labels:
app: service-provider
spec:
hosts:
- service-provider
http:
- match:
- headers:
tag:
exact: v2
route:
- destination:
host: service-provider
subset: v2
fallback:
host: service-provider
subset: v1
- route:
- destination:
host: service-provider
subset: v1
EOF

这条TrafficRouter指定了一条最简单的流量路由规则,将请求头tag为v2的HTTP请求路由到v2版本,其余的流量都路由到v1版本。如果 v2 版本没有对应的节点,则将流量fallback至v1版本。 停止v2版本的ProviderApplication后,继续发送一条请求头 tag 为 v2 的HTTP请求

curl --location --request GET '127.0.0.1:18083/router-test' --header 'tag: v2'

因为v2版本没有服务提供者,因此流量被fallback至 v1 版本。

Route in 30.221.132.228: 18081,version is v1.

上述详细示例代码可以在社区Github上示例代码中获取。

服务鉴权

正常生产场景,微服务应用都具有安全要求,不会让任意的服务都可直接调用。因此需要对调用该应用的上游应用进行服务鉴权,保证应用自身的安全。 未配置服务鉴权Consumer 1、2、3和Provider在同一个命名空间内,Consumer 1、2、3默认可以调用Provider的所有Path(Path 1、2和3)。 image.png 配置服务鉴权规则后,应用间合法的调用关系如下图所示: image.png 设置所有Path的鉴权可以对Provider的所有Path设置鉴权规则,例如Provider所有Path的鉴权规则设置为拒绝Consumer 1调用(黑名单),则允许Consumer 2、3调用(白名单)。 设置指定Path的鉴权在设置所有Path的鉴权基础上,还可以设置Consumer指定Path的鉴权规则,例如按所有Path的鉴权方式,Consumer 2、3可以访问Provider的所有Path,但Provider的Path2涉及一些核心业务或数据,不希望Consumer 2调用,可以将Path 2对Consumer 2的鉴权方式设置为黑名单(拒绝调用),则Consumer 2只能访问Provider的Path 1和Path 3。 目前,Spring Cloud Alibaba Mesh支持了Istio的大部分鉴权规则,支持了除了需要mTLS支持以外的鉴权规则,支持了Istio的所有字符串匹配模式以及规则的逻辑运算。 image.png

使用方式

1. 导入依赖并配置应用

修改pom.xml文件,引入Istio资源转换以及Spring Cloud Alibaba鉴权模块(推荐通过云原生应用脚手架 start.aliyun.com 进行项目构建试用):

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.10-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
</dependencies>

在应用的 application.yml 配置文件中配置Istio相关元数据:

server:
port: ${SERVER_PORT:80}
spring:
cloud:
governance:
auth:
# 是否开启鉴权
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
# 是否开启Istio配置转换
enabled: ${ISTIO_CONFIG_ENABLE:true}
# Istiod IP
host: ${ISTIOD_ADDR:127.0.0.1}
# Istiod 端口
port: ${ISTIOD_PORT:15010}
# 轮询Istio线程池大小
polling-pool-size: ${POLLING_POOL_SIZE:10}
# 轮询Istio时间间隔
polling-time: ${POLLING_TIMEOUT:10}
# Istiod鉴权token(访问Istiod 15012端口时可用)
istiod-token: ${ISTIOD_TOKEN:}
# 是否打印xds相关日志
log-xds: ${LOG_XDS:true}
2. 运行应用程序

在导入好以上的依赖并且在application.yml文件中配置了相关配置之后,可以将此应用程序运行起来,启动一个简单的Spring Boot应用,其中只含有一个简单的接口,此接口将会把本次请求的详细信息返回给客户端。 启动应用后,控制台上会打印出以下信息,说明此应用正在监听Istio控制面下发的配置: image.png

3. 通过Istio控制面下发鉴权配置

在使用如下命令通过Istio下发一条鉴权规则至demo应用,这条规则的限制了访问该应用的请求header:

kubectl apply -f - << EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: http-headers-allow
namespace: ${namespace_name}
spec:
selector:
matchLabels:
app: ${app_name}
action: ALLOW
rules:
- when:
- key: request.headers[User-Agent]
values: ["PostmanRuntime/*"]
EOF

之后发送一个带User-Agent头部的HTTP请求来验证规则是否生效:

while true;
do curl localhost/auth -H "User-Agent: PostmanRuntime/7.29.2";
sleep 0.1;
echo "";
done;

由于此请求由于携带了正确的HTTP Header信息,将会返回: image.png 之后发送一个不带User-Agent头部的HTTP请求来验证规则是否生效:

while true;
do curl localhost/auth;
sleep 0.1;
echo "";
done;

由于此请求没有携带正确的HTTP Header信息,将会返回: image.png 上述详细示例代码可以在社区Github上示例代码中获取。