为什么要使用微服务网关
不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护,如下图:
如果让客户端直接与各个微服务通讯,可能会有很多问题:
- 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
- 在某些场景下存在跨域请求的问题
- 加大身份认证的难度,每个微服务需要独立认证
因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可!
微服务网关封装了应用程序的内部结构,客户端只须跟网关交互,而无须直接调用特定微服务的接口。这样开发就可以得到简化。不仅如此,使用微服务还有以下优点:
- 易于监控
- 易于认证
- 减少了客户端与各个微服务之间的交互次数
Zuul简介
Zuul是 Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud对Zuul进行了整合和增强
Zuul也整合了 Spring Cloud Ribbon 与 Spring Cloud Hystrix;
使用Zuul
“路由”是指根据请求URL,将请求分配到对应的处理程序。在微服务体系中,Zuul负责接收所有的请求。根据不同的URL匹配规则,将不同的请求转发到不同的微服务处理;
1:新建Spring Boot项目EurekaServer,添加依赖Eureka Server
2:启动类
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3:application.yml
server:
port: 8761
eureka:
client:
registerWithEureka: false #是否将自己注册到Eureka Server,默认为True。由于当前应用就是Eureka Server,故false
fetchRegistry: false #是否从Eureka Server获取注册信息,默认True。因为这是一个单节点的Eureka Server,不需要同步其他的Eureka Server节点,故false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
4:新建Spring Boot项目EurekaClient,添加依赖Eureka Client、Web
5:启动类
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
6:控制器MyController
@RestController
public class MyController {
@RequestMapping("/eureka/test") //等会通过Zuul来访问它,测试Zuul
public String test()
{
return "Hello Zuul";
}
}
7:新建Spring Boot项目ServerZuul,添加依赖Zuul、Eureka Client
8:启动类
@EnableZuulProxy
@SpringBootApplication
public class ServerZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ServerZuulApplication.class, args);
}
}
9:application.yml
server:
port: 8040
spring:
application:
name: Zuul
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
zuul-service: # 这里是路由id,随意写
path: /zuul-service/** # 这里是映射路径,这里将所有请求前缀为/zuul-service/的请求,转发到http://127.0.0.1:8020处理
url: http://127.0.0.1:8020 # 映射路径对应的实际url地址
sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务
配置解析:
- zuul-service:配置路由id,可以随意取名
- url:映射路径对应的实际url地址
- path:配置映射路径,这里将所有请求前缀为/zuul-service/的请求,转发到http://127.0.0.1:8020处理
10:运行测试
启动项目EurekaServer
启动项目EurekaClient
启动项目ServerZuul
访问:http://localhost:8040/zuul-service/eureka/test
Zuul的路由端点
当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。借助这个端点,可以方便、直观地查看以及管理Zuul的路由。
spring-cloud-starter-zuul已经包含了spring-cloud-starter-actuator。因此我们编写的ServerZuul已经具备路由管理能力。
运行测试:
访问:http://localhost:8040/actuator/routes
可以看到从路径到微服务的映射!
如果访问出现404错误:
并且访问http://localhost:8040/actuator,没有发现routes端点:
则需要在application.yml中新增如下配置即可:
management:
endpoints:
web:
exposure:
include: '*'
路由配置详解
微服务一般是由几十、上百个服务组成,对于一个URL请求,最终会确认一个服务实例进行处理。如果对每个服务实例手动指定一个唯一访问地址,然后根据URL去手动实现请求匹配,这样做显然就不合理。
Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置。
1:简化路由配置
(1)添加Eureka客户端依赖(使用Zuul小节第7步已经加了)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)开启Eureka客户端发现功能
@EnableDiscoveryClient
public class ServerZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ServerZuulApplication.class, args);
}
}
(3)添加Eureka配置,获取服务信息(使用Zuul小节第9步已经有了)
(4)修改映射配置,通过服务名称获取
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能;
zuul:
routes:
zuul-service:
path: /zuul-service/** # 这里是映射路径
serviceId: EurekaClinet #配置转发的微服务名称
访问:http://localhost:8040/zuul-service/eureka/test,结果是一样的!
而大多数情况下,我们的路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.serviceId=path
上面的配置可以简化为一条:
zuul:
routes:
EurekaClinet: /zuul-service/**
上面已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
- 默认情况下,一切服务的映射路径就是服务名本身:例如服务名为eurekaclinet ,则默认的映射路径就是: /eurekaclinet/**
所以,当我们什么都没配置时,访问http://localhost:8040/eurekaclinet/eureka/test结果与上面一样!
2:忽略指定微服务
zuul:
ignored-services: eurekaclinet
访问:http://localhost:8040/eurekaclinet/eureka/test
因为已经设置忽略了它,所以Zuul不会代理它。自然会报错!
3:忽略所有微服务,只路由指定微服务
zuul:
ignored-services: '*' # 使用'*'可忽略所有微服务
routes:
eurekaclinet: /client/**
让Zuul只路由eurekaclinet微服务
4:路由前缀
zuul:
prefix: /api
strip-prefix: false
routes:
eurekaclinet: /zuul-service/** #/api/zuul-service/eureka/tests
更改EurekaClient的MyController
@RestController
public class MyController {
@RequestMapping("/api/eureka/test") //等会通过Zuul来访问它,测试Zuul
public String test()
{
return "Hello Zuul";
}
}
访问:http://localhost:8040/api/zuul-service/eureka/test会被转发到/eurekaclinet/api/eureka/test
访问:http://localhost:8040/routes
使用Zuul上传文件
1:新建Spring Boot项目ServerZuulUpload,添加依赖Web、Eureka Client(2.0.3版本没有包含Web)、Actuator
2:启动类
@EnableEurekaClient
@SpringBootApplication
public class ServerZuulUploadApplication {
public static void main(String[] args) {
SpringApplication.run(ServerZuulUploadApplication.class, args);
}
}
3:控制类MyController
@Controller
public class MyController {
/**
* 上传文件
* ps.该示例比较简单,没有做IO异常、文件大小、文件非空等处理
* @param file 待上传的文件
* @return 文件在服务器上的绝对路径
* @throws IOException IO异常
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public @ResponseBody String handleFileUpload(@RequestParam(value = "file", required = true) MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
File fileToSave = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes, fileToSave);
return fileToSave.getAbsolutePath();
}
}
4:application.yml
server:
port: 8050
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: Upload
servlet:
multipart:
max-file-size: 2000Mb
max-request-size: 2500Mb
将该服务注册到EurekaServer,并配置了文件上传大小的限制!
运行测试(使用的是Postman):
启动项目
Zuul的过滤器
Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了4种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期:
- PRE:这种过滤器在请求被路由之前调用
- ROUTING:这种过滤器将请求路由到微服务
- POST:这种过滤器在路由到微服务以后执行
- ERROR:在其他阶段发生错误时执行该过滤器
除了默认的过滤器类型,Zuul还允许创建自定义的过滤器类型。
1:编写Zuul过滤器,只需继承抽象类ZuulFilter,然后实现几个抽象方法就可以了
public class PreRequestLogFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PreRequestLogFilter.class);
@Override
public String filterType() { //返回过滤器类型。有pre、route、post、error等几种取值,对应上文的几种
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {//返回一个int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;//返回一个boolen值来判断该过滤器是否被执行,true表示执行,false表示不执行
}
@Override
public Object run() {//过滤器的具体逻辑。这里让它打印请求的HTTP方法以及请求的地址
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
PreRequestLogFilter.LOGGER.info(String.format("send %s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
2:Zuul启动类:
@EnableZuulProxy
@SpringBootApplication
public class ServerZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ServerZuulApplication.class, args);
}
@Bean
public PreRequestLogFilter preRequestLogFilter() {
return new PreRequestLogFilter();
}
}
3:运行测试:
启动项目EurekaServer
启动项目EurekaClient
启动项目ServerZuul
多次访问:http://localhost:8040/api/zuul-service/eureka/test
禁用Zuul过滤器
Zuul的application.yml
zuul:
PreRequestLogFilter: #zuul.<ClassNmae>.<filterType>.disable=true即可禁用
pre:
disable: true
启动项目后多次访问:http://localhost:8040/api/zuul-service/eureka/test,控制台并不会打印相关信息;
Zuul的容错与回退
在Spring Cloud中,Zuul默认已经包整合了Hystrix。
1:添加依赖HystrixDashboard
2:启动类:
@EnableHystrixDashboard
@SpringBootApplication
public class ServerHystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(ServerHystrixDashboardApplication.class, args);
}
}
3:运行测试
启动项目EurekaServer
启动项目EurekaClient
启动项目ServerZuul
访问:http://localhost:8040/api/zuul-service/eureka/test可获得正常结果
访问:http://localhost:8040/actuator/hystrix.stream
访问:http://localhost:8040/hystrix
Zuul的Hystrix监控的粒度是微服务,而不是某个API;同时也说明,所有经过Zuul的请求,都会被Hystrix保护起来。
关闭项目EurekaClient
访问:http://localhost:8040/eurekaclinet/eureka/test
下面来谈谈如何为Zuul实现回退!
为Zuul实现回退
想要为Zuul实现回退,需要实现FallbackProvider接口。在实现类中,指定为哪个微服务提供回退,并提供一个ClientHttpResponse作为回退响应。
@Component
public class MyFallbackProvider implements FallbackProvider{
@Override
public String getRoute() {
// 表明是为哪个微服务提供回退,*表示为所有微服务提供回退
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return this.fallbackResponse();
}
}
@Override
public ClientHttpResponse fallbackResponse() {
return this.response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("服务不可用,请稍后再试。".getBytes());
}
@Override
public HttpHeaders getHeaders() {
// headers设定
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
添加回退之后,重复上面的测试会发现:
参考书籍:Spring Cloud与Docker微服务架构实战
以上只是学习所做的笔记,以供日后参考!!!