Go web 开发数据库管理平台,利用远程过程调用(RPC)实现对MySQL数据库的管理和使用

前言

做DBA,最基本的工作就是需要管理公司的数据库系统。工作中,常常需要维护的数据库数量是非常多的。小公司可能有个几十套,上百套。大一点的公司,甚至可能有上万的数据库实例。对于数据库,如果我们不通过开发或者购买些第三方管理平台,维护工作是一个非常繁重的事情。

因为这个初衷,笔者想开启数据库云平台开发这个项目。

借开发过程中的几个具体问题的思考和实现,分享一些常见的技术问题。

本文主要分享,在go语言中,如何使用grpc框架去开发对MySQL数据库的操作等相关功能。

rpc框架介绍

RPC是(Remote Procedure Call)的简称,英文直译过来就是"远程的过程调用",可以理解为有不同的两个运行操作系统的服务器A和服务器B,从服务器A可以调用服务器B的某些程序的方法,反过来B调用A也是一样的。那么,被调用的一方,我们称它为服务端,调用的一方,也就是命令请求方,我们称它为客户端。

如何实现rpc功能呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBLwTCww-1661345390797)(https://grpc.io/img/landing-2.svg)]

gRPC 是一个现代开源的高性能远程过程调用 (RPC) 框架,在微服务,移动通信,浏览器等场景中有丰富的应用。其丰富的开发语言支持,双向流式传输,身份验证,负载均衡和健康检查都是非常亮眼的功能。

在数据库领域,TiDB的源码中,便用到了大量的gRPC实现不同的功能节点之间的通信。

在众多的面试题中,有大佬也会去问gRPC跟HTTP协议的区别,以及为什么gRPC有更好的性能等,gRPC也是一个面试的热点和难点。

gRPC还是一个CNCF的一个孵化项目,在云原生方向有大量的应用。

关于它的更多的介绍,可以参考grpc官网 https://grpc.io/about/

如何使用gRPC

gRPC通过protobuf实现对数据的传输,关于protobuf的使用,主要是下载安装protobuf的二进制文件,可以参考七米老师的blog

https://www.liwenzhou.com/posts/Go/Protobuf3-language-guide-zh/

七米老师是一个非常优秀的开发者,同时他也是老男孩教育的go语言讲师,笔者的go语言课程就是学自七米老师的bilibili课程。

以上为基本的知识储备,后面是本节的主要内容,我们通过gRPC实现对MySQL的使用操作。

通过gRPC操作MySQL

1、实现各种备份

定义通信消息

首先我们要定义一个MySQLBackUP的server端,将其部署在MySQL所在的节点上。

通过proto3定义grpc通信的message

首先是一个BackUpRequest的请求消息

message BackupTaskRequest {
  MySQLConn MySQLConn = 1;    // 备份MySQL的连接信息
  BackUpType BackUpType = 2;  // 备份类型
}

message MySQLConn {
  string MySQLUser = 1;
  string MySQLUserpasswd = 2;
  string MySQLHost = 3;
  uint32 MySQLPort = 4;
}

message BackUpType{
  enum Types {
    // 物理备份全备
    FullBackUpWithXtra = 0;
    // 物理备份增备 -- 留个标志 一般不用
    IncrBackUpWithXtra = 1;
    // 逻辑备份 需要先落地再上传 这种选择节点的时候 看是否找几台中间机器
    // 逻辑备份使用mydmper 全备
    FullBackUpWithMydumper = 2;
    // 逻辑备份使用mudmper 单表
    SingleTableBackUpWithMydumper = 3;
    // 有些操作系统环境太次了,不支持安装mydumper 使用mysqldump
    FullBackUpWithMySQLDump = 4;
    SingleTableBackUpWithMySQLDump = 5;
  }
  Types Type = 1;
}

有了备份的请求消息之后,还需要有一个返回消息来告诉我们任务是否成功,或者作为异步消息,不关心是否成功,通过备份的任务脚本来往其他地方记录备份执行情况

message BackupTaskResponse{
  string MessageInfo = 1;
  string MessageWarn = 2;
}

接着我们定义备份服务的核心,即备份服务

service MySQLBackupService{
  rpc NewBackup(BackupTaskRequest) returns (BackupTaskResponse){};
}
// 这个代码的意思是我们这个备份服务,接收一个备份任务的请求体和返回一个备份服务的返回体,这两个的内容为我们上方定义的消息的内容

有了定义消息之后,我们可以通过proto3工具来为我们生成自动化的代码

protoc --go_out=../pb --go_opt=paths=source_relative \
  --go-grpc_out=../pb --go-grpc_opt=paths=source_relative ./*.proto
# 这个的意思是我们的protoc命令行工具会把我们当前目录下的所有的.proto文件编译成go语言的代码

之后我们发现在当前同级目录的pd文件夹下,生成了如下文件

mysqlbackup.pb.go
mysqlbackup_grpc.pb.go

他已经帮我们把相关的消息都整理成go代码了,举例来看这个备份任务请求结构体

type BackupTaskRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	WorkVm          *WorkVm          `protobuf:"bytes,1,opt,name=WorkVm,proto3" json:"WorkVm,omitempty"`
	MySQLConn       *MySQLConn       `protobuf:"bytes,2,opt,name=MySQLConn,proto3" json:"MySQLConn,omitempty"`
	SaasDBMySQLConn *SaasDBMySQLConn `protobuf:"bytes,3,opt,name=SaasDBMySQLConn,proto3" json:"SaasDBMySQLConn,omitempty"`
	BackUpType      *BackUpType      `protobuf:"bytes,4,opt,name=BackUpType,proto3" json:"BackUpType,omitempty"`
	RemoteStorageS3 *RemoteStorageS3 `protobuf:"bytes,5,opt,name=RemoteStorageS3,proto3" json:"RemoteStorageS3,omitempty"`
	BackUpTimeout   uint32           `protobuf:"varint,6,opt,name=BackUpTimeout,proto3" json:"BackUpTimeout,omitempty"`
	DomainId        uint32           `protobuf:"varint,7,opt,name=DomainId,proto3" json:"DomainId,omitempty"`
}

源码生成之后,建议不要手动修改,如果发现有不合适的定义消息,可以修改对应的.proto文件并重新生成一次代码。

服务端的实现

grpc有四种类型的同步模式,分为:无任何流传输 ,服务端到客户端单项流传输,客户端到服务端单项流传输,双向流传输

备份任务,我们只需要通过一个任务下发,所以不需要任何流传输,

根据自动化代码备注,通过声明一个不需要任何流传输的服务

type BackUpServer struct {
	pb.UnimplementedMySQLBackupServiceServer
}

之后我们可以定义这个结构体的指针接收类型的func,这个类型的方法名称,我们可以从mysqlbackup_grpc.pb.go文件的最后几个func中得到他的名称,比如

func _MySQLBackupService_NewBackup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(BackupTaskRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(MySQLBackupServiceServer).NewBackup(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/hello_grpc.MySQLBackupService/NewBackup",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(MySQLBackupServiceServer).NewBackup(ctx, req.(*BackupTaskRequest))
	}
	return interceptor(ctx, in, info, handler)
}

于是我们定义如下类型的备份函数,这个函数要求第一个参数是上下文ctx,它同时接受一个BackupTaskRequest,并返回BackupTaskResponse

func (server *BackUpServer) NewBackup(con context.Context, req *pb.BackupTaskRequest) (*pb.BackupTaskResponse, error) {
// 相关消息的内容可以通过Get开头的这几个函数得到
	backupType := req.GetBackUpType()
  mysqlConn := req.GetMySQLConn()
}

由于server部署在mysql所在节点,所以可以直接调用所在节点的命令行工具
此时我们可以声明一个taskjob的任务,根据备份类型分别选择不同的备份方式
taskjob通过定义BackUpJob方法来实现,需要注意的是,我们要保护数据库不能同时发起多个备份任务,所以这里可以通过go的metux锁来保护下任务顺序。

func (server *BackUpServer) BackupJob(ctx context.Context, j *backjobmetadata, db *gorm.DB) error {
	// 加锁 defer unlock 这里必须加一个lock,否则可能出现多个备份进程存在的情况 不允许 同时有不同类型的备份任务
	// TODO 发起备份的时候,先检查 实例的状态是否是 available ,再决定是否可以进行备份
	onlyOneBackJobRun.Lock()
	defer onlyOneBackJobRun.Unlock()

	if j.BackUpType.Type == pb.BackUpType_FullBackUpWithXtra {
	} else if j.BackUpType.Type == pb.BackUpType_IncrBackUpWithXtra {

	} else if j.BackUpType.Type == pb.BackUpType_FullBackUpWithMydumper {

	} else if j.BackUpType.Type == pb.BackUpType_SingleTableBackUpWithMydumper {

	} else if j.BackUpType.Type == pb.BackUpType_FullBackUpWithMySQLDump {
		return server.UseMysqlDump(ctx, j, db) // 后面可以走自己的实现方式来调度这个任务
	} else if j.BackUpType.Type == pb.BackUpType_SingleTableBackUpWithMySQLDump {
	}
	return nil
}

此时,备份的流程,基本已经有了,需要把我们的备份服务在main函数里开启grpc服务,并把任务声明出来

func main() {
	// openssl生成SAN证书
	//https://www.cnblogs.com/outsrkem/p/16056756.html
	creditsServeKey := "/Users/anderalex/go/src/workgrpc/certify/server.key"
	creditsServeCrt := "/Users/anderalex/go/src/workgrpc/certify/server.crt"
	creds, _ := credentials.NewServerTLSFromFile(creditsServeCrt, creditsServeKey)
	s := grpc.NewServer(grpc.Creds(creds))
	listen, err := net.Listen("tcp", ":3000")
	if err != nil {
		log.Panic("xxxxxx")
	}

	/* 备份服务 */
	pb.RegisterMySQLBackupServiceServer(s, &BackUpServer{})
  /* ... 其他任务服务 ... */
}

这里我是用的是x509 ca证书的方式来实现客户端跟服务端端加密认证

客户端的实现

客户端的实现相对较简单,我们只需要将通信的方法通过grpc给它声明出来,然后把请求消息和接收返回消息的功能实现

func main() {
	creditsServePem := "/Users/anderalex/go/src/workgrpc/certify/server.crt"
	commandName := "example.server.com"
	creds, _ := credentials.NewClientTLSFromFile(creditsServePem, commandName)

	conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithTransportCredentials(creds))
	if err != nil {
		fmt.Println(err)
	}
	client := pb.NewMySQLBackupServiceClient(conn)
	ctx, cancle := context.WithTimeout(context.Background(), time.Second)
	defer cancle()
	// 唯一要做的就是我们需要把请求消息和接收返回消息的功能实现
	res, err := client.NewBackup(ctx, NewBackupTaskRequest(vmip, host, user, passwd, port ,100, pb.BackUpType_FullBackUpWithMySQLDump))

	if err != nil {
		fmt.Println(err)
	}
// 处理req的返回消息
  fmt.Println(res.GetMessageInfo())
}	

分别运行服务端,客户端,观察是否符合预期

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐