背景
目前監控系統主要使用zabbix監控硬件和prometheus監控操作系統、基礎服務的組合。近期在openstack線上環境開始使用VPC的功能,導致部署在openstack集羣外的prometheus無法通過虛擬機ip獲取到相應的node_exporter的監控數據。同時由於網段逐漸增多,不同網段策略不同導致prometheus到部分非VPC雲主機的node_exporter不通,基於隔離和安全考慮並不是所有網段能開牆。
因此需要尋找一種新的方案來完成openstack集羣上所有云主機的監控。
方案
最初制定方案時並沒有網段不通的問題,僅考慮VPC問題。
1.放棄使用VPC
業務屬性決定該方案不可行。
2.每臺VPC雲主機增加floating ip
不如不使用VPC
3.Libvirt+guestfs
由libvirt採集cpu、內存監控,guestfs採集分區數據。但libvirt採集的內存是將used+buffer\cache一起計算無法區分開,給雲平臺提供此數據沒問題,但對用戶來講相對來說不準確。
Guestfs採集分區數據需要將ceph rbd以只讀方式掛載到本地,不確定在頻繁監控場景下對雲主機的磁盤會帶來什麼影響
4.VPC內創建雲主機部署prometheus
每個VPC申請一臺雲主機搭建Prometheus,該Prometheus僅監控該VPC內雲主機。
5.網絡節點VPC NameSpace部署Prometheus
和前一方案類似,並且減少雲主機的創建與維護
6.雲主機監控採用Zabbix
導致操作系統監控數據在zabbix和prometheus中均存在,與監控系統原則衝突
7.Node_exporter Push到Gateway
在雲主機流量出來比進去容器,可以修改node_exporter支持Push,定時將監控數據推送到Push Gateway,再由Prometheus向gateway拉取監控數據。
Gateway只能做單點或分片,沒法簡單用多Gateway做負載。
8.Node_exporter Push到Prometheus
改動較大,後期版本升級難度高。
9.開發VPC exporter
編寫vpc exporter,並將vpc-exporter部署到網絡節點
prometheus向vpc-exporter請求數據時,vpc-exporter抓取所有vpc下所有主機的node-exporter接口獲取監控數據並返回;
Vpc相應的網絡namespace目前是分散在兩個網絡節點,可通過hash方式降低單個vpc-exporter的抓取量。
結論
經過討論暫定使用方案:VPC內創建雲主機部署prometheus。
但在POC和調研過程中發現雲主機使用VPC網絡需要創建在相應租戶中,帶來是否將該虛擬機在雲管對租戶屏蔽以及度量計費如何處理的問題以及雲主機創建與維護的問題,同時考慮到非VPC網段網絡不通的問題,最終使用開發vpc exporter的方案來進行雲主機的監控。
此方案天然支持複雜網絡情況的雲主機監控,但存在的缺點也比較明顯:
- 網絡節點壓力增加
- 雲主機監控頻率固定,後期無法針對單臺雲主機修改監控頻率。
目前規模小的情況下暫時沒有問題,等明年將linux bridge切換成ovs之後再考慮其它的方案來規避上述缺點。
代碼邏輯
- 查詢當前ACTIVE的網絡id和所在host,當前設置是每個網絡在兩個網絡節點啓動namespace並提供服務
- 查詢雲主機及其使用的網絡id
- 根據雲主機ip的int值 % 同一網絡namespace個數,與當前網絡節點在網絡host列表的序號是否相等進行簡單的分片確定當前exporter待抓取的雲主機ip列表
- 抓取
核心代碼
抓取
使用golang的os/exec 模塊完成
func scrapeBySubProcess(ip string, namespace string) ([]metric, int, error) {
ctx, cancelFunc := context.WithTimeout(context.TODO(), 10*time.Second)
defer cancelFunc()
// cancelFunc.
url := fmt.Sprintf("http://%s:%d/metrics", ip, config.InstanceMetricPort)
proc := exec.CommandContext(ctx, "ip", "netns", "exec", namespace, "curl", "-S", "-s", "-m", "5", url)
var stdout, stderr bytes.Buffer
proc.Stdout = &stdout
proc.Stderr = &stderr
if err := proc.Run(); err != nil {
errOutput := string(stderr.Bytes())
logrus.Errorf("cat not get metrics,namespace:%s,url:%s,error:%s, message:%s", namespace, url, err.Error(), errOutput)
return nil, 0, err
}
metrics := parseBody(stdout.Bytes(), "text/plain")
return metrics, len(stdout.Bytes()), nil
}
解析
使用Prometheus提供的客戶端包完成:github.com/prometheus/prometheus/pkg/textparse
func parseBody(body []byte, contentType string) []metric {
p := textparse.New(body, contentType)
metrics := make([]metric, 0)
for {
var err error
var et textparse.Entry
if et, err = p.Next(); err != nil {
if err == io.EOF {
err = nil
}
break
}
if et != textparse.EntrySeries {
continue
}
_, _, v := p.Series()
var lset labels.Labels
p.Metric(&lset)
var labels []string
var labelValues []string
var metricName string
for _, l := range lset {
if l.Name == "__name__" {
metricName = l.Value
} else {
labels = append(labels, l.Name)
labelValues = append(labelValues, l.Value)
}
}
metrics = append(metrics, metric{
name: metricName,
labels: labels,
labelValues: labelValues,
value: v,
})
}
return metrics
}