背景

需求场景是,将 dubbo2 框架的应用迁移到 istio 网格中,dubbo2 使用的注册中心可能有 zookeeper、nacos、etcd,这里对 dubbo 接入网格的几种方式做个对比。

dubbo 接入 mesh 方案

dubbo 接入 mesh,这里要区分 dubbo 和 dubbo3,下面是目前的方案汇总:

  • dubbo2:开源社区方案,如 Aeraki。
  • dubbo3:需开启新特性(新的地址发现模型、基于 HTTP/2 的 Triple 协议、统一的路由规则),可参考 升级到 Dubbo3 来迁移 dubbo3,支持两种部署方式
    • Sidecar 模式:只能用 Triple 协议,Dubbo ThinSDK 将只提供面向业务应用的编程 API、RPC 传输通信能力,其余服务治理 包括地址发现、负载均衡、路由寻址等都统一下沉到 Sidecar。
    • Proxyless 模式:同时支持 Dubbo2、Triple 协议,但只支持应用级服务发现的地址模型,Dubbo3 SDK 直接实现 xDS 协议解析,Dubbo 进程可以与控制面(Control Plane)直接通信,进而实现控制面对流量管控、服务治理、可观测性、安全等的统一管控

这几种方案的对比如下:

方案 dubbo 版本 支持协议 支持注册中心 调用方式 优点 缺点|
社区项目 aeraki dubbo2 dubbo、 thrift 和其他私有协议 zookeeper、nacos、etcd 等 interface 级 兼容 dubbo3、支持协议较多 部分功能可能受限,同时会有一定的性能和容量瓶颈
dubbo3 Sidecar dubbo3 triple 只支持 k8s 应用级,providedBy 参数指定 平滑升级、多语言、业务侵入小 不支持自定义注册中心、sidecar 的性能损耗、部署环境受限
dubbo3 proxyless dubbo3 triple、dubbo2 只支持 k8s 应用级,providedBy 参数指定 没有 proxy 中转的损耗、架构简单、便于遗留系统的平滑迁移 不支持自定义注册中心

文章最后针对一个特殊场景,只需要支持 dubbo2 协议和第三方注册中心,并且使用 dubbo 自身的服务治理能力的场景,提出一个简化方案,不引入 Aeraki 组件和 MetaProtocol Proxy 代理,只使用 dubbo2istio,手动生成 EnvoyFilter 来支持 dubbo 协议。

istio 支持第三方注册中心原理

istio 1.8 不再通过 in-tree 方式支持外部注册中心,istio 1.9 改为 MCP-over-XDS 的方式支持外部注册中心。其支持的流程为:

  • 为第三方注册中心编写 MCP-over-XDS server。
  • MCP-over-XDS server 获取第三方注册中心的数据,并转换为 ServiceEntry。
  • 推送给 pilot(如 nacos,pilot 启动时需设置 configSource 参数) 或者写到 ServiceEntry CR 中(如 dubbo2istio)。
  • pilot 接受到数据后,会对比内存中的数据,保证数据一致。

通过这种方式,原有 SDK 注册和调用方式不需要做更改,能更加快速的迁移原有服务。因为 nacos 已经支持了 MCP-over-XDS,这里看下 nacos 支持时,是怎么配置的。nacos 配置:

1
nacos.istio.mcp.server.enabled=true # nacos 开启 istio 同步

istio 配置:

1
2
3
4
5
6
7
8
rootNamespace: istio-system
trustDomain: cluster.local
configSources: # 配置 xds 地址为 nacos
  - address: xds://xxx:18848
# 开启捕获 DNS 请求,解析自定义的 ServiceEntry,参考 [dns 代理](https://preliminary.istio.io/latest/zh/docs/ops/configuration/traffic-management/dns-proxy/)
proxyMetadata:  # 为代理提供的额外环境变量,会写到代理的启动配置中
  ISTIO_META_DNS_AUTO_ALLOCATE: \"true\" # 为每个 ServiceEntry 自动分配一个不同的独立地址
  ISTIO_META_DNS_CAPTURE: \"true\"  # 开启 dns 代理,应用程序的 DNS 请求将会被重定向到 Sidecar

dubbo-demo 代码解读

下面示例的代码来自 aeraki-dubbo-demo,因为之前不了解 java 和 dubbo,这里整理下其中的一些知识。

Maven 和 POM 说明

Maven 是专门为 Java 项目打造的管理和构建工具,它的主要功能有:

  • 提供了一套标准化的项目结构
  • 提供了一套标准化的构建流程(编译,测试,打包,发布……)
  • 提供了一套依赖管理机制

项目目录结构中 pom.xml 是 Maven 的配置文件,POM 是项目对象模型(Project Object Model),用于管理项目的依赖关系、构建设置、插件配置等。执行任务或目标时,Maven 会在当前目录中查找 POM。它读取 POM,获取所需的配置信息,然后执行目标。POM 中可以指定以下配置:

  • 项目依赖
  • 插件
  • 执行目标
  • 项目构建 profile
  • 项目版本
  • 项目开发者列表
  • 相关邮件列表信息

以下是一个简单的 pom.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 项目分组,公司或者组织的唯一标志 -->
    <groupId>org.apache.dubbo</groupId>
    <!-- 版本号 -->
    <version>1.0-SNAPSHOT</version>
    <!-- 模型版本 -->
    <modelVersion>4.0.0</modelVersion>
    <!-- 项目的唯一 ID -->
    <artifactId>dubbo-samples-basic</artifactId>

    <!-- 属性,其他地方使用${属性名}的方式引用该属性 -->
    <properties>
        <source.level>1.8</source.level>
        <target.level>1.8</target.level>
        <main-class>org.apache.dubbo.samples.basic.BasicProvider</main-class>
    </properties>

    <!-- 依赖 -->
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>
</project>

Maven 主要会使用到以下命令:

  • mvn package:用于创建项目的可执行 JAR 或 WAR 包,通常情况下,生成的文件名遵循格式“-.”,比如上面例子中生成的名称是 dubbo-samples-basic-1.0-SNAPSHOT.jar
  • mvn install: 把打好的包放入本地仓库 (~/.m2/repository)
  • mvn clean: 清除各个模块 target 目录及里面的内容
  • mvn deploy: 部署,把包发布到远程仓库

其中 mvn package 打包后生成的 jar 包,可以用“jar -xf xx.jar”解压,我们将上面生成的 jar 解压,目录如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
.
├── LICENSE.txt
├── META-INF
├── MapValue.proto
├── ThrowablePB.proto
├── about.html
├── about_files
│   └── LICENSE_CDDL.txt
├── afu
│   ├── org
│   └── plume
├── android
│   └── annotation
├── auth.proto
├── com
│   ├── alibaba
│   ├── embedded
│   ├── fasterxml
│   ├── google
│   └── sun
├── election.proto
├── google
│   ├── api
│   ├── cloud
│   ├── geo
│   ├── logging
│   ├── longrunning
│   ├── protobuf
│   ├── rpc
│   └── type
├── grpc
│   └── lb
├── io
│   ├── etcd
│   ├── grpc
│   ├── netty
│   ├── perfmark
│   └── prometheus
├── javassist
├── javax
├── jetty-dir.css
├── jline
├── kv.proto
├── lock.proto
├── log4j.properties
├── nacos-log4j2.xml
├── nacos-logback.xml
├── nacos-version.txt
├── net
│   ├── jcip
│   └── jodah
├── org
│   ├── aopalliance
│   ├── apache  # class 位置
│   ├── checkerframework
│   ├── codehaus
│   ├── eclipse
│   ├── jboss
│   ├── reflections
│   ├── scannotation
│   ├── slf4j
│   ├── springframework
│   └── yaml
├── plugin.properties
├── rpc.proto
├── security
│   └── serialize.blockedlist
└── spring  # 应用的 xml 配置
    ├── dubbo-demo-consumer.xml
    ├── dubbo-demo-provider.xml
    └── dubbo-second-provider.xml

在运行的时候,直接执行以下类似的命令即可:

1
java -cp ./dubbo-samples-basic-1.0-SNAPSHOT.jar org.apache.dubbo.samples.basic.BasicProvider

代码解读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class BasicProvider {

    public static void main(String[] args) throws Exception {
        //new EmbeddedZooKeeper(2181, false).start();
        // wait for embedded zookeeper start completely.
        //Thread.sleep(1000);
        // 使用到 xml 文件来描述 Spring 中 Bean,在后面可获取到相应的 Bean
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-demo-provider.xml");
        context.start();

        System.out.println("dubbo service started");
        new CountDownLatch(1).await();
    }
}

查看其 bean 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 激活${...}占位符的替换,根据指定的属性文件解析 -->
    <context:property-placeholder/>

    <!-- 当前应用名 -->
    <dubbo:application name="dubbo-sample-provider">
      <dubbo:parameter key="aeraki_meta_app_namespace" value="${AERAKI_META_APP_NAMESPACE}" />
      <dubbo:parameter key="aeraki_meta_app_service_account" value="${AERAKI_META_APP_SERVICE_ACCOUNT}" />
      <dubbo:parameter key="aeraki_meta_app_version" value="${AERAKI_META_APP_VERSION}" />
      <dubbo:parameter key="aeraki_meta_workload_selector" value="${AERAKI_META_WORKLOAD_SELECTOR}" />
      <dubbo:parameter key="aeraki_meta_locality" value="${AERAKI_META_LOCALITY}" />
      <dubbo:parameter key="service_group" value="${SERVICE_GROUP}" />
    </dubbo:application>

    <!-- 注册中心配置,address:地址 -->
    <!-- register:是否向此注册中心注册服务,如果设为 false,将只订阅,不注册 -->
    <!-- subscribe:是否向此注册中心订阅服务,如果设为 false,将只注册,不订阅 -->
    <dubbo:registry address="${REGISTRY_ADDR}" register="${REGISTER}" subscribe="false" timeout="60000">
        <!--
        <dubbo:parameter key="group" value="test" />
        <dubbo:parameter key="namespace" value="test" />
        -->
    </dubbo:registry>

    <!-- bean id 和类的全限定名 -->
    <bean id="demoProvider" class="org.apache.dubbo.samples.basic.impl.DemoProviderImpl"/>

    <!-- 服务提供者暴露服务配置,interface:服务接口名,ref:服务对象实现引用 -->
    <dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoProvider"/>
    <dubbo:service interface="org.apache.dubbo.samples.basic.api.TestService" ref="demoProvider" />
    <!-- group:接口有多个实现用分组区分,version:服务版本 -->
    <dubbo:service interface="org.apache.dubbo.samples.basic.api.ComplexService" ref="demoProvider" group="test" version="1.0.0"/>
    <dubbo:service interface="org.apache.dubbo.samples.basic.api.ComplexService" ref="demoProvider" group="product" version="2.0.0"/>

    <!-- 服务消费者引用服务配置,id:服务引用 BeanId -->
    <dubbo:reference id="secondService" check="true" interface="org.apache.dubbo.samples.basic.api.SecondService" url="dubbo://org.apache.dubbo.samples.basic.api.secondservice:20880" timeout="3000"/>
</beans>

dubbo2 接入 Aeraki Mesh

这里参考 Aeraki 官网实例,并将其安装步骤做了简化,下面说明了 dubbo 在 Aeraki Mesh 中,如何接入不同的注册中心 zookeeper、nacos、etcd。

安装 istio 和 Aeraki

istio 安装配置文件 istio_config.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  profile: default
  values:
    global:
      logging:
        level: default:debug
  meshConfig:
    enableTracing: true # 是否开启 trace,需要设置 trace 收集配置
    accessLogFile: /dev/stdout
    accessLogFormat: "[%START_TIME%] %REQ(X-META-PROTOCOL-APPLICATION-PROTOCOL)%
     %RESPONSE_CODE% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% \"%UPSTREAM_TRANSPORT_FAILURE_REASON%\"
     %BYTES_RECEIVED% %BYTES_SENT% %DURATION% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(X-REQUEST-ID)%\" %UPSTREAM_CLUSTER%
     %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %ROUTE_NAME%\n"
    defaultConfig:
      holdApplicationUntilProxyStarts: true # 添加一个 hook 来延迟应用启动,等 pod 代理准备好接收流量后
      proxyMetadata:
        ISTIO_META_DNS_CAPTURE: "true" # 开启 dns 代理
      proxyStatsMatcher:    # 代理统计匹配器
        inclusionPrefixes:
          - thrift
          - dubbo
          - kafka
          - meta_protocol
        inclusionRegexps:
          - .*dubbo.*
          - .*thrift.*
          - .*kafka.*
          - .*zookeeper.*
          - .*meta_protocol.*
      tracing: # tracing 配置
        sampling: 100
        zipkin:
          address: zipkin.istio-system:9411
  components:   # 参考 [IstioComponentSetSpec](https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/#IstioComponentSetSpec)
    pilot:
      hub: istio    # 镜像
      tag: 1.14.5   # tag
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cd aeraki
vim istio_config.yaml # 编辑 istio 配置
istioctl install -y -f istio_config.yaml # 安装 istio
make install    # 安装 aeraki

# 下载代码和创建命名空间
git clone https://github.com/aeraki-mesh/dubbo2istio.git
cd dubbo2istio
kubectl create ns meta-dubbo
kubectl label namespace meta-dubbo istio-injection=enabled --overwrite=true

接入 zookeeper 实例

执行命令:

1
kubectl apply -f demo/k8s/zk -n meta-dubbo

等待服务启动后查看部署情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@dev:~/zt/istio/dubbo2istio# kubens meta-dubbo
✔ Active namespace is "meta-dubbo"
root@dev:~/zt/istio/dubbo2istio# kubectl get pods
NAME                                            READY   STATUS    RESTARTS   AGE
dubbo2istio-5c49bc5f8d-4wns5                    1/1     Running   0          79m
dubbo-sample-consumer-6749fc7df9-lc7hj          2/2     Running   0          79m
dubbo-sample-provider-v2-566c4fd8bb-9w6mg       2/2     Running   0          79m
dubbo-sample-provider-v1-86d5d5cc7c-vvg5d       2/2     Running   0          79m
zookeeper-cc7f59dd6-tlnpg                       1/1     Running   0          79m
dubbo-sample-second-provider-7958bfbbdc-qnz7b   2/2     Running   0          79m

查看 consumer 日志:

1
2
3
4
5
6
7
Periodically call dubbo server
Start a http server for e2e test
...
Hello Aeraki, response from dubbo-sample-provider-v1-86d5d5cc7c-vvg5d/10.42.0.27
Hello Aeraki, response from dubbo-sample-provider-v2-566c4fd8bb-9w6mg/10.42.0.30
Hello Aeraki, response from dubbo-sample-provider-v1-86d5d5cc7c-vvg5d/10.42.0.27
Hello Aeraki, response from dubbo-sample-provider-v2-566c4fd8bb-9w6mg/10.42.0.30

接入 nacos 实例

执行命令:

1
2
3
# 先卸载上面的 zookeeper 部署
kubectl delete -f demo/k8s/zk -n meta-dubbo
kubectl apply -f demo/k8s/nacos -n meta-dubbo

同样查看 consumer 日志:

1
2
3
4
5
6
7
Periodically call dubbo server
Start a http server for e2e test
...
Hello Aeraki, response from dubbo-sample-provider-v1-76c9c7f88c-6mbbk/10.42.0.40
Hello Aeraki, response from dubbo-sample-provider-v2-5bb778d49b-ln8d4/10.42.0.39
Hello Aeraki, response from dubbo-sample-provider-v1-76c9c7f88c-6mbbk/10.42.0.40
Hello Aeraki, response from dubbo-sample-provider-v2-5bb778d49b-ln8d4/10.42.0.39

接入 etcd 实例

执行命令:

1
2
3
# 先卸载上面的 nacos 部署
kubectl delete -f demo/k8s/nacos -n meta-dubbo
kubectl apply -f demo/k8s/etcd -n meta-dubbo

同样查看 consumer 日志:

1
2
3
4
5
6
7
Periodically call dubbo server
Start a http server for e2e test
...
Hello Aeraki, response from dubbo-sample-provider-v1-5d9496c8bc-zdhdk/10.42.0.51
Hello Aeraki, response from dubbo-sample-provider-v2-59c8778bb-8hbmk/10.42.0.54
Hello Aeraki, response from dubbo-sample-provider-v1-5d9496c8bc-zdhdk/10.42.0.51
Hello Aeraki, response from dubbo-sample-provider-v2-59c8778bb-8hbmk/10.42.0.54

Aeraki mesh 逻辑分析

以使用 zookeeper 注册中心为例,这里先查看 dubbo2istio 的日志:

1
2
3
2023-02-27T11:57:25.254890Z  info  dubbo2istio runs in Zookeeper mode: registry: default, addr: zookeeper:2181
2023-02-27T11:57:25.257483Z  info  connected to 10.43.194.10:2181
2023-02-27T11:57:33.374613Z  info  service entry aeraki-org-apache-dubbo-samples-basic-api-demoservice has been updated: {"metadata":{"name":"aeraki-org-apache-dubbo-samples-basic-api-demoservice","namespace":"meta-dubbo","resourceVersion":"421514","creationTimestamp":null,"labels":{"manager":"aeraki","registry":"dubbo2istio"},"annotations":{"interface":"org.apache.dubbo.samples.basic.api.DemoService","workloadSelector":"dubbo-sample-provider"}},"spec":{"hosts":["org.apache.dubbo.samples.basic.api.demoservice"],"ports":[{"number":20880,"protocol":"tcp","name":"tcp-metaprotocol-dubbo","targetPort":20880}],"location":"MESH_INTERNAL","resolution":"STATIC","endpoints":[{"address":"10.42.0.64","ports":{"tcp-metaprotocol-dubbo":20880},"labels":{"anyhost":"true","application":"dubbo-sample-provider","default":"true","deprecated":"false","dubbo":"2.0.2","dynamic":"true","generic":"false","interface":"org.apache.dubbo.samples.basic.api.DemoService","metadata-type":"remote","methods":"testVoid-sayHello","pid":"8","registryName":"default","release":"1.0-SNAPSHOT","revision":"1.0-SNAPSHOT","service_group":"batchjob","side":"provider","timestamp":"1677499051587","version":"v2"},"locality":"bj/800005","serviceAccount":"default"},{"address":"10.42.0.63","ports":{"tcp-metaprotocol-dubbo":20880},"labels":{"anyhost":"true","application":"dubbo-sample-provider","default":"true","deprecated":"false","dubbo":"2.0.2","dynamic":"true","generic":"false","interface":"org.apache.dubbo.samples.basic.api.DemoService","metadata-type":"remote","methods":"testVoid-sayHello","pid":"7","registryName":"default","release":"1.0-SNAPSHOT","revision":"1.0-SNAPSHOT","service_group":"user","side":"provider","timestamp":"1677499051569","version":"v1"},"locality":"bj/800002","serviceAccount":"default"}]},"status":{}}

针对 demoservice 生成了一个名为 aeraki-org-apache-dubbo-samples-basic-api-demoservice 的 ServiceEntry,用命令获取其描述如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  annotations:
    interface: org.apache.dubbo.samples.basic.api.DemoService
    workloadSelector: dubbo-sample-provider
  labels:
    manager: aeraki
    registry: dubbo2istio
  name: aeraki-org-apache-dubbo-samples-basic-api-demoservice
  namespace: meta-dubbo
spec:
 # 与该服务相关的虚拟 IP 地址,可以是 CIDR 前缀。对于 HTTP 流量,生成的路由配置将包括地址和主机字段值的 http 路由域,目的地将根据 HTTP 主机/授权头来识别。如果指定了一个或多个 IP 地址,如果目的地 IP 与地址字段中指定的 IP/CIDRs 相匹配,传入的流量将被识别为属于该服务。如果地址字段为空,流量将仅根据目标端口进行识别。
  addresses:
  - 240.240.0.46
  hosts:    # 此服务关联的 hosts,可以是带通配符的 DNS 名称
  - org.apache.dubbo.samples.basic.api.demoservice
  location: MESH_INTERNAL   # 指定服务是在网格外部还是网格内部
  ports:
  - name: tcp-metaprotocol-dubbo # Aeraki 会识别后面的应用协议并进行相应的七层处理
    number: 20880
    protocol: tcp
    targetPort: 20880
  resolution: STATIC
  endpoints:
  - address: 10.42.0.64 # dubbo-sample-provider-v2 的 pod ip
    labels:   # endpoints 相关的标签
      anyhost: "true"
      application: dubbo-sample-provider
      default: "true"
      deprecated: "false"
      dubbo: 2.0.2
      dynamic: "true"
      generic: "false"
      interface: org.apache.dubbo.samples.basic.api.DemoService
      metadata-type: remote
      methods: testVoid-sayHello
      pid: "8"
      registryName: default
      release: 1.0-SNAPSHOT
      revision: 1.0-SNAPSHOT
      service_group: batchjob
      side: provider
      timestamp: "1677499051587"
      version: v2
    locality: bj/800005
    ports:
      tcp-metaprotocol-dubbo: 20880
    serviceAccount: default
  - address: 10.42.0.63     # # dubbo-sample-provider-v1 的 pod ip
    # ...
    serviceAccount: default

ServiceEntry 是可以添加额外的服务项到 istio 内部的服务注册中心,这样网格可以将请求转发到这些指定的服务,这些服务的 endpoints 可以是 VM 负载或者 kubernetes 的 pods。 其中某些 host 是一个必选字段,表示与 ServiceEntry 相关的主机名,可以是一个 DNS 域名,还可以使用前缀模糊匹配。在使用上有以下几个说明:

  • HTTP 的流量,在这个字段匹配 HTTP Header 的 Host 或 Authority。
  • HTTPS 或 TLS 流量,这个字段匹配 SNI。
  • 其他协议的流量,这个字段不生效,使用下面的 addresses 和 port 字段。
  • 当 resolution 被设置为 DNS 类型并且没有指定 endpoints 时,这个字段将作为后端的域名来进行路由。 这时在 istiod 中 pods 中通过curl http://127.0.0.1:15014/debug/registryz查看此注册项,如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "Attributes": {
    "ServiceRegistry": "External",
    "Name": "org.apache.dubbo.samples.basic.api.demoservice",
    "Namespace": "meta-dubbo",
    "Labels": {
      "manager": "aeraki",
      "registry": "dubbo2istio"
    },
    "ExportTo": null,
    "LabelSelectors": null,
    "ClusterExternalAddresses": {
      "Addresses": null
    },
    "ClusterExternalPorts": null
  },
  "ports": [{
    "name": "tcp-metaprotocol-dubbo",
    "port": 20880,
    "protocol": "TCP"
  }],
  "creationTime": "2023-03-15T06:38:02Z",
  "hostname": "org.apache.dubbo.samples.basic.api.demoservice",
  "clusterVIPs": {
    "Addresses": null
  },
  "defaultAddress": "240.240.0.46",
  "Resolution": 0,
  "MeshExternal": false,
  "ResourceVersion": ""
}

继续看下 consumer 中的代理配置,可以看到配置的 cluster 名称为 ServiceEntry 中的 hosts,并配置了其 endpoints 为两个 provider pods 的 ip:

1
2
3
4
5
6
root@dev:~/zt/istio/dubbo2istio/demo/k8s/test-zk# istioctl proxy-config cluster dubbo-sample-consumer-67fc956cc8-c8ltf | grep demo
org.apache.dubbo.samples.basic.api.demoservice                      20880     -          outbound      EDS
root@dev:~/zt/istio/dubbo2istio/demo/k8s/test-zk# istioctl proxy-config endpoints dubbo-sample-consumer-67fc956cc8-c8ltf --cluster "outbound|20880||org.apache.dubbo.samples.basic.api.demoservice"
ENDPOINT              STATUS      OUTLIER CHECK     CLUSTER
10.42.0.159:20880     HEALTHY     OK                outbound|20880||org.apache.dubbo.samples.basic.api.demoservice
10.42.0.160:20880     HEALTHY     OK                outbound|20880||org.apache.dubbo.samples.basic.api.demoservice

上面的逻辑解决了如何找到 provider 地址的问题,但是因为 consumer 中使用的是 dubbo 协议,而 istio 中目前不支持 dubbo 协议,因此 envoy 无法进行出向流量的劫持。

在 Aeraki 中通过 meta-protocol-proxy 代理来解决的,这个代理目前支持 dubbo 和 thrift 协议。 Aeraki 服务会监测上面的 ServiceEntry,生成对应的 EnvoyFilter,将 tcp 网络过滤器替换为自定义的 meta_protocol_proxy 协议,由 istiod 下发到代理中。下面示例项目中生成的 EnvoyFilter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  labels:
    manager: Aeraki
  name: aeraki-outbound-org.apache.dubbo.samples.basic.api.demoservice-240.240.0.6-20880
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        filterChain:
          filter:
            name: envoy.filters.network.tcp_proxy # 上面设置的是 tcp 协议,因此会生成 tcp 的 filter
        name: 240.240.0.6_20880
    patch:
      operation: REPLACE  # 替换上面的网络过滤器
      value:
        name: envoy.filters.network.meta_protocol_proxy # 替换 Aeraki 的自定义协议 meta_protocol_proxy
        typed_config:
          '@type': type.googleapis.com/udpa.type.v1.TypedStruct
          type_url: type.googleapis.com/aeraki.meta_protocol_proxy.v1alpha.MetaProtocolProxy
          value:
            accessLog:
            - name: envoy.access_loggers.file
              typedConfig:
                '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                path: /dev/stdout
            applicationProtocol: dubbo  # 应用层协议为 dubbo
            codec:
              name: aeraki.meta_protocol.codec.dubbo  # 应用层编码和解码协议
            metaProtocolFilters:  # 一个单独的七层协议过滤器列表,默认为 router 即直接路由
            - name: aeraki.meta_protocol.filters.router
            rds:  # 设置路由由 rDS 动态获取
              configSource:
                apiConfigSource:
                  apiType: GRPC
                  grpcServices:
                  - envoyGrpc:
                      clusterName: aeraki-xds
                  transportApiVersion: V3
                resourceApiVersion: V3
              # routeConfigName 路由配置的名称
              routeConfigName: org.apache.dubbo.samples.basic.api.demoservice_20880
            statPrefix: outbound|20880||org.apache.dubbo.samples.basic.api.demoservice

在 EnvoyFilter 中设置了 routeConfigName,将会通过 xDS 下发给代理,代理中的路由配置如下,其中设置了路由到上面所示的 cluster 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "version_info": "1678172533",
  "route_config": {
  "@type": "type.googleapis.com/aeraki.meta_protocol_proxy.config.route.v1alpha.RouteConfiguration",
  "name": "org.apache.dubbo.samples.basic.api.demoservice_20880",
  "routes": [
    {
    "name": "default",
    "route": {
      "cluster": "outbound|20880||org.apache.dubbo.samples.basic.api.demoservice"
    }
  ]
  },
  "last_updated": "2023-03-07T07:02:13.705Z"
}

查看 envoy 中此集群的配置:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
    {
     "version_info": "2023-03-15T06:38:56Z/18",
     "cluster": {
      "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
      "name": "outbound|20880||org.apache.dubbo.samples.basic.api.demoservice",
      "type": "EDS", // 服务发现类型,用来解析 cluster,参考 [Cluster.DiscoveryType](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto.html#enum-config-cluster-v3-cluster-discoverytype)
      "eds_cluster_config": { // EDS 类型时的配置
       "eds_config": {
        "ads": {}, // 当前为空,在这里可以指定要使用的 ADS 服务
        "initial_fetch_timeout": "0s",
        "resource_api_version": "V3"
       },
       //  可选的集群替代名,用来提交给 EDS
       "service_name": "outbound|20880||org.apache.dubbo.samples.basic.api.demoservice"
      },
      "connect_timeout": "10s",
      "lb_policy": "LEAST_REQUEST",
      "circuit_breakers": {}, // 熔断设置
      "metadata": { // 用来提供关于 cluster 的额外信息,可以被用来做统计、日志和改变过滤器行为
       "filter_metadata": { // map<string, Struct>,key 是逆序的 DNS 过滤器名称,比如 com.acme.widget
        "istio": { // 这里是指:istio 相关的过滤器?
         "config": "/apis/networking.istio.io/v1alpha3/namespaces/meta-dubbo/destination-rule/dubbo-sample-provider",
         "default_original_port": 20880,
         "services": [
          {
           "name": "org.apache.dubbo.samples.basic.api.demoservice",
           "host": "org.apache.dubbo.samples.basic.api.demoservice",
           "namespace": "meta-dubbo"
          }
         ]
        }
       }
      },
      "common_lb_config": { // 所有负载均衡实现的通用配置
       "locality_weighted_lb_config": {} // 使用 [位置加权负载均衡](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/locality_weight#arch-overview-load-balancing-locality-weighted-lb)
      },
      // 有序的网络过滤器链,在 envoy 向此上游 cluster 请求时会应用
      "filters": [
       {
        "name": "istio.metadata_exchange",
        // 通过 PILOT_ENABLE_METADATA_EXCHANGE 开启的,pilot 会开启元数据交换过滤器,用来在遥测过滤器中使用
        "typed_config": {
         "@type": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange",
         "protocol": "istio-peer-exchange"
        }
       }
      ],
      "transport_socket_matches": [ //为不同的 endponints 配置不同的传输 sockets
       {
        "name": "tlsMode-istio",
        "match": { // 可选的端点元数据匹配准则,与元数据匹配的端点的连接将使用此传输套接字配置
         "tlsMode": "istio"
        },
        "transport_socket": {
         "name": "envoy.transport_sockets.tls", // 用于对不信任的下行和上行流量进行保护
         "typed_config": {
          "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
          "common_tls_context": {
           "tls_params": {
            "tls_minimum_protocol_version": "TLSv1_2",
            "tls_maximum_protocol_version": "TLSv1_3"
           },
           "alpn_protocols": [
            "istio-peer-exchange",
            "istio"
           ],
           "tls_certificate_sds_secret_configs": [
            // ...
           ],
           "combined_validation_context": {
            "default_validation_context": {
             "match_subject_alt_names": [
              {
               "exact": "spiffe://cluster.local/ns/meta-dubbo/sa/default"
              }
             ]
            },
            "validation_context_sds_secret_config": {
             "name": "ROOTCA",
             "sds_config": {//...}
            }
           }
          },
          // 服务器名称指示,创建 TLS 后端连接时使用,SNI 是 TLS 握手期间的一个扩展,它允许客户端在建立 TLS 连接时指定要访问的服务器名称。服务器可以使用这个信息来选择正确的证书以及配置 TLS 连接
          "sni": "outbound_.20880_._.org.apache.dubbo.samples.basic.api.demoservice"
         }
        }
       },
       {
        "name": "tlsMode-disabled",
        "match": {},
        "transport_socket": {
         "name": "envoy.transport_sockets.raw_buffer",
         "typed_config": {
          "@type": "type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer"
         }
        }
       }
      ]
     },
     "last_updated": "2023-03-15T06:38:56.637Z"
    },

使用 EnvoyFilter 接入 dubbo2

这里考虑一个场景,只需要支持 dubbo2 协议和第三方注册中心,并且使用 dubbo 自身的服务治理能力,那可以对上述逻辑进行简化,不引入 Aeraki 组件和 MetaProtocol Proxy 代理,只使用 dubbo2istio,手动生成 EnvoyFilter 来支持 dubbo 协议。

因为上面测试生成了 EnvoyFilter 和 ServiceEntry 等资源,这里先进行清理工作:

1
2
3
4
5
6
7
8
kubens meta-dubbo
cd dubbo2istio
kubectl delete -f demo/k8s/zk/dubbo-example.yaml
kubectl delete deploy -nistio-system aeraki
// 查看 aeraki 生成的 envoyfilter,然后删除
kubectl get envoyfilter -nistio-system | grep aeraki
// 查看 aeraki 生成的 serviceentry,然后删除
kubectl get serviceentry

这里将测试用例也进行了简化,provider.yaml 如下所示,去掉了 sidecar 相关的 annotations,因此默认会使用 envoy 代理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dubbo-sample-provider-v1
  labels:
    app: dubbo-sample-provider
spec:
  selector:
    matchLabels:
      app: dubbo-sample-provider
  replicas: 1
  template:
    metadata:
      labels:
        app: dubbo-sample-provider
        version: v1
    spec:
      containers:
        - name: dubbo-sample-provider
          image: aeraki/dubbo-sample-provider
          imagePullPolicy: Always
          env:
            - name: REGISTRY_ADDR
              value: zookeeper://zookeeper:2181
            - name: REGISTER
              value: "true"
            - name: AERAKI_META_APP_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - containerPort: 20880

部署 provider 后,查看 dubbo2istio 生成的 ServiceEntry 为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  annotations:
    interface: org.apache.dubbo.samples.basic.api.DemoService
    workloadSelector: ""
  labels:
    manager: aeraki
    registry: dubbo2istio
  name: aeraki-org-apache-dubbo-samples-basic-api-demoservice
  namespace: meta-dubbo
spec:
  endpoints:
  - address: 10.42.0.20
    labels:
      anyhost: "true"
      application: dubbo-sample-provider
      default: "true"
      deprecated: "false"
      dubbo: 2.0.2
      dynamic: "true"
      generic: "false"
      interface: org.apache.dubbo.samples.basic.api.DemoService
      metadata-type: remote
      methods: testVoid-sayHello
      pid: "7"
      registryName: default
      release: 1.0-SNAPSHOT
      revision: 1.0-SNAPSHOT
      service_group: ""
      side: provider
      timestamp: "1679308798700"
    ports:
      tcp-metaprotocol-dubbo: 20880
    serviceAccount: default
  hosts:
  - org.apache.dubbo.samples.basic.api.demoservice
  location: MESH_INTERNAL
  ports:
  - name: tcp-metaprotocol-dubbo
    number: 20880
    protocol: tcp
    targetPort: 20880
  resolution: STATIC

编辑此 ServiceEntry 增加 addresses 字段,分配一个保留网段的全局唯一 ip 值,比如 240.240.0.2,当 consumer 端请求到 host 服务时,经过域名解析后会请求到 240.240.0.2。 分配了 addresses 后,那 EnvoyFilter 中需要替换的过滤器就确定了。部署如下所示的 EnvoyFilter,会将配置中请求的 TCP 过滤器替换为 Dubbo 过滤器,并设置了路由的匹配方法和目标集群,这样请求会被转发到目标集群对应的 Endpoints(Istio 根据 ServiceEntry 的 Endpoints 信息下发到 envoy 中)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: aeraki-inbound-org.apache.dubbo.samples.basic.api.demoservice-240.240.0.2-20880
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      listener:
        name: 240.240.0.2_20880
        filterChain:
          filter:
            name: "envoy.filters.network.tcp_proxy"
    patch:
      operation: REPLACE
      value:
        name: envoy.filters.network.dubbo_proxy
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.dubbo_proxy.v3.DubboProxy
          protocol_type: Dubbo
          serialization_type: Hessian2
          statPrefix: outbound|20880||org.apache.dubbo.samples.basic.api.demoservice
          route_config:
          - name: outbound|20880||org.apache.dubbo.samples.basic.api.demoservice
            interface: org.apache.dubbo.samples.basic.api.DemoService
            routes:
            - match:
                method:
                  name:
                    exact: sayHello
              route:
                cluster: outbound|20880||org.apache.dubbo.samples.basic.api.demoservice

继续部署如下所示的 consumer,部署描述中也去掉了 sidecar 相关的 annotations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dubbo-sample-consumer
  labels:
    app: dubbo-sample-consumer
spec:
  selector:
    matchLabels:
      app: dubbo-sample-consumer
  replicas: 1
  template:
    metadata:
      labels:
        app: dubbo-sample-consumer
    spec:
      containers:
        - name: dubbo-sample-consumer
          image: aeraki/dubbo-sample-consumer
          env:
            - name: mode
              value: demo
          ports:
            - containerPort: 9009

查看日志可以看到,consumer 服务正常请求 provider 了。这里是通过手动修改 ServiceEntry,手动编写 EnvoyFilter,比较繁琐,可以将此过程自动化,新增一个模块 EController,整体框图如下所示。

econtrol_dubbo2istio.png

参考