本文主要介绍Thanos在TKEStack中的应用和实践。Thanos是一个CNCF沙箱项目,旨在提供Prometheus的增强功能,包括全局视角、高可用、历史数据查询等功能。TKE(Tencent Kubernetes Engine)是腾讯云容器服务的简称,TKEStack是其开源版本,致力于提供一个支持多租户多集群的原生Kubernetes容器平台。
在Kubernetes监控场景下,Prometheus已经成为事实上的标准,TKEStack也不例外,其监控功能完全基于Prometheus构建。然而Prometheus对于高可用、全局视角以及历史数据存储没有一套完整的解决方案,为了解决这些问题,多个开源项目应运而生,Thanos就是其中一个。接下来我们首先介绍Thanos整体架构,然后讨论一下现有架构在TKEStack中遇到的问题,最后以TKEStack为例看Thanos的真实案例。
Thanos整体架构
这是Thanos官网给出的整体架构图:
为了解决历史数据查询问题,需要将Prometheus本地的数据收集到一个长期存储中,而Prometheus的tsdb数据是以一个一个的block的形式存在,天然适合对象存储的方式,因此Thanos选择了对象存储,目前已支持各大云计算厂商的对应产品。但是Prometheus本身不支持把数据写到对象存储中,这个时候其实有两种思路:
- 使用另一个组件,将Prometheus写到本地存储中的数据上传到对象存储
- 利用Prometheus 的remote write功能,将数据复制一份到一个中转服务,再由中转服务上传至对象存储中
Thanos选择的是第一种方案,即sidecar模式(为了解决sidecar模式的问题,Thanos也根据方案二准备了receive模式,还未正式发布,后文会介绍)。Sidecar是作为辅助容器运行在Prometheus的pod中,直接读取tsdb上传block到对象存储,至此解决了数据的长期存储问题。那么对象存储中的历史数据如何查询呢?很自然的想到需要一个组件能读取对象存储的数据并解析,最后根据用户的查询请求返回对应的时间序列,这个组件就是Thanos store gateway。
第二个需求是全局视角,即用户希望从同一个入口查询到所有的Prometheus实例及其历史数据。一个简单的方案就是提供一个组件,将用户请求转发到所有的Prometheus以及Thanos store gateway,再汇总数据返回给用户。Thanos query组件就是采用了类似的方案,不过为了性能考虑,Thanos query并没有直接对接Prometheus的HTTP APi,而是对接了由sidecar暴露的gRPC API,这个API在Thanos中叫做store API。这个API在Thanos中使用广泛,前文提到的store gateway对外也是提供这个API。这样的好处是对Thanos query来说,不论是Prometheus还是对象存储,都是一个个提供了store API的endpoint,在逻辑上完全等价的,不需要做特殊处理。
最后是高可用,Prometheus的高可用方案比较简单,就是启动多个相同配置的实例,这个时候带来的问题是数据出现重复,因此查询需要能够支持去重。因为Thanos query具有全局视角,去重的实现就比较容易了。只需要给每组互为备份的Prometheus打上用于区分的label,在query的查询阶段就可以根据label来将重复数据过滤。比如给两个Prometheus分别打上label replica=0,replica=1,当获取的数据中两个series仅仅是replica这个label不同,就可以认为这两个series是重复的,只返回其中一个即可。
Thanos sidecar,query和store gateway,这三个组件就组成了Thanos的核心,另外还有Rule组件可以基于全局数据告警,以及Compact组件用来合并对象存储上的小block和提供downsampling功能,这两个组件的功能不是必须的,这里就不再详细介绍。
Sidecar模式在TKEStack中的问题
在分析sidecar模式的问题前,先简单介绍一下TKEStack的整体架构:
Global集群作为管理集群,运行着TKEStack的各个服务,用来管理其他所有业务集群。对于监控系统来说,Prometheus运行在业务集群中,用来收集监控数据;Thanos各组件都运行在global集群,TKEStack的控制台通过Thanos query获取各个集群的监控数据并展示。看起来整体架构是OK的,但在实际使用中我们遇到下面的问题:
- Thanos query连接哪些store API endpoint是由其配置文件决定的,这些endpoint如果有新增、变更或者删除都需要更新配置文件。因为集群会不断新增、变化或者删除,endpoint的变化不可避免,这就导致了运维自动化的困难。Thanos官方提供了一种解决方案是利用consul做服务发现来代替配置文件更新,但这样又需要引入consul这个组件,增加了运维管理的成本和难度
- Prometheus的高可用变得非常重要。Prometheus本地的数据分为两种,一种是已经固化的block,还有一种是正在写入的WAL。其中WAL中存储的是当前最新的数据,这部分数据会不断变化,而超过一定时限的数据会固化为block,block是只读的。因为对象存储的不可修改特性,Thanos sidecar只能上传已经固化的block,因此对象存储中其实永远没有最新的这部分数据,这些数据都在一个个的Prometheus的WAL中。为了保证这些最新数据的可靠性,Prometheus要么使用共享存储,要么部署多实例,这些方案有的会增加运维复杂度,有的会增加几倍的数据存储消耗以及内存消耗(大规模k8s集群中Prometheus的内存消耗相当可观)。
为了解决以上问题,我们尝试寻找利用Prometheus remote write的解决方案,发现Thanos社区已经提出了receive组件,该组件能较好的解决上面的两个问题。使用Thanos receive后,整体的架构如下:
Receive组件实现了remote write接口,Prometheus可以将数据实时推送到Receive上;Receive本身实际上相当于一个没有收集功能的Prometheus,它接受到的数据同样通过tsdb写到本地WAL,然后超过一定时限后生成block,这些block会上传到对象存储。这样处理的结果是,最新的数据在receive组件上可以获取到,Prometheus的最新数据如果丢失并不影响,因此Prometheus的高可用性可以适当降低。同时因为query和receive部署在同一个k8s集群中,可以使用k8s的dnssrv功能做服务发现,receive组件的变化本身也比较少,这样就比较好的解决了第一个问题。
Receive组件本身的高可用也有一定的支持,目前可以使用hashring的方式组织多个Receive实例来实现分布式服务,具体的配置将在下一节详细介绍。
Thanos在TKEStack中的实际部署
核心组件是Thanos receive,使用statefulset部署多个实例,每个实例都使用hostpath本地存储。Receive组件可以通过hashring实现多副本冗余,下面的例子中为了节省存储空间没有做多副本配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
apiVersion: v1
kind: ConfigMap
metadata:
name: thanos-receive-hashrings
namespace: kube-system
data:
thanos-receive-hashrings: |
[
{
"hashring": "soft-tenants",
"endpoints":
[
"thanos-receive-0.thanos-receive.kube-system.svc.cluster.local:10901",
"thanos-receive-1.thanos-receive.kube-system.svc.cluster.local:10901",
"thanos-receive-2.thanos-receive.kube-system.svc.cluster.local:10901"
]
}
]
|
Hashring中配置了3个节点,节点的地址是利用了k8s的dnssrv通过headless service实现的,接下来准备对象存储配置文件,这里我们采用了腾讯云COS:
1
2
3
4
5
6
7
|
type: COS
config:
bucket: "tkestack"
region: "ap-shanghai"
app_id: "xxxxxx"
secret_key: "xxxxxx"
secret_id: "xxxxxxx" |
将以上信息写到thanos-object.yaml,创建对应的secret:
1
|
kubectl create secret generic thanos-objstore-config -n kube-system --from-file=thanos-object.yaml
|
创建statefulset,注意receive.local-endpoint参数务必和hashring中的endpoint一致,receive需要根据这个判断自己在hashring中的位置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
kubernetes.io/name: thanos-receive
name: thanos-receive
namespace: kube-system
spec:
replicas: 3
selector:
matchLabels:
kubernetes.io/name: thanos-receive
serviceName: thanos-receive
template:
metadata:
labels:
kubernetes.io/name: thanos-receive
spec:
containers:
- args:
- receive
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:10902
- --remote-write.address=0.0.0.0:19291
- --objstore.config=$(OBJSTORE_CONFIG)
- --tsdb.path=/var/thanos/receive
- --tsdb.retention=2h
- --label=replica="$(NAME)"
- --label=receive="true"
- --receive.hashrings-file=/etc/thanos/thanos-receive-hashrings
- --receive.local-endpoint=$(NAME).thanos-receive.kube-system.svc.cluster.local:10901
env:
- name: NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OBJSTORE_CONFIG
valueFrom:
secretKeyRef:
key: thanos-object.yaml
name: thanos-objstore-config
image: thanosio/thanos:v0.11.0
livenessProbe:
failureThreshold: 100
httpGet:
path: /-/healthy
port: 10902
scheme: HTTP
periodSeconds: 30
name: thanos-receive
ports:
- containerPort: 10901
name: grpc
- containerPort: 10902
name: http
- containerPort: 19291
name: remote-write
readinessProbe:
httpGet:
path: /-/ready
port: 10902
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 30
resources:
limits:
cpu: "8"
memory: 48Gi
requests:
cpu: "4"
memory: 8Gi
volumeMounts:
- mountPath: /var/thanos/receive
name: thanos-receive-data
readOnly: false
- mountPath: /etc/thanos/
name: thanos-receive-hashrings
terminationGracePeriodSeconds: 120
volumes:
- name: thanos-receive-data
hostPath:
path: /data/thanos-receive
type: DirectoryOrCreate
- configMap:
defaultMode: 420
name: thanos-receive-hashrings
name: thanos-receive-hashrings
|
创建两个service,分别用于query和Prometheus的访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/port: "10902"
prometheus.io/scrape: "false"
labels:
kubernetes.io/cluster-service: "true"
kubernetes.io/name: thanos-receive
name: thanos-receive
namespace: kube-system
spec:
ports:
- name: http
port: 10902
protocol: TCP
targetPort: 10902
- name: remote-write
port: 19291
protocol: TCP
targetPort: 19291
- name: grpc
port: 10901
protocol: TCP
targetPort: 10901
selector:
kubernetes.io/name: thanos-receive
clusterIP: None
-----------------------
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/cluster-service: "true"
kubernetes.io/name: thanos-receive-nodeport
name: thanos-receive-nodeport
namespace: kube-system
spec:
ports:
- name: http
port: 10902
protocol: TCP
targetPort: 10902
- name: remote-write
port: 19291
protocol: TCP
targetPort: 19291
- name: grpc
port: 10901
protocol: TCP
targetPort: 10901
selector:
kubernetes.io/name: thanos-receive
type: NodePort
|
注意第一个service是headless service(clusterIP: None),第二个service是nodeport类型,便于跨集群访问。接下来是store-gateway:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/component: object-store-gateway
app.kubernetes.io/instance: thanos-store
app.kubernetes.io/name: thanos-store
app.kubernetes.io/version: v0.11.0
name: thanos-store
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: object-store-gateway
app.kubernetes.io/instance: thanos-store
app.kubernetes.io/name: thanos-store
serviceName: thanos-store
template:
metadata:
labels:
app.kubernetes.io/component: object-store-gateway
app.kubernetes.io/instance: thanos-store
app.kubernetes.io/name: thanos-store
app.kubernetes.io/version: v0.11.0
spec:
containers:
- args:
- store
- --data-dir=/var/thanos/store
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:10902
- --objstore.config=$(OBJSTORE_CONFIG)
- --experimental.enable-index-header
env:
- name: OBJSTORE_CONFIG
valueFrom:
secretKeyRef:
key: thanos-object.yaml
name: thanos-objstore-config
image: thanosio/thanos:v0.11.0
livenessProbe:
failureThreshold: 8
httpGet:
path: /-/healthy
port: 10902
scheme: HTTP
periodSeconds: 30
name: thanos-store
ports:
- containerPort: 10901
name: grpc
- containerPort: 10902
name: http
readinessProbe:
failureThreshold: 20
httpGet:
path: /-/ready
port: 10902
scheme: HTTP
periodSeconds: 5
terminationMessagePolicy: FallbackToLogsOnError
volumeMounts:
- mountPath: /var/thanos/store
name: data
readOnly: false
terminationGracePeriodSeconds: 120
nodeName: testnode
volumes:
- name: data
hostPath:
path: /data/thanos-store
type: DirectoryOrCreate
--------------------------------------------
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: object-store-gateway
app.kubernetes.io/instance: thanos-store
app.kubernetes.io/name: thanos-store
app.kubernetes.io/version: v0.11.0
name: thanos-store
namespace: kube-system
spec:
clusterIP: None
ports:
- name: grpc
port: 10901
targetPort: 10901
- name: http
port: 10902
targetPort: 10902
selector:
app.kubernetes.io/component: object-store-gateway
app.kubernetes.io/instance: thanos-store
app.kubernetes.io/name: thanos-store
|
Store gateway进行了简单处理,指定了node并使用hostpath,因为主要是为历史数据查询服务,对高可用的要求并不高。最后是thanos query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
kubernetes.io/name: thanos-querier
name: thanos-querier
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
kubernetes.io/name: thanos-querier
template:
metadata:
labels:
kubernetes.io/name: thanos-querier
spec:
containers:
- args:
- query
- --query.replica-label=prometheus_replica
- --query.replica-label=replica
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:9090
- --store=dnssrv+_grpc._tcp.thanos-store.kube-system.svc.cluster.local
- --store=dnssrv+_grpc._tcp.thanos-receive.kube-system.svc.cluster.local
- --query.replica-label=ruler_replica
image: thanosio/thanos:v0.11.0
livenessProbe:
failureThreshold: 4
httpGet:
path: /-/healthy
port: 9090
scheme: HTTP
periodSeconds: 30
name: thanos-querier
ports:
- containerPort: 10901
name: grpc
- containerPort: 9090
name: http
readinessProbe:
httpGet:
path: /-/ready
port: 9090
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 30
resources:
limits:
cpu: "4"
memory: 8Gi
requests:
cpu: "2"
memory: 2Gi
terminationGracePeriodSeconds: 120
--------------------------------
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/name: thanos-querier
name: thanos-querier
namespace: kube-system
spec:
ports:
- name: grpc
port: 10901
targetPort: grpc
- name: http
port: 9090
targetPort: http
selector:
kubernetes.io/name: thanos-querier
|
最后修改Prometheus的remote write配置,将数据写入thanos receive的nodeport service暴露的接口中,整个监控的部署就完成了。