尝试用go语言写一个获取redis指标的redis-exporter

公司里面的中间件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:

0.下载依赖:

go get -u github.com/garyburd/redigo/redis

1.连接redis数据库,用go获取需要被展示的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的相关接口。

2.将数据发给指标

先准备一个结构体,把需要获取的东西放进去,因为后面需要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)

3.提供metrics接口等待prometheus调用。这里我们net/http处理请求

提供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集群如果超时则结束对该集群的信息访问。

你可能感兴趣的:(go,exporter,redis)