查看原文
其他

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

欣宸 K8S中文社区 2019-12-18

作者:欣宸,前阿里工程师


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

概览

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

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

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

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

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

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

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


  1. @RestController



  2. public class DiscoveryController {




  3. @Autowired



  4. private DiscoveryClient discoveryClient;




  5. /**



  6. * 探针检查响应类



  7. * @return



  8. */



  9. @RequestMapping("/health")



  10. public String health() {



  11. return "health";



  12. }




  13. /**



  14. * 返回远程调用的结果



  15. * @return



  16. */



  17. @RequestMapping("/getservicedetail")



  18. public String getUri(



  19. @RequestParam(value = "servicename", defaultValue = "") String servicename) {



  20. return "Service [" + servicename + "]'s instance list : " + JSON.toJSONString(discoveryClient.getInstances(servicename));



  21. }




  22. /**



  23. * 返回发现的所有服务



  24. * @return



  25. */



  26. @RequestMapping("/services")



  27. public String services() {



  28. return this.discoveryClient.getServices().toString()



  29. + ", "



  30. + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());



  31. }



  32. }


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

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

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



  1. <dependency>



  2. <groupId>org.springframework.cloud</groupId>



  3. <artifactId>spring-cloud-kubernetes-discovery</artifactId>



  4. <version>1.0.1.RELEASE</version>



  5. </dependency>



2、打开spring-cloud-kubernetes-discovery的源码,地址是:https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-discovery ,在这个工程中发现了文件spring.factories:



3、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加载过程》


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


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



  1. /**



  2. * Bootstrap config for Kubernetes discovery config client.



  3. *



  4. * @author Zhanwei Wang



  5. */



  6. @Configuration



  7. @ConditionalOnProperty("spring.cloud.config.discovery.enabled")



  8. @Import({ KubernetesAutoConfiguration.class,



  9. KubernetesDiscoveryClientAutoConfiguration.class })



  10. public class KubernetesDiscoveryClientConfigClientBootstrapConfiguration {




  11. }



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



  1. @Bean



  2. @ConditionalOnMissingBean



  3. public KubernetesClient kubernetesClient(Config config) {



  4. return new DefaultKubernetesClient(config);



  5. }



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



  1. @Bean



  2. @ConditionalOnMissingBean



  3. @ConditionalOnProperty(name = "spring.cloud.kubernetes.discovery.enabled", matchIfMissing = true)



  4. public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client,



  5. KubernetesDiscoveryProperties properties,



  6. KubernetesClientServicesFunction kubernetesClientServicesFunction,



  7. DefaultIsServicePortSecureResolver isServicePortSecureResolver) {



  8. return new KubernetesDiscoveryClient(client, properties,



  9. kubernetesClientServicesFunction, isServicePortSecureResolver);



  10. }



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


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

接下来看第二个问题;

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

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



  1. @RequestMapping("/services")



  2. public String services() {



  3. return this.discoveryClient.getServices().toString()



  4. + ", "



  5. + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());



  6. }


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

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



  1. public List<String> getServices(Predicate<Service> filter) {



  2. return this.kubernetesClientServicesFunction.apply(this.client).list().getItems()



  3. .stream().filter(filter).map(s -> s.getMetadata().getName())



  4. .collect(Collectors.toList());



  5. }


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


  1. @Bean



  2. public KubernetesClientServicesFunction servicesFunction(



  3. KubernetesDiscoveryProperties properties) {



  4. if (properties.getServiceLabels().isEmpty()) {



  5. return KubernetesClient::services;



  6. }




  7. return (client) -> client.services().withLabels(properties.getServiceLabels());



  8. }


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

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

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



  1. @Override



  2. public MixedOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> services() {



  3. return new ServiceOperationsImpl(httpClient, getConfiguration(), getNamespace());



  4. }



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



  1. public L list() throws KubernetesClientException {



  2. try {



  3. HttpUrl.Builder requestUrlBuilder = HttpUrl.get(getNamespacedUrl()).newBuilder();




  4. String labelQueryParam = getLabelQueryParam();



  5. if (Utils.isNotNullOrEmpty(labelQueryParam)) {



  6. requestUrlBuilder.addQueryParameter("labelSelector", labelQueryParam);



  7. }




  8. String fieldQueryString = getFieldQueryParam();



  9. if (Utils.isNotNullOrEmpty(fieldQueryString)) {



  10. requestUrlBuilder.addQueryParameter("fieldSelector", fieldQueryString);



  11. }




  12. Request.Builder requestBuilder = new Request.Builder().get().url(requestUrlBuilder.build());



  13. L answer = handleResponse(requestBuilder, listType);



  14. updateApiVersion(answer);



  15. return answer;



  16. } catch (InterruptedException | ExecutionException | IOException e) {



  17. throw KubernetesClientException.launderThrowable(forOperationType("list"), e);



  18. }



  19. }


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


  1. public URL getRootUrl() {



  2. try {



  3. if (apiGroup != null) {



  4. return new URL(URLUtils.join(config.getMasterUrl().toString(), "apis", apiGroup, apiVersion));



  5. }



  6. return new URL(URLUtils.join(config.getMasterUrl().toString(), "api", apiVersion));



  7. } catch (MalformedURLException e) {



  8. throw KubernetesClientException.launderThrowable(e);



  9. }



  10. }


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

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

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


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



  1. 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) {



  2. super(client, config, null, apiVersion, "services", namespace, name, cascading, item, resourceVersion, reloadingFromServer, gracePeriodSeconds, labels, labelsNot, labelsIn, labelsNotIn, fields);



  3. }


至此,第二个问题“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实战可以更顺畅,也能从原理出发继续深入的分析和学习。

本文作者:欣宸,前阿里工程师

欢迎关注我的公众号 (ID:程序员欣宸)

K8S培训推荐

Kubernetes线下实战培训,采用3+1+1新的培训模式(3天线下实战培训,1年内可免费再次参加,每期前10名报名,可免费参加价值3600元的线上直播班;),资深一线讲师,实操环境实践,现场答疑互动,培训内容覆盖:Kubernetes集群搭建、Kubernetes设计、Pod、常用对象操作,Kuberentes调度系统、QoS、Helm、网络、存储、CI/CD、日志监控等。开班城市:北京/深圳/上海/成都点击查看更多课程信息!


推荐阅读

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存