微服務中的服務發現
什么是服務發現
服務發現是微服務架構中的關鍵機制,用于確定各個微服務的地址。例如,在一個 API Server
服務中,我們可能需要調用 User
服務來處理用戶注冊、登錄和信息查詢,也可能需要 Product
服務來獲取商品相關信息。那么,如何發現并訪問這些服務呢?
傳統服務發現方法
最簡單的方式是使用數據庫存儲服務名稱及其對應的地址。每當有新的服務實例啟動時,我們將其地址注冊到數據庫中。客戶端在訪問該服務時,可以從數據庫中查詢到對應的地址并發起請求。
在 Kitex
框架中,我們通常使用 ETCD
作為服務注冊與發現的數據庫,下面通過一個示例演示如何實現。
在 ETCD 中注冊服務
在服務啟動前,我們需要將其注冊到 ETCD
中,以便其他微服務能夠發現并訪問它:
func kitexInit() (opts []server.Option) {cfg := config.GetConfig()// RegistryAddress為etcd數據庫地址r, _ := etcd.NewEtcdRegistry(cfg.Registry.RegistryAddress)// address為user服務的地址address := cfg.KitexConfig.Address + cfg.KitexConfig.Portaddr, _:= net.ResolveTCPAddr("tcp", address)// 保存server的配置信息。opts = append(opts,server.WithServiceAddr(addr),server.WithRegistry(r),server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: cfg.KitexConfig.Service,},),)return
}func main() {config.InitConfig() // 初始化配置文件db.InitDB() // 初始化dbopts := kitexInit() // 初始化服務配置信息svr := user.NewServer(new(UserServiceImpl), opts...) //創建newservererr := svr.Run() // 開啟server服務,并實時注冊到etcd數據庫中if err != nil {log.Println(err.Error())}
}
這樣,我們就成功開啟了一個服務,并且成功將其注冊到etcd
數據庫中。使其他服務可以通過 ETCD
進行發現和訪問。
通過客戶端調用服務
在微服務架構中,我們需要使用客戶端來調用 User
服務,并通過 ETCD
進行服務發現:
func initUserClient() userservice.Client {// 1. 創建 etcd 服務解析器(連接注冊中心)r, err := etcd.NewEtcdResolver(registryAddr)if err != nil {log.Fatalln(r)}// 2. 配置客戶端選項:聲明使用 etcd 作為服務發現源opts := []client.Option{client.WithResolver(r),}// 生命etcd進行服務發現。userClient, err := userservice.NewClient("douyinec.user", opts...)if err != nil {log.Fatalln(err)}return userClient
}
處理多個服務實例的情況
每次調用userClient
客戶端時,client
會向etcd
查詢,微服務地址,并發送請求。如果 etcd
里存儲了多個 user service
的地址(即多個實例部署了 user service
),那么 client
會從 etcd
獲取所有可用的 user service
地址,并根據負載均衡策略選擇一個進行調用。
Kitex
默認使用隨機負載均衡,但可以通過 client.WithLoadBalancer()
設定不同的策略,比如輪詢`(Round Robin)、最小連接數(Least Connection)等。
處理服務變更(崩潰或重啟)
如果 User
服務實例不可用了,會發生什么?
User
服務啟動后,會通過etcd.NewEtcdRegistry(...)
注冊自身地址到ETCD
。- 在運行期間,
Kitex
定期向ETCD
發送心跳,用于維持服務的可用狀態。 - 如果
User
服務崩潰或手動關閉:
- 它將停止發送心跳信號
ETCD
發現心跳超時(例如 10s 內未收到心跳)ETCD
自動將該User
服務實例從注冊列表中移除
如果 User 服務重新啟動到了新的地址,客戶端還能找到它嗎?
客戶端每次請求時都會向 ETCD
查詢最新的 User
服務地址,不會緩存舊的地址。
當 User
服務重新啟動并注冊到新的地址時,客戶端會自動獲取最新的服務位置。
這樣,無論 User
服務的實例數量如何變化,客戶端始終能夠找到可用的服務實例,實現了動態的服務發現和負載均衡。
k8s中服務發現的方法
在 Kubernetes(k8s)
中,每個應用服務對應一個 Service
,k8s
通過service
進行服務發現,它充當了訪問 Pod
的穩定入口。Service
通過 Endpoint
關聯到具體的 Pod
,并按照一定的負載均衡策略將請求路由到后端的 Pod。
Service、Endpoint 與 Pod 的關系
在 Kubernetes 中,
- Pod 是運行應用程序的最小單位,每個 Pod 可能包含一個或多個容器。
- Service 提供了一個穩定的訪問入口,它會自動發現符合標簽選擇器(selector)的 Pod,并將請求負載均衡地轉發給它們。
- Endpoint 記錄了 Service 關聯的 Pod 的實際 IP 和端口。
整個流程如下:
- Service 負責管理和暴露一組 Pod。
- Endpoint 維護了當前 Service 關聯的 Pod 列表。
- kube-proxy 監聽 Service 變更,并基于 Endpoint 配置負載均衡規則。
- 客戶端訪問 Service 時,kube-proxy 負責將請求轉發給 Endpoint 中的 Pod。
查看當前 Kubernetes 集群中的 Service
我們可以通過kubectl get service -o wide
查看當前存在的所有service。實例輸出:
kubectl get service -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 297d <none>
manager NodePort 10.43.196.96 <none> 8090:32545/TCP 191d app.oam.dev/component=manager
localstorage NodePort 10.43.183.49 <none> 8189:30621/TCP 191d app.oam.dev/component=localstorage
scheduler NodePort 10.43.8.18 <none> 5525:31965/TCP 191d app.oam.dev/component=scheduler
redis NodePort 10.43.167.133 <none> 6379:32155/TCP 191d app.oam.dev/component=redis
其中 redis 服務的 Cluster-IP
為 10.43.167.133
,我們可以繼續查詢其 Endpoint。
查看 Service 關聯的 Endpoints
使用以下命令查看 kubectl get endpoints redis -o wide
。實例輸出:
kubectl get endpoints redis -o wide
NAME ENDPOINTS AGE
redis 10.42.0.18:6379 191d
從這里可以看到,redis 服務對應的 Pod 運行在 10.42.0.18:6379
。
查看 Service 詳細信息
之后通過kubectl describe service redis
查看詳情。實例輸出:
IP Families: IPv4
IP: 10.43.167.133
IPs: 10.43.167.133
Port: redis 6379/TCP
TargetPort: 6379/TCP
NodePort: redis 32155/TCP
Endpoints: 10.42.0.18:6379
Session Affinity: None
External Traffic Policy: Local```,
這里,這里的 Endpoints
字段表明,該 Service
的流量被轉發到了 10.42.0.18:6379
上運行的 Pod
。其中IP
字段標識Service
的 ClusterIP
。Port: redis 6379/TCP
定義Service
的端口映射。TargetPort
標識Service
關聯的 Pod
實際監聽的端口,流量會被轉發到該端口。
Service 的服務發現機制
Kubernetes 主要通過兩種方式實現服務發現:
- 環境變量
Kubernetes
在Pod
啟動時,會為其關聯的Service
自動創建一組環境變量(處于同一namespace
的Service
),例如REDIS_SERVICE_HOST=10.43.167.133
和REDIS_SERVICE_PORT=6379
,在同一命名空間中的pod
可以通過這些環境變量連接Service
。
- DNS 解析方式
Kubernetes
內置的CoreDNS
服務會為Service
自動創建DNS
記錄。例如,redis
服務的DNS
記錄是redis.default.svc.cluster.local
,集群內部的Pod
直接訪問redis:6379
即可連接。其中default
是服務所在的命名空間
我們運行ping redis.default.svc.cluster.local
,發現返回ip
地址10.42.0.18
,即redis service
的地址。
redis.default.svc.cluster.local
被稱為 FQDN(Fully Qualified Domain Name,全限定域名)。
全限定域名信息存儲在 Kubernetes
內部的 CoreDNS
組件中。其中Kubernetes API Server
監聽 Service
資源的創建或刪除。CoreDNS
通過 kube-dns
插件 監聽這些變化,并在內部的 DNS
服務器 里維護這些 Service
的解析記錄。當 Pod
需要訪問 Service
,會向 CoreDNS
查詢 xxx.namespace.svc.cluster.local
,CoreDNS
解析出 ClusterIP
并返回給 Pod
,這樣 Pod
就能訪問 Service
了。
Service 的負載均衡原理
當多個 Pod
運行同一個 Service
時,Kubernetes
通過 kube-proxy
進行負載均衡,主要有以下幾種模式:
-
iptables(默認)
:kube-proxy
使用iptables
規則將流量隨機轉發到Endpoints
。 -
IPVS(更高效)
:使用IP Virtual Server
進行流量分發,支持更多的負載均衡策略(如rr
輪詢、wrr
權重輪詢、lc
最小連接數等)。