目录

GSoC Proposal: Support for Kubernetes Service Discovery(zh-CN)

摘要

在使用 ShenYu 的时候,我发现注册中心部分的文档有误,提交了 Issue 。通过和社区开发者的交流,以及阅读源码,我确认了问题并提交了 PR 以修复。这给了我极大的申请本项目的信心。

本申请书主要涉及:Apache ShenYu 注册中心的架构;如何将部署在 Kubernetes 中的微服务注册到 ShenYu(整体方案设计、代码细节);成果、时间计划、承诺等。

1 背景

1.1 Apache ShenYu

Apache ShenYu是一个用于服务代理、协议转换和API治理的Java原生API网关。目前,ShenYu在微服务场景下具有优良的可用性和性能。

然而,ShenYu对Kubernetes的支持仍然相对较弱。

1.2 课题阐述

要想让 ShenYu 成为一个彻底的"Cloud-Native"的网关,不是一蹴而就的。

而当前这个课题所研究的便是ShenYu云原生化的第一步:使得部署在 Kubernetes 内的微服务也能注册到ShenYu Admin 中,并且利用 Kubernetes 充当注册中心。

2 分析 Apache ShenYu 的服务注册机制

我在研读了 Client Registry Design-官方文档Register Center Source Code Analysis of Http Register-官方博客 之后画了这样一张 ShenYu 服务注册机制的核心流程图。去除了诸如 Disruptor 一类的缓存组件,因为这些细节与本课题无关。

ShenYu服务注册机制
ShenYu服务注册机制

如图所示,Microservice 代表要接入 ShenYu 的微服务,ShenYu Admin 是用来处理服务注册、插件规则配置的 ShenYu 组件,而 ShenYu Gateway 是实际的处理、转发网络请求的 ShenYu 网关组件。

其中,黄色的线条代表服务注册相关逻辑,绿色的线条代表用户发起网络请求时的流量处理路径。

下面,让我基于这个图,介绍一下 Apache ShenYu 的服务注册流程和特点。

2.1 服务注册流程

  1. 微服务在启动后,通过 Register Client 向 Register Center 发送自身服务信息(meatadata, uri, port)
  2. ShenYu Admin 实时监听 Register Center 的数据变化,在数据发生变更后,进行数据解析
  3. ShenYu Admin 通过 Data Sync 模块将微服务的详细信息同步到 ShenYu Gateway。然后 Gateway 将数据缓存在 Cache 中,作为后续处理/转发网络流量的依据。

还有一些值得注意的细节:

  1. Register Client 不需要微服务方手动实现,ShenYu 提供了 SDK。例如 gRPC 协议的微服务注册,只要引入以下依赖,并且配置注册中心的连接信息即可:

    <dependency>
        <groupId>org.apache.shenyu</groupId>
        <artifactId>shenyu-spring-boot-starter-plugin-grpc</artifactId>
        <version>${project.version}</version>
    </dependency>
    
  2. Register Client 和 Register Center 间,Register Center 和 ShenYu Admin 间还有 Disruptor 队列用来提供解耦、缓冲作用。

  3. ShenYu Admin 和 ShenYu Gateway 中间的 “Data Sync” 流程,其实也用了另外的 Register Center 来实现,图中隐藏了技术细节。

2.2 Apache ShenYu 的服务注册特点

Apache ShenYu 的服务注册的最大特点是,由微服务方主动发起注册这个动作。

这带来了这样几个特性:

  1. 注册时能够携带的微服务信息更多。对比 Kubernetes 的原生服务发现,Kubernetes 无法直接感知 Pod 内部运行的每个后端接口的详细情况,必须在 YAML 内主动声明。而 ShenYu 提供了 Java 的注解,使得微服务能在代码编写时直接将每一个后端接口分别注册到 ShenYu Admin,而不需要额外的配置。
  2. 使用了注册中心解耦 Register Client 和 Register Server,使得拓展更加容易。同样也有新的代价,每个微服务都必须拥有读写注册中心的权限。因为微服务不直接与 ShenYu Admin 通信(除 HTTP 注册方式外),而是直接将微服务数据写入注册中心。

3 方案设计 - 利用Kubernetes充当 Register Center

Register Center 本质上就是一个用来存储微服务元数据的中间件,Register Client 写入,Register Server 读取。所以我们要做的,其实是把微服务的 metadata 和 uri(包含port) 信息存储到 Kubernetes 中,并且在 Register Server 端监听。

3.1 存储方式的选择

目前主要有三种方式在 K8s 中存储信息。

  1. 将信息存到 Kubernetes 的 Pod 的 Annotations/Labels 中。前面提到过,ShenYu 的服务注册是每个微服务独立、主动注册的,而一个微服务其实就对应一个进程,也就是Kubernetes 的 Pod。看起来用 Pod Annotataions/Labels 更优(例如dubbo-go就是采用的这种方式),但由于 ShenYu 的 metadata 不依赖于具体的某个微服务实例(即Pod)而存在,故不采用。
  2. 将信息存到 ConfigMap 中。这和 ConfigMap 用于存储配置的初衷一致,我决定采取这个方案。同时,使用 Labels 筛选特定的 ConfigMap。
  3. 将信息存储到 Secert 中。这和 ConfigMap 类似,多了数据加密,但意义不大,不采用。

3.2 存储细节

3.2.1 使用一个还是多个ConfigMap

我们可以只使用一个 ConfigMap 来存储所有的微服务 key-value 数据,但这显然是不合理的:

  1. 每次微服务发生变动都会修改同一个 ConfigMap
  2. Kubernetes 的对象资源大小存在上限

故采用多个 ConfigMap 来存储微服务数据,利用 Labels 筛选。

3.2.2 ConfigMap 详细设计

我们先来看看使用 etcd 作为注册中心时,是如何存储数据的:

shenyu
   ├──regsiter
   ├    ├──metadata
   ├    ├     ├──${rpcType}
   ├    ├     ├      ├────${contextPath}
   ├    ├     ├               ├──${ruleName} : save metadata data of MetaDataRegisterDTO
   ├    ├──uri
   ├    ├     ├──${rpcType}
   ├    ├     ├      ├────${contextPath}
   ├    ├     ├               ├──${ip:prot} : save uri data of URIRegisterDTO
   ├    ├     ├               ├──${ip:prot}

可以看到,数据主要分为以下几层:

register->metadata/uri->${rpcType}->${contextPath}->details

下面我们针对 ConfigMap 设计新的存储方法:

  1. 顶层设计

首先,我将所有的 ConfigMap 都存储在 shenyu 命名空间内,所有的 ConfigMap 名字都以 register- 为前缀,并且打上 shenyu.apache.org/register: true 的标签,用于区分以后可能存在的与注册中心无关的ConfigMap。

  1. metadata/uri 的处理

接着, metadatauri 分别存储在两个不同的 ConfigMap 中,分别以 register-metadata-register-uri- 作为前缀名,并分别打上 shenyu.apache.org/register-type: metadatashenyu.apache.org/register-type: uri 的 Label。(其中,不同的前缀名是为了人类可读,不同的 Label 用于程序筛选。)

  1. rpcType 的处理

下面一层就是 rpcType,类似的,前缀格式是 register-metadata-${rpcType}-register-uri-${rpcType}-,如 register-metadata-http 。同时使用 shenyu.apache.org/register-rpc-type: ${rpcType} 作为标签,如 shenyu.apache.org/register-rpc-type: http

  1. contextPath 的处理

筛选方面,打上 shenyu.apache.org/register-context-path: ${contextPath} 标签。

命名方面,因为 contextPath 很可能不满足 ConfigMap 的命名规则(不是一个 DNS 子域名),所以不将其直接添加到 ConfigMap 前缀中,而是与后面的 ruleNameip:port 先拼接再哈希,并且采取一定的机制保证 ConfigMap 名字不重复(比如时间戳)。

  1. ruleName 和 ip:port 的处理

其中 ruleName 记录的是 metadata,而 ip:port 记录的是 uri 信息,二者的值都是一个 json。

考虑到可能存在某个微服务特别庞大进而导致个别 contextPath 数据超过容量限制的情况,我打算一个 ConfigMap 只存一条 ruleName 或 ip:port 记录,使用标签 shenyu.apache.org/register-rule-name: ${ruleName}shenyu.apache.org/register-ip-port: ${ipPort} 。ConfigMap 内容格式是 data: {...}

3.2.3 示例

以下的示例中,建立了一个名为 app1 的微服务,设定了 contextPath 为 /app1 ,注册了两个接口。并且运行了两个实例(Pod),分别在 10.42.0.34:818910.42.0.36:8189

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: shenyu
  name: register-metadata-http-asdfio-1679973837912
  labels:
    "shenyu.apache.org/register": "true"
    "shenyu.apache.org/register-type": "metadata"
    "shenyu.apache.org/register-context-path": "app1"
    "shenyu.apache.org/register-rule-name": "app1--app1-hello"
data:
  data: |
    {"appName":"app1","contextPath":"/app1","path":"/app1/hello","pathDesc":"spring annotation register","rpcType":"http","serviceName":"org.apache.shenyu.examples.http.controller.SpringMvcMappingPathController","methodName":"hello","ruleName":"/app1/hello","enabled":true,"pluginNames":[],"registerMetaData":true,"timeMillis":1679973837912,"addPrefixed":false}    

---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: shenyu
  name: register-metadata-http-dfgjkl-1679973837913
  labels:
    "shenyu.apache.org/register": "true"
    "shenyu.apache.org/register-type": "metadata"
    "shenyu.apache.org/register-context-path": "app1"
    "shenyu.apache.org/register-rule-name": "app1--app1-request-**"
data:
  data: |
    {"appName":"app1","contextPath":"/app1","path":"/app1/request/**","rpcType":"http","serviceName":"org.apache.shenyu.examples.http.controller.RequestController","ruleName":"/app1/request/**","enabled":true,"pluginNames":[],"registerMetaData":true,"timeMillis":1679973837911,"addPrefixed":false}    

---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: shenyu
  name: register-metadata-http-oujijk-1679973837914
  labels:
    "shenyu.apache.org/register": "true"
    "shenyu.apache.org/register-type": "uri"
    "shenyu.apache.org/register-context-path": "app1"
    "shenyu.apache.org/register-ip-port": "10.42.0.34:8189"
data:
  data: |
    {"protocol":"http://","appName":"app1","contextPath":"/app1","rpcType":"http","host":"10.42.0.34","port":8189}    
---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: shenyu
  name: register-metadata-http-cxvhyu-1679973837915
  labels:
    "shenyu.apache.org/register": "true"
    "shenyu.apache.org/register-type": "uri"
    "shenyu.apache.org/register-context-path": "app1"
    "shenyu.apache.org/register-ip-port": "10.42.0.36:8189"
data:
  data: |
    {"protocol":"http://","appName":"app1","contextPath":"/app1","rpcType":"http","host":"10.42.0.36","port":8189}    

3.3 Register Client 与 Register Server 代码编写

实现 Register Client 和 Register Server 的核心在于,实现 ShenyuClientRegisterRepositoryShenyuServerRegisterRepository 两个 SPI。

3.3.1 ShenyuClientRegisterRepository

下面是 ShenyuClientRegisterRepository 的定义

/**
 * Shenyu client register repository.
 */
@SPI
public interface ShenyuClientRegisterRepository {
    default void init(ShenyuRegisterCenterConfig config) {}
    void persistInterface(MetaDataRegisterDTO metadata);
    default void persistURI(URIRegisterDTO registerDTO) {}
    default void persistApiDoc(ApiDocRegisterDTO apiDocRegisterDTO) {}
    default void close() {}
}
  • Init():我将根据用户配置生成对应的操作 Kubernetes 的 Java Client,参考 spring-cloud-kubernetes 中的 client 建立和 ConfigMap 操作代码。
  • persistInterface():使用 Kubernetes Java Client 将 metadata 按照上文的存储方式存入 ConfigMap 中。
  • persistURI():将 metadata 存入 ConfigMap 中。
  • persistApiDoc():这是近期新添加的接口,还在完善中,其他注册中心暂未实现该接口。
  • close():关闭 Kubernetes Java Client。

3.3.2 ShenyuServerRegisterRepository

下面是 ShenyuServerRegisterRepository 的定义:

/**
 * Shenyu client server register repository.
 */
@SPI
public interface ShenyuClientServerRegisterRepository {
    
    default void init(ShenyuRegisterCenterConfig config) {}
    
    default void init(ShenyuClientServerRegisterPublisher publisher, ShenyuRegisterCenterConfig config) {}

    default void close() {}
}

需要做的工作主要在第二个接口上:

  1. 初始化 Kubernetes Java Client
  2. 使用 Kubernetes 的 List-Watch 机制,订阅 ConfigMap 的更新
  3. 检测到 ConfigMap 变动后,及时处理

4 支持部署在 Kubernetes 的微服务注册到 ShenYu 的一些细节

除了上文讲述的最核心的注册中心的设计,本课题还需要处理一些细节。

4.1 网络联通问题

在微服务主动向 ShenYu Admin 注册时,只能获取到微服务的 Pod IP(这个IP是虚拟的,只有Kubernetes内部可以正常使用)。而用户所有的网络请求又通过 ShenYu Gateway 转发,所以如果 ShenYu Gateway 不在 Kubernetes 内部,那么网络就会中断。

解决办法就是,将 ShenYu Admin 和 ShenYu Gateway 都部署在 Kubernetes 中。经过文档翻阅和实验,Kubernetes 的各个 CNI 插件实现了 Pod IP 的跨 Node 访问,我们无须担心。

那么如何将 ShenYu 的各个组件部署到 Kubernetes 内呢?幸运的是,社区已经有了初版的 ShenYu Helm Chart,可以在快速在 Kubernetes 中部署 ShenYu。

4.2 Kubernetes 权限问题 - Service Account

由于每个微服务和 ShenYu Admin 都需要读写 ConfigMap,而微服务和 ShenYu 和 Admin 都在 Pod 中,所以需要为每个 Pod 配置一个 Service Account,赋予其访问 Kubernetes 的权限,具体可参考:Configure Service Accounts for Pods

配置大概是这样:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: shenyu-configmap-full-access
  namespace: shenyu
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["*"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: shenyu-kubernetes-register
  namespace: shenyu
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: shenyu-configmap-full-access-binding
  namespace: shenyu
subjects:
- kind: ServiceAccount
  name: shenyu-register
  namespace: shenyu
roleRef:
  kind: Role
  name: shenyu-configmap-full-access
  apiGroup: rbac.authorization.k8s.io

然后我们需要将 ShenYu Admin 和微服务的所有 Pod 的 spec.serviceAccountName 设置为 shenyu-kubernetes-register

手动创建 ServiceAccount、Role、RoleBinding 似乎有些繁琐,能否更简单一些呢?

当然!本课题将尝试更新 ShenYu Helm Chart,自动生成正确的 ServiceAccount 等对象,用户只需要在部署的时候修改 Pod 的 spec.serviceAccountName

5 能够为社区带来什么

在本课题完成后,将为 Apache Shenyu 社区带来以下贡献:

  1. 使 ShenYu 对云原生的支持迈出一大步,支持 Kubernetes 内的微服务部署到 ShenYu
  2. 为 ShenYu 贡献一个新的注册中心,基于 Kubernetes Configmap
  3. 优化 ShenYu 的 Kubernetes 生态,例如加强 ShenYu Helm Chart
  4. 为其他开发者的“云”开发之旅提供经验,例如如何快速在本地开发、调试 ShenYu 的 Kubernetes 相关组件

6 项目交付成果

  1. 支持将部署在 Kubernetes 内的微服务部署到 ShenYu
  2. 设计 ConfigMap 存储 ShenYu 微服务注册的 metadata 和 uri 存储细节
  3. 实现 Kubernetes 注册中心(主要是ShenyuClientRegisterRepositoryShenyuServerRegisterRepository 两个 SPI)
  4. 完善其他细节,比如在 ShenYu Helm Chart 中自动创建 Service Account
  5. 完善开发文档、用户文档,执行必要的手动测试,添加单元测试、集成测试等

7 时间计划表

时间内容安排
Preparing(Before Accepted GSoC contributor projects announced)与社区保持沟通,继续做贡献,发现部署、云相关的Bug,提交Issue与PR
May 4 - 28(Community Bonding Period)熟悉 Kubernetes Java Client 使用;探索 ShenYu 与 Kubernetes 结合的最佳开发流程。
May 29 - July 09(Before Midterm Evaluations)完成demo,实现对Kubernets内微服务的注册的支持,提交中期评估
July 10 - August 21(Work Period)优化代码细节,编写测试,完善文档
August 21 - 28(Final week)提交最终工作成果并准备最终导师评估

8 About Me

In GSoC 2023,我只向 ShenYu 提交了 proposal。

我是一名研究生二年级的计算机学生,热爱编程,尤其对云原生领域感兴趣。

我曾在 DevStream 持续做过贡献,主要是 DevOps 相关的一些工作。在那里,我感受到了开源的乐趣,爱上了开源独有的共同编码、异步沟通、注重协作的氛围。

在 ShenYu,在网关领域,我又是一名新人。我热切地希望我能为 ShenYu 的云原生化作出贡献。

9 Community engagement

10 其他承诺

关于GSoC

  • 时间:GSoC 编码编写的时间段,我刚好过暑假。所以我有整个暑假的时间去参与 GSoC 的项目与向社区做贡献。我计划每周在 GSoC 的这个项目上花40个小时。

  • 能力:我对 Kubernetes 有一定的了解,并对项目的核心技术做了验证(例如观察 etcd 作为注册中心时的数据存储格式、测试Kubernestes的网络)。我相信通过自己的努力,并积极与社区交流,能够完成本项目。

持续参与

在 GSoC 结束后,我将继续在 ShenYu 社区做贡献:

  • 继续推进 ShenYu 的云原生化
  • 参与 ShenYu Client Golang 的维护(我对 Go 语言也很感兴趣)
  • 为 ShenYu 优化用户体验,如前面提到的文档问题

References

  1. https://shenyu.apache.org/docs/design/register-center-design/
  2. https://shenyu.apache.org/blog/RegisterCenter-SourceCode-Analysis-Http-Register
  3. https://shenyu.apache.org/docs/user-guide/property-config/register-center-access/#http-registry-config
  4. https://dubbogo.github.io/zh-cn/docs/md/registry-center/design-and-implementation-of-dubbo-go-and-k8s-registry.html
  5. https://kubernetes.io/docs/concepts/configuration/configmap/#configmap-object
  6. https://github.com/spring-cloud/spring-cloud-kubernetes
  7. https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
  8. https://github.com/devstream-io/devstream