微服務之服務掛的太干脆,Nacos還沒反應過來,怎么辦?
本文轉載自微信公眾號「程序新視界」,作者二師兄。轉載本文請聯系程序新視界公眾號。
前言
我們知道通過Nacos等注冊中心可以實現微服務的治理。但引入了Nacos之后,真的就像理想中那樣所有服務都由Nacos來完美的管理了嗎?Too young,too simple!
今天這篇文章就跟大家聊聊,當服務異常宕機,Nacos還未反應過來時,可能會發生的狀況以及現有的解決方案。
Nacos的健康檢查
故事還要從Nacos對服務實例的健康檢查說起。
Nacos目前支持臨時實例使用心跳上報方式維持活性。Nacos客戶端會維護一個定時任務,每隔5秒發送一次心跳請求,以確保自己處于活躍狀態。
Nacos服務端在15秒內如果沒收到客戶端的心跳請求,會將該實例設置為不健康,在30秒內沒收到心跳,會將這個臨時實例摘除。
如果服務突然掛掉
在正常業務場景下,如果關閉掉一個服務實例,默認情況下會在關閉之前主動調用注銷接口,將Nacos服務端注冊的實例清除掉。
如果服務實例還沒來得注銷已經被干掉,比如正常kill一個應用,應用會處理完手頭的事情再關閉,但如果使用kill -9來強制殺掉,就會出現無法注銷的情況。
針對這種意外情況,服務注銷接口是無法被正確調用的,此時就需要健康檢查來確保該實例被刪除。
通過上面分析的Nacos健康檢查機制,我們會發現服務突然掛掉之后,會有15秒的間隙。在這段時間,Nacos服務端還沒感知到服務掛掉,依舊將該服務提供給客戶端使用。
此時,必然會有一部分請求被分配到異常的實例上。針對這種情況,又該如何處理呢?如何確保服務不影響正常的業務呢?
自定義心跳周期
針對上面的問題,我們最容易想到的是解決方案就是縮短默認的健康檢查時間。
原本15秒才能發現服務異常,標記為不健康,那么是否可以將其縮短呢?這樣錯誤影響的范圍便可以變小,變得可控。
針對此,Nacos 1.1.0之后提供了自定義心跳周期的配置。如果你基于客戶端進行操作,在創建實例時,可在實例的metadata數據中進行心跳周期、健康檢查過期時間及刪除實例時間的配置。
相關示例如下:
- String serviceName = randomDomainName();
- Instance instance = new Instance();
- instance.setIp("1.1.1.1");
- instance.setPort(9999);
- Map<String, String> metadata = new HashMap<String, String>();
- // 設置心跳的周期,單位為毫秒
- metadata.put(PreservedMetadataKeys.HEART_BEAT_INTERVAL, "3000");
- // 設置心跳超時時間,單位為毫秒;服務端6秒收不到客戶端心跳,會將該客戶端注冊的實例設為不健康:
- metadata.put(PreservedMetadataKeys.HEART_BEAT_TIMEOUT, "6000");
- // 設置實例刪除的超時時間,單位為毫秒;即服務端9秒收不到客戶端心跳,會將該客戶端注冊的實例刪除:
- metadata.put(PreservedMetadataKeys.IP_DELETE_TIMEOUT, "9000");
- instance.setMetadata(metadata);
- naming.registerInstance(serviceName, instance);
如果是基于Spring Cloud Alibaba的項目,可通過如下方式配置:
- spring:
- application:
- name: user-service-provider
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- heart-beat-interval: 1000 #心跳間隔。單位為毫秒。
- heart-beat-timeout: 3000 #心跳暫停。單位為毫秒。
- ip-delete-timeout: 6000 #Ip刪除超時。單位為毫秒。
在某些Spring Cloud版本中,上述配置可能無法生效。也可以直接配置metadata的數據。配置方式如下:
- spring:
- application:
- name: user-service-provider
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- metadata:
- preserved.heart.beat.interval: 1000 #心跳間隔。時間單位:毫秒。
- preserved.heart.beat.timeout: 3000 #心跳暫停。時間單位:毫秒。即服務端6秒收不到客戶端心跳,會將該客戶端注冊的實例設為不健康;
- preserved.ip.delete.timeout: 6000 #Ip刪除超時。時間單位:秒。即服務端9秒收不到客戶端心跳,會將該客戶端注冊的實例刪除;
其中第一種配置,感興趣的朋友可以看一下NacosServiceRegistryAutoConfiguration中相關組件的實例化。在某些版本中由于NacosRegistration和NacosDiscoveryProperties實例化的順序問題會導致配置未生效。此時可考慮第二種配置形式。
上面的配置項,最終會在NacosServiceRegistry在進行實例注冊時通過getNacosInstanceFromRegistration方法進行封裝:
- private Instance getNacosInstanceFromRegistration(Registration registration) {
- Instance instance = new Instance();
- instance.setIp(registration.getHost());
- instance.setPort(registration.getPort());
- instance.setWeight(nacosDiscoveryProperties.getWeight());
- instance.setClusterName(nacosDiscoveryProperties.getClusterName());
- instance.setEnabled(nacosDiscoveryProperties.isInstanceEnabled());
- // 設置Metadata
- instance.setMetadata(registration.getMetadata());
- instance.setEphemeral(nacosDiscoveryProperties.isEphemeral());
- return instance;
- }
其中setMetadata方法即是。
通過Nacos提供的心跳周期配置,再結合自身的業務場景,我們就可以選擇最適合的心跳檢測機制,盡最大可能避免對業務的影響。
這個方案看起來心跳周期越短越好,但這樣會對Nacos服務端造成一定的壓力。如果服務器允許,還是可以盡量縮短的。
Nacos的保護閾值
在上述配置中,我們還要結合自身的項目情況考慮一下Nacos保護閾值的配置。
在Nacos中針對注冊的服務實例有一個保護閾值的配置項。該配置項的值為0-1之間的浮點數。
本質上,保護閾值是⼀個⽐例值(當前服務健康實例數/當前服務總實例數)。
⼀般流程下,服務消費者要從Nacos獲取可⽤實例有健康/不健康狀態之分。Nacos在返回實例時,只會返回健康實例。
但在⾼并發、⼤流量場景會存在⼀定的問題。比如,服務A有100個實例,98個實例都處于不健康狀態,如果Nacos只返回這兩個健康實例的話。流量洪峰的到來可能會直接打垮這兩個服務,進一步產生雪崩效應。
保護閾值存在的意義在于當服務A健康實例數/總實例數 < 保護閾值時,說明健康的實例不多了,保護閾值會被觸發(狀態true)。
Nacos會把該服務所有的實例信息(健康的+不健康的)全部提供給消費者,消費者可能訪問到不健康的實例,請求失敗,但這樣也⽐造成雪崩要好。犧牲了⼀些請求,保證了整個系統的可⽤。
在上面的解決方案中,我們提到了可以自定義心跳周期,其中能夠看到實例的狀態會由健康、不健康和移除。這些參數的定義也要考慮到保護閾值的觸發,避免雪崩效應的發生。
SpringCloud的請求重試
即便上面我們對心跳周期進行了調整,但在某一實例發生故障時,還會有短暫的時間出現Nacos服務沒來得及將異常實例剔除的情況。此時,如果消費端請求該實例,依然會出現請求失敗。
為了構建更為健壯的應用系統,我們希望當請求失敗的時候能夠有一定策略的重試機制,而不是直接返回失敗。這個時候就需要開發人來實現重試機制。
在微服務架構中,通常我們會基于Ribbon或Spring Cloud LoadBalancer來進行負載均衡處理。除了像Ribbon、Feign框架自身已經支持的請求重試和請求轉移功能。Spring Cloud也提供了標準的loadbalancer相關配置。
關于Ribbon框架的使用我們在這里就不多說了,重點來看看Spring Cloud是如何幫我們實現的。
異常模擬
我們先來模擬一下異常情況,將上面講到的先將上面的心跳周期調大,以方便測試。
然后啟動兩個provider和一個consumer服務,負載均衡基于Spring Cloud LoadBalancer來處理。此時通過consumer進行請求,你會發現LoadBalancer通過輪訓來將請求均勻的分配到兩個provider上(打印日志)。
此時,通過kill -9命令將其中一個provider關掉。此時,再通過consumer進行請求,會發現成功一次,失敗一次,這樣交替出現。
解決方案
我們通過Spring Cloud提供的LoadBalancerProperties配置類中定義的配置項來對重試機制進行配置,詳細的配置項目可以對照該類的屬性。
在consumer的application配置中添加retry相關配置:
- spring:
- application:
- name: user-service-consumer
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- loadbalancer:
- retry:
- # 開啟重試
- enabled: true
- # 同一實例最大嘗試次數
- max-retries-on-same-service-instance: 1
- # 其他實例最大嘗試次數
- max-retries-on-next-service-instance: 2
- # 所有操作開啟重試(慎重使用,特別是POST提交,冪等性保障)
- retry-on-all-operations: true
上述配置中默認retry是開啟的。
max-retries-on-same-service-instance指的是當前實例嘗試的次數,包括第一次請求,這里配置為1,也就是第一次請求失敗就轉移到其他實例了。當然也可以配置大于1的數值,這樣還會在當前實例再嘗試一下。
max-retries-on-next-service-instance配置的轉移請求其他實例時最大嘗試次數。
retry-on-all-operations默認為false,也就是說只支持Get請求的重試。這里設置為true支持所有的重試。既然涉及到重試,就需要保證好業務的冪等性。
當進行上述配置之后,再次演示異常模擬,會發現即使服務掛掉,在Nacos中還存在,依舊可以正常進行業務處理。
關于Ribbon或其他同類組件也有類似的解決方案,大家可以相應調研一下。
解決方案的坑
在使用Spring Cloud LoadBalancer時其實有一個坑,你可能會遇到上述配置不生效的情況。這是為什么呢?
其實是因為依賴引入的問題,Spring Cloud LoadBalancer的重試機制是基于spring-retry的,如果沒有引入對應的依賴,便會導致配置無法生效。而官方文檔業務未給出說明。
- <dependency>
- <groupId>org.springframework.retry</groupId>
- <artifactId>spring-retry</artifactId>
- </dependency>
另外,上述實例是基于Spring Cloud 2020.0.0版本,其他版本可能有不同的配置。
小結
在使用微服務的時候并不是將Spring Cloud的組件集成進去就完事了。這篇文章我們可以看到即便集成了Nacos,還會因為心跳機制來進行一些折中處理,比如調整心跳頻次。
同時,即便調整了心跳參數,還需要利用其它組件來兼顧請求異常時的重試和防止系統雪崩的發生。關注一下吧,持續更新微服務系列實戰內容。