实战_Spring_Cloud(2)https://developer.aliyun.com/article/1543983
微服务网关(Zuul)
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。 Zuul的主要功能是路由转发和过滤器。zuul默认和Ribbon结合实现了负载均衡的功能
工作原理
zuul的核心是一系列的filters, 其作用类比Servlet框架的Filter,或者AOP。zuul把请求路由到用户处理逻辑的过程中,这些filter参与一些过滤处理,比如Authentication,Load Shedding等
Zuul使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。这些过滤器可帮助我们执行以下功能:
- 身份验证和安全性 - 确定每个资源的身份验证要求并拒绝不满足这些要求的请求
- 洞察和监控 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图
- 动态路由 - 根据需要动态地将请求路由到不同的后端群集
- 压力测试 - 逐渐增加群集的流量以衡量性能。
- Load Shedding - 为每种类型的请求分配容量并删除超过限制的请求
- 静态响应处理 - 直接在边缘构建一些响应,而不是将它们转发到内部集群
添加网关
- 新建api-gateway子模块,作为服务网关、服务发现客户端、获取配置客户端,因此需要引入以下依赖。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies>
- 在启动类上增加
EnableDiscoveryClient
和@EnableZuulProxy
注解。
@EnableDiscoveryClient @EnableZuulProxy @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }
- 启动服务,看看是否能正常获取配置,并注册到Eureka Server。
- Zuul网关目前暴露的端口是8080,之前我们访问商品服务的api,是通过调用 http://localhost:11100/api/v1/product/productInfos来访问的,现在我们就可以通过Zuul,根据商品的服务名称shopping-produc来访问 http://localhost:8080/shopping-product//api/v1/product/productInfos,非常轻松的实现了路由的功能。
自定义路由
默认的路由规则是按照服务的名称来路由服务,当然我们也可以自定义。在zuul中,路由匹配的路径表达式采用ant风格定义
通配符 | 说明 |
? | 匹配任意单个字符 |
* | 匹配任意数量的字符 |
** | 匹配任意数量的字符,支持多级目录 |
zuul: routes: # 简洁写法 shopping-product: /product/**
- 将命名为product的所有路径都映射到shopping-product服务中去,然后通过product名称来访问,依旧能访问成功。
- 如果我们需要查看目前所有的路径映射呢,首先得引入actuator
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
- 其次,需要放开actuator维护端口的权限
management: endpoints: web: exposure: include: "*"
- 访问 http://localhost:8080/actuator/routes ,可以看到目前网关的所有路由映射
- 如果需要定义哪些方法不能通过网关调用,还可以设置排除哪些路由的规则
zuul: routes: # 简洁写法 shopping-product: /product/** # 排除某些路由 ignored-patterns: - /**/productInfos
这样我们再访问这个接口时,就提示 Not Found 错误了
Cookie与头信息
默认情况下,spring cloud zuul在请求路由时,会过滤掉http请求头信息中一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,默认包括cookie,set-Cookie,authorization三个属性。所以,我们在开发web项目时常用的cookie在spring cloud zuul网关中默认时不传递的,这就会引发一个常见的问题,如果我们要将使用了spring security,shiro等安全框架构建的web应用通过spring cloud zuul构建的网关来进行路由时,由于cookie信息无法传递,我们的web应用将无法实现登录和鉴权。有时候,针对某些路由,我们需要传递这个cookie。
zuul: routes: # 完全写法 product-route: path: /product/** serviceId: shopping-product # 将指定路由的敏感头设置为空 sensitiveHeaders:
动态路由
之前路由的配置都是写在配置文件中,如果路由规则变化以后,需要重启网关服务。但是实际生产环境,一般都需要动态的加载路由的配置,不能轻易重启网关服务。
- 将配置信息集中到统一配置中心服务进行管理,具体实施参考前面章节-统一配置中心。
eureka: client: serviceUrl: defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/ #指定服务注册地址 spring: application: name: api-gateway #应用名称 cloud: config: discovery: enabled: true service-id: config-server
- 将zuul配置属性定义成支持动态刷新,增加
@RefreshScope
注解
@Component public class ZuulConfiguration { @ConfigurationProperties("zuul") @RefreshScope public ZuulProperties zuulProperties(){ return new ZuulProperties(); } }
自定义Filter
设想以下场景:我们需要判断用户请求的参数是否包含认证信息,如果包含token信息,则可以访问,否则禁止访问。可以用Zuul Filter很方便的实现在网关端,统一进行认证。
- 新建自定义的Filter,并继承ZuulFilter,默认需要实现4个接口
- filterType():返回 filter 的类型,设置为
PRE_TYPE
- filterOrder():返回 filter 的顺序,设置为
PRE_DECORATION_FILTER_ORDER-1
- shouldFilter():是否启用 filter,设置为
true
- run():执行具体的过滤器逻辑
/** * 验证token 过滤器 */ @Component public class TokenFilter extends ZuulFilter { @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); //测试在url参数中获取token String token = request.getParameter("token"); if(StringUtils.isEmpty(token)){ currentContext.setSendZuulResponse(false); currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; } }
- 验证结果,如果url中添加了 token 参数,TokenFilter 验证通过,正确返回结果;如果没有 token 参数,则返回 401(UNAUTHORIZED)错误
- 还可以在调用接口返回中,设置响应头
@Component public class AddResHeaderFilter extends ZuulFilter{ @Override public String filterType() { return POST_TYPE; } @Override public int filterOrder() { return SEND_RESPONSE_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse response = requestContext.getResponse(); response.setHeader("X-Foo", UUID.randomUUID().toString()); return null; } }
限流
这里介绍一种限流的设计方案:
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
Google公司已经实现了上述的令牌桶的算法,直接使用 RateLimiter 就可以通过Zuul实现限流的功能:
@Component public class RateLimitFilter extends ZuulFilter { private static final RateLimiter RATE_LIMITER = RateLimiter.create(100); @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return SERVLET_DETECTION_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { if (!RATE_LIMITER.tryAcquire()) { throw new RuntimeException("未能获取到令牌."); } return null; } }
小结
整体项目结构如下:
spring-cloud-app
--api-gateway(服务网关)
--config-server(统一配置中心)
--eureka-server(服务注册中心)
--shopping-common(购物公共模块)
--shopping-product(商品服务模块)
--shopping-order(订单服务模块)
目前所有的客户端请求,首先被发送到统一网关服务处理,然后由网关进行限流、熔断、权限验证、记录日志等等,然后根据自定义的路由规则,再分发到不同的应用服务中去,应用服务器返回处理结果后,由网关统一返回给客户端。
服务容错(Hystrix)
在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。
设计原则
- 防止任何单个依赖项耗尽所有容器(如Tomcat)用户线程。
- 甩掉包袱,快速失败而不是排队。
- 在任何可行的地方提供回退,以保护用户不受失败的影响。
- 使用隔离技术(如隔离板、泳道和断路器模式)来限制任何一个依赖项的影响。
- 通过近实时的度量、监视和警报来优化发现时间。
- 通过配置的低延迟传播来优化恢复时间。
- 支持对Hystrix的大多数方面的动态属性更改,允许使用低延迟反馈循环进行实时操作修改。
- 避免在整个依赖客户端执行中出现故障,而不仅仅是在网络流量中。
如何实现
- 用一个HystrixCommand 或者 HystrixObservableCommand (这是命令模式的一个例子)包装所有的对外部系统(或者依赖)的调用,典型地它们在一个单独的线程中执行
- 调用超时时间比你自己定义的阈值要长。有一个默认值,对于大多数的依赖项你是可以自定义超时时间的。
- 为每个依赖项维护一个小的线程池(或信号量);如果线程池满了,那么该依赖性将会立即拒绝请求,而不是排队。
- 调用的结果有这么几种:成功、失败(客户端抛出异常)、超时、拒绝。
- 在一段时间内,如果服务的错误百分比超过了一个阈值,就会触发一个断路器来停止对特定服务的所有请求,无论是手动的还是自动的。
- 当请求失败、被拒绝、超时或短路时,执行回退逻辑。
- 近实时监控指标和配置变化。
触发降级
在实际工作中,尤其是分布式、微服务越来越普遍的今天,一个服务经常需要调用其他的服务,即RPC调用,而调用最多的方式还是通过http请求进行调用,这里面就有一个问题了,如果调用过程中,因为网络等原因,造成某个服务调用超时,如果没有熔断机制,此处的调用链路将会一直阻塞在这里,在高并发的环境下,如果许多个请求都卡在这里的话,服务器不得不为此分配更多的线程来处理源源不断涌入的请求。
更恐怖的是,如果这是一个多级调用,即此处的服务的调用结果还被其他服务调用了,这就形成了所谓的雪崩效应,后果将不堪设想。因此,需要某种机制,在一定的异常接口调用出现的时候,能够自动发现这种异常,并快速进行服务降级。
- 首先,shopping-order项目模拟一个远程调用shopping-product服务http请求
/** * Hystrix 测试 */ @RestController @RequestMapping("api/hystrix") public class HystrixController { @GetMapping("/getProductEnv") public String getProductEnv() { RestTemplate restTemplate = new RestTemplate(); return restTemplate.postForObject("http://localhost:11100/api/env", null, String.class); } }
- 如果此时将shopping-product服务关闭,则shopping-order调用远程服务不可用,进入等待,超时时返回 Error Page的错误页面。其实我们希望服务不可用的时候直接处理,返回通知服务的不可用状态。可以引入Hystrix。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
- 在启动类上增加
@EnableCircuitBreaker
注解,或者将@SpringBootApplication
、@EnableDiscoveryClient
、@EnableCircuitBreaker
三个合并成一个@SpringCloudApplication
注解。
@EnableFeignClients(basePackages = "tech.lancelot.shoppingorder.client") //@SpringBootApplication //@EnableDiscoveryClient //@EnableCircuitBreaker @SpringCloudApplication public class ShoppingOrderApplication { public static void main(String[] args) { SpringApplication.run(ShoppingOrderApplication.class, args); } }
- 修改 HystrixController,增加
@HystrixCommand
注解,并指定调用方法失败时的错误处理回调。也可以为整个类增加@DefaultProperties
注解,定义一个默认的返回方法
/** * Hystrix 测试 */ @RestController @RequestMapping("api/hystrix") public class HystrixController { @HystrixCommand(fallbackMethod = "defaultFallback") @GetMapping("/getProductEnv") public String getProductEnv() { RestTemplate restTemplate = new RestTemplate(); return restTemplate.postForObject("http://localhost:11100/api/env", null, String.class); } // 默认服务不可达的返回信息 private String defaultFallback() { return "太拥挤了, 请稍后再试~~"; } }
- 重启启动后,再次访问接口,就会发现接口直接返回错误信息,不会阻塞在这里。
超时设置
如果我们没有配置默认的超时时间,Hystrix 将取 default_executionTimeoutInMilliseconds(1秒)作为默认超时时间,也可以自定义超时时间。
- 代码中修改默认超时配置(改为3秒):
@HystrixCommand(commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")})
这样的话,shopping-order调用远程服务,超过3s之后,立刻返回错误处理,不会再阻塞。
- 可以在配置文件中定义HystrixCommand属性
hystrix: command: default: # 方法默认属性 execution: isolation: thread: timeoutInMilliseconds: 1000 getProductEnv: # 该名称方法属性 execution: isolation: thread: timeoutInMilliseconds: 3000
熔断机制
如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
熔断器有三个状态 CLOSED
、OPEN
、HALF_OPEN
熔断器默认关闭状态,当触发熔断(至少有 circuitBreaker.requestVolumeThreshold 个请求,错误率达到 circuitBreaker.errorThresholdPercentage)后状态变更为 OPEN
,在等待到指定的时间(circuitBreaker.sleepWindowInMilliseconds),Hystrix会放请求检测服务是否开启,这期间熔断器会变为HALF_OPEN
半开启状态,熔断探测服务可用则继续变更为 CLOSED
关闭熔断器。
- 在方法上增加熔断属性的相关设置
@HystrixCommand(commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //设置熔断 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//请求数达到后才计算 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //休眠时间窗 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //错误率 })
实战_Spring_Cloud(4)https://developer.aliyun.com/article/1543990