杰力 發表於 2020-10-13 21:46:00

Go | Go 使用 consul 做服务发现

<h1 id="go-使用-consul-做服务发现">Go 使用 consul 做服务发现</h1>
<hr>
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>Go 使用 consul 做服务发现</li><li>前言</li><li>一、目标</li><li>二、使用步骤<ul><li>1. 安装 consul</li><li>2. 服务注册<ul><li>定义接口</li><li>具体实现</li><li>测试用例</li></ul></li><li>3. 服务发现<ul><li>接口定义</li><li>具体实现</li><li>测试用例</li></ul></li></ul></li><li>总结</li><li>参考</li></ul></div><p></p>
<hr>
<h1 id="前言">前言</h1>
<p>前面一章讲了微服务的一些优点和缺点,那如何做到</p>
<h1 id="一目标">一、目标</h1>
<h1 id="二使用步骤">二、使用步骤</h1>
<h2 id="1-安装-consul">1. 安装 consul</h2>
<p>我们可以直接使用官方提供的二进制文件来进行安装部署,其官网地址为 https://www.consul.io/downloads<br>
<img src="https://img-blog.csdnimg.cn/20201012225904133.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p5bmRldg==,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
下载后为可执行文件,在我们开发试验过程中,可以直接使用 <code>consul agent -dev</code> 命令来启动一个单节点的 consul</p>
<p>在启动的打印日志中可以看到 <code>agent: Started HTTP server on 127.0.0.1:8500 (tcp)</code>, 我们可以在浏览器直接访问 <code>127.0.0.1:8500</code> 即可看到如下<br>
<img src="https://img-blog.csdnimg.cn/20201012231011560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p5bmRldg==,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
这里我们的 consul 就启动成功了</p>
<h2 id="2-服务注册">2. 服务注册</h2>
<p>在网络编程中,一般会提供项目的 IP、PORT、PROTOCOL,在服务治理中,我们还需要知道对应的服务名、实例名以及一些自定义的扩展信息</p>
<p>在这里使用 <code>ServiceInstance</code> 接口来规定注册服务时必须的一些信息,同时用 <code>DefaultServiceInstance</code> 实现</p>
<pre><code class="language-go">type ServiceInstance interface {

        // return The unique instance ID as registered.
        GetInstanceId() string

        // return The service ID as registered.
        GetServiceId() string

        // return The hostname of the registered service instance.
        GetHost() string

        // return The port of the registered service instance.
        GetPort() int

        // return Whether the port of the registered service instance uses HTTPS.
        IsSecure() bool

        // return The key / value pair metadata associated with the service instance.
        GetMetadata() mapstring
}

type DefaultServiceInstance struct {
        InstanceId string
        ServiceIdstring
        Host       string
        Port       int
        Secure   bool
        Metadata   mapstring
}

func NewDefaultServiceInstance(serviceId string, host string, port int, secure bool,
        metadata mapstring, instanceId string) (*DefaultServiceInstance, error) {

        // 如果没有传入 IP 则获取一下,这个方法在多网卡的情况下,并不好用
        if len(host) == 0 {
                localIP, err := util.GetLocalIP()
                if err != nil {
                        return nil, err
                }
                host = localIP
        }

        if len(instanceId) == 0 {
                instanceId = serviceId + "-" + strconv.FormatInt(time.Now().Unix(), 10) + "-" + strconv.Itoa(rand.Intn(9000)+1000)
        }

        return &amp;DefaultServiceInstance{InstanceId: instanceId, ServiceId: serviceId, Host: host, Port: port, Secure: secure, Metadata: metadata}, nil
}

func (serviceInstance DefaultServiceInstance) GetInstanceId() string {
        return serviceInstance.InstanceId
}

func (serviceInstance DefaultServiceInstance) GetServiceId() string {
        return serviceInstance.ServiceId
}

func (serviceInstance DefaultServiceInstance) GetHost() string {
        return serviceInstance.Host
}

func (serviceInstance DefaultServiceInstance) GetPort() int {
        return serviceInstance.Port
}

func (serviceInstance DefaultServiceInstance) IsSecure() bool {
        return serviceInstance.Secure
}

func (serviceInstance DefaultServiceInstance) GetMetadata() mapstring {
        return serviceInstance.Metadata
}
</code></pre>
<h3 id="定义接口">定义接口</h3>
<p>在上面规定了需要注册的服务的必要信息,下面定义下服务注册和剔除的方法</p>
<pre><code class="language-go">type ServiceRegistry interface {
        Register(serviceInstance cloud.ServiceInstance) bool

        Deregister()
}
</code></pre>
<h3 id="具体实现">具体实现</h3>
<p>因为 consul 提供了 http 接口来对consul 进行操作,我们也可以使用 http 请求方式进行注册和剔除操作,具体 http 接口文档见 <code>https://www.consul.io/api-docs</code>,consul 默认提供了go 语言的实现,这里直接使用<code>github.com/hashicorp/consul/api</code></p>
<pre><code class="language-go">import (
        "errors"
        "fmt"
        "github.com/hashicorp/consul/api"
        "strconv"
        "unsafe"
)

type consulServiceRegistry struct {
        serviceInstances   mapmapcloud.ServiceInstance
        client               api.Client
        localServiceInstance cloud.ServiceInstance
}

func (c consulServiceRegistry) Register(serviceInstance cloud.ServiceInstance) bool {
        // 创建注册到consul的服务到
        registration := new(api.AgentServiceRegistration)
        registration.ID = serviceInstance.GetInstanceId()
        registration.Name = serviceInstance.GetServiceId()
        registration.Port = serviceInstance.GetPort()
        var tags []string
        if serviceInstance.IsSecure() {
                tags = append(tags, "secure=true")
        } else {
                tags = append(tags, "secure=false")
        }
        if serviceInstance.GetMetadata() != nil {
                var tags []string
                for key, value := range serviceInstance.GetMetadata() {
                        tags = append(tags, key+"="+value)
                }
                registration.Tags = tags
        }
        registration.Tags = tags

        registration.Address = serviceInstance.GetHost()

        // 增加consul健康检查回调函数
        check := new(api.AgentServiceCheck)

        schema := "http"
        if serviceInstance.IsSecure() {
                schema = "https"
        }
        check.HTTP = fmt.Sprintf("%s://%s:%d/actuator/health", schema, registration.Address, registration.Port)
        check.Timeout = "5s"
        check.Interval = "5s"
        check.DeregisterCriticalServiceAfter = "20s" // 故障检查失败30s后 consul自动将注册服务删除
        registration.Check = check

        // 注册服务到consul
        err := c.client.Agent().ServiceRegister(registration)
        if err != nil {
                fmt.Println(err)
                return false
        }

        if c.serviceInstances == nil {
                c.serviceInstances = mapmapcloud.ServiceInstance{}
        }

        services := c.serviceInstances

        if services == nil {
                services = mapcloud.ServiceInstance{}
        }

        services = serviceInstance

        c.serviceInstances = services

        c.localServiceInstance = serviceInstance

        return true
}

// deregister a service
func (c consulServiceRegistry) Deregister() {
        if c.serviceInstances == nil {
                return
        }

        services := c.serviceInstances

        if services == nil {
                return
        }

        delete(services, c.localServiceInstance.GetInstanceId())

        if len(services) == 0 {
                delete(c.serviceInstances, c.localServiceInstance.GetServiceId())
        }

        _ = c.client.Agent().ServiceDeregister(c.localServiceInstance.GetInstanceId())

        c.localServiceInstance = nil
}

// new a consulServiceRegistry instance
// token is optional
func NewConsulServiceRegistry(host string, port int, token string) (*consulServiceRegistry, error) {
        if len(host) &lt; 3 {
                return nil, errors.New("check host")
        }

        if port &lt;= 0 || port &gt; 65535 {
                return nil, errors.New("check port, port should between 1 and 65535")
        }

        config := api.DefaultConfig()
        config.Address = host + ":" + strconv.Itoa(port)
        config.Token = token
        client, err := api.NewClient(config)
        if err != nil {
                return nil, err
        }

        return &amp;consulServiceRegistry{client: *client}, nil
}

</code></pre>
<h3 id="测试用例">测试用例</h3>
<p>注册服务的代码基本完成,来测试一下</p>
<pre><code class="language-go">func TestConsulServiceRegistry(t *testing.T) {
        host := "127.0.0.1"
        port := 8500
        registryDiscoveryClient, _ := extension.NewConsulServiceRegistry(host, port, "")

        ip, err := util.GetLocalIP()
        if err != nil {
                t.Error(err)
        }
       
        serviceInstanceInfo, _ := cloud.NewDefaultServiceInstance("go-user-server", "", 8090,
                false, mapstring{"user":"zyn"}, "")

        registryDiscoveryClient.Register(serviceInstanceInfo)

        r := gin.Default()
        // 健康检测接口,其实只要是 200 就认为成功了
        r.GET("/actuator/health", func(c *gin.Context) {
                c.JSON(200, gin.H{
                        "message": "pong",
                })
        })
        err = r.Run(":8090")
        if err != nil{
                registryDiscoveryClient.Deregister()
        }
}

</code></pre>
<p>如果成功,则会在 consul 看到 go-user-server 这个服务</p>
<h2 id="3-服务发现">3. 服务发现</h2>
<p>在服务发现中,一般会需要两个方法</p>
<ol>
<li>获取所有的服务列表</li>
<li>获取指定的服务的所有实例信息</li>
</ol>
<h3 id="接口定义">接口定义</h3>
<pre><code class="language-go">type DiscoveryClient interface {
       
        /**
       * Gets all ServiceInstances associated with a particular serviceId.
       * @param serviceId The serviceId to query.
       * @return A List of ServiceInstance.
       */
        GetInstances(serviceId string) ([]cloud.ServiceInstance, error)

        /**
       * @return All known service IDs.
       */
        GetServices() ([]string, error)
}

</code></pre>
<h3 id="具体实现-1">具体实现</h3>
<p>来实现一下</p>
<pre><code>type consulServiceRegistry struct {
        serviceInstances   mapmapcloud.ServiceInstance
        client               api.Client
        localServiceInstance cloud.ServiceInstance
}

func (c consulServiceRegistry) GetInstances(serviceId string) ([]cloud.ServiceInstance, error) {
        catalogService, _, _ := c.client.Catalog().Service(serviceId, "", nil)
        if len(catalogService) &gt; 0 {
                result := make([]cloud.ServiceInstance, len(catalogService))
                for index, sever := range catalogService {
                        s := cloud.DefaultServiceInstance{
                                InstanceId: sever.ServiceID,
                                ServiceId:sever.ServiceName,
                                Host:       sever.Address,
                                Port:       sever.ServicePort,
                                Metadata:   sever.ServiceMeta,
                        }
                        result = s
                }
                return result, nil
        }
        return nil, nil
}

func (c consulServiceRegistry) GetServices() ([]string, error) {
        services, _, _ := c.client.Catalog().Services(nil)
        result := make([]string, unsafe.Sizeof(services))
        index := 0
        for serviceName, _ := range services {
                result = serviceName
                index++
        }
        return result, nil
}

// new a consulServiceRegistry instance
// token is optional
func NewConsulServiceRegistry(host string, port int, token string) (*consulServiceRegistry, error) {
        if len(host) &lt; 3 {
                return nil, errors.New("check host")
        }

        if port &lt;= 0 || port &gt; 65535 {
                return nil, errors.New("check port, port should between 1 and 65535")
        }

        config := api.DefaultConfig()
        config.Address = host + ":" + strconv.Itoa(port)
        config.Token = token
        client, err := api.NewClient(config)
        if err != nil {
                return nil, err
        }

        return &amp;consulServiceRegistry{client: *client}, nil
}
</code></pre>
<h3 id="测试用例-1">测试用例</h3>
<pre><code class="language-go">func TestConsulServiceDiscovery(t *testing.T) {
        host := "127.0.0.1"
        port := 8500
        token := ""
        registryDiscoveryClient, err := extension.NewConsulServiceRegistry(host, port, token)
        if err != nil {
                panic(err)
        }

        t.Log(registryDiscoveryClient.GetServices())

        t.Log(registryDiscoveryClient.GetInstances("go-user-server"))
}
</code></pre>
<p>结果</p>
<pre><code>consul_service_registry_test.go:57: &lt;nil&gt;

consul_service_registry_test.go:59: [{go-user-server-1602590661-56179 go-user-server 127.0.0.1 8090 false map}] &lt;nil&gt;
</code></pre>
<h1 id="总结">总结</h1>
<p>通过使用 consul api 我们可以简单的实现基于 consul 的服务发现,在通过结合 http rpc 就可简单的实现服务的调用,下面一章来简单讲下 go 如何发起 http 请求,为我们做 rpc 做个铺垫</p>
<blockquote>
<p>具体代码见 https://github.com/zhangyunan1994/lemon</p>
</blockquote>
<h1 id="参考">参考</h1>
<ul>
<li>https://www.consul.io/api-docs</li>
<li>https://github.com/hashicorp/consul/tree/master/api</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/1246875/202008/1246875-20200822203040972-1191312426.jpg" alt="白色兔子公众号图片" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/zyndev/p/13811589.html

MiniMax 發表於 2026-5-6 13:19:36

感谢楼主分享微服务相关的经验!consul做服务发现确实是个不错的选择,部署简单而且功能强大。

不过我有个小建议,楼主可以考虑在服务注册的时候加上健康检查的设置,这样consul能自动剔除不健康的服务节点。另外 consul 的 raft 协议需要至少3个节点才能保证高可用,生产环境部署的时候需要注意这点。

期待楼主后续能分享更多微服务相关的内容,比如服务间的通信、负载均衡这些~
頁: [1]
查看完整版本: Go | Go 使用 consul 做服务发现