本文主要介绍ThanosTKEStack中的应用和实践。Thanos是一个CNCF沙箱项目,旨在提供Prometheus的增强功能,包括全局视角、高可用、历史数据查询等功能。TKE(Tencent Kubernetes Engine)是腾讯云容器服务的简称,TKEStack是其开源版本,致力于提供一个支持多租户多集群的原生Kubernetes容器平台。

在Kubernetes监控场景下,Prometheus已经成为事实上的标准,TKEStack也不例外,其监控功能完全基于Prometheus构建。然而Prometheus对于高可用、全局视角以及历史数据存储没有一套完整的解决方案,为了解决这些问题,多个开源项目应运而生,Thanos就是其中一个。接下来我们首先介绍Thanos整体架构,然后讨论一下现有架构在TKEStack中遇到的问题,最后以TKEStack为例看Thanos的真实案例。

Thanos整体架构

这是Thanos官网给出的整体架构图:

architecture overview

为了解决历史数据查询问题,需要将Prometheus本地的数据收集到一个长期存储中,而Prometheus的tsdb数据是以一个一个的block的形式存在,天然适合对象存储的方式,因此Thanos选择了对象存储,目前已支持各大云计算厂商的对应产品。但是Prometheus本身不支持把数据写到对象存储中,这个时候其实有两种思路:

  1. 使用另一个组件,将Prometheus写到本地存储中的数据上传到对象存储
  2. 利用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的整体架构:

Architecture Of TKE

Global集群作为管理集群,运行着TKEStack的各个服务,用来管理其他所有业务集群。对于监控系统来说,Prometheus运行在业务集群中,用来收集监控数据;Thanos各组件都运行在global集群,TKEStack的控制台通过Thanos query获取各个集群的监控数据并展示。看起来整体架构是OK的,但在实际使用中我们遇到下面的问题:

  1. Thanos query连接哪些store API endpoint是由其配置文件决定的,这些endpoint如果有新增、变更或者删除都需要更新配置文件。因为集群会不断新增、变化或者删除,endpoint的变化不可避免,这就导致了运维自动化的困难。Thanos官方提供了一种解决方案是利用consul做服务发现来代替配置文件更新,但这样又需要引入consul这个组件,增加了运维管理的成本和难度
  2. Prometheus的高可用变得非常重要。Prometheus本地的数据分为两种,一种是已经固化的block,还有一种是正在写入的WAL。其中WAL中存储的是当前最新的数据,这部分数据会不断变化,而超过一定时限的数据会固化为block,block是只读的。因为对象存储的不可修改特性,Thanos sidecar只能上传已经固化的block,因此对象存储中其实永远没有最新的这部分数据,这些数据都在一个个的Prometheus的WAL中。为了保证这些最新数据的可靠性,Prometheus要么使用共享存储,要么部署多实例,这些方案有的会增加运维复杂度,有的会增加几倍的数据存储消耗以及内存消耗(大规模k8s集群中Prometheus的内存消耗相当可观)。

为了解决以上问题,我们尝试寻找利用Prometheus remote write的解决方案,发现Thanos社区已经提出了receive组件,该组件能较好的解决上面的两个问题。使用Thanos receive后,整体的架构如下:

tkestack-thanos-arch

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暴露的接口中,整个监控的部署就完成了。