ETCD 是一个高可用的分布式键值key-value数据库,可用于服务发现。
ETCD 采用raft 一致性算法,基于 Go语言实现。
ETCD作为一个高可用键值存储系统,天生就是为集群化而设计的。由于Raft算法在做决策时需要多数节点的投票,所以etcd一般部署集群推荐奇数个节点(3,5,7) ,一般用3个够了;
5个节点:ETCD 官方推荐至少使用5个节点来保证大多数情况下的一致性和可用性。在GCP上,你可以使用5个节点来构建一个稳健的ETCD集群。
7个节点:对于更高的可用性和容错能力,可以考虑使用7个节点。这意味着即使有3个节点故障,集群仍然可以保持大多数节点在线
本章简述etcd 源码 编译部署测试
这里以单节点为例 (集群机子资源可能不够,够了再搭)
1:环境
ubuntu22.0.* (ETCD)
win11 (test)
go 1.24
etcd (3.5.20) downlaod:https://github.com/etcd-io/etcd/tags
2:编译安装
先安装好go, etcd (3.5.20) 要求go 1.23.* +
unzip 后 解压后的根目录
./build.sh
bin 里 有3个执行文件
3:启动
直接手动启动了,参数简单设置下
etcd --name "test1" --data-dir "./data" --listen-client-urls http://0.0.0.0:8080 --advertise-client-urls http://192.168.1.100:8080
--name 是节点名称,--data-dir 是数据存储目录,--listen-client-urls 是监听客户端连接的地址和端口,--advertise-client-urls 是向客户端通告的地址和端口。 没有中转的话,局域网 一般 两者相同,广域网 ,advertise-client-urls 换成公网IP 或 中转IP:PROT
4:demo测试
1> login 与 game login 需要关注 game ,
login 代码
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// HallService 代表 hall 服务
type HallService struct {
ServiceID string
Address string
}
// Register 方法用于将 hall 服务注册到 etcd 并启动定时续约
func (hs *HallService) Register(cli *clientv3.Client, leaseTTL int64) error {
ctx, cancel := context.WithCancel(context.Background())
// 创建租约
leaseResp, err := cli.Grant(ctx, leaseTTL)
if err != nil {
cancel()
return err
}
leaseID := leaseResp.ID
// 设置键值对,关联租约
key := fmt.Sprintf("hall/%s", hs.ServiceID)
_, err = cli.Put(ctx, key, hs.Address, clientv3.WithLease(leaseID))
if err != nil {
cancel()
return err
}
// 启动定时续约协程
go func() {
ticker := time.NewTicker(time.Duration(leaseTTL/2) * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 续约租约
_, err := cli.KeepAliveOnce(ctx, leaseID)
if err != nil {
log.Printf("Failed to renew lease for hall service %s: %v", hs.ServiceID, err)
} else {
log.Printf("Successfully renewed lease for hall service %s", hs.ServiceID)
}
case <-ctx.Done():
log.Printf("Lease renewal goroutine for hall service %s is stopped due to context cancellation", hs.ServiceID)
return
}
}
}()
return nil
}
func main() {
// etcd 向客户端公开的地址(--advertise-client-urls),客户端使用此地址连接
advertiseClientURLs := "http://192.168.1.100:8080"
// 配置 etcd 客户端,使用 --advertise-client-urls 指定的地址
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{
advertiseClientURLs},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalf("Failed to create etcd client: %v", err)
}
defer cli.Close()
// 创建 hall 服务实例
hallService := &HallService{
ServiceID: "login-001",
Address: "192.168.1.300:9999",
}
// 注册 hall 服务到 etcd
leaseTTL := int64(10) // 租约有效期 10 秒
err = hallService.Register(cli, leaseTTL)
if err != nil {
log.Fatalf("Failed to register hall service: %v", err)
}
fmt.Println("Hall service registered successfully.")
//得到之前的
resp, err1 := cli.Get(context.Background(), "game/", clientv3.WithPrefix())
if err1 != nil {
fmt.Println(err1)
return
} else {
for _, ev := range resp.Kvs {
fmt.Printf("%s : %s\n", ev.Key, ev.Value)
}
}
// 监听 game 服务相关的 key
gameKeyPrefix := "game/"
rch := cli.Watch(context.Background(), gameKeyPrefix, clientv3.WithPrefix())
fmt.Printf("Watching game services under key prefix: %s\n", gameKeyPrefix)
for wresp := range rch {
if wresp.Err() != nil {
log.Printf("Error watching game services: %v", wresp.Err())
continue
}
for _, ev := range wresp.Events {
switch ev.Type {
case clientv3.EventTypePut:
keyStr := string(ev.Kv.Key)
valueStr := string(ev.Kv.Value)
if len(keyStr) > len(gameKeyPrefix) && strings.HasSuffix(keyStr, "/health") {
gameID := keyStr[len(gameKeyPrefix) : strings.Index(keyStr[len(gameKeyPrefix):], "/")+len(gameKeyPrefix)]
fmt.Printf("Game service %s health updated to: %s\n", gameID, valueStr)
} else {
fmt.Printf("Game service added/updated: Key=%s, Value=%s\n", keyStr, valueStr)
}
case clientv3.EventTypeDelete:
fmt.Printf("Game service removed: Key=%s\n", string(ev.Kv.Key))
}
}
}
}
注意 cli.Watch 前面启动的game 是不知道,可以配合 get 使用 ,超时 金融交易系统建议 1 - 3 秒 ,游戏 1-5
续约 为 超时 1/3 -1/2 用定时器更新
game
package main
import (
"context"
"fmt"
"log"
"math"
"math/rand"
"os"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// GameService 代表 game 服务
type GameService struct {
ServiceID string
Address string
}
// ServerConfig 服务器配置
type ServerConfig struct {
MaxCPUUsage float64 // CPU 使用率阈值
MaxMemoryUsage float64 // 内存使用率阈值
MaxConnections int // 最大客户端连接数
CheckInterval time.Duration // 健康检查间隔
}
// ServerHealth 服务器健康信息
type ServerHealth struct {
CPUUsage float64
MemoryUsage float64
Connections int
HealthScore float64
}
// CalculateHealthScore 计算健康度得分
func CalculateHealthScore(health ServerHealth, config ServerConfig) float64 {
cpuScore := (config.MaxCPUUsage - health.CPUUsage) / config.MaxCPUUsage
memoryScore := (config.MaxMemoryUsage - health.MemoryUsage) / config.MaxMemoryUsage
connectionScore := float64(health.Connections) / float64(config.MaxConnections)
// 综合得分,可根据需求调整权重
healthScore := (cpuScore + memoryScore + connectionScore) / 3 * 100
return math.Max(0, math.Min(100, healthScore))
}
// 模拟获取客户端连接数的函数,实际中需要根据具体网络库实现
func getClientConnections() int {
// 这里简单返回一个模拟值,实际中需要替换为真实的连接数获取逻辑
return 50
}
// 模拟将客户端连接导向其他服务器的函数
func redirectClientToOtherServer() {
fmt.Println("Redirecting client to other server...")
}
// Register 方法用于将 game 服务注册到 etcd
func (gs *GameService) Register(cli *clientv3.Client, leaseTTL int64) error {
ctx, cancel := context.WithCancel(context.Background())
// 创建租约
leaseResp, err := cli.Grant(ctx, leaseTTL)
if err != nil {
cancel()
return err
}
leaseID := leaseResp.ID
// 设置键值对,关联租约
key := fmt.Sprintf("game/%s", gs.ServiceID)
_, err = cli.Put(ctx, key, gs.Address, clientv3.WithLease(leaseID))
if err != nil {
cancel()
return err
}
// 启动定时续约协程
go func() {
ticker := time.NewTicker(time.Duration(leaseTTL/2) * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 续约租约
_, err := cli.KeepAliveOnce(ctx, leaseID)
if err != nil {
log.Printf("Failed to renew lease for game service %s: %v", gs.ServiceID, err)
} else {
log.Printf("Successfully renewed lease for game service %s", gs.ServiceID)
}
case <-ctx.Done():
log.Printf("Lease renewal goroutine for game service %s is stopped due to context cancellation", gs.ServiceID)
return
}
}
}()
// 启动健康度上报协程
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 模拟健康度
health := rand.Intn(100)
healthKey := fmt.Sprintf("game/%s/health", gs.ServiceID)
_, err := cli.Put(ctx, healthKey, fmt.Sprintf("%d", health))
if err != nil {
log.Printf("Failed to report health for game service %s: %v", gs.ServiceID, err)
} else {
log.Printf("Reported health %d for game service %s", health, gs.ServiceID)
}
case <-ctx.Done():
log.Printf("Health report goroutine for game service %s is stopped due to context cancellation", gs.ServiceID)
return
}
}
}()
// 在程序退出时取消上下文
go func() {
<-context.Background().Done()
cancel()
}()
return nil
}
func main() {
serverid := "1"
switch len(os.Args) {
case 2:
serverid = os.Args[1]
}
// 服务器配置
//config := ServerConfig{
// MaxCPUUsage: 80,
// MaxMemoryUsage: 80,
// MaxConnections: 200,
// CheckInterval: 5 * time.Second,
//}
// etcd 向客户端公开的地址(--advertise-client-urls),客户端使用此地址连接
advertiseClientURLs := "http://192.168.1.100:8080"
// 配置 etcd 客户端,使用 --advertise-client-urls 指定的地址
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{
advertiseClientURLs},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalf("Failed to create etcd client: %v", err)
}
defer cli.Close()
// 创建 game 服务实例
serveridstr := fmt.Sprintf("game-%v", serverid)
Addressstr := fmt.Sprintf("192.168.1.20%v:8888", serverid)
gameService := &GameService{
//ServiceID: "game-001",
//Address: "192.168.1.200:8888",
ServiceID: serveridstr,
Address: Addressstr,
}
// 注册 game 服务到 etcd
leaseTTL := int64(10) // 租约有效期 10 秒
err = gameService.Register(cli, leaseTTL)
if err != nil {
log.Fatalf("Failed to register game service: %v", err)
}
fmt.Printf("Game service registered successfully(%v-%v)\n.", serveridstr, Addressstr)
// 保持程序运行
select {
}
}
mod 文件 两个度差不多,DEMO 没那么复杂
module game
go 1.24.0
require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.20 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.20 // indirect
go.etcd.io/etcd/client/v3 v3.5.20 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
运行结果
5:如果对你又帮助,麻烦点个赞,加个关注
集群 等机子资源够了,再搞个