第五篇: 路由网关(zuul)
在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、断路器、智能路由、配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统。
在Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(zuul、Ngnix),再到达服务网关(zuul集群),然后再到具体的服务。
服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理(重点),配置服务的配置文件放在git仓库,方便开发人员随时改配置。
一、Zuul简介
Zuul做为网关层,自身也是一个微服务,跟其它服务Service-1,Service-2, ... Service-N一样,都注册在eureka server上,可以相互发现,zuul能感知到哪些服务在线,同时通过配置路由规则(后面会给出示例),可以将请求自动转发到指定的后端微服务上,对于一些公用的预处理(比如:权限认证,token合法性校验,灰度验证时部分流量引导之类),可以放在所谓的过滤器(ZuulFilter)里处理,这样后端服务以后新增了服务,zuul层几乎不用修改。
Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/api/user转发到到user服务,/api/shop转发到到shop服务。zuul默认和Ribbon结合实现了负载均衡的功能。
二、创建eureka-service-zuul工程
打jar包。pom.xml如下:
<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.sun</groupId> <artifactId>eureka-service-zuul</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>service-zuul</name> <description>Demo project for Spring Boot</description> <parent> <groupId>com.sun</groupId> <artifactId>springcloud-parent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies> </project>
在其入口applicaton类加上注解@EnableZuulProxy,开启zuul的功能,新建ServiceZuulApplication启动类,代码如下:
package com.sun; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableZuulProxy @EnableEurekaClient public class ServiceZuulApplication { public static void main(String[] args) { SpringApplication.run( ServiceZuulApplication.class, args ); } }
加上配置文件application.yml加上以下的配置代码:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8769
spring:
application:
name: service-zuul
zuul:
routes:
api-a:
path: /api-a/**
serviceId: service-ribbon
api-b:
path: /api-b/**
serviceId: service-feign
这里说一下,application.yml是放到src/main/resources文件夹下的。
解释一下:上面这段配置表示,/api-a/**开头的url请求,将转发到service-ribbon这个微服务上,/api-b/**开头的url请求,将转发到service-feign这个微服务上。
首先指定服务注册中心的地址为http://localhost:8761/eureka/,服务的端口为8769,服务名为service-zuul;
以/api-a/ 开头的请求都转发给service-ribbon服务;以/api-b/开头的请求都转发给service-feign服务;
三、启动工程
依次启动eureka-server,eureka-client,eureka-service-ribbon,eureka-service-feign,eureka-service-zuul五个工程。
打开浏览器访问:http://localhost:8769/api-a/hi?name=sun ;浏览器显示:
hi sun ,i am from port:8763
打开浏览器访问:http://localhost:8769/api-b/hi?name=sun ;浏览器显示:
hi sun ,i am from port:8763
这说明zuul起到了路由的作用.
四、服务过滤
zuul不仅只是路由,并且还能过滤,做一些安全验证。继续改造工程,新建一个类MyFilter继承自ZuulFilter:
package com.sun; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; @Component public class MyFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(MyFilter.class); @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(); 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; } }
- filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,其常量值在org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 中定义具体如下:
// Zuul Filter TYPE constants ----------------------------------- /** * {@link ZuulFilter#filterType()} error type. */ public static final String ERROR_TYPE = "error"; /** * {@link ZuulFilter#filterType()} post type. */ public static final String POST_TYPE = "post"; /** * {@link ZuulFilter#filterType()} pre type. */ public static final String PRE_TYPE = "pre"; /** * {@link ZuulFilter#filterType()} route type. */ public static final String ROUTE_TYPE = "route";
这时访问:http://localhost:8769/api-a/hi?name=sun ;网页显示:
token is empty
访问 http://localhost:8769/api-a/hi?name=sun&token=22 ; 网页显示:
hi sun ,i am from port:8763
按说到这里已经结束了。不过有一点思考:zuul路由了ribbon和feign,前面我们说了,ribbon和feign是有熔断机制的,那现在,如果ribbon或feign或eureka-client挂了,zuul该如何熔断?
在这里我们可以看到,当关掉这几个客户端服务后,会出现:
提示没有熔断的fallback,这样说就是zuul路由到服务消费者ribbon/feign,消费者访问eureka-server,通过server找生产者eureka-client,发现生产者over了。
消费者ribbon/feign熔断,但是zuul不认,需要显式的对zuul进行熔断处理:
五、zuul熔断
新建ServiceHiFallbackProvider类,扩展自FallbackProvider接口:
package com.sun; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; @Component public class ServiceHiFallbackProvider implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); //指定要处理的 service。 @Override public String getRoute() { return "service-ribbon"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info("Excption {}",reason); } return fallbackResponse(); } 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("The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
当ribbon/feign或者服务生产者client挂了,都会返回:
The service is unavailable.