Golang - 微服务中使用 Consul 和 etcd 进行服务发现和注册

一、使用 Consul 实现服务发现与注册

1. 安装和运行 Consul

# 下载 Consul
wget https://releases.hashicorp.com/consul/1.16.2/consul_1.16.2_linux_amd64.zip
unzip consul_1.16.2_linux_amd64.zip
sudo mv consul /usr/local/bin/
​
# 启动 Consul 开发模式(仅用于测试)
consul agent -dev

2. 创建服务注册代码

package main
​
import (
    "fmt"
    "log"
    "net/http"
    "strconv"
​
    consul "github.com/hashicorp/consul/api"
)
​
func main() {
    // 创建服务器
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Service is healthy")
    })
​
    // 注册服务到 Consul
    serviceName := "user-service"
    serviceID := "user-service-1"
    servicePort := 8080if err := registerServiceWithConsul(serviceName, serviceID, servicePort); err != nil {
        log.Fatalf("Failed to register service: %v", err)
    }
​
    fmt.Printf("Service registered with Consul. Starting HTTP server on port %d...\n", servicePort)
    if err := http.ListenAndServe(":"+strconv.Itoa(servicePort), nil); err != nil {
        log.Fatalf("HTTP server error: %v", err)
    }
}
​
func registerServiceWithConsul(serviceName, serviceID string, servicePort int) error {
    // 创建 Consul 客户端配置
    config := consul.DefaultConfig()
    config.Address = "localhost:8500" // Consul 默认地址
// 创建客户端
    client, err := consul.NewClient(config)
    if err != nil {
        return fmt.Errorf("failed to create consul client: %v", err)
    }
​
    // 准备服务注册信息
    registration := &consul.AgentServiceRegistration{
        ID:      serviceID,
        Name:    serviceName,
        Port:    servicePort,
        Address: "localhost", // 服务地址
        Check: &consul.AgentServiceCheck{
            HTTP:     fmt.Sprintf("http://localhost:%d/health", servicePort),
            Interval: "10s",
            Timeout:  "3s",
        },
        Tags: []string{"go", "microservice"},
    }
​
    // 注册服务
    return client.Agent().ServiceRegister(registration)
}

3. 服务发现代码

package main
​
import (
    "fmt"
    "log"
    "net/http"
​
    consul "github.com/hashicorp/consul/api"
)
​
func main() {
    // 创建服务调用 API
    http.HandleFunc("/call-user-service", func(w http.ResponseWriter, r *http.Request) {
        serviceURL, err := discoverServiceWithConsul("user-service")
        if err != nil {
            http.Error(w, fmt.Sprintf("Service discovery error: %v", err), http.StatusInternalServerError)
            return
        }
​
        fmt.Fprintf(w, "Discovered user-service at: %s", serviceURL)
        // 实际应用中,这里会使用 http.Client 调用发现的服务
    })
​
    fmt.Println("Client service started on port 8081...")
    if err := http.ListenAndServe(":8081", nil); err != nil {
        log.Fatalf("HTTP server error: %v", err)
    }
}
​
func discoverServiceWithConsul(serviceName string) (string, error) {
    // 创建 Consul 客户端配置
    config := consul.DefaultConfig()
    config.Address = "localhost:8500"// 创建客户端
    client, err := consul.NewClient(config)
    if err != nil {
        return "", fmt.Errorf("failed to create consul client: %v", err)
    }
​
    // 查询服务
    services, _, err := client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return "", fmt.Errorf("failed to discover service: %v", err)
    }
​
    if len(services) == 0 {
        return "", fmt.Errorf("no healthy instances of service %s found", serviceName)
    }
​
    // 简单负载均衡:这里只返回第一个健康的服务实例
    // 实际生产中可实现更复杂的负载均衡策略
    service := services[0]
    serviceURL := fmt.Sprintf("http://%s:%d", service.Service.Address, service.Service.Port)
​
    return serviceURL, nil
}

二、使用 etcd 实现服务发现与注册

1. 安装和运行 etcd

# 下载和安装 etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.5.9/etcd-v3.5.9-linux-amd64.tar.gz
tar -xzvf etcd-v3.5.9-linux-amd64.tar.gz
cd etcd-v3.5.9-linux-amd64
sudo mv etcd etcdctl /usr/local/bin/
​
# 启动 etcd
etcd

2. 创建服务注册代码

package main
​
import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
​
    clientv3 "go.etcd.io/etcd/client/v3"
)
​
type ServiceInfo struct {
    Name    string `json:"name"`
    ID      string `json:"id"`
    Address string `json:"address"`
    Port    int    `json:"port"`
}
​
func main() {
    // 创建服务器
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Service is healthy")
    })
​
    // 服务信息
    serviceName := "payment-service"
    serviceID := "payment-service-1"
    servicePort := 8082// 注册服务到 etcd
    cancelCtx, cancel := context.WithCancel(context.Background())
    defer cancel()
​
    if err := registerServiceWithEtcd(cancelCtx, serviceName, serviceID, servicePort); err != nil {
        log.Fatalf("Failed to register service: %v", err)
    }
​
    fmt.Printf("Service registered with etcd. Starting HTTP server on port %d...\n", servicePort)
    if err := http.ListenAndServe(":"+strconv.Itoa(servicePort), nil); err != nil {
        log.Fatalf("HTTP server error: %v", err)
    }
}
​
func registerServiceWithEtcd(ctx context.Context, serviceName, serviceID string, servicePort int) error {
    // 创建 etcd 客户端
    client, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        return fmt.Errorf("failed to create etcd client: %v", err)
    }
​
    // 创建服务信息
    serviceInfo := ServiceInfo{
        Name:    serviceName,
        ID:      serviceID,
        Address: "localhost",
        Port:    servicePort,
    }
​
    // 编码服务信息为 JSON
    serviceData, err := json.Marshal(serviceInfo)
    if err != nil {
        return fmt.Errorf("failed to marshal service info: %v", err)
    }
​
    // 服务注册键
    key := fmt.Sprintf("/services/%s/%s", serviceName, serviceID)
​
    // 设置 TTL 为 15 秒的租约
    lease, err := client.Grant(ctx, 15)
    if err != nil {
        return fmt.Errorf("failed to create lease: %v", err)
    }
​
    // 将服务信息放入 etcd,带有租约
    _, err = client.Put(ctx, key, string(serviceData), clientv3.WithLease(lease.ID))
    if err != nil {
        return fmt.Errorf("failed to register service: %v", err)
    }
​
    // 保持租约活跃(心跳)
    keepAliveCh, err := client.KeepAlive(ctx, lease.ID)
    if err != nil {
        return fmt.Errorf("failed to keep lease alive: %v", err)
    }
​
    // 在后台处理租约续约响应
    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case ka, ok := <-keepAliveCh:
                if !ok {
                    log.Println("Keep alive channel closed, reregistering...")
                    // 实际应用中这里应该有重新注册的逻辑
                    return
                }
                log.Printf("Lease renewed: %d", ka.ID)
            }
        }
    }()
​
    return nil
}

3. 服务发现代码

package main
​
import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
​
    clientv3 "go.etcd.io/etcd/client/v3"
)
​
type ServiceInfo struct {
    Name    string `json:"name"`
    ID      string `json:"id"`
    Address string `json:"address"`
    Port    int    `json:"port"`
}
​
func main() {
    // 创建服务调用 API
    http.HandleFunc("/call-payment-service", func(w http.ResponseWriter, r *http.Request) {
        serviceURL, err := discoverServiceWithEtcd("payment-service")
        if err != nil {
            http.Error(w, fmt.Sprintf("Service discovery error: %v", err), http.StatusInternalServerError)
            return
        }
​
        fmt.Fprintf(w, "Discovered payment-service at: %s", serviceURL)
        // 实际应用中,这里会使用 http.Client 调用发现的服务
    })
​
    fmt.Println("Client service started on port 8083...")
    if err := http.ListenAndServe(":8083", nil); err != nil {
        log.Fatalf("HTTP server error: %v", err)
    }
}
​
func discoverServiceWithEtcd(serviceName string) (string, error) {
    // 创建 etcd 客户端
    client, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        return "", fmt.Errorf("failed to create etcd client: %v", err)
    }
    defer client.Close()
​
    // 查询服务
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
​
    // 查询指定服务名称下的所有实例
    key := fmt.Sprintf("/services/%s/", serviceName)
    resp, err := client.Get(ctx, key, clientv3.WithPrefix())
    if err != nil {
        return "", fmt.Errorf("failed to discover service: %v", err)
    }
​
    if len(resp.Kvs) == 0 {
        return "", fmt.Errorf("no instances of service %s found", serviceName)
    }
​
    // 简单负载均衡:这里只返回第一个服务实例
    // 实际生产中可实现更复杂的负载均衡策略
    var serviceInfo ServiceInfo
    if err := json.Unmarshal(resp.Kvs[0].Value, &serviceInfo); err != nil {
        return "", fmt.Errorf("failed to unmarshal service info: %v", err)
    }
​
    serviceURL := fmt.Sprintf("http://%s:%d", serviceInfo.Address, serviceInfo.Port)
    return serviceURL, nil
}

三、两种方案的对比与选择建议

Consul 优势

  • 内置健康检查和DNS接口
  • 提供Web UI,方便监控和管理
  • 支持多数据中心
  • 更适合对服务发现有完整需求的场景

etcd 优势

  • 更简单的设计,专注于分布式键值存储
  • 更轻量级,资源消耗较低
  • Kubernetes 内部使用,与 Kubernetes 集成更好
  • 更适合需要自定义服务发现逻辑的场景

选择建议

  • 如果已经使用 Kubernetes或者只需要简单的配置共享和服务注册,优先考虑 etcd
  • 如果需要完整的服务发现解决方案,选择 Consul

四、实际使用建议

依赖管理

# Consul 依赖
go get github.com/hashicorp/consul/api
​
# etcd 依赖
go get go.etcd.io/etcd/client/v3

错误处理与重试逻辑

  • 添加指数退避重试机制
  • 实现服务注册失败的恢复策略

负载均衡

  • 上述示例只实现了简单的选择第一个可用服务
  • 生产环境中应实现轮询、随机或基于权重的负载均衡

缓存与优化

  • 考虑在客户端缓存服务发现结果
  • 使用 etcd 的 Watch 或 Consul 的长轮询机制监听服务变化
posted @ 2025-07-25 19:37  李若盛开  阅读(85)  评论(0)    收藏  举报