Spring Cloud 在 Kubernetes下的服务发现与负载均衡

Spring Cloud 在 Kubernetes下的服务发现与负载均衡,第1张

Spring Cloud 在 Kubernetes下的服务发现与负载均衡 前言

上一章描述了如何通过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 findServices(){
        log.info("当前注册中心下所有服务");
        List 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



3、测试
  • 获取当前注册中心有哪些服务
  • 尝试rest template调用 provider服务
  • 尝试feign 调用provider服务
    • 查看日志
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是可以获取所有服务列表的

    • 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都是独立的,不会冲突
  • 3、 测试:
4、kubernetes下的负载均衡:
  • 修改provider的代码
 @SneakyThrows
    @GetMapping("/provider-hello")
    public String sayHello(HttpServletRequest request){
        Thread.sleep(2000);
        System.out.println("======收到服务调用请求=====");
        return "hello world");
    }
  • 增加service-provider 的pod数量:
kubectl apply -f service-provider-deploy.yaml
  • 等待重启,然后查看pods状态和provider的数量

  • 访问: servicer-consumer 访问provider

  • 查看日志
    pod1:

    pod2:

  • 多发几次请求:

    pod1:

    pod2:

    完成。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5623978.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存