k8s

Spring cloud 在 kubernetes下的服务发现与负载均衡

Posted by zjh on December 7, 2021

前言

上一章描述了如何通过docker搭建一个简单的kubernetes集群,有了k8s,就可以用它来玩点其他东西;本文通过搭建简单的springboot项目,演示如何通过kubernetes进行服务注册,旨在使用K8S中自身的服务发现功能,不使用其他的服务发现组件,通过 Spring 的 spring-cloud-kubernetes 来搭建SpringCloud项目。

1、kubernetes Service 概述

每个 Pod 都有自己的 IP 地址,但是在 Deployment 中,在同一时刻运行的 Pod 集合可能与稍后运行该应用程序的 Pod 集合不同。

  • 这导致了一个问题: 如果一组 Pod(称为“后端”)为集群内的其他 Pod(称为“前端”)提供功能, 那么前端如何找出并跟踪要连接的 IP 地址,以便前端可以使用提供工作负载的后端部分?其实这个问题放在微服务下来讲的话,就是如何处理服务发现?
  • 为了解决这个问题,就要使用到kubernetes的另外一种资源:Service(SVC)。
  • SVC服务是Kubernetes里的核心资源对象之一,其实可以理解成我们微服务架构中的一个微服务。SVC一旦被创建,Kubernetes就会自动为它分配一个可用的Cluster IP,在svc的整个生命周期内,Cluster IP不会发生改变。
  • Kubernetes使用DNS提供服务注册功能。 Kubernetes 为 Pods 提供自己的 IP 地址,并为一组 Pod 提供相同的 DNS 名, 并且可以在它们之间进行负载均衡。
  • 定义SVC:
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

上述配置创建一个名称为 my-service 的 Service 对象,它会将请求代理到使用 TCP 端口 9376,并且具有标签 app=MyApp 的 Pod 上。

2、项目搭建

接下来就使用kubernetes的服务发现功能,不另外使用其他服务注册组件,搭建Spring Cloud 微服务架构。

  • 项目结构
    • service-consumer
    • service-provider

      2.1 均采用 gradle 构建springboot项目

  • build.gradle
      implementation 'org.springframework.boot:spring-boot-starter-web'
     	implementation "org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE"
      implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-all:1.1.2.RELEASE'
      implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap:3.0.3'
      implementation 'org.springframework.cloud:spring-cloud-starter-netflix-ribbon:2.2.6.RELEASE'
    

    2.2 配置文件

  • service-consumer -application.yml ``` server: port: 30001 spring: application: name: service-consumer cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true kubernetes: reload: enabled: true mode: polling period: 5000

logging: level: org.springframework.cloud.gateway: debug org.springframework.cloud.loadbalancer: debug

- service-provider - application.yml

server: port: 30000 spring: application: name: service-provider cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true kubernetes: reload: enabled: true mode: polling period: 5000

logging: level: org.springframework.cloud.gateway: debug org.springframework.cloud.loadbalancer: debug

#### 2.3 DockerFile
- service-provider

FROM openjdk:8-jdk-alpine ENV jarName=”service-provider.jar” COPY build/libs/$jarName $jarName ENTRYPOINT java -Duser.timezone=GMT+8 -jar $jarName

- service-consumer

FROM openjdk:8-jdk-alpine ENV jarName=”service-consumer.jar” COPY build/libs/$jarName $jarName ENTRYPOINT java -Duser.timezone=GMT+8 -jar $jarName

#### 2.4 简单的controller
- service-consumer

@Slf4j @RestController @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients @RequiredArgsConstructor public class BootstrapApplication {

private final DiscoveryClient discoveryClient;


private final ProviderServiceClient providerServiceClient;


private final RestTemplate restTemplate = new RestTemplate();

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

@GetMapping("/feign/hello")
public String consumerHello(){
    log.info("消费服务:service-consumer");
    return providerServiceClient.sayHello();
}

@GetMapping("/rest/hello")
public String restHello(){
    return restTemplate.getForObject("http://service-provider/provider-hello", String.class);
}

@GetMapping("/rest-port/hello")
public String restHelloWithPort(){
    return restTemplate.getForObject("http://service-provider:30000/provider-hello", String.class);
}
@GetMapping("/rest-ip/hello")
public String restHelloWithIP(){
    return restTemplate.getForObject("http://10.100.235.65:30000/provider-hello", String.class);
}



@GetMapping("/consumers/services")
public List<String> findServices(){
    log.info("当前注册中心下所有服务");
    List<String> services = discoveryClient.getServices();
    services.stream().map(discoveryClient::getInstances).forEach(v ->
            v.forEach(s -> System.out.printf("%s:%s  uri:%s%n", s.getHost(), s.getPort(), s.getUri())));

    return services;
}

}

@FeignClient(value = “service-provider”) public interface ProviderServiceClient { @GetMapping(“/provider-hello”) String sayHello(); }


- service-provider

@RestController @SpringBootApplication @EnableDiscoveryClient public class BootstrapApplication {

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


@GetMapping("/provider-hello")
public String sayHello(){
    return "hello world";
}

}

####  2.5编译打包

./gradlew build

docker build -t $APPLICATION_NAME:$VERSION .


#### 2.6 构建一个k8s template

PROJECT_NAME:SpringBoot工程的服务名称

REPLACE_IMAGE: Docker镜像

PROJECT_PORT: SpringBoot工程的服务端口

定义Deployment

apiVersion: apps/v1 kind: Deployment metadata: name: PROJECT_NAME labels: app: PROJECT_NAME spec: replicas: 1 template: metadata: name: PROJECT_NAME labels: app: PROJECT_NAME spec: containers: - name: PROJECT_NAME image: REPLACE_IMAGE ports: - containerPort: PROJECT_PORT imagePullPolicy: IfNotPresent

镜像仓库,(不指定则使用本地仓库)

  #imagePullSecrets:
   # - name: regcred-aliyun
  restartPolicy: Always   selector:
matchLabels:
  app: PROJECT_NAME

定义SVC

apiVersion: v1 kind: Service metadata: name: PROJECT_NAME spec: selector: app: PROJECT_NAME ports: - port: PROJECT_PORT #外部映射端口 targetPort: PROJECT_PORT # 服务运行端口 nodePort: PROJECT_PORT # node访问端口 type: NodePort

> 两个项目分别根据模板中的注释描述编写yaml文件,其中,ports内配置的nodePort的类型,为了方便直接通过本机访问, 例如:
> >`service-consumer-deploy.yaml`
>
> > `service-provider-deploy.yaml`

#### 2.7 构建k8s服务
> 使用 `kubectl apply -f 文件名` 构建服务,例如:
> ```
> kubectl apply -f service-consumer-deploy.yaml
> kubectl apply -f service-provider-deploy.yaml
> ```
#### 2.8 查看构建进度,构建完成后,找到nodeIP访问

kubectl get po kubectl get svc kubectl get node -o wide

![在这里插入图片描述](https://img-blog.csdnimg.cn/37cd5c78e017478cbf02ab158be42ed7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)
![在这里插入图片描述](https://img-blog.csdnimg.cn/fde4c640c87e465f94cd1fbcb4293c0b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)
![在这里插入图片描述](https://img-blog.csdnimg.cn/9683561a7531425a8a9a9239d2d219f8.png)
# 3、测试
- 获取当前注册中心有哪些服务
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/f51535c9e02a4b1d8704b25968b4eda7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 尝试rest template调用 provider服务
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/36a5dbb2d8be4050a8388a8be25a81ad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 尝试feign 调用provider服务
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/49022e480a4747f0901a28aa36c925da.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)
  - 查看日志
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/6813a502141a4948a2e8350903bfbbe4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)

## x、歇一下
- 聊一聊原理
> K8S其实已经维护了服务实例列表,每个服务对应的Endpoint也保存在K8S集群etcd数据库中,所以spring-cloud-kubernetes所做的工作,就是在服务调用时,找到找到服务的Endpoint,从而完成服务调用。我们发现spring-cloud-kubernetes也是通过实现DiscoveryClient接口,实现类KubernetesDiscoveryClient,具体源码这里就不叙述了。

- 配置pods端口转发

kubectl port-forward podsNAME 外部端口:内部服务端口

#例如 kubectl port-forward podsNAME 80:30001

#例如 kubectl port-forward podsNAME :30001 # 随机分配

- 至于为什么没有调通feign:
  -  首先服务发现没有问题,通过`DiscoveryClient` 调用`getServices`是可以获取所有服务列表的
     ![在这里插入图片描述](https://img-blog.csdnimg.cn/621a331294344e85b7a0df142ae54814.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_20,color_FFFFFF,t_70,g_se,x_16)

  - 1、可能是因为内部转发端口应该配置成80,目前是30001,导致走dns之后,默认端口为80,访问失败。
  - 2、 feign配置问题
  - 3、 下篇解答


---
## End.解决feign调用失败:
本来想说的是下篇解答,但是想了一下也没几个字描述,昨晚失败的主要原因是:feign没有找到可用的服务。k8s下,不借用其他服务发现的组件,那么默认是从etcd下走的dns解析,所以解决这个问题就显而易见了。
- 1、在feignClient上加url

@FeignClient(name = “service-provider”, url = “http://service-provider”) public interface ProviderServiceClient { @GetMapping(“/provider-hello”) String sayHello(); }

- 2、为了不指定端口,修改映射端口为80,反正每个Cluster IP都是独立的,不会冲突
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/95bc9d676d764cd0a0067ba393f2c939.png)
- 3、 测试:
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/7ad79197534249e3a12d40ff9608d54c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_17,color_FFFFFF,t_70,g_se,x_16)
# 4、kubernetes下的负载均衡:
- 修改provider的代码

@SneakyThrows @GetMapping(“/provider-hello”) public String sayHello(HttpServletRequest request){ Thread.sleep(2000); System.out.println(“======收到服务调用请求=====”); return “hello world”); }

- 增加service-provider 的pod数量:
  ![在这里插入图片描述](https://img-blog.csdnimg.cn/703b3a6104c74c2f9ff5a3c3c41ebf2c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5biF54K45LqG55qEQ2hlc3Rlcg==,size_15,color_FFFFFF,t_70,g_se,x_16)

kubectl apply -f service-provider-deploy.yaml ```

  • 等待重启,然后查看pods状态和provider的数量在这里插入图片描述
  • 访问: servicer-consumer 访问provider 在这里插入图片描述
  • 查看日志 pod1: 在这里插入图片描述 pod2: 在这里插入图片描述
  • 多发几次请求:

    pod1: 在这里插入图片描述 pod2: 在这里插入图片描述 完成。