简介
- 网关服务器是啥?为啥需要网关服务器?
- 近几年随着一些互联网大公司的崛起,高额的市场红利吸引着公司业务市场的不断拓张,为服务器带来的另一个挑战,便是巨大的流量冲击和快速的业务变更,市场变化太快,谁能抢占先机,就能优先占据更多的市场份额。业务的快速试错、高并发流量的支持对开发和服务器有着极高的要求,传统的单体应用已经无法满足市场需求,而微服务则是这种场景演化出来的产物。
- 但是由于微服务是通过业务领域和其负载压力对其进行细粒度的拆分,服务的数量往往会拆的越来越细,服务数量繁多,服务实例的数量更是多,我们总不能直接把这些服务一个一个暴露给调用者吧,于是衍生出了一个新的产物—>服务器网关。
- 服务器网关是整个平台的流量入口,为平台内的微服务提供了很多基础能力,其功能具有统一资源授权认证、服务安全、流量控制、故障转移、负载均衡、全链路压测、灰度测试、动态路由、服务动态伸缩、服务降级等等功能。
本文将介绍其基本原理,以及如何在实际工作中使用ZUUL,内容有
- ZUUL原理介绍
- ZUUL拦截器模型讲解
- ZUUL反向代理功能的应用与配置说明
- 故障转移、负载均衡、服务熔断的实现和参数讲解与调优
- 如何自定义过滤器,以及其使用场景介绍
- 服务调用异常统一处理,并自定义响应
- Cookies、Authorization等敏感信息控制与说明
- 自定义熔断处理
原文:传送门
注:本文基于springcloud2.1.3 Greenwich.RELEASE 版本
参考文章
1、ZUUL原理介绍
- zuul也是通过一个servlet实现的,走的还是springMVC模型,可以理解为其在
DispatchServlet
的基础上进一步做了处理,拦截了其需要的请求进行处理。其提供了过滤器机制来对请求进行额外的处理,ZUUL默认提供了一些默认的过滤器,开发者也可以通过自定义filter来拓展其功能。- 同一个请求的不同过滤器之间可以共享和修改同一个Request的请求、响应、url等信息,通过一个
RequestContext
的工具可以获取当前请求的HttpServletRequest
和HttpServletResponse
等信息,通过ThreadLocal
实现的,FilterConstants
里定义了一些常用的常量信息。- 启用注解
@EnableZuulServer
,其为基础版的ZUUL,支持如下拦截器
- Pre filters:
ServletDetectionFilter
:本过滤器的作用是判断请求的来源,判断请求是从dispatcherServlet来的还是 从zuulServlet来的,并将判断结果存放到RequestContext中。在后续的Servlet30WrapperFilter、PreDecorationFilter有用到这个参数。FormBodyWrapperFilter
:主要是解析表单数据并重新编码,提供可重复读取的request InputStream,供后续服务使用。DebugFilter
: 用于打印服务调用日志,通常用于调试,默认不开启,需要配置zuul.debug.request:true
可开启。SendForwardFilter
:该过滤器只对请求上下文中存在forward.to
(FilterConstants.FORWARD_TO_KEY
)参数的请求进行处理。即处理之前我们路由规则中forward
的本地跳转,也就是处理重定向的请求。- Post filters:
SendResponseFilter
:将反向代理的后端服务响应写入当前请求对应的响应体中。- Error filters:
SendErrorFilter
:如果RequestContext.getThrowable()
不为空时,默认跳转到/error
路由,也可以修改该路由地址,配置成自定义的错误处理器,这样就可以修改错误信息。
- 启用注解
@EnableZuulProxy
,其为升级版的ZUUL,增加了对反向代理功能的实现,支持额外的拦截器如下
- Pre filters:
PreDecorationFilter
: 实现了请求如何路由到下游服务的一些列规则。- Route filters:
RibbonRoutingFilter
:使用Ribbon, Hystrix,某种Http Client来发送请求。Service IDs 被暂存在RequestContext
的FilterConstants.SERVICE_ID_KEY
中。- 这个过滤器可以使用不同的http client进行对下游服务的请求处理,常见的client如下
* Apache `HttpClient`:默认的client。 * Squareup `OkHttpClient` v3:需要引入`com.squareup.okhttp3:okhttp`maven依赖包,而且需要启用配置`ribbon.okhttp.enabled=true`。 * Netflix Ribbon HTTP client:启用配置`ribbon.restclient.enabled=true`即可。但是这种client有一些弊端,不建议使用,其会有一个自动重试机制,而且对http的协议支持不是很完善。
2、ZUUL拦截器模型讲解
-
过滤器模型如下图所示
- 过滤器有四种类型
- PRE :预处理过滤器,在请求处理前进行一些额外的处理。
- ROUTING :路由过滤器,处理请求如何路由的过滤器,比如它可以决定一个请求该调用哪个下游服务的哪个接口。
- POST :后过滤器,当下游服务对请求处理完成后,进行进一步的额外处理。比如对响应信息的额外处理等等。
- ERROR :错误过滤器,对处理失败的请求进行额外的处理。
- 过滤器常用特性
- 类型 : 过滤器的类型,取值:
error
,post
,pre
,route
,可以参考FilterConstants.XXX_TYPE
,对应类ZuulFilter.filterType
方法。- **执行顺序Order **:同类过滤器的执行顺序值,值越小优先级越高,对应类
ZuulFilter.filterOrder
方法。- 请求过滤规则 :只有请求满足该过滤器定义的规则,该过滤器才会对其进行处理,对应类
ZuulFilter.shouldFilter
方法。- 过滤器处理逻辑 : 进行业务逻辑操作,对应类
ZuulFilter.run
方法。
3、ZUUL反向代理功能的应用与配置说明
3.1 项目构建
- 引入maven依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- eureka注册中心客户端包,用于配置中心服务自动注册与发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 项目启动类
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class SpringApplication {
public static void main(String[] args) {
SpringApplication.run(SpringApplication.class, args);
}
}
- 配置信息
server:
#配置服务端口
port: 1201
#配置注册中心信息,详情请参考本系列springcloud注册中心搭建相关教程
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8763/eureka/
instance:
prefer-ip-address: true
leaseRenewalIntervalInSeconds: 10
health-check-url-path: /actuator/health
# zuul路由信息配置
zuul:
host:
connect-timeout-millis: 2000
max-per-route-connections: 100
max-total-connections: 1000
socket-timeout-millis: 6000
SendErrorFilter:
post:
disable: true #禁用默认的错误过滤器,项目里我们自定义错误处理器
routes: #配置中心路由信息
#1.简约配置
service-A: /aaa/** #规则为:服务serviceId : URL映射
service-B: /bbb/**
#2.或者这样配置
service-A:
path: '/aaa/**'
#服务ID
serviceId:'service-A'
#或者使用url
url: https://downstream
- 1、2配置二选一即可
例如:后端有个微服务serviceId 为service-A,则网关服务器接收到的请求URL只要满足/aaa/开头的,都被代理给service-A服务进行处理,service-A服务接收到的请求URL中默认会去掉/aaa/。
3.2 配置详解
- 路由连接数配置和超时配置详解
- 最大请求连接数配置:
- zuul最大连接数配置:
* 总连接数配置:`zuul.host.maxTotalConnections` 默认值为200 * 单个后端服务最大请求连接数配置:`zuul.host.maxPerRouteConnections`,默认值为20
- ribbon最大连接数配置:
* 总连接数配置:`ribbon.MaxTotalHttpConnections` * 单个后端服务最大请求连接数配置:`zuul.MaxConnectionsPerHost`
- hystrix熔断最大连接数配置:
- 两种请求处理模式,信号量和线程池,具体配置请求参考springcloud微服务组件之feign的应用
- 连接数配置大小关系: zuul > hystrix > ribbon
- 超时时间配置
- 全局超时时间配置:
* `zuul.host.connect-timeout-millis` :最大连接超时时间,单位微秒 * `zuul.host.socket-timeout-millis` :最大套接字超时时间,单位微秒,该时间指一次请求的总体超时时间,详情请查看处理一次HTTP请求的步骤。
- ribbon超时时间配置:
- 默认配置
* `ribbon.ConnectTimeout`:请求连接超时时间,单位微秒 * `ribbon.ReadTimeout`: 读取请求输入流超时时间,单位微秒 * `ribbon.MaxAutoRetries`: 最大自动重试次数 * `ribbon.MaxAutoRetriesNextServer` :换实例重试最大次数 * 针对某个服务进行配置 * `service-A.ribbon.ConnectTimeout`:请求连接超时时间,单位微秒 * `service-A.ribbon.ReadTimeout`: 读取请求输入流超时时间,单位微秒 * `service-A.ribbon.MaxAutoRetries`: 最大自动重试次数 * `service-A.ribbon.MaxAutoRetriesNextServer` :换实例重试最大次数
- hystrix熔断超时时间:
* `feign.client.config.default` :全局默认配置 * `feign.client.config.default.connectTimeout` :最大连接超时时间,单位微秒 * `feign.client.config.default.readTimeout` :最大连接超时时间,单位微秒 * `feign.client.config.service-A`:针对某个服务单独配置 * `feign.client.config.service-A.connectTimeout` :最大连接超时时间,单位微秒 * `feign.client.config.service-A.readTimeout` :最大连接超时时间,单位微秒
- 超时时间配置大小关系: zuul > hystrix > ribbon
4、故障转移、负载均衡、服务熔断的实现和参数讲解与调优
zuul 的反向代理底层实现使用的是ribbon组件,利用ribbon的配置,即可做到故障转移、负载均衡、服务熔断等特性
- 故障转移 :
- 自动重试最大次数:
ribbon.MaxAutoRetries
- 换实例重试次数:
ribbon.MaxAutoRetriesNextServer
- 自动重试最大次数:
- 启动熔断
- 启动类添加注解:
@EnableHystrix
- 配置信息添加:
- 启动类添加注解:
hystrix: #熔断相关配置
command:
default:
fallback:
enabled: true #启用熔断类处理失败feign接口
circuitBreaker: #断路器相关配置
sleepWindowInMilliseconds: 5000 #进入熔断状态后休息5秒后再重试
forceClosed: false #启用断路器
requestVolumeThreshold: 50 #10s内达到50个失败就熔断
errorThresholdPercentage: 10 #失败率达到10%后就进入熔断
- 更多配置请参考springcloud微服务组件之feign的应用
- ribbon饥饿加载配置
zuul:
ribbon:
eager-load:
enabled: true #启用应用后,便初始化各个下游服务的ribbon客户端,默认延迟加载
5、如何自定义过滤器,以及其使用场景介绍
- 新建java类
QueryParamPreFilter
为路由过滤器预设参数,可以重定向服务
public class QueryParamPreFilter extends ZuulFilter {
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("sample") != null) {
// put the serviceId in `RequestContext`
ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
}
return null;
}
}
- 实例化过滤器
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class SpringApplication {
public static void main(String[] args) {
SpringApplication.run(SpringApplication.class, args);
}
//实例化
@Bean
public QueryParamPreFilter queryParamPreFilter() {
return new QueryParamPreFilter();
}
}
- 常见使用场景
- 预处理请求参数
- 预处理路由信息
- 安全校验
- 服务限流
- 服务动态版本控制
- 统一响应处理
- 统一错误处理
- 接口调用日志处理
- 流量控制
6、 服务调用异常统一处理,并自定义响应
- 禁用系统默认错误处理器
zuul:
SendErrorFilter:
post:
disable: true #禁用默认的错误过滤器,项目里我们自定义错误处理器
- 新建类
ErrorFilter
@Component
@Slf4j
public class ErrorFilter extends ZuulFilter {
@Autowired(required = false)
private DictManager dictManager;
/**
* @return
* @author danyuan
*/
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().containsKey("throwable");
}
private static Throwable getOriginException(Throwable e){
e = e.getCause();
while(e.getCause() != null){
e = e.getCause();
}
return e;
}
/**
* @return
* @throws ZuulException
* @author danyuan
*/
@Override
public Object run() throws ZuulException {
Throwable e=getOriginException(ctx.getThrowable());
HttpServletResponse response = ctx.getResponse();
response.setContentType("application/json; charset=utf8");
ErrorResponse errorResp = new ErrorResponse(e.getMessage());
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
log.error(e.getMessage());
try {
PrintWriter writer = response.getWriter();
//返回自定义错误信息
writer.print(JSONObject.toJSONString(errorResp));
} catch (IOException ie) {
log.error(ie.getMessage());
} finally {
if (writer != null) {
writer.close();
}
}
return null;
}
/**
* @return
* @author danyuan
*/
@Override
public String filterType() {
return "error";
}
/**
* @return
* @author danyuan
*/
@Override
public int filterOrder() {
return -1;
}
}
@Getter
@Setter
@ToString
@NoArgsConstructor
public class ErrorResponse impliments implements Serializable{
/**
*serialVersionUID
*/
private static final long serialVersionUID = 1L;
private int code = -1;
private String errMsg;
public ErrorResponse(String errMsg){
this.errMsg = errMsg;
}
}
7、 Cookies、Authorization等敏感信息控制与说明
ZUUL在反向代理下游服务时,默认会对请求响应体中的敏感信息进行过滤,但是在某些应用场景需要开启ZUUL对这些信息的透传
- 需要开启的场景有
- 需要设置应用会话信息、cookies时
- 安全中心授权时,需要透传Authorization信息
- 配置信息
- 取值有:
Cookie
,Set-Cookie
,Authorization
,也可以是一些其他的HTTP Header - 默认值为:
Cookie,Set-Cookie,Authorization
,默认这些信息都不允许通过 - 全局配置:zuul.sensitiveHeaders
- 针对某个服务进行配置:
zuul.routes.service-A.sensitiveHeaders
- 取值有:
- 配置示例
zuul:
routes:
service-A:
path: /aaa/**
#运行该服务的所有敏感信息通过
sensitiveHeaders:
url: https://downstream
8、 自定义熔断处理
- 对某个服务进行自定义熔断处理
class ServiceAFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "service-A";
}
@Override
public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return 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() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
- 自定义所有服务的熔断处理
class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
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);
return headers;
}
};
}
}