服務(wù)發(fā)現(xiàn) Service Discovery
Zuul被構(gòu)建為可以無縫的和Eureka一起運(yùn)行佑刷,但是也可以通過配置來指定靜態(tài)服務(wù)列表或者使用其他的服務(wù)發(fā)現(xiàn)惶楼。
使用Eureka的標(biāo)準(zhǔn)方法如下:
### 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會(huì)自動(dòng)從給出的VIP中選擇來自Eureka的服務(wù)列表,用于Ribbon client的api救欧。更多關(guān)于Ribbon的配置可以參考其官方文檔.
如果配置Zuul不使用Eureka衰粹,使用靜態(tài)服務(wù)列表或者服務(wù)發(fā)現(xiàn)提供者,需要保持配置屬性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
注意颜矿,在這個(gè)配置中使用了靜態(tài)服務(wù)器列表寄猩,跟使用Eureka的配置相比,除了關(guān)閉了eureka骑疆,還有一個(gè)點(diǎn)是ServerListClassName的配置田篇,類是ConfigurationBasedServerList
,而Eureka是DiscoveryEnabledNIWSServerList
箍铭。
負(fù)載均衡 Load Balancing
默認(rèn)情況下泊柬,Zuul的負(fù)載均衡使用的是Ribbon的ZoneAwareLoadBalancer。該類的算法就是對(duì)在服務(wù)發(fā)現(xiàn)中可用實(shí)例的輪詢诈火,可用實(shí)例是成功追蹤到了響應(yīng)的可用區(qū)域(zone)兽赁。負(fù)載均衡器將對(duì)每個(gè)區(qū)域保持統(tǒng)計(jì),如果失敗比例超過配置閾值則丟棄該區(qū)域冷守。
如果你想要使用自己的負(fù)載均衡器刀崖,你需要為Ribbon client命名空間設(shè)置NFLoadBalancerClassName
屬性或者是覆蓋DefaultClientChannelManager
類的getLoadBalancerClass()方法。注意拍摇,此時(shí)你自己的類需要繼承DynamicServerListLoadBalancer
亮钦。
Ribbon也允許用戶配置負(fù)載均衡規(guī)則。例如充活,可以將RoundRobinRule
換成WeightedResponseTimeRule
蜂莉,AvailabilityFilteringRule
,或者你自己的規(guī)則混卵,更多細(xì)節(jié)參考官方文檔
連接池 Connecting Pool
Zuul沒有使用Ribbon來為響應(yīng)創(chuàng)建連接映穗,而是使用Netty client創(chuàng)建了自己的連接池。Zuul為每個(gè)主機(jī)每個(gè)事件循環(huán)創(chuàng)建一個(gè)連接池幕随。這樣做是為了降低線程上下文切換的成本蚁滋,確保了請(qǐng)求事件循環(huán)和響應(yīng)事件循環(huán)的完整性。帶來的結(jié)果就是無論請(qǐng)求是哪個(gè)事件循環(huán)在執(zhí)行合陵,但是整個(gè)請(qǐng)求過程都在同一個(gè)線程中執(zhí)行枢赔。
這種策略的一個(gè)副作用是當(dāng)有大量的Zuul實(shí)例并且每個(gè)實(shí)例都有大量的事件循環(huán)在運(yùn)行時(shí),最小數(shù)量的連接都可能會(huì)讓后端服務(wù)器的連接都很高拥知。在配置連接池時(shí)記住這一點(diǎn)是很重要的踏拜。
如下是一些有用的配置,默認(rèn)值低剔。
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
連接池也輸出一些度量指標(biāo)速梗,如果需要收集則可以參考Spectator
狀態(tài)分類 Status Categories
盡管HTTP狀態(tài)是通用的肮塞,但是并沒有提供更細(xì)粒度的狀態(tài)。為了指定更多的失敗模型姻锁,Zuul創(chuàng)建了可能失敗原因的枚舉枕赵,如下表。
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類來設(shè)置或者獲得狀態(tài)位隶,例如:
// set
StatusCategoryUtils.setStatusCategory(request.getContext(), ZuulStatusCategory.SUCCESS)
// get
StatusCategoryUtils.getStatusCategory(response)
Zuul2中的類源碼有改動(dòng)拷窜,上文中的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);
}
}
}
}
重試機(jī)制 Retries
重試是保證彈性的關(guān)鍵特性之一笋妥。在Zuul中懊昨,重試會(huì)被認(rèn)真對(duì)待并且大量的使用了重試。重試請(qǐng)求的邏輯如下:
對(duì)錯(cuò)誤的重試 Retry on errors
- 如果錯(cuò)誤是讀超時(shí)春宣,連接重置或者連接錯(cuò)誤酵颁,則zuul會(huì)重試
對(duì)狀態(tài)碼的重試 Retry on status codes
- 如果狀態(tài)碼是503,則zuul會(huì)重試
- 如果狀態(tài)碼配置為了冪等的月帝,并且方法是GET躏惋,HEAD,OPTIONS之一嚷辅,zuul會(huì)重試其掂。如何配置狀態(tài)碼為冪等使用屬性
zuul.retry.allowed.statuses.idempotent
,下文會(huì)列出潦蝇。
Zuul在一個(gè)臨時(shí)狀態(tài)是不會(huì)重試的: - 如果已經(jīng)開始給client發(fā)送響應(yīng)
- 如果丟失了內(nèi)容體(body),尤其是緩存或者刪除了內(nèi)容
相關(guān)配置屬性為:
# 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
設(shè)置重試次數(shù)
zuul.retry.allowed.statuses.idempotent
是逗號(hào)分隔的列表深寥,如500,501,502,503
請(qǐng)求通行證/快照 Request Passport
在Zuul由1升級(jí)為2的過程中攘乒,Netflix開發(fā)并開源了一些好用的工具,Request Passport就是調(diào)試最好的工具惋鹅。它是基于納秒按照時(shí)間序列記錄的一個(gè)請(qǐng)求所有的瞬時(shí)狀態(tài)则酝。
成功請(qǐng)求示例 Example of successful request
如下是Netflix提供的一個(gè)示例,一個(gè)運(yùn)行多個(gè)filter的簡(jiǎn)單請(qǐng)求闰集,有一些IO操作沽讹,大理請(qǐng)求,對(duì)response執(zhí)行filter武鲁,然后將結(jié)果寫回的客戶端爽雄。
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]}
超時(shí)示例 Example of a timeout
該示例是一個(gè)超時(shí)示例。跟前邊的實(shí)例相似沐鼠,但是并不是響應(yīng)請(qǐng)求的時(shí)間間隔和超時(shí)事件挚瘟。
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]}
你可以記錄通行證叹谁,把它添加到請(qǐng)求/響應(yīng)頭中或者輸出到一個(gè)存儲(chǔ)中以便接下來的調(diào)試〕烁牵可以通過通道(channel)或者回話上下文(session context)來取出passport焰檩。例如:
// from channel
CurrentPassport passport = CurrentPassport.fromChannel(channel);
// from context
CurrentPassport passport = CurrentPassport.fromSessionContext(context);
請(qǐng)求嘗試 Request Attempts
另一個(gè)非常有用的調(diào)試特性是記錄Zuul產(chǎn)生的請(qǐng)求嘗試。我們?cè)诿總€(gè)響應(yīng)中添加它僅僅作為內(nèi)部頭使用订框,它使跟蹤和調(diào)試請(qǐng)求對(duì)我們和內(nèi)部合作伙伴來說變的非常簡(jiǎn)單析苫。
成功請(qǐng)求示例 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"}]
失敗請(qǐng)求的示例 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)來獲取請(qǐng)求嘗試(request attempts),例如:
// from context
RequestAttempts attempts = RequestAttempts.getFromSessionContext(context);
原始并發(fā)保護(hù) Origin Concurrency Protection
有些時(shí)候原始請(qǐng)求處理服務(wù)器會(huì)發(fā)生問題穿扳,尤其是請(qǐng)求量超過他們本身容量時(shí)衩侥。我們知道,Zuul作為一個(gè)代理纵揍,后端有問題的原始請(qǐng)求處理服務(wù)器會(huì)占滿連接和內(nèi)存從而潛在的影響其他的服務(wù)器顿乒。為了保護(hù)原始服務(wù)器和Zuul本身,我們?cè)O(shè)置了并發(fā)限制來平滑的實(shí)現(xiàn)服務(wù)中斷泽谨。
有兩種方式管理原始服務(wù)器并發(fā):
總體服務(wù)器并發(fā) Overall Origin Concurrency
zuul.origin.<originName>.concurrency.max.requests // default: 200
zuul.origin.<originName>.concurrency.protect.enabled // default: true
也就是對(duì)服務(wù)器請(qǐng)求總量超過設(shè)置最大值璧榄,不管后臺(tái)服務(wù)器是多少臺(tái)。
每個(gè)服務(wù)器的并發(fā) Per Server Concurrency
<originName>.ribbon.MaxConnectionsPerHost // default: 50
也就是對(duì)后端每個(gè)服務(wù)器的最大請(qǐng)求量吧雹,超過則zuul限制請(qǐng)求骨杂。
如果請(qǐng)求超過總體并發(fā)或者每個(gè)服務(wù)器并發(fā),Zuul會(huì)返回503給請(qǐng)求端雄卷,而不會(huì)把請(qǐng)求繼續(xù)轉(zhuǎn)發(fā)給后端服務(wù)器處理搓蚪。
HTTP/2
Zuul可以在HTTP/2的模式下運(yùn)行。在這種模式中丁鹉,程序需要一個(gè)SSL證書,如果運(yùn)行在ELB之后揣钦,則必須使用TCP 監(jiān)聽器雳灾。具體示例餐參考sample app
HTTP/2相關(guān)配置屬性如下:
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可以運(yùn)行在相互的TLS模式,也就是雙方都提供證書認(rèn)證冯凹。在這種模式下谎亩,程序必須都有SSL證書,并有請(qǐng)求證書的信任存儲(chǔ)宇姚。作為HTTP/2匈庭,必須在ELB的TCP監(jiān)聽器后運(yùn)行。
代理協(xié)議 Proxy Protocol
代理協(xié)議在使用TCP監(jiān)聽器的時(shí)候是非常重要的特性浑劳,在Zuul中可以通過如下的服務(wù)端設(shè)置開啟:
// 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會(huì)在過濾器中被正確的寫入HttpRequestMessage
,然后可以通過通道(channel)直接接收:
String clientIp = channel.attr(SourceAddressChannelHandler.ATTR_SOURCE_ADDRESS).get();
壓縮 GZip
Zuul自帶了GZipResponseFilter
魔熏,可以對(duì)輸出的響應(yīng)進(jìn)行壓縮紊选。
是否壓縮啼止,取決于內(nèi)容類型、內(nèi)容大小以及請(qǐng)求頭Accept-Encoding
中是否包含gzip
兵罢。