Go实现简易服务发现

众所周知,要请求一个服务,必须要知道服务的网络地址(IP和端口)。随着微服务的发展,越来越多的用户、请求和需求使得请求服务这项工作变得非常困难。在基于云原生的微服务时代,我们的服务由于各种情况会经常发生变更,例如自动伸缩、升级和故障。因为这些变化,服务实例会不断获取新的IP。

这就是服务发现进入微服务场景的地方。我们需要某种系统能定时跟踪所有服务,并更新服务的IP/端口,这样客户端就可以无缝请求到服务。

服务发现在观念上很简单:核心组件就是服务注册,本质就是存储服务网络地址(IP/端口)的数据库。这种机制在服务实例启动和停止时更新服务注册表。

有两种主流的服务发现方式:

  • 服务端和客户端直接与服务注册组件通信。
  • 部署基础设施例如K8S等,来处理服务发现。

本文将使用第三方注册模式来实现我们的服务发现功能。这种模式无需服务自身主动注册,通过第三方registar模块来完成(该模块定时调docker ps -a来获取服务信息进行注册)。


架构图

反向代理

为了实现反向代理,这里使用httputil包。主要目的是为了提供负载均衡。为了实现客户端请求以轮询方式路由到服务端,我们采用简单的算法,根据请求总数对注册服务数量取模,可以很简单的找到服务端并代理请求。

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
    }
    //请求数对服务实例个数取模
    backendIndex := int(atomic.LoadUint64(&a.RequestCount) % uint64(a.SRegistry.Len()))
    fmt.Printf("Request routing to instance %d\n", backendIndex)
    //将请求代理转发到对应的服务端
    a.SRegistry.GetByIndex(backendIndex).
        proxy.
        ServeHTTP(w, r)
}

上面的代码很简单,定义Application结构体,包含RequestCount请求总数和SRegistry服务注册字段。定义请求处理函数Handle,根据请求总数和后端服务实例个数取模,调用GetByIndex函数读取服务IP和端口,将请求代理转发到对应的后端服务。

Registar

使用time.Tick实现定时发现服务变化。这里我们是一个服务实例对应一个容器。每当定时到了,执行docker ps -a使用docker官方SDK来读取服务变化。使用-a参数是为了知道哪个容器退出了,需要将它从服务注册列表中删除。如果有新的容器增加且是运行状态,检查是否已注册到服务列表,如果没有就将其添加到注册列表中。

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" //后端服务实例docker镜像名称
    ContainerRunningState = "running" //服务运行状态
)

func (r *Registrar) Observe() {
    for range time.Tick(r.Interval) { //定时器
        //获取容器列表
        cList, _ := r.DockerCLI.ContainerList(context.Background(), types.ContainerListOptions{
            All: true,
        })
         //没有容器运行,意味着没有后端服务可用,清空注册列表
        if len(cList) == 0 {
            r.SRegistry.RemoveAll()
            continue
        }
        //镜像过滤名称不是hello的容器,也就是指定服务
        for _, c := range cList {
            if c.Image != HelloServiceImageName {
                continue
            }
             //根据容器ID查找该后端服务是否已经注册
            _, exist := r.SRegistry.GetByContainerID(c.ID)

            if c.State == ContainerRunningState {
                //容器运行但是为注册,执行注册操作
                if !exist {
                    addr := fmt.Sprintf("http://localhost:%d", c.Ports[0].PublicPort)
                    r.SRegistry.Add(c.ID, addr)
                }
            } else {
                //容器不是运行状态,但已注册需移除
                if exist {
                    r.SRegistry.RemoveByContainerID(c.ID)
                }
            }
        }
    }
}

上面的代码Observe方法,主要是定时调docker ps -a命令,根据镜像名称HelloServiceImageName来锁定我们的服务实例,同一类服务使用的相同的镜像。根据前面讨论的结论,容器正常运行就考虑添加到注册列表,否则移除。

服务注册

服务注册是一个非常基本的结构体切片,使用sync.RWMutex来实现并发同步,保存所有的正常服务实例列表。会定时更新列表。


package main

import (
    "fmt"
    "net/http/httputil"
    "net/url"
    "sync"
)
//定义后端服务结构体
type backend struct {
    proxy       *httputil.ReverseProxy  //代理转发
    containerID string   //容器ID
}
//服务注册结构体
type ServiceRegistry struct {
    mu       sync.RWMutex
    backends []backend
}
//初始化
func (s *ServiceRegistry) Init() {
    s.mu = sync.RWMutex{}   
    s.backends = []backend{}  //默认服务列表为空
}
//向服务列表添加服务,也即是注册服务
func (s *ServiceRegistry) Add(containerID, addr string) {
    s.mu.Lock()
    defer s.mu.Unlock()

    URL, _ := url.Parse(addr)

    s.backends = append(s.backends, backend{
       //根据后端服务创建代理对象,用于转发请求
        proxy:       httputil.NewSingleHostReverseProxy(URL),
        containerID: containerID,
    })
}
//根据容器ID查询注册列表,支持并发
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
}
//根据容器ID读取后端服务实例
func (s *ServiceRegistry) GetByIndex(index int) backend {
    s.mu.RLock()
    defer s.mu.RUnlock()

    return s.backends[index]
}
//根据容器ID移除服务
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
}
//清除服务注册列表
func (s *ServiceRegistry) RemoveAll() {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.backends = []backend{}
}
//获取服务实例个数
func (s *ServiceRegistry) Len() int {
    s.mu.RLock()
    defer s.mu.RUnlock()

    return len(s.backends)
}
//打印服务列表
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

总结:

本文有几个知识点值得了解下:

  • 服务发现的基本逻辑和实现方式。
  • 使用httputil包来实现请求代理转发,实现反向代理功能。
  • 列表的并发读取同步。
  • 简单的轮询算法实现方式。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容