微服务 Spring Cloud 实战 Eureka+Gateway+Feign+Hystrix
前言
我所在项目组刚接到一个微服务改造需求,技术选型为 Spring Cloud,具体需求是把部分项目使用 Spring Cloud 技术进行重构。本篇文章 中介绍了 Eureka、Gateway、Feign 和 Hystrix 这些组件的用途,我整合这几个组件写了一个 demo,涉及四个工程分别是:注册中心 Eureka、网关 Gateway、服务 1、服务 2,涉及的功能分别有:使用 Zuul 配置动态路由、使用 ZuulFilter 过滤器实现 IP 白名单、使用 Feign 实现负载均衡的服务调用、使用 Hystrix 实现服务隔离、熔断、降级。
概念介绍
Eureka
Eureka 是 Spring Cloud 集合的重要组件之一,作为服务注册及发现的中心,即全部服务都会注册到 Eureka,其他工程需要使用服务时也是从 Eureka 得到(即服务发现)。Eureka 区分 server 和 client 两个性质,一般来说除 Eureka 工程外都是 client,比如:Gateway 工程是作为 Eureka 的 client。
Eureka 是可以集群部署,防止单个 Eureka 场景下宕机后导致 client 获取不了最新的服务列表(每个 client 都会缓存一份服务列表到本地),也可以防止 Eureka 宕机后 client 工程重启后获取不了服务列表,而导致无法使用服务。生产环境一般都是部署两个或者三个 Eureka 节点。
Gateway
Gateway 也是 Spring Cloud 集合的组件之一,主要是做动态路由(类似 Nginx)和请求过滤。动态路由是根据自定义的配置,把请求转发到指定的工程,类似于 Nginx;请求过滤基于 Filter 链的方式实现鉴权(比如:调用接口时校验是否携带 token)、限流(比如:固定哪些 IP 才可以调用接口)、监控(记录所有请求记录加以分析)等功能。
Feign
Feign 也是 Spring Cloud 集合的组件之一,是用来实现负载均衡的服务调用。提起 Feign 往往会想起 Ribbon 这个组件,Ribbon 也是实现负载均衡的服务调用,Ribbon 需要结合 RestTemplate 模板使用,每个服务调用的类都要注入 RestTemplate,用起来比较麻烦,Feign 是在 Ribbon 的基础上再进行封装,只需创建一个接口并使用注解方式配置对应的哪个实例及路径即可。
Hystrix
Hystrix 也是 Spring Cloud 集合的组件之一,是用来实现服务隔离、熔断、降级。隔离是把每个请求在不同的线程上执行,从而实现服务调用过程中出现问题也不影响其他服务的调用;熔断是在服务调用过程中,如果失败次数比例超过阈值(默认 50%),此时熔断器会切换到 open 状态(即所有请求都直接失败),熔断器在 open 状态保持一段时间后(默认 5 秒),会自动切换到 half-open 状态,此时如果有请求过来,则会根据请求结果来切换熔断器的状态,如果请求成功则把状态切换到 close,如果失败则切换到 open;降级是在请求过程中出现超时或者异常的情况下,直接中断请求避免拖垮整个微服务,返回自定义的信息(比如:服务繁忙),从而达到服务降级的效果。
Eureka 工程代码分析
代码结构
pom.xml 文件配置
下面配置是工程需要使用的所有 jar 和 maven 打包策略。
4.0.0
org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE
com.example
demo-Eureka
0.0.1-SNAPSHOT
demo-Eureka
Demo project for Spring Boot
1.8
Dalston.SR1
org.springframework.boot
spring-boot-starter
org.springframework.cloud
spring-cloud-starter-Eureka-server
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
true
启动类的配置
启动类代码非常简单,加上 @SpringBootApplication 和 @EnableEurekaServer 即可,@SpringBootApplication 是开启自动配置;@EnableEurekaServer 是开启 EurekaServer 服务,即此工程作为 Eureka 的服务端,即服务注册及发现的中心。
@SpringBootApplication
@EnableEurekaServer //开启 EurekaServer 服务
public class DemoEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(DemoEurekaApplication.class, args);
}
}
application.yml 配置
application.yml 文件中配置了注册中心的实例名为 registry-server,端口为 8888,主机名为 127.0.0.1(也可以是域名),不优先使用 ip,唯一 id 是由 ip 地址 + 端口(比如:127.0.0.1:8888/)等等,具体配置说明请看下面的代码。
spring:
application:
name: registry-server
server:
port: 8888
Eureka:
instance:
hostname: 127.0.0.1
prefer-ip-address: false #是否优先使用 ip 地址(与 hostname 相比)
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
client:
register-with-Eureka: false #实例是否在 Eureka 服务器上注册自己的信息以供其他服务发现,默认为 true
fetch-registry: false #此客户端是否获取 Eureka 服务器注册表上的注册信息,默认为 true
service-url:
defaultZone: http://${Eureka.instance.hostname}:${server.port}/Eureka/ #与 Eureka 注册服务中心的通信地址
server:
enable-self-preservation: false #服务端开启自我保护模式。无论什么情况,服务端都会保持一定数量的服务。避免 client 与 server 的网络问题,而出现大量的服务被清除。
response-cache-update-interval-ms: 1000 #多长时间更新一次缓存中的服务注册数据(单位:毫秒)
eviction-interval-timer-in-ms: 3000 #开启清除无效服务的定时任务并设置时间间隔(单位:毫秒),默认 1 分钟
Gateway 工程代码分析
代码结构
pom.xml 文件配置
下面配置是工程需要使用的所有 jar 和 maven 打包策略。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
com.example
demo-Gateway
0.0.1-SNAPSHOT
demo-Gateway
Demo project for Spring Boot
UTF-8
UTF-8
1.8
Greenwich.SR2
org.springframework.cloud
spring-cloud-starter-netflix-Eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
io.springfox
springfox-swagger2
2.6.1
io.springfox
springfox-swagger-ui
2.6.1
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
true
启动类的配置
启动类代码非常简单,加上 @SpringBootApplication 和 @EnableEurekaServer 即可,@SpringBootApplication 是开启自动配置;@EnableEurekaServer 是开启网关服务。
@EnableZuulProxy
@SpringBootApplication
public class DemoGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DemoGatewayApplication.class, args);
}
}
application.yml 配置
application.yml 文件中配置了注册中心的实例名为 Gateway,端口为 9999,Eureka 地址(比如:127.0.0.1:8888/Eureka/),路由规则等等,具体配置说明请看下面的代码。
spring:
application:
name: Gateway
server:
port: 9999
Eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8888/Eureka/
instance:
hostname: com.example.Gateway
prefer-ip-address: true #是否优先使用 ip 地址
instance-id: ${spring.cloud.client.ip-address}:${server.port}
non-secure-port-enabled: true #是否启用 http 通信端口
secure-port-enabled: false #是否启用 https 通信端口
zuul:
routes:
local: #本地工程路由配置
path: /**
stripPrefix: false
serviceId: forward:/
user1: #service-user1 工程路由配置
path: /user1/**
stripPrefix: false
serviceId: service-user1
user2: #service-user2 工程路由配置
path: /user2/**
stripPrefix: false
serviceId: service-user2
IP 白名单代码分析
下面代码是使用 ZuulFilter 过滤器机制实现 IP 限流功能,在把请求转发到具体实例之前,判断请求的 ip 是否在白名单内,如果是则允许访问,否则禁止访问,以达到 IP 限流效果。
package com.example.demoGateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
* IP 白名单,即 IP 限流
*/
@Component
public class IpWhitelistFilter extends ZuulFilter {
private Logger logger = LogManager.getLogger(getClass());
private static List ipWhiteList = new ArrayList<>();
/**
* 为了方便演示,此处固定写死白名单内容,正常来说是读外部数据库,比如 mysql、redis 等
*/
static {
ipWhiteList.add("127.0.0.1");
ipWhiteList.add("localhost");
}
/**
* pre:在请求被路由(转发)之前调用
* route:在路由(请求)转发时被调用
* error:服务网关发生异常时被调用
* post:在路由(转发)请求后调用
* @return
*/
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String remoteHost = request.getRemoteHost(); //获取请求 IP
logger.log(Level.INFO, request.getMethod() + " request ip : {}", remoteHost);
if (!(ipWhiteList.contains(remoteHost))) {
logger.log(Level.WARN, request.getMethod() + " request from " + remoteHost + " is unauthorized.");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
logger.log(Level.INFO, "access ip ok");
return null;
}
}
service-user1 服务工程(Feign + Hystrix)代码分析
代码结构
pom.xml 文件配置
下面配置是工程需要使用的所有 jar 和 maven 打包策略。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
com.example
demo-service-user1
0.0.1-SNAPSHOT
demo-service-user1
Demo project for Spring Boot
UTF-8
UTF-8
1.8
Greenwich.SR2
org.springframework.boot
spring-boot-starter
org.springframework.cloud
spring-cloud-starter-netflix-Eureka-client
org.springframework.cloud
spring-cloud-starter-openFeign
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-Hystrix
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
true
启动类的配置
启动类代码非常简单,加上 @SpringBootApplication、@EnableFeignClients 和 @EnableHystrix 即可,@SpringBootApplication 是开启自动配置;@EnableFeignClients 是开启 Feign 服务,即负载均衡的服务调用;@EnableHystrix 是开启熔断服务。
@EnableHystrix //开启熔断服务
@EnableFeignClients //开启 Feign 服务,即负载均衡的服务调用
@SpringBootApplication
public class DemoServiceUser1Application {
public static void main(String[] args) {
SpringApplication.run(DemoServiceUser1Application.class, args);
}
}
application.yml 配置
application.yml 文件中配置了注册中心的实例名为 service-user1,端口为 8081,Eureka 地址(比如:127.0.0.1:8888/Eureka/),是否开启 Hystrix 服务等等,具体配置说明请看下面的代码。
spring:
application:
name: service-user1
server:
port: 8081
Eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8888/Eureka/
instance:
hostname: com.example.user1
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
non-secure-port-enabled: true
secure-port-enabled: false
Feign:
Hystrix:
enabled: true
Controller 类和 Service 类
下面代码是微服务中的 service-user1 工程调用 service-user2 工程接口,使用 Feign 组件进行服务调用,使用 Hystrix 组件实现熔断功能。
package com.example.demo.controller;
import com.example.demo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
/**
* 本工程接口
* @return
*/
@GetMapping(value = "/user1/test1")
public String test1() {
return "this is service-user1 service test1 method";
}
/**
* 调用 service-user2 服务接口
* @return
*/
@GetMapping(value = "/user1/test2")
public String test2() {
return demoService.user2test2();
}
/**
* 调用 service-user2 服务接口,验证熔断功能
* @return
*/
@RequestMapping(value = "/user1/test3")
public String test3() {
return demoService.user2test3();
}
}
package com.example.demo.service;
import org.springframework.cloud.openFeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* service-user2 服务接口
*/
@Component
@FeignClient(name = "service-user2", fallback = DemoServiceFallback.class)
public interface DemoService {
@RequestMapping(value = "/user2/test2", method = RequestMethod.GET)
String user2test2();
@RequestMapping(value = "/user2/test3", method = RequestMethod.GET)
String user2test3();
}
package com.example.demo.service;
import org.springframework.stereotype.Component;
/**
* DemoService 熔断实现
*/
@Component
public class DemoServiceFallback implements DemoService{
@Override
public String user2test2() {
return "this is service-user2 service test2 fallback method";
}
@Override
public String user2test3() {
return "this is service-user2 service test3 fallback method";
}
}
service-user2 服务工程代码分析
代码结构
pom.xml 文件配置
下面配置是工程需要使用的所有 jar 和 maven 打包策略。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
com.example
demo-service-user2
0.0.1-SNAPSHOT
demo-service-user2
Demo project for Spring Boot
UTF-8
UTF-8
1.8
Greenwich.SR2
org.springframework.boot
spring-boot-starter
org.springframework.cloud
spring-cloud-starter-netflix-Eureka-client
org.springframework.cloud
spring-cloud-starter-openFeign
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
true
启动类的配置
启动类代码非常简单,加上 @SpringBootApplication 即可,@SpringBootApplication 是开启自动配置。
@SpringBootApplication
public class DemoServiceUser2Application {
public static void main(String[] args) {
SpringApplication.run(DemoServiceUser2Application.class, args);
}
}
application.yml 配置
application.yml 文件中配置了注册中心的实例名为 service-user2,端口为 8082,Eureka 地址(比如:127.0.0.1:8888/Eureka/)等等,具体配置说明请看下面的代码。
spring:
application:
name: service-user2
server:
port: 8082
Eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8888/Eureka/
instance:
hostname: com.example.user2
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
non-secure-port-enabled: true
secure-port-enabled: false
Controller 类
下面代码是两个非常简单的接口,单纯用来测试服务调用是否成功及熔断是否起效。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping(value = "/user2/test2")
public String test2() {
return "this is service-user2 service test2 method";
}
@GetMapping(value = "/user2/test3")
public String test3() {
int i = 1/0; //故意抛异常,测试熔断功能
return "this is service-user2 service test3 method";
}
}
接口测试
1. 查看 Eureka 基本信息和注册实例信息。
url:http://127.0.0.1:8888/
2. 测试接口 test1,经过统一网关,仅涉及 service-user1 工程。
url:http://127.0.0.1:9999/user1/test1
响应结果:
this is service-user1 service test1 method
3. 测试接口 test2,经过统一网关,涉及 service-user1 和 service-user2 工程,测试 Feign。
url:http://127.0.0.1:9999/user1/test2
响应结果:
this is service-user2 service test2 method
4. 测试接口 test3,经过统一网关,涉及 service-user1 和 service-user2 工程,测试 Feign + Hystrix。
url:http://127.0.0.1:9999/user1/test3
响应结果:
this is service-user2 service test3 fallback method
注意事项
1. Eureka 工程必须把是否在 Eureka 上注册自己标识设置为 false,该标识默认为 true,因为本身就是 Eureka 服务端,不需要自己注册自己;把是否获取 Eureka 上的服务列表标识设置为 false,该标识默认为 true,因为 Eureka 客户端才需要获取服务列表。
2. 权限控制模块应该放在网关 Gateway 工程上实现,因为网关是统一入口,所以请求都经过这里,最适合做统一鉴权。
3. 如果网关 Gateway 工程也提供接口,而且使用了 ZuulFilter 过滤器(比如:上面提到的 IP 白名单),必须配置 serviceId 为 forward:/,不然调用接口时不经过 ZuulFilter 过滤器。
zuul:
routes:
local: #本地工程路由配置,名字随意起
path: /**
stripPrefix: false
serviceId: forward:/
4. 如果使用 Hystrix 组件实现熔断功能,则需要配置 Hystrix 的 enabled 属性为 true。
Feign:
Hystrix:
enabled: true
总结
通过这次的微服务 Spring Cloud 实战,让我们认识了 Eureka、Gateway、Feign 和 Hystrix 是什么,可以实现什么功能;让我们掌握了如何配置注册中心 Eureka,如何配置网关 Gateway 的动态路由,如何使用 ZuulFilter 过滤器实现相关业务,如何使用 Feign 实现服务之间的负载均衡调用,如何使用 Hystrix 实现熔断机制等技术。