写点什么

spring-cloud-kubernetes 背后的三个关键知识点

作者:程序员欣宸
  • 2022 年 4 月 18 日
  • 本文字数:5227 字

    阅读完需:约 17 分钟

spring-cloud-kubernetes背后的三个关键知识点

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 在前文《你好spring-cloud-kubernetes》一文中,对 spring-cloud-kubernetes 这个 SpringCloud 官方 kubernetes 服务框架有了基本了解,今天来小结此框架涉及的关键技术,为后面的深入学习做准备;

概览

  • 总结下来有三个关键知识点需要深入理解:

  1. DiscoveryClient 是个接口,对应的实现类是哪个?

  2. discoveryClient.getServices()方法取得了 kubernetes 的 service 信息,这背后的机制是什么?java 应用是怎样取得所在 kubernetes 的服务信息的?

  3. kubernetes 的 service 信息存在哪里?如何将这些信息给出去?

  • 接下来我们逐一分析每个知识点;

DiscoveryClient 接口的实现类实例从何而来

  • 先来回顾一下上一章的 DiscoveryController.java 的内容:


@RestControllerpublic class DiscoveryController {
@Autowired private DiscoveryClient discoveryClient;
/** * 探针检查响应类 * @return */ @RequestMapping("/health") public String health() { return "health"; }
/** * 返回远程调用的结果 * @return */ @RequestMapping("/getservicedetail") public String getUri( @RequestParam(value = "servicename", defaultValue = "") String servicename) { return "Service [" + servicename + "]'s instance list : " + JSON.toJSONString(discoveryClient.getInstances(servicename)); }
/** * 返回发现的所有服务 * @return */ @RequestMapping("/services") public String services() { return this.discoveryClient.getServices().toString() + ", " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); }}
复制代码


  • 上述代码中,我们并没有写创建 DiscoveryClient 实例的代码,discoveryClient 从何而来?

  • 这一切,要从 DiscoveryController.java 所在项目的 pom.xml 说起;

  1. 在 pom.xml 中,有对 spring-cloud-kubernetes 框架的依赖配置:

<dependency>  <groupId>org.springframework.cloud</groupId>  <artifactId>spring-cloud-kubernetes-discovery</artifactId>  <version>1.0.1.RELEASE</version></dependency>
复制代码
  1. 打开 spring-cloud-kubernetes-discovery 的源码,地址是:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-discovery ,在这个工程中发现了文件 spring.factories:

  2. spring 容器启动时,会寻找 classpath 下所有 spring.factories 文件(包括 jar 文件中的),spring.factories 中配置的所有类都会实例化,我们在开发 springboot 时常用到的 XXX-starter.jar 就用到了这个技术,效果是一旦依赖了某个 starter.jar 很多功能就在 spring 初始化时候自动执行了(例如 mysql 的 starter,启动时会连接数据库),关于此技术的详情,请参考以下三篇文章:《自定义 spring boot starter 三部曲之一:准备工作》、《自定义 spring boot starter 三部曲之二:实战开发》、《自定义 spring boot starter 三部曲之三:源码分析 spring.factories 加载过程》

  3. spring.factories 文件中有两个类:KubernetesDiscoveryClientAutoConfiguration 和 KubernetesDiscoveryClientConfigClientBootstrapConfiguration 都会被实例化;

  4. 先看 KubernetesDiscoveryClientConfigClientBootstrapConfiguration,很简单的源码,KubernetesAutoConfiguration 和 KubernetesDiscoveryClientAutoConfiguration 这两个类会被实例化:

/** * Bootstrap config for Kubernetes discovery config client. * * @author Zhanwei Wang */@Configuration@ConditionalOnProperty("spring.cloud.config.discovery.enabled")@Import({ KubernetesAutoConfiguration.class,        KubernetesDiscoveryClientAutoConfiguration.class })public class KubernetesDiscoveryClientConfigClientBootstrapConfiguration {
}
复制代码


  1. 在 KubernetesAutoConfiguration 的源码中,会实例化一个重要的类:DefaultKubernetesClient,如下:

@Bean@ConditionalOnMissingBeanpublic KubernetesClient kubernetesClient(Config config) {    return new DefaultKubernetesClient(config);}
复制代码


  1. 再看 KubernetesDiscoveryClientAutoConfiguration 源码,注意 kubernetesDiscoveryClient 方法,这里面实例化了 DiscoveryController 所需的 DiscoveryClient 接口实现,还要重点关注的地方是 KubernetesClient 参数的值,是上面提到的 DefaultKubernetesClient 对象:

@Bean@ConditionalOnMissingBean@ConditionalOnProperty(name = "spring.cloud.kubernetes.discovery.enabled", matchIfMissing = true)public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client,            KubernetesDiscoveryProperties properties,            KubernetesClientServicesFunction kubernetesClientServicesFunction,            DefaultIsServicePortSecureResolver isServicePortSecureResolver) {  return new KubernetesDiscoveryClient(client, properties,                                       kubernetesClientServicesFunction, isServicePortSecureResolver);}
复制代码


  1. 至此,第一个问题算是弄清楚了:我们编写的 DiscoveryController 类所需的 DiscoveryClient 接口实现类是 KubernetesDiscoveryClient,用到的是 spring 规范中的 spring.factories

  2. 另外有一点很重要,下面要用到的:KubernetesDiscoveryClient 有个成员变量是 KubernetesClient,该变量的值是 DefaultKubernetesClient 实例;

  • 接下来看第二个问题;

java 应用怎么能取得所在 kubernetes 的服务信息

  1. 看看 DiscoveryController 是如何获取所在 kubernetes 的服务信息的:

@RequestMapping("/services")    public String services() {        return this.discoveryClient.getServices().toString()                + ", "                + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());    }
复制代码


如上所示,discoveryClient.getServices()方法返回了所有 kubernetes 的服务信息;

2. discoveryClient 对应的类是 spring-cloud-kubernetes 项目的 KubernetesDiscoveryClient.java,看方法:

public List<String> getServices(Predicate<Service> filter) {        return this.kubernetesClientServicesFunction.apply(this.client).list().getItems()                .stream().filter(filter).map(s -> s.getMetadata().getName())                .collect(Collectors.toList());    }
复制代码


  • 这段代码的关键在于 this.kubernetesClientServicesFunction.apply(this.client).list(),先看 KubernetesClientServicesFunction 实例的初始化过程,在 KubernetesDiscoveryClientAutoConfiguration 类中:

@Beanpublic KubernetesClientServicesFunction servicesFunction(            KubernetesDiscoveryProperties properties) {  if (properties.getServiceLabels().isEmpty()) {    return KubernetesClient::services;  }
return (client) -> client.services().withLabels(properties.getServiceLabels());}
复制代码


  • KubernetesClientServicesFunction 是个 lambda 表达式,用于 KubernetesClient 的时候,返回 KubernetesClient.services()的结果,如果指定了标签过滤,就用指定的标签来做过滤(也就是 kubernetes 中的标签选择器的效果)

  • 因此,数据来源其实就是上面的 this.client,调用其 services 方法的返回结果;

  1. KubernetesDiscoveryClient.getServices 方法中的 this.client 是什么呢?分析前面的问题时已经提到过了,就是 DefaultKubernetesClient 类的实例,所以,此时要去看 DefaultKubernetesClient.services 方法,发现 client 是 ServiceOperationsImpl 实例:

@Override  public MixedOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> services() {    return new ServiceOperationsImpl(httpClient, getConfiguration(), getNamespace());  }
复制代码


  1. 接着看 ServiceOperationsImpl.java,我们关心的是它的 list 方法,此方法在父类 BaseOperation 中找到:

public L list() throws KubernetesClientException {    try {      HttpUrl.Builder requestUrlBuilder = HttpUrl.get(getNamespacedUrl()).newBuilder();
String labelQueryParam = getLabelQueryParam(); if (Utils.isNotNullOrEmpty(labelQueryParam)) { requestUrlBuilder.addQueryParameter("labelSelector", labelQueryParam); }
String fieldQueryString = getFieldQueryParam(); if (Utils.isNotNullOrEmpty(fieldQueryString)) { requestUrlBuilder.addQueryParameter("fieldSelector", fieldQueryString); }
Request.Builder requestBuilder = new Request.Builder().get().url(requestUrlBuilder.build()); L answer = handleResponse(requestBuilder, listType); updateApiVersion(answer); return answer; } catch (InterruptedException | ExecutionException | IOException e) { throw KubernetesClientException.launderThrowable(forOperationType("list"), e); } }
复制代码


  • 展开上面代码的 handleResponse 方法,可见里面是一次 http 请求,至于请求的地址,可以展开 getNamespacedUrl()方法,里面调用的 getRootUrl 方法如下:

public URL getRootUrl() {    try {      if (apiGroup != null) {        return new URL(URLUtils.join(config.getMasterUrl().toString(), "apis", apiGroup, apiVersion));      }      return new URL(URLUtils.join(config.getMasterUrl().toString(), "api", apiVersion));    } catch (MalformedURLException e) {      throw KubernetesClientException.launderThrowable(e);    }  }
复制代码


  • 可见最终的地址应该是:xxxxxx/api/v1 或者 xxxxxx/apis/xx/v1 这样的字符串。

  • 这样的字符串意味着什么呢?这是访问 kubernetes 的 API Server 时用到的 URL 标准格式,有关 API Server 服务的详情请参考官方文档,地址是:https://kubernetes.io/docs/reference/using-api/api-concepts/

  • 如下图,用 OperationSupport 类的源码和官方文档的 URL 截图做个对比,大家就一目了然了:

  1. 还剩个小问题,上图中,OperationSupport 类的成员变量 resourceT 是什么值?官方文档示例中是"pods",在获取 service 的时候又该是多少呢?顺着源码一路找下去,找到了类的构造方法,如下所示,第五个参数就是 resourceT,这里直接被写死为"services":

public ServiceOperationsImpl(OkHttpClient client, Config config, String apiVersion, String namespace, String name, Boolean cascading, Service item, String resourceVersion, Boolean reloadingFromServer, long gracePeriodSeconds, Map<String, String> labels, Map<String, String> labelsNot, Map<String, String[]> labelsIn, Map<String, String[]> labelsNotIn, Map<String, String> fields) {    super(client, config, null, apiVersion, "services", namespace, name, cascading, item, resourceVersion, reloadingFromServer, gracePeriodSeconds, labels, labelsNot, labelsIn, labelsNotIn, fields);  }
复制代码


  • 至此,第二个问题“controller 中用到的 kubernetes 服务数据从何而来"已经清楚了:最终是调用 okhttp 的 newCall 方法向 kubernetes 的 API Server 发起 http 请求,获取 service 资源的数据列表;

  • 接下来,该最后一个问题了;

API Server 收到请求后做了什么?

  • 关于 API Server 如何响应各类 http 请求,本文只做一些简单的说明,详细信息还请参考官方文档,地址是:https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/

  • 如下图所示,在 kubernetes 环境中,pod、service 这些资源的数据都存储在 etcd,任何服务想要增删改查 etcd 的数据,都只能通过向 API Server 发起 RestFul 请求的方式来完成,咱们的 DiscoveryController 类获取所有 service 也是发请求到 API Server,由 API Server 从 etcd 中取得 service 的数据返回给 DiscoveryController:

  • 如果您想弄清楚 service 数据在 etcd 中如何存储的,可以参考《查看 k8s 的 etcd 数据》一文,亲自动手连接 etcd 查看里面的 service 内容;

  • 至此,spring-cloud-kubernetes 背后的三个关键知识点都已经学习了,下图算是对这些问题的一个小结:


  • 希望以上的分析总结能对您有参考作用,由于对基本原理都已经了解,后面的 spring-cloud-kubernetes 实战可以更顺畅,也能从原理出发继续深入的分析和学习。

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

发布于: 2022 年 04 月 18 日阅读数: 21
用户头像

搜索"程序员欣宸",一起畅游Java宇宙 2018.04.19 加入

前腾讯、前阿里员工,从事Java后台工作,对Docker和Kubernetes充满热爱,所有文章均为作者原创,个人Github:https://github.com/zq2599/blog_demos

评论

发布
暂无评论
spring-cloud-kubernetes背后的三个关键知识点_java_程序员欣宸_InfoQ写作平台