「容器管理系统」开发篇:9. ETCD 的应用

回顾

项目已开源:基于 Golang 的 容器管理系统

前言

在开发篇第 7 章咱们已经讲了:

  • ETCD 的由来,历史背景
  • 为什么要用ETCD?
  • ETCD 的核心竞争力
  • ETCD V2 和 V3 版本的区别
  • ETCD 与其他同类型产品的优缺点

这篇咱们就讲讲

  • 如何使用 Golang 调用 ETCD ?
  • 如何应用在实战项目中?
  • 如何对注册后的服务进行数据库记录?
  • 如何后续对 ETCD 的操作并同步到数据库中?

安装 ETCD

linux 采取源码安装

sh 复制代码
# 拉取最新的 etcd 源码包
curl -l https://github.com/etcd-io/etcd/releases/download/v3.5.11/etcd-v3.5.11-linux-s390x.tar.gz
# 解压
tar -xzvf etcd-v3.5.11-linux-s390x.tar.gz
# 进入解压后的目录
cd etcd-v3.5.11-linux-s390x
# 执行文件
./etcd --version
./etcdctl --version

mac 采取 Homebrew 安装

sh 复制代码
# 安装 etcd
brew install etcd
# 启动 etcd
brew services start etcd
# 查看 etcd 版本
etcdctl --version

docker 安装

sh 复制代码
# 拉取 etcd 镜像
docker pull gcr.io/etcd-development/etcd:v3.5.11
# 运行 etcd 镜像
docker run \
  -p 2379:2379 \
  -p 2380:2380 \
  --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \
  --name etcd-gcr-v3.5.11 \
  gcr.io/etcd-development/etcd:v3.5.11 \
  /usr/local/bin/etcd \
  --name s1 \
  --data-dir /etcd-data \
  --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 new \
  --log-level info \
  --logger zap \
  --log-outputs stderr

Dockerfile 安装

sh 复制代码
curl -l https://github.com/etcd-io/etcd/releases/download/v3.5.11/etcd-v3.5.11-linux-s390x.tar.gz
# 解压
tar -xzvf etcd-v3.5.11-linux-s390x.tar.gz
# 进入解压后的目录
cd etcd-v3.5.11-linux-s390x
# 创建 Dockerfile 文件
touch Dockerfile
  • 写入以下内容:
dockerfile 复制代码
FROM alpine:latest
ADD etcd /usr/local/bin/
ADD etcdctl /usr/local/bin/
ADD etcdutl /usr/local/bin/
RUN mkdir -p /var/etcd/
RUN mkdir -p /var/lib/etcd/
RUN chmod -R 755 /usr/local/bin
EXPOSE 2379 2380
CMD ["/usr/local/bin/etcd"]
  • 打包镜像
sh 复制代码
docker build . -t etcd:v3.5.11
  • 运行镜像
sh 复制代码
# 运行 etcd 镜像
docker run \
  -p 2379:2379 \
  -p 2380:2380 \
  --name etcd-gcr-v3.5.11 \
  etcd:v3.5.11 \
  --name s1 \
  --data-dir /etcd-data \
  --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 new \
  --log-level info \
  --logger zap \
  --log-outputs stderr

docker-compose 安装

yaml 复制代码
 # etcd 服务
etcd:
    image: gcr.io/etcd-development/etcd:v3.5.11
    restart: always
    hostname: etcd
    container_name: etcd
    privileged: true
    ports:
        - 20079:2379
        - 20080:2380
    # 环境变量 --env
    environment:
        - ALLOW_NONE_AUTHENTICATION=yes
        - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
        - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
        - ETCD_DATA_DIR=/var/lib/etcd
    volumes:
      # 映射宿主机储存目录
      - ./server/etcd/data:/var/lib/etcd
    #设置容器启动命令 
    command: ["/usr/local/bin/etcd"]

ETCD 的 SDK

Golang 调用 ETCD 的 SDK:

sh 复制代码
go.etcd.io/etcd/api/v3 
  • 安装 ETCD 的 SDK
sh 复制代码
go get go.etcd.io/etcd/api/v3

安装完成之后会在 go.mod 文件中增加:

go 复制代码
go.etcd.io/etcd/api/v3 v3.5.10
go.etcd.io/etcd/client/v3 v3.5.10
go.etcd.io/etcd/client/pkg/v3 v3.5.10

到这一步,就可以在 Golang 项目中使用 ETCD 了

对 ETCD 的 SDK 简单的封装

  • 定义初始化链接客户端
go 复制代码
package etcd

import (
    "fmt"
    clientv3 "go.etcd.io/etcd/client/v3"
    "time"
)

type EtcdClient struct {
    Cli      *clientv3.Client
    KVCli    clientv3.KV
    LeaseCli clientv3.Lease   // 租约句柄
    LeaseID  clientv3.LeaseID // 租约ID
}

var etcdLocalMap = []string{"127.0.0.1:20079"}

var dialTimeout = 5 * time.Second

// NewClient 创建 etcd client 句柄
func NewClient() EtcdClient {
    cli, err := clientv3.New(clientv3.Config{
       Endpoints:            etcdLocalMap, 
       AutoSyncInterval:     0,            
       DialTimeout:          dialTimeout,  
       DialKeepAliveTime:    0,           
       DialKeepAliveTimeout: dialTimeout,  
       MaxCallSendMsgSize:   0,            
       MaxCallRecvMsgSize:   0,           
       TLS:                  nil,          // 设置客户端安全凭据(如果有的话)。
       Username:             "",           // 是用于身份验证的用户名。
       Password:             "",           // 是用于身份验证的密码。
       RejectOldCluster:     false,        // 当设置时,将拒绝针对过时的集群创建客户端。默认为不拒绝
       DialOptions:          nil,          
       Context:              nil,          
       Logger:               nil,          // 设置客户端日志,如果为空,默认使用 LogConfig
       LogConfig:            nil,          // 配置客户端日志,如果为零,则使用默认记录器
       PermitWithoutStream:  false,        
    })

    if err != nil {
       panic(err)
    }

    fmt.Printf("[etcd] connect %#v to success... \r\n", etcdLocalMap)

    return EtcdClient{
       Cli:      cli,
       KVCli:    clientv3.NewKV(cli),
       LeaseCli: clientv3.NewLease(cli),
    }
}
  • 定义注册服务和查看服务
go 复制代码
package etcd

import (
    "context"
    "go.etcd.io/etcd/api/v3/mvccpb"
    "time"
)

// PutService 注册服务
func (e *EtcdClient) PutService(key, val string) error {
    ctx, cancel := context.WithTimeout(e.Cli.Ctx(), 2*time.Second)
    _, err := e.KVCli.Put(ctx, key, val)
    cancel()
    return err
}

// DelService 删除服务
func (e *EtcdClient) DelService(key string) error {
    ctx, cancel := context.WithTimeout(e.Cli.Ctx(), 2*time.Second)
    _, err := e.KVCli.Delete(ctx, key)
    cancel()
    return err
}

// GetService 服务发现
func (e *EtcdClient) GetService(key string) ([]*mvccpb.KeyValue, error) {
    ctx, cancel := context.WithTimeout(e.Cli.Ctx(), time.Second)
    resp, err := e.KVCli.Get(ctx, key)
    cancel()
    if err != nil {
       return nil, err
    }
    if len(resp.Kvs) > 0 {
       return resp.Kvs, nil
    }
    return nil, err
}
  • 定义数据库存储
go 复制代码
type CloudEtcd struct {
    common.Model
    Name       string `json:"name" gorm:"size:200;not null;uniqueIndex;default:'';comment:注册服务名"`
    Remark     string `json:"remark" gorm:"size:255;not null;index;default:'';comment:备注"`
    Content    string `json:"content" gorm:"text;not null;index;default:'';comment:注册内容"`
    IsSub      uint32 `json:"is_sub" gorm:"not null;index;default:0;comment:是否订阅"`
    SubUserID  string `json:"sub_user_id" gorm:"text;not null;index;default:'';comment:订阅的用户ID"`
    IsDelete   uint32 `json:"is_delete" gorm:"not null;index;default:0;comment:是否软删除"`
    IsRegister uint32 `json:"is_register" gorm:"not null;index;default:0;comment:是否注册成功"`
    common.ControlBy
    common.ModelTime
}

func (c CloudEtcd) TableName() string {
    return "cloud_etcd"
}

func (c *CloudEtcd) ParseFields(p any) *CloudEtcd {
    if p == nil {
       return c
    }
    pjson, err := json.Marshal(p)
    if err != nil {
       return c
    }

    err = json.Unmarshal(pjson, c)
    if err != nil {
       return c
    }
    return c
}

使用 ETCD 的 SDK 进行服务注册发现

  • 包初始化 ETCD 全局句柄
go 复制代码
var etcdClient etcd.EtcdClient

func init() {
    etcdClient = etcd.NewClient()
}
  • 服务注册并入库记录
go 复制代码
func CreatePut(c *gin.Context) {
    var params common.EtcdRequest
    if err := c.ShouldBindJSON(&params); err != nil {
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    var cloudEtcd models.CloudEtcd
    // 验证roleKey标识,唯一
    var count int64
    err := db.D().Model(cloudEtcd).Where("name = ? and is_delete = ?", params.Name, 0).Count(&count).Error
    if err != nil {
       response.Error(c, constant.ErrorDB, err, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    if count > 0 {
       response.Error(c, constant.ErrorDBRecordExist, nil, constant.ErrorMsg[constant.ErrorDBRecordExist])
       return
    }
    auth, err := base.GetAuth(c)
    if err != nil {
       response.Error(c, constant.ErrorNotLogin, err, constant.ErrorMsg[constant.ErrorNotLogin])
       return
    }
    // etcd 注册
    err = etcdClient.PutService(params.Name, params.Content)
    if err != nil {
       xlog.Error(traceId.GetLogContext(c, "etcd put service fail, err: ", logz.F("err", err)))
       response.Error(c, constant.ErrorEtcd, err, constant.ErrorMsg[constant.ErrorEtcd])
       return
    }
    // 入库
    cloudEtcd.ParseFields(params)
    cloudEtcd.SetCreateBy(uint32(auth.UID))
    cloudEtcd.CreateTime = uint32(time.Now().Unix())
    cloudEtcd.IsRegister = 1
    res := db.D().Create(&cloudEtcd)
    if res.RowsAffected == 0 || res.Error != nil {
       response.Error(c, constant.ErrorDB, err, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    response.OK(c, nil, constant.ErrorMsg[constant.Success])
}
  • 定义接口请求参数
go 复制代码
type EtcdRequest struct {
    ID        int32  `json:"id,omitempty"`
    Name      string `json:"name,omitempty"`
    Remark    string `json:"remark,omitempty"`
    Content   string `json:"content,omitempty"`
    IsSub     uint32 `json:"is_sub,omitempty"`
    SubUserID string `json:"sub_user_id,omitempty"`
}
  • 查看全部服务,未逻辑删除的数据

查询未逻辑删除的数据,注册失败的也会查询到

go 复制代码
func GetService(c *gin.Context) {
    var params common.SearchRequest
    if err := c.ShouldBindJSON(&params); err != nil {
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    selectFields := structs.ToTags(models.CloudEtcd{}, "json")
    var cloudEtcdResp []*models.CloudEtcd
    pageList := &pagination.Pagination{
       PageIndex: params.Page,
       PageSize:  params.PageSize,
    }
    err := db.D().Scopes(pagination.Paginate(
       cloudEtcdResp,
       pageList,
       db.D().Select(selectFields).Where("position(? in `name`)", params.SearchKey),
    )).Where("is_delete = ?", 0).Find(&cloudEtcdResp).Error
    if err != nil {
       response.Error(c, constant.ErrorDB, err, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    pageList.Rows = cloudEtcdResp
    response.OK(c, pageList, constant.ErrorMsg[constant.Success])
}
  • 删除服务注册

对 ETCD 进行服务删除,对入库记录进行软删除

go 复制代码
func DelService(c *gin.Context) {
    var params common.EtcdRequest
    if err := c.ShouldBindJSON(&params); err != nil {
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    var cloudEtcd models.CloudEtcd
    cloudEtcd.ParseFields(params)
    err := db.D().Save(cloudEtcd).Error
    if err != nil {
       response.Error(c, constant.ErrorDB, err, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    // 删除 etcd 注册的 key
    err = etcdClient.DelService(params.Name)
    if err != nil {
       xlog.Error(traceId.GetLogContext(c, "etcd del service fail, err: ", logz.F("err", err)))
    }
    response.OK(c, nil, constant.ErrorMsg[constant.Success])
}
  • 手动注册 / 撤销重新注册
go 复制代码
// 手动注册 / 撤销重新注册
func PutService(c *gin.Context) {
    var params common.EtcdRequest
    if err := c.ShouldBindJSON(&params); err != nil {
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    var cloudEtcd models.CloudEtcd
    cloudEtcd.ParseFields(params)
    cloudEtcd.IsRegister = 1
    // etcd 注册
    err := etcdClient.PutService(params.Name, params.Content)
    if err != nil {
       xlog.Error(traceId.GetLogContext(c, "etcd put service fail, err: ", logz.F("err", err)))
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    cloudEtcd.IsRegister = 1
    // 更新
    res := db.D().Save(cloudEtcd)
    if res.RowsAffected == 0 || res.Error != nil {
       response.Error(c, constant.ErrorDB, err, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    response.OK(c, nil, constant.ErrorMsg[constant.Success])
}

扩展:ETCD 服务间的订阅

服务注册之后,在数据库层面是独立的存在,订阅功能是为了在同一个接口中使用多个注册服务的场景

go 复制代码
// 服务订阅
func SubscribeService(c *gin.Context) {
    var params common.EtcdRequest
    if err := c.ShouldBindJSON(&params); err != nil {
       response.Error(c, constant.ErrorParams, err, constant.ErrorMsg[constant.ErrorParams])
       return
    }
    var cloudEtcd models.CloudEtcd
    cloudEtcd.ParseFields(params)
    // 更新
    res := db.D().Save(cloudEtcd)
    if res.RowsAffected == 0 || res.Error != nil {
       response.Error(c, constant.ErrorDB, res.Error, constant.ErrorMsg[constant.ErrorDB])
       return
    }
    response.OK(c, nil, constant.ErrorMsg[constant.Success])
}

结束语

本章节主要内容围绕着 ETCD 的 SDK 的使用来说明,讲述了,如果使用 SDK,并应用到项目中的一些方案和应用。

  • 采取了:ETCD 注册后入库记录方案
  • 对 ETCD 操作后同步数据库记录
  • 扩展服务的订阅功能
相关推荐
minsin27 分钟前
【linux】【docker】Docker默认网段配置导致无法访问
docker
悲伤的创可贴2 小时前
Docker安装以及简单使用
linux·docker·centos
方圆师兄3 小时前
docker快速搭建kafka
docker·容器·kafka
小的~~4 小时前
k8s使用本地docker私服启动自制的flink集群
docker·flink·kubernetes
诚诚k4 小时前
docker存储
运维·docker·容器
sorel_ferris4 小时前
Ubuntu-24.04中Docker-Desktop无法启动
linux·ubuntu·docker
多多*5 小时前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
NiNg_1_2346 小时前
使用Docker Compose一键部署
运维·docker·容器
萠哥啥都行6 小时前
Linux安装Docker以及Docker入门操作
运维·docker·容器
王哲晓6 小时前
Linux通过yum安装Docker
java·linux·docker