微服务中的服务调用至少需要考虑以下几个方面:

  • 服务注册:动态存储服务的访问地址
  • 服务发现:动态获取服务的访问地址
  • 负载均衡:按照一定的规则获取同一服务的不同实例

它们在SpringCloud和K8S中有着不同的实现方式:

SpringCloudK8S
服务注册与服务发现注册中心(Erueka、Nacos等)Service(kube-dns)
负载均衡负载均衡组件(Ribbon、SpringCloudLoadBalancer等)Service(kube-proxy等)

下面将结合代码分别介绍SpinrgCloud与K8S中服务注册、服务发现、负载均衡的过程。

一、SpringCloud中的服务注册与服务发现

(一)SpringCloud服务注册与服务发现编程模型

SpringCloud在SpringCloudCommons模块中定义了服务注册与服务发现的抽象层,如下:

  • 服务注册接口ServiceRegistry
  • 服务发现接口DiscoveryClient

在maven依赖中引入Eureka或Nacos后,将会基于SpringBoot的工厂加载机制自动加载上述接口的实现:

  • 服务注册接口ServiceRegistry的实现:EurekaServiceRegistryNacosServiceRegistry
  • 服务发现接口DiscoveryClient的实现:NacosDiscoveryClientNacosDiscoveryClientCompositeDiscoveryClientSimpleDiscoveryClient

由于组件注入时使用的是ServiceRegistryDiscoveryClient这两个抽象接口,因此,无须修改代码即可实现不同注册中心的无缝切换,只需要替换maven依赖与对应的注册中心的配置即可。

下面将基于Nacos演示一下服务注册与服务发现的用法(Eureka的用法完全一致)。

(二)Nacos注册中心服务

根据Nacos官网步骤启动Nacos注册中心。

访问http://127.0.0.1:8848进入Nacos首页。

(三)Nacos服务提供者

在pom.xml中引入nacos-discovery依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

在application.properties(或application.yml)中引入nacos相关配置:

server.port=8081
spring.application.name=nacos-provider

spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.namespace=public

定义应用入口并暴露一个服务:

@SpringBootApplication
public class NacosProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosProviderApplication.class, args);
    }
    @RestController
    class EchoController {
        @GetMapping("/echo")
        public String echo(HttpServletRequest request) {
            return "echo:" + request.getParameter("name");
        }
    }
}

上述代码详见github

启动应用后,该服务将自动注册到Nacos注册中心。查看注册中心中该服务的状态:

curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos-provider'  

正常返回如下:

{"name":"DEFAULT_GROUP@@nacos-provider","groupName":"DEFAULT_GROUP","clusters":"","cacheMillis":10000,"hosts":[{"ip":"172.26.2.233","port":8081,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@nacos-provider","metadata":{"preserved.register.source":"SPRING_CLOUD","IPv6":"[fc00:f853:ccd:e793:0:0:0:1]"},"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"ipDeleteTimeout":30000}],"lastRefTime":1683528099856,"checksum":"","allIPs":false,"reachProtectionThreshold":false,"valid":true}

(四)Nacos服务消费者

在pom.xml中引入与Nacos服务提供者一样的nacos-discovery依赖。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

在application.properties(或application.yml)中引入nacos相关配置。除了端口和服务名外,其它配置与Nacos服务提供者完全一致。

server.port=8080
spring.application.name=nacos-consumer

spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.namespace=public

在代码中通过DiscoveryClient获取Nacos服务提供者的实例地址,通过RestTemplate访问。并使用@EnableDiscoveryClient(autoRegister = false)以避免将服务消费者注册到注册中心。

@SpringBootApplication
@EnableDiscoveryClient(autoRegister = false)
public class NacosConsumerApplication {

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

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @RestController
    class HelloController {
        @Autowired
        private DiscoveryClient discoveryClient;

        @Autowired
        private RestTemplate restTemplate;

        private String serviceName = "nacos-provider";

        @GetMapping("/services")
        public String info() {
            String serviceInfo = discoveryClient.getServices().toString();
            String instanceInfo = discoveryClient.getInstances(serviceName).stream().map(instance -> "InstanceId: " + instance.getInstanceId() + "<br>ServiceId: " + instance.getServiceId() + "<br>Host: " + instance.getHost() + "<br>Port: " + instance.getPort() + "<br>").collect(Collectors.joining());
            return serviceInfo + "<br><br>" + instanceInfo;
        }

        @GetMapping("/hello")
        public String hello() {
            List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
            ServiceInstance serviceInstance = instances.stream().findAny().orElseThrow(() -> new IllegalStateException("no " + serviceName + " instance available"));
            return restTemplate.getForObject("http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/echo?name=nacos",String.class);
        }
    }
}

上述代码详见github

启动应用后,访问对应服务:

curl -X GET 'http://127.0.0.1:8080/hello?name=nacos'

正常返回如下:

echo:nacos

二、SpringCloud中的负载均衡

(一)SpringCloud负载均衡编程模型

SpringCloud在SpringCloudCommons模块中定义了负载均衡的抽象层:LoadBalancerClientLoadBalanced

LoadBalancerClient的实现有:基于Ribbon的RibbonLoadBalancerClient、基于SpringCloudLoadBalancer的BlockingLoadBalancerClient

(二)基于@LoadBalanced注解与Feign组件实现客户端负载均衡

在pom.xml中引入Feign、服务发现与负载均衡的依赖。

<!-- OpenFeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 服务发现组件 -->
<!-- 该组件依赖了SpringCloudLoadBalancer组件(老版本为Ribbon)来实现负载均衡 -->
<!-- 如果将负载均衡交给K8S,就没有必要引入客户端负载均衡组件了,直接将@FeignClient的url指向K8S的Service地址即可 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 负载均衡组件(Feign需要) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

在application.properties(或application.yml)中引入nacos相关配置。该配置与Nacos服务消费者完全一致。

server.port=8080
spring.application.name=open-feign

spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.namespace=public

定义Feign服务。

@FeignClient(name = "nacos-provider") // url = "http://localhost:8081"
interface EchoApi {
    @GetMapping("/echo")
    String echo(@RequestParam("name") String name);
}

定义应用入口。

@EnableFeignClients
@SpringBootApplication
public class OpenFeignApplication {

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

    @RestController
    class HelloController {
        @Autowired
        EchoApi echoApi;

        @GetMapping("/hello")
        public String hello() {
            return echoApi.echo("nacos");
        }
    }
}

上述代码详见github

停止Nacos服务提供者Nacos服务消费者两个应用。随后启动三个Nacos服务提供者应用(修改server.port环境变量),再启动当前应用,验证负载均衡效果。

curl -X GET 'http://127.0.0.1:8080/hello?name=nacos'

正常返回如下:

echo:nacos

查看三个Nacos服务提供者的控制台日志,多次调用会在不同的应用控制台显示相关信息。

三、K8S中的服务注册、服务发现与负载均衡

K8S通过Service对象定义一个服务,一旦Service定义成功,就自动支持了服务注册、服务发现与负载均衡。Service可以基于Pod、Deployment、另外一个Service等对象对外暴露服务,下面将以Deployment为例创建Service。

创建并执行deployment.yml文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
kubectl apply -f deployment.yml

创建并执行service.yml文件:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80
kubectl apply -f service.yml 

上述Service的创建过程与下述命令等效:

kubectl expose deployment nginx-deployment --name nginx-service --port=8080 --target-port=80

至此,nginx-service服务创建成功,其自动支持了以下功能:

  • 服务注册:在DNS中写入了nginx-service的记录
  • 服务发现:其它服务调用nginx-service时,会从DNS中获取到对应的Service记录,并根据Service找到Pod的ip
  • 负载均衡:一个Service可以对应多个Pod,访问Service会自动转发到其中某一个Pod

为了验证上述功能,可以进入容器访问对应服务:

kubectl get pod # 获取pod name
kubectl exec -it nginx-deployment-6b7f675859-dn9jj bash # 进入对应的pod容器
curl nginx-service:8080 # 访问nginx-service服务

正常将会返回nginx首页的html文本。

如果使用minikube,可以使用minikube service命令暴露服务,如下所示:

minikube service nginx-service

随后浏览器会自动打开nginx首页。

四、总结

SpringCloud通过Erueka、Nacos等注册中心实现服务注册与服务发现,通过Ribbon、SpringCloudLoadBalancer等负载均衡组件实现客户端的负载均衡。

K8S通过Service等对象实现服务注册、服务发现以及服务端的负载均衡。

可以看到,SpringCloud与K8S中的服务注册、服务发现与负载均衡的角度不同:SpringCloud偏向于开发,而K8S则更偏向于运维。纯K8S、纯SpringCloud、以及SpringCloud与K8S混用这三种场景都可能存在,不能说哪个一定更好,只能视情况而定。比如说:技术栈是Java,并且没有专职运维或熟悉K8S的人员,那么SpringCloud就是很好的选择;如果技术栈为Go等语言,或者有容器化部署的需求与能力,则可以考虑更通用的K8S。

同时,除了服务注册、服务发现以及负载均衡外,SpringCloud与K8S还有其它技术重叠的地方,如:配置管理、服务路由(网关)等等,后续也将继续分析SpringCloud与K8S在微服务中的其它不同用法。

参考文档

  1. 《深入理解SpringCloud与实战 方剑 著》
  2. SpringCloud官方网站
  3. 黑马程序员SpringCloud微服务技术栈教程
  4. K8S官方网站