Zuul2.1文档
- What is Zuul?
- Why did we build Zuul?
- How We Use Zuul At Netflix
- Getting Started 2.0
- How It Works 2.0
- Server Configuration
- Filters
- Core Features
- Push Messaging
What is Zuul?
Zuul是从设备和web站点来的所有访问Netflix 流程序的请求的前门。作为一个边缘服务程序,Zuul带来动态路由、监视、弹性和安全能力。它也有能力把请求路由到多个适当的 Amazon Auto Scaling Groups。
Why did we build Zuul?
Netflix API流量的数量和多样性,有时候导致快速产生问题而没有报警。我们需要一个系统,它允许我们为应对这些情况而快速改变行为。
Zuul使用很多不同类型的过滤器,可以快速灵活地把功能应用到我们的边缘服务。这些过滤器帮助我们执行下列功能:
*认证和安全-为每个需要的资源做识别认证,拒绝不安全的请求
*洞察和监控-跟踪有意义的数据,在边缘做统计,以便准确了解产品的运行情况
*动态路由-把请求动态路由到不同的后端集群
*压力测试-逐渐增加集群的流量,来评估性能
*甩负荷-为每种请求分配容量,拒绝超限的请求
*静态响应处理-在边缘直接生成一些响应而不访问内部集群
*多区域弹性-跟 AWS区域做路由,让ELB的使用更多样化,让边缘接近成员
How We Use Zuul At Netflix
Getting Started 2.0
Maven的例子:
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<version>2.1.2</version>
</dependency>
Gradle:
compile "com.netflix.zuul:zuul-core:2.1.2"
How It Works 2.0
Architectural Overview
Zuul 2.0是一个 Netty服务器,运行 inbound过滤器,然后使用一个Netty 客户端代理这些请求,然后经过 outbound过滤器返回响应。
Filters
Zuul的过滤器是处理业务逻辑的核心。他们有权力做大范围的的做,能在请求-响应的生存期的不同部分内运行。
*输入过滤-在路由前执行,可以做认证、路由、装饰等类似的工作
*端过滤-能返回静态响应,否则内建的 ProxyEndpoint过滤器讲把请求路由到源
*输出过滤-获得响应以后执行,能用来测量、装饰响应或者增加自定义头。
有两种类型的过滤器:同步和异步。因为我们在事件循环上运行,在过滤器里不要阻塞。如果你需要阻塞,就在异步过滤器里做,使用一个单独的线程池;否则,可以使用同步过滤器。
Server Configuration
Server Modes
当前支持的服务器模式是:
*HTTP
*HTTP/2 (requires TLS)
*HTTP - Mutual TLS
你可以在参考 sample appliction看该怎么配置。通过修改 SERVER_TYPE变量,可以在不同模式运行。
HTTP
该模式打算运行在ELB HTTP监听器后面,它结束TLS,传递 XFF头。
如果你想运行 plaintext模式,没有任何 ELB面向你,出于安全考虑,你可能想去掉代理头。可以这样设置:
channelConfig.set(CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER);
HTTP/2
ELB不支持 HTTP/2。所以,如果你想使用 HTTP/2,你可以使用 ELB TCP监听器,在Zuul里终止该协议。 HTTP/2配置需要一个 SSL cert,启用代理协议代替XFF头。
如果你终止 HTTP/2,使用ALB,可以使用上面的配置。
Mutual TLS
ELB也不支持交互TLS,所以,你不得不使用 ELB TCP监听器,在Zuul里终止使用TLS。在该模式下,你将需要一个 TLS cert和一个客户证书的信任的存储。也需要启用代理协议代替XFF头。
Filters
过滤器是 Zuul功能的核心。他们负责程序的业务逻辑,执行各种任务。
*Type: 过滤器被应用前,常要被定义,可以是任何字符串
*Async: 过滤器是同步的还是异步的,通常意味着你需要使用外部线程或者内部的
*Execution Order: 在Type中应用,如果有多个过滤器时的执行顺序
*Criteria: 过滤器的执行条件
*Action: 条件得到满足时,执行的动作
Zuul提供的框架可以动态读、编译和运行过滤器。过滤器之间不直接通信-通过每个请求里的RequestContext共享状态。
过滤器使用 Groovy语言写的,所以,Zuul支持任何基于JVM的语言。每一个过滤器的源码写到Zuul服务的特定目录下,周期性的轮询,看是否有变化。从磁盘读入修改后的过滤器,在运行的服务里动态编译,处理随后的每个请求时,都会被Zuul调
Incoming
输入过滤器在请求被代理前执行。大部分业务逻辑一般在这时候执行。例如:认证、动态路由、限速、 DDoS保护、指标。
Endpoint
端过滤器基于输入过滤器的执行情况处理请求。 Zuul的内建的 ProxyEndpoint过滤器负责代理请求访问后端服务,所以,这类过滤器一般用来返回静态内容。例如,健康检查响应,静态错误响应、404响应等。
Outgoing
输出过滤器处理接收后端返回后的动作。一般用来完善响应、增加指标。例如:保存统计信息、增加或者删除标准头,给实时流系统发数据、压缩。
Async
过滤器可以同步或者异步执行。如果过滤器不做很多工作,不阻塞,可以通过扩展 HttpInboundSyncFilter或者 HttpOutboundSyncFilter安全地使用同步过滤器。
如果需要从其他服务或者缓存取数据,或者其他比较重的计算。这时候,你不要阻塞Netty线程,应该使用异步过滤器,它返回一个 Observable包装的响应。此时,你应该扩展 HttpInboundFilter或者 HttpOutboundFilter。
Extracting Body Content
缺省地,Zuul不缓存body 的内容,在 body被接收之前,就把接收到的头发给源了。过滤器依赖头数据时,这样做是高效而理想的。如果你想在 inbound或者 outbound过滤器了提取请求或者响应的body,需要明确地告诉Zull缓存body。你可以在过滤器里重写needsBodyBuffered()方法:
@Override
boolean needsBodyBuffered(HttpResponseMessage input) {
return true
}
Useful Filters
sample app里有一些有用的过滤器。
Sample Filters
*DebugRequest-查找一个query param,给请求增加一个额外的调试日志
*Healthcheck-返回200的简单的静态端点过滤器,如果启动正确
*ZuulResponseFilter-增加路由细节、请求执行、状态和错误原因的信息头
Core Filters
*GzipResponseFilter-打开gzip响应输出
*SurgicalDebugFilter-调试的时候,可以把特定的请求路由到其他机器
Core Features
Service Discovery
Zuul可以无缝地和 Eureka工作。也可以和静态服务列表或者你选择的其他发现服务一起工作。
这样和 Eureka服务器一起工作:
### Load balancing backends with Eureka
eureka.shouldUseDns=true
eureka.eurekaServer.context=discovery/v2
eureka.eurekaServer.domainName=discovery${environment}.netflix.net
eureka.eurekaServer.gzipContent=true
eureka.serviceUrl.default=http://${region}.${eureka.eurekaServer.domainName}:7001/${eureka.eurekaServer.context}
api.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
api.ribbon.DeploymentContextBasedVipAddresses=api-test.netflix.net:7001
上面的配置,指定了 Eureka上下文和位置。然后,Zuul自动选择服务列表,让Ribbon客户端使用特定的VIP。
想让Zuul使用静态服务列表或者其他发现提供者,可以配置 listOfServers属性:
### Load balancing backends without Eureka
eureka.shouldFetchRegistry=false
api.ribbon.listOfServers=100.66.23.88:7001,100.65.155.22:7001
api.ribbon.client.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
api.ribbon.DeploymentContextBasedVipAddresses=api-test.netflix.net:7001
注意:服务列表的类名用 ConfigurationBasedServerList代替了 DiscoveryEnabledNIWSServerList。
Load Balancing
Zuul缺省使用 Ribbon的 ZoneAwareLoadBalancer做负载均衡。采用轮询有效实例的算法,跟踪有效zone的成功。负载均衡器持续统计每个 zone,如果失败率超过配置的阈值,将去掉该zone。
如果想自定义负载均衡,可以设置 Ribbon客户端名称空间的 NFLoadBalancerClassName属性,或者重写 DefaultClientChannelManager的getLoadBalancerClass()方法。注意,你的类应该扩展 DynamicServerListLoadBalancer。
Ribbon也允许你配置负载均衡规则。例如,RoundRobinRule 可以设置为 WeightedResponseTimeRule、 AvailabilityFilteringRule或者你自己的规则。
你可以在here找到细节。
Connection Pooling
Zuul不使用 Ribbon建立流出的连接,而使用Netty客户端创建了自己的连接池。 Zuul为每个主机,每个事件循环建立连接池。这样做,可以减少线程间的上下文切换,确保inbound事件循环和outbound事件循环的完整性。结果是整个请求在同样的线程内运行,而不管哪个事件循环运行它。
该策略的一个副作用是,如果运行很多Zuul实例,每个实例有很多事件循环,到每个后端服务的连接会很多。配置连接池时,要注意这一点。
连接池的一些有用的设置,和他们的默认值是:
**Ribbon Client Config Properties**
<originName>.ribbon.ConnectionTimeout // default: 500 (ms)
<originName>.ribbon.MaxConnectionsPerHost // default: 50
<originName>.ribbon.ConnIdleEvictTimeMilliSeconds // default: 60000 (ms)
<originName>.ribbon.ReceiveBufferSize // default: 32 * 1024
<originName>.ribbon.SendBufferSize // default: 32 * 1024
<originName>.ribbon.UseIPAddrForServer // default: true
**Zuul Properties**
# Max amount of requests any given connection will have before forcing a close
<originName>.netty.client.maxRequestsPerConnection // default: 1000
# Max amount of connection per server, per event loop
<originName>.netty.client.perServerWaterline // default: 4
# Netty configuration connection
<originName>.netty.client.TcpKeepAlive // default: false
<originName>.netty.client.TcpNoDelay // default: false
<originName>.netty.client.WriteBufferHighWaterMark // default: 32 * 1024
<originName>.netty.client.WriteBufferLowWaterMark // default: 8 * 1024
<originName>.netty.client.AutoRead // default: false
连接池输出很多指标,如果想采集,看一看 Spectator注册。
Status Categories
HTTP状态是通用的,不提供很多粒度。为了更具体的失败模式,我们增加了一个可能失败的枚举。
StatusCategory | Definition |
---|---|
SUCCESS | 成功 |
SUCCESS_NOT_FOUND | 代理成功,状态是404 |
SUCCESS_LOCAL_NOTSET | 请求成功,没设置StatusCategory |
SUCCESS_LOCAL_NO_ROUTE | 术上成功,但是没有找到路由 |
FAILURE_LOCAL | 本地Zuul失败(异常等) |
FAILURE_LOCAL_THROTTLED_ORIGIN_SERVER_MAXCONN | 请求超最大连接限制 |
FAILURE_LOCAL_THROTTLED_ORIGIN_CONCURRENCY | 请求超源的并发限制 |
FAILURE_LOCAL_IDLE_TIMEOUT | 请求因为idle超时失败 |
FAILURE_CLIENT_CANCELLED | 客户端取消,请求失败 |
FAILURE_CLIENT_PIPELINE_REJECT | 客户端试图发送管道HTTP请求,请求失败 |
FAILURE_CLIENT_TIMEOUT | 来自客户端的读超时(例如POST body被截断),请求失败 |
FAILURE_ORIGIN | 源返回失败(例如状态500) |
FAILURE_ORIGIN_READ_TIMEOUT | 请求源超时 |
FAILURE_ORIGIN_CONNECTIVITY | 连不到源 |
FAILURE_ORIGIN_THROTTLED | 源扼杀了请求(例如状态503) |
FAILURE_ORIGIN_NO_SERVERS | 没有有效的源的服务 |
FAILURE_ORIGIN_RESET_CONNECTION | 请求完成前,源复位了连接 |
可以使用 StatusCategoryUtils类获取或者设置状态:
// set
StatusCategoryUtils.setStatusCategory(request.getContext(), ZuulStatusCategory.SUCCESS)
// get
StatusCategoryUtils.getStatusCategory(response)
Retries
一个关键特性是重试。Zuul的重试是可扩展的。当重试一个请求的时候,使用下面的逻辑做决定:
Retry on errors
*如果发生读超时错误,就复位连接或者连接错误
Retry on status codes
*如果状态码是503
*如果状态码是配置的幂等状态(见下面),并且method是下列之一:GET、HEAD或者OPTIONS
在一个短暂的状态时不重试,特别是:
*已经开始向客户发送响应
*body不完整(只缓存了部分或者body被截断)
相关的属性:
# Sets a retry limit for both error and status code retries
<originName>.ribbon.MaxAutoRetriesNextServer // default: 0
# This is a comma-delimited list of status codes
zuul.retry.allowed.statuses.idempotent // default: 500
Request Passport
这是我们的一个调试的好工具。它是请求过程中,按时间排序的状态集,带nanoseconds时间戳。
可以记录 passport,可以加到头里,或者持久化保存。可以使用 channel或者 session context。
// from channel
CurrentPassport passport = CurrentPassport.fromChannel(channel);
// from context
CurrentPassport passport = CurrentPassport.fromSessionContext(context);
Request Attempts
另一个有用的调试特性是跟踪请求尝试。一般把它加到每个响应的内部的头里。
Example of successful request
[{"status":200,"duration":192,"attempt":1,"region":"us-east-1","asg":"simulator-v154","instanceId":"i-061db2c67b2b3820c","vip":"simulator.netflix.net:7001"}]
Example of failed request
[{"status":503,"duration":142,"attempt":1,"error":"ORIGIN_SERVICE_UNAVAILABLE","exceptionType":"OutboundException","region":"us-east-1","asg":"simulator-v154","instanceId":"i-061db2c67b2b3820c","vip":"simulator.netflix.net:7001"},
{"status":503,"duration":147,"attempt":2,"error":"ORIGIN_SERVICE_UNAVAILABLE","exceptionType":"OutboundException","region":"us-east-1","asg":"simulator-v154","instanceId":"i-061db2c67b2b3820c","vip":"simulator.netflix.net:7001"}]
可以在 outbound过滤器里,从session 上下文获得请求尝试:
// from context
RequestAttempts attempts = RequestAttempts.getFromSessionContext(context);
Origin Concurrency Protection
有时候,源会遇上麻烦,尤其是请求超过容量的时候。我们是一个代理,坏的源可能占用我们的连接和内存,影响其他源。为了保护源和Zuul,使用并发限制,防止服务中断。
有两种办法管理源并发:
Overall Origin Concurrency
zuul.origin.<originName>.concurrency.max.requests // default: 200
zuul.origin.<originName>.concurrency.protect.enabled // default: true
Per Server Concurrency
<originName>.ribbon.MaxConnectionsPerHost // default: 50
如果超限,Zuul向客户端返回503。
HTTP/2
Zuul能在 HTTP/2模式下运行。此时,它需要一个SSL证书,如果Zuul在ELB的后面,需要使用 TCP监听器。
相关属性:
server.http2.max.concurrent.streams // default: 100
server.http2.initialwindowsize // default: 5242880
server.http2.maxheadertablesize // default: 65536
server.http2.maxheaderlistsize // default: 32768
Mutual TLS
Zuul能运行在交互TLS模式下。此时,需要SSL证书,也要保存发来的证书。像 HTTP/2那样,运行在ELB的TCP监听器后面。
Proxy Protocol
使用TCP监听器时,代理协议是一个重要特性,使用下面的配置打开:
// strip XFF headers since we can no longer trust them
channelConfig.set(CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER);
// prefer proxy protocol when available
channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, true);
// enable proxy protocol
channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, true);
客户端IP被正确设置到过滤器的 HttpRequestMessage,也可以这样检索:
String clientIp = channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get();
GZip
Zuul的输出 GzipResponseFilter,可以使用gzip处理响应。
根据内容类型、body大小和请求头Accept-Encoding是否包含gzip 做决定。
Push Messaging
Zulu 2.0支持推送消息-从服务器给客户发消息。支持两种协议, WebSockets和SSE。
sample app演示了两种推送。
Authentication
Zuul推送服务必须对每个接入的连接做鉴权。可以自定义鉴权。通过扩展抽象类 PushAuthHandler,实现它的方法doAuth() 。请参考SamplePushAuthHandler。
Client Registration and Lookup
鉴权完成以后, Zuul推送服务注册每个已鉴权的连接或者用户标识,这样可以知道消息推给了谁。实现 PushUserAuth接口,如果鉴权成功,doAuth()返回它的一个实例。参考例子SamplePushUserAuth。
每个 Zuul推送服务使用PushConnectionRegistry 在内存里维护一个本地的全部已连接的客户端的注册信息。对于单实例的推送,在本地内存注册已经足够了。如果是多节点的推送集群,需要一个二级的外部全局数据源。此时,查找一个客户端需要两步。首先在全局的外部存储里查找客户端连接的推送服务,然后在返回的服务里的本地注册里查找实际的连接。
可以通过扩展 PushRegistrationHandler,重载registerClient()方法,来整合外部的全局注册。 Zuul推送允许使用任何数据源做全局注册,数据源最好支持下列属性
*Low read latency-低读延迟
*TTL or automatic record expiry of some sort-TTL或其他的自动超时机制
*Sharding-分区
*Replication-复制
有这些特性意味着你的推送集群能水平扩展到数百万的连接。 Redis、 Cassandra和Amazon DynamoDB都是比较好的选择。
例子 SampleWebSocketPushRegistrationHandler和SampleSSEPushRegistrationHandler显示了怎么在推送服务里整合 WebSocket和 SSE连接。
Accepting new Push connection
SampleWebSocketPushChannelInitializer和SampleSSEPushChannelInitializer演示了如何设置 Netty管道来接收 WebSocket和 SSE连接。这些类基于使用的协议为每个连接设置鉴权和注册处理器。
Load balancers vs WebSockets and SSE
推送连接和普通的请求/响应类型的HTTP连接是不同的,他们是持久、长期存活的。一旦建立了连接,甚至在没有待处理的请求时,仍然保持连接。而一般的负载均衡器在一段时间不活跃后,会断开连接。Amazon Elastic Load Balancers (ELB) 和旧版本的 HAProxy、 Nginx都是这样的。你的推送集群如何在负载均衡器后面工作,你有两个选择:
*使用最新版的支持WebSocket 代理的负载均衡器,比如 HAProxy、 Nginx或者Application Load Balancer (ALB)代替 ELB,或者
*让负载均衡器运行在4层,而不是7层。大多数负载均衡器-包括ELBs-支持TCP模式。此时,他们只代理TCP包,而不解析/解释应用协议。
你可能也需要增加负载均衡器的空闲超时值,缺省值通常是秒级的,不足以支持长期存活的经常空闲的推送连接。
Configuration options
Name | Description | Default value |
---|---|---|
zuul.push.registry.ttl.seconds | 全局注册的超时时间 | 1800 seconds |
zuul.push.reconnect.dither.seconds | 每个客户端的最长连接期的随机窗口。以后重连的间隔 | 180 seconds |
zuul.push.client.close.grace.period | 服务器等待客户端关闭连接的时间,超时在服务器侧强制关闭连接 | 4 seconds |
如果使用 Netflix OSS Archaius,可以在运行期修改上述配置,而不用重启服务。