open-falcon 源码解析(一)

open-falcon 是小米开源的企业级监控系统,我最近在抽空读它的源码,是为了我能够在很短的时间内搭建出一套好用的监控系统,而且如果有不满足的需求可以很快修改。

下面是 open-falcon 的架构图,组件还挺多的,到目前为止我读完了 Agent、Transfer、Graph 和 Query 四个部分。

falcon-arch

 

Agent 部分

Agent 获取的数据格式为:
type MetricValue struct {
Endpoint  string      `json:”endpoint”`
Metric    string      `json:”metric”`
Value     interface{} `json:”value”`
Step      int64       `json:”step”`
Type      string      `json:”counterType”`
Tags      string      `json:”tags”`
Timestamp int64       `json:”timestamp”`
}

1. Type 是 rrd DsType(数据源类型),比如 GAUGE、COUNTER 和 DERIVE;
2. 当收集 网卡、硬盘使用率、硬盘 IO、进程、端口、目录大小、url 时,会使用 Tags,其他不会,因为这些信息需要额外知道比如 网卡设备、硬盘分区、硬盘设备、进程名称、端口号、目录名称、url 链接等信息;
3. Agent 会起一个 goroutine,获取要检测的 url 链接、端口号、进程名、目录大小等信息,这些信息是本机无法知道;
4. Step 值由配置文件中的 Transfer.Interval 决定;
5. 每个 Metric 收集和传输时间间隔由 Transfer.Interval 决定,收集到的信息会传递给 Transfer;
6. 支持「插件」,插件目录列表通过 RPC 获取,目录列表中的所有目录下的脚本都会当做插件执行,脚本名称要包括执行超时时间(以 _ 分割),而且脚本的输出要符合 MetricValue 的格式。

 

Transfer 部分

Transfer 收到 Agent 发来的数据后,会先做一下清理,不合法的数据会被忽略,比如 Type 不合法,Value 为空,Step <= 0 等。

然后 Transfer 把格式改成 MetaData:
type MetaData struct {
Metric string `json:”metric”`
Endpoint string `json:”endpoint”`
Timestamp int64 `json:”timestamp”`
Step int64 `json:”step”`
Value float64 `json:”value”`
CounterType string `json:”counterType”`
Tags map[string]string `json:”tags”`
}
1. CounterType 是 MetricValue 的 Type 值 。
2. Tags 会从 key1=value1,key2=value2 字符串变成类似 {key1:value1, key2:value2} 的 map。

然后 Transfer 把数据插入 Graph 和 Judge 内存队列,插入到哪台 Graph 机器 或者 Judge 机器 (称为 node )由一致性 hash 决定,队列以 node 为 key,根据 node 可以拿到队列。

然后对于 Graph 和 Judge 的每一个 node,都起一个 goroutine 来发送,根据 node 拿到 addr,对每一个 addr 已经初始化了 RPC 连接池,从连接池中选一个连接,然后 RPC 调用。

这里我有一个疑问:如果 Graph 或者 Judge 的一个 node 挂了,那么一致性 hash 会自动摘除吗?目前看起来不会。一个解决办法是所有 Graph 或者 Judge 机器都向 zookeeper 注册一个临时节点,而 Transfer 来监听变化,如果变化就更新一致性 hash。

另外,数据在插入 Graph 的队列之前会被改成下面的格式:
type GraphItem struct {
Endpoint  string            `json:”endpoint”`
Metric    string            `json:”metric”`
Tags      map[string]string `json:”tags”`
Value     float64           `json:”value”`
Timestamp int64             `json:”timestamp”`
DsType    string            `json:”dstype”`
Step      int               `json:”step”`
Heartbeat int               `json:”heartbeat”`
Min       string            `json:”min”`
Max       string            `json:”max”`
}

1. DsType、Step、Heartbeat、Min 和 Max 都是 rrd 的概念;
2. Step 不能小于 30s;
3. 如果 MetaData 的 CounterType 是 GAUGE,则 DsType 也是 GAUGE,如果 CounterType 是 COUNTER 或 DERIVE,DsType 都会被改成 DERIVE;
4. DsType 如果是 GAUGE,Min 和 Max 分别是 U、U,如果是 DERIVE,Min 和 Max 分别是 0、U。

rrd 的相关内容参考这里

 

Graph 部分

收到的 GraphItem 数据存储在 GraphItemMap 结构的 GraphItems 变量中,GraphItemMap 结构如下:
type GraphItemMap struct {
sync.RWMutex
A    []map[string]*SafeLinkedList
Size int
}

1. map[string]*SafeLinkedList 的 key 格式是 checksum_dsType_step(称为 ckey),其中 checksum 是 Endpoint, Metric 和 Tags 三者的 md5,*SafeLinkedList 存的则是 GraphItem;
2. A 中 有 Size 个 map[string]*SafeLinkedList,0 到 Size-1 这些数字由 hashKey(ckey) % Size 取得。

Graph 收到数据后,会做三件事:
1. 存入定义的 GraphItems 结构中,Graph 会事先启动一个叫 rrdtool 的 goroutine,不断从 GraphItems 中读取 GraphItem 存入 rrd 数据库。

rrd 文件路径格式是:基础目录/md5前两位/md5/dsType/step.rrd

为了让 rrd 性能满足需求,设置了各种合并策略,比如 1 分钟一个点存 12 小时,5 分钟一个点存 2 天(取平均、最大、最小) 等。

2. 更新索引。

索引由两个缓存变量保存:unIndexedItemCache 和 indexedItemCache,前者保存未建立索引的数据,后者保存已经建立索引的数据。
它们结构一样,它们都以 Endpoint、Metric 和 Tags 的 md5 为 key,value 结构如下:
type IndexCacheItem struct {
UUID string
Item *cmodel.GraphItem
}
UUID 是 endpoint/metric/tags/dstype/step 组成的字符串。

有一个专门的 goroutine 从 unIndexedItemCache 去数据,来循环建立(增量)索引。

有三种索引,分别是 endpoint_ts 索引、tag_endpoint 索引 和 endpoint_counter 索引,索引信息存在 Mysql 中。

额外的,Graph 提供 /proc http 接口来更新全量索引(数据从 indexedItemCache 中获取),默认建立两天内数据的索引。

3. 存入 HistoryCache,HistoryCache 供查看最近收到的 GraphItem,HistoryCache同样以 Endpoint、Metric 和 Tags 的 Checksum 为 key,每个 key 默认只保存三条 GraphItem。

 

Query 部分

查询组件,向 Graph 查询数据。

1. history 接口。

查询参数为:
type GraphHistoryParam struct {
Start int `json:”start”`
End int `json:”end”`
CF string `json:”cf”`
EndpointCounters []cmodel.GraphInfoParam `json:”endpoint_counters”`
}

GraphInfoParam 结构为:
type GraphInfoParam struct {
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
}

上两个结构合并成如下一个个结构,然后通过 RPC 向 Graph 查询:
type GraphQueryParam struct {
Start int64 `json:”start”`
End int64 `json:”end”`
ConsolFun string `json:”consolFuc”`
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
}

查询结果保存在下面的结构:
type GraphQueryResponse struct {
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
DsType string `json:”dstype”`
Step int `json:”step”`
Values []*RRDData `json:”Values”`
}

RRDData 如下:
type RRDData struct {
Timestamp int64 `json:”timestamp”`
Value JsonFloat `json:”value”`
}

查询过程:
1). 根据 Endpoint 和 Counter 查询出 dsType 和 step,其中 Counter 需要是 Metric/Tags 格式;
2). 计算出 rrd 文件路径,获取 rrd 中的数据;
3). 算出 ckey,获取缓存中的数据。
4). 聚合处理。

2. info 接口。

请求结构是:
type GraphInfoParam struct {
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
}

返回结构是:
type GraphInfoResp struct {
ConsolFun string `json:”consolFun”`
Step int `json:”step”`
Filename string `json:”filename”`
}

3. last 接口。

请求结构是:
type GraphLastParam struct {
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
}

返回结构是:
type GraphLastResp struct {
Endpoint string `json:”endpoint”`
Counter string `json:”counter”`
Value *RRDData `json:”value”`
}

 

DDNS 报错:RRset exists (value dependent)’ prerequisite not satisfied

问题场景:

我们的装机系统使用 DHCP + DNS 环境来保存 SN 和 控制卡 IP 的映射关系,很久之前开始发现控制卡因为损坏更换之后,使用 idrac-SN 解析出来的 IP 是老的,连不上,而 控制卡已经通过 DHCP 获得了新的 IP ( 通过 /var/lib/dhcpd/dhcpd.leases 查看 )。

 

看 DNS 的日志 /var/named/data/named.run 有类似如下报错:

21-Jul-2015 16:13:37.002 client 10.2.1.1#19205: updating zone ‘ilo.xxx.com/IN’: update unsuccessful: idrac-1K39D02.ilo.xxx.com: ‘name not in use’ prerequisite not satisfied (YXDOMAIN)
21-Jul-2015 16:13:37.003 client 10.2.1.1#44065: updating zone ‘ilo.xxx.com/IN’: update unsuccessful: idrac-1K39D02.ilo.xxx.com/TXT: ‘RRset exists (value dependent)’ prerequisite not satisfied (NXRRSET)

看起来是 DHCPD 更新 DNS 失败,显示 TXT 已经存在,条件不满足。

TXT 在这里是 mac 和 控制卡 hostname 等的哈希,更换控制卡之后 TXT 发生变化,和 DNS 中的 TXT 不一致,所以更新不了。

然后我做了两个尝试:
1. 把 DNS 中的 TXT/A/PTR 记录删掉,重启控制卡重新获取 IP,发现 DNS 此时可以正确加上;
2. 只把 TXT 记录删掉,同样重启控制卡重新获取 IP,不会添加 DNS。

 

手动删除 TXT/A/PTR 记录太麻烦了,最好的方法是在 DHCP 中加一个选项:

update-conflict-detection false

如果 TXT 不存在或者不匹配,都会重写 TXT/A/PTR 到 DNS,是完全 OK 的,已验证。

 

http://comments.gmane.org/gmane.network.dhcp.isc.dhcp-client/6202

https://lists.isc.org/pipermail/dhcp-users/2013-January/016367.html

https://lists.isc.org/pipermail/dhcp-users/2013-January/016378.html

 

docker 基础镜像的管理

本文讨论基础镜像的管理,只有一个问题:基础镜像之间会有依赖关系,被依赖的镜像变化时,依赖它的镜像都要重新 build。

我采用的是下面的方式,下面每一个名称都是一个目录,目录名就是镜像名,目录名是层级结构,以 – 分割,前面的被后面的依赖。

centos6
centos6-java7
centos6-java7-tomcat6
centos6-java7-tomcat7
centos6-java7-tomcat8
centos6-java8
centos6-java8-tomcat6
centos6-java8-tomcat7
centos6-java8-tomcat8
centos6-python2.6
centos6-python2.6-tornado
centos7
centos7-java7
centos7-java7-tomcat6
centos7-java7-tomcat7
centos7-java7-tomcat8
centos7-java8
centos7-java8-tomcat6
centos7-java8-tomcat7
centos7-java8-tomcat8
centos7-python2.6
centos7-python2.6-tornado

构建依赖通过字符串匹配判断,比如如果构建  centos6-python2.6,centos6-python2.6-tornado 也会被构建。

# python build.py -n centos6-python2.6
name:centos6-python2.6
names:[‘centos6-python2.6’, ‘centos6-python2.6-tornado’]
cmd:cd centos6-python2.6 && docker build –rm=true -t dockerhub.internal.nosa.me/centos6-python2.6 . && docker push dockerhub.internal.nosa.me/centos6-python2.6
build centos6-python2.6 succ
cmd:cd centos6-python2.6-tornado && docker build –rm=true -t dockerhub.internal.nosa.me/centos6-python2.6-tornado . && docker push dockerhub.internal.nosa.me/centos6-python2.6-tornado
build centos6-python2.6-tornado succ

build.py 脚本在 这里