SpringCloud 服務的平滑上下線

  • 2019 年 10 月 6 日
  • 筆記

吐槽

以前都是手擼 RPC,最近接觸 SpringCloud,深感痛心。主要有以下幾點: 1)代碼量巨大,找 BUG 時間長,超級複雜的設計 2)版本管理混亂,經常出現莫名其妙的配置錯誤(所以 2.0 是打死不敢上生產啊) 3)Netflix 公司的有些代碼,實在是讓人費解,根本就不考慮擴展性 4)生態鏈龐大,學習成本大

建議準備上微服務的同學,固定下一個版本,不要隨意更新或降級。拿 tomcat 的 basedir來說, 1.5.81.5.131.5.16版本是換來換去,不小心點會出事故的。

server:    port: 21004    context-path: /    tomcat:      basedir: file:.

如上, basedir先是從 .換到 file:.,又從 file:.換成 .,連兼容代碼都木有。有木有想打死工程師?

前言

今天主要談的話題,是 平滑的上下線功能。所謂平滑,指的是發版無感知,不至於等到夜深人靜的時候偷偷去搞。某些請求時間可以長點,但不能失敗,尤其是對支付來說,想花錢花不出去是很讓人苦惱的;花了錢買不到東西是很讓人惱火的。整體來說,SpringCloud 功能齊全,經過一段時間的踩坑後使用起來還是非常舒服的。

我們的微服務,大體集成了以下內容。

嗯,一個龐大的生態

問題

那麼問題來了,SpringCloud 到註冊中心的註冊是通過 Rest接口調用的。它不能像 ZooKeeper那樣,有問題節點反饋及時生效。也不能像 Redis那麼快的去輪訓,太嬌貴怕輪壞了。如下圖:

有三個要求:

1)ServiceA 下線一台實例後,Zuul 網關的調用不能失敗 2)ServiceB 下線一台實例後,ServiceA 的 Feign 調用不能失敗 3)服務上線下線,Eureka 服務能夠快速感知

說白了就一件事,怎樣盡量縮短服務下線後 Zuul 和其他被依賴服務的發現時間,並在這段時間內保證請求不失敗。

解決時間問題

影響因子

1) Eureka 的兩層緩存問題 (這是什麼鬼

EurekaServer 默認有兩個緩存,一個是 ReadWriteMap,另一個是 ReadOnlyMap。有服務提供者註冊服務或者維持心跳時時,會修改 ReadWriteMap。當有服務調用者查詢服務實例列表時,默認會從 ReadOnlyMap 讀取(這個在原生 Eureka 可以配置,SpringCloud Eureka 中不能配置,一定會啟用 ReadOnlyMap 讀取),這樣可以減少 ReadWriteMap 讀寫鎖的爭用,增大吞吐量。EurekaServer 定時把數據從 ReadWriteMap 更新到 ReadOnlyMap 中

2) 心跳時間

服務提供者註冊服務後,會定時心跳。這個根據服務提供者的 Eureka 配置中的服務刷新時間決定。還有個配置是服務過期時間,這個配置在服務提供者配置但是在 EurekaServer 使用了,但是默認配置 EurekaServer 不會啟用這個字段。需要配置好 EurekaServer 的掃描失效時間,才會啟用 EurekaServer 的主動失效機制。在這個機制啟用下:每個服務提供者會發送自己服務過期時間上去,EurekaServer 會定時檢查每個服務過期時間和上次心跳時間,如果在過期時間內沒有收到過任何一次心跳,同時沒有處於保護模式下,則會將這個實例從 ReadWriteMap 中去掉

3)調用者服務從 Eureka 拉列表的輪訓間隔

4) Ribbon 緩存

解決方式

1) 禁用 Eureka 的 ReadOnlyMap 緩存 (Eureka 端)

eureka.server.use-read-only-response-cache: false

2) 啟用主動失效,並且每次主動失效檢測間隔為 3s (Eureka 端)

eureka.server.eviction-interval-timer-in-ms: 3000

eureka.server.responseCacheUpdateInvervalMseureka.server.responseCacheAutoExpirationInSeconds在啟用了主動失效後其實沒什麼用了。默認的 180s 真夠把人給急瘋的。

3) 服務過期時間 (服務提供方)

eureka.instance.lease-expiration-duration-in-seconds: 15

超過這個時間沒有接收到心跳 EurekaServer 就會將這個實例剔除。EurekaServer 一定要設置 eureka.server.eviction-interval-timer-in-ms 否則這個配置無效,這個配置一般為服務刷新時間配置的三倍。默認 90s!

4) 服務刷新時間配置,每隔這個時間會主動心跳一次 (服務提供方)

eureka.instance.lease-renewal-interval-in-seconds: 5

默認 30s

5) 拉服務列表時間間隔 (客戶端)

eureka.client.registryFetchIntervalSeconds: 5

默認 30s

6) ribbon 刷新時間 (客戶端)

ribbon.ServerListRefreshInterval: 5000

ribbon 竟然也有緩存,默認 30s

這些超時時間相互影響,竟然三個地方都需要配置,一不小心就會出現服務不下線,服務不上線的囧境。不得不說 SpringCloud 的這套默認參數簡直就是在搞笑。

重試

那麼一台服務器下線,最長的不可用時間是多少呢?(即請求會落到下線的服務器上,請求失敗)。趕的巧的話,這個基本時間就是 eureka.client.registryFetchIntervalSeconds+ribbon.ServerListRefreshInterval, 大約是 8秒的時間。如果算上服務端主動失效的時間,這個時間會增加到 11秒

如果你只有兩個實例,極端情況下服務上線的發現時間也需要 11 秒,那就是 22 秒的時間。

理想情況下,在這 11 秒之間,請求是失敗的。假如你的 QPS 是 1000,部署了四個節點,那麼在 11 秒中失敗的請求數量會是 1000 / 4 * 11 = 2750,這是不可接受的。所以我們要引入重試機制。

SpringCloud 引入重試還是比較簡單的。但不是配置一下就可以的,既然用了重試,那麼就還需要控制超時。可以按照以下的步驟: 1) 引入 pom (千萬別忘了哦)

<dependency>      <groupId>org.springframework.retry</groupId>      <artifactId>spring-retry</artifactId>  </dependency>

2) 加入配置

ribbon.OkToRetryOnAllOperations:true  #(是否所有操作都重試,若false則僅get請求重試)  ribbon.MaxAutoRetriesNextServer:3  #(重試負載均衡其他實例最大重試次數,不含首次實例)  ribbon.MaxAutoRetries:1  #(同一實例最大重試次數,不含首次調用)  ribbon.ReadTimeout:30000  ribbon.ConnectTimeout:3000  ribbon.retryableStatusCodes:404,500,503  #(那些狀態進行重試)  spring.cloud.loadbalancer.retry.enable:true  # (重試開關)

發佈系統

OK, 機制已經解釋清楚,但是實踐起來還是很繁雜的,讓人焦躁。比如有一個服務有兩個實例,我要一台一台的去發佈,在發佈第二台之前,起碼要等上 11 秒。如果手速太快,那就是災難。所以一個配套的發佈系統是必要的。

首先可以通過 rest 請求去請求 Eureka,主動去隔離一台實例,多了這一步,可以減少至少 3 秒服務不可用的時間(還是比較划算的)。

然後通過打包工具打包,推包。依次上線替換。

市面上沒有這樣的持續集成工具,那麼發佈系統就需要定製,這也是一部分工作量。

到此,僅僅是解決了 SpringCloud 微服務平滑上下線的功能,至於灰度,又是另外一個話題了。有條件的公司選擇自研還是很明智的,不至於將功能拉低到如此的水平。

不過大體不用擔心,你的公司能不能活下去,還是一個未知數。Netflix 都忍了,在座的各位能比它強大么?

SpringCloud的存在是視覺污染,而你我的存在是星光點點。

可我的錢包,還是空空的。

要不要幫幫忙?