ETCD搭建及应用

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:如果对你又帮助,麻烦点个赞,加个关注
集群 等机子资源够了,再搞个

猜你喜欢

转载自blog.csdn.net/yunteng521/article/details/146568063