眾所周知灾搏,要請求一個(gè)服務(wù)挫望,必須要知道服務(wù)的網(wǎng)絡(luò)地址(IP和端口)。隨著微服務(wù)的發(fā)展狂窑,越來越多的用戶媳板、請求和需求使得請求服務(wù)這項(xiàng)工作變得非常困難。在基于云原生的微服務(wù)時(shí)代泉哈,我們的服務(wù)由于各種情況會經(jīng)常發(fā)生變更蛉幸,例如自動伸縮、升級和故障丛晦。因?yàn)檫@些變化奕纫,服務(wù)實(shí)例會不斷獲取新的IP。
這就是服務(wù)發(fā)現(xiàn)進(jìn)入微服務(wù)場景的地方烫沙。我們需要某種系統(tǒng)能定時(shí)跟蹤所有服務(wù)匹层,并更新服務(wù)的IP/端口,這樣客戶端就可以無縫請求到服務(wù)锌蓄。
服務(wù)發(fā)現(xiàn)在觀念上很簡單:核心組件就是服務(wù)注冊升筏,本質(zhì)就是存儲服務(wù)網(wǎng)絡(luò)地址(IP/端口)的數(shù)據(jù)庫。這種機(jī)制在服務(wù)實(shí)例啟動和停止時(shí)更新服務(wù)注冊表瘸爽。
有兩種主流的服務(wù)發(fā)現(xiàn)方式:
- 服務(wù)端和客戶端直接與服務(wù)注冊組件通信您访。
- 部署基礎(chǔ)設(shè)施例如K8S等,來處理服務(wù)發(fā)現(xiàn)剪决。
本文將使用第三方注冊模式來實(shí)現(xiàn)我們的服務(wù)發(fā)現(xiàn)功能灵汪。這種模式無需服務(wù)自身主動注冊檀训,通過第三方registar模塊來完成(該模塊定時(shí)調(diào)docker ps -a來獲取服務(wù)信息進(jìn)行注冊)。
反向代理
為了實(shí)現(xiàn)反向代理识虚,這里使用httputil包肢扯。主要目的是為了提供負(fù)載均衡。為了實(shí)現(xiàn)客戶端請求以輪詢方式路由到服務(wù)端担锤,我們采用簡單的算法蔚晨,根據(jù)請求總數(shù)對注冊服務(wù)數(shù)量取模,可以很簡單的找到服務(wù)端并代理請求肛循。
package main
import (
"fmt"
"net/http"
"sync/atomic"
)
type Application struct {
RequestCount uint64
SRegistry *ServiceRegistry
}
func (a *Application) Handle(w http.ResponseWriter, r *http.Request) {
atomic.AddUint64(&a.RequestCount, 1)
if a.SRegistry.Len() == 0 {
w.Write([]byte(`No backend entry in the service registry`))
return
}
//請求數(shù)對服務(wù)實(shí)例個(gè)數(shù)取模
backendIndex := int(atomic.LoadUint64(&a.RequestCount) % uint64(a.SRegistry.Len()))
fmt.Printf("Request routing to instance %d\n", backendIndex)
//將請求代理轉(zhuǎn)發(fā)到對應(yīng)的服務(wù)端
a.SRegistry.GetByIndex(backendIndex).
proxy.
ServeHTTP(w, r)
}
上面的代碼很簡單铭腕,定義Application結(jié)構(gòu)體,包含RequestCount請求總數(shù)和SRegistry服務(wù)注冊字段多糠。定義請求處理函數(shù)Handle累舷,根據(jù)請求總數(shù)和后端服務(wù)實(shí)例個(gè)數(shù)取模,調(diào)用GetByIndex函數(shù)讀取服務(wù)IP和端口夹孔,將請求代理轉(zhuǎn)發(fā)到對應(yīng)的后端服務(wù)被盈。
Registar
使用time.Tick實(shí)現(xiàn)定時(shí)發(fā)現(xiàn)服務(wù)變化。這里我們是一個(gè)服務(wù)實(shí)例對應(yīng)一個(gè)容器搭伤。每當(dāng)定時(shí)到了,執(zhí)行docker ps -a使用docker官方SDK來讀取服務(wù)變化怜俐。使用-a參數(shù)是為了知道哪個(gè)容器退出了,需要將它從服務(wù)注冊列表中刪除拍鲤。如果有新的容器增加且是運(yùn)行狀態(tài),檢查是否已注冊到服務(wù)列表季稳,如果沒有就將其添加到注冊列表中擅这。
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"time"
)
type Registrar struct {
Interval time.Duration
DockerCLI *client.Client
SRegistry *ServiceRegistry
}
const (
HelloServiceImageName = "hello" //后端服務(wù)實(shí)例docker鏡像名稱
ContainerRunningState = "running" //服務(wù)運(yùn)行狀態(tài)
)
func (r *Registrar) Observe() {
for range time.Tick(r.Interval) { //定時(shí)器
//獲取容器列表
cList, _ := r.DockerCLI.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
//沒有容器運(yùn)行,意味著沒有后端服務(wù)可用仲翎,清空注冊列表
if len(cList) == 0 {
r.SRegistry.RemoveAll()
continue
}
//鏡像過濾名稱不是hello的容器,也就是指定服務(wù)
for _, c := range cList {
if c.Image != HelloServiceImageName {
continue
}
//根據(jù)容器ID查找該后端服務(wù)是否已經(jīng)注冊
_, exist := r.SRegistry.GetByContainerID(c.ID)
if c.State == ContainerRunningState {
//容器運(yùn)行但是為注冊谭确,執(zhí)行注冊操作
if !exist {
addr := fmt.Sprintf("http://localhost:%d", c.Ports[0].PublicPort)
r.SRegistry.Add(c.ID, addr)
}
} else {
//容器不是運(yùn)行狀態(tài),但已注冊需移除
if exist {
r.SRegistry.RemoveByContainerID(c.ID)
}
}
}
}
}
上面的代碼Observe方法票渠,主要是定時(shí)調(diào)docker ps -a命令逐哈,根據(jù)鏡像名稱HelloServiceImageName來鎖定我們的服務(wù)實(shí)例,同一類服務(wù)使用的相同的鏡像问顷。根據(jù)前面討論的結(jié)論禀梳,容器正常運(yùn)行就考慮添加到注冊列表肠骆,否則移除。
服務(wù)注冊
服務(wù)注冊是一個(gè)非呈赐龋基本的結(jié)構(gòu)體切片,使用sync.RWMutex來實(shí)現(xiàn)并發(fā)同步莉钙,保存所有的正常服務(wù)實(shí)例列表。會定時(shí)更新列表磁玉。
package main
import (
"fmt"
"net/http/httputil"
"net/url"
"sync"
)
//定義后端服務(wù)結(jié)構(gòu)體
type backend struct {
proxy *httputil.ReverseProxy //代理轉(zhuǎn)發(fā)
containerID string //容器ID
}
//服務(wù)注冊結(jié)構(gòu)體
type ServiceRegistry struct {
mu sync.RWMutex
backends []backend
}
//初始化
func (s *ServiceRegistry) Init() {
s.mu = sync.RWMutex{}
s.backends = []backend{} //默認(rèn)服務(wù)列表為空
}
//向服務(wù)列表添加服務(wù),也即是注冊服務(wù)
func (s *ServiceRegistry) Add(containerID, addr string) {
s.mu.Lock()
defer s.mu.Unlock()
URL, _ := url.Parse(addr)
s.backends = append(s.backends, backend{
//根據(jù)后端服務(wù)創(chuàng)建代理對象席赂,用于轉(zhuǎn)發(fā)請求
proxy: httputil.NewSingleHostReverseProxy(URL),
containerID: containerID,
})
}
//根據(jù)容器ID查詢注冊列表时迫,支持并發(fā)
func (s *ServiceRegistry) GetByContainerID(containerID string) (backend, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, b := range s.backends {
if b.containerID == containerID {
return b, true
}
}
return backend{}, false
}
//根據(jù)容器ID讀取后端服務(wù)實(shí)例
func (s *ServiceRegistry) GetByIndex(index int) backend {
s.mu.RLock()
defer s.mu.RUnlock()
return s.backends[index]
}
//根據(jù)容器ID移除服務(wù)
func (s *ServiceRegistry) RemoveByContainerID(containerID string) {
s.mu.Lock()
defer s.mu.Unlock()
var backends []backend
for _, b := range s.backends {
if b.containerID == containerID {
continue
}
backends = append(backends, b)
}
s.backends = backends
}
//清除服務(wù)注冊列表
func (s *ServiceRegistry) RemoveAll() {
s.mu.Lock()
defer s.mu.Unlock()
s.backends = []backend{}
}
//獲取服務(wù)實(shí)例個(gè)數(shù)
func (s *ServiceRegistry) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.backends)
}
//打印服務(wù)列表
func (s *ServiceRegistry) List() {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.backends {
fmt.Println(s.backends[i].containerID)
}
}
源代碼地址:
https://github.com/Abdulsametileri/simple-service-discovery
總結(jié):
本文有幾個(gè)知識點(diǎn)值得了解下:
- 服務(wù)發(fā)現(xiàn)的基本邏輯和實(shí)現(xiàn)方式。
- 使用httputil包來實(shí)現(xiàn)請求代理轉(zhuǎn)發(fā)便监,實(shí)現(xiàn)反向代理功能扎谎。
- 列表的并發(fā)讀取同步。
- 簡單的輪詢算法實(shí)現(xiàn)方式胧奔。