网关是系统唯一对外的入口,介于客户端与服务器端之间,用于对请求进行鉴权、限流、 路由、监控等功能。
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、鉴权等处理
Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得。
注意:Zuul服务最终还是会注册进Eureka
https://github.com/Netflix/zuul/wiki/Getting-Started
zuul与eureka的入口功能示意图
这里需要一个 EurekaServer,一个 zuul 网关,及两个消费者。
(1) 创建工程
复制工程 msc-consumer-fallbackmethod8080,重命名为msc-zuul-consumer8080
(2) 修改配置文件
(1) 创建工程
复制工程 msc-zuul-consumer--8080,并重命名为 msc-zuul-consumer--8090。
(2) 修改配置文件
当用户提交某请求后,该请求应交给哪个微服务的哪个主机来处理,通过配置可以完成。
(1) 创建工程 msc-zuul9000
(2) 导入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.jd</groupId> <artifactId>msc-zuul9000</artifactId> <version>0.0.1-SNAPSHOT</version> <name>msc-zuul9000</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR5</spring-cloud.version> </properties> <dependencies> <!--eureka客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--zuul依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 复制代码
(3) 修改启动类
package com.jd; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @EnableZuulProxy //开启zuul代理模式 @SpringBootApplication public class MscZuul9000Application { public static void main(String[] args) { SpringApplication.run(MscZuul9000Application.class, args); } } 复制代码
(4) 修改配置文件
server: port: 9000 spring: application: name: msc-zuul-depart eureka: instance: prefer-ip-address: true client: service-url: #defaultZone: http://10.68.96.71:8761/eureka #指定当前client要连接的eureka server defaultZone: http://localhost:8761/eureka 复制代码
测试
启动
使用路由访问服务
http://localhost:9000/msc-consumer-8080/consumer/depart/get/1
(1) 修改配置文件
前面的访问方式,需要将微服务名称暴露给用户,会存在安全性问题。所以,可以自定义路径来替代微服务名称,即自定义路由策略。
在配置文件中添加如下配置。
zuul: routes: #指定微服务路由规则 msc-consumer-8080: /msc8080/** msc-consumer-8090: /msc8090/** 复制代码
使用路由规则路由访问服务
http://localhost:9000/msc8090/consumer/depart/get/1
前面的设置方式可以使用指定的路由路径访问到相应微服务,但使用微服务名称也可以访问到,为了防止服务侵入,可以将服务名称屏蔽。
在配置文件中添加如下内容:
zuul: routes: #指定微服务路由规则 msc-consumer-8080: /msc8080/** msc-consumer-8090: /msc8090/** #屏蔽指定微服务名称 #ignored-services: msc-consumer-8080 #屏蔽所有微服务名称 ignored-services: "*" 复制代码
在配置路由策略时,可以为路由路径配置一个统一的前辍,以便为请求归类。在前面的
配置文件中增加如下配置。
zuul: #指定统一的前缀 prefix: /jd 复制代码
这时必须加上前缀才能访问
可以指定屏蔽掉的路径 URI,即只要用户请求中包含指定的 URI 路径,那么该请求将无法访问到指定的服务。通过该方式可以限制用户的权限。
配置文件中增加如下配置。
zuul: #指定统一的前缀 prefix: /jd #屏蔽指定的URI ignored-patterns: /**/list/** 复制代码
重启工程再访问
默认情况下,像 Cookie、Set-Cookie 等敏感请求头信息会被 zuul 屏蔽掉,我们可以将这些默认屏蔽去掉,当然,也可以添加要屏蔽的请求头。
从源码 ZuulProperties.java中可看到
//服务降级方法,响应给客户端的备选方案 public Depart getHystrixHandler(@PathVariable("id") int id, HttpServletRequest request){ System.out.println("token="+request.getHeader("token")); System.out.println("Set-Cookie="+request.getHeader("Set-Cookie")); Depart depart = new Depart(); depart.setId(id); depart.setName("no this depart-8080"); return depart; } 复制代码
运行工程
指定某个请求头屏蔽
zuul: #指定token 被屏蔽 sensitive-headers: token 复制代码
运行工程
用户提交的请求被路由到一个指定的微服务中,若该微服务名称的主机有多个,则默认采用负载均衡策略是轮询。
(1) 创建三个消费者工程
msc-zuul-consumer8180
msc-zuul-consumer8280
msc-zuul-consumer8380
修改msc-zuul-consumer8180的application.yml文件
server: port: 8180 spring: #指定当前微服务对外暴露的名称 application: name: msc-consumer 复制代码
修改处理器
//服务降级方法,响应给客户端的备选方案 public Depart getHystrixHandler(@PathVariable("id") int id){ Depart depart = new Depart(); depart.setId(id); depart.setName("no this depart-8180"); return depart; } 复制代码
依次修改msc-zuul-consumer8280,msc-zuul-consumer8380工程的文件
(2) 修改msc-zuul9000配置文件
(3) 修改负载均衡策略
修改 msc-zuul9000 的启动类,在其中直接添加如下代码,更换负载均衡策略为随机算法。
@EnableZuulProxy //开启zuul代理模式 @SpringBootApplication public class MscZuul9000Application { public static void main(String[] args) { SpringApplication.run(MscZuul9000Application.class, args); } //设置负载均衡算法为"随机算法" @Bean public IRule loadBalanceRule(){ return new RandomRule(); } } 复制代码
访问验证是否为随机
当消费者调用提供者时由于各种原因出现无法调用的情况时,消费者可以进行服务降级。
那么,若客户端通过网关调用消费者无法调用时,是否可以进行服务降级呢?当然可以,zuul具有服务降级功能。
(1) 创建工程 msc-zuul-fallback-9000
复制 msc-zuul-9000 工程,并重命名为 msc-zuul-fallback-9000。
(2) 定义 fallback 类
这里仅仅是对名称为 msc-consumer-8080 的微服务进行降级处理。
在msc-zuul-fallback9000工程中创建ConsumerFallback类
@Component public class ConsumerFallback implements FallbackProvider { //指定要降级的微服务名称 @Override public String getRoute() { //仅对指定的微服务进行降级 //return "msc-consumer-8080"; //对所有微服务降级 return "*"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { //若微服务不是msc-consumer-8080,则不进行降级 if(!"msc-consumer-8080".equals(route)){ return null; } //仅对msc-consumer-8080进行降级 return new ClientHttpResponse(){ @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } @Override public InputStream getBody() throws IOException { //设置降级信息 String msg = "fallback:" + route; return new ByteArrayInputStream(msg.getBytes()); } @Override public HttpStatus getStatusCode() throws IOException { //返回状态常量 return HttpStatus.SERVICE_UNAVAILABLE; } @Override public int getRawStatusCode() throws IOException { //返回状态码 return HttpStatus.SERVICE_UNAVAILABLE.value(); } @Override public String getStatusText() throws IOException { //返回状态码对应的状态短语 return HttpStatus.SERVICE_UNAVAILABLE.getReasonPhrase(); } @Override public void close() { } }; } } 复制代码
启动eureka和msc-zuul-fallback工程
访问
http://localhost:9000/msc8090/consumer/depart/get/1
http://localhost:9000/msc8080/consumer/depart/get/1
在服务路由之前、中、后,可以对请求进行过滤,使其只能访问它应该访问到的资源,
增强安全性。此时需要通过 ZuulFilter 过滤器来实现对外服务的安全控制。
该过滤的条件是,只有请求参数携带有 user 的请求才可访问/msc8080工程,否则返回 401,未授权。当然,对/msc8080 工程的访问没有限制。简单来说就是,只有当访问/msc8080且 user 为空时是不可以通过过滤的,其它请求都可以。
(1) 创建工程
复制 msc-zuul-9000 工程,并重命名为 msc-zuul-filter-9000
(2) 定义 RouteFilter 类
/** * @author lijun * @create 2020-02-18 22:10 */ @Component public class RouterFilter extends ZuulFilter { //指定路由之前执行过滤 @Override public String filterType() { //返回 pre return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { //在系统最小值-3的前面执行 return -5; } @Override public boolean shouldFilter() { //获取当前的请求上下文对象 RequestContext context = RequestContext.getCurrentContext(); //从请求上下文中获取当前请求信息 HttpServletRequest request = context.getRequest(); String user = request.getParameter("user"); String uri = request.getRequestURI(); if(uri.contains("/msc8080") && StringUtils.isEmpty(user)){ //指定当前请求未通过zuul过滤 context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } //对请求通过过滤后的逻辑 @Override public Object run() throws ZuulException { System.out.println("通过过滤"); return null; } } 复制代码
测试
启动如下4个工程
(1) 令牌桶算法 该算法可以应对突发流量情况。
(1) 创建工程
复制工程 msc-zuul-filter9000,并命名为 msc-zuul-tokenbucket9000
(2) 修改 RouteFilter 类
这里导入的 RateLimiter 是 Google 的类。这里为了测试的方便,使令牌桶每秒仅生成 2 个令牌。即每秒可以处理 2 个请求。
@Component public class RouterFilter extends ZuulFilter { //每秒产生2个令牌 private static final RateLimiter RATE_LIMITER = RateLimiter.create(2); //指定路由之前执行过滤 @Override public String filterType() { //返回 pre return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { //在系统最小值-3的前面执行 return -5; } @Override public boolean shouldFilter() { //获取当前的请求上下文对象 RequestContext context = RequestContext.getCurrentContext(); //RATE_LIMITER.tryAcquire() 若可以立即获取到1个令牌,则返回true,否则返回false 不阻塞 //RATE_LIMITER.tryAcquire(5,3, TimeUnit.SECONDS) 若3秒内获取5个令牌,则返回true,否则返回false,不阻塞 //RATE_LIMITER.acquire() 获取1个令牌,若获取不到,则阻塞直到获取到 if(!RATE_LIMITER.tryAcquire()){ //指定当前请求未通过zuul过滤,默认为true context.setSendZuulResponse(false); //向客户端响应429,请求数量过多 context.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value()); return false; } //从请求上下文中获取当前请求信息 HttpServletRequest request = context.getRequest(); String user = request.getParameter("user"); String uri = request.getRequestURI(); if(uri.contains("/msc8080") && StringUtils.isEmpty(user)){ //指定当前请求未通过zuul过滤 context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } //对请求通过过滤后的逻辑 @Override public Object run() throws ZuulException { System.out.println("通过过滤"); return null; } } 复制代码
测试
启动如下3个工程
使用 Guava 的 RateLimit 令牌桶算法可以实现对请求的限流,但其限流粒度有些大。有 个老外使用路由过滤,针对 Zuul 编写了一个限流库(spring-cloud-zuul-ratelimit),提供多种细 粒度限流策略,在导入该依赖后我们就可以直接使用了。
其限流策略,即限流查验的对象类型有:
user:针对用户的限流,即对单位时间窗内经过网关的用户数量的限制。
origin:针对客户端IP的限流,即对单位时间窗内经过网关的IP数量的限制。
url:针对请求URL的限流,即对单位时间窗内经过网关的URL数量的限制。
(1) 创建工程
复制工程 msc-zuul-tokenbucket9000,并命名为 msc-zuul-ratelimit9000。
(2) 删除 RouteFilter
删除 RouteFilter 类,该工程中不使用。
(3) 添加依赖
<!-- spring-cloud-zuul-ratelimit 依赖 --> <dependency> <groupId>com.marcosbarbero.cloud</groupId> <artifactId>spring-cloud-zuul-ratelimit</artifactId> <version>2.0.5.RELEASE</version> </dependency> 复制代码
(4) 修改配置文件
zuul: #指定统一的前缀 prefix: /jd routes: #指定微服务路由规则 msc-consumer-8080: /msc8080/** msc-consumer-8090: /msc8090/** #对限流策略进行配置 ratelimit: enabled: true #开启限流 #在一个单位时间之内通过该zuul的用户数量、ip数量及url数量,都不能超过3个 default-policy: #设置限流策略 refresh-interval: 3 #限流单位时间窗口大小 单位秒 quota: 1 #指定限流的时间窗口数量 limit: 3 #指定的单位时间窗口内启动限流的限定值 type: user,origin,url # 指定限流查验的类型 复制代码
(5) 添加异常处理页面
在 src/main/resources 目录下再定义新的目录 public/error,必须是这个目录名称。然后 在该目录中定义一个异常处理页面,名称必须是异常状态码,扩展名必须为 html。
(1) 什么是灰度发布
灰度发布,又名金丝雀发布,是系统迭代更新、平滑过渡的一种上线发布方式。
(2) Zuul 灰度发布原理
生产环境中,可以实现灰度发布的技术很多,我们这里要讲的是 zuul 对于灰度发布的
实现。而其实现也是基于 Eureka 元数据的。
(1) 修改 msc-zuul-consumer8180
server: port: 8180 spring: #指定当前微服务对外暴露的名称 application: name: msc-consumer eureka: instance: metadata-map: host-mark: running-host 复制代码
(2) 修改 msc-zuul-consumer8280
server: port: 8280 spring: #指定当前微服务对外暴露的名称 application: name: msc-consumer eureka: instance: metadata-map: host-mark: running-host 复制代码
(3) 修改 msc-zuul-consumer8380
server: port: 8380 spring: #指定当前微服务对外暴露的名称 application: name: msc-consumer eureka: instance: metadata-map: host-mark: gray-host 复制代码
(1) 创建工程
复制工程 msc-zuul-9000,并重命名为 msc-zuul-gray-9000
(2) 添加依赖
在 pom 文件中增加新的依赖。
<dependency> <groupId>io.jmnarloch</groupId> <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId> <version>2.1.0</version> </dependency> 复制代码
(3) 修改配置文件
将配置文件内容修改如下(仅仅就是将之前配置的那些多余内容删除了):
server: port: 9000 spring: application: name: msc-zuul-depart eureka: instance: prefer-ip-address: true client: service-url: #defaultZone: http://10.68.96.71:8761/eureka #指定当前client要连接的eureka server defaultZone: http://localhost:8761/eureka zuul: routes: #指定微服务路由规则 msc-consumer: /msc-lb/** 复制代码
(4) 定义过滤器
package com.jd.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder; import org.apache.commons.lang.StringUtils; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @author lijun * @create 2020-02-19 12:12 */ @Component public class GrayFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return -5; } @Override public boolean shouldFilter() { //所有请求都通过zuul过滤 return true; } @Override public Object run() throws ZuulException { System.out.println("通过zuul过滤了"); //获取请求上下文 RequestContext context = RequestContext.getCurrentContext(); //获取请求 HttpServletRequest request = context.getRequest(); //获取指定的请求头信息,该头信息在浏览器提交请求时携带,用于区分该请求被路由到哪个主机处理 String mark = request.getHeader("gray-mark"); //默认将请求路由到running-host上 RibbonFilterContextHolder.getCurrentContext().add("host-mark", "running-host"); //若mark的值不为空且值为enable,则将请求路由到gray-host,其它请求路由到默认的running-host if(!StringUtils.isEmpty(mark) && "enable".equals(mark)){ RibbonFilterContextHolder.getCurrentContext().add("host-mark", "gray-host"); } return null; } } 复制代码
[root@master01 ~]# mkdir -p /root/app/demo/zuul
[root@master01 zuul]# mkdir consumer8090
[root@master01 zuul]# mkdir consumer8080
[root@master01 zuul]# cat Dockerfile
# 基于哪个镜像 FROM java:8 # 将本地文件夹挂载到当前容器 VOLUME /root/app/demo/zuul # 复制文件到容器 ADD cloud-zuul-gateway.jar /cloud-zuul-gateway.jar # 声明需要暴露的端口 EXPOSE 9000 # 配置容器启动后执行的命令 ENTRYPOINT ["java","-jar","/cloud-zuul-gateway.jar"] 复制代码
在目录/root/app/demo/zuul下执行 docker build构建镜像
[root@master01 zuul]# docker build -t cloud-zuul-gateway:v1 .
[root@master01 zuul]# docker tag cloud-zuul-gateway:v1 registry.cn-hangzhou.aliyuncs.com/registry_tomato/zuul:v1
备注:docker 需要先登录 docker login
[root@master01 zuul]# docker push registry.cn-hangzhou.aliyuncs.com/registry_tomato/zuul:v1
[root@master01 zuul]# cat zuul.yml
#deploy apiVersion: apps/v1 kind: Deployment metadata: name: cloud-zuul-gateway spec: selector: matchLabels: app: cloud-zuul-gateway replicas: 1 template: metadata: labels: app: cloud-zuul-gateway spec: containers: - name: cloud-zuul-gateway image: registry.cn-beijing.aliyuncs.com/registry_tomato/microservice-zuul:v1 ports: - containerPort: 9000 --- #service apiVersion: v1 kind: Service metadata: name: cloud-zuul-gateway-service spec: ports: - port: 9000 targetPort: 9000 nodePort: 39000 type: NodePort selector: app: cloud-zuul-gateway 复制代码
Dockerfile构建消费者微服务镜像
[root@master01 zuul]# cd consumer8080/
[root@master01 consumer8080]# cat Dockerfile
# 基于哪个镜像 FROM java:8 # 将本地文件夹挂载到当前容器 VOLUME /root/app/demo/zuul/consumer8080 # 复制文件到容器 ADD consumer.jar /consumer.jar # 声明需要暴露的端口 EXPOSE 8080 # 配置容器启动后执行的命令 ENTRYPOINT ["java","-jar","/consumer.jar"] 复制代码
[root@master01 consumer8080]# docker build -t microservice-zuulconsumer:v80 .
[root@master01 consumer8080]# docker tag microservice-zuulconsumer:v80 registry.cn-beijing.aliyuncs.com/registry_tomato/microservice-consumer:v80
[root@master01 consumer8080]# vi consumer.yml
#deploy apiVersion: apps/v1 kind: Deployment metadata: name: consumer spec: #deploy apiVersion: apps/v1 kind: Deployment metadata: name: zuulconsumer80 spec: selector: matchLabels: app: zuulconsumer80 replicas: 1 template: metadata: labels: app: zuulconsumer80 spec: containers: - name: consumer image: registry.cn-beijing.aliyuncs.com/registry_tomato/microservice-consumer:v80 ports: - containerPort: 8080 --- #service apiVersion: v1 kind: Service metadata: name: zuulconsumer80-service spec: ports: - port: 8080 protocol: TCP nodePort: 31080 type: NodePort selector: app: zuulconsumer80 复制代码
[root@master01 zuul]# kubectl apply -f zuul.yml
[root@master01 consumer8090]# kubectl apply -f consumer.yml
[root@master01 consumer8080]# kubectl apply -f consumer.yml
[root@master01 zuul]# kubectl get pods -o wide
[root@master01 zuul]# kubectl get svc -o wide
路径屏蔽测试成功了
使用前缀,用路由规则访问成功
代码下载地址:
https://github.com/coderTomato/microservicecloud.git