公司里面的中间件exporter都是我2年前用python写的,python调用prometheus的接口写一些exporter很好写,最近也算是学了一段时间go语言,那么尝试一下用go语言写一个redis-exporter,就简单的获取其几个指标,比如connected_client,dbsize,used_memory。
prometheus有好几种类型的指标,csdn一搜就知道,我就不多说了,我们这里就用最简单的类型:Gauge (实时数据,可增可减)。
好了,开始把**》》》》**
先创建一个go工程>>>go modules>>>
分析:有哪些步骤?
1.首先用go获取redis数据
2.将数据发给指标
3.提供metrics接口等待prometheus调用。
使用redigo获取redis的数据,因为重点在exporter,因此其他的就随便做做,比如redis连接直接写死,用虚机上面的redis,端口就默认的6379:
go get -u github.com/garyburd/redigo/redis
package main
import (
"fmt"
"github.com/garyburd/redigo/redis"
"log"
"strconv"
"strings"
)
// 定义需要收集的信息
type Metric struct {
ConnectedClients float64 `json:"connected_clients"`
DbSize float64 `json:"dbSize"`
UsedMemory float64 `json:"used_memory"`
}
func main() {
redisHost := "192.168.235.131:6379"
// 连接redis
rs, err := redis.Dial("tcp", redisHost)
// 操作完后自动关闭
defer rs.Close()
if err != nil {
log.Fatalf("connect redis failed, err:%v", err)
return
}
// 获取info,然后过滤出需要的信息
rs.Send("MULTI")
rs.Send("info")
info, err := redis.Strings(rs.Do("EXEC"))
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
}
infoSlice := strings.Split(info[0], "\n\r")
var (
connectedClients int = 0
usedMemory int = 0
)
for _, dbInfo := range infoSlice {
splitInfo := strings.Split(dbInfo, ":")
switch splitInfo[0] {
case "connected_clients":
connectedClients, _ = strconv.Atoi(splitInfo[1])
case "used_memory":
usedMemory, _ = strconv.Atoi(splitInfo[1])
}
// 获取到connectedClients, usedMemory
fmt.Println(connectedClients, usedMemory)
}
// 获取dbSize的数字
rs.Send("MULTI")
rs.Send("dbSize")
dbSize, err := rs.Do("EXEC")
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
}
dbSize = strings.Trim(fmt.Sprintf("%v",dbSize), "[]")
fmt.Println(dbSize)
}
现在我们已经获取的redis的需要展示的信息了,接下来学习关于prometheus的相关接口。
先准备一个结构体,把需要获取的东西放进去,因为后面需要set进去,而set的数字是一个float64的,所以这里我们就先定义好
// 定义需要被收集的信息的结构体
type Metric struct {
ConnectedClients float64
DbSize float64
UsedMemory float64
}
再创建对应的指标变量:
// 创建需要被收集的指标变量,可以附带相关摘要,都是gauge这种,如果使用prometheus.NewGauge则需要自己注册
// 反之,如果使用promauto.NewGauge则自动注册
// 因为我们需要在注册的时候放入其他相关标签(比如传入redisHost)
var (
connectedClients = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_connected_clients",
Help: "this_is_redis_connected_clients",
ConstLabels: map[string]string{"redis_port": redisHost},
})
dbSize = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_dbSize",
Help: "this_is_redis_dbSize",
ConstLabels: map[string]string{"redis_port": redisHost},
})
usedMemory = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_used_memory",
Help: "this_is_redis_used_memory",
ConstLabels: map[string]string{"redis_port": redisHost},
})
)
指标注册,这里我们用到上面定义的指标结构体,其实在数据产生后,应该以结构体的方式返回出来,然后在后面的函数中调用时传入:
// 初始化就直接注册上面列出的指标,上面也描述了,因为没有使用auto所以需要自己手动注册
prometheus.MustRegister(connectedClients)
prometheus.MustRegister(dbSize)
prometheus.MustRegister(usedMemory)
// 把go语言自带的指标也发上去
prometheus.MustRegister(prometheus.NewBuildInfoCollector())
connectedClients.Set(redisMetrics.ConnectedClients)
dbSize.Set(redisMetrics.DbSize)
usedMemory.Set(redisMetrics.UsedMemory)
提供2个接口:
// 给一个点击链接的接口
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
redis exporter
click redis exporter
"/metrics" + `">Metrics
`))
})
// 真实的请求metrics的接口
http.HandleFunc("/metrics", getMetricsHandler)
http.ListenAndServe(":8081", nil)
请求到/metrics时,执行以下函数:
func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
redisHost := "192.168.235.131:6379"
// 从redis中获取指标
redisMetrics, err := getRedisInfo(redisHost)
if err != nil {
log.Fatalf("get redis metrics failed, err:%v", err)
}
setMetricsInfo(redisMetrics,redisHost)
promhttp.Handler().ServeHTTP(w, r)
// 使用net/http包的话,请求过来的信息需要自己处理,输入到log记录发送的请求
fmt.Println(time.Now().Format("2006/01/02/ 03:04:05"), r.Method, r.URL)
}
#################################################华丽的分割线###############################################
package main
import (
"fmt"
"github.com/garyburd/redigo/redis"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
"strconv"
"strings"
"time"
)
// 定义需要被收集的信息的结构体
type Metric struct {
ConnectedClients float64
DbSize float64
UsedMemory float64
}
func getRedisInfo(redisHost string) (*Metric, error) {
// 连接redis
rs, err := redis.Dial("tcp", redisHost)
// 操作完后自动关闭
defer rs.Close()
// 若连接出错,则打印错误信息,返回
if err != nil {
log.Fatalf("connect redis failed, err:%v", err)
return nil, err
}
// 获取info,然后过滤出需要的信息
rs.Send("MULTI")
rs.Send("info")
info, err := redis.Strings(rs.Do("EXEC"))
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
}
// 获取info信息,并且切分用于遍历分析
infoSlice := strings.Split(info[0], "\r\n")
var (
connectedClients float64 = 0
usedMemory float64 = 0
)
for _, dbInfo := range infoSlice {
splitInfo := strings.Split(dbInfo, ":")
switch splitInfo[0] {
case "connected_clients":
// 获取到connectedClients
connectedClients, _ = strconv.ParseFloat(splitInfo[1], 64)
case "used_memory":
// usedMemory
usedMemory, _ = strconv.ParseFloat(splitInfo[1], 64)
}
}
// 获取dbSize的数字
rs.Send("MULTI")
rs.Send("dbSize")
dbSize, err := rs.Do("EXEC")
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
return nil, err
}
strBbSize := strings.Trim(fmt.Sprintf("%v", dbSize), "[]")
intDbSize, _ := strconv.ParseFloat(strBbSize, 64)
return &Metric{
ConnectedClients: connectedClients,
DbSize: intDbSize,
UsedMemory: usedMemory,
}, nil
}
func setMetricsInfo(redisMetrics *Metric,redisHost string) {
// 创建需要被收集的指标变量,可以附带相关摘要,都是gauge这种,如果使用prometheus.NewGauge则需要自己注册
// 反之,如果使用promauto.NewGauge则自动注册
// 因为我们需要在注册的时候放入其他相关标签(比如传入redisHost)
var (
connectedClients = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_connected_clients",
Help: "this_is_redis_connected_clients",
ConstLabels: map[string]string{"redis_port": redisHost},
})
dbSize = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_dbSize",
Help: "this_is_redis_dbSize",
ConstLabels: map[string]string{"redis_port": redisHost},
})
usedMemory = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_used_memory",
Help: "this_is_redis_used_memory",
ConstLabels: map[string]string{"redis_port": redisHost},
})
)
// 初始化就直接注册上面列出的指标,上面也描述了,因为没有使用auto所以需要自己手动注册
prometheus.MustRegister(connectedClients)
prometheus.MustRegister(dbSize)
prometheus.MustRegister(usedMemory)
// 把go语言自带的指标也发上去
prometheus.MustRegister(prometheus.NewBuildInfoCollector())
connectedClients.Set(redisMetrics.ConnectedClients)
dbSize.Set(redisMetrics.DbSize)
usedMemory.Set(redisMetrics.UsedMemory)
}
func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
redisHost := "192.168.235.131:6379"
// 从redis中获取指标
redisMetrics, err := getRedisInfo(redisHost)
if err != nil {
log.Fatalf("get redis metrics failed, err:%v", err)
}
setMetricsInfo(redisMetrics,redisHost)
promhttp.Handler().ServeHTTP(w, r)
// 使用net/http包的话,请求过来的信息需要自己处理,输入到log记录发送的请求
fmt.Println(time.Now().Format("2006/01/02/ 03:04:05"), r.Method, r.URL)
}
func main() {
// 给一个点击链接的接口
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
redis exporter
click redis exporter
"/metrics" + `">Metrics
`))
})
// 真实的请求metrics的接口
http.HandleFunc("/metrics", getMetricsHandler)
http.ListenAndServe(":8081", nil)
}
以上就是一个exporter的开发思路和简单的demo。
那么问题来了。
我的部门是云平台组,我们公司redis用的很重,测试+生产用了一百多套6节点redis集群,所以问题就很明显了
当我们云上有很多个redis或者是多个redis集群怎么办?
分析:
1.另起一个协程:go func(){} 定时去拉取环境里面的所有redis集群的redisport
2.再起一个协程,先从获取用来redisport的协程里拿到数据,
3.遍历所有需要监控的redis集群的连接信息,然后依次取值,打标签,设置数据,函数也单独起一个协程,
那么开始测试,我在我的虚机里再起一个8888端口的redis-server:
这样我们就得到了2个redis:
这里为了简便,获取多个redisHost的过程我就不描述了,毕竟大家对这个部分获取的方式各有不同,我就直接用一个slice硬写进去,假装是用go关键字协程写进去的。如下:
// 定义一个redisPort的切片,里面装了多个需要被监控的redis
// 定义一个redisPort的切片,里面装了多个需要被监控的redis
redisHostSlice := []string{"192.168.235.131:6379", "192.168.235.131:8888"}
// 定义一个通道,容量为10,用来接收查询到的数据
metricsCh := make(chan *Metric, 10)
for _, redisHost := range redisHostSlice {
wg.Add(1)
go sendInfoToChan(metricsCh, redisHost)
}
然后和简单版不同的地方主要是:开多个go协程去获取多个redis的数据,然后将数据写入到一个chan里面,然后主线程用一个for无限循环等待数据写入,发现有数据进来就将数据设置进去。
还使用了sync.WateGroup,这样在完成所有数据写入chan之后,关闭chan,然后当无限for循环读完一个已经关闭的chan时,自动退出。
具体代码如下:
#################################################华丽的分割线###############################################
package main
import (
"fmt"
"github.com/garyburd/redigo/redis"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
var wg sync.WaitGroup
// 定义需要被收集的信息的结构体
type Metric struct {
redisHost string
ConnectedClients float64
DbSize float64
UsedMemory float64
}
func getRedisInfo(redisHost string) (*Metric, error) {
// 连接redis
rs, err := redis.Dial("tcp", redisHost)
// 操作完后自动关闭
defer rs.Close()
// 若连接出错,则打印错误信息,返回
if err != nil {
log.Fatalf("connect redis failed, err:%v", err)
return nil, err
}
// 获取info,然后过滤出需要的信息
rs.Send("MULTI")
rs.Send("info")
info, err := redis.Strings(rs.Do("EXEC"))
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
}
// 获取info信息,并且切分用于遍历分析
infoSlice := strings.Split(info[0], "\r\n")
var (
connectedClients float64 = 0
usedMemory float64 = 0
)
for _, dbInfo := range infoSlice {
splitInfo := strings.Split(dbInfo, ":")
switch splitInfo[0] {
case "connected_clients":
// 获取到connectedClients
connectedClients, _ = strconv.ParseFloat(splitInfo[1], 64)
case "used_memory":
// usedMemory
usedMemory, _ = strconv.ParseFloat(splitInfo[1], 64)
}
}
// 获取dbSize的数字
rs.Send("MULTI")
rs.Send("dbSize")
dbSize, err := rs.Do("EXEC")
if err != nil {
log.Fatalf("get redis info failed, err:%v", err)
return nil, err
}
strBbSize := strings.Trim(fmt.Sprintf("%v", dbSize), "[]")
intDbSize, _ := strconv.ParseFloat(strBbSize, 64)
return &Metric{
redisHost: redisHost,
ConnectedClients: connectedClients,
DbSize: intDbSize,
UsedMemory: usedMemory,
}, nil
}
func setMetricsInfo(redisMetrics *Metric) {
// 创建需要被收集的指标变量,可以附带相关摘要,都是gauge这种,如果使用prometheus.NewGauge则需要自己注册
// 反之,如果使用promauto.NewGauge则自动注册
// 因为我们需要在注册的时候放入其他相关标签(比如传入redisHost)
var (
connectedClients = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_connected_clients",
Help: "this_is_redis_connected_clients",
ConstLabels: map[string]string{"redis_port": redisMetrics.redisHost},
})
dbSize = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_dbSize",
Help: "this_is_redis_dbSize",
ConstLabels: map[string]string{"redis_port": redisMetrics.redisHost},
})
usedMemory = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "redis_used_memory",
Help: "this_is_redis_used_memory",
ConstLabels: map[string]string{"redis_port": redisMetrics.redisHost},
})
)
// 初始化就直接注册上面列出的指标,上面也描述了,因为没有使用auto所以需要自己手动注册
prometheus.MustRegister(connectedClients)
prometheus.MustRegister(dbSize)
prometheus.MustRegister(usedMemory)
connectedClients.Set(redisMetrics.ConnectedClients)
dbSize.Set(redisMetrics.DbSize)
usedMemory.Set(redisMetrics.UsedMemory)
}
func sendInfoToChan(mCh chan<- *Metric, redisHost string) {
// 从redis中获取指标
redisMetrics, err := getRedisInfo(redisHost)
if err != nil {
log.Fatalf("get redis metrics failed, err:%v", err)
}
// 将指标发送给一个通道
mCh <- redisMetrics
wg.Done()
}
func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
// 定义一个redisPort的切片,里面装了多个需要被监控的redis
redisHostSlice := []string{"192.168.235.131:6379", "192.168.235.131:8888"}
// 定义一个通道,容量为10,用来接收查询到的数据
metricsCh := make(chan *Metric, 10)
for _, redisHost := range redisHostSlice {
wg.Add(1)
go sendInfoToChan(metricsCh, redisHost)
}
wg.Wait()
close(metricsCh)
// 将数据set到prometheus中
for {
for data := range metricsCh {
// 从通道中取出数据,执行set
setMetricsInfo(data)
}
fmt.Println("数据设置完毕")
break
}
// 把go语言自带的指标也发上去 ,注意,这个指标只能发一次,不可以循环多次,因此把他从循环中拿出来,单放这个位置。
prometheus.MustRegister(prometheus.NewBuildInfoCollector())
// promhttp.Handler()这个返回一个http.Handler,这个Handler接口有一个ServeHTTP方法,然后把w,r传进去就行
promhttp.Handler().ServeHTTP(w, r)
// 使用net/http包的话,请求过来的信息需要自己处理,输入到log记录发送的请求
fmt.Println(time.Now().Format("2006/01/02/ 03:04:05"), r.Method, r.URL)
}
func main() {
// 给一个点击链接的接口
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
redis exporter
click redis exporter
"/metrics" + `">Metrics
`))
})
// 真实的请求metrics的接口
http.HandleFunc("/metrics", getMetricsHandler)
http.ListenAndServe(":8081", nil)
}
同理我们就可以写很多exporter了。
后续升级:
1.这里只是用的redis的包,如果需要做redis集群还需要redis集群的包:github.com/chasex/redis-go-cluster
2.连接redis集群如果超时则结束对该集群的信息访问。