likes
comments
collection
share

Dubbo3源码(四)K8S注册发现

作者站长头像
站长
· 阅读数 22

前言

早在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。

Dubbo3源码(四)K8S注册发现

消费者镜像,设置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。

Dubbo3源码(四)K8S注册发现

可以拿到正常响应。

Dubbo3源码(四)K8S注册发现

二、服务注册

注册流程回顾

step1:RegistryProtocol#export

调用底层rpc协议暴露rpc服务,比如TripleProtocol#export;

ServiceDiscoveryRegistry委派对应ServiceDiscovery,将url存储到MetadataInfo管理。

Dubbo3源码(四)K8S注册发现

step2:MetadataServiceNameMapping#map

构建rpc服务到应用名的映射关系,发布到元数据中心(2.7.5的时候就是配置中心)。

Dubbo3源码(四)K8S注册发现

注意:当注册中心为nacos和zk时,自动识别元数据中心,其他仅支持redis作为元数据中心。所以用k8s元数据中心在哪,consumer如何获取rpc服务和应用名的映射关系

step3:DefaultApplicationDeployer#exportMetadataService

暴露MetadataService服务。

Dubbo3源码(四)K8S注册发现

step4:DefaultApplicationDeployer#registerServiceInstance

调用ServiceDiscovery注册应用实例。

Dubbo3源码(四)K8S注册发现

AbstractServiceDiscovery#register:

发布MetadataInfo,供消费者通过rpc调用获取;

子类执行注册逻辑,比如zk注册ServiceInstance对应临时znode。

Dubbo3源码(四)K8S注册发现

注册

无论是提供者还是消费者,都需要创建KubernetesServiceDiscovery用于注册和发现。

KubernetesServiceDiscovery构造阶段会连接apiserver,校验当前pod存在。

即:GET /api/v1/namespaces/{namespace}/pods/{podName}。

namespace:通过注册中心url获取。

podName:通过环境变量中的HOSTNAME获取。

Dubbo3源码(四)K8S注册发现

KubernetesServiceDiscovery#doRegister

服务注册整体流程的最后一步,注册ServiceInstance阶段,k8s注册仅仅修改pod的annotation

普通注册中心注册应用实例,比如zk注册临时znode对consumer可见。

Dubbo3源码(四)K8S注册发现

io.dubbo/metadata包含一些关键的信息,比如revisionRpc服务MetadataService信息

这部分和其他注册中心一致,zk会保存在/services/{应用名}/{实例地址}这个znode的数据中。

Dubbo3源码(四)K8S注册发现

探针

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。

Dubbo3源码(四)K8S注册发现

默认Dubbo只提供了一个StartupProbe。

DeployerStartupProbe:如果有一个ModuleDeployer正在运行,就返回true。

Dubbo3源码(四)K8S注册发现

这其实是一个非常早期的阶段,ApplicationDeployer已经准备完成(比如连接配置中心、元数据中心等),但是ModuleDeployer还未执行服务暴露/引用。

DefaultModuleDeployer#startSync:

Dubbo3源码(四)K8S注册发现

ReadinessProbe就绪探针

当启动探针检测通过,即ApplicationDeployer初始化完成,ModuleDeployer正在运行,进入就绪探针探测。

如下配置Deployment:初始延迟5秒,后续每隔5秒向22222端口发送GET/ready请求。

只有当readinessProbe正常返回,才会暴露Endpoint。

readinessProbe:
  httpGet:
    path: /ready
    port: 22222
  initialDelaySeconds: 5
  periodSeconds: 5

Ready#execute

所有ReadinessProbe检测通过,应用才进入就绪状态。

Dubbo3源码(四)K8S注册发现

dubbo提供了两个ReadinessProbe。

DeployerReadinessProbe

所有ModuleDeployer启动完成返回true。

Dubbo3源码(四)K8S注册发现

一般情况下,如果一个ModuleDeployer处于started状态,会紧接着暴露MetadataService并修改pod的annotation。

但是这里会不会导致在未暴露MetadataService的情况下,收到consumer查询元数据的情况?

(consumer订阅的地方再看)

ProviderReadinessProbe

DeployerReadinessProbe探针更多的是应用纬度的检测,

ProviderReadinessProbe是rpc服务级别的检测。

通过条件有两种:

1)当前应用没有提供任何rpc服务;

2)部分或全部rpc服务已经暴露;

反过来说,只要不是所有rpc服务都下线,就返回true。

Dubbo3源码(四)K8S注册发现

为什么会有这个探针,主要是支持通过qos控制rpc服务上下线。

BaseOffline#doUnexport:

Dubbo3源码(四)K8S注册发现

不传参的情况下,请求22222端口offline端点,直接下线所有rpc服务,container的就绪探针检测返回失败。

Dubbo3源码(四)K8S注册发现

导致endpoint列表中68机器被下线。

Dubbo3源码(四)K8S注册发现

请求online端点,所有rpc服务又会重新上线。

LivenessProbe存活探针

当启动探针检测通过,即ApplicationDeployer初始化完成,ModuleDeployer正在运行,进入存活探针探测。

比如下面的配置:

延迟5秒后,每隔5秒请求22222端口的live端点,如果超过3次失败,根据restartPolicy处理。

livenessProbe:
  httpGet:
    path: /live
    port: 22222
  initialDelaySeconds: 5
  periodSeconds: 5

Live#execute:所有LivenessProbe检测通过,应用存活。

Dubbo3源码(四)K8S注册发现

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,应用级别订阅:

Dubbo3源码(四)K8S注册发现

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。

Dubbo3源码(四)K8S注册发现

AbstractServiceNameMapping#getAndListen

优先从元数据中心获取,比如zk、nacos、redis,单独k8s是不支持的。

其次从注册中心url上的subscribed-services参数获取,这种配置方式作用于未配置providedBy的所有Reference。

比如:kubernetes://apiserver?subscribed-services=a,b,c,d。

Dubbo3源码(四)K8S注册发现

所以在不部署元数据中心的情况下,接入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变更;Dubbo3源码(四)K8S注册发现

通知Directory部分不再赘述,和2.7.5大致相同。

就是按照revision分组,挑一个ServiceInstance查询MetadataService。

然后组装providerUtls,通知Directory。

Directory调用rpc协议构造底层通讯Invoker。

接下来主要看k8s在构造ServiceInstance和订阅上的特点。

查询ServiceInstance

KubernetesServiceDiscovery#getInstances

查询service下的endpoint。

Dubbo3源码(四)K8S注册发现

KubernetesServiceDiscovery#toServiceInstance

1)根据Service上的Selector,找所有pod。

Dubbo3源码(四)K8S注册发现

2)统计Endpoint下所有端口,即Service定义的targetPort。

Dubbo3源码(四)K8S注册发现

3)根据pod+端口纬度,构建ServiceInstance。

这里将提供者在pod上写入的io.dubbo/metadata注解反序列化到ServiceInstance.metadata。

注意:如果拿不到这个注解,consumer最终不会将这个pod作为ServiceInstance放到最后的结果集里,这也是为什么DeployerReadinessProbe没有问题。

Dubbo3源码(四)K8S注册发现

资源监听

KubernetesServiceDiscovery#addServiceInstancesChangedListener

对于k8s来说,需要监听多个资源,包括Endpoint、Pod、Service。

Dubbo3源码(四)K8S注册发现

Endpoint无论增删改都需要监听。

感知provider上下线(就绪探针),重点就是监听Endpoint变更。

当发生变更,根据Endpoint和Service需要重新构建ServiceInstance列表,通知Directory。

Dubbo3源码(四)K8S注册发现

Pod只监听更新。

比较重要的是,provider在启动的最后阶段,暴露元数据服务之后,会修改pod的注解。

此时会触发consumer感知到pod更新,consumer会重新查询service下所有instance,通知Directory。

Dubbo3源码(四)K8S注册发现

Service也只监听更新。

主要场景是Service的selector指向变更。

需要销毁原有Service下的Pod监听,重新触发watchPods。

Dubbo3源码(四)K8S注册发现

Service目标端口

关于部署时Service目标端口的一个问题。

我部署的时候端口配的是triple协议默认端口50051,而实际代码用的是dubbo协议的20880。

运行起来却也没有什么问题。

Dubbo3源码(四)K8S注册发现

这个targetPort一般不会被用到最后rpc调用的通讯上。

ServiceInstancesChangedListener#getServiceUrlsCache

在获取完revision对应元数据后,构造providerUrl时,对于port有一个逻辑。

1)优先,取对端元数据MetadataInfo.ServiceInfo.port指定rpc服务对应端口;

2)其次,取ServiceInstance.metadata的dubbo.endpoints对应端口,这个是对端写在pod注解里的;

3)兜底,才会走ServiceInstance自身的port,即k8s Service定义的targetPort;

Dubbo3源码(四)K8S注册发现

总结

服务注册

Dubbo3源码(四)K8S注册发现

服务注册流程分为以下几步:

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服务信息等。

Dubbo3源码(四)K8S注册发现

服务发现

Dubbo3源码(四)K8S注册发现

在订阅流程中,重点有三点(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模块提供了几个探针的默认实现,同一个类型的探针,需要全部校验通过,才算通过。

Dubbo3源码(四)K8S注册发现

启动探针 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服务使用。

欢迎大家评论或私信讨论问题。

本文原创,未经许可不得转载。

欢迎关注公众号【程序猿阿越】。