Spring Cloud Kubernetes服务注册与发现实现原理与源码分析

727 阅读7分钟

博客搬家-原文链接

前面我们已经分析完OpenFeignRibbon的源码,包括两者的整合使用,以及Ribbon的重试机制,从最顶层调用接口开始到负载均衡的实现。今天我们分析更底层的实现,即服务注册与发现。

本篇内容包含:

  • Spring Cloud Commonsserviceregistrydiscovery
  • Spring Cloud Kubernetes服务注册与发现实现原理
  • Spring Cloud Kubernetes Core源码分析
  • Spring Cloud Kubernetes Discovery源码分析

使用Spring Cloud Kubernetes搭建的demo级微服务项目sck-demoGithub地址: github.com/wujiuye/sha…

Spring Cloud Commons的serviceregistry与discovery

Spring Cloud CommonsSpring Cloud的核心组件,提供各种接口,其实就是通过定义接口来定义规范,方便将各种框架整合到Spring Cloud微服务项目中,这些功能或必须或可选。

例如,通过实现Spring Cloud定义的服务注册和发现接口,我们可以自己实现服务注册和发现以便接入新鲜主流的服务注册中心,如NacosApollo

Spring Cloud Commons只定义接口,不提供实现。我们前面分析Ribbon源码时就接触了其中一个接口,负载均衡接口LoadBalancerClient,由spring-cloud-netflix-ribbon提供接口的实现。

Spring Cloud Commonsserviceregistry包与discovery包分别定义服务注册接口和服务发现接口:

  • 服务注册接口ServiceRegistry:定义注册方法和注销方法;
  • 服务发现接口DiscoveryClient:定义获取所有服务ID方法以及根据服务ID获取所有实例(节点)方法。

Spring Cloud Kubernetes服务注册与发现实现原理

sck-demo项目搭建之初,我们是跟着官方提供的demo去实现服务注册和发现的,也就是在每个服务的Application类上添加一个@EnableDiscoveryClient注解,并且我们也并未配置Kubernetes的地址,但我们使用DiscoveryClient确能获取到服务,要解开这些疑惑我们需要了解Kubernetes,以及了解Spring Cloud Kubernetes Discovery的源码。

前面我们在分析Ribbon的源码时也了解到,Ribbon并非通过DiscoveryClient去获取服务提供者的,Ribbon通过提供一个ServerList接口让使用者自己去实现来完成Ribbon的服务发现,Spring Cloud Kubernetes Ribbon的作用就是实现RibbonServerList接口,从Kubernetes获取可用的服务提供者,Ribbon定时调用ServerList更新自身缓存的服务提供者列表,默认30秒更新一次。

实际上,Spring Cloud Kubernetes Ribbon也并未使用到Spring Cloud Kubernetes Discovery提供的DiscoveryClient接口的实现来获取服务列表,而是直接从Kubernetes中获取,正是因为如此,笔者尝试去掉@EnableDiscoveryClient注解后,以及去掉Spring Cloud Kubernetes Discovery的依赖后,项目依然能正常运作。

需要注意,Spring Cloud Kubernetes Ribbon依赖Spring Cloud Kubernetes Core,如果去掉Spring Cloud Kubernetes Discovery,可能就要自动手动添加Spring Cloud Kubernetes Core的依赖,否则服务启动失败。

Spring Cloud Kubernetes Discovery实现DiscoveryClient接口只是能够让我们通过DiscoveryClient获取服务提供者, Spring Cloud Kubernetes Discovery实现的服务注册接口也并非真正的去注册服务,可以这么说,在Spring Cloud Kubernetes项目中Spring Cloud Kubernetes Discovery是一个多余的存在。

如果去掉Spring Cloud Kubernetes Discovery后,我们想要获取某个服务的当前可用服务提供者怎么获取呢?我们可以通过使用RibbonServerList去获取,由Spring Cloud Kubernetes Ribbon实现。

我们来做个实验:

第一步:排除spring-cloud-kubernetes-discovery依赖,并且注释掉@EnableDiscoveryClient注解;

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-kubernetes-discovery</artifactId>
        </exclusion>
    </exclusions>
</dependency>

第二步:从ServerList获取某个服务的当前可用服务提供者;

@RestController
@Slf4j
@RequestMapping
public class DemoTestController {

    @Resource
    private SpringClientFactory factory;

    @GetMapping("/ServerList")
    public Object test3() {
        return factory.getInstance(ProviderConstant.SERVICE_NAME, ServerList.class)
                .getUpdatedListOfServers();
    }

}

想要取得ServerList就必须要通过SpringClientFactory去拿,通过学习前面两篇Ribbon源码分析的文章我们了解到,Ribbon会为每个Client创建一个ApplicationContext,目的是实现环境隔离,让我们能够为调用每个服务提供者使用不同的配置。因此,我们需要通过SpringClientFactory拿到对应ClientApplicationContext,再从该ApplicationContext中获取ServerList,最后再调用ServerListgetUpdatedListOfServers方法从注册中心获取当前可用的服务实例列表。

测试结果如下图所示。

那么问题来了,服务是怎么注册到注册中心的,以及Spring Cloud Kubernetes RibbonSpring Cloud Kubernetes Discovery又是怎么从注册中心拿到服务提供者列表的?

第一个问题:服务怎么注册到注册中心的?

不需要注册,使用Spring Cloud Kubernetes服务不需要注册,当Pod启动起来时,就已经“注册”到Kubernetesetcd了,这便是使用Spring Cloud Kubernetes的好处,直接使用Kubernetes的原生服务实现服务发现和注册。

第二个问题:怎么从注册中心拿到服务提供者列表的?

我们从minikube(使用minikube搭建的本地单节点Kubernetes集群)的dashboard就能找到服务的节点信息,如下图所示。

这些数据都是存储在Kubernetesetcd服务上的,Spring Cloud Kubernetes RibbonSpring Cloud Kubernetes Discovery都是通过KubernetesClientKubernetes交互,调用KubernetesAPI读取服务信息的。

  • Spring Cloud Kubernetes Ribbon的实现(以KubernetesEndpointsServerList为例)
public class KubernetesEndpointsServerList extends KubernetesServerList {

	KubernetesEndpointsServerList(KubernetesClient client,
			KubernetesRibbonProperties properties) {
		super(client, properties);
	}

	@Override
	public List<Server> getUpdatedListOfServers() {
		List<Server> result = new ArrayList<>();
        // this.getClient().endpoints().withName(this.getServiceId()).get()
		Endpoints endpoints = StringUtils.isNotBlank(this.getNamespace())
				? this.getClient().endpoints().inNamespace(this.getNamespace())
						.withName(this.getServiceId()).get()
				: this.getClient().endpoints().withName(this.getServiceId()).get();
        // ....
	}
}
  • Spring Cloud Kubernetes Discovery的实现
public class KubernetesDiscoveryClient implements DiscoveryClient {

	private final KubernetesDiscoveryProperties properties;
	
	private KubernetesClient client;

    //......

	@Override
	public List<ServiceInstance> getInstances(String serviceId) {
        // this.client.endpoints().withName(serviceId).get()
		List<Endpoints> endpointsList = this.properties.isAllNamespaces()
				? this.client.endpoints().inAnyNamespace()
						.withField("metadata.name", serviceId).list().getItems()
				: Collections
						.singletonList(this.client.endpoints().withName(serviceId).get());
        // ......
		return instances;
	}
}

所以两者都是调用KubernetesClientendpoints方法获取某个服务的节点信息,想要了解KubernetesClient是怎么获取服务节点信息的,我们并不需要去看KubernetesClient的源码,也没必要,KubernetesClient不过是封装了http请求。想要了解Kubernetes提供的API可以看下这个文档:kubernetes.io/docs/refere…

对应本例的API如下图所示。

想要试一下API?本地使用kubectl proxy命令可运行一个Kubernetes API代理服务。

例如:

MacBook-Pro:sck-demo wjy$ kubectl proxy --port=8004
Starting to serve on 127.0.0.1:8004

使用kubectl proxy --port=8004开启Kubernetes API代理服务,监听请求的端口为8004,启动成功后我们就可以使用127.0.0.1:8004访问KubernetesAPI了。

例如获取sck-demo-provider这个服务的所有endpoints,在浏览器输入:

127.0.0.1:8004/api/v1/namespaces/default/endpoints/sck-demo-provider

其中default为名称空间,sck-demo-provider为服务名。响应结果如下图所示。

相当于使用kubectl get endpoints [服务名]命令。

例如:

MacBook-Pro:sck-demo wjy$ kubectl get endpoints sck-demo-provider
NAME                ENDPOINTS                                         AGE
sck-demo-provider   172.17.0.2:8080,172.17.0.4:8080,172.17.0.6:8080   8d

EndpointsPod是什么关系,Endpoints是个什么概念,我们知道PodKubernetes调度的最小单位,一个Pod中可以有运行多个Docker容器,即运行多个应用,但一般只会运行一个容器,那么EndpointsPod的关系是什么?

EndpointsKubernetes集群中的一个资源对象,存储在etcd中,用来记录一个Service对应的所有Pod的访问地址。例如,Kubernetes集群中创建一个名为sck-demo-providerService,就会生成一个同名的Endpoint对象,Endpoint就是Service关联的PodIP地址和端口(Service配置selector,否则不会生成Endpoint对象)。

Spring Cloud Kubernetes Core源码分析

该模块是Spring Cloud Kubernetes项目的核心模块,Spring Cloud Kubernetes的其它模块都依赖它实现与Kubernetes交互。

Spring Cloud Kubernetes Core主要实现自动配置KubernetesClientKubernetesClient封装与Kubernetes交互的API请求。调用KubernetesClient的方法就是向KubernetesAPI服务发起一个请求。

该模块的spring.factories文件配置了两个配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.kubernetes.KubernetesAutoConfiguration\

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.kubernetes.profile.KubernetesProfileEnvironmentPostProcessor
  • KubernetesAutoConfiguration

读取前缀为spring.cloud.kubernetes.client的配置,根据配置创建KubernetesClient

你是否有疑问,为什么我们在sck-demo项目中什么也没有配置(kubernetes相关的),却能启动服务,且能调用kubernetesAPI

当我们不配置KubernetesMasterUrl时,默认使用的是:https://kubernetes.default.svc,默认API版本为V1,默认不配置名称空间。支持配置的参数可阅读KubernetesClientProperties或者io.fabric8.kubernetes.client.Config的源码。

Spring Cloud Kubernetes Core还实现spring-boot-actuate健康监控包的HealthIndicator接口,KubernetesHealthIndicator实现HealthIndicator接口的health方法,获取当前Pod信息,如果获取到说明服务正常。KubernetesHealthIndicatorKubernetesAutoConfiguration中实例化并注册到Spring容器。

  • KubernetesProfileEnvironmentPostProcessor

这是一个EnvironmentPostProcessorKubernetesProfileEnvironmentPostProcessor的作用是拦截Environment的初始化,如果当前应用是运行在容器内,则会调用EnvironmentaddActiveProfile追加"kubernetes",服务启动时会打印如下日记:

INFO  c.w.s.p.SckProviderApplication - The following profiles are active: kubernetes,dev

Spring Cloud Kubernetes Discovery源码分析

该模块实现Spring Cloud的服务发现接口DiscoveryClient,实现类为KubernetesDiscoveryClientKubernetesDiscoveryClient使用KubernetesClient请求Kubernetes API获取Endpoints

同时也实现了服务注册接口ServiceRegistry,有发现就有注册,ServiceRegistry的实现类为KubernetesServiceRegistry,该类实现接口的注册、注销方法只打印日记,什么也不做。

public class KubernetesServiceRegistry
		implements ServiceRegistry<KubernetesRegistration> {

	private static final Log log = LogFactory.getLog(KubernetesServiceRegistry.class);

	@Override
	public void register(KubernetesRegistration registration) {
		log.info("Registering : " + registration);
	}

	@Override
	public void deregister(KubernetesRegistration registration) {
		log.info("DeRegistering : " + registration);
	}

	//.......
}

所以,Spring Cloud Kubernetes Discovery模块没什么好分析的。下一篇分析Spring Cloud Kubernetes的动态配置。