时序时空数据库TSDB简介和使用方案可以看文档 https://www.docs4dev.com/docs/zh/opentsdb/2.3/reference
TSDB对外只提供restful api,而且TSDB作为时序数据库,在后台的微服务的使用大部分场景只做查询,数据的写入交由其他的数据采集服务负责。我们之前有个使用场景是通过canal去监听mysql binlog,将实时的数据写入TSDB中。由其他服务通过http post方式去读取。
阿里云提供的TSDB HTTP API方法看以看https://help.aliyun.com/document_detail/63557.html?spm=a2c4g.11186623.6.643.1c3641eboXiKe6
TSDB查询的http api方法如下:
http://ip:port/api/query
TSDB查询的包体如下:
/*单值查询*/
type Query struct {
Aggregator string `json:"aggregator"` //表示 OpenTSDB 查询时的预聚合方法,
Metric string `json:"metric"` //指标名
Rate bool `json:"rate,omitempty"` //是否计算指定指标值的增长速率;计算公式: Vt-Vt-1/t1-t-1
Tags map[string]string `json:"tags,omitempty"` //指定标签过滤,和filters冲突,这里用tags,不用filters
Downsample string `json:"downsample,omitempty"` //表示 OpenTSDB 查询时的采样方法,目前统一设置为: 0all-last 全局采样
Limit int `json:"limit,omitempty"` //数据分页,子查询返回数据点的最大数目
Offset int `json:"offset,omitempty"` //数据分页,子查询返回数据点的偏移量
DpValue string `json:"dpValue,omitempty"` //根据提供条件过滤返回数据点,支持”>”, “<”, “=”,”<=”, “>=”, “!=”。
}
type QueryParams struct {
Start interface{} `json:"start"`
End interface{} `json:"end,omitempty"`
Queries []Query `json:"queries,omitempty"`
}
Metric是查询你需要的 指标名,可以理解为mysql或者mongodb里面的表,这些表里面有很多字段,想要查询的字段写在Tags中
TSDB查询成功会返回这样的参数:
type QueryResponse struct {
Metric string `json:"metric"`
Tags map[string]string `json:"tags"`
AggregateTags []string `json:"aggregateTags"`
Dps map[string]float64 `json:"dps"`
}
如果查询失败不是直接在http上返回error,而是直接返回一个error的报文:
/*返回错误结构体*/
type QueryErrorMsg struct {
Error ErrorMsg `json:"error"`
}
type ErrorMsg struct {
Code int `json:"code"` //错误码
Message string `json:"message"` //错误消息
Trace string `json:"trace"` //打印的服务端堆栈信息
}
TSDB 时序数据库的含义指它能方便的查出从数据第一次写入时间到任意时间段的值,比如我们需要获取哪一天的哪一秒的值,就可以构造这样的参数:这里用curl 先模拟一下:
curl -X POST -H "Accept:application/json" -H "Content-Type:application/json" http://10.1.0.11:4242/api/query -d '
{
"start": "1474387200000",
"end": "1537459200000",
"queries": [
{
"aggregator": "last", //聚合方法,有sum , count , min, max,last等等,具体含义看上面给的链接
"downsample": "0all-last", //降采样 (Downsample) 说明,具体如下,这里设置为0all-last表示全局采样
"index": 0,
"metric": "xxxxxx", //这里用指标名,写入的时候设置的值
"tags": {
"field": "name" //这里如果需要查询多个值可以用 | ,比如同时查询name 和year 可以用"name|year",如果设置为”*“表示查询所有值
}
}
]
}
'
返回:
[
{
"metric": "xxxxxx",
"tags": {
"field": "name"
},
"aggregateTags": [
],
"dps": {
"1474387200": 1231.9999999999998
}
}
]
dps的value表示查询得到的值
在线上服务的使用过程我们不能每来一个请求就创建一个http连接去请求TSDB,再加上后台可能有多台TSDB服务器做集群,为了保证高可用,所以需要做连接池,负载均衡,错误重试,超时调度,熔断等等一系列措施。
首先我们设计一个TSDB Balance,主要用来做负载均衡。这里可以为每一个TSDB的ip设计相应的权重,如果某台TSDB服务器出现故障,可以自动的降低权重,并且可以设计一个配置的超时时间恢复该连接的权重,这个时间对TSDB服务器进行修复,这样就可以达到高可用的目的。
type TsdbInstance struct {
Host string
Weight int //配置权重
CurrentWeight int //当前权重
}
type TsdbBalance struct {
sync.RWMutex
ip []*TsdbInstance
}
func NewTsdbBalance() *TsdbBalance {
Tsdb := &TsdbBalance{}
Tsdb.ip = make([]*TsdbInstance, 0)
return Tsdb
}
func (t *TsdbBalance) Register(ts []TsdbInstance) {
for i := 0; i < len(ts); i++ {
t.ip = append(t.ip, &ts[i])
}
}
func (t *TsdbBalance) GetRandTsdbInstance() *TsdbInstance {
if len(t.ip) == 0 {
return nil
}
var best *TsdbInstance
total := 0
t.RLock()
for i := 0; i < len(t.ip); i++ {
instance := t.ip[i]
instance.CurrentWeight += instance.Weight
total += instance.Weight
if best == nil || instance.CurrentWeight > best.CurrentWeight {
best = instance
}
}
t.RUnlock()
if best == nil {
return nil
}
best.CurrentWeight -= total
return best
}
func (t *TsdbBalance) UpDateWeight(ts *TsdbInstance) error {
for i := 0; i < len(t.ip); i++ {
if t.ip[i].Host == ts.Host {
t.Lock()
t.ip[i].Weight = ts.Weight
t.Unlock()
return nil
}
}
return fmt.Errorf("tsdb instance not found ")
}
对于TSDB服务器请求,需要设计一个连接池去进行连接的复用,可以使用net/http自带的连接池:
func createHTTPClient() *http.Client {
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: MaxIdleConns, //最大空闲连接数
MaxIdleConnsPerHost: MaxIdleConnsPerHost, //最大与服务器的连接数 默认是2
IdleConnTimeout: IdleConnTimeout * time.Second, //空闲连接保持时间
},
}
return client
}
这里参数需要对TSDB服务器所在的机器配置进行合理的评估,通过压测分析最合理的配置~~,不然会发现和不用连接池的时间差别不大。连接池主要作用是连接的复用,合理配置参数才能有更高的效率,不要盲目的套用网上某些博客上配置。
请求连接的过程如下:
q := &QueryParams{
Start: v.StartTime,
End: v.EndTime,
}
query := Query{
Aggregator: aggregator,
Downsample: downsample,
Metric: "xxxxxx",
Tags: map[string]string{"field": v.FieldName},
}
q.Queries = append(q.Queries, query)
data, err := json.Marshal(*q)
if err != nil {
return nil, err
}
instance := tsdbBalance.GetRandTsdbInstance()
req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/query", instance.Host), bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var rsp []QueryResponse
err = json.Unmarshal(body, &rsp)
if err != nil {
var errMsg QueryErrorMsg
err = json.Unmarshal(body, &errMsg)
if err != nil {
return nil, err
}
/*如果是服务端出错,应该中断与该服务端的连接*/
if errMsg.Error.Code == 500 || errMsg.Error.Code == 501 {
err := tsdbBalance.UpDateWeight(&TsdbInstance{
Host: instance.Host,
Weight: 0,
})
if err != nil {
return nil, err
}
} else if errMsg.Error.Code == 503 { /*如果是服务端过载,应该降低与该服务端连接的权重,暂时先除2,后期设计一个定时系统恢复权重*/
_ = tsdbBalance.UpDateWeight(&TsdbInstance{
Host: instance.Host,
Weight: instance.Weight / 2,
})
} else {
return nil, err
}
}