Appearance
服务注册与发现
本篇是 Go 微服务系列的第二篇。我们将深入服务注册与发现这一微服务基础设施的核心主题。在分布式系统中,服务实例的地址会动态变化(容器漂移、扩缩容、节点宕机),调用方不能再写死 IP 地址。本篇将讲解服务发现的原理,并使用 Consul 和 Etcd 完成一个真实的服务注册与发现闭环。
一、为什么需要服务注册与发现
1. 静态配置的痛点
假设有一个订单服务需要调用用户服务,最直接的做法是把用户服务的地址写在配置文件里:
yaml
user_service:
host: 192.168.1.10
port: 8080这种做法在以下场景会立即崩溃:
- 容器化部署后,每次重启 IP 都可能变化。
- 用户服务扩容到 5 个实例,配置文件如何维护?
- 某个实例宕机,调用方如何感知并避开它?
- 跨机房部署,调用方如何选择最近的实例?
2. 服务发现的核心思路
引入一个注册中心(Registry),所有服务实例启动时把自己的地址注册上去,调用方从注册中心查询目标服务的可用实例列表,并在本地做负载均衡。
┌──────────┐ register ┌──────────────┐
│ Service A│ ──────────► │ Registry │
│ 1.2.3.4 │ ◄────────── │ (Consul/Etcd)│
└──────────┘ health └──────┬───────┘
│ query
┌──────────┐ ▼
│ Service B│ ◄────────────────────────────
│ 5.6.7.8 │ gets list of A instances
└──────────┘3. 客户端发现 vs 服务端发现
客户端发现:调用方直接查询注册中心,自己做负载均衡。代表实现:Netflix Ribbon、go-kit SD。
- 优点:少一跳网络,无单点;客户端可定制负载均衡策略。
- 缺点:客户端逻辑复杂,多语言要重复实现。
服务端发现:调用方请求一个固定的路由层(如网关、Service Mesh sidecar),由路由层查询注册中心并转发。代表实现:Kubernetes Service、Envoy。
- 优点:客户端无感知,多语言友好。
- 缺点:多一跳网络,路由层需高可用。
本篇聚焦客户端发现模式,使用 Consul 和 Etcd 实战。
二、Consul 实战
1. Consul 简介
Consul 是 HashiCorp 开源的服务网格解决方案,核心能力包括:
- 服务发现:服务注册、DNS/HTTP 查询。
- 健康检查:HTTP、TCP、Script、TTL 多种检查方式。
- KV 存储:分布式键值存储,可用于配置中心。
- 多数据中心:原生支持多数据中心 federation。
- Raft 一致性:强一致性的集群协议。
2. 安装 Consul
最简单的方式是使用 Docker:
bash
docker run -d --name consul \
-p 8500:8500 -p 8600:8600/udp \
consul:1.15 agent -dev -client=0.0.0.0启动后访问 http://localhost:8500 即可看到 Consul Web UI。
也可以下载二进制直接运行:
bash
consul agent -dev -client=0.0.0.03. 注册服务(HTTP API)
Consul 提供了 HTTP API,可以直接注册服务:
bash
curl -X PUT http://localhost:8500/v1/agent/service/register -d '{
"Name": "user-service",
"ID": "user-1",
"Address": "127.0.0.1",
"Port": 8080,
"Check": {
"HTTP": "http://127.0.0.1:8080/health",
"Interval": "10s"
}
}'4. Go SDK 注册服务
实际项目里我们通常使用官方 Go SDK:github.com/hashicorp/consul/api。
下面是一个完整的服务端注册示例:
go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/hashicorp/consul/api"
)
// ConsulRegistrar 封装 Consul 注册逻辑
type ConsulRegistrar struct {
client *api.Client
id string
}
func NewConsulRegistrar(addr string) (*ConsulRegistrar, error) {
cfg := api.DefaultConfig()
cfg.Address = addr
client, err := api.NewClient(cfg)
if err != nil {
return nil, err
}
return &ConsulRegistrar{client: client}, nil
}
func (r *ConsulRegistrar) Register(serviceName, serviceID, address string, port int) error {
r.id = serviceID
reg := &api.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: address,
Port: port,
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", address, port),
Interval: "10s",
Timeout: "3s",
},
}
return r.client.Agent().ServiceRegister(reg)
}
func (r *ConsulRegistrar) Deregister() error {
return r.client.Agent().ServiceDeregister(r.id)
}
func main() {
consulAddr := "127.0.0.1:8500"
serviceName := "user-service"
serviceID := "user-service-1"
host := "127.0.0.1"
port := 8080
registrar, err := NewConsulRegistrar(consulAddr)
if err != nil {
log.Fatalf("new consul registrar: %v", err)
}
if err := registrar.Register(serviceName, serviceID, host, port); err != nil {
log.Fatalf("register service: %v", err)
}
defer registrar.Deregister()
log.Println("service registered to consul")
// 启动 HTTP 服务
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"id":1,"name":"alice"}`))
})
srv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: mux}
go func() {
log.Printf("http server on :%d", port)
if err := srv.ListenAndServe(); err != nil && err.Error() != "http: Server closed" {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}5. 客户端发现
调用方从 Consul 拉取服务实例列表,并在本地做负载均衡:
go
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"time"
"github.com/hashicorp/consul/api"
)
// ServiceDiscovery 基于 Consul 的服务发现
type ServiceDiscovery struct {
client *api.Client
}
func NewServiceDiscovery(consulAddr string) (*ServiceDiscovery, error) {
cfg := api.DefaultConfig()
cfg.Address = consulAddr
c, err := api.NewClient(cfg)
if err != nil {
return nil, err
}
return &ServiceDiscovery{client: c}, nil
}
// HealthyInstances 返回某个服务的健康实例
func (sd *ServiceDiscovery) HealthyInstances(serviceName string) ([]*api.ServiceEntry, error) {
services, _, err := sd.client.Health().Service(serviceName, "", true, nil)
if err != nil {
return nil, err
}
return services, nil
}
// Pick 随机选一个实例(最简单的负载均衡)
func (sd *ServiceDiscovery) Pick(serviceName string) (*api.ServiceEntry, error) {
instances, err := sd.HealthyInstances(serviceName)
if err != nil {
return nil, err
}
if len(instances) == 0 {
return nil, fmt.Errorf("no instance for %s", serviceName)
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return instances[r.Intn(len(instances))], nil
}
func main() {
sd, err := NewServiceDiscovery("127.0.0.1:8500")
if err != nil {
log.Fatal(err)
}
// 模拟客户端调用:每次随机选一个实例
for i := 0; i < 5; i++ {
inst, err := sd.Pick("user-service")
if err != nil {
log.Printf("pick failed: %v", err)
continue
}
addr := fmt.Sprintf("http://%s:%d/users/1", inst.Service.Address, inst.Service.Port)
log.Printf("calling instance: %s:%d", inst.Service.Address, inst.Service.Port)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("call failed: %v", err)
cancel()
continue
}
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
cancel()
var u map[string]interface{}
_ = json.Unmarshal(body, &u)
fmt.Printf("response: %+v\n", u)
}
}6. 健康检查机制
Consul 的健康检查有几种类型:
- HTTP 检查:定期 GET 一个 URL,2xx 视为健康。
- TCP 检查:尝试建立 TCP 连接,成功即视为健康。
- Script 检查:执行脚本,根据退出码判断(生产环境慎用,有安全风险)。
- TTL 检查:服务主动上报「我还活着」,超时未上报视为不健康。
- Docker 检查:通过 Docker daemon 检查容器状态。
TTL 检查示例(适合不想暴露健康端口的服务):
go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/hashicorp/consul/api"
)
func startTTLHeartbeat(client *api.Client, serviceID, checkID string, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
err := client.Agent().UpdateTTL(checkID, "alive", "pass")
cancel()
if err != nil {
log.Printf("ttl update failed: %v", err)
}
_ = ctx
}
}()
}
func main() {
cfg := api.DefaultConfig()
cfg.Address = "127.0.0.1:8500"
client, err := api.NewClient(cfg)
if err != nil {
log.Fatal(err)
}
serviceID := "user-service-2"
checkID := "user-service-2-ttl"
reg := &api.AgentServiceRegistration{
ID: serviceID,
Name: "user-service",
Port: 8081,
Check: &api.AgentServiceCheck{
TTL: "30s",
},
}
if err := client.Agent().ServiceRegister(reg); err != nil {
log.Fatal(err)
}
defer client.Agent().ServiceDeregister(serviceID)
startTTLHeartbeat(client, serviceID, checkID, 10*time.Second)
fmt.Println("service with TTL check started")
time.Sleep(2 * time.Minute)
}三、Etcd 实战
1. Etcd 简介
Etcd 是 CoreOS 开发的分布式 KV 存储,使用 Raft 协议保证强一致性。它是 Kubernetes 的核心组件之一,也常被用作服务注册中心。
Etcd 的特点是:
- 简单:gRPC 接口,KV 模型清晰。
- 安全:支持 TLS、RBAC。
- 快速: Benchmarks 显示在多节点集群上每秒可处理上万次写入。
- 可靠:Raft 协议保证强一致性。
2. 启动 Etcd
Docker 启动单节点:
bash
docker run -d --name etcd \
-p 2379:2379 -p 2380:2380 \
gcr.io/etcd-development/etcd:v3.5 \
/usr/local/bin/etcd \
--name s1 \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://0.0.0.0:2379 \
--listen-peer-urls http://0.0.0.0:2380 \
--initial-advertise-peer-urls http://0.0.0.0:2380 \
--initial-cluster s1=http://0.0.0.0:2380 \
--initial-cluster-token tkn \
--initial-cluster-state new3. 基础 KV 操作
使用 go.etcd.io/etcd/client/v3:
go
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ctx := context.Background()
// Put
_, err = cli.Put(ctx, "/foo", "bar")
if err != nil {
log.Fatal(err)
}
// Get
resp, err := cli.Get(ctx, "/foo")
if err != nil {
log.Fatal(err)
}
for _, ev := range resp.Kvs {
fmt.Printf("key=%s value=%s\n", ev.Key, ev.Value)
}
// Get with prefix
_, _ = cli.Put(ctx, "/users/1", "alice")
_, _ = cli.Put(ctx, "/users/2", "bob")
r2, _ := cli.Get(ctx, "/users/", clientv3.WithPrefix())
for _, ev := range r2.Kvs {
fmt.Printf("prefix key=%s value=%s\n", ev.Key, ev.Value)
}
}4. 基于租约(Lease)的服务注册
Etcd 的 Lease 机制非常适合服务注册:服务注册时绑定一个 Lease,并定期发送 KeepAlive 续约。一旦服务进程崩溃停止续约,Lease 自动过期,对应的 key 会被删除,调用方就能感知到。
go
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// EtcdRegistrar 基于 Etcd Lease 的服务注册
type EtcdRegistrar struct {
cli *clientv3.Client
leaseID clientv3.LeaseID
key string
}
func NewEtcdRegistrar(endpoints []string) (*EtcdRegistrar, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
return &EtcdRegistrar{cli: cli}, nil
}
func (r *EtcdRegistrar) Register(serviceName, instanceAddr string, ttl int64) error {
ctx := context.Background()
// 申请租约
lease, err := r.cli.Grant(ctx, ttl)
if err != nil {
return err
}
r.leaseID = lease.ID
r.key = fmt.Sprintf("/services/%s/%s", serviceName, instanceAddr)
// 写入 key 并绑定租约
_, err = r.cli.Put(ctx, r.key, instanceAddr, clientv3.WithLease(lease.ID))
if err != nil {
return err
}
// 自动续约
ch, err := r.cli.KeepAlive(context.Background(), lease.ID)
if err != nil {
return err
}
go func() {
for range ch {
// 续约响应,可记录日志
}
}()
return nil
}
func (r *EtcdRegistrar) Deregister() error {
_, err := r.cli.Delete(context.Background(), r.key)
return err
}
func main() {
r, err := NewEtcdRegistrar([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
if err := r.Register("user-service", "127.0.0.1:8080", 10); err != nil {
log.Fatal(err)
}
defer r.Deregister()
fmt.Println("registered to etcd")
time.Sleep(5 * time.Minute)
}5. 基于 Watch 的服务发现
调用方通过 Watch 监听某个服务前缀下所有 key 的变化,本地维护一份实时实例列表:
go
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// EtcdDiscovery 基于 Etcd Watch 的服务发现
type EtcdDiscovery struct {
cli *clientv3.Client
prefix string
mu sync.RWMutex
instances []string
}
func NewEtcdDiscovery(endpoints []string, prefix string) (*EtcdDiscovery, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
d := &EtcdDiscovery{cli: cli, prefix: prefix}
if err := d.loadInitial(); err != nil {
return nil, err
}
go d.watch()
return d, nil
}
func (d *EtcdDiscovery) loadInitial() error {
resp, err := d.cli.Get(context.Background(), d.prefix, clientv3.WithPrefix())
if err != nil {
return err
}
d.mu.Lock()
for _, ev := range resp.Kvs {
d.instances = append(d.instances, string(ev.Value))
}
d.mu.Unlock()
return nil
}
func (d *EtcdDiscovery) watch() {
rch := d.cli.Watch(context.Background(), d.prefix, clientv3.WithPrefix())
for wresp := range rch {
for _, ev := range wresp.Events {
d.mu.Lock()
switch ev.Type {
case 0: // PUT
d.instances = append(d.instances, string(ev.Kv.Value))
case 1: // DELETE
val := string(ev.PrevKv.Value)
for i, v := range d.instances {
if v == val {
d.instances = append(d.instances[:i], d.instances[i+1:]...)
break
}
}
}
d.mu.Unlock()
}
}
}
func (d *EtcdDiscovery) List() []string {
d.mu.RLock()
defer d.mu.RUnlock()
out := make([]string, len(d.instances))
copy(out, d.instances)
return out
}
func main() {
d, err := NewEtcdDiscovery([]string{"127.0.0.1:2379"}, "/services/user-service/")
if err != nil {
log.Fatal(err)
}
defer d.cli.Close()
for i := 0; i < 10; i++ {
fmt.Printf("instances: %v\n", d.List())
time.Sleep(3 * time.Second)
}
}四、客户端负载均衡
1. 常见负载均衡策略
- 随机(Random):从列表中随机选一个,简单但可能不均。
- 轮询(Round Robin):按顺序循环选择,请求量均衡。
- 加权轮询(Weighted RR):根据实例配置权重分配。
- 最少连接(Least Connections):选择当前活跃请求数最少的实例。
- 一致性哈希(Consistent Hash):相同请求特征路由到同一实例,便于缓存。
2. 实现一个加权轮询负载均衡器
go
package main
import (
"fmt"
"sync"
)
// Instance 服务实例
type Instance struct {
Addr string
Weight int
}
// WeightedRoundRobin 加权轮询负载均衡器(平滑轮询算法)
type WeightedRoundRobin struct {
mu sync.Mutex
instances []*Instance
weights []int // 当前权重
}
func NewWeightedRoundRobin(instances []*Instance) *WeightedRoundRobin {
w := make([]int, len(instances))
return &WeightedRoundRobin{instances: instances, weights: w}
}
func (lb *WeightedRoundRobin) Next() *Instance {
lb.mu.Lock()
defer lb.mu.Unlock()
if len(lb.instances) == 0 {
return nil
}
total := 0
var best int
for i, ins := range lb.instances {
lb.weights[i] += ins.Weight
total += ins.Weight
if i == 0 || lb.weights[i] > lb.weights[best] {
best = i
}
}
lb.weights[best] -= total
return lb.instances[best]
}
func main() {
instances := []*Instance{
{Addr: "10.0.0.1:8080", Weight: 5},
{Addr: "10.0.0.2:8080", Weight: 1},
{Addr: "10.0.0.3:8080", Weight: 1},
}
lb := NewWeightedRoundRobin(instances)
counts := map[string]int{}
for i := 0; i < 70; i++ {
ins := lb.Next()
counts[ins.Addr]++
}
for addr, c := range counts {
fmt.Printf("%s -> %d\n", addr, c)
}
}五、go-kit 的服务发现集成
go-kit 是一个 Go 微服务工具集,它的 sd(service discovery)子包提供了对 Consul、Etcd、Zookeeper 等多个注册中心的统一封装。
下面是一个使用 go-kit + Consul 的简化示例骨架:
go
package main
import (
"context"
"fmt"
"log"
"time"
consulsd "github.com/go-kit/kit/sd/consul"
"github.com/hashicorp/consul/api"
)
func main() {
consulConfig := api.DefaultConfig()
consulConfig.Address = "127.0.0.1:8500"
consulClient, err := api.NewClient(consulConfig)
if err != nil {
log.Fatal(err)
}
client := consulsd.NewClient(consulClient)
// 服务注册
registrar := consulsd.NewRegistrar(client, &consulsd.ServiceRegistration{
Name: "user-service",
ID: "user-1",
Address: "127.0.0.1",
Port: 8080,
Tags: []string{"go", "user"},
}, log.New(log.Writer(), "registrar: ", log.LstdFlags))
registrar.Register()
defer registrar.Deregister()
// 服务发现(订阅者)
// 真实项目里需要传入 instancer + endpointer,这里仅演示骨架
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fmt.Println("service registered via go-kit")
<-ctx.Done()
}go-kit 的优势在于:把服务发现、负载均衡、重试、熔断等横切关注点抽象为统一的接口,切换注册中心只需替换 SD 实现,业务代码无需改动。
六、完整示例:服务端注册 + 客户端发现
最后我们用一个完整可运行的例子把本篇内容串起来。这个例子包含两个独立的 main 程序:
服务端(保存为 server/main.go):
go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/hashicorp/consul/api"
)
func main() {
port := 8080
if len(os.Args) > 1 {
fmt.Sscanf(os.Args[1], "%d", &port)
}
serviceID := fmt.Sprintf("user-service-%d", port)
// 注册到 Consul
cfg := api.DefaultConfig()
cfg.Address = "127.0.0.1:8500"
consul, err := api.NewClient(cfg)
if err != nil {
log.Fatal(err)
}
reg := &api.AgentServiceRegistration{
ID: serviceID,
Name: "user-service",
Address: "127.0.0.1",
Port: port,
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://127.0.0.1:%d/health", port),
Interval: "5s",
Timeout: "2s",
},
}
if err := consul.Agent().ServiceRegister(reg); err != nil {
log.Fatal(err)
}
defer consul.Agent().ServiceDeregister(serviceID)
log.Printf("registered as %s on :%d", serviceID, port)
// 启动 HTTP
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/users/1", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(fmt.Sprintf(`{"id":1,"name":"alice","port":%d}`, port)))
})
srv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err.Error() != "http: Server closed" {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
log.Println("server exited")
}客户端(保存为 client/main.go):
go
package main
import (
"context"
"fmt"
"log"
"math/rand"
"net/http"
"time"
"github.com/hashicorp/consul/api"
)
func main() {
cfg := api.DefaultConfig()
cfg.Address = "127.0.0.1:8500"
consul, err := api.NewClient(cfg)
if err != nil {
log.Fatal(err)
}
for i := 0; i < 10; i++ {
instances, _, err := consul.Health().Service("user-service", "", true, nil)
if err != nil {
log.Printf("discover failed: %v", err)
continue
}
if len(instances) == 0 {
log.Println("no healthy instance")
continue
}
// 随机选一个实例
ins := instances[rand.Intn(len(instances))]
addr := fmt.Sprintf("http://%s:%d/users/1",
ins.Service.Address, ins.Service.Port)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("call failed: %v", err)
cancel()
continue
}
cancel()
log.Printf("call %s -> %d", addr, resp.StatusCode)
_ = resp.Body.Close()
time.Sleep(1 * time.Second)
}
fmt.Println("done")
}启动方式:
bash
# 终端1:启动 Consul
consul agent -dev -client=0.0.0.0
# 终端2、3、4:启动 3 个服务实例
go run server/main.go 8080
go run server/main.go 8081
go run server/main.go 8082
# 终端5:运行客户端
go run client/main.go你会看到客户端的请求被分散到三个实例上,证明服务发现和负载均衡都在工作。
七、小结
本篇我们学习了微服务架构中最基础也最重要的能力——服务注册与发现:
- 核心思路:通过一个注册中心集中维护服务实例列表,调用方动态查询并负载均衡。
- 两种模式:客户端发现(调用方自己查询)和服务端发现(路由层查询转发)。
- Consul:提供 HTTP API、Web UI、多类型健康检查、KV 存储,是开箱即用的方案。Go SDK 通过
github.com/hashicorp/consul/api使用。 - Etcd:基于 Raft 的强一致性 KV,配合 Lease + Watch 实现服务注册与发现,是 Kubernetes 的核心组件。
- 健康检查:HTTP / TCP / TTL 多种方式,保证下线实例能被及时剔除。
- 客户端负载均衡:随机、轮询、加权轮询、最少连接、一致性哈希等策略。
- go-kit sd:抽象了多注册中心的差异,便于切换。
下一篇我们将进入 gRPC,学习如何用 Protobuf 定义服务契约、生成 Go 代码,并实现四种调用模式(一元、服务端流、客户端流、双向流)。gRPC 的高性能和强类型契约,是构建大规模 Go 微服务通信的首选。
延伸阅读:
- Consul 官方文档:https://developer.hashicorp.com/consul/docs
- Etcd 官方文档:https://etcd.io/docs/
- go-kit 服务发现:https://pkg.go.dev/github.com/go-kit/kit/sd