一、使用 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 := 8080
if 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 的长轮询机制监听服务变化