草庐IT

Go实现简易服务发现

Go语言由浅入深 2023-03-28 原文

众所周知,要请求一个服务,必须要知道服务的网络地址(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包来实现请求代理转发,实现反向代理功能。
  • 列表的并发读取同步。
  • 简单的轮询算法实现方式。

有关Go实现简易服务发现的更多相关文章

  1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  3. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  4. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

    在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  7. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

    您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐