回顾
项目已开源:基于 Golang 的 容器管理系统
- 「容器管理系统」1. 开篇:框架选型和环境搭建
- 「容器管理系统」开发篇:1. 初始化配置和日志监控
- 「容器管理系统」开发篇:2. 封装gin统一返回JSON
- 「容器管理系统」开发篇:3. JWT(JSON Web Token)的应用
- 「容器管理系统」开发篇:4. Gin 如何优雅的使用 struct 的 tag 标签
- 「容器管理系统」开发篇:5. 如何实现 RBAC 权限管理(一)
- 「容器管理系统」开发篇:5. 如何实现 RBAC 权限管理(二)
- 「容器管理系统」开发篇:6. 如何在项目中集成 casbin 策略授权库?
- 「容器管理系统」开发篇:7. 初识 ETCD
- 「容器管理系统」开发篇:8. docker 的应用
前言
在开发篇第 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:
shgo.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(¶ms); 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(¶ms); 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(¶ms); 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(¶ms); 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(¶ms); 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 操作后同步数据库记录
- 扩展服务的订阅功能