Kubernetes / Linux Note / 运维笔记

Kubernetes (一) SpringCloud 组件进行服务发现

Einic Yeo · 7月1日 · 2019年 · · · ·

系统环境:

  • Kubernetes 版本:1.14.0
  • SpringCloud Kubernetes 版本:1.0.2.RELEASE
  • 示例部署文件 Github 地址:https://mirrors.infvie.org/blog-example/springcloud/springcloud-kubernetes/springcloud-kubernetes-discovery-demo

一、介绍

1.1、组件简介

       这里主要介绍的是如何在 Kubernetes 中使用 SpringCloud 框架开发 Java 应用,在这个过程中主要使用的组件就是 SpringCloud Kubernetes 来完成服务发现、动态配置,利用 Feign 来进行服务间的通信等。

       这里先简单介绍下 SpringCloud Kubernetes,它主要是提供了使用 Kubernetes 本地服务的 Spring Cloud 通用接口实现。目标是促进 Spring Cloud 和运行在 Kubernetes 中的 Spring Boot 应用程序的集成。

       SpringCloud Kubernetes 能在 Kubernetes 中完成服务发现和配置监听功能主要依赖 Fabric8 提供的 Kubernetes Client 组件,该组件是 Kubernetes Java API 的第三方客户端,它的主要功能是远程操作 Kubernetes API 完成在 Kubernetes 环境下的一系列操作。

1.2、功能简介

主要提供以下几种功能:

  • 在 Kubernetes 中实现服务发现、服务名称解析功能。
  • 在 Kubernetes 中读取 ConfigMaps 和 Secrets 的配置,当 ConfigMap 或 Secret 更改时重新加载应用程序属性。
  • 在 Kubernetes 可去掉 Kubernetes 自带的服务负载均衡,实现与 Ribbon 结合,通过 Ribbon 完成负载均衡。

1.3、服务发现简介

       在原生的 SpringCloud 中,我们服务发现大多数是通过将服务注册到注册中心(如 Eureka)后通后,各个服务通过连接注册中心,从注册中心定时获取服务列表及服务地址的这种方式完成服务发现。

       在 Kubernetes 中所有的 ServicePod 等信息都会存入 Etcd 中记录,相当于 Ectd 扮演了一个注册中心的角色,并且在 Kubernetes 中能通过 CoreDNS 完成通过 服务名称 + 端口号 方式让各个服务间能相互通信,由此可知我们要是能获一个服务的服务名端口号就能完成服务间的通信工作,所以重点就在如何获取这两个值。

       SpringCloud Kubernetes 这个组件的服务发现目的就是获取上面所述的 Kubernetes 中一个或者多个 Namespace 下的所有 服务列表,且在过滤列表时候设置过滤的 端口号 ,这样获取到服务列表后就能让依赖它们的 SpringBoot 或者其它框架的应用完成服务发现工作,让服务能够通过 http://ServiceName:Port 这种方式进行访问。

1.4、综上个人感慨

       综上所述,应用 SpringCloud Kubernetes 作用就是通过它获取 Kubernetes 下的服务列表进行服务发现。故而在这种情况下,没必要在继续坚持在 Kubernetes 上再启动一个 Eureka,然后为了 Eureka 高可用设置为有状态模式 StatefulSet,感觉这有点…..多此一举。

二、环境配置

2.1、配置说明

       使用 SrpingCloud Kubernetes 相关组件的服务,在本地测试的时候并不需将程序构建成 Docker 镜像,然后在 Kubernetes 中部署它,因为 SpringCloud Kubernetes 组件依赖于 Fabric8 Kubernetes Java 客户端,可以通过使用 http 协议与 Kubernetes API 进行通信,通过控制 API 来完成一些列操作,所以我们只需要配置 本地环境变量,让其能够有权限调用 Kubernetes API 即可,这样也方便在本地进行程序的调试工作。

2.2、配置操作

       这里我们需要平时使用 Kubectl 工具时使用的凭证文件 config 将其放在系统的用户目录中,如果是 Linux 环境且能操作 Kubectl 命令,在该系统中直接用 SpringCloud Kubernetes 组件即可,不过考虑大多数人开发环境是 Windows 系统,本人也是,所以需要将这个 config 复制出来,放到系统的用户目录的 .kube 下面,这里记录下这个操作过程。

由于在用户目录下无法直接创建带”.”的文件夹,所以这里打开 cmd 命令行窗口工具,进入个人用户目录,然后创建”.kube”文件夹,之后将 config 文件复制到该文件夹下即可。

使用管理员身份打开 Windows 的 cmd 命令行窗口

Microsoft Windows [版本 10.0.17763.1]

(c) 2018 Microsoft Corporation。 保留所有权利。

c:\Users\infvie>

使用”cd”命令进入用户目录

$ cd /users/infvie

使用”mkdir”命令创建”.kube”文件夹

$ mkdir .kube

将 linux 中的 config 文件拷贝到上面创建的”.kube”目录下

这样连接 Kubernetes API 的认证配置环境设置完成,下面将写一个基于 SpringCloud Kubernetes 例子,用于发现版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作! Kubernetes 下的服务列表,测试一下该配置是否能用。

三、SpringCloud Kubernetes 服务发现示例项目

这里用 SpringCloud Kubernetes 中的服务发现组件 spring-cloud-starter-kuberne版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!tes 来演示获取 Kubernetes 环境中的 服务列表和服务实例信息。

3.1、Maven 引入相关变量

在”pom.xml”文件中引入 SpringBoot 与 SpringCloud Kubernetes 相关 Jar。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>club.infvie</groupId>
    <artifactId>springcloud-k8s-discovery-demo</artifactId>
    <version>0.0.1</version>
    <name>springcloud-k8s-discovery-demo</name>
    <description>SpringCloud Kubernetes Discovery Demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--SpringBoot Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SpringBoot Actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud Kubernetes Discovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-kubernetes</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3.2、创建一个 Controller 类获取服务列表

这里创建一个 /service/instance 两个接口,用于获取连接 Kubernetes 集群配置文件用户所拥有权限的 Namespace 下的所有 Service 列表和服务实例。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
public class ServiceController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/service")
    public List<String> getServiceList(){
        return discoveryClient.getServices();
    }

    @GetMapping("/instance")
    public Object getInstance(@RequestParam("name") String name){
        return discoveryClient.getInstances(name);
    }

}

3.3、创建 application 配置文件

创建 application.yaml 配置文件,设置一些 SpringBoot 基本参数和开启服务发现

spring:
  application:
    name: springcloud-kubernetes-discovery-demo
  cloud:
    kubernetes:
      discovery:
        enabled: true   #开启服务发现

server:
  port: 8080

management:
  server:
    port: 8081
  endpoints:
    web:
      exposure:
        include: "*"

3.4、创建启动类并启用服务发现注解

创建一个 SpringBoot 项目的启动类,且引入服务发现注解 @EnableDiscoveryClient。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

3.5、测试接口

上面示例中定义了两个接口分别为:

  • 服务列表发现接口:/service
  • 服务实例信息接口:/instance

这里先访问调用 “/service” 接口获取 Kubernetes 中的服务列表,输入地址:http://localhost:8080/service 可以看到服务列表如下所示:

[
    "jenkins",
    "service-customer",
    "service-provider",
    "sonatype-nexus",
    "springboot-admin-k8s",
    "springboot-helloworld",
    "springboot-prometheus-demo",
    "springcloud-k8s-config",
    "swagger-kubernetes"
]

然后再测试 “/instance” 接口,参数 name 输入一个上面服务列表中获取的服务名,这里输入地址:http://localhost:8080/instance?name=springboot-helloworld 可以获得这个服务信息如下所示:

[
    {
        "instanceId": "9950be16-95d3-11e9-a040-000c29d98697",
        "serviceId": "springboot-helloworld",
        "secure": false,
        "metadata": {
            "app": "springboot-helloworld",
            "port.server": "8080",
            "admin": "enabled",
            "group": "A",
            "port.management": "8081"
        },
        "scheme": "http://",
        "host": "10.20.2.89",
        "port": 8081,
        "uri": "http://10.20.2.89:8081"
    }
]

由上可知通过 spring-cloud-starter-kubernetes 组件能够发现服务所在当前 Namespace 下的所有 Service 列表,且能够通过 Service名称 获取服务的基本信息。

四、服务发现可配参数

下面将介绍下 SpringCloud Kubernetes 服务发现组件有哪些基本参数可用进行配置。

4.1、可配置参数表

在 SpringCloud Kubernetes 官方 Github 和对应说明文档中并没有发现”服务发现”模块的配置参数,所以无奈本人只好自己查看 SpringCloud Kubernetes 的源码,分析它可配置的参数并且用 Debug 模式对这些参数进行效验,总结出下面这些配置参数和用法。

参数名称类型默认值参数描述
spring.cloud.kubernetes.discovery.enabledBooleantrue是否启用服务发现
spring.cloud.kubernetes.discovery.filterString“”从 Kubernetes 获取 Service 列表的过滤条件,通过Spring EL表达式来写
spring.cloud.kubernetes.discovery.serviceLabelsMap“”指定特定 Label 的 Service 通过筛选,为空则不根据 Label 进行筛选
spring.cloud.kubernetes.discovery.primaryPortNameString“”指定允许通过筛选的 Service 端口名称,为空则默认添加全部端口
spring.cloud.kubernetes.discovery.knownSecurePortsSet443,8443指定 Kubernetes 的安全端口列表
spring.cloud.kubernetes.discovery.metadata.addLabelsBooleantrue是否获取服务实例后,在 Label 前面添加前缀
spring.cloud.kubernetes.discovery.metadata.labelsPrefixString“”设置获取服务实例后往其 Label 添加前缀的值
spring.cloud.kubernetes.discovery.metadata.addAnnotationsBoole版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!antrue是否获取服务实例后,在 Annotations 前面添加前缀
spring.cloud.kubernetes.discovery.metadata.annotationsPrefixString“”设置获取服务实例后往其 Annotations 添加前缀的值
spring.cloud.kubernetes.discovery.metadata.addPortsBooleantrue是否获取服务实例后,在 Ports 前面添加前缀
spring.cloud.kubernetes.discovery.metadata.portsPrefixString“”设置获取服务实例后往其 Ports 添加前缀的值

4.2、配置参数说明及示例

(1)、开启服务发现

  • 参数:spring.cloud.kubernetes.discovery.enabled
  • 可配参数:false、true
  • 参数描述:是否开启 SpringCloud Kubernetes 服务发现。
  • 例子:

开启服务发现:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true

关闭服务发现:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: false

(2)、配置标签过滤服务

  • 参数:spring.cloud.kubernetes.discovery.serviceLabels
  • 可配参数:k8s Service 的 metadata 中的 Label 属性。
  • 参数描述:可以通过设置该参数,根据该参数的 Label 键值对来过滤 Kubernetes 服务发现列表,如果服务不带设置的标签就将服务过滤掉。
  • 例子:

例如 Kubernetes 中有两个 Service 对象,内容如下:

Service A:

kind: Service
apiVersion: v1
metadata:
  name: service-A
  namespace: infviecloud
  labels:
    app: service-A
    group: A
spec:
  type: ClusterIP
  ports:
    - name: server
      port: 8080
      targetPort: 8080
  selector:
    app: service-A

Service B:

kind: Service
apiVersion: v1
metadata:
  name: service-B
  namespace: infviecloud
  labels:
    app: service-B
    group: B
spec:
  type: ClusterIP
  ports:
    - name: server 
      port: 8080
      targetPort: 8080
  selector:
    app: service-B
版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!

在 SpringCloud Kubernetes 服务发现例子中配置serviceLabels参数来完成只发现带”group:A”标签的服务,然后进行效果验证:

  • 配置:
spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        serviceLabels:
          group: A          #只发现带 group:A 的标签的服务

运行程序得出结论: 服务发现列表中只有 Service A,说明只服务发现带”group:A”标签的服务。

(3)、配置端口过滤服务

  • 参数:spring.cloud.kubernetes.discovery.primaryPortName
  • 可配参数:k8s Service 的 Ports 的 name 属性。
  • 参数描述:如果该 Service 只有一个端口则直接设置该端口为服务发现端口。如果存在多个端口则利用配置的端口名和 Service 的端口名称进行匹配,如果匹配上就设置该端口为服务发现端口,如果都没有匹配上,则过滤掉此服务。
  • 例子:

例如 Kubernetes 中有两个 Service 版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!对象,内容如下:

Service A:

kind: Service
apiVersion: v1
metadata:
  name: service-A
  namespace: infviecloud
  labels:
    app: service-A
spec:
  type: ClusterIP
  ports:
    - name: server      #只设置一个端口
      port: 8080
      targetPort: 8080
  selector:
    app: service-A

Service B:

kind: Service
apiVersion: v1
metadata:
  name: service-B
  namespace: infviecloud
  labels:
    app: service-B
spec:
  type: ClusterIP
  ports:
    - name: server      #端口1
      port: 8080
      targetPort: 8080
    - name: management  #端口2
      port: 8081
      targetPort: 8081
  selector:
    app: service-B

在 SpringCloud Kubernetes 服务发现例子中设置三个配置,然后进行效果验证:

  • 配置1:设置过滤端口名为 server。
  • 配置2:设置过滤端口名为 management。
  • 配置3:设置过滤端口名为两个 Service 中都不存在的端口名。

配置1:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        primaryPortName: server     #Service A、Service B 都存在的端口名

配置2:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        primaryPortName: management  #Service B 存在的端口名

配置3:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        primaryPortName: none        #两个service都不存在的端口名

然后进行效果验证,得到下面结论:

  • 配置1:发现 Service A (端口名:server) 与 Service B (端口名:server) 服务实例。
  • 配置2:发现 Service A (端口名:server) 与 Service B (端口名:management) 服务实例。
  • 配置3:发现 Service A (端口名:server) 服务实例。

由此可知,当服务只存在一个端口时候,直接设置为服务发现端口,如果存在多个端口,则和设置参数中输入的端口名进行匹配来筛选服务发现端口,如果都未匹配上则过滤该服务。一般此配置参数和上一个的过滤标签参数配合使用。

(4)、通过 SpringEL 表达式过滤服务

  • 参数:spring.cloud.kubernetes.discovery.filter
  • 可配参数:SpringEL 中 Boolean 类型的表达式,表达式中的值只能为 Kubernetes Service 对象中存在的参数。
  • 参数描述:可以通过在参数中写一段 SpringEL Boolean 类型的表达式来过滤服务,如果发现的服务实例中对象参数中的值在 SpringEL 表达式中为 true,则将该服务加入服务发现类别,为 false 则过滤掉该服务。
  • 例子:

这里展示一个 Kubernetes 中的 Service,然后写 SpringEL 表达式来验证该服务是否能加入服务发现列表。

kind: Service
apiVersion: v1
metadata:
  name: service-A
  namespace: infviecloud
  labels:
    app: service-A
spec:
  type: ClusterIP
  ports:
    - name: server
      port: 8080
      targetPort: 8080
  selector:
    app: service-A

在 SpringCloud Kubernetes 服务发现例子中设置几个不同的 SpringEL 表达式配置进行验证:

  • 配置1:发现服务名不为 service-A 的所有服务
  • 配置2:发现服务中包含一个或两个端口的所有服务
  • 配置3:发现服务类型为 NodePort 的所有服务

配置1:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        filter: "metadata.name != 'service-A'"

配置2:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        filter: "spec.ports.size() == 1 or spec.ports.size() == 2"

配置3:

spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
        filter: "spec.type == 'NodePort'"

由于本人对 SpringEL 表达式并不精通,为了测试表达式是否正确只能查看源码,然后根据其验证模式,模拟创建 Service 实例,然后进行 SpringEL 表达式验证这个过程,可以在这个过程中验证自己写的 SpringEL 表达式是否能和 Service实例 匹配上,具体代码如下所示:

import io.fabric8.kubernetes.api.model.*;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 测试 SpringEL 表达式
 */
public class SpringEL {

    public static void main(String[] args) {
        // 输入待验证的表达式
        String springEL = "metadata.name == 'test-project'";
        // 创建模拟的 Service 对象
        Service service = createService();
        // 验证 SpringEL 表达式和对象中现有的值是否匹配
        boolean isTrue = verification(service,springEL);
        //输出结果
        System.out.println(isTrue);
    }

    /**
     * 创建模拟的 Service 对象,和 SpringCloud Kubernetes 服务发现实例保持一致
     * @return
     */
    static Service createService(){
        /** 设置Status **/
        ServiceStatus serviceStatus = new ServiceStatus();
        serviceStatus.setLoadBalancer(new LoadBalancerStatus());
        /** 设置Spec **/
        ServiceSpec serviceSpec = new ServiceSpec();
        serviceSpec.setClusterIP("10.10.1.11");
        // 设置type
        serviceSpec.setType("NodePort");
        // 设置 Selector
        Map selector = new HashMap();
        selector.put("app", "test-project");
        serviceSpec.setSelector(selector);
        // 设置端口1
        ServicePort servicePort1 = new ServicePort();
        servicePort1.setName("server");
        servicePort1.setPort(8080);
        servicePort1.setNodePort(30080);
        servicePort1.setProtocol("TCP");
        servicePort1.setTargetPort(new IntOrString("8080"));
        // 设置端口2
        ServicePort servicePort2 = new ServicePort();
        servicePort2.setName("management  ");
        servicePort2.setPort(8081);
        servicePort2.setNodePort(30081);
        servicePort2.setProtocol("TCP");
        servicePort2.setTargetPort(new IntOrString("8081"));
        // 将两个端口加如Spec
        List<ServicePort> servicePortList = new ArrayList<>();
        servicePortList.add(servicePort1);
        servicePortList.add(servicePort2);
        serviceSpec.setPorts(servicePortList);
        /** 设置 Metadata **/
        ObjectMeta objectMeta = new ObjectMeta();
        objectMeta.setName("test-project");
        objectMeta.setNamespace("infviecloud");
        // 设置Label
        Map labels = new HashMap();
        labels.put("app", "test-project");
        labels.put("group", "b");
        objectMeta.setLabels(labels);
        objectMeta.setResourceVersion("3373499");
        objectMeta.setCreationTimestamp("2019-06-23T16:24:39Z");
        /** 设置Service **/
        Service service = new Service();
        service.setKind("Service");
        service.setApiVersion("v1");
        service.setStatus(serviceStatus);
        service.setSpec(serviceSpec);
        service.setMetadata(objectMeta);
        return service;
    }

    /**
     * 使用 SpringEL 表达式测试 filter
     * @param object
     * @param springEL
     * @return
     */
    static boolean verification(Object object,String springEL){
        // 创建上下文环境
        SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build();
        // 创建SpEL表达式的解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 验证表达式验证内容是否和对象中的值匹配并返回 Boolean 结果
        return parser.parseExpression(springEL).getValue(evalCtxt, object, Boolean.class);
    }

}

五、源码分析

5.1、服务发现实现类源码

public class KubernetesDiscoveryClient implements DiscoveryClient {

    ......

    /**
     * 获取描述信息,默认为下面固定的字符串
     * @return
     */
    @Override
    public String description() {
        return "Kubernetes Discovery Client";
    }
    
    /**
     * 获取服务实例列表
     * @param  serviceId K8S 中的 Service 名称
     * @return
     */
    @Override
    public List<ServiceInstance> getInstances(String serviceId) {
        Assert.notNull(serviceId,"[Assertion failed] - the object argument must not be null");
        // 根据服务名查找 K8S 中 endpoints 信息和 EndpointSubset 列表
        Endpoints endpoints = this.client.endpoints().withName(serviceId).get();
        List<EndpointSubset> subsets = getSubsetsFromEndpoints(endpoints);
        // 创建 ServiceInstance 列表,用于存储通过 API 获取过来的 Service 实例信息
        List<ServiceInstance> instances = new ArrayList<>();
        // 判断 Subsets 是否为空,为空则代表 Endpoints 没有关联的 Pod,相当于 Service 是没有关联 Pod 的无效服务
        if (!subsets.isEmpty()) {
            // 获取 Service 信息、Service Metadata 数据、和自定义参数对象 properties 中的 metadataProps 参数
            final Service service = this.client.services().withName(serviceId).get();
            final Map<String, String> serviceMetadata = new HashMap<>();
            KubernetesDiscoveryProperties.Metadata metadataProps = this.properties.getMetadata();
            // 根据配置文件中是否设置 isAddLabels 来决定是否将自定义 Annotations 加入到 serviceMetadata
            if (metadataProps.isAddLabels()) {
                Map<String, String> labelMetadata = getMapWithPrefixedKeys(
                        service.getMetadata().getLabels(),
                        metadataProps.getLabelsPrefix());
                if (log.isDebugEnabled()) {
                    log.debug("Adding label metadata: " + labelMetadata);
                }
                serviceMetadata.putAll(labelMetadata);
            }
            // 根据配置文件中是否设置 isAddAnnotations 来决定是否将自定义 Annotations 加入到 serviceMetadata
            if (metadataProps.isAddAnnotations()) {
                Map<String, String> annotationMetadata = getMapWithPrefixedKeys(
                        service.getMetadata().getAnnotations(),
                        metadataProps.getAnnotationsPrefix());
                if (log.isDebugEnabled()) {
                    log.debug("Adding annotation metadata: " + annotationMetadata);
                }
                serviceMetadata.putAll(annotationMetadata);
            }
            // 循环 EndpointSubset 获取部分参数拼凑 KubernetesServiceInstance 对象
            for (EndpointSubset s : subsets) {
                Map<String, String> endpointMetadata = new HashMap<>(serviceMetadata);
                // 检测参数是否设置 isAddPorts 参数,将自定义端口前缀加入到默认端口名前
                if (metadataProps.isAddPorts()) {
                    // 获取端口列表 Map
                    Map<String, String> ports = s.getPorts().stream()
                            .filter(port -> !StringUtils.isEmpty(port.getName()))
                            .collect(toMap(EndpointPort::getName,port -> Integer.toString(port.getPort())));
                    // 设置自定义前缀,设置 portMetadata Map 的 Key 改成(prefix + key)
                    Map<String, String> portMetadata = getMapWithPrefixedKeys(ports,metadataProps.getPortsPrefix());
                    if (log.isDebugEnabled()) {
                        log.debug("Adding port metadata: " + portMetadata);
                    }
                    endpointMetadata.putAll(portMetadata);
                }
                // 一个 Subset 中包含多个 Address 地址,从 EndpointSubset 中获取 addresses 列表
                List<EndpointAddress> addresses = s.getAddresses();
                // 根据 Address 循环获取 UID 和端口列表,然后拼接服务实例对象 KubernetesServiceInstance
                for (EndpointAddress endpointAddress : addresses) {
                    String instanceId = null;
                    if (endpointAddress.getTargetRef() != null) {
                        instanceId = endpointAddress.getTargetRef().getUid();
                    }
                    EndpointPort endpointPort = findEndpointPort(s);
                    // 通过上面获取的部分数据来拼接服务实例对象
                    instances.add(new KubernetesServiceInstance(instanceId, serviceId,
                            endpointAddress, endpointPort, endpointMetadata,
                            this.isServicePortSecureResolver
                                    .resolve(new DefaultIsServicePortSecureResolver.Input(
                                            endpointPort.getPort(),
                                            service.getMetadata().getName(),
                                            service.getMetadata().getLabels(),
                                            service.getMetadata().getAnnotations()))));
                }
            }
        }
        return instances;
    }

    /**
     * 从 EndpointSubset 获取端口号列表
     * @param  s EndpointSubset 
     * @return
     */
    private EndpointPort findEndpointPort(EndpointSubset s) {
        // 获取端口号列表
        List<EndpointPort> ports = s.getPorts();
        EndpointPort endpointPort;
        // 判断端口号数量,如果只有一个端口,就直接获取下标0的端口号
        if (ports.size() == 1) {
            endpointPort = ports.get(0);
        }
        else {
            // 如果存在多个端口号,就以此根据 properties 中设置的 "PrimaryPortName" 参数,
            // 根据这个参数值和 Service 端口名对比来过滤端口号。
            Predicate<EndpointPort> portPredicate;
            if (!StringUtils.isEmpty(properties.getPrimaryPortName())) {
                portPredicate = port -> properties.getPrimaryPortName()
                        .equalsIgnoreCase(port.getName());
            }
            // 如果为未设置筛选端口名称参数,则默认都为 true,都加入端口列表
            else {
                portPredicate = port -> true;
            }
            // 根据上面 boolean 值,来过滤端口号
            endpointPort = ports.stream().filter(portPredicate).findAny()
                    .orElseThrow(IllegalStateException::new);
        }
        return endpointPort;
    }

    /**
     * 从 Endpoints 中获取 Subset
     * @param  endpoints endpoints 对象
     * @return 
     */
    private List<EndpointSubset> getSubsetsFromEndpoints(Endpoints endpoints) {
        if (endpoints == null) {
            return new ArrayList<>();
        }
        if (endpoints.getSubsets() == null) {
            return new ArrayList<>();
        }
        return endpoints.getSubsets();
    }

    /**
     * 如果自定义参数中设置了前缀,如 label前缀、port前缀、annotation前缀等,就将获取的示例信息对应的key前加上这个前缀
     * @param  map    端口 map 集合
     * @param  prefix 自定义端口名称前缀
     * @return  
     */
    private Map<String, String> getMapWithPrefixedKeys(Map<String, String> map,String prefix) {
        if (map == null) {
            return new HashMap<>();
        }
        if (!StringUtils.hasText(prefix)) {
            return map;
        }
        final Map<String, String> result = new HashMap<>();
        map.forEach((k, v) -> result.put(prefix + k, v));
        return result;
    }

    /**
     * 获取经过 filter 中 springEL 表达式过滤后的服务列表
     * @return 
     */
    @Override
    public List<String> getServices() {
        // 从参数配置类中获取 filter 中的 SprngEL 表达式
        String spelExpression = this.properties.getFilter();
        Predicate<Service> filteredServices;
        // 如果未设置 filter 参数,则不过滤任何服务
        if (spelExpression == null || spelExpression.isEmpty()) {
            filteredServices = (Service instance) -> true;
        }
        // 如果设置 filter 参数,则根据 filter 中的 SpringEL 表达式进行验证,符合则为 true 否则为 false
        else {
            Expression filterExpr = this.parser.parseExpression(spelExpression);
            filteredServices = (Service instance) -> {
                Boolean include = filterExpr.getValue(this.evalCtxt, instance,
                        Boolean.class);
                if (include == null) {
                    return false;
                }
                return include;
            };
        }
        // 过滤服务,然后返回过滤后的服务列表
        return getServices(filteredServices);
    }

    /**
     * 根据 filter 过滤服务列表
     * @param  filter 过滤列表
     * @return      
     */
    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());
    }

}

5.2、服务发现参数配置类源码

@ConfigurationProperties("spring.cloud.kubernetes.discovery")
public class KubernetesDiscoveryProperties {

    /** 设置为 true 则启动服务发现 */
    private boolean enabled = true;

    /** 本地服务实例的名称。 */
    @Value("${spring.application.name:unknown}")
    private String serviceName = "unknown";

    /**
     * 在从Kubernetes API服务器检索到服务之后过滤服务的SpEL表达式。
     */
    private String filter;

    /** 设置使用HTTPS的安全端口。 */
    private Set<Integer> knownSecurePorts = new HashSet<Integer>() {
        {
            add(443);
            add(8443);
        }
    };

    /**
     * 如果设置了,则只从Kubernetes API服务器获取与这些标签匹配的服务。
     */
    private Map<String, String> serviceLabels = new HashMap<>();

    /**
     * 如果设置了该端口,则当为服务定义多个端口时,使用具有给定名称的端口作为主要端口。
     */
    private String primaryPortName;

    private Metadata metadata = new Metadata();

    public boolean isEnabled() {
        return this.enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getServiceName() {
        return this.serviceName;
    }

    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }

    public String getFilter() {
        return this.filter;
    }

    public void setFilter(String filter) {
        this.filter = filter;
    }

    public Set<Integer> getKnownSecurePorts() {
        return this.knownSecurePorts;
    }

    public void setKnownSecurePorts(Set<Integer> knownSecurePorts) {
        this.knownSecurePorts = knownSecurePorts;
    }

    public Map<String, String> getServiceLabels() {
        return this.serviceLabels;
    }

    public void setServiceLabels(Map<String, String> serviceLabels) {
        this.serviceLabels = serviceLabels;
    }

    public String getPrimaryPortName() {
        return primaryPortName;
    }

    public void setPrimaryPortName(String primaryPortName) {
        this.primaryPortName = primaryPortName;
    }

    public Metadata getMetadata() {
        return this.metadata;
    }

    public void setMetadata(Metadata metadata) {
        this.metadata = metadata;
    }

    @Override
    public String toString() {
        return new ToStringCreator(this).append("enabled", this.enabled)
                .append("serviceName", this.serviceName).append("filter", this.filter)
                .append("knownSecurePorts", this.knownSecurePorts)
                .append("serviceLabels", this.serviceLabels)
                .append("metadata", this.metadata).toString();
    }

    /**
     * Metadata 配置项
     */
    public class Metadata {

        /**
         * 设置好后,服务的Kubernetes标签将包含在返回的ServiceInstance的metadata中。
         */
        private boolean addLabels = true;

        /**
         * 当设置addtags时,这将用作metadata映射中键名的前缀。
         */
        private String labelsPrefix;

        /**
         * 设置好后,服务的Kubernetes注释将包含为返回的ServiceInstance的元数据。
         */
        private boolean addAnnotations = true;

        /**
         * 当设置addAnnotations时,它将用作metadata映射中键名的前缀。
         */
        private String annotationsPrefix;

        /**
         * 设置好后,任何已命名的Kubernetes服务端口都将包含ServiceInstance的metadata数据。
         */
        private boolean addPorts = true;

        /**
         * 当设置addPorts时,这将用作 metadata 映射中键名的前缀。
         */
        private String portsPrefix = "port.";

        public boolean isAddLabels() {
            return this.addLabels;
        }
        public void setAddLabels(boolean addLabels) {
            this.addLabels = addLabels;
        }
        public String getLabelsPrefix() {
            return this.labelsPrefix;
        }
        public void setLabelsPrefix(String labelsPrefix) {
            this.labelsPrefix = labelsPrefix;
        }
        public boolean isAddAnnotations() {
            return this.addAnnotations;
        }
        public void setAddAnnotations(boolean addAnnotations) {
            this.addAnnotations = addAnnotations;
        }
        public String getAnnotationsPrefix() {
            return this.annotationsPrefix;
        }
        public void setAnnotationsPrefix(String annotationsPrefix) {
            this.annotationsPrefix = annotationsPrefix;
        }
        public boolean isAddPorts() {
            return this.addPorts;
        }
        public void setAddPorts(boolean addPorts) {
            this.addPorts = addPorts;
        }
        public String getPortsPrefix() {
            return this.portsPrefix;
        }
        public void setPortsPrefix(String portsPrefix) {
            this.portsPrefix = portsPrefix;
        }

        @Override
        public String toString() {
            return new ToStringCreator(this).append("addLabels", this.addLabels)
                    .append("labelsPrefix", this.labelsPrefix)
                    .append("addAnnotations", this.addAnnotations)
                    .append("annotationsPrefix", this.annotationsPrefix)
                    .append("addPorts", this.addPorts)
                    .append("portsPrefix", this.portsPrefix).toString();
        }
    }
}

0 条回应