微服务场景下,每一个微服务对外暴露了一组细粒度的服务。客户端的请求可能会涉及到一串的服务调用,如果将这些微服务都暴露给客户端,那么客户端需要多次请求不同的微服务才能完成一次业务处理,增加客户端的代码复杂度。另外,对于微服务我们可能还需要服务调用进行统一的认证和校验等等。微服务架构虽然可以将我们的开发单元拆分的更细,降低了开发难度,但是如果不能够有效的处理上面提到的问题,可能会造成微服务架构实施的失败。
Zuul参考GOF设计模式中的Facade模式,将细粒度的服务组合起来提供一个粗粒度的服务,所有请求都导入一个统一的入口,那么整个服务只需要暴露一个api,对外屏蔽了服务端的实现细节,也减少了客户端与服务器的网络调用次数。这就是API服务网关(API Gateway)服务。我们可以把API服务网关理解为介于客户端和服务器端的中间层,所有的外部请求都会先经过API服务网关。因此,API服务网关几乎成为实施微服务架构时必须选择的一环。
Spring Cloud Netflix的Zuul组件可以做反向代理的功能,通过路由寻址将请求转发到后端的粗粒度服务上,并做一些通用的逻辑处理。
通过Zuul我们可以完成以下功能:
- 动态路由
- 监控与审查
- 身份认证与安全
- 压力测试: 逐渐增加某一个服务集群的流量,以了解服务性能;
- 金丝雀测试
- 服务迁移
- 负载剪裁: 为每一个负载类型分配对应的容量,对超过限定值的请求弃用;
- 静态应答处理
1. 构建网关
1.1 构建Zuul-Server
编写pom.xml文件
Zuul-Server
是一个标准的Spring Boot应用,所以还是继承自我们之前的parent:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>twostepsfromjava.cloud</groupId>
<artifactId>twostepsfromjava-cloud-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>zuul-server</artifactId>
<name>Spring Cloud Sample Projects: Zuul Proxy Server</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里我们增加了spring-cloud-starter-zuul
的依赖。
编写启动类
/**
* TwoStepsFromJava Cloud -- Zuul Proxy 服务器
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@EnableZuulProxy
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
这里我们增加了对主应用类增加了@EnableZuulProxy
,用以启动Zuul的路由服务。
编写配置文件application.properties
server.port=8280
spring.application.name=ZUUL-PROXY
eureka.client.service-url.defaultZone=http://localhost:8260/eureka
这里定义服务名称为: ZUUL-PROXY
,端口设为: 8280
。
1.2 构建User-Service
为了后面的则是我们再增加一个微服务: 用户服务。
编写pom.xml文件
同样继承自我们之前的parent:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>twostepsfromjava.cloud</groupId>
<artifactId>twostepsfromjava-cloud-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>user-service</artifactId>
<name>Spring Cloud Sample Projects: User Service Server</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>service-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写启动类
启动类和之前的Product-Service
一样,所以这里不再列出来。
编写服务接口
示例的服务接口非常简单,就是根据给定的登录名称查询一个用户信息。如下:
/**
* User API服务
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@RestController
@RequestMapping("/users")
public class UserEndpoint {
protected Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
@Value("${server.port:2200}")
private int serverPort = 2200;
@RequestMapping(value = "/{loginName}", method = RequestMethod.GET)
public User detail(@PathVariable String loginName) {
String memos = "I come form " + this.serverPort;
return new User(loginName, loginName, "/avatar/default.png", memos);
}
}
其中User
类定义在之前的service-api
项目中,代码如下:
/**
* 用户信息DTO对象
*
* @author CD826(CD826Dong@gamil.com)
* @since 1.0.0
*/
public class User {
private static final long serialVersionUID = 1L;
// ========================================================================
// fields =================================================================
private String loginName; // 用户登陆名称
private String name; // 用户姓名
private String avatar; // 用户头像
private String memos; // 信息备注
// ========================================================================
// constructor ============================================================
public User() {
}
public User(String loginName, String name, String avatar, String memos) {
this.loginName = loginName;
this.name = name;
this.avatar = avatar;
this.memos = memos;
}
// ==================================================================
// setter/getter ====================================================
// ... 省略,请自行补充 ...
}
编写配置文件application.properties
server.port=2200
spring.application.name=USER-SERVICE
eureka.client.service-url.defaultZone=http://localhost:8260/eureka
这里定义服务名称为: USER-SERVICE
,默认端口设为: 2200
。
代码修改,就是这么多,下面让我们启动进行测试。
1.3 启动测试
启动各服务
请按照下面的顺序启动各服务器:
- Service-discovery
- Product-Service
- User-Service(2200)
- User-Service(2300):
java -jar user-service-1.0.0-SNAPSHOT.jar --server.port=2300
- Zuul-Server
Ok, 服务启动后我们可以在Eureka服务器看到如下界面:
这里我们启动两个User-Service
主要是为了后面进行负载均衡测试使用。
测试路由服务
首先,我们在浏览器中输入以下地址: http://localhost:8280/product-service/products,将会显示以下界面:
然后,我们在浏览器中输入以下地址: http://localhost:8280/user-service/users/admin,将会显示以下界面:
可见,Zuul-Server
已经帮我们路由到相应的微服务。
负载均衡测试
接下来我们测试一下负载均衡是否可以正常工作。前面我们已经启动了两个User-Service
微服务,端口分别为:2200和2300。我们多次在浏览器中输入以下地址: http://localhost:8280/user-service/users/admin进行请求,我们将会看到以下信息会在屏幕中交替输出:
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2200"}
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2300"}
可见,负载均衡也是正常工作的。
Hystrix容错与监控测试
之前我们是在Mall-Web
项目中集成Hystrix
的监控,那么我们启动该服务。然后在Hystrix Dashboard中输入: http://localhost:8280/hystrix.stream,然后进行监控,那么我们将看到如下界面:
这说明,Zuul已经整合了Hystrix。
spring-cloud-starter-zuul
本身已经集成了hystrix和ribbon,所以Zuul天生就拥有线程隔离和断路器的自我保护能力,以及对服务调用的客户端负载均衡功能。但是,我们需要注意,当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用HystrixCommand
来包装,所以这类路由请求就没有线程隔离和断路器保护功能,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
2. Zuul配置
2.1 路由配置详解
或许你会觉得神奇,之前我们什么也没有配置,通过http://localhost:8280/product-service/products、http://localhost:8280/user-service/users/admin已经可以正确的访问到我们的微服务了,这就是Zuul的默认路由映射功能在起作用,那么接下来具体来看看Zuul是怎么进行路由配置的。
1) 服务路由默认规则
当我们构建API服务网关时引入Eureka时,那么Zuul会自动为每个服务都创建一个默认路由规则: 访问路径的前缀为serviceId
配置的服务名称,也就是之前为什么我们能够所使用:
http://localhost:8280/product-service/products
来访问Product-Service中所提供的products服务端点的原因。
2) 自定义微服务访问路径
配置格式为: zuul.routes.微服务Id = 指定路径,如:
zuul.routes.user-service = /user/**
这样,我们后面就可以通过/user/
来访问user-service
所提供的服务,比如之前的访问可以更改为: http://localhost:8280/user/users/admin。
所要配置的路径可以指定一个正则表达式来匹配路径,因此,/user/*
只能匹配一级路径,但是通过/user/**
可以匹配所有以/user/
开头的路径。
3) 忽略指定微服务
配置格式为: zuul.ignored-services=微服务Id1,微服务Id2…,多个微服务之间使用逗号分隔。如:
zuul.ignored-services=user-service,product-service
4) 同时指定微服务Id和对应路径
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B
5) 同时指定微服务Url和对应路径
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8080/api-a
如之前所述,通过url配置的路由不会由HystrixCommand来执行,自然,也就得不到Ribbon的负载均衡、降级、断路器等功能。所以在实施尽量使用serviceId进行配置,也可以采用下面的配置方式。
6) 指定多个服务实例及负载均衡
如果需要配置多个服务实例,则配置如下:
zuul.routes.user.path: /user/**
zuul.routes.user.serviceId: user
ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081
7) forward跳转到本地url
zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user
8) 路由前缀
可以通过zuul.prefix
可为所有的映射增加统一的前缀。如: /api
。默认情况下,代理会在转发前自动剥离这个前缀。如果需要转发时带上前缀,可以配置: zuul.stripPrefix=false
来关闭这个默认行为。例如:
zuul.routes.users.path=/myusers/**
zuul.routes.users.stripPrefix=false
注意:
zuul.stripPrefix
只会对zuul.prefix
的前缀起作用。对于path指定的前缀不会起作用。
9) 路由配置顺序
如果想按照配置的顺序进行路由规则控制,则需要使用YAML,如果是使用propeties文件,则会丢失顺序。例如:
zuul:
routes:
users:
path: /myusers/**
legacy:
path: /**
上例如果是使用properties文件进行配置,则legacy
就可能会先生效,这样users
就没效果了。
10) 自定义转换
我们也可以一个转换器,让serviceId
和路由之间使用正则表达式来自动匹配。例如:
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
这样,serviceId为“users-v1”的服务,就会被映射到路由为“/v1/users/”的路径上。任何正则表达式都可以,但是所有的命名组必须包括servicePattern和routePattern两部分。如果servicePattern没有匹配一个serviceId,那就会使用默认的。在上例中,一个serviceId为“users”的服务,将会被映射到路由“/users/”中(不带版本信息)。这个特性默认是关闭的,而且只适用于已经发现的服务。
2.2 Zuul的Header设置
敏感Header设置
同一个系统中各个服务之间通过Headers来共享信息是没啥问题的,但是如果不想Headers中的一些敏感信息随着HTTP转发泄露出去话,需要在路由配置中指定一个忽略Header的清单。
默认情况下,Zuul在请求路由时,会过滤HTTP请求头信息中的一些敏感信息,默认的敏感头信息通过zuul.sensitiveHeaders
定义,包括Cookie
、Set-Cookie
、Authorization
。配置的sensitiveHeaders
可以用逗号分割。
对指定路由的可以用下面进行配置:
# 对指定路由开启自定义敏感头
zuul.routes.[route].customSensitiveHeaders=true
zuul.routes.[route].sensitiveHeaders=[这里设置要过滤的敏感头]
设置全局:
zuul.sensitiveHeaders=[这里设置要过滤的敏感头]
忽略Header设置
如果每一个路由都需要配置一些额外的敏感Header时,那你可以通过zuul.ignoredHeaders
来统一设置需要忽略的Header。如:
zuul.ignoredHeaders=[这里设置要忽略的Header]
在默认情况下是没有这个配置的,如果项目中引入了Spring Security
,那么Spring Security
会自动加上这个配置,默认值为: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries
。
此时,如果还需要使用下游微服务的Spring Security的Header时,可以增加下面的设置:
zuul.ignoreSecurityHeaders=false
2.3 Zuul Http Client
Zuul的Http客户端支持Apache Http、Ribbon的RestClient和OkHttpClient,默认使用Apache HTTP客户端。可以通过下面的方式启用相应的客户端:
# 启用Ribbon的RestClient
ribbon.restclient.enabled=true
如果需要使用OkHttpClient需要注意在你的项目中已经包含
com.squareup.okhttp3
相关包。
3. Zuul容错与回退
我们再来仔细看一下之前Hystrix的监控界面:
请注意,Zuul的Hystrix监控的粒度是微服务,而不是某个API,也就是所有经过Zuul的请求都会被Hystrix保护起来。假如,我们现在把Product-Service
服务关闭,再来访问会出现什么结果呢?结果可能不是我们所想那样,如下:
呃,比较郁闷是么!那么如何为Zuul实现容错与回退呢?
Zuul提供了一个ZuulFallbackProvider
接口,通过实现该接口就可以为Zuul实现回退功能。那么让我们改造之前的Zuul-Server
。
3.1 实现回退方法
代码如下:
/**
* Product Service服务失败回退处理
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@Component
public class ProductServiceFallbackProvider implements ZuulFallbackProvider {
protected Logger logger = LoggerFactory.getLogger(ProductServiceFallbackProvider.class);
@Override
public String getRoute() {
// 注意: 这里是route的名称,不是服务的名称,
// 如果这里写成大写PRODUCT-SERVICE将无法起到回退作用
return "product-service";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("商品服务暂不可用,请稍后重试!".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
}
需要说明的是:
-
getRoute
方法返回了我们要为那个微服务提供回退。这里需要注意的返回的值是route的名称,不是服务的名称,不能够写为:PRODUCT-SERVICE
,否则该回退将不起作用; -
fallbackResponse
方法返回ClientHttpResponse
对象,作为我们的回退响应。这里实现非常简单仅仅是返回:商品服务暂不可用,请稍后重试! 的提示。
3.2 重启测试
重启Zuul-Server
,再重复上面的实验,将会看到以下界面:
说明,回退方法已经起作用了。如果你的没有起作用,那么仔细检查一下getRoute
的返回是否正确。
你可以到这里下载本篇的代码。