Golang Kitex 框架中,结合 ETCD 实现后端服务部分的部署、注册、发现以及客户端调用。包括每个步骤的详细代码实现
1. ETCD 部署与配置
首先,我们需要部署 ETCD 集群。在生产环境中,ETCD 通常部署在多个节点上,以保证高可用性和容错能力。这里,我们简单介绍如何在三台机器上部署一个 ETCD 集群。
1.1 安装 ETCD
在每台机器上下载和安装 ETCD:
# 下载 ETCD wget https://github.com/etcd-io/etcd/releases/download/v3.5.0/etcd-v3.5.0-linux-amd64.tar.gz # 解压 tar -xvzf etcd-v3.5.0-linux-amd64.tar.gz cd etcd-v3.5.0-linux-amd64 # 将 etcd 和 etcdctl 移动到系统路径 sudo mv etcd /usr/local/bin/ sudo mv etcdctl /usr/local/bin/
1.2 配置 ETCD 集群
在三台机器(假设为 192.168.1.1
, 192.168.1.2
, 192.168.1.3
)上分别编辑 /etc/etcd/etcd.conf.yml
配置文件:
Node 1 (192.168.1.1):
name: node1 data-dir: /var/lib/etcd listen-peer-urls: http://192.168.1.1:2380 listen-client-urls: http://192.168.1.1:2379,http://127.0.0.1:2379 advertise-client-urls: http://192.168.1.1:2379 initial-advertise-peer-urls: http://192.168.1.1:2380 initial-cluster: node1=http://192.168.1.1:2380,node2=http://192.168.1.2:2380,node3=http://192.168.1.3:2380 initial-cluster-token: etcd-cluster initial-cluster-state: new
Node 2 (192.168.1.2) 和 Node 3 (192.168.1.3) 中的配置文件仅需修改 name
和 IP 地址。
1.3 启动 ETCD 集群
在每台机器上启动 ETCD:
etcd --config-file=/etc/etcd/etcd.conf.yml
检查 ETCD 集群状态:
etcdctl --endpoints=http://192.168.1.1:2379 endpoint health
2. Kitex 服务端 - 注册服务到 ETCD
Kitex 是一个高效的微服务框架,它可以与 ETCD 进行集成,实现服务注册和发现。我们在服务端启动时将服务注册到 ETCD。
2.1 安装 Kitex 及 ETCD 客户端
首先,安装 Kitex 和 ETCD 客户端依赖:
go get github.com/cloudwego/kitex go get go.etcd.io/etcd/client/v3
2.2 创建 Kitex 服务端
在 main.go
中编写服务端的代码,完成 ETCD 服务注册:
package main import ( "context" "fmt" "log" "time" clientv3 "go.etcd.io/etcd/client/v3" "github.com/cloudwego/kitex/server" "github.com/cloudwego/kitex/server/registry/etcd" "user_service/kitex_gen/userservice" ) func registerService() { // 创建 ETCD 客户端 client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://192.168.1.1:2379", "http://192.168.1.2:2379", "http://192.168.1.3:2379"}, DialTimeout: 5 * time.Second, }) if err != nil { log.Fatalf("failed to connect to etcd: %v", err) } defer client.Close() // 注册服务到 ETCD serviceName := "/services/user-service/192.168.1.1:8888" _, err = client.Put(context.Background(), serviceName, "UserService instance at 192.168.1.1:8888") if err != nil { log.Fatalf("failed to register service: %v", err) } fmt.Println("Service registered successfully!") } func main() { // 注册服务到 ETCD registerService() // 创建并启动 Kitex 服务 svr := userservice.NewServer(&UserServiceImpl{}, server.WithServiceAddr(":8888"), server.WithRegistry(etcd.NewEtcdRegistry([]string{"http://192.168.1.1:2379"})), ) // 运行服务 err := svr.Run() if err != nil { fmt.Printf("Server failed to start: %v\n", err) } }
在 main.go
中,我们完成了以下工作:
服务注册到 ETCD:使用 etcd.Put() 将服务信息(包括地址)注册到 ETCD 中。
Kitex 服务器启动:通过 server.WithRegistry(etcd.NewEtcdRegistry(...)) 将 Kitex 服务与 ETCD 注册中心绑定,实现服务注册和发现。
3. Kitex 客户端 - 从 ETCD 获取服务地址并调用
客户端需要从 ETCD 获取服务实例的地址,并发起调用。
3.1 获取所有服务实例
首先,我们需要从 ETCD 中获取所有注册的服务实例地址。ETCD 存储的是以服务名为路径的键值对(key-value),值就是服务的实际地址。
func getAllServiceAddresses() []string { // 创建 ETCD 客户端 client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://192.168.1.1:2379", "http://192.168.1.2:2379", "http://192.168.1.3:2379"}, DialTimeout: 5 * time.Second, }) if err != nil { log.Fatalf("failed to connect to etcd: %v", err) } defer client.Close() // 获取服务实例 resp, err := client.Get(context.Background(), "/services/user-service/", clientv3.WithPrefix()) if err != nil { log.Fatalf("failed to get service instances: %v", err) } var addresses []string for _, kv := range resp.Kvs { // 提取注册的服务实例地址 addresses = append(addresses, string(kv.Value)) } return addresses }
在上面的代码中,client.Get()
使用了 clientv3.WithPrefix()
,这意味着它会获取以 /services/user-service/
为前缀的所有键值对,即所有注册的 user-service
服务实例的地址。
3.2 获取服务地址
客户端需要连接到 ETCD,并获取可用的服务实例地址。以下是获取服务地址的代码:
从 ETCD 获取服务实例:
package main import ( "context" "fmt" "go.etcd.io/etcd/client/v3" "log" "time" "user-service/kitex_gen/user" "user-service/kitex_gen/user/userservice" ) func getServiceAddress() string { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://192.168.1.1:2379", "http://192.168.1.2:2379", "http://192.168.1.3:2379"}, DialTimeout: 5 * time.Second, }) if err != nil { log.Fatalf("failed to connect to etcd: %v", err) } defer client.Close() // 获取注册的服务实例 resp, err := client.Get(context.Background(), "/services/user-service/", clientv3.WithPrefix()) if err != nil { log.Fatalf("failed to get services: %v", err) } if len(resp.Kvs) > 0 { fmt.Printf("Found service instance at: %s\n", resp.Kvs[0].Value) return string(resp.Kvs[0].Value) // 返回第一个服务实例的地址 } log.Fatal("No service instance found") return "" } func main() { // 获取服务实例地址 serviceAddress := getServiceAddress() // 创建 Kitex 客户端 client := userservice.MustNewClient("user-service", client.WithHostPorts(serviceAddress)) // 调用 GetUserInfo 接口 res, err := client.GetUserInfo(context.Background(), 123) if err != nil { log.Fatalf("failed to call service: %v", err) } fmt.Println("User Info:", res) }
3.3 负载均衡选择服务实例
假设我们从 ETCD 获取到了多个服务实例的地址,接下来我们需要实现一个简单的负载均衡策略。这里我们可以选择轮询策略(当然也可以使用其他的策略)。
3.2.1 轮询策略
func getServiceAddress(addresses []string) string { // 使用当前的时间戳来轮询服务实例 index := time.Now().UnixNano() % int64(len(addresses)) return addresses[index] }
该函数根据当前的时间戳选择一个服务实例,简单且高效。你也可以使用更复杂的负载均衡算法(比如最少连接、权重轮询等)。
3.4 创建 Kitex 客户端并调用服务
有了服务实例的地址后,客户端就可以创建 Kitex 客户端并调用服务接口。
func main() { // 从 ETCD 获取所有注册的服务实例 addresses := getAllServiceAddresses() if len(addresses) == 0 { log.Fatal("No service instances found") } // 轮询选择一个服务地址 serviceAddress := getServiceAddress(addresses) // 创建 Kitex 客户端,选择一个服务地址 client := userservice.MustNewClient("user-service", client.WithHostPorts(serviceAddress)) // 调用服务 res, err := client.GetUserInfo(context.Background(), 123) if err != nil { log.Fatalf("failed to call service: %v", err) } fmt.Println("User Info:", res) }
在这段代码中,客户端首先从 ETCD 获取所有可用的服务实例地址,并使用轮询算法选择一个地址。然后,它会使用 Kitex 提供的 MustNewClient
方法创建客户端实例,并通过该客户端调用服务的 GetUserInfo
方法。
4. 服务注册与发现的完整流程
4.1 服务端的注册流程(Kitex 与 ETCD)
服务端在启动时需要将自己的服务信息注册到 ETCD 中,这包括服务名、服务地址、端口等信息。每当服务端启动时,注册过程都会执行一次,这样客户端才能从 ETCD 中获取到最新的服务实例。
我们已经介绍了如何在服务端注册服务。现在,我们补充一些关于如何处理服务健康检查和超时处理的内容。
4.1.1 服务健康检查(可选)
为了确保客户端只调用健康的服务实例,你可以在服务注册时附带健康检查机制。这样,服务实例在 ETCD 中注册时会定期向 ETCD 发送心跳,表明它仍然可用。如果某个服务实例没有及时响应心跳,它将被标记为不健康,客户端就不会再调用该服务。
以下是如何使用 ETCD 的 TTL(生存时间)和 租约 来实现心跳检查:
func registerServiceWithTTL() { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://192.168.1.1:2379", "http://192.168.1.2:2379", "http://192.168.1.3:2379"}, DialTimeout: 5 * time.Second, }) if err != nil { log.Fatalf("failed to connect to etcd: %v", err) } defer client.Close() // 创建一个租约,设置 TTL 为 10 秒 leaseResp, err := client.Grant(context.Background(), 10) if err != nil { log.Fatalf("failed to create lease: %v", err) } // 使用租约注册服务,并为服务设置 TTL key := "/services/user-service/192.168.1.1:8888" value := "UserService instance at 192.168.1.1:8888" _, err = client.Put(context.Background(), key, value, clientv3.WithLease(leaseResp.ID)) if err != nil { log.Fatalf("failed to register service with TTL: %v", err) } fmt.Println("Service registered with TTL successfully!") // 定期续租,防止服务实例过期 go func() { for { _, err := client.KeepAliveOnce(context.Background(), leaseResp.ID) if err != nil { log.Printf("failed to renew lease: %v", err) break } time.Sleep(5 * time.Second) // 每 5 秒续租一次 } }() } func main() { // 注册服务,并启动健康检查 registerServiceWithTTL() // 启动服务 svr := userservice.NewServer(&UserServiceImpl{}) err := svr.Run() if err != nil { fmt.Printf("Server failed to start: %v\n", err) } }
这样,服务在 ETCD 中注册后,会定期续租以保持其健康状态。如果租约过期,ETCD 会自动删除该服务,客户端将不再收到该服务的地址。
4.2 客户端的服务发现与调用
客户端首先需要从 ETCD 获取服务实例的地址,然后才能进行调用。在生产环境中,通常需要处理多个服务实例的负载均衡。为了支持多个服务实例的负载均衡,客户端可以从 ETCD 获取所有注册的服务实例,并使用某种负载均衡策略(例如 轮询、随机、最少连接 等)来选择一个实例进行调用。
4.2.1 获取多个服务实例
客户端获取注册的服务实例地址时,可以通过 ETCD 查询所有实例。例如:
func getAllServiceAddresses() []string { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://192.168.1.1:2379", "http://192.168.1.2:2379", "http://192.168.1.3:2379"}, DialTimeout: 5 * time.Second, }) if err != nil { log.Fatalf("failed to connect to etcd: %v", err) } defer client.Close() // 获取所有注册的服务实例 resp, err := client.Get(context.Background(), "/services/user-service/", clientv3.WithPrefix()) if err != nil { log.Fatalf("failed to get service instances: %v", err) } var addresses []string for _, kv := range resp.Kvs { addresses = append(addresses, string(kv.Value)) } return addresses }
该函数返回所有注册的服务实例地址。
4.2.2 负载均衡
客户端可以采用负载均衡策略来从多个服务实例中选择一个进行调用。以下是一个简单的 轮询 策略:
func getServiceAddress(addresses []string) string { // 采用简单的轮询方式(假设客户端的请求数较少) index := time.Now().UnixNano() % int64(len(addresses)) return addresses[index] } func main() { // 从 ETCD 获取所有注册的服务实例 addresses := getAllServiceAddresses() if len(addresses) == 0 { log.Fatal("No service instances found") } // 轮询选择一个服务地址 serviceAddress := getServiceAddress(addresses) // 创建 Kitex 客户端 client := userservice.MustNewClient("user-service", client.WithHostPorts(serviceAddress)) res, err := client.GetUserInfo(context.Background(), 123) if err != nil { log.Fatalf("failed to call service: %v", err) } fmt.Println("User ID:", res) }
这种方式会根据时间戳和服务实例数量选择一个服务实例,简单而有效。
5. 多台服务器的集群与服务调用
通过上面的配置,当你在多台服务器上运行服务时,客户端可以从 ETCD 获取所有服务实例的地址,并通过负载均衡策略进行调用。假设你有多个服务实例部署在不同的服务器上,ETCD 作为统一的服务注册中心,它保证了服务的高可用性。
5.1 多台服务器部署示例
假设你的服务实例部署在 3 台服务器上,服务注册和发现的过程如下:
Server 1 上注册:/services/user-service/192.168.1.1:8888
Server 2 上注册:/services/user-service/192.168.1.2:8888
Server 3 上注册:/services/user-service/192.168.1.3:8888
客户端可以从 ETCD 获取到所有服务地址,并通过轮询、随机等方式调用某个服务。
5.2 动态扩缩容支持
如果需要在运行时动态增加或删除服务实例,可以直接在 ETCD 中注册新的服务实例,客户端会自动获取到最新的服务实例列表。通过这种方式,集群的规模可以动态调整,且客户端无需修改代码即可适应变化。
6. 健康检查(可选)
为了避免客户端调用不可用的服务实例,ETCD 提供了健康检查机制。通过定期续租,服务实例可以持续在 ETCD 中保持有效,失效的服务实例会被自动从 ETCD 中移除。客户端只会获取到健康的服务实例。
这种机制结合 Kitex 框架实现的微服务架构,可以大大提高系统的健壮性和可扩展性。
7. 总结
ETCD 集群部署:通过在多台服务器上部署 ETCD 节点形成集群,确保高可用性。
服务注册:服务端启动时将自身地址注册到 ETCD 中,客户端可以动态获取服务实例地址。
健康检查:使用 ETCD 的 TTL 和租约机制确保服务实例的健康性,避免客户端访问不可用的服务。
服务发现与负载均衡:客户端通过查询 ETCD 获取所有服务实例地址,并使用负载均衡策略选择一个实例进行调用。
扩展性与高可用:ETCD 提供了强一致性,能够保证服务注册信息的可靠性和一致性,适应分布式环境下的服务发现需求。
通过这种方式,你可以在分布式环境下实现微服务的高可用性、负载均衡和自动化服务发现,特别适合在多台服务器上部署的场景。
扩缩容的变化,而无需重启。
小结
通过上述步骤,我们实现了一个基于 ETCD 的微服务架构,其中包括:
服务注册到 ETCD
客户端从 ETCD 获取服务实例
负载均衡策略选择服务实例进行调用
动态扩容与缩容的自动适应
健康检查和服务失效机制
这些组件共同工作,可以保证在分布式环境下的服务发现、调用、负载均衡和高可用性。
最后
以上就是名字长了才好记最近收集整理的关于golang的Kitex框架微服务api接口后端ectd分布式部署和详细实现的全部内容,更多相关golang内容请搜索靠谱客的其他文章。
发表评论 取消回复