如何理解從前端nginx到后端微服務高可用架構問題,下面從nginx
、gateway
、nacos
、各個服務節點
的角度講解下應該如何進行高可用,比如nginx是前端向后端進行的負載均衡,也相當于均衡地向各個gateway網關進行請求,再由gateway網關拉取注冊在nacos的服務,均衡地分發請求。
下面我將從前端到后端,逐一拆解每個組件在高可用架構中的角色和協作方式。
一個更完整的高可用架構如下圖所示:
1. 前端 (Frontend) -> Nginx (負載均衡器)
Nginx 在這里的作用是反向代理和負載均衡。
高可用實踐
Nginx本身高可用:單臺 Nginx 是單點,所以需要主從備份。通常使用 Keepalived 方案,為主從兩臺 Nginx 分配一個虛擬IP (VIP)。客戶端(瀏覽器、App)只訪問這個VIP。當
主Nginx
宕機時,Keepalived會自動將VIP漂移到從Nginx
,實現無縫切換。更現代的方案是直接使用云廠商的負載均衡器(SLB/CLB/ALB),它們自身就是高可用的。
作用: 將前端所有請求均勻地(如輪詢、加權、最少連接等策略)分發到后端的多個 Gateway 實例上。如果某個Gateway實例宕機,Nginx會自動將其從健康檢查中剔除,不再轉發流量。
2. Nginx -> Spring Cloud Gateway (API 網關)
Gateway 是統一的流量入口,負責路由、鑒權、限流、熔斷等。
高可用實踐
Gateway多實例部署:你需要啟動多個完全相同的 Gateway 實例(比如在K8s上部署3個Pod,或在多臺服務器上部署3個Jar包)。
無狀態化: Gateway 實例本身不能保存狀態(如Session)。所有實例都是平等的,任何一臺處理請求的結果都一樣。這是水平擴展的前提。
服務注冊: 所有這些Gateway實例都會把自己注冊到Nacos集群(注意:是集群,不是單點)上,服務名通常是 gateway-service。
3. Gateway & 服務節點 -> Nacos (服務發現中心)
高可用實踐
Nacos集群部署: Nacos 本身必須以集群模式(通常至少3個節點) 部署,共享同一個高可用的MySQL數據庫(或其它持久化存儲)。這樣,任何一個Nacos節點掛掉,其他節點依然能提供服務,數據也不會丟失。
客戶端配置: Gateway 和所有微服務節點在配置Nacos地址時,不能只配一個,必須配置整個Nacos集群的地址列表。
# 在Gateway和所有微服務的application.yml中
spring:cloud:nacos:discovery:server-addr: 192.168.1.101:8848,192.168.1.102:8848,192.168.1.103:8848config:server-addr: 192.168.1.101:8848,192.168.1.102:8848,192.168.1.103:8848
工作原理:
客戶端(Gateway或服務)啟動時,會隨機或按順序嘗試連接這個列表中的某個Nacos節點。
一旦連接成功,客戶端會與Nacos集群建立心跳和長連接,進行服務注冊、訂閱和配置拉取。
Nacos集群內部通過Raft協議保證數據一致性。任何一個節點收到的注冊信息,都會同步到其他節點。
因此,Gateway 無論連接到 Nacos 集群中的哪一個節點,都能獲取到全量的、一致的服務列表。它據此來做本地負載均衡(默認是RoundRobin輪詢)。
4. Gateway -> 各個服務節點 (Service Instances)
Gateway 從 Nacos 獲取到健康的服務實例列表后,會根據預設的路由規則,將請求轉發到具體的某個服務實例上(如 user-service 的 192.168.1.201:8080)。
高可用實踐
服務多實例部署:這是最基本的要求。每個微服務(如 user-service, order-service)都必須有多個實例部署在不同的服務器或容器中。
健康檢查:Nacos 客戶端會定期向Server發送心跳,Nacos Server也會主動檢查客戶端健康狀態。如果某個服務實例宕機,Nacos會在很短時間內(秒級)將其標記為不健康并從服務列表中剔除。Gateway 會定時從Nacos拉取最新的服務列表,因此它不會再把請求發往已經宕機的實例。
客戶端負載均衡:Gateway 在轉發時,會使用 LoadBalancerClient(如Spring Cloud LoadBalancer)在本地服務列表中進行負載均衡(如輪詢),而不是每次都請求Nacos。這大大提高了性能并降低了Nacos的壓力。
5. 各個服務節點 -> 下游依賴 (數據庫、緩存等)
高可用實踐
微服務本身高可用了嗎?還不夠。如果它們依賴的數據庫、Redis、MQ等是單點的,那整個系統依然脆弱。
數據層高可用:必須使用數據庫主從復制、讀寫分離集群(如MySQL MGR、Redis Sentinel/Cluster、RabbitMQ鏡像隊列等)。這樣即使一個數據庫節點宕機,整個系統仍可提供只讀服務或通過切換繼續服務。
總結與梳理:一次完整的請求流
-
用戶 訪問 api.yourcompany.com (VIP)。
-
Nginx (SLB) 接收到請求,根據負載均衡算法,將其轉發到一臺健康的 Gateway 實例上。
-
Gateway 接收到請求,解析路徑,確定要訪問的服務(如 /user/api/1 -> user-service)。
-
Gateway 查看其本地的服務列表(這個列表是它從連接的Nacos集群節點那里定時拉取并緩存的),通過負載均衡器選出一個健康的 user-service 實例地址(如 192.168.1.201:8080)。
-
Gateway 將請求轉發給該 user-service 實例。
-
user-service 實例處理請求,期間可能需要調用其他服務(通過Nacos發現)或訪問高可用的數據庫集群。
-
處理完成后,響應結果原路返回給用戶。
高可用就是在你這個思路的每一個環節上都去掉“單點”,通過多實例+負載均衡+健康檢查的方式,讓整個鏈條上的任何一個環節宕機都不會導致全局故障。
問題一
如果nginx負載均衡給了gateway,gateway也負載均衡給了各個服務節點,請求是如何分發的?如:
nginx->g1->s1(第一次),nginx->g2->s2(第二次),nginx->g3->s3(第三次)
Gateway 在轉發請求時,默認的負載均衡策略是輪詢(RoundRobin),并且它的負載均衡對象是它所連接的所有健康實例。
詳細流程分解
我們假設所有組件都已啟動并注冊完畢,狀態健康。
- 初始狀態:服務注冊
-
三個 user-service 實例 s1, s2, s3 啟動,并向 Nacos 集群 注冊自己。
-
三個 gateway 實例 g1, g2, g3 啟動,也向 Nacos 集群 注冊自己,并從 Nacos 獲取到了 user-service 的完整實例列表 [s1, s2, s3]。
- 請求流程
-
第一次請求:
-
用戶請求到達 Nginx。
-
Nginx 根據其負載均衡策略(比如默認的輪詢),從 [g1, g2, g3] 中選擇了 g1,將請求轉發給它。
-
g1 接收到請求,解析后發現需要路由到 user-service。
-
g1 查看自己本地緩存的、從Nacos獲取的服務列表 [s1, s2, s3]。
-
g1 使用其內置的負載均衡器(默認輪詢),從列表中選擇第一個實例,比如 s1,將請求轉發給 s1。
-
路徑:Nginx -> g1 -> s1
-
-
第二次請求:
-
用戶的下一個請求到達 Nginx。
-
Nginx 繼續輪詢,這次從 [g1, g2, g3] 中選擇了 g2,將請求轉發給它。
-
g2 接收到請求,同樣需要路由到 user-service。
-
g2 也擁有完整的服務列表 [s1, s2, s3]。
-
g2 的負載均衡器獨立工作,它也會使用輪詢策略。但關鍵點在于:g2 的負載均衡器有自己的輪詢計數器,它與 g1 的計數器是獨立的、不共享的。
-
因此,g2 的負載均衡器也會從 [s1, s2, s3] 中選擇第一個實例,即 s1。
-
路徑:Nginx -> g2 -> s1
-
-
第三次請求:
-
Nginx 輪詢到 g3。
-
g3 獨立地輪詢,選擇 s1。
-
路徑:Nginx -> g3 -> s1
-
-
第四次請求:
-
Nginx 又輪詢回 g1。
-
此時,g1 的負載均衡器會選擇列表中的下一個實例,即 s2。
-
路徑:Nginx -> g1 -> s2
-
以此類推。
這是一個兩級負載均衡系統。
第一級(Nginx):在 Gateway 實例 之間進行負載均衡。
第二級(Spring Cloud Gateway):在每個 Gateway 實例內部,針對目標服務實例進行負載均衡。
負載均衡器獨立性:每個 Gateway 實例內部的負載均衡器都是獨立工作的,它們之間不會同步“上次選到了哪個實例”這種狀態信息。因為它們的設計是無狀態的。
最終分布:從全局來看,如果所有組件都使用默認的輪詢策略,并且請求量足夠大,那么請求會均勻地分布到所有的服務實例(s1, s2, s3) 上。雖然某幾個連續的請求可能碰巧都落到了 s1 上,但長期來看是均衡的。
唯一的偏差是忽略了每個Gateway內部的負載均衡器是獨立工作的,導致最初的幾個請求可能不會像你想象的那樣完美地輪詢到不同的服務實例。
但在大規模并發下,這種獨立性和無狀態設計正是保證系統可擴展性和高可用的關鍵。
問題二
如果Gateway 實例本身不能保存狀態(如Session),那gateway是怎么能做到負載均衡的
這觸及了無狀態服務和負載均衡的核心機制。不過既然是無狀態的,它怎么“記住”該輪詢到哪個節點呢?
關鍵在于:負載均衡不需要“記住”長期狀態,它只需要一個非常簡單的、短暫的“指針”即可。而這個指針(通常只是一個原子整數)是保存在每個Gateway實例的內存中的,不需要共享。
下面來詳細解釋幾種常見的負載均衡策略是如何在無狀態的Gateway中工作的:
1. 輪詢 (Round Robin) - 默認策略
這是最常用的策略。
工作原理: 每個Gateway實例內部都維護一個本地原子計數器 (Atomic Integer),初始值為0。
過程:
-
當請求到達Gateway實例(例如g1)時,它要決定轉發到哪個服務實例(例如user-service的s1, s2, s3)。
-
g1的負載均衡器會從本地緩存的服務列表[s1, s2, s3]中,根據當前計數器的值取模運算來選擇實例:index = counter++ % instanceList.size()。
-
假設計數器當前是0:0 % 3 = 0 -> 選擇s1。
-
下一個請求到來,計數器變為1:1 % 3 = 1 -> 選擇s2。
-
再下一個,計數器變為2:2 % 3 = 2 -> 選擇s3。
-
再下一個,計數器變為3:3 % 3 = 0 -> 又選擇s1。
-
為什么這是“無狀態”的?
- 這個計數器只存在于g1實例的內存中。它不關心其他Gateway實例(g2, g3)的計數器是多少。
- 它不關心之前是誰處理了哪個用戶的請求。每一個新請求對它來說都是獨立的,它只是簡單地根據本地計數器+1,然后選擇。
- 如果g1實例重啟,計數器重置為0,負載均衡會從頭開始,但這不影響系統的正確性,只是打破了均勻性,很快又會恢復。
這就解釋了之前的例子:為什么連續兩個請求nginx->g1->s1和nginx->g2->s1,兩個Gateway都選擇了s1。因為g1和g2的計數器是獨立的,并且都從0開始。
2. 隨機 (Random)
工作原理: 更簡單。每次有請求需要轉發時,直接在健康的服務實例列表中隨機選擇一個。
無狀態性: 完全不需要任何記錄。每次選擇都是一個獨立事件。在大流量下,結果也是均勻分布的。
3. 最少連接數 (Least Connections) - 需要輕微“狀態”
這個策略需要知道每個服務實例當前正在處理的連接數,它似乎需要“狀態”。
工作原理: 選擇當前處理連接數最少的服務實例。
無狀態性: 這里的“狀態”是瞬時狀態,而不是會話狀態。
Gateway會通過心跳或定時拉取從注冊中心(如Nacos)獲取每個服務實例的當前健康狀態和元數據(其中可以包含近似的連接數信息)。
或者,Gateway的負載均衡器自己內部為一個服務實例列表維護一個非常簡單的、本地的連接數計數器(例如,每轉發一個請求給s1,就給s1的計數器+1,收到響應后就-1)。
這個“狀態”仍然是本地的、短暫的。它存在于單個Gateway實例的內存中,用于做下一次選擇的決策。如果這個Gateway實例宕機,這個狀態就消失了,但無傷大雅,新上線的實例會重新開始計數。它不需要被持久化,也不需要在不同Gateway實例間共享。
問題三
如果Gateway 在轉發時,會使用 LoadBalancerClient在本地服務列表中進行負載均衡(如輪詢),而不是每次都請求Nacos。那么當服務A某個實例A1宕機了,gateway怎么知道這個實例A1宕機了呢
這是一個非常關鍵的問題,它直接關系到微服務架構的可靠性和實時性。Gateway 并不是神機妙算,它能知道 A1 宕機,依賴于一套由 Nacos 和 客戶端 共同協作的服務健康檢查機制。
整個過程可以概括為:“Nacos 負責發現和通知,Gateway 負責更新本地列表”。
下面是詳細的機制分解:
服務健康檢查 (Health Check)
Nacos 通過兩種主要方式來探測服務實例的健康狀態:
客戶端心跳上報 (Client Beat) - 主要方式
工作原理: 當服務實例 A1、A2、A3 啟動并成功注冊到 Nacos 后,它們會定期(例如每5秒) 向 Nacos Server 發送一個心跳包,說:“我還活著!”
故障判斷: 如果 Nacos Server 在一定時間(例如15秒) 內沒有收到來自 A1 的心跳,它就會將 A1 實例的狀態標記為“不健康”(healthy = false)。如果再經過一段時間(例如30秒)還沒收到心跳,Nacos 會直接將這個實例從注冊列表中剔除。
服務端主動探測 (Server Probe) - 補充方式
工作原理: Nacos Server 也可以主動發起檢查。例如,它可能會嘗試調用服務實例 A1 的一個健康檢查接口(如 /actuator/health)。
故障判斷: 如果連續幾次調用失敗或超時,Nacos Server 就會判定該實例不健康。
第一種方式是默認且最常用的。
Gateway 如何獲取到最新的服務列表?
現在,Nacos 已經知道 A1 宕機了,但它需要把這個消息告訴所有關心 service-A 的消費者,也就是 Gateway。這個過程是通過 “訂閱-推送”機制 實現的。
訂閱 (Subscribe):
當 Gateway(g1, g2, g3)啟動時,它向 Nacos 注冊自己的同時,也會訂閱它所關心的服務(如 service-A)的變更。
Gateway 與 Nacos Server 之間維護著一個長連接。
推送 (Push) / 拉取 (Pull):
理想模式(推送): 當 Nacos 檢測到 service-A 的實例列表發生變化(如 A1 被剔除),它會立即通過長連接主動推送這個變更事件給所有訂閱了 service-A 的 Gateway 實例。
兜底模式(拉取): 同時,Gateway 客戶端也會定時(例如每10秒) 主動向 Nacos 拉取一次最新的服務列表,作為一個補償和備份機制,防止推送消息丟失。
更新本地緩存:
Gateway 在收到推送消息或拉取到新列表后,會立即更新其內存中緩存的 service-A 的服務實例列表。舊的列表 [A1, A2, A3] 會被更新為 [A2, A3]。
此后,Gateway 的 LoadBalancerClient 在進行負載均衡選擇時,只會從健康的、最新的列表 [A2, A3] 中進行選擇。 它再也“看不見”已經宕機的 A1 了,因此絕對不會再把請求轉發給它。
所以,Gateway 能知道實例宕機,并不是因為它自己能探測到,而是因為它信任并依賴于 Nacos 這個“服務大腦”來告訴它誰才是健康的。 這種設計解耦了各個組件的職責,使得系統更加清晰和可擴展。
延遲說明: 這個過程不是瞬時的,會有幾秒到幾十秒的延遲(默認配置下通常在15-30秒)。這是為了權衡網絡抖動和實時性做出的設計。對于絕大多數業務場景,這個延遲是可接受的。如果需要更高的敏感性,可以調小心跳和超時的間隔,但這會增加Nacos的壓力和網絡開銷。
問題四
那么gateway是怎么訂閱nacos的服務的
Gateway(或者說其底層的Spring Cloud LoadBalancer)是通過 “事件驅動” 和 “長連接推送” 相結合的方式來訂閱Nacos服務的。
整個過程的核心是 NacosServiceDiscovery 和 NacosWatch 這兩個組件。
詳細的訂閱與通知流程
1. 啟動時:初始拉取與訂閱
當你的Gateway應用啟動時,發生了以下事情:
注冊與發現客戶端初始化:
由于你引入了 spring-cloud-starter-alibaba-nacos-discovery
依賴,Spring Cloud 會自動配置 NacosServiceDiscovery 客戶端。
這個客戶端會讀取配置文件中 spring.cloud.nacos.discovery.server-addr
的地址,與Nacos服務器集群建立連接。
獲取初始服務列表:
Gateway在需要路由到某個服務(例如 user-service)之前,NacosServiceDiscovery 會先調用 getInstances(serviceId) 方法。
該方法會直接向Nacos服務器發起一次HTTP查詢請求,獲取到 user-service 所有健康的實例列表(例如 [A1, A2, A3]),并緩存在Gateway的本地內存中。
發起訂閱(Subscribe):
在獲取到初始列表后,NacosWatch 組件會代表Gateway向Nacos服務器發起一個訂閱。
這個訂閱本質上是一個“監聽”請求,它告訴Nacos服務器:“我是Gateway,我關心 user-service 這個服務,如果它的實例列表有任何變化(增、刪、改),請立刻通知我。”
為了實現實時通知,Gateway會和Nacos服務器建立一個UDP或HTTP長連接(具體取決于Nacos的版本和配置),作為推送數據的通道。
2. 運行時:事件推送與本地更新
當Nacos注冊中心的服務列表發生變化時(例如實例A1宕機):
Nacos檢測到變化:Nacos服務器通過心跳超時機制,發現 user-service 的實例A1宕機,并將其從注冊列表中剔除。
Nacos主動推送事件:由于Gateway之前已經訂閱了 user-service,Nacos服務器會立刻通過之前建立的長連接通道,向Gateway推送一個事件通知。這個通知非常簡單,只包含基本的服務名等元數據,內容是:“注意!user-service 的服務列表變了!”。
Gateway觸發更新回調:Gateway端的Nacos客戶端收到這個推送事件后,會觸發一個事先注冊好的回調函數(Callback)。
拉取最新列表并更新緩存:
這個回調函數不會直接相信推送消息里的具體內容,而是會再次調用 NacosServiceDiscovery.getInstances(serviceId) 方法。
該方法會再次查詢Nacos服務器,獲取到 user-service 最新的、完整的實例列表(現在是 [A2, A3])。
最終,這個全新的列表會覆蓋掉Gateway本地內存中緩存的舊列表。
負載均衡器生效:此后,所有新進來的請求,其負載均衡(如LoadBalancerClient)都會基于這個最新的、正確的列表 [A2, A3] 來進行,從而避免了將請求發往已經宕機的A1實例。
為什么是“推送+拉取”模式?
你可能會問,為什么Nacos不直接把最新的列表推過來,而是要讓Gateway再拉取一次?
這是一種非常經典的設計,權衡了可靠性和數據量:
可靠性: 推送消息可能因為網絡問題而丟失。如果只依賴推送,Gateway可能會漏掉變更。而讓客戶端在收到推送后主動拉取一次,可以確保最終拿到的一定是最準確的數據。
數據量: 服務列表可能很大。直接推送整個實例列表數據包會很大,占用網絡帶寬。而只推送一個簡單的“有變化”的通知,讓客戶端自己去拉取,網絡效率更高。
代碼層面如何體現?
雖然你看不到完整的流程,但在你的Gateway項目中,最相關的配置和代碼邏輯是:
配置中心:你的 application.yml 中配置了Nacos服務器地址,這是一切的基礎。
spring:cloud:nacos:discovery:server-addr: localhost:8848
路由配置:你在配置路由時指定的 uri: lb://user-service,其中的 lb 協議就是觸發上述所有流程的開關。它告訴Gateway這個路由需要用到負載均衡,需要去發現服務名為 user-service 的實例。