在《你好 spring-cloud-kubernetes》一文中, 对 spring-cloud-kubernetes 这个 SpringCloud 官方 kubernetes 服务框架有了基本了解, 今天来小结此框架涉及的关键技术, 为后面的深入学习做准备;
系列文章列表
本文是《spring-cloud-kubernetes 实战系列》的第三篇, 全文链接如下:
《spring-cloud-kubernetes 官方 demo 运行实战》
《你好 spring-cloud-kubernetes》
《spring-cloud-kubernetes 背后的三个关键知识点》
《spring-cloud-kubernetes 的服务发现和轮询实战 (含熔断)》
《spring-cloud-kubernetes 与 SpringCloud Gateway》
《spring-cloud-kubernetes 与 k8s 的 configmap》
概览
总结下来有三个关键知识点需要深入理解:
DiscoveryClient 是个接口, 对应的实现类是哪个?
discoveryClient.getServices() 方法取得了 kubernetes 的 service 信息, 这背后的机制是什么? java 应用是怎样取得所在 kubernetes 的服务信息的?
kubernetes 的 service 信息存在哪里? 如何将这些信息给出去?
接下来我们逐一分析每个知识点;
DiscoveryClient 接口的实现类实例从何而来
先来回顾一下上一章的 DiscoveryController.java 的内容:
- @RestController
- public 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 说起;
在 pom.xml 中, 有对 spring-cloud-kubernetes 框架的依赖配置:
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-kubernetes-discovery</artifactId>
- <version>1.0.1.RELEASE</version>
- </dependency>
打开 spring-cloud-kubernetes-discovery 的源码, 地址是: https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-discovery , 在这个工程中发现了文件 spring.factories:
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 加载过程》
spring.factories 文件中有两个类: KubernetesDiscoveryClientAutoConfiguration 和 KubernetesDiscoveryClientConfigClientBootstrapConfiguration 都会被实例化;
先看 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 {
- }
在 KubernetesAutoConfiguration 的源码中, 会实例化一个重要的类: DefaultKubernetesClient, 如下:
- @Bean
- @ConditionalOnMissingBean
- public KubernetesClient kubernetesClient(Config config) {
- return new DefaultKubernetesClient(config);
- }
再看 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);
- }
至此, 第一个问题算是弄清楚了: 我们编写的 DiscoveryController 类所需的 DiscoveryClient 接口实现类是 KubernetesDiscoveryClient, 用到的是 spring 规范中的 spring.factories
另外有一点很重要, 下面要用到的: KubernetesDiscoveryClient 有个成员变量是 KubernetesClient, 该变量的值是 DefaultKubernetesClient 实例;
接下来看第二个问题;
java 应用怎么能取得所在 kubernetes 的服务信息
看看 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 的服务信息;
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 类中:
- @Bean
- public 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 方法的返回结果;
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());
- }
接着看 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 截图做个对比, 大家就一目了然了:
还剩个小问题, 上图中, 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 实战可以更顺畅, 也能从原理出发继续深入的分析和学习.
来源: https://www.cnblogs.com/bolingcavalry/p/11452643.html