本人水平:参加工作六个月,刚看完一本《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)
}
}
}