# 服务的注册与发现(Eureka)

# SpringCloud 简介

以下简介来自 SpringCloud 官网(https://spring.io/projects/spring-cloud)

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.

quote from https://spring.io/projects/spring-cloud

Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。分布式系统的协调导致了样板模式,使用 Spring Cloud 开发人员可以快速地支持实现这些模式的服务和应用程序。他们将在任何分布式环境中运行良好,包括开发人员自己的笔记本电脑,裸机数据中心,以及 Cloud Foundry 等托管平台。

quote from https://www.springcloud.cc/spring-cloud-dalston.html

# 创建 Eureka 服务注册中心及服务提供者

ps. 学习完 Eureka 之后,可以再了解一下 Consul。

# 创建 maven 主工程

你可以选择从 https://start.spring.io/ 下载初始化项目。

也可以 new >> project >> Spring Initializr 进行初始化项目。

# 创建 Eureka Server 注册服务中心

1.1 创建 module 并选择 Eureka Server 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

1.2 在启动类中添加注解 @EnableEurekaServer

package com.xfc.eureka.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

1.3 配置文件 application.yml

server:
  port: 8761
# 通过 eureka.client.registerWithEureka:false 和 fetchRegistry:false 来表明自己是一个 eureka server.
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
spring:
  application:
    name: eurka-server

1.4 启动注册服务中心

​ 启动主类并访问 http://localhost:8761/

# 创建 Eureka Client 服务提供者

1.1 创建 module 并选择 Eureka Discovery Client 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

1.2 在启动类中添加注解 @EnableEurekaClient

package com.xfc.eureka.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class, args);
    }
}

1.3 配置文件 application.yml

server:
  port: 8762
# 指定当前服务名称
spring:
  application:
    name: eureka-client-1
# 指定 eureka-server
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

1.4 启动 Eureka Client

​ 注意:启动 Eureka Client 时应当保证 Eureka Client 也处于启动状态。

​ 启动主类并访问 http://localhost:8761/ 进行查看。

​ 提示信息: EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. 参考 https://blog.csdn.net/qq_26975307/article/details/86563590

Instances currently registered with Eureka 列表中查看已注册的 Eureka 服务实例。

# Eureka 服务注册成功

至此,一个 Eureka 服务即注册成功,同时注册多个 Eureka 服务,与 Eureka Client 方式相同,只须注意端口冲突即可。

# 服务消费者(rest+ribbon)

在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于 http restful 的。Spring cloud 有两种服务调用方式,一种是 ribbon+restTemplate,另一种是 feign。

# ribbon 简介

Ribbon is a client side IPC library that is battle-tested in cloud. It provides the following features

  • Load balancing

  • Fault tolerance

  • Multiple protocol (HTTP, TCP, UDP) support in an asynchronous and reactive model

  • Caching and batching

    ---- 摘自 https://github.com/Netflix/ribbon

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的,包括后续我们将要介绍的 Feign,它也是基于 Ribbon 实现的工具。

---- 摘自 https://www.jianshu.com/p/1bd66db5dc46

# 准备工作

根据 springcloud001 文档,同时创建 Eureka Server 和两个 Eureka Client。

注意:除端口不同外,两个 Eureka Client 的代码完全相同,以不同端口来模拟负载均衡。

# 创建服务消费者

# 新建一个 module,并添加 ribbon 及 eureka client 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

# 配置文件

server:
  port: 8764
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: service-ribbon

# 配置启动类

在启动类中添加 @EnableEurekaClient@EnableDiscoveryClient 注解,并注入一个开启负载均衡的 RESTFul 模板。

package com.xfc.service.ribbon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ServiceRibbonApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRibbonApplication.class, args);
    }
    @Bean
    @LoadBalanced// 开启负载均衡
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

# 在 EurekaClient 的服务中编写接口

这里直接在两个 EurekaClient 的启动类中编写 RESTFul 风格的接口即可,即修改启动类,如下:

package com.xfc.eureka.client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableEurekaClient
@RestController
public class EurekaClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClient2Application.class, args);
    }
    @Value("${server.port}")
    String port;
    @RequestMapping("/test")
    public String test(@RequestParam(value = "name", defaultValue = "ErDong") String name) {
        return "hi " + name + " ,this test api is from port: " + port;
    }
}

# 在 ribbon 模块中添加测试

5.1 新建 TestService.java

package com.xfc.service.ribbon.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 12:46
 * @Description:
 */
@Service
public class TestService {
    @Autowired
    RestTemplate restTemplate;
    public String test(String name) {
        return restTemplate.getForObject("http://EUREKA-CLIENT/test?name=" + name, String.class);
    }
}

5.1 新建 TestController.java

package com.xfc.service.ribbon.controller;
import com.xfc.service.ribbon.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 12:56
 * @Description:
 */
@RestController
public class TestController {
    @Autowired
    TestService testService;
    @GetMapping(value = "/test")
    public String test(@RequestParam String name) {
        return testService.test(name);
    }
}

# 启动服务及测试负载均衡

  1. 分别启动 EurekaServer,两个 EurekaClient 及 ServiceRibbon。

  2. 访问 http://localhost:8761/ 已注册的服务实例。

    可以看到端口分别为 87628763 两个 EUREKA-CLIENT 实例,及一个端口为 8764EUREKA-CLIENT 实例。

  3. 负载均衡测试

    多次访问 http://localhost:8764/test?name=testUser

    可以看到相应结果如下:

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    ……

  4. 测试结论

    从测试结果我们可以看到,端口分别为 8762 何 8763 的两个 EUREKA-CLIENT 实例被轮流调用,即实现了负载均衡。

# 服务消费者(Feign)

在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于 http restful 的。Spring cloud 有两种服务调用方式,一种是 ribbon+restTemplate,另一种是 feign。

# Feign 简介

Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.

---- 摘自 https://github.com/OpenFeign/feign

Feign 是一个声明式的伪 Http 客户端,它使得写 Http 客户端变得更简单。使用 Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用 Feign 注解和 JAX-RS 注解。Feign 支持可插拔的编码器和解码器。Feign 默认集成了 Ribbon,并和 Eureka 结合,默认实现了负载均衡的效果。

简而言之:

  • Feign 采用的是基于接口的注解
  • Feign 整合了 ribbon,具有负载均衡的能力
  • 整合了 Hystrix,具有熔断的能力

---- 摘自 https://blog.csdn.net/forezp/article/details/81040965

# 准备工作

根据 springcloud001 文档,同时创建 Eureka Server 和两个 Eureka Client。

注意:除端口不同外,两个 Eureka Client 的代码完全相同,以不同端口来模拟负载均衡。

# 创建服务消费者

# 新建一个 module,并添加 feign 及 eureka client 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

# 配置文件

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8765
spring:
  application:
    name: service-feign

# 配置启动类

在启动类中添加 @EnableEurekaClient @EnableFeignClients@EnableDiscoveryClient 注解开启 Feign 的功能。

package com.xfc.service.feign;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceFeignApplication.class, args);
    }
}

# 定义一个 Feign 接口

注意: 此部分与 springcloud002 中的部分相同。

这里直接在两个 EurekaClient 的启动类中编写 RESTFul 风格的接口即可,即修改启动类,如下:

package com.xfc.eureka.client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableEurekaClient
@RestController
public class EurekaClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClient2Application.class, args);
    }
    @Value("${server.port}")
    String port;
    @RequestMapping("/test")
    public String test(@RequestParam(value = "name", defaultValue = "ErDong") String name) {
        return "hi " + name + " ,this test api is from port: " + port;
    }
}

# 在 feign 模块中添加测试

4.1 新建 TestService.java

package com.xfc.service.feign.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 14:38
 * @Description:
 */
@FeignClient(value = "EUREKA-CLIENT")// 指定服务名称
public interface TestService {
    // 指定服务方法及参数
    @RequestMapping(value = "/test",method = RequestMethod.GET)
    String test(@RequestParam(value = "name") String name);
}

5.2 新建 TestController.java

package com.xfc.service.feign.controller;
import com.xfc.service.feign.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 14:40
 * @Description:
 */
@RestController
public class TestController {
    @Autowired
    TestService testService;
    @GetMapping(value = "/test")
    public String test(@RequestParam String name) {
        return testService.test(name);
    }
}

# 启动服务及测试负载均衡

  1. 分别启动 EurekaServer,两个 EurekaClient 及 ServiceFeign。

  2. 访问 http://localhost:8761/ 已注册的服务实例。

    可以看到端口分别为 87628763 两个 EUREKA-CLIENT 实例,及一个端口为 8765SERVICE-FEIGN 实例。

  3. 负载均衡测试

    多次访问 http://localhost:8765/test?name=testUser

    可以看到相应结果如下:

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    ……

  4. 测试结论

    从测试结果我们可以看到,端口分别为 8762 何 8763 的两个 EUREKA-CLIENT 实例被轮流调用,即实现了负载均衡。

# 熔断器(Hystrix)

在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在 Spring Cloud 可以用 RestTemplate+Ribbon 和 Feign 来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩” 效应。为了解决这个问题,业界提出了熔断器(断路器)模型。

# 熔断器简介

Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.

---- 摘自 https://github.com/Netflix/hystrix

Hystrix 中文介绍可参考 https://www.jianshu.com/p/76dc45523807

# 准备工作

根据 springcloud001 文档,同时创建 Eureka Server 和两个 Eureka Client。

注意:除端口不同外,两个 Eureka Client 的代码完全相同,以不同端口来模拟负载均衡。

# 在 ribbon 中使用断路器

注意: 此部分内容基于 springcloud002 文档。

#serice-ribbon 模块添加依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

# 配置启动类

在启动类中添加 @EnableHystrix 注解开启 Hystrix 功能。

package com.xfc.service.ribbon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableHystrix
public class ServiceRibbonApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRibbonApplication.class, args);
    }
    @Bean
    @LoadBalanced// 开启负载均衡
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

# 改写 TestService

package com.xfc.service.ribbon.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 12:46
 * @Description:
 */
@Service
public class TestService {
    @Autowired
    RestTemplate restTemplate;
    // 对当前方法创建熔断器的功能,并指定 fallbackMethod 熔断方法
    @HystrixCommand(fallbackMethod = "testError")
    public String test(String name) {
        return restTemplate.getForObject("http://EUREKA-CLIENT/test?name=" + name, String.class);
    }
    //fallbackMethod 熔断方法
    public String testError(String name) {
        return "hi,"+name+",sorry,error!";
    }
}

# 启动服务及测试熔断器功能

  1. 分别启动 EurekaServer,两个 EurekaClient 及 ServiceRibbon。

  2. 访问 http://localhost:8761/ 已注册的服务实例。

    可以看到端口分别为 87628763 两个 EUREKA-CLIENT 实例,及一个端口为 8764SERVICE-FEIGN 实例。

  3. 熔断器功能测试

    多次访问 http://localhost:8764/test?name=testUser

    可以看到相应结果如下:

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    ……

    此时断开端口为 8763EUREKA-CLIENT 实例。

    再次进行多次访问,结果如下:

    hi,testUser,sorry,error!

    hi testUser ,this test api is from port: 8762

    hi,testUser,sorry,error!

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8762

    ……

    此时再断开端口为 8762EUREKA-CLIENT 实例。

    再次进行多次访问,结果如下:

    hi,testUser,sorry,error!

    hi,testUser,sorry,error!

    hi,testUser,sorry,error!

    hi,testUser,sorry,error!

    ……

  4. 测试结论

    从测试结果我们可以看到,端口分别为 8762 何 8763 的两个 EUREKA-CLIENT 实例在正常启用时,正常实现负载均衡,当其中某个服务实例出现故障时,客户端会进入 fallbackMethod 指定的熔断方法,直接返回一组字符串,而不是等待响应超时,这很好的控制了容器的线程阻塞。

# 在 feign 中使用断路器

Feign 是自带断路器的,在 D 版本的 Spring Cloud 之后,默认关闭。

#serice-feign 模块添加依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

# 配置文件

添加熔断器配置:

# feign 开启 hystrix 支持
feign:
  hystrix:
    enabled: true

# 改写 TestService

新建 TestService 接口的实现类 TestServiceHystric.java 作为熔断器。

package com.xfc.service.feign.service.hystric;
import com.xfc.service.feign.service.TestService;
import org.springframework.stereotype.Component;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 15:39
 * @Description:
 */
@Component
public class TestServiceHystric implements TestService {
    @Override
    public String test(String name) {
        return "hi,"+name+",sorry,error!";
    }
}

在 @FeignClient 注解中指定 fallback

package com.xfc.service.feign.service;
import com.xfc.service.feign.service.hystric.TestServiceHystric;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 0027 14:38
 * @Description:
 */
@FeignClient(value = "EUREKA-CLIENT", fallback = TestServiceHystric.class)// 指定服务名称,指定熔断器
public interface TestService {
    // 指定服务方法及参数
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    String test(@RequestParam(value = "name") String name);
}

# 启动服务及测试熔断器功能

与 ribbon 中的测试方法相同。

# 路由网关(zuul)

在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、断路器、智能路由、配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统。

在 Spring Cloud 微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(zuul、Ngnix),再到达服务网关(zuul 集群),然后再到具体的服。,服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理(下一篇文章讲述),配置服务的配置文件放在 git 仓库,方便开发人员随时改配置。

# Zuul 简介

Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. As an edge service application, Zuul is built to enable dynamic routing, monitoring, resiliency and security. It also has the ability to route requests to multiple Amazon Auto Scaling Groups as appropriate.

---- 摘自 https://github.com/Netflix/zuul/wiki

Zuul 的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如 /api/user 转发到到 user 服务,/api/shop 转发到到 shop 服务。zuul 默认和 Ribbon 结合实现了负载均衡的功能。

zuul 有以下功能:

  • Authentication
  • Insights
  • Stress Testing
  • Canary Testing
  • Dynamic Routing
  • Service Migration
  • Load Shedding
  • Security
  • Static Response handling
  • Active/Active traffic management

---- 摘自 https://blog.csdn.net/forezp/article/details/81041012

# 准备工作

接续上一文档,在原有工程上添加新的模块。

# 创建 zuul 路由网关

# 新建一个 module,并添加 zuul 及 eureka client 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

# 配置文件

server:
  port: 8769
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: service-zuul
# 以 /api-a/ 开头的请求转发至 service-ribbon 服务
# 以 /api-b/ 开头的请求转发至 service-feign 服务
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: service-ribbon
    api-b:
      path: /api-b/**
      serviceId: service-feign

# 配置启动类

在启动类中添加 @EnableEurekaClient@EnableZuulProxy 开启 zuul 的功能。

package com.xfc.service.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
@EnableDiscoveryClient
public class ServiceZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceZuulApplication.class, args);
    }
}

# 启动服务及测试路由网关

  1. 分别启动 EurekaServer,两个 EurekaClient 及 ServiceRibbon。

  2. 分别启动 ServiceRibbon 和 ServiceFeign 两个服务消费者。

  3. 启动 ServiceZuul 网关。

  4. 访问:

    多次访问 http://localhost:8769/api-a/test?name=testUser

    结果如下:

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    ……

    多次访问 http://localhost:8769/api-b/test?name=testUser

    结果如下:

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    hi testUser ,this test api is from port: 8762

    hi testUser ,this test api is from port: 8763

    ……

  5. 测试结论

    zuul 起到路由的作用,它将不同请求分向不同的服务消费者进行处理。

# 服务过滤

zuul 不仅只是路由,并且还能过滤,做一些安全验证。

# 添加过滤器

新建 TokenFilter.java,用于过滤 token。

package com.xfc.service.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
 * @Auther: ErDong
 * @Email: xfc_exclave@163.com
 * @Date: 2019/12/27 21:29
 * @Description:
 */
@Component
public class TokenFilter extends ZuulFilter {
    private static Logger log = LoggerFactory.getLogger(TokenFilter.class);
    /**
     * filterType:返回一个字符串代表过滤器的类型,在 zuul 中定义了四种不同生命周期的过滤器类型,如下:
     * 1. pre:前置过滤器
     * 2. routing:路由之时
     * 3. post: 路由之后
     * 4. rror:发送错误调用
     *
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }
    /**
     * 过滤的顺序,数字越大,优先级越低
     *
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }
    /**
     * 是否要进行过滤
     *
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }
    /**
     * 过滤器的具体逻辑
     *
     * @return
     */
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString()));
        Object accessToken = request.getParameter("token");
        if (accessToken == null) {
            log.warn("token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("token is empty");
            } catch (Exception e) {
            }
            return null;
        }
        log.info("ok");
        return null;
    }
}

# 启动服务及测试路由过滤

  1. 访问

    访问 http://localhost:8769/api-a/test?name=testUser

    结果:token is empty

    访问 http://localhost:8769/api-b/test?name=testUser

    结果:token is empty

    访问 http://localhost:8769/api-a/test?name=testUser&token=abc

    结果:hi testUser ,this test api is from port: 8762

    访问 http://localhost:8769/api-b/test?name=testUser&token=abc

    结果:hi testUser ,this test api is from port: 8763

  2. 测试结论

    zuul 实现了服务过滤。

# 分布式配置中心(Spring Cloud Config)

使用配置服务来保存各个服务的配置文件,即 Spring Cloud Config。

# 简介

Spring Cloud Config 为分布式系统中的外部配置提供服务器和客户端支持。使用 Config Server ,您可以在所有环境中管理应用程序的外部属性。客户端和服务器上的概念映射与 Spring EnvironmentPropertySource 抽象相同,因此它们与 Spring 应用程序非常契合,但可以与任何以任何语言运行的应用程序一起使用。随着应用程序通过从开发人员到测试和生产的部署流程,您可以管理这些环境之间的配置,并确定应用程序具有迁移时需要运行的一切。服务器存储后端的默认实现使用 git,因此它轻松支持标签版本的配置环境,以及可以访问用于管理内容的各种工具。可以轻松添加替代实现,并使用 Spring 配置将其插入。

---- 摘自 https://www.springcloud.cc/spring-cloud-config.html

在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在 Spring Cloud 中,有分布式配置中心组件 spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程 Git 仓库中。在 spring cloud config 组件中,分两个角色,一是 config server,二是 config client。
---- 摘自 https://blog.csdn.net/forezp/article/details/81041028

# 构建 Config Server

# 新建一个 module,并添加 config server 依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>

# 配置文件

server:
  port: 8888
# 如果 Git 仓库为公开仓库,可以不填写用户名和密码
spring:
  application:
    name: config-server
  cloud:
    config:
      label: master # 配置仓库的分支
      server:
        git:
          uri: https://github.com/forezp/SpringcloudConfig/ # 配置 git 仓库地址
          searchPaths: respo # 配置仓库路径
          username: # 访问 git 仓库的用户名
          password: # 访问 git 仓库的用户密码

# 配置启动类

在启动类中添加 @EnableConfigServer 注解开启配置服务器的功能。

package com.xfc.config.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

# 启动服务及测试 ConfigServer

启动服务后,访问 http://localhost:8888/config-client-dev.properties

结果:

democonfigclient.message: hello spring io

foo: foo version 21

结论:配置服务中心可以从远程程序获取配置信息。

http 请求地址和资源文件映射如下:

/application}/{profiles:.\[(-\].)

/{application}/{profiles}/

/{application}-{profiles}.properties

/{label}/{application}-{profiles}.properties

{application}-{profiles}.json

/{label}/{application}-{profiles}.json

/{application}-{profiles}.yml 或 /{application}-{profiles}.yml

/{label}/{application}-{profiles}.yml 或 /{label}/{application}-{profiles}.yml

# 构建 Config Client

# 新建一个 module,并添加 config client 依赖。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

# 配置文件

server:
  port: 8881
spring:
  application:
    name: config-client
  cloud:
    config:
      label: master
      profile: dev # dev:开发环境,test:测试环境,pro:生产环境
      uri: http://localhost:8888/ # 指明配置服务中心的网址

# 创建测试 API

在启动类中添加 API。

package com.xfc.config.client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class ConfigClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientApplication.class, args);
    }
    @Value("${foo}")
    String foo;
    @RequestMapping(value = "/getFoo")
    public String getFoo() {
        return foo;
    }
}

启动并访问 http://localhost:8881/getFoo

结果:foo version 21

结论:config-client 从 config-server 获取了 foo 的属性,而 config-server 是从 git 仓库读取的。

# 高可用的分布式配置中心

# 改造 config-server

  1. 添加 eureka client 依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 修改配置文件

    server:
      port: 8888
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    # 如果 Git 仓库为公开仓库,可以不填写用户名和密码
    spring:
      application:
        name: config-server
      cloud:
        config:
          label: master # 配置仓库的分支
          server:
            git:
              uri: https://github.com/forezp/SpringcloudConfig/ # 配置 git 仓库地址
              searchPaths: respo # 配置仓库路径
              username: # 访问 git 仓库的用户名
              password: # 访问 git 仓库的用户密码
    1. 主类添加 @EnableEurekaClient 注解

# 改造 config-client

  1. 添加 eureka client 依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 修改配置文件

    server:
      port: 8881
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    spring:
      application:
        name: config-client
      cloud:
        config:
          label: master
          profile: dev # dev:开发环境,test:测试环境,pro:生产环境
          discovery:
            enabled: true
            serviceId: config-server # 通过服务名指定配置服务中心(需要 config-server 和 config-client 都在服务注册中心注册)
    #      uri: http://localhost:8888/ # 通过网址指定配置服务中心
    1. 主类添加 @EnableEurekaClient 注解

# 启动并测试

依次启动 eureka-serverconfig-serverconfig-client

访问 http://localhost:8761/

结果:可以看到 config-serverconfig-client 均注册到服务注册中心。

访问 http://localhost:8881/getFoo

结果:foo version 21

结论: config-serverconfig-client 可以同时作为 EurekaClient 注册到服务注册中心,最终实现高可用。

# 消息总线(Spring Cloud Bus)

Spring Cloud Bus 将分布式的节点用轻量的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控。本文要讲述的是用 Spring Cloud Bus 实现通知微服务架构的配置文件的更改。

# 准备工作

下载安装 Erlang 及 RabbitMQ。

# 改造 config-client 模块

# 依赖

添加 bus-ampq 依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

# 配置文件

修改配置文件。

server:
  port: 8881
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh
spring:
  application:
    name: config-client
  cloud:
    config:
      label: master
      profile: dev # dev:开发环境,test:测试环境,pro:生产环境
      discovery:
        enabled: true
        serviceId: config-server # 通过服务名指定配置服务中心(需要 config-server 和 config-client 都在服务注册中心注册)
#      uri: http://localhost:8888/ # 通过网址指定配置服务中心
    bus:
      enabled: true
      trace:
        enabled: true
  rabbitmq:
    host: localhost
    password: guest
    port: 5672
    username: guest

# 启动类

启动类中添加类注解 @RefreshScope

# 启动服务及测试

复制一份 config-client ,端口改为 8882。

依次启动 eureka-serverconfig-server 及端口为 8881 和 8882 的两个 config-client

访问 http://localhost:8881/getFoo

访问 http://localhost:8882/getFoo

返回结果均如下:

foo version 3

此时前往配置中心仓库修改 foo 配置为 foo version 22

访问 getFoo,返回结果仍为 foo version 3

使用 http 工具发送 POST 请求

http://localhost:8881/actuator/bus-refresh

查看 config-server 控制台,显示已从配置中心仓库中获取到最新的配置文件。

再次访问 getFoo,返回结果如下:

foo version 22

结论:

当 git 文件更改的时候,通过 pc 端用 post 向端口为 8882 的 config-client 发送请求 /bus/refresh/;此时 8882 端口会发送一个消息,由消息总线向其他服务传递,从而使整个微服务集群都达到更新配置文件。

# 服务链路追踪(Spring Cloud Sleuth)

# 简介

Add sleuth to the classpath of a Spring Boot application (see below for Maven and Gradle examples), and you will see the correlation data being collected in logs, as long as you are logging requests.

---- 摘自 https://github.com/spring-cloud/spring-cloud-sleuth

Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin,你只需要在 pom 文件中引入相应的依赖即可。

# 构建 server-zipkin

# 下载 zipkin

下载地址:https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/

# 运行

java -jar D:\jar\zipkin\zipkin-server-2.10.1-exec.jar

# 访问

zipkin 默认端口为 9411

浏览器访问:http://localhost:9411

# 创建测试模块

创建模块 service-zipkin-test1service-zipkin-test2

# 创建模块并添加依赖

添加 zipkin-client 依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

# 配置文件

server:
  port: 8989
spring:
  application:
    name: service-zipkin-test2
  zipkin:
    base-url: http://localhost:9411
    sleuth:
      sampler:
        probability: 1.0

# 启动类

模块 service-zipkin-test1 启动类:

package com.xfc.service.zipkin.test1;
import brave.sampler.Sampler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.logging.Level;
import java.util.logging.Logger;
@SpringBootApplication
@RestController
public class ServiceZipkinTest1Application {
    private static final Logger LOG = Logger.getLogger(ServiceZipkinTest1Application.class.getName());
    public static void main(String[] args) {
        SpringApplication.run(ServiceZipkinTest1Application.class, args);
    }
    @Autowired
    private RestTemplate restTemplate;
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    @RequestMapping("/test1")
    public String callTest2() {
        LOG.log(Level.INFO, "interface of test1 application");
        return restTemplate.getForObject("http://localhost:8989/test2", String.class);
    }
    @RequestMapping("/info")
    public String info() {
        LOG.log(Level.INFO, "application info of test1");
        return "application info of test1";
    }
    @Bean
    public Sampler defaultSampler() {
        return Sampler.ALWAYS_SAMPLE;
    }
}

模块 service-zipkin-test2 启动类:

package com.xfc.service.zipkin.test2;
import brave.sampler.Sampler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.logging.Level;
import java.util.logging.Logger;
@SpringBootApplication
@RestController
public class ServiceZipkinTest2Application {
    public static void main(String[] args) {
        SpringApplication.run(ServiceZipkinTest2Application.class, args);
    }
    private static final Logger LOG = Logger.getLogger(ServiceZipkinTest2Application.class.getName());
    @Autowired
    private RestTemplate restTemplate;
    @RequestMapping("/test2")
    public String callTest1Info() {
        LOG.log(Level.INFO, "interface of test2 application");
        return restTemplate.getForObject("http://localhost:8988/info", String.class);
    }
    @RequestMapping("/info")
    public String home() {
        LOG.log(Level.INFO, "application info of test2");
        return "application info of test2";
    }
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    @Bean
    public Sampler defaultSampler() {
        return Sampler.ALWAYS_SAMPLE;
    }
}

# 访问测试

  • 参考上文启动 server-zipkin 服务。

  • 启动模块 service-zipkin-test1service-zipkin-test2

  • 访问:http://localhost:8989/test2,返回结果:

    application info of test1

  • 访问:http://localhost:8988/test1,返回结果:

    application info of test1

  • 访问:http://localhost:9411/,进行追踪

    即可在 依赖 中查看到两个工程模块之间的依赖关系。

# 参考

  • 方志朋的专栏