
做微服務這段時間被問得最多的問題之一就是服務之間到底是怎么找到彼此的IP 寫死不就行了嘛短期可以服務一多、實例一變、一擴縮容馬上就會亂成一鍋粥。這也是為什么現在只要聊到微服務架構服務發現絕對是繞不開的一環。我這邊用的是 Consul 來做的服務注冊與發現從集群搭建到 Spring Cloud 集成完整跑了一遍期間踩了不少坑也把原理層面的東西摸了個七七八八。這篇就把我的實操過程和經驗整理出來給正準備做服務發現、或者正在 Eureka、Nacos、Consul 之間糾結的團隊一個參考。這篇文章會覆蓋這么幾個部分為什么需要服務發現、Consul 的核心原理與數據模型、單機和集群怎么搭、服務注冊和健康檢查怎么做、Spring Cloud 怎么集成最后還有我實際運維中遇到的高頻問題和排查思路。無論你是剛拆微服務的新手還是已經在維護注冊中心的開發這篇文章里應該都有能直接拿去用的東西。1. 為什么微服務離不開服務發現1.1 沒有注冊中心時服務調用有多痛苦先回到最原始的場景。假設你有一個訂單服務和一個用戶服務訂單服務要調用戶服務的接口。最笨的辦法就是配置里寫死用戶服務的 IP 和端口比如http://192.168.1.10:8080。剛開始實例少確實夠用。但一旦用戶服務部署了 3 個節點或者某臺機器掛了要縮容麻煩就來了你得手動改配置、改 Nginx 上游、重新 reload而且掛掉的那個節點 Nginx 并不知道照樣往里轉發流量線上就開始零星報錯。我見過不少團隊在這個階段用 Nginx 做反向代理把服務地址統一收斂到 Nginx然后業務代碼只調 Nginx。這比寫死 IP 好一些但本質上是把手動改配置從業務代碼轉移到了 Nginx 配置實例變動的通知依然靠人肉。一旦微服務規模上了兩位數每次發布、擴容、故障轉移都要改一遍 Nginx運維壓力非常大而且極易出錯。這不是工具不好用而是思路錯了。Nginx 適合做流量入口的負載均衡不適合做服務間動態調用的注冊表。服務之間的調用關系是動態的實例隨時在變必須有一個組件能實時記錄當前有哪些服務、各自在哪個地址、是否健康并且把這個信息自動同步給所有調用方。1.2 服務發現幫我們解決了哪三件事服務發現解決的核心問題其實就三件事。第一件是服務注冊。服務啟動時自動把自己的 IP、端口、服務名、元數據信息上報給注冊中心。第二件是健康檢查。注冊中心定期探測服務的存活狀態發現實例不健康就自動標記、摘除不再把流量分給它。第三件是服務發現與負載均衡。調用方在發起請求前先向注冊中心拿一份可用實例列表然后按照負載均衡策略挑一個發起調用。可以用一個生活化的類比來理解。你去一家熱門餐廳吃飯門口有個等位取號系統。你到了之后先取號這就是注冊系統會不斷喊號沒人應答的就跳過這就是健康檢查輪到你的號時服務員帶你去空桌這就是發現與分配。如果沒有這個取號系統你就得挨個桌子問有沒有空位效率極低而且很多桌子已經坐滿了人你卻不知道。現在主流的注冊中心方案有 Consul、Nacos、Eureka、ZooKeeper 等。Eureka 2.x 已經停止維護ZooKeeper 更偏向分布式協調場景。Nacos 在國內用得多功能也很全自帶配置中心和注冊中心。我選擇 Consul 主要看中它的多數據中心支持、一致性協議更成熟、以及和 Spring Cloud 的集成度很高后面我會詳細講。2. Consul 服務發現的核心原理2.1 先認識 Consul 里的角色與端口Consul 是 HashiCorp 家的產品核心由 Agent 組成。Agent 有兩種運行模式Server 模式和 Client 模式。Server 節點負責維護集群狀態、處理查詢和寫入請求、參與 Raft 一致性協議選舉是 Consul 集群的大腦。生產環境一般部署 3 個或 5 個 Server 節點必須是奇數因為 Raft 協議要求多數派才能提交數據。Client 模式則是一個輕量代理部署在每臺業務機器上負責轉發請求給 Server、執行健康檢查、維護本機的服務注冊信息。業務進程不直接和 Server 集群通信而是先找本機 Client再由 Client 轉發這是一個很典型的分層設計。Consul 用到了幾個端口我用一張表整理了一下方便排查問題的時候對照端口協議用途8500HTTP提供 REST API 和 Web UI服務注冊、查詢都走這里8600TCP/UDPDNS 接口可以通過域名解析服務地址8300TCPServer 節點之間的 RPC 通信8301TCP/UDP同數據中心內 Agent 間 gossip 通信LAN8302TCP/UDP跨數據中心 Agent 間 gossip 通信WAN我剛開始部署的時候沒注意端口問題結果集群起來之后成員之間一直互相看不到排查了半天才發現是防火墻把 8301 端口給攔了。如果你也遇到 Agent 日志里反復出現 join 失敗優先檢查這幾個端口是否放通。2.2 服務注冊與查詢的數據模型Consul 里最核心的數據模型是 Service。一個服務實例用下面幾個關鍵字段描述ID實例的唯一標識同一個服務下不能重復Name服務名邏輯上的服務名稱Tags標簽可以用來區分版本、環境等Address 和 Port實例的訪問地址和端口Check健康檢查配置這里有個容易混淆的點Consul 的服務查詢接口有兩套/v1/catalog/service/{name}和/v1/health/service/{name}。前者返回的是注冊表里的原始數據不管實例是否健康都會返回后者只返回通過健康檢查的實例。實際調用的時候一定要用/v1/health/service/{name}否則你把流量打到一個已經掛掉的實例上故障排查會非常痛苦。我自己在項目里就遇到過這樣的問題服務調用的下游實例已經宕機了但調用方還是能拿到它的地址。查了半天發現代碼里用的是 catalog 接口改成 health 接口之后掛掉的實例被自動過濾掉問題立刻消失。這個細節在 Consul 官方文檔里寫得不算醒目但生產環境非常重要。2.3 三種健康檢查方式的選擇邏輯Consul 的健康檢查有三種模式適用場景完全不同很多人一開始容易搞混。第一種是 HTTP 檢查。Consul 定期請求你指定的 HTTP 接口比如/actuator/health根據返回的 HTTP 狀態碼判斷是否健康。只要接口返回 200就認為實例存活。這是我用得最多的一種因為 Spring Boot 的 Actuator 天然提供了健康檢查端點可以直接對接。第二種是 TCP 檢查。Consul 定期嘗試和實例的 IP:Port 建立 TCP 連接連得上就認為健康。適合沒有 HTTP 接口的服務比如數據庫連接、自定義 RPC 服務。第三種是 TTL 檢查。服務實例自己定期主動上報心跳告訴 Consul 我還活著。如果超過指定時間沒有上報就判定為不健康。這種模式下 Consul 不會主動探測適合那些不方便提供 HTTP 端點、或者內部有復雜存活判斷邏輯的服務。選擇邏輯其實很簡單能用 HTTP 檢查就用 HTTP 檢查因為它最直接地反映了服務的真實可用狀態服務本身沒有 HTTP 接口就用 TCP需要服務自己決定是否存活、或者不想讓注冊中心主動探測的場景選 TTL。但要注意TTL 模式依賴業務代碼主動上報心跳一旦業務線程卡死心跳可能還在發實際服務已經不能處理請求了這會造成誤判所以能用 HTTP 檢查的地方我一般不會用 TTL。2.4 Consul 的一致性保證與多數據中心Consul 的 Server 節點采用 Raft 協議保證數據一致性。Raft 是一種分布式一致性算法核心思想是選舉一個 Leader 節點負責處理寫入請求其他節點同步數據。寫入操作必須得到多數派節點確認才算成功所以集群里掛掉的節點不能超過半數否則整個集群會變成只讀狀態服務注冊和更新都會失敗。這個機制保證了數據不會丟但也帶來一個運維常識Consul 集群的 Server 節點數最好是 3 或 5不要因為節省成本只部署 2 個因為 2 個節點掛 1 個就湊不齊多數派了連一臺都不掛反而不如單點穩定。我后面會詳細演示 3 節點集群怎么搭。Consul 還支持多數據中心每個數據中心有獨立的 Server 集群數據中心之間通過 WAN gossip 協議交換服務目錄信息。這一點在做異地多活或跨機房容災時很有價值應用層不需要感知物理機房的差異直接通過服務名就能拿到對端機房的可用實例。如果你的公司暫時沒有多機房需求這個功能可以先了解但選型時多一個加分項總是好的。3. 從零搭建 Consul 集群并完成服務注冊3.1 單機快速體驗開發模式先從最簡單的單機模式開始跑通了再上集群。Consul 的安裝很簡單直接從官網下載二進制包解壓后把可執行文件放到 PATH 里就行。啟動開發模式consul agent -dev-dev模式會啟動一個單節點的 Consul所有功能默認開啟非常適合本地調試。啟動成功后打開瀏覽器訪問http://127.0.0.1:8500就能看到 Consul 的 Web UI。界面上有 Services、Nodes、ACL 等菜單服務注冊進來后在 Services 頁面就能看到實例列表和健康狀態。開發模式下如果提示端口被占用可以用-http-port指定其他端口。我做本地實驗時常用consul agent -dev -http-port18500避開可能被占用的 8500 端口。3.2 3 節點集群搭建實操生產環境我不會用單節點至少搭 3 個 Server 節點的集群。這里演示在 3 臺 Linux 服務器上搭建假設三臺機器的 IP 分別是 10.0.0.11、10.0.0.12、10.0.0.13。每臺機器上先準備一個配置文件consul.hcl內容大致如下data_dir /opt/consul/data log_level INFO server true bootstrap_expect 3 ui true bind_addr 0.0.0.0 client_addr 0.0.0.0 retry_join [10.0.0.11, 10.0.0.12, 10.0.0.13]然后依次在三臺機器上執行consul agent -config-dir/etc/consul.d第一臺啟動的時候因為bootstrap_expect 3Consul 會等待 3 個 Server 節點都加入后才開始選舉 Leader。這個參數的意思是期望的 Server 節點數用于避免過早選舉產生腦裂。等三臺機器全部啟動后在任意一臺執行consul members應該能看到三個節點都是alive狀態。再執行consul operator raft list-peers可以看到有一個節點是 leader其他節點是 followerRaft 集群正常工作了。這里有個經驗生產環境的 Server 節點最好是奇數3 個或 5 個。原因在 Raft 協議里說過了要湊多數派。另外如果集群規模很大或者請求量很高還可以給 Server 節點前加一層負載均衡但一般的微服務規模用不到業務請求會優先打到本機的 Client Agent再轉發給 Server壓力可控。3.3 通過 REST API 注冊、查詢、注銷服務Consul 提供了完整的 REST API我先用 curl 演示最基礎的服務注冊流程。注冊一個名為user-service的服務實例到 Consulcurl -X PUT http://127.0.0.1:8500/v1/agent/service/register \ -H Content-Type: application/json \ -d { ID: user-service-1, Name: user-service, Tags: [primary], Address: 192.168.1.100, Port: 8080, Check: { HTTP: http://192.168.1.100:8080/actuator/health, Interval: 10s } }這里注意兩個細節。第一注冊接口是/v1/agent/service/register走的是本機 Agent。第二Check 里的 HTTP 地址要填業務實例的地址不是 Consul 的地址Consul 會主動去探測這個接口。注冊成功后在瀏覽器 UI 里能看到這個服務。查詢可用實例用 health 接口curl http://127.0.0.1:8500/v1/health/service/user-service返回結果里每個實例會帶一個Checks數組里面Status為passing的才是健康實例。服務下線時要調用注銷接口curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/user-service-1這個接口是 Agent 級別的只注銷本機 Agent 上注冊的這個實例。搞清楚 agent 和 catalog 兩套 API 的區別能避免很多誤操作。4. Spring Cloud 集成 Consul服務注冊與調用4.1 服務提供者注冊到 Consul手動用 curl 注冊服務只是為了理解原理真實項目里不會這么干都是讓框架自動完成。Spring Cloud 對 Consul 的集成非常成熟我在 Spring Boot 2.x Spring Cloud 2021.0.x 環境下測試過步驟很簡潔。在服務提供者項目里引入依賴dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-consul-discovery/artifactId /dependency然后在application.yml里配置 Consul 地址和注冊信息spring: application: name: user-service cloud: consul: host: 127.0.0.1 port: 8500 discovery: instance-id: ${spring.application.name}-${spring.cloud.client.ip-address}-${server.port} prefer-ip-address: true health-check-path: /actuator/health health-check-interval: 10s主類上加上EnableDiscoveryClient注解SpringBootApplication EnableDiscoveryClient public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }啟動應用后服務會自動注冊到 Consul。這里instance-id的配置非常關鍵如果一臺機器上同一個服務部署了多個實例端口不同那么 ID 里帶上 IP 和端口就能保證唯一否則會出現后面的實例把前面的實例覆蓋掉的情況這是我在多實例部署時踩過的坑。prefer-ip-address: true會讓服務注冊時優先使用 IP 而不是主機名。如果不開這個配置在容器環境或內網 DNS 不完善的環境下注冊到 Consul 的地址可能是主機名其他服務解析不了調用就會失敗。4.2 服務消費者通過 Consul 找到并調用服務服務消費者的配置和服務提供者幾乎一樣只是不注冊自身的情況更多。如果某個服務只是調用別人不需要被別人調用可以在配置里關閉注冊spring: cloud: consul: discovery: register: false調用方式有兩種主流方案。一種是 RestTemplate 加LoadBalanced注解Configuration public class RestTemplateConfig { Bean LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }然后直接通過服務名調用String result restTemplate.getForObject(http://user-service/api/user/1, String.class);另一種是用 OpenFeign聲明式調用更符合微服務的風格FeignClient(name user-service) public interface UserClient { GetMapping(/api/user/{id}) String getUser(PathVariable(id) Long id); }這兩套方式底層的原理是一樣的攔截到服務名后向 Consul 查詢可用實例列表再用負載均衡策略選一個實例發起請求。Spring Cloud LoadBalancer 默認的負載均衡策略是輪詢你也可以根據自己的需求替換成隨機、最少連接數等策略。我第一次用 RestTemplate 調服務名時報了UnknownHostException原因就是忘加LoadBalanced注解。這個注解的作用是給 RestTemplate 注入一個攔截器讓它能識別http://user-service這種服務名格式并走服務發現邏輯。沒有這個注解RestTemplate 只會把它當普通域名去 DNS 解析自然就失敗了。4.3 健康檢查、優雅下線與自動摘除Spring Cloud Consul 默認的健康檢查路徑就是/actuator/health但前提是項目里引入了 Spring Boot Actuator。如果沒引入健康檢查請求會返回 404Consul 會把實例標記為不健康。所以一定要在服務提供者項目里加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependencyhealth-check-interval: 10s表示每 10 秒檢查一次。這個值不要設得太短否則頻率太高會浪費不必要的資源也不要設得太長否則實例掛了之后下游最長要等一個周期才能感知到。10 秒是我覺得比較平衡的配置如果對時效性要求高可以壓到 5 秒。還有一個很實用的配置是deregister-critical-service-after。當健康檢查連續失敗實例進入 critical 狀態后如果超過這個時間還沒有恢復Consul 會自動把實例從注冊表里刪除spring: cloud: consul: discovery: deregister-critical-service-after: 2m這個配置我強烈建議加上否則一個實例掛了之后它的記錄會一直躺在 Consul 服務列表里UI 上看著紅叉一片數據也不干凈。加了這個配置后Consul 會在 2 分鐘后自動清理。優雅下線方面Spring Cloud 在應用關閉時會自動從 Consul 注銷服務實例不需要額外寫代碼。但如果你用的是容器編排系統比如 Kubernetes 或 Docker Compose要注意關閉順序。先讓 Consul 把實例標記為不健康并停止接收新流量再真正銷毀容器這樣才能做到滾動發布無感知。單純依賴進程退出時的注銷邏輯在容器被強殺時往往來不及執行。5. 常見問題與排查技巧實錄5.1 服務列表里看不到服務這個是最常見的問題排查思路從簡單到復雜排開先看 Consul UI 的 Services 頁面確認服務有沒有注冊成功再看服務提供者的啟動日志有沒有報錯然后用curl http://127.0.0.1:8500/v1/agent/services查看本機 Agent 上注冊的服務列表。一個容易被忽略的原因是配置的spring.cloud.consul.host指向了錯誤的機器或者端口不是 8500。另外檢查服務提供者和 Consul 之間的網絡連通性在服務提供者所在機器上直接執行telnet {consul_host} 8500如果端口不通說明網絡層面有問題。踩過的一個隱蔽坑是服務注冊請求確實發出去了但注冊到 Consul 的地址是內網 Docker 網段的 IP比如172.17.0.2其他機器訪問不了。這就是沒有配prefer-ip-address: true或者容器網絡配置不當造成的。解決方法是配置spring: cloud: consul: discovery: prefer-ip-address: true ip-address: 宿主機對外IP # 可選手動指定注冊IP5.2 控制臺健康狀態紅叉但服務本身正常服務進程明明還在跑接口也能通但 Consul UI 里顯示健康檢查失敗。這時候先看 Consul 配置的健康檢查路徑是什么再手動在 Consul 所在機器上 curl 一下這個地址。如果是/actuator/health返回 404說明服務提供者沒引入 Actuator或者 context-path 配置導致路徑不對。Spring Boot 如果設置了server.servlet.context-path/api那么健康檢查端點也會跟著變化Consul 配置里的health-check-path也要改成/api/actuator/health。還有一種情況是健康檢查返回了 200但檢查頻率太高把服務打掛了表現就是服務偶爾可用偶爾不可用。我有一次把 interval 配成了 1 秒結果 Consul 集群對每個實例每秒發一個請求業務高峰期把服務拖得很慢。后來調整為 10 秒一切正常。健康檢查的頻率不是越高越好還是一個平衡問題。5.3 服務實例被自動摘除后反復重連如果實例處于不太健康的狀態Consul 會標記為 critical然后deregister-critical-service-after時間一到就刪除注冊信息。但服務端的 Spring Cloud Consul 組件有自動重連機制會嘗試重新注冊于是在 UI 上看到的現象就是服務一直在注冊、刪除、注冊、刪除之間反復橫跳。這種情況下核心問題是實例本身不穩定可能是內存溢出、數據庫連接池耗盡、或者磁盤滿了。先去查服務日志和健康檢查端點的返回內容Actuator 的/actuator/health返回體里會帶上各組件的健康狀態比如 MySQL 連接、Redis、磁盤空間等能直接指出是哪一個組件出了問題。5.4 集群出現腦裂或不可寫Consul 集群的 Server 節點網絡發生分區時Raft 協議會觸發重新選舉。如果某個分區的節點數湊不齊多數派這個分區就不可寫服務注冊和更新都會失敗。這不是 Consul 的 bug而是 Raft 為防止腦裂寫的固有機制。排查方法是登錄 Server 節點執行consul operator raft list-peers查看 Raft 狀態。如果 leader 一直在切換或者沒有 leader說明節點間網絡不穩定檢查 8300 端口連通性和機房之間的專線質量。另一個常見原因是服務器時鐘偏差太大Raft 對時鐘一致性有要求生產環境務必配置好 NTP 時間同步。5.5 服務下線時沒有及時清空進程被 kill 之后服務實例在 Consul 里還會存在一段時間直到健康檢查連續失敗后才被標記為 critical再等到deregister-critical-service-after觸發才被清理。這是正常現象但如果是主動發布最好在發布腳本里先調用注銷接口把實例從 Consul 里摘掉再停進程。寫一個簡單的下線腳本#!/bin/bash SERVICE_IDuser-service-192.168.1.100-8080 curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/${SERVICE_ID} kill -TERM $(pgrep -f user-service)這里調的還是本機 Agent 的接口所以腳本在服務提供者機器上執行即可。結合 CI/CD 流水線在停止容器前先執行這個下線步驟可以讓發布期間下游調用方始終只訪問存活實例真正實現無感發布。6. 一點擴展ACL 安全與配置中心玩法6.1 別忽略 ACL 安全Consul 老版本曝出過一些安全漏洞大多和 ACL 權限校驗繞過有關。如果只是在內網跑很多人會忽略安全配置但微服務架構里注冊中心掌握著所有服務的地址一旦被入侵整個系統的調用拓撲就暴露了風險非常高。Consul 支持完整的 ACL 系統可以為不同的服務、Key 配置細粒度的讀寫權限。簡單做法是啟用 ACLacl { enabled true default_policy deny tokens { master your-bootstrap-token } }開啟后所有 API 請求都需要帶 Token 頭。Spring Cloud 的 Consul 集成也支持配置 Tokenspring: cloud: consul: config: acl-token: your-token discovery: acl-token: your-token我的建議是即使內網環境也把 ACL 開啟至少做個基礎防護。同時盡量使用較新版本的 Consul官方修復安全漏洞后在 release note 里都有記錄及時升級比什么防護都管用。6.2 Consul 還能當輕量配置中心用Consul 的內置 KV 存儲除了支撐服務發現也可以直接用做配置中心。雖然沒有 Nacos 的命名空間、分組、灰度這些豐富功能但對中小團隊來說夠用。Spring Cloud Consul Config 的接入方式和 Nacos 類似。引入依賴dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-consul-config/artifactId /dependency配置里指定 KV 路徑spring: cloud: consul: config: enabled: true prefixes: config default-context: application然后在 Consul 的 KV 里創建config/user-service/data這樣的路徑存放配置文件內容。配合spring-cloud-starter-bus可以實現配置變更后的自動刷新。不過要提醒一句Consul 的 KV 功能適合存一些簡單的、變更頻率不高的配置。如果配置項特別多、需要分環境分團隊管理、需要灰度發布還是用 Nacos 或者 Apollo 這類專業配置中心更合適。選型要看團隊體量沒有銀彈。我在實際使用中最深的一個體會是服務發現這塊選哪個注冊中心不是最難的難的是把健康檢查、優雅上下線、負載均衡這些細節真正落實到生產環境里。很多人項目跑起來看著一切正常等到發布日才發現流量打到了正在關停的實例上或者服務擴容后新實例遲遲沒有被下游感知到。這些坑大多不是注冊中心本身的問題而是配置和使用姿勢的問題。希望這篇文章能讓你在搭服務發現的時候少走些彎路。最后再分享一個小習慣無論用 Consul 還是其他注冊中心上線前一定要把“實例下線 - 健康檢查失敗 - 自動摘除”這條鏈路完整演練一遍確認每個環節的時間都符合預期。這個流程順暢了線上發布和故障處理會省去很多麻煩。