open-falcon源码阅读(三)——hbs源码阅读

本人水平:参加工作六个月,刚看完一本《go实战》的菜鸡

代码版本:2019年1月15日使用go get github.com/open-falcon/falcon-plus拉下来的代码

1 概览

1.1 目录结构

  • cache:hbs本身就是一个大缓存,读取用户在web页面配置的信息,再把信息提供给agent和judge
  • db:数据库的操作
  • g:全局共享信息的存放,包括配置信息,日志等级,运行最大进程数
  • http:http服务,提供一些缓存信息的查询接口
  • rpc:rpc服务,agent信息的上报,judge查询告警策略的rpc服务

1.2 main函数

func main() {
	cfg := flag.String("c", "cfg.json", "configuration file")
	version := flag.Bool("v", false, "show version")
	flag.Parse()

	// 打印版本
	if *version {
		fmt.Println(g.VERSION)
		os.Exit(0)
	}

	// 读取配置
	g.ParseConfig(*cfg)

	// 初始化数据库和缓存
	db.Init()
	cache.Init()

	// 清除长期没有心跳的过期 agent
	go cache.DeleteStaleAgents()

	// 开启网络服务
	go http.Start()
	go rpc.Start()

	// 等待停止信号,关闭数据库,退出
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		fmt.Println()
		db.DB.Close()
		os.Exit(0)
	}()

	// 阻塞 main 函数
	select {}
}

2 初始化数据库、缓存

2.1 初始化数据库

比较简单啦,配置的各种信息读取一下,然后ping一下测试

/*
	初始化数据库
 */
func Init() {
	var err error
	DB, err = sql.Open("mysql", g.Config().Database) // 获取配置的数据库连接信息
	if err != nil {
		log.Fatalln("open db fail:", err)
	}

	DB.SetMaxOpenConns(g.Config().MaxConns)  // 设置最大连接数
	DB.SetMaxIdleConns(g.Config().MaxIdle)  // 设置最大空闲连接数

	err = DB.Ping()  // 测试数据库连接
	if err != nil {
		log.Fatalln("ping db fail:", err)
	}
}

2.2 初始化缓存

这部分完成的工作是,前端页面上用户配置的一些主机、主机组、模板、插件、策略、表达式等信息,都是存放在数据库falcon_portal里的,hbs要周期性地从数据库把这些信息加载到内存里

实际上是开了一个goroutine周期性地完成缓存初始化的工作,既然是周期性执行,还是称为同步吧,同步了9种信息,一个一个说

/*
	初始化缓存
 */
func Init() {
	log.Println("cache begin")

	log.Println("#1 GroupPlugins...")
	GroupPlugins.Init()

	log.Println("#2 GroupTemplates...")
	GroupTemplates.Init()

	log.Println("#3 HostGroupsMap...")
	HostGroupsMap.Init()

	log.Println("#4 HostMap...")
	HostMap.Init()

	log.Println("#5 TemplateCache...")
	TemplateCache.Init()

	log.Println("#6 Strategies...")
	Strategies.Init(TemplateCache.GetMap())

	log.Println("#7 HostTemplateIds...")
	HostTemplateIds.Init()

	log.Println("#8 ExpressionCache...")
	ExpressionCache.Init()

	log.Println("#9 MonitoredHosts...")
	MonitoredHosts.Init()

	log.Println("cache done")

	go LoopInit()

}

/*
	周期性执行初始化
 */
func LoopInit() {
	for {
		time.Sleep(time.Minute)
		GroupPlugins.Init()
		GroupTemplates.Init()
		HostGroupsMap.Init()
		HostMap.Init()
		TemplateCache.Init()
		Strategies.Init(TemplateCache.GetMap())
		HostTemplateIds.Init()
		ExpressionCache.Init()
		MonitoredHosts.Init()
	}
}

2.2.1 主机组-plugin信息同步

hbs会把用户在前端页面配置的主机组绑定的plugin信息同步给agent,所以hbs自己也要把这些信息缓存在内存里

首先,用户在前端页面配置绑定plugin后,这些信息是存在数据库falcon_portal的plugin_dir表里,grp_id就是主机组id,dir是用户配置的plugin目录,其他都很好理解不多说

每个周期的同步代码是这样,就是定时从数据库查询,缓存在一个map里

/*
	线程安全的主机组 ID - plugin 关系
 */
type SafeGroupPlugins struct {
	sync.RWMutex  // 读写锁
	M map[int][]string  // 一个主机组(int)可以对应多个 plugin(string)
}

var GroupPlugins = &SafeGroupPlugins{M: make(map[int][]string)}  // 缓存的主机组 ID -plugin 关系

/*
	从数据库查询主机组 ID - plugin 关系
 */
func (this *SafeGroupPlugins) Init() {
	m, err := db.QueryPlugins()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

2.2.2 主机组-模板信息同步

主机组和模板的关联关系存在falcon_portal的grp_tpl里,grp_id是主机组ID,tpl_id是模板ID

信息同步的代码大同小异了

/*
	线程安全的主机组 ID - 模板 ID 关系
 */
type SafeGroupTemplates struct {
	sync.RWMutex
	M map[int][]int
}

var GroupTemplates = &SafeGroupTemplates{M: make(map[int][]int)}  // 缓存的主机组 ID -plugin关系

/*
	从数据库查询主机组 ID - 模板 ID 关系
 */
func (this *SafeGroupTemplates) Init() {
	m, err := db.QueryGroupTemplates()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

2.2.3 主机组-模板信息同步

这个信息存在falcon_portal的grp_host表里

信息同步的代码,是用主机id作为map的主键,可能是用主机查询主机组的操作更为常见吧

/*
	线程安全的主机 ID - 主机组 ID 关系
 */
type SafeHostGroupsMap struct {
	sync.RWMutex
	M map[int][]int
}

var HostGroupsMap = &SafeHostGroupsMap{M: make(map[int][]int)}  // 缓存的主机 ID - 主机组 ID 关系

/*
	从数据库查询主机 ID - 主机组 ID 关系
 */
func (this *SafeHostGroupsMap) Init() {
	m, err := db.QueryHostGroups()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

2.2.4 主机名-主机ID信息同步

这个信息存在falcon_portal的host表里,都很通俗易懂

啊啊啊,这些一模一样的代码不能用更优雅的方式写吗

/*
	线程安全的主机名-主机 ID 关系
 */
type SafeHostMap struct {
	sync.RWMutex
	M map[string]int
}

var HostMap = &SafeHostMap{M: make(map[string]int)}  // 缓存的主机名-主机 ID 关系

/*
	从数据库查询主机名-主机 ID 关系
 */
func (this *SafeHostMap) Init() {
	m, err := db.QueryHosts()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

2.2.5 模板信息同步

falcon_portal的tpl表,parent_id是集成的父模板ID,action是模板里绑定的通知、回调等动作,这些存在另一个叫action的表里,action_id就是这个表的id

从数据库获取了所有的模板信息,为方便查询,用id作为map的键

/*
	线程安全的模板 ID - 模板关系
 */
type SafeTemplateCache struct {
	sync.RWMutex
	M map[int]*model.Template
}

var TemplateCache = &SafeTemplateCache{M: make(map[int]*model.Template)}  // 缓存的模板 ID - 模板关系

/*
	从数据库查询模板 ID - 模板关系
 */
func (this *SafeTemplateCache) Init() {
	ts, err := db.QueryTemplates()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = ts
}

2.2.6 策略信息同步

策略就是模板里一条条的告警策略,存在falcon_portal的strategy表,里面的run_begin和run_end是策略的生效时间,所以hbs在同步的时候只会取出当前处于生效时间段内的策略

/*
	线程安全的策略 ID - 策略关系
 */
type SafeStrategies struct {
	sync.RWMutex
	M map[int]*model.Strategy
}

var Strategies = &SafeStrategies{M: make(map[int]*model.Strategy)}  // 缓存的策略 ID - 策略关系

/*
	从数据库查询策略 ID - 策略关系
 */
func (this *SafeStrategies) Init(tpls map[int]*model.Template) {
	m, err := db.QueryStrategies(tpls)
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

策略从数据库查询还需要一点处理

一个是生效时间段的检测,一共三种情况,①如果没有设置开始、结束生效时间,就全天生效,②如果设置了生效时间,且当前时间大于开始时间、小于结束时间,生效,比如开始时间是2:00,结束时间是4:00,现在是3:00,就符合这种情况,③但是如果想在凌晨22:00到第二天4:00生效呢,就需要第三种情况了,现在时间小于开始时间、大于结束时间(不过不理解为什么还要检测一下开始时间大于结束时间,感觉这一步是多余的)

一个是tag和模板的处理,如果没有对应的模板信息,策略就是错误的一条数据,要跳过(比如模板已经被删除了,策略还没来得及删除??一种猜想不一定对)

/*
	查询策略 ID - 策略关系
 */
func QueryStrategies(tpls map[int]*model.Template) (map[int]*model.Strategy, error) {
	ret := make(map[int]*model.Strategy)

	if tpls == nil || len(tpls) == 0 {
		return ret, fmt.Errorf("illegal argument")
	}

	// sql 语句
	now := time.Now().Format("15:04")
	sql := fmt.Sprintf(
		"select %s from strategy as s where (s.run_begin='' and s.run_end='') "+  // 开始生效和结束生效时间都设置为空
			"or (s.run_begin <= '%s' and s.run_end >= '%s')"+  // 当前时间处在生效时间段,生效时间段未跨过凌晨 0 点
			"or (s.run_begin > s.run_end and !(s.run_begin > '%s' and s.run_end < '%s'))",  // 当前时间处在生效时间段,生效时间段跨过凌晨 0 点
		"s.id, s.metric, s.tags, s.func, s.op, s.right_value, s.max_step, s.priority, s.note, s.tpl_id",
		now,
		now,
		now,
		now,
	)

	rows, err := DB.Query(sql)
	if err != nil {
		log.Println("ERROR:", err)
		return ret, err
	}

	defer rows.Close()
	for rows.Next() {  // 数据库查询结果整理成策略对象
		s := model.Strategy{}
		var tags string
		var tid int
		err = rows.Scan(&s.Id, &s.Metric, &tags, &s.Func, &s.Operator, &s.RightValue, &s.MaxStep, &s.Priority, &s.Note, &tid)
		if err != nil {
			log.Println("ERROR:", err)
			continue
		}

		tt := make(map[string]string)

		if tags != "" {  // tag 整理成 map
			arr := strings.Split(tags, ",")
			for _, tag := range arr {
				kv := strings.SplitN(tag, "=", 2)
				if len(kv) != 2 {
					continue
				}
				tt[kv[0]] = kv[1]
			}
		}

		s.Tags = tt
		s.Tpl = tpls[tid]  // 通过模板 ID,查询对应的模板对象
		if s.Tpl == nil {
			log.Printf("WARN: tpl is nil. strategy id=%d, tpl id=%d", s.Id, tid)
			// 如果策略没有对应的模板,视为错误的策略,日志记录并跳过本条策略的处理
			continue
		}

		ret[s.Id] = &s
	}

	return ret, nil
}

2.2.7 主机ID-模板ID信息同步

用了一个inner join查的grp_tpl和grp_host,查到主机和模板的对应关系

/*
	线程安全的主机 ID - 模板 ID 关系
 */
type SafeHostTemplateIds struct {
	sync.RWMutex
	M map[int][]int
}

var HostTemplateIds = &SafeHostTemplateIds{M: make(map[int][]int)}  // 缓存的主机 ID - 模板 ID 关系

/*
	从数据库查询主机 ID - 模板 ID 关系
 */
func (this *SafeHostTemplateIds) Init() {
	m, err := db.QueryHostTemplateIds()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}

2.2.8 表达式同步

表达式就是expression,是针对tag的一种批量化操作表达式,存在falcon_portal的expression表,大部分字段都和策略一致,pause表示这个策略有没有处于暂停状态,0表示正常工作

/*
	线程安全的表达式
 */
type SafeExpressionCache struct {
	sync.RWMutex
	L []*model.Expression
}

var ExpressionCache = &SafeExpressionCache{}  // 缓存的表达式

/*
	从数据库查询表达式
 */
func (this *SafeExpressionCache) Init() {
	es, err := db.QueryExpressions()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.L = es
}

从数据库查的时候,会排除掉没有action_id和pause不为0的异常项

/*
	查询表达式
 */
func QueryExpressions() (ret []*model.Expression, err error) {
	sql := "select id, expression, func, op, right_value, max_step, priority, note, action_id from expression where action_id>0 and pause=0"  // 查询不在暂停状态、且有对应 action 的表达式
	rows, err := DB.Query(sql)
	if err != nil {
		log.Println("ERROR:", err)
		return ret, err
	}

	defer rows.Close()
	for rows.Next() {
		e := model.Expression{}
		var exp string
		err = rows.Scan(
			&e.Id,
			&exp,
			&e.Func,
			&e.Operator,
			&e.RightValue,
			&e.MaxStep,
			&e.Priority,
			&e.Note,
			&e.ActionId,
		)

		if err != nil {
			log.Println("WARN:", err)
			continue
		}

		e.Metric, e.Tags, err = parseExpression(exp)
		if err != nil {
			log.Println("ERROR:", err)
			continue
		}

		ret = append(ret, &e)
	}

	return ret, nil
}

2.2.9 主机信息同步

同步所有目前在监控的主机,从host表里查不在维护时间段的主机

/*
	线程安全的主机 ID -主机关系
 */
type SafeMonitoredHosts struct {
	sync.RWMutex
	M map[int]*model.Host
}

var MonitoredHosts = &SafeMonitoredHosts{M: make(map[int]*model.Host)}  // 缓存的主机 ID -主机关系

/*
	从数据库查询主机 ID -主机关系
 */
func (this *SafeMonitoredHosts) Init() {
	m, err := db.QueryMonitoredHosts()
	if err != nil {
		return
	}

	this.Lock()
	defer this.Unlock()
	this.M = m
}
/*
	查询主机 ID -主机关系
 */
func QueryMonitoredHosts() (map[int]*model.Host, error) {
	hosts := make(map[int]*model.Host)
	now := time.Now().Unix()
	sql := fmt.Sprintf("select id, hostname from host where maintain_begin > %d or maintain_end < %d", now, now)  // 查询不在维护时间的主机信息
	rows, err := DB.Query(sql)
	if err != nil {
		log.Println("ERROR:", err)
		return hosts, err
	}

	defer rows.Close()
	for rows.Next() {
		t := model.Host{}
		err = rows.Scan(&t.Id, &t.Name)
		if err != nil {
			log.Println("WARN:", err)
			continue
		}
		hosts[t.Id] = &t
	}

	return hosts, nil
}

3 网络服务

3.1 http

开启http服务的代码和agent一模一样

/*
	开启 http 服务
 */
func Start() {
	if !g.Config().Http.Enabled {  // 如果配置不开启 http,返回
		return
	}

	addr := g.Config().Http.Listen  // 获取 http 服务启动的地址
	if addr == "" {
		return
	}
	s := &http.Server{
		Addr:           addr,
		MaxHeaderBytes: 1 << 30,
	}
	log.Println("http listening", addr)
	log.Fatalln(s.ListenAndServe())  // 开启 http
}

 看一下各种API,在初始化函数init()里,就两类

/*
	初始化 API
 */
func init() {
	configCommonRoutes()
	configProcRoutes()
}

3.1.1 基本API

都是跟hbs组件自身相关的API

/*
	基本 API
 */
func configCommonRoutes() {
	// hbs 组件是否健康
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	// hbs 组件版本
	http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(g.VERSION))
	})

	// hbs 工作目录
	http.HandleFunc("/workdir", func(w http.ResponseWriter, r *http.Request) {
		RenderDataJson(w, file.SelfDir())
	})

	// 重新加载配置,该 API 只能在本地调用
	http.HandleFunc("/config/reload", func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.RemoteAddr, "127.0.0.1") {
			g.ParseConfig(g.ConfigFile)
			RenderDataJson(w, g.Config())
		} else {
			w.Write([]byte("no privilege"))
		}
	})
}

3.1.2  缓存信息查询API

hbs缓存的各种信息都可以通过API查询

/*
	hbs 缓存信息查询 API
 */
func configProcRoutes() {
	// hbs 缓存的表达式信息
	http.HandleFunc("/expressions", func(w http.ResponseWriter, r *http.Request) {
		RenderDataJson(w, cache.ExpressionCache.Get())
	})

	// hbs 缓存的 agent 信息
	http.HandleFunc("/agents", func(w http.ResponseWriter, r *http.Request) {
		RenderDataJson(w, cache.Agents.Keys())
	})

	// hbs 缓存的 host 信息
	http.HandleFunc("/hosts", func(w http.ResponseWriter, r *http.Request) {
		data := make(map[string]*model.Host, len(cache.MonitoredHosts.Get()))
		for k, v := range cache.MonitoredHosts.Get() {
			data[fmt.Sprint(k)] = v
		}
		RenderDataJson(w, data)
	})

	// hbs 缓存的策略信息
	http.HandleFunc("/strategies", func(w http.ResponseWriter, r *http.Request) {
		data := make(map[string]*model.Strategy, len(cache.Strategies.GetMap()))
		for k, v := range cache.Strategies.GetMap() {
			data[fmt.Sprint(k)] = v
		}
		RenderDataJson(w, data)
	})

	// hbs 缓存的模板信息
	http.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
		data := make(map[string]*model.Template, len(cache.TemplateCache.GetMap()))
		for k, v := range cache.TemplateCache.GetMap() {
			data[fmt.Sprint(k)] = v
		}
		RenderDataJson(w, data)
	})

	// 查询 hbs 缓存中,某个主机绑定的 plugin
	http.HandleFunc("/plugins/", func(w http.ResponseWriter, r *http.Request) {
		hostname := r.URL.Path[len("/plugins/"):]
		RenderDataJson(w, cache.GetPlugins(hostname))
	})

}

调用的get函数基本都很简单,plugin的稍微复杂一些

/*
	根据主机名,获取对应的 plugin
 */
func GetPlugins(hostname string) []string {
	hid, exists := HostMap.GetID(hostname)  // 查询主机名对应的主机 ID
	if !exists {
		return []string{}
	}

	gids, exists := HostGroupsMap.GetGroupIds(hid)  // 根据主机 ID,查询主机组 ID
	if !exists {
		return []string{}
	}

	pluginDirs := make(map[string]struct{})  // 用 map 保存 plugin,因为不同主机组可能绑定相同的 plugin,用 map 达到去重效果
	for _, gid := range gids {
		plugins, exists := GroupPlugins.GetPlugins(gid)  // 根据主机组 ID,查询对应的 plugin 目录
		if !exists {
			continue
		}

		for _, plugin := range plugins {
			pluginDirs[plugin] = struct{}{}
		}
	}

	size := len(pluginDirs)
	if size == 0 {
		return []string{}
	}

	dirs := make([]string, size)
	i := 0
	for dir := range pluginDirs {
		dirs[i] = dir
		i++
	}

	sort.Strings(dirs)
	return dirs
}

agent之前还没有接触到,放在rpc里面讲

3.2 rpc

开启服务的代码也很简单

/*
	开启 rpc 服务
 */
func Start() {
	addr := g.Config().Listen

	server := rpc.NewServer()

	server.Register(new(Agent))  // 注册 Agent 服务
	server.Register(new(Hbs))  // 注册 Hbs 服务

	l, e := net.Listen("tcp", addr)
	if e != nil {
		log.Fatalln("listen error:", e)
	} else {
		log.Println("listening", addr)
	}

	for {
		conn, err := l.Accept()  // 接收到请求
		if err != nil {
			log.Println("listener accept fail:", err)
			time.Sleep(time.Duration(100) * time.Millisecond)  // 请求错误,暂停 100 毫秒
			continue
		}
		go server.ServeCodec(jsonrpc.NewServerCodec(conn))  // 处理请求
	}
}

3.2.1 Agent rpc

都知道agent会定时上报信息给hbs,这是他最主要的功能

agent信息缓存部分的代码在cache.agents.go,虽然在cache包里,但Agent的信息是每个agent通过rpc调用上报的,和其他需要不停从数据库同步的信息区别很大,一个是hbs从数据库获取再给agent,一个是agent汇报给hbs再存入数据库

就是agent上报信息的增删改查,以及把agent信息同步到falcon_portal的host表的操作

/*
	线程安全的 agent 信息缓存
 */
type SafeAgents struct {
	sync.RWMutex
	M map[string]*model.AgentUpdateInfo  // 用 map 保存 agent 上报的信息,键是主机名
}

var Agents = NewSafeAgents()

/*
	创建新的 agent 信息缓存
 */
func NewSafeAgents() *SafeAgents {
	return &SafeAgents{M: make(map[string]*model.AgentUpdateInfo)}
}

/*
	处理上报的 agent 信息
 */
func (this *SafeAgents) Put(req *model.AgentReportRequest) {
	val := &model.AgentUpdateInfo{
		LastUpdate:    time.Now().Unix(),
		ReportRequest: req,
	}

	if agentInfo, exists := this.Get(req.Hostname); !exists ||  // 该 agent 以前没有上报过信息
		agentInfo.ReportRequest.AgentVersion != req.AgentVersion ||  // 该 agent 的客户端版本变更
		agentInfo.ReportRequest.IP != req.IP ||  // 该 agent 的 IP 变更
		agentInfo.ReportRequest.PluginVersion != req.PluginVersion {  // 该 agent 的 plugin 版本变更

		db.UpdateAgent(val)  // 更新数据库中 agent 信息
	}

	// 更新hbs 时间
	this.Lock()
	this.M[req.Hostname] = val
	this.Unlock()
}

/*
	通过主机名查询 agent 上报信息
 */
func (this *SafeAgents) Get(hostname string) (*model.AgentUpdateInfo, bool) {
	this.RLock()
	defer this.RUnlock()
	val, exists := this.M[hostname]
	return val, exists
}

/*
	删除主机名对应的 agent 上报信息
 */
func (this *SafeAgents) Delete(hostname string) {
	this.Lock()
	defer this.Unlock()
	delete(this.M, hostname)
}

/*
	获取所有上报过信息的 agent  主机名
 */
func (this *SafeAgents) Keys() []string {
	this.RLock()
	defer this.RUnlock()
	count := len(this.M)
	keys := make([]string, count)
	i := 0
	for hostname := range this.M {
		keys[i] = hostname
		i++
	}
	return keys
}

同步数据到数据库的时候,会根据cfg.json中hosts一项的不同分开处理,如果hosts为空,表示允许新增主机名,如果不为空,hbs不会增加新的主机名记录

/*
	更新数据库中的 agent 信息
 */
func UpdateAgent(agentInfo *model.AgentUpdateInfo) {
	sql := ""
	if g.Config().Hosts == "" {  // 如果 cfg.json 中 hosts 一项为空,表示允许插入新主机名的记录
		sql = fmt.Sprintf(
			"insert into host(hostname, ip, agent_version, plugin_version) values ('%s', '%s', '%s', '%s') on duplicate key update ip='%s', agent_version='%s', plugin_version='%s'",  // 如果主机名已经存在,只更新 IP、agent 版本、plugin 版本
			agentInfo.ReportRequest.Hostname,
			agentInfo.ReportRequest.IP,
			agentInfo.ReportRequest.AgentVersion,
			agentInfo.ReportRequest.PluginVersion,
			agentInfo.ReportRequest.IP,
			agentInfo.ReportRequest.AgentVersion,
			agentInfo.ReportRequest.PluginVersion,
		)
	} else {
		// sync, just update
		sql = fmt.Sprintf(
			"update host set ip='%s', agent_version='%s', plugin_version='%s' where hostname='%s'",  // 如果 cfg.json 中 hosts 一项不为空,表示不允许插入新主机名的记录,只能更新
			agentInfo.ReportRequest.IP,
			agentInfo.ReportRequest.AgentVersion,
			agentInfo.ReportRequest.PluginVersion,
			agentInfo.ReportRequest.Hostname,
		)
	}

	_, err := DB.Exec(sql)
	if err != nil {
		log.Println("exec", sql, "fail", err)
	}

}

然后可以看一下定义了哪些Agent的rpc调用了

3.2.2 Hbs rpc

这部分代码是hbs和judge之间的交互,主要就是judge要从hbs这里获取每个主机绑定了什么告警策略

因为涉及到模板的继承,这部分逻辑是最复杂的一段代码了,我也是看了很久才看懂了一点。对于一台主机,首先获取所有的模板和父模板组成继承链(代码里称之为bucket),然后去除被其他继承链包含的继承链(例如有三个模板:模板1->模板2->模板3,某台主机绑定了模板1和模板2,模板1->模板2->模板3就包含了模板2->模板3),最后把所有告警策略抽取出来,对于相同的counter,子模板配置的策略会覆盖父模板的,但是一个模板内允许多个相同counter的策略存在,为了安全起见,还加了一个策略ID去重的方案,原作者认为可能可以去除这项操作

先模板去重、再告警策略去重,推测是可以提升速度

/*
	从 hbs 缓存获取表达式
 */
func (t *Hbs) GetExpressions(req model.NullRpcRequest, reply *model.ExpressionResponse) error {
	reply.Expressions = cache.ExpressionCache.Get()
	return nil
}

/*
	获取配置的所有模板策略
 */
func (t *Hbs) GetStrategies(req model.NullRpcRequest, reply *model.StrategiesResponse) error {
	reply.HostStrategies = []*model.HostStrategy{}
	// 获取主机 ID - 模板 ID 映射表,一个主机可以对应多个模板
	hidTids := cache.HostTemplateIds.GetMap()
	sz := len(hidTids)
	if sz == 0 {
		return nil
	}

	// 获取当前在监控状态的主机 ID -主机关系
	hosts := cache.MonitoredHosts.Get()
	if len(hosts) == 0 {
		// 没有主机处于监控状态,返回
		return nil
	}

	// 获取缓存的模板 ID - 模板映射表
	tpls := cache.TemplateCache.GetMap()
	if len(tpls) == 0 {
		return nil
	}

	// 获取缓存的策略 ID - 策略关系映射表
	strategies := cache.Strategies.GetMap()
	if len(strategies) == 0 {
		return nil
	}

	// 生成模板 ID -策略的映射表
	tpl2Strategies := Tpl2Strategies(strategies)

	hostStrategies := make([]*model.HostStrategy, 0, sz)
	for hostId, tplIds := range hidTids {

		h, exists := hosts[hostId]
		if !exists {
			continue
		}

		// 获取一台主机配置的所有策略(包括父模板的处理)
		ss := CalcInheritStrategies(tpls, tplIds, tpl2Strategies)
		if len(ss) <= 0 {
			continue
		}

		// 生成 host -策略对象
		hs := model.HostStrategy{
			Hostname:   h.Name,
			Strategies: ss,
		}

		hostStrategies = append(hostStrategies, &hs)

	}

	reply.HostStrategies = hostStrategies
	return nil
}

/*
	生成模板 ID -策略的映射表
 */
func Tpl2Strategies(strategies map[int]*model.Strategy) map[int][]*model.Strategy {
	ret := make(map[int][]*model.Strategy)
	for _, s := range strategies {
		if s == nil || s.Tpl == nil {
			continue
		}
		if _, exists := ret[s.Tpl.Id]; exists {
			ret[s.Tpl.Id] = append(ret[s.Tpl.Id], s)
		} else {
			ret[s.Tpl.Id] = []*model.Strategy{s}
		}
	}
	return ret
}

/*
	获取主机绑定的所有策略
 */
func CalcInheritStrategies(allTpls map[int]*model.Template, tids []int, tpl2Strategies map[int][]*model.Strategy) []model.Strategy {
	// 处理所有模板 ID,找到每个模板的所有父模板,组成模板继承链(即把一个模板的所有父模板放在一维数组里)
	tpl_buckets := [][]int{}
	for _, tid := range tids {
		ids := cache.ParentIds(allTpls, tid)  // 获取某个模板的所有父模板(倒序排列,最顶层的模板排在数组第一个)
		if len(ids) <= 0 {
			continue
		}
		tpl_buckets = append(tpl_buckets, ids)
	}

	// 处理所有继承链,去掉被其他继承链包含的继承链,余下的存放到二维数组中
	count := len(tpl_buckets)
	uniq_tpl_buckets := [][]int{}
	for i := 0; i < count; i++ {  //  判断每条模板链是否是有效的
		var valid bool = true
		for j := 0; j < count; j++ {  // 跳过和自己的比较
			if i == j {
				continue
			}
			// 如果第 i 条模板链和其他模板链相同,直接判断为有效
			// 因为 i、j 两条模板链相同,会认为 i 包含 j,j 包含 i,
			// 在下面的包含判断中,两条模板链都不会被判断为有效,所以直接跳过
			if slice_int_eq(tpl_buckets[i], tpl_buckets[j]) {
				break
			}
			// 如果第 i 条继承链被其他继承链包含,则这条继承链无效
			// 例如存在模板继承链 1->2->3
			// 主机绑定了模板 1 和 2,则上一步得到 1->2->3 和 2->3 两条模板链
			// 2->3 已经被 1->2->3 包含,可以去掉
			if slice_int_lt(tpl_buckets[i], tpl_buckets[j]) {
				valid = false
				break
			}
		}
		if valid {  // 将有效的模板链保存到二维数组中
			uniq_tpl_buckets = append(uniq_tpl_buckets, tpl_buckets[i])
		}
	}

	strategies := []model.Strategy{}  // 保存一台主机绑定的所有策略,作为结果返回
	// 用映射完成一台主机的策略 ID 去重,key 是策略 ID
	// 代码原作者认为此操作可能可以省略,不过为了安全还是加上
	exists_by_id := make(map[int]struct{})  

	for _, bucket := range uniq_tpl_buckets {
		// 用映射来保存一条模板继承链的所有策略,key 是 metric + 排序后 tags 生成的字符串作为 uuid,达到去重的目的
		bucket_stras_map := make(map[string][]*model.Strategy)  
		// 按父模板到子模板的顺序处理,如果存在相同的告警策略
		// 即 metric + tags 相同,阈值无论是否相同,都认为是一条告警策略
		// 这种情况下,会用子模板的策略覆盖父模板的
		for _, tid := range bucket {
			// 处理一个模板,获取模板对应的策略,保存在映射中,键值是 metric + 排序后的 tags 生成 uuid 字符串
			the_tid_stras := make(map[string][]*model.Strategy)

			if stras, ok := tpl2Strategies[tid]; ok {
				for _, s := range stras {
					uuid := fmt.Sprintf("metric:%s/tags:%v", s.Metric, utils.SortedTags(s.Tags))  // metric + 排序后的 tags,生成 uuid 字符串
					if _, ok2 := the_tid_stras[uuid]; ok2 {  // 策略按 uuid 存放
						// 同一个模板内,允许一个 uuid 对应多个告警策略(比如指标轻度超标发邮件,重度超标发短信之类的设置)
						the_tid_stras[uuid] = append(the_tid_stras[uuid], s)  
					} else {
						the_tid_stras[uuid] = []*model.Strategy{s}
					}
				}
			}

			// 子模板存在 uuid 和父模板相同的策略,覆盖父模板的策略
			for uuid, ss := range the_tid_stras {
				bucket_stras_map[uuid] = ss
			}
		}

		last_tid := bucket[len(bucket)-1]  // 最年轻的子模板 ID

		// 所有策略的模板指向最年轻的子模板
		for _, ss := range bucket_stras_map {
			for _, s := range ss {
				valStrategy := *s
				if _, exist := exists_by_id[valStrategy.Id]; !exist {  // exists_by_id 中不存在策略 ID,证明未处理过该策略
					if valStrategy.Tpl.Id != last_tid {
						valStrategy.Tpl = allTpls[last_tid]
					}
					strategies = append(strategies, valStrategy)
					exists_by_id[valStrategy.Id] = struct{}{}  // 标记该策略 ID 为处理过
				}
			}
		}
	}

	return strategies
}

/*
	判断数组中是否含有某个元素
 */
func slice_int_contains(list []int, target int) bool {
	for _, b := range list {
		if b == target {
			return true
		}
	}
	return false
}

/*
	判断两个数组是否相同
 */
func slice_int_eq(a []int, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i, av := range a {
		if av != b[i] {
			return false
		}
	}
	return true
}

/*
	判断数组 b 是否包含数组 a 的所有元素
 */
func slice_int_lt(a []int, b []int) bool {
	for _, i := range a {
		if !slice_int_contains(b, i) {
			return false
		}
	}
	return true
}

4 清除过期agent

每个agent上报的信息都带有时间戳一枚,hbs会定时去看,如果最新的时间戳都是一天以前的,hbs就认为这个agent狗带了,不想搭理你,清除你的信息

/*
	周期性清理过期 agent
 */
func DeleteStaleAgents() {
	duration := time.Hour * time.Duration(24)
	for {
		time.Sleep(duration)
		deleteStaleAgents()
	}
}

/*
	清理一天没有心跳的 agent
 */
func deleteStaleAgents() {
	before := time.Now().Unix() - 3600*24  // 一天前的时间
	keys := Agents.Keys()  // 获取所有上报过 agent 信息的主机名
	count := len(keys)
	if count == 0 {
		return
	}

	for i := 0; i < count; i++ {
		curr, _ := Agents.Get(keys[i])  // 获取一个上报信息
		if curr.LastUpdate < before {  // 如果更新时间大于一天前,删除该信息
			Agents.Delete(curr.ReportRequest.Hostname)
		}
	}
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值