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
一类的缓存组件,因为这些细节与本课题无关。
如图所示,Microservice
代表要接入 ShenYu 的微服务,ShenYu Admin
是用来处理服务注册、插件规则配置的 ShenYu 组件,而 ShenYu Gateway
是实际的处理、转发网络请求的 ShenYu 网关组件。
其中,黄色的线条代表服务注册相关逻辑,绿色的线条代表用户发起网络请求时的流量处理路径。
下面,让我基于这个图,介绍一下 Apache ShenYu 的服务注册流程和特点。
2.1 服务注册流程
- 微服务在启动后,通过 Register Client 向 Register Center 发送自身服务信息(meatadata, uri, port)
- ShenYu Admin 实时监听 Register Center 的数据变化,在数据发生变更后,进行数据解析
- ShenYu Admin 通过 Data Sync 模块将微服务的详细信息同步到 ShenYu Gateway。然后 Gateway 将数据缓存在 Cache 中,作为后续处理/转发网络流量的依据。
还有一些值得注意的细节:
-
Register Client 不需要微服务方手动实现,ShenYu 提供了 SDK。例如
gRPC
协议的微服务注册,只要引入以下依赖,并且配置注册中心的连接信息即可:<dependency> <groupId>org.apache.shenyu</groupId> <artifactId>shenyu-spring-boot-starter-plugin-grpc</artifactId> <version>${project.version}</version> </dependency>
-
Register Client 和 Register Center 间,Register Center 和 ShenYu Admin 间还有
Disruptor
队列用来提供解耦、缓冲作用。 -
ShenYu Admin 和 ShenYu Gateway 中间的 “Data Sync” 流程,其实也用了另外的 Register Center 来实现,图中隐藏了技术细节。
2.2 Apache ShenYu 的服务注册特点
Apache ShenYu 的服务注册的最大特点是,由微服务方主动发起注册这个动作。
这带来了这样几个特性:
- 注册时能够携带的微服务信息更多。对比 Kubernetes 的原生服务发现,Kubernetes 无法直接感知 Pod 内部运行的每个后端接口的详细情况,必须在 YAML 内主动声明。而 ShenYu 提供了 Java 的注解,使得微服务能在代码编写时直接将每一个后端接口分别注册到 ShenYu Admin,而不需要额外的配置。
- 使用了注册中心解耦 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 中存储信息。
- 将信息存到 Kubernetes 的 Pod 的 Annotations/Labels 中。前面提到过,ShenYu 的服务注册是每个微服务独立、主动注册的,而一个微服务其实就对应一个进程,也就是Kubernetes 的 Pod。看起来用 Pod Annotataions/Labels 更优(例如dubbo-go就是采用的这种方式),但由于 ShenYu 的 metadata 不依赖于具体的某个微服务实例(即Pod)而存在,故不采用。
- 将信息存到 ConfigMap 中。这和 ConfigMap 用于存储配置的初衷一致,我决定采取这个方案。同时,使用 Labels 筛选特定的 ConfigMap。
- 将信息存储到 Secert 中。这和 ConfigMap 类似,多了数据加密,但意义不大,不采用。
3.2 存储细节
3.2.1 使用一个还是多个ConfigMap
我们可以只使用一个 ConfigMap 来存储所有的微服务 key-value 数据,但这显然是不合理的:
- 每次微服务发生变动都会修改同一个 ConfigMap
- 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 设计新的存储方法:
- 顶层设计
首先,我将所有的 ConfigMap 都存储在 shenyu
命名空间内,所有的 ConfigMap 名字都以 register-
为前缀,并且打上 shenyu.apache.org/register: true
的标签,用于区分以后可能存在的与注册中心无关的ConfigMap。
- metadata/uri 的处理
接着, metadata
和 uri
分别存储在两个不同的 ConfigMap 中,分别以 register-metadata-
和 register-uri-
作为前缀名,并分别打上 shenyu.apache.org/register-type: metadata
和 shenyu.apache.org/register-type: uri
的 Label。(其中,不同的前缀名是为了人类可读,不同的 Label 用于程序筛选。)
- 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
。
- contextPath 的处理
筛选方面,打上 shenyu.apache.org/register-context-path: ${contextPath}
标签。
命名方面,因为 contextPath 很可能不满足 ConfigMap 的命名规则(不是一个 DNS 子域名),所以不将其直接添加到 ConfigMap 前缀中,而是与后面的 ruleName
或 ip:port
先拼接再哈希,并且采取一定的机制保证 ConfigMap 名字不重复(比如时间戳)。
- 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:8189
和 10.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 的核心在于,实现 ShenyuClientRegisterRepository
和 ShenyuServerRegisterRepository
两个 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() {}
}
需要做的工作主要在第二个接口上:
- 初始化 Kubernetes Java Client
- 使用 Kubernetes 的 List-Watch 机制,订阅 ConfigMap 的更新
- 检测到 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 社区带来以下贡献:
- 使 ShenYu 对云原生的支持迈出一大步,支持 Kubernetes 内的微服务部署到 ShenYu
- 为 ShenYu 贡献一个新的注册中心,基于 Kubernetes Configmap
- 优化 ShenYu 的 Kubernetes 生态,例如加强 ShenYu Helm Chart
- 为其他开发者的“云”开发之旅提供经验,例如如何快速在本地开发、调试 ShenYu 的 Kubernetes 相关组件
6 项目交付成果
- 支持将部署在 Kubernetes 内的微服务部署到 ShenYu
- 设计 ConfigMap 存储 ShenYu 微服务注册的 metadata 和 uri 存储细节
- 实现 Kubernetes 注册中心(主要是
ShenyuClientRegisterRepository
和ShenyuServerRegisterRepository
两个 SPI) - 完善其他细节,比如在 ShenYu Helm Chart 中自动创建 Service Account
- 完善开发文档、用户文档,执行必要的手动测试,添加单元测试、集成测试等
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
- 我加入了 Apache ShenYu 的邮件列表,并且出席了社区例会,积极与社区开发者沟通
- 在撰写 Proposal 的时候,我发现了文档中存在的一些问题,并且提交了 Issue:[BUG] Inconsistent of register-center-access
- 在和社区沟通后,确认是文档有误。我提交了 PR 以修复文档:[ISSUE #876] Inconsistent of register-center-access
- 在调试代码的过程中,我发现了项目主仓库的另一个 Bug 并提交 PR 修复:[Fix] missing actuator dependency and port error in examples http
- 我也向 ShenYu Helm Chart 贡献了代码
10 其他承诺
关于GSoC
-
时间:GSoC 编码编写的时间段,我刚好过暑假。所以我有整个暑假的时间去参与 GSoC 的项目与向社区做贡献。我计划每周在 GSoC 的这个项目上花40个小时。
-
能力:我对 Kubernetes 有一定的了解,并对项目的核心技术做了验证(例如观察 etcd 作为注册中心时的数据存储格式、测试Kubernestes的网络)。我相信通过自己的努力,并积极与社区交流,能够完成本项目。
持续参与
在 GSoC 结束后,我将继续在 ShenYu 社区做贡献:
- 继续推进 ShenYu 的云原生化
- 参与 ShenYu Client Golang 的维护(我对 Go 语言也很感兴趣)
- 为 ShenYu 优化用户体验,如前面提到的文档问题
References
- https://shenyu.apache.org/docs/design/register-center-design/
- https://shenyu.apache.org/blog/RegisterCenter-SourceCode-Analysis-Http-Register
- https://shenyu.apache.org/docs/user-guide/property-config/register-center-access/#http-registry-config
- https://dubbogo.github.io/zh-cn/docs/md/registry-center/design-and-implementation-of-dubbo-go-and-k8s-registry.html
- https://kubernetes.io/docs/concepts/configuration/configmap/#configmap-object
- https://github.com/spring-cloud/spring-cloud-kubernetes
- https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
- https://github.com/devstream-io/devstream