基于GO语言实现分布式定时任务学习(四)---- master 代码开发与实现

master 功能点

  1. 提供http接口供 任务录入、任务查看、任务强制杀死 等基础功能;
  2. 提供服务端启动等参数化设置;

master 启动

master启动需要做到解析配置文件、创建etcd连接交互、创建socket监听形成web服务。

  1. 配置文件解析
    配置文件解析主要包括录入etcd信息、mysql信息、以及服务器端口等,我们把这些信息形成json文件。

master.json

	 {
	  "API接口服务端口": "提供任务增删改查服务",
	  "apiPort": 8070,
	  "API接口读超时": "单位是毫秒",
	  "apiReadTimeout": 5000,
	  "API接口写超时": "单位是毫秒",
	  "apiWriteTimeout": 5000,
	  "etcd的集群列表": "配置多个, 避免单点故障",
	  "etcdEndpoints": [
	    "127.0.0.1:2379"
	  ],
	  "etcd的连接超时": "单位毫秒",
	  "etcdDialTimeout": 5000,
	  "web页面根目录": "静态页面,前后端分离开发",
	  "webroot": "./webroot",
	  "mongodb地址": "采用mongodb URI",
	  "mongodbUri": "mongodb://127.0.0.1:2379:27017",
	  "mongodb连接超时时间": "单位毫秒",
	  "mongodbConnectTimeout": 5000
	}

并且提供对应的config struct 对json文件进行装填 :

Config.go

import (
	"io/ioutil"
	"encoding/json"
)

// 程序配置
type Config struct {
	ApiPort int	`json:"apiPort"`
	ApiReadTimeout int	`json:"apiReadTimeout"`
	ApiWriteTimeout int	`json:"apiWriteTimeout"`
	EtcdEndpoints []string `json:"etcdEndpoints"`
	EtcdDialTimeout int `json:"etcdDialTimeout"`
	WebRoot string `json:"webroot"`
	MongodbUri string `json:"mongodbUri"`
	MongodbConnectTimeout int `json:"mongodbConnectTimeout"`
}

var (
	// 单例
	G_config *Config
)

// 加载配置
func InitConfig(filename string) (err error) {
	var (
		content []byte
		conf Config
	)

	// 1, 把配置文件读进来
	if content, err = ioutil.ReadFile(filename); err != nil {
		return
	}

	// 2, 做JSON反序列化
	if err = json.Unmarshal(content, &conf); err != nil {
		return
	}

	// 3, 赋值单例
	G_config = &conf

	return
}

下来在master main 函数中,基于命令行启动参数加载该配置文件

master.go (main包)

var (
	confFile string // 配置文件路径
)

// 解析命令行参数
func initArgs() {
	// arg1 : 解析结果赋值的变量
	// arg2 : 命令行中的key
	// arg3 : 默认的value
	// arg4 : 相应的描述
	// master -config ./master.json -xxx 123 -yyy ddd
	// idea 默认从gopath下执行,需要指定到该包下进行执行
	//
	flag.StringVar(&confFile, "config", "./master.json", "指定master.json")
	flag.Parse()
}

func main() {
	// 初始化命令行参数
	initArgs()
	// 加载配置
	if err = master.InitConfig(confFile); err != nil {
		goto ERR
	}

}
  1. Web服务启动加载

ApiServer.go

var (
	G_apiServer *ApiServer
)

// 任务Http接口
type ApiServer struct {
	httpServer *http.Server
}

//  初始化服务
func InitApiServer() (err error) {

	var (
		mux           *http.ServeMux //定制路由
		httpServer    *http.Server
		listener      net.Listener
		staticDir     http.Dir     // 静态文件根目录
		staticHandler http.Handler // 静态文件的HTTP回调
	)

	mux = http.NewServeMux()
	mux.HandleFunc("/job/save", handlerJobSave)
	mux.HandleFunc("/job/delete", handleJobDelete)
	mux.HandleFunc("/job/list", handleJobList)
	mux.HandleFunc("/job/kill", handleJobKill)
	//mux.HandleFunc("/job/log", handleJobLog)
	//mux.HandleFunc("/worker/list", handleWorkerList)

	// 静态文件目录
	staticDir = http.Dir(G_config.WebRoot)
	staticHandler = http.FileServer(staticDir)
	mux.Handle("/", http.StripPrefix("/", staticHandler)) //   ./webroot/index.html
	//strconv.ItoA  int转字符串
	if listener, err = net.Listen("tcp", ":"+strconv.Itoa(G_config.ApiPort)); err != nil {
		return
	}

	httpServer = &http.Server{
		ReadTimeout:  time.Duration(G_config.ApiReadTimeout) * time.Millisecond,
		WriteTimeout: time.Duration(G_config.ApiWriteTimeout) * time.Millisecond,
		Handler:      mux,
	}

	G_apiServer = &ApiServer{
		httpServer: httpServer,
	}

	go httpServer.Serve(listener)

	return nil

}

master.go (main包)

	//启动
	if err = master.InitApiServer(); err != nil {
		goto ERR
	}
  1. etcd 以及 job任务

JobMgr.go

var (
	// 单例
	G_jobMgr *JobMgr
)

// 任务管理器
type JobMgr struct {
	client *clientv3.Client
	kv     clientv3.KV
	lease  clientv3.Lease
}

// 初始化管理器
func InitJobMgr() (err error) {

	var (
		config clientv3.Config
		client *clientv3.Client
		kv     clientv3.KV
		lease  clientv3.Lease
	)

	// 初始化配置
	config = clientv3.Config{
		Endpoints:   G_config.EtcdEndpoints,                                     // 集群地址
		DialTimeout: time.Duration(G_config.EtcdDialTimeout) * time.Millisecond, // 连接超时
	}

	// 建立连接
	if client, err = clientv3.New(config); err != nil {
		return
	}

	// 得到KV和Lease的API子集
	kv = clientv3.NewKV(client)
	lease = clientv3.NewLease(client)

	// 赋值单例
	G_jobMgr = &JobMgr{
		client: client,
		kv:     kv,
		lease:  lease,
	}
	return
}

master.go (main包)

	//  任务管理器
	if err = master.InitJobMgr(); err != nil {
		goto ERR
	}

master 功能实现

1:录入任务
client ApiServer.go JobMgr.go etcd request for add cronJob for add KV for insert KV add kv par [ doPut ] 向ETCD 中添加任务 success success success client ApiServer.go JobMgr.go etcd

基于IDEA client 发送request如下:

###
POST http://localhost:8070/job/save
Accept: */*
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded

job= {"name": "job1","command": "echo hello","cronExpr": "*/5 * * * * * *"}

涉及代码见下:

ApiServer.go

//路由中添加 url-func
mux.HandleFunc("/job/save", handlerJobSave)

//保存任务 Post  job = {"name" :"job1" , "command":"echo hello" , "cronExpr: "* * * * *"}
func handlerJobSave(resp http.ResponseWriter, req *http.Request) {
	var (
		err     error
		postJob string
		job     common.Job
		oldJob  *common.Job
		bytes   []byte
	)

	// 1, 解析POST表单
	if err = req.ParseForm(); err != nil {
		goto ERR
	}
	// 2, 取表单中的job字段
	postJob = req.PostForm.Get("job")
	// 3, 反序列化job
	if err = json.Unmarshal([]byte(postJob), &job); err != nil {
		goto ERR
	}
	// 4, 保存到etcd
	if oldJob, err = G_jobMgr.SaveJob(&job); err != nil {
		goto ERR
	}
	// 5, 返回正常应答 ({"errno": 0, "msg": "", "data": {....}})
	if bytes, err = common.BuildResponse(0, "success", oldJob); err == nil {
		resp.Write(bytes)
	}
	return

ERR:
	// 6, 返回异常应答
	if bytes, err = common.BuildResponse(-1, err.Error(), nil); err == nil {
		resp.Write(bytes)
	}

}

其中common.BuildResponse为包装http请求的一个应答结构体

// HTTP接口应答
type Response struct {
	Errno int `json:"errno"`
	Msg string `json:"msg"`
	Data interface{} `json:"data"`
}

// 应答方法
func BuildResponse(errno int, msg string, data interface{}) (resp []byte, err error) {
	// 1, 定义一个response
	var (
		response Response
	)

	response.Errno = errno
	response.Msg = msg
	response.Data = data

	// 2, 序列化json
	resp, err = json.Marshal(response)
	return
}

其中G_jobMgr.SaveJob 为etcd交互类中的方法,代码如下:

// 保存任务
func (jobMgr *JobMgr) SaveJob(job *common.Job) (oldJob *common.Job, err error) {

	// 把任务保存到/cron/jobs/任务名 -> json
	var (
		jobKey    string
		jobValue  []byte
		putResp   *clientv3.PutResponse
		oldJobObj common.Job
	)

	// etcd的保存key
	jobKey = common.JOB_SAVE_DIR + job.Name

	// 任务信息json
	if jobValue, err = json.Marshal(job); err != nil {
		return
	}

	// 保存到etcd
	if putResp, err = jobMgr.kv.Put(context.TODO(), jobKey, string(jobValue), clientv3.WithPrevKV()); err != nil {
		return
	}

	// 如果是更新, 那么返回旧值
	if putResp.PrevKv != nil {
		// 对旧值做一个反序列化
		if err = json.Unmarshal(putResp.PrevKv.Value, &oldJobObj); err != nil {
			err = nil //防止旧值不是一个正确的 job json ,所以做错误移除
			return
		}
		oldJob = &oldJobObj
	}

	return

}


2:删除任务

基于IDEA client 发送request如下:

###
POST http://localhost:8070/job/delete
Accept: */*
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded

name=job1

流程与删除相同,不赘述流程。
部分代码如下 (//todo完整代码调试通过后随github给出):

JobMgr.go

// 删除任务
func (jobMgr *JobMgr) DeleteJob(name string) (oldJob *common.Job, err error) {
	var (
		jobKey    string
		delResp   *clientv3.DeleteResponse
		oldJobObj common.Job
	)

	// etcd中保存任务的key
	jobKey = common.JOB_SAVE_DIR + name

	// 从etcd中删除它
	if delResp, err = jobMgr.kv.Delete(context.TODO(), jobKey, clientv3.WithPrevKV()); err != nil {
		return
	}

	// 返回被删除的任务信息
	if len(delResp.PrevKvs) != 0 {
		// 解析一下旧值, 返回它
		if err = json.Unmarshal(delResp.PrevKvs[0].Value, &oldJobObj); err != nil {
			err = nil
			return
		}
		oldJob = &oldJobObj
	}
	return
}

ApiServer.go

func InitApiServer() (err error) {
	//
	initAPmux.HandleFunc("/job/delete", handleJobDelete)
	//
}
// 删除任务接口
// POST /job/delete   name=job1
func handleJobDelete(resp http.ResponseWriter, req *http.Request) {
	var (
		err    error // interface{}
		name   string
		oldJob *common.Job
		bytes  []byte
	)

	// POST:   a=1&b=2&c=3
	if err = req.ParseForm(); err != nil {
		goto ERR
	}

	// 删除的任务名
	name = req.PostForm.Get("name")

	// 去删除任务
	if oldJob, err = G_jobMgr.DeleteJob(name); err != nil {
		goto ERR
	}

	// 正常应答
	if bytes, err = common.BuildResponse(0, "success", oldJob); err == nil {
		resp.Write(bytes)
	}
	return

ERR:
	if bytes, err = common.BuildResponse(-1, err.Error(), nil); err == nil {
		resp.Write(bytes)
	}
}

3:任务遍历

基于IDEA client 发送request如下:

POST http://localhost:8070/job/list
Content-Type: application/x-www-form-urlencoded

总体流程与上述一致,不进行赘述,相关代码如下:

JobMgr.go

//列举任务
func (jobMgr *JobMgr) ListJobs() (jobList []*common.Job, err error) {
	var (
		dirKey  string
		getResp *clientv3.GetResponse
		kvPair  *mvccpb.KeyValue
		job     *common.Job
	)

	// 任务保存的目录
	dirKey = common.JOB_SAVE_DIR

	// 获取目录下所有任务信息
	if getResp, err = jobMgr.kv.Get(context.TODO(), dirKey, clientv3.WithPrefix()); err != nil {
		return
	}

	// 初始化数组空间
	jobList = make([]*common.Job, 0) // len(jobList) == 0

	// 遍历所有任务, 进行反序列化
	for _, kvPair = range getResp.Kvs {
		job = &common.Job{}
		if err = json.Unmarshal(kvPair.Value, job); err != nil {
			err = nil
			continue
		}
		//赋值以避免首地址改变
		jobList = append(jobList, job)
	}
	return
}

ApiServer.go

func InitApiServer() (err error) {
	//
	initAPmux.HandleFunc("/job/delete", handleJobDelete)
	//
}

// 列举所有crontab任务
func handleJobList(resp http.ResponseWriter, req *http.Request) {
	var (
		jobList []*common.Job
		bytes   []byte
		err     error
	)

	// 获取任务列表
	if jobList, err = G_jobMgr.ListJobs(); err != nil {
		goto ERR
	}

	// 正常应答
	if bytes, err = common.BuildResponse(0, "success", jobList); err == nil {
		resp.Write(bytes)
	}
	return

ERR:
	if bytes, err = common.BuildResponse(-1, err.Error(), nil); err == nil {
		resp.Write(bytes)
	}
}

4:强杀一个任务

基于IDEA client 发送request如下:

###
POST http://localhost:8070/job/kill
Content-Type: application/x-www-form-urlencoded

name=job1

任务的执行是依赖worker执行的,呢么只有相应的worker才可以做到对任务线程进行移除,所以mater需要将强杀命令广播到所有的worker,令做该任务的worker收到command;

client ApiServer.go JobMgr.go etcd worker request for add cronJob for add KV with leased for insert KV with leased add kv with leased in kill path watch kill path notify par [ doPut ] success 移除kill path 下发布的任务 par [ doKill ] success success client ApiServer.go JobMgr.go etcd worker

相关代码如下:

JobMgr.go

// 杀死任务
func (jobMgr *JobMgr) KillJob(name string) (err error) {
	// 更新一下key=/cron/killer/任务名
	var (
		killerKey      string
		leaseGrantResp *clientv3.LeaseGrantResponse
		leaseId        clientv3.LeaseID
	)

	// 通知worker杀死对应任务
	killerKey = common.JOB_KILLER_DIR + name

	//让worker监听到一次put操作
	if leaseGrantResp, err = jobMgr.lease.Grant(context.TODO(), 1); err != nil {
		return
	}

	// 租约ID
	leaseId = leaseGrantResp.ID

	// 创建带有租约的kv
	if _, err = jobMgr.kv.Put(context.TODO(), killerKey, "", clientv3.WithLease(leaseId)); err != nil {
		return
	}
	return
}

强杀的KV没必要长期存在,因为worker监听得到消息之后就可以删除,所以设置一秒的生命周期,做自动移除使用。当然,也可以自己给自己加一个定时任务,去做定时删除该path下的任务。

ApiServer.go

func InitApiServer() (err error) {
	//
	mux.HandleFunc("/job/kill", handleJobKill)
	//
}

// 强制杀死某个任务
// POST /job/kill  name=job1
func handleJobKill(resp http.ResponseWriter, req *http.Request) {
	var (
		err   error
		name  string
		bytes []byte
	)

	// 解析POST表单
	if err = req.ParseForm(); err != nil {
		goto ERR
	}

	// 要杀死的任务名
	name = req.PostForm.Get("name")

	// 杀死任务
	if err = G_jobMgr.KillJob(name); err != nil {
		goto ERR
	}

	// 正常应答
	if bytes, err = common.BuildResponse(0, "success", nil); err == nil {
		resp.Write(bytes)
	}
	return

ERR:
	if bytes, err = common.BuildResponse(-1, err.Error(), nil); err == nil {
		resp.Write(bytes)
	}
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值