服务发现 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,必须配置Eureka的上下文(context)和位置(location),给出配置后,Zuul会自动从给出的VIP中选择来自Eureka的服务列表,用于Ribbon client的api。更多关于Ribbon的配置可以参考其官方文档.
如果配置Zuul不使用Eureka,使用静态服务列表或者服务发现提供者,需要保持配置属性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
注意,在这个配置中使用了静态服务器列表,跟使用Eureka的配置相比,除了关闭了eureka,还有一个点是ServerListClassName的配置,类是ConfigurationBasedServerList
,而Eureka是DiscoveryEnabledNIWSServerList
。
负载均衡 Load Balancing
默认情况下,Zuul的负载均衡使用的是Ribbon的ZoneAwareLoadBalancer。该类的算法就是对在服务发现中可用实例的轮询,可用实例是成功追踪到了响应的可用区域(zone)。负载均衡器将对每个区域保持统计,如果失败比例超过配置阈值则丢弃该区域。
如果你想要使用自己的负载均衡器,你需要为Ribbon client命名空间设置NFLoadBalancerClassName
属性或者是覆盖DefaultClientChannelManager
类的getLoadBalancerClass()方法。注意,此时你自己的类需要继承DynamicServerListLoadBalancer
。
Ribbon也允许用户配置负载均衡规则。例如,可以将RoundRobinRule
换成WeightedResponseTimeRule
,AvailabilityFilteringRule
,或者你自己的规则,更多细节参考官方文档
连接池 Connecting Pool
Zuul没有使用Ribbon来为响应创建连接,而是使用Netty client创建了自己的连接池。Zuul为每个主机每个事件循环创建一个连接池。这样做是为了降低线程上下文切换的成本,确保了请求事件循环和响应事件循环的完整性。带来的结果就是无论请求是哪个事件循环在执行,但是整个请求过程都在同一个线程中执行。
这种策略的一个副作用是当有大量的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状态是通用的,但是并没有提供更细粒度的状态。为了指定更多的失败模型,Zuul创建了可能失败原因的枚举,如下表。
StatusCategory | Definition |
---|---|
SUCCESS | Successful request |
SUCCESS_NOT_FOUND | Succesfully proxied but status was 404 |
SUCCESS_LOCAL_NOTSET | Successful request but no StatusCategory was set |
SUCCESS_LOCAL_NO_ROUTE | Technically successful, but no routing found for the request |
FAILURE_LOCAL | Local Zuul failure (e.g. exception thrown) |
FAILURE_LOCAL_THROTTLED_ORIGIN_SERVER_MAXCONN | Request throttled due to max connection limit reached to origin server |
FAILURE_LOCAL_THROTTLED_ORIGIN_CONCURRENCY | Request throttled due to origin concurrency limit |
FAILURE_LOCAL_IDLE_TIMEOUT | Request failed due to idle connection timeout |
FAILURE_CLIENT_CANCELLED | Request failed because client cancelled |
FAILURE_CLIENT_PIPELINE_REJECT | Request failed because client attempted to send pipelined HTTP request |
FAILURE_CLIENT_TIMEOUT | Request failed due to read timeout from the client (e.g. truncated POST body) |
FAILURE_ORIGIN | The origin returned a failure (i.e. 500 status) |
FAILURE_ORIGIN_READ_TIMEOUT | The request to the origin timed out |
FAILURE_ORIGIN_CONNECTIVITY | Could not connect to origin |
FAILURE_ORIGIN_THROTTLED | Origin throttled the request (i.e. 503 status) |
FAILURE_ORIGIN_NO_SERVERS | Could not find any servers to connect to for the origin |
FAILURE_ORIGIN_RESET_CONNECTION | Origin reset the connection before the request could complete |
可以通过StatusCategoryUtils类来设置或者获得状态,例如:
// set
StatusCategoryUtils.setStatusCategory(request.getContext(), ZuulStatusCategory.SUCCESS)
// get
StatusCategoryUtils.getStatusCategory(response)
Zuul2中的类源码有改动,上文中的set不再是request获取的RequestContext,而是RequestContext的替代类SessionContext,
get不再是response,而是ZuulMessage或者RequestContext的替代类SessionRequest,
源码如下:
public class StatusCategoryUtils {
private static final Logger LOG = LoggerFactory.getLogger(StatusCategoryUtils.class);
public static StatusCategory getStatusCategory(ZuulMessage msg) {
return getStatusCategory(msg.getContext());
}
public static StatusCategory getStatusCategory(SessionContext ctx) {
return (StatusCategory) ctx.get(CommonContextKeys.STATUS_CATGEORY);
}
public static void setStatusCategory(SessionContext ctx, StatusCategory statusCategory) {
ctx.set(CommonContextKeys.STATUS_CATGEORY, statusCategory);
}
public static StatusCategory getOriginStatusCategory(SessionContext ctx) {
return (StatusCategory) ctx.get(CommonContextKeys.ORIGIN_STATUS_CATEGORY);
}
public static boolean isResponseHttpErrorStatus(HttpResponseMessage response) {
boolean isHttpError = false;
if (response != null) {
int status = response.getStatus();
isHttpError = isResponseHttpErrorStatus(status);
}
return isHttpError;
}
public static boolean isResponseHttpErrorStatus(int status) {
return (status < 100 || status >= 500);
}
public static void storeStatusCategoryIfNotAlreadyFailure(final SessionContext context, final StatusCategory statusCategory) {
if (statusCategory != null) {
final StatusCategory nfs = (StatusCategory) context.get(CommonContextKeys.STATUS_CATGEORY);
if (nfs == null || nfs.getGroup().getId() == ZuulStatusCategoryGroup.SUCCESS.getId()) {
context.set(CommonContextKeys.STATUS_CATGEORY, statusCategory);
}
}
}
}
重试机制 Retries
重试是保证弹性的关键特性之一。在Zuul中,重试会被认真对待并且大量的使用了重试。重试请求的逻辑如下:
对错误的重试 Retry on errors
- 如果错误是读超时,连接重置或者连接错误,则zuul会重试
对状态码的重试 Retry on status codes
- 如果状态码是503,则zuul会重试
- 如果状态码配置为了幂等的,并且方法是GET,HEAD,OPTIONS之一,zuul会重试。如何配置状态码为幂等使用属性
zuul.retry.allowed.statuses.idempotent
,下文会列出。
Zuul在一个临时状态是不会重试的: - 如果已经开始给client发送响应
- 如果丢失了内容体(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
<originName>.ribbon.MaxAutoRetriesNextServer
设置重试次数
zuul.retry.allowed.statuses.idempotent
是逗号分隔的列表,如500,501,502,503
请求通行证/快照 Request Passport
在Zuul由1升级为2的过程中,Netflix开发并开源了一些好用的工具,Request Passport就是调试最好的工具。它是基于纳秒按照时间序列记录的一个请求所有的瞬时状态。
成功请求示例 Example of successful request
如下是Netflix提供的一个示例,一个运行多个filter的简单请求,有一些IO操作,大理请求,对response执行filter,然后将结果写回的客户端。
CurrentPassport {start_ms=1523578203359,
[+0=IN_REQ_HEADERS_RECEIVED,
+260335=FILTERS_INBOUND_START,
+310862=IN_REQ_LAST_CONTENT_RECEIVED,
+1053435=MISC_IO_START,
+2202112=MISC_IO_STOP,
+3917598=FILTERS_INBOUND_END,
+4157288=ORIGIN_CH_CONNECTING,
+4218319=ORIGIN_CONN_ACQUIRE_START,
+4443588=ORIGIN_CH_CONNECTED,
+4510115=ORIGIN_CONN_ACQUIRE_END,
+4765495=OUT_REQ_HEADERS_SENDING,
+4799545=OUT_REQ_LAST_CONTENT_SENDING,
+4820669=OUT_REQ_HEADERS_SENT,
+4822465=OUT_REQ_LAST_CONTENT_SENT,
+4830443=ORIGIN_CH_ACTIVE,
+20811792=IN_RESP_HEADERS_RECEIVED,
+20961148=FILTERS_OUTBOUND_START,
+21080107=IN_RESP_LAST_CONTENT_RECEIVED,
+21109342=ORIGIN_CH_POOL_RETURNED,
+21539032=FILTERS_OUTBOUND_END,
+21558317=OUT_RESP_HEADERS_SENDING,
+21575084=OUT_RESP_LAST_CONTENT_SENDING,
+21594236=OUT_RESP_HEADERS_SENT,
+21595122=OUT_RESP_LAST_CONTENT_SENT,
+21659271=NOW]}
超时示例 Example of a timeout
该示例是一个超时示例。跟前边的实例相似,但是并不是响应请求的时间间隔和超时事件。
CurrentPassport {start_ms=1523578490446,
[+0=IN_REQ_HEADERS_RECEIVED,
+139712=FILTERS_INBOUND_START,
+1364667=MISC_IO_START,
+2235393=MISC_IO_STOP,
+3686560=FILTERS_INBOUND_END,
+3823010=ORIGIN_CH_CONNECTING,
+3891023=ORIGIN_CONN_ACQUIRE_START,
+4242502=ORIGIN_CH_CONNECTED,
+4311756=ORIGIN_CONN_ACQUIRE_END,
+4401724=OUT_REQ_HEADERS_SENDING,
+4453035=OUT_REQ_HEADERS_SENT,
+4461546=ORIGIN_CH_ACTIVE,
+45004599181=ORIGIN_CH_READ_TIMEOUT,
+45004813647=FILTERS_OUTBOUND_START,
+45004920343=ORIGIN_CH_CLOSE,
+45004945985=ORIGIN_CH_CLOSE,
+45005052026=ORIGIN_CH_INACTIVE,
+45005246081=FILTERS_OUTBOUND_END,
+45005359480=OUT_RESP_HEADERS_SENDING,
+45005379978=OUT_RESP_LAST_CONTENT_SENDING,
+45005399999=OUT_RESP_HEADERS_SENT,
+45005401335=OUT_RESP_LAST_CONTENT_SENT,
+45005486729=NOW]}
你可以记录通行证,把它添加到请求/响应头中或者输出到一个存储中以便接下来的调试。可以通过通道(channel)或者回话上下文(session context)来取出passport。例如:
// from channel
CurrentPassport passport = CurrentPassport.fromChannel(channel);
// from context
CurrentPassport passport = CurrentPassport.fromSessionContext(context);
请求尝试 Request Attempts
另一个非常有用的调试特性是记录Zuul产生的请求尝试。我们在每个响应中添加它仅仅作为内部头使用,它使跟踪和调试请求对我们和内部合作伙伴来说变的非常简单。
成功请求示例 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* filter中通过回话上下文(session context)来获取请求尝试(request attempts),例如:
// from context
RequestAttempts attempts = RequestAttempts.getFromSessionContext(context);
原始并发保护 Origin Concurrency Protection
有些时候原始请求处理服务器会发生问题,尤其是请求量超过他们本身容量时。我们知道,Zuul作为一个代理,后端有问题的原始请求处理服务器会占满连接和内存从而潜在的影响其他的服务器。为了保护原始服务器和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限制请求。
如果请求超过总体并发或者每个服务器并发,Zuul会返回503给请求端,而不会把请求继续转发给后端服务器处理。
HTTP/2
Zuul可以在HTTP/2的模式下运行。在这种模式中,程序需要一个SSL证书,如果运行在ELB之后,则必须使用TCP 监听器。具体示例餐参考sample app
HTTP/2相关配置属性如下:
server.http2.max.concurrent.streams // default: 100
server.http2.initialwindowsize // default: 5242880
server.http2.maxheadertablesize // default: 65536
server.http2.maxheaderlistsize // default: 32768
相互的TLS Mutual TLS
Zuul可以运行在相互的TLS模式,也就是双方都提供证书认证。在这种模式下,程序必须都有SSL证书,并有请求证书的信任存储。作为HTTP/2,必须在ELB的TCP监听器后运行。
代理协议 Proxy Protocol
代理协议在使用TCP监听器的时候是非常重要的特性,在Zuul中可以通过如下的服务端设置开启:
// 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
,然后可以通过通道(channel)直接接收:
String clientIp = channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get();
压缩 GZip
Zuul自带了GZipResponseFilter
,可以对输出的响应进行压缩。
是否压缩,取决于内容类型、内容大小以及请求头Accept-Encoding
中是否包含gzip
。
网友评论