Dubbo3源码(四)K8S注册发现
前言
早在2.7.5版本,dubbo就实现了应用级别注册与发现,从而在3.x版本能够支持k8s注册发现模型。
本文基于dubbo3.2.0分析k8s注册发现,包括以下内容:
- 应用级别注册与发现在3.x中的实现:简单回顾,与2.7类似,大部分只是做了优化和重构
- 服务注册:Pod注解变更、三种探针
- 服务发现:应用名映射、查询应用实例、监听资源变更
一、案例
消费者
消费者业务代码,指定引用dubbo-demo-k8s-provider提供的DemoService服务。
@SpringBootApplication
@EnableDubbo
@RestController
public class ConsumerApplication {
private static final Logger logger = LoggerFactory.getLogger(ConsumerApplication.class);
@DubboReference(check = false, providedBy = "dubbo-demo-k8s-provider")
private DemoService demoService;
@GetMapping("/invoke")
public String invokeHttp() {
String name = new Date().toString();
logger.info("start invoke, name = " + name);
return demoService.sayHello(name);
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
消费者application.yaml配置。
注册中心协议:kubernetes。
注册中心地址:apiserver对应的service地址。
注册中心参数:指定namespace用dubbo-demo。
spring:
application:
name: dubbo-demo-k8s-consumer
dubbo:
application:
name: ${spring.application.name}
protocol:
name: dubbo
port: 20880
registry:
address: kubernetes://kubernetes.default.svc.cluster.local:443?namespace=dubbo-demo
apiserver对应service为namespace=default下的kubernetes。
消费者镜像,设置dubbo.application.migration.step=FORCE_APPLICATION强制走应用级别服务发现。
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<from>
<image>eclipse-temurin:8-jre</image>
</from>
<to>
<image>${project.artifactId}:${project.version}</image>
</to>
<container>
<environment>
<dubbo.application.migration.step>FORCE_APPLICATION</dubbo.application.migration.step>
</environment>
<jvmFlags>
<jvmFlag>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=31000</jvmFlag>
</jvmFlags>
</container>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
提供者
提供者业务代码。
@SpringBootApplication
@EnableDubbo
@DubboService
public class ProviderApplication implements DemoService {
private static final Logger logger = LoggerFactory.getLogger(ProviderApplication.class);
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
@Override
public String sayHello(String name) {
logger.info("Hello " + name + ", request from consumer: "
+ RpcContext.getContext().getRemoteAddress());
return "Hello " + name + ", response from provider: "
+ RpcContext.getServiceContext().getLocalAddress();
}
}
提供者application.yaml配置,register-mode=instance应用级别注册,注册中心与消费者一致。
spring:
application:
name: dubbo-demo-k8s-provider
dubbo:
application:
name: ${spring.application.name}
register-mode: instance
protocol:
name: dubbo
port: 20880
registry:
address: kubernetes://kubernetes.default.svc.cluster.local:443?namespace=dubbo-demo
提供者镜像,与消费者一致,除了镜像名。
k8s部署
Deployment省略。
服务提供者Service,暴露50051端口。
注意,我这里端口其实配错了,难道不应该是20880吗。
apiVersion: v1
kind: Service
metadata:
name: dubbo-demo-k8s-provider
namespace: dubbo-demo
spec:
selector:
app: dubbo-demo-k8s-provider
ports:
- protocol: TCP
port: 50051
targetPort: 50051
服务消费者Service,暴露8080端口。
apiVersion: v1
kind: Service
metadata:
name: dubbo-demo-k8s-consumer
namespace: dubbo-demo
spec:
selector:
app: dubbo-demo-k8s-consumer
ports:
- protocol: TCP
name: tomcat
port: 8080
我这里用higress配置dubbo-demo.com/invoke转发到消费者暴露8080端口的Service。
可以拿到正常响应。
二、服务注册
注册流程回顾
step1:RegistryProtocol#export
调用底层rpc协议暴露rpc服务,比如TripleProtocol#export;
ServiceDiscoveryRegistry委派对应ServiceDiscovery,将url存储到MetadataInfo管理。
step2:MetadataServiceNameMapping#map
构建rpc服务到应用名的映射关系,发布到元数据中心(2.7.5的时候就是配置中心)。
注意:当注册中心为nacos和zk时,自动识别元数据中心,其他仅支持redis作为元数据中心。所以用k8s元数据中心在哪,consumer如何获取rpc服务和应用名的映射关系?
step3:DefaultApplicationDeployer#exportMetadataService
暴露MetadataService服务。
step4:DefaultApplicationDeployer#registerServiceInstance
调用ServiceDiscovery注册应用实例。
AbstractServiceDiscovery#register:
发布MetadataInfo,供消费者通过rpc调用获取;
子类执行注册逻辑,比如zk注册ServiceInstance对应临时znode。
注册
无论是提供者还是消费者,都需要创建KubernetesServiceDiscovery用于注册和发现。
KubernetesServiceDiscovery构造阶段会连接apiserver,校验当前pod存在。
即:GET /api/v1/namespaces/{namespace}/pods/{podName}。
namespace:通过注册中心url获取。
podName:通过环境变量中的HOSTNAME获取。
KubernetesServiceDiscovery#doRegister:
服务注册整体流程的最后一步,注册ServiceInstance阶段,k8s注册仅仅修改pod的annotation。
普通注册中心注册应用实例,比如zk注册临时znode对consumer可见。
io.dubbo/metadata包含一些关键的信息,比如revision和Rpc服务MetadataService信息。
这部分和其他注册中心一致,zk会保存在/services/{应用名}/{实例地址}这个znode的数据中。
探针
k8s中的提供者要让consumer感知到上线,取决于Probe探针。
如果不配置探针,pod会直接暴露给consumer,收到Service流量。
这里我们直接采用dubbo-qos中自带的几个探针接口,实际生产用不用那是另外回事。
StartupProbe启动探针
在容器启动初始阶段,需要先经过启动探针探测。
比如deployment的yaml中对于容器配置startupProbe如下:
每隔periodSeconds=10秒,向22222端口,发送GET/startup请求。
如果成功1次,则容器启动成功,交给存活探针和就绪探针检测。
如果经过30次还未成功,则容器启动失败,根据restartPolicy处理,一般就是重启。
containers:
- name: server
image: dubbo-demo-k8s-provider:0.0.1
imagePullPolicy: Never
startupProbe:
httpGet:
path: /startup
port: 22222
failureThreshold: 30
periodSeconds: 10
Startup#execute:
只有所有StartupProbe扩展点实现都返回true,才会返回200,否则返回503。
默认Dubbo只提供了一个StartupProbe。
DeployerStartupProbe:如果有一个ModuleDeployer正在运行,就返回true。
这其实是一个非常早期的阶段,ApplicationDeployer已经准备完成(比如连接配置中心、元数据中心等),但是ModuleDeployer还未执行服务暴露/引用。
DefaultModuleDeployer#startSync:
ReadinessProbe就绪探针
当启动探针检测通过,即ApplicationDeployer初始化完成,ModuleDeployer正在运行,进入就绪探针探测。
如下配置Deployment:初始延迟5秒,后续每隔5秒向22222端口发送GET/ready请求。
只有当readinessProbe正常返回,才会暴露Endpoint。
readinessProbe:
httpGet:
path: /ready
port: 22222
initialDelaySeconds: 5
periodSeconds: 5
Ready#execute:
所有ReadinessProbe检测通过,应用才进入就绪状态。
dubbo提供了两个ReadinessProbe。
DeployerReadinessProbe
所有ModuleDeployer启动完成返回true。
一般情况下,如果一个ModuleDeployer处于started状态,会紧接着暴露MetadataService并修改pod的annotation。
但是这里会不会导致在未暴露MetadataService的情况下,收到consumer查询元数据的情况?
(consumer订阅的地方再看)
ProviderReadinessProbe
DeployerReadinessProbe探针更多的是应用纬度的检测,
ProviderReadinessProbe是rpc服务级别的检测。
通过条件有两种:
1)当前应用没有提供任何rpc服务;
2)部分或全部rpc服务已经暴露;
反过来说,只要不是所有rpc服务都下线,就返回true。
为什么会有这个探针,主要是支持通过qos控制rpc服务上下线。
BaseOffline#doUnexport:
不传参的情况下,请求22222端口offline端点,直接下线所有rpc服务,container的就绪探针检测返回失败。
导致endpoint列表中68机器被下线。
请求online端点,所有rpc服务又会重新上线。
LivenessProbe存活探针
当启动探针检测通过,即ApplicationDeployer初始化完成,ModuleDeployer正在运行,进入存活探针探测。
比如下面的配置:
延迟5秒后,每隔5秒请求22222端口的live端点,如果超过3次失败,根据restartPolicy处理。
livenessProbe:
httpGet:
path: /live
port: 22222
initialDelaySeconds: 5
periodSeconds: 5
Live#execute:所有LivenessProbe检测通过,应用存活。
dubbo未提供LivenessProbe任何实现,所以存活探针检测都会返回正常。
三、服务发现
引用流程回顾
忽略3.x迁移相关逻辑,服务引用的大致流程是:
1)Registry#subscribe:订阅服务并获取服务提供者urls,通知Directory;
2)RegistryDirectory#notifyDirectory:收到服务提供者urls,委派RpcProtocol#refer构造底层通讯invokers,如TripleInvoker;
3)Cluster#join,封装Directory为ClusterInvoker;
4)ProxyFactory#getProxy:创建rpc服务代理;
重点回顾一下第一步,即ServiceDiscoveryRegistry#subscribe,应用级别订阅:
1)serviceDiscovery#subscribe:将订阅url保存到当前应用的MetadataInfo中
2)获取rpc服务->应用名称列表(关注点)
之前我们在2.7.5看到的是走zk一类配置中心。
这里和2.7.5的区别在于,3.x支持监听映射关系变更,且这里走元数据中心。
3)subscribeUrls(特殊点) :根据应用名查询并订阅应用实例ServiceInstance,构造rpc服务提供者urls,通知Directory
这里和之前2.7.5的区别在于,查询revision对应MetadataInfo,可以走本地缓存(内存&文件系统)。
映射应用名称
在之前2.7.5的应用级别服务注册发现分析中,我们用zk作为注册中心。
zk会被自动识别为元数据中心,用于存储rpc服务和应用的映射关系。
/dubbo/mapping/{rpc服务}节点的数据部分,是逗号分割的应用名称列表。
但是如果使用k8s做注册中心,k8s并不能作为元数据中心存储这个映射关系。
ServiceDiscoveryRegistry#doSubscribe:
优先从订阅url中获取provided-by参数,即优先选取订阅时指定的服务提供者应用。
比如DubboReference注解指定providedBy为dubbo-demo-k8s-provider。
AbstractServiceNameMapping#getAndListen:
优先从元数据中心获取,比如zk、nacos、redis,单独k8s是不支持的。
其次从注册中心url上的subscribed-services参数获取,这种配置方式作用于未配置providedBy的所有Reference。
比如:kubernetes://apiserver?subscribed-services=a,b,c,d。
所以在不部署元数据中心的情况下,接入k8s后必须指定rpc服务的提供者应用。
如果不指定,只会打印日志,不会订阅任何服务。
No interface-apps mapping found in local cache, stop subscribing,
will automatically wait for mapping listener callback...
订阅
ServiceDiscoveryRegistry#subscribeURLs:
1)ServiceDiscovery#getInstance查询应用下所有ServiceInstance,通知Directory;
2)ServiceDiscovery#addServiceInstancesChangedListener订阅ServiceInstance变更;
通知Directory部分不再赘述,和2.7.5大致相同。
就是按照revision分组,挑一个ServiceInstance查询MetadataService。
然后组装providerUtls,通知Directory。
Directory调用rpc协议构造底层通讯Invoker。
接下来主要看k8s在构造ServiceInstance和订阅上的特点。
查询ServiceInstance
KubernetesServiceDiscovery#getInstances:
查询service下的endpoint。
KubernetesServiceDiscovery#toServiceInstance:
1)根据Service上的Selector,找所有pod。
2)统计Endpoint下所有端口,即Service定义的targetPort。
3)根据pod+端口纬度,构建ServiceInstance。
这里将提供者在pod上写入的io.dubbo/metadata注解反序列化到ServiceInstance.metadata。
注意:如果拿不到这个注解,consumer最终不会将这个pod作为ServiceInstance放到最后的结果集里,这也是为什么DeployerReadinessProbe没有问题。
资源监听
KubernetesServiceDiscovery#addServiceInstancesChangedListener:
对于k8s来说,需要监听多个资源,包括Endpoint、Pod、Service。
Endpoint无论增删改都需要监听。
感知provider上下线(就绪探针),重点就是监听Endpoint变更。
当发生变更,根据Endpoint和Service需要重新构建ServiceInstance列表,通知Directory。
Pod只监听更新。
比较重要的是,provider在启动的最后阶段,暴露元数据服务之后,会修改pod的注解。
此时会触发consumer感知到pod更新,consumer会重新查询service下所有instance,通知Directory。
Service也只监听更新。
主要场景是Service的selector指向变更。
需要销毁原有Service下的Pod监听,重新触发watchPods。
Service目标端口
关于部署时Service目标端口的一个问题。
我部署的时候端口配的是triple协议默认端口50051,而实际代码用的是dubbo协议的20880。
运行起来却也没有什么问题。
这个targetPort一般不会被用到最后rpc调用的通讯上。
ServiceInstancesChangedListener#getServiceUrlsCache:
在获取完revision对应元数据后,构造providerUrl时,对于port有一个逻辑。
1)优先,取对端元数据MetadataInfo.ServiceInfo.port指定rpc服务对应端口;
2)其次,取ServiceInstance.metadata的dubbo.endpoints对应端口,这个是对端写在pod注解里的;
3)兜底,才会走ServiceInstance自身的port,即k8s Service定义的targetPort;
总结
服务注册
服务注册流程分为以下几步:
1)RegistryProtocol#export:
rpc服务暴露,服务信息存储到内存MetadataInfo;
2)MetadataServiceNameMapping#map:
构建rpc服务到应用名的映射关系,发布到元数据中心;
这里仅考虑k8s做注册中心,没有元数据中心的情况下,这步跳过;
3)DefaultApplicationDeployer#exportMetadataService:
暴露内置rpc服务MetadataService,供consumer获取MetadataInfo;
4)DefaultApplicationDeployer#registerServiceInstance:
注册ServiceInstance实例。
在k8s中的实现是,发布MetadataInfo,修改pod的annotation。
io.dubbo/metadata中包含revision、元数据rpc服务信息等。
服务发现
在订阅流程中,重点有三点(ServiceDiscoveryRegistry#subscribe):
1)将rpc服务映射到应用;
2)查询应用实例;
3)订阅k8s资源变更;
在k8s中需要用户指定rpc服务和应用的映射关系,主要有两种方式:
1)在Reference层面指定provided-by,比如 @DubboReference(providedBy = "a");
2)在Registry层面指定subscribed-services,比如:kubernetes://apiserver?subscribed-services=a,b,c,d;
查询应用实例流程:
1)查询应用名对应Endpoint,即k8s Service下的Endpoint;
2)查询Service定义中的Selector;
3)根据Selector查询所有Pod;
4)统计Service定义下所有targetPort,根据Pod+targetPort纬度创建ServiceInstance;
订阅k8s资源变更:
1)Endpoint:监听增/删/改,重新走查询应用实例流程;
2)Pod:监听改,主要是Pod的annotation变更,重新走查询应用实例流程;
3)Service:监听改,注销原Pod监听,重新走pod监听;
关于Service的targetPort:
一般情况下,Service的targetPort不会作为最终rpc调用的port使用。
在构造providerUrls时,选择rpc端口:
1)优先,取对端元数据MetadataInfo.ServiceInfo.port指定rpc服务对应端口;
2)其次,取ServiceInstance.metadata的dubbo.endpoints对应端口,这个是对端写在pod注解里的;
3)兜底,才会走ServiceInstance自身的port,即k8s Service定义的targetPort;
探针
k8s中消费者感知到提供者上线,是基于提供者的Endpoint是否挂到Service之下。
而只有通过启动探针和就绪探针检测,提供者的Endpoint才会暴露,接收到Service流量。
dubbo-qos模块提供了几个探针的默认实现,同一个类型的探针,需要全部校验通过,才算通过。
启动探针 DeployerStartupProbe:
有一个ModuleDeployer正在运行返回true。
这是一个早期阶段,ApplicationDeployer已经准备完成(比如连接配置中心、元数据中心等),但是ModuleDeployer还未执行服务暴露/引用。
就绪探针DeployerReadinessProbe:
应用级别,所有ModuleDeployer启动完成返回true。
这可以认为在上述流程第三步前,ModuleDeployer就启动完成了,Endpoint会被暴露。
此时还未暴露MetadataService,也未修改Pod的annotation,但是consumer会过滤这些annotation为空的Pod。
就绪探针ProviderReadinessProbe:
Rpc服务级别,只要不是所有rpc服务都下线,就返回true。
所谓rpc服务下线,即ProviderModel.RegisterStatedURL#isRegistered=false。
这个探针主要是配合qos命令offline/online,手动上下线rpc服务使用。
欢迎大家评论或私信讨论问题。
本文原创,未经许可不得转载。
欢迎关注公众号【程序猿阿越】。