master 功能点
- 提供http接口供 任务录入、任务查看、任务强制杀死 等基础功能;
- 提供服务端启动等参数化设置;
master 启动
master启动需要做到解析配置文件、创建etcd连接交互、创建socket监听形成web服务。
配置文件解析
:
配置文件解析主要包括录入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
}
}
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
}
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:录入任务
基于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;
相关代码如下:
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)
}
}