Go實(shí)現(xiàn)簡易服務(wù)發(fā)現(xiàn)

眾所周知灾搏,要請求一個(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)行注冊)。


架構(gòu)圖

反向代理

為了實(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)方式胧奔。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末预吆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拐叉,更是在濱河造成了極大的恐慌,老刑警劉巖凤瘦,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宿礁,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔬芥,警方通過查閱死者的電腦和手機(jī)梆靖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姑子,“玉大人,你說我怎么就攤上這事街佑『纯浚” “怎么了舆乔?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵希俩,是天一觀的道長。 經(jīng)常有香客問我颜武,道長拖吼,這世上最難降的妖魔是什么鳞上? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任篙议,我火速辦了婚禮怠硼,結(jié)果婚禮上鬼贱,老公的妹妹穿的比我還像新娘香璃。我一直安慰自己,他們只是感情好葡秒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布眯牧。 她就那樣靜靜地躺著蹋岩,像睡著了一般学少。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上禁偎,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機(jī)與錄音如暖,去河邊找鬼。 笑死盒至,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的樱衷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矩桂,長吁一口氣:“原來是場噩夢啊……” “哼痪伦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起网沾,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤辉哥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后醋旦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寇窑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年箩张,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窗市。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片先慷。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡论熙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脓诡,到底是詐尸還是另有隱情,我是刑警寧澤祝谚,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站次泽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏意荤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一玖像、第九天 我趴在偏房一處隱蔽的房頂上張望齐饮。 院中可真熱鬧,春花似錦沈矿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽就珠。三九已至醒颖,卻和暖如春妻怎,著一層夾襖步出監(jiān)牢的瞬間泞歉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工榛丢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晰赞。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像掖鱼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子丰刊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容