Clickhouse作为Kubernetes日志管理解决方案中的存储


ELK技术栈(尤其是Elasticsearch)是最流行的日志管理解决方案。但是在生产环境中运行Elasticsearch几年之后,有以下几个问题:
  • 这是一项复杂的技术。此外,关于内部结构的文档也不是很清楚。通常,你需要深厚的专业知识才能驾驭。

  • 尽管它提供了分片所需的工具,但是这并不是代表这是一件简单的事。在遇到容量问题或是其他阻塞问题时,很难进行调试和故障排查。

  • 需要花费大量的精力来配置和维护索引和映射等。否则与资源使用情况相比,其性能不好。这使其成为了一种昂贵的解决方案。


我认为它成为日志处理热门选项的原因是:
  • 默认的安装和配置非常简单,你可以使服务非常轻松地运行,
    它与Logstash和Kibana组成了一个日志处理的生态系统。
  • 由于它具有面向文档的数据库性质,因此非常灵活,尤其是在你的日志架构频繁更改(“字段”中的更改)的情况下。


如果你需要快速有效的解决方案,那么这是一种出色的技术。另外,如果你的平台要求不是很高,则可以不用花很多时间进行配置和优化。

但是某些场景下
  • 日志量非常大。
  • 日志结构和存储信息的方式不经常更改。
  • 日志处理不是我们的核心业务,我们并不需要全文索引。
  • 除了数据查询,我们需要丰富的聚合和数学函数进行日志分析。


我们不能享受Elasticsearch的优势带来的好处,反而会极大增加我们的日志存储成本。那么在以上特定场景下,尤其是Kubernetes场景下,是否存在另外一种低成本而有效的解决方案那?

社区中已经存在 Loki 专门为收集Kubernetes pod 日志的解决方案。Loki是受Prometheus启发,水平可扩展,高可用的多租户日志聚合系统。它的设计具有很高的成本效益,并且易于操作。它不索引日志的内容,而是为每个日志流设置一组标签。相比ELK,Loki具有以下特点:

  • 不对日志进行全文索引。通过压缩并仅索引元数据,Loki更加易于操作且运行成本更低。

  • 使用与Prometheus相同的标签对日志流进行索引和分组,从而使你能够使用与Prometheus相同的标签在指标和日志之间无缝切换。

  • 特别适合存储Kubernetes Pod日志。诸如Pod标签之类的元数据会自动被抓取并建立索引。

  • Grafana v6.0+ 原生支持Loki。


不过Loki目前正在开发中,不推荐生产环境使用。

参照Loki的思路,今天我们来探索和落地使用Clickhouse作为Kubernetes日志管理解决方案中的存储。

为什么是Clickhouse?

ClickHouse 是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。Clickhouse 具备一些独特的功能:

  • 真正的列式数据库管理系统。

  • 数据压缩。若想达到比较优异的性能,数据压缩确实起到了至关重要的作用。

  • 数据的磁盘存储。许多的列式数据库(如 SAP HANA, Google PowerDrill)只能在内存中工作,这种方式会造成比实际更多的设备预算。ClickHouse被设计用于工作在传统磁盘上的系统,它提供每GB更低的存储成本,但如果有可以使用SSD和内存,它也会合理的利用这些资源。

  • 多核心并行处理。ClickHouse会使用服务器上一切可用的资源,从而以最自然的方式并行处理大型查询。

  • 多服务器分布式处理。在ClickHouse中,数据可以保存在不同的shard上,每一个shard都由一组用于容错的replica组成,查询可以并行地在所有shard上进行处理。这些对用户来说是透明的。
  • 支持SQL。ClickHouse支持基于SQL的声明式查询语言,该语言大部分情况下是与SQL标准兼容的。 支持的查询包括 GROUP BY,ORDER BY,IN,JOIN以及非相关子查询。

  • 实时的数据更新。ClickHouse支持在表中定义主键。为了使查询能够快速在主键中进行范围查找,数据总是以增量的方式有序的存储在MergeTree中。因此,数据可以持续不断地高效的写入到表中,并且写入的过程中不会存在任何加锁的行为。

  • 索引。按照主键对数据进行排序,这将帮助ClickHouse在几十毫秒以内完成对数据特定值或范围的查找。

  • 支持数据复制和数据完整性。ClickHouse使用异步的多主复制技术。当数据被写入任何一个可用副本后,系统会在后台将数据分发给其他副本,以保证系统在不同副本上保持相同的数据。在大多数情况下ClickHouse能在故障后自动恢复,在一些少数的复杂情况下需要手动恢复。


其实一句话总结,Clickhouse 是一个支持sql查询的海量数据存储的高性能高可用的列式的分析数据库。

与Loki类比,使用Clickhouse 存储Kubernetes 日志,并不对全文索引,利用Regex 查询代替全文索引,只对Pod标签之类的元数据(namespace,pod_name,container_name等)进行索引,提高查询速度。更好的数据压缩,对比Elasticserach,意味着更小的磁盘和内存使用。

方案设计

cbs.png


关于此架构,有以下几点:

  • 每个Kubernetes集群通过DaemonSet方式部署Flunet bit。负责收集日志并写到Clickhouse集群中。关于Flunet bit 需要定制开发Clickhouse的output插件,这将在下面详细讲述。

  • 由于Clickhouse 出色的写入性能,目前我们没有使用kafka。

  • Clickhouse 集群部署,需要zk集群做一致性表数据复制。


Clickhouse 高可用集群部署

而clickhouse 的集群示意图如下:

ck-cluster.png

  • ReplicatedMergeTree + Distributed。ReplicatedMergeTree里,共享同一个ZK路径的表,会相互复制,注意是,相互同步数据。
  • 每个IDC有3个分片,各自占1/3数据。实际在公有云上,可以是三个AZ。
  • 每个节点,依赖ZK,各自有2个副本。

  • 写入的时候,通过DNS轮询或是负载均衡的方式写本地表。实际使用中,通过DNS轮询的方式,保证多个分片的数据均衡的效果小于负载均衡。

  • 读取的时候,读取Distributed表。Clickhouse会自动做聚合。


此处关于Clickhouse 用户配置方面,出于安全考虑,我们配置了users.xml文件,增加了只读用户,并且设置了密码。只读用户在查询端使用。具体如下:
<?xml version="1.0"?>
<yandex>
<!-- Profiles of settings. -->
<profiles>
    <!-- Default settings. -->
    <default>
        <!-- Maximum memory usage for processing single query, in bytes. -->
        <max_memory_usage>10000000000</max_memory_usage>

        <!-- Use cache of uncompressed blocks of data. Meaningful only for processing many of very short queries. -->
        <use_uncompressed_cache>0</use_uncompressed_cache>

        <!-- How to choose between replicas during distributed query processing.
             random - choose random replica from set of replicas with minimum number of errors
             nearest_hostname - from set of replicas with minimum number of errors, choose replica
              with minimum number of different symbols between replica's hostname and local hostname
              (Hamming distance).
             in_order - first live replica is chosen in specified order.
             first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
        -->
        <load_balancing>random</load_balancing>
    </default>

    <!-- Profile that allows only read queries. -->
    <readonly>
        <readonly>1</readonly>
    </readonly>
</profiles>

<!-- Users and ACL. -->
<users>
    <!-- If user name was not specified, 'default' user is used. -->
    <default>
        <!-- Password could be specified in plaintext or in SHA256 (in hex format).

             If you want to specify password in plaintext (not recommended), place it in 'password' element.
             Example: <password>qwerty</password>.
             Password could be empty.

             If you want to specify SHA256, place it in 'password_sha256_hex' element.
             Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
             Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).

             If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
             Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>

             How to generate decent password:
             Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
             In first line will be password and in second - corresponding SHA256.

             How to generate double SHA1:
             Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | openssl dgst -sha1 -binary | openssl dgst -sha1
             In first line will be password and in second - corresponding double SHA1.
        -->
        <password>xxxxxx</password>

        <!-- List of networks with open access.

             To open access from everywhere, specify:
                <ip>::/0</ip>

             To open access only from localhost, specify:
                <ip>::1</ip>
                <ip>127.0.0.1</ip>

             Each element of list has one of the following forms:
             <ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
                 2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
             <host> Hostname. Example: server01.yandex.ru.
                 To check access, DNS query is performed, and all received addresses compared to peer address.
             <host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.yandex\.ru$
                 To check access, DNS PTR query is performed for peer address and then regexp is applied.
                 Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
                 Strongly recommended that regexp is ends with $
             All results of DNS requests are cached till server restart.
        -->
        <networks incl="networks" replace="replace">
            <ip>::/0</ip>
        </networks>

        <!-- Settings profile for user. -->
        <profile>default</profile>

        <!-- Quota for user. -->
        <quota>default</quota>

        <!-- For testing the table filters -->
        <databases>
            <test>
                <!-- Simple expression filter -->
                <filtered_table1>
                    <filter>a = 1</filter>
                </filtered_table1>

                <!-- Complex expression filter -->
                <filtered_table2>
                    <filter>a + b &lt; 1 or c - d &gt; 5</filter>
                </filtered_table2>

                <!-- Filter with ALIAS column -->
                <filtered_table3>
                    <filter>c = 1</filter>
                </filtered_table3>
            </test>
        </databases>
    </default>

    <!-- Example of user with readonly access. -->
    <readonly_admin>
        <password>xxxxx</password>
        <networks incl="networks" replace="replace">
            <ip>::/0</ip>
        </networks>
        <profile>readonly</profile>
        <quota>default</quota>
    </readonly_admin>
</users>

<!-- Quotas. -->
<quotas>
    <!-- Name of quota. -->
    <default>
        <!-- Limits for time interval. You could specify many intervals with different limits. -->
        <interval>
            <!-- Length of interval. -->
            <duration>3600</duration>

            <!-- No limits. Just calculate resource usage for time interval. -->
            <queries>0</queries>
            <errors>0</errors>
            <result_rows>0</result_rows>
            <read_rows>0</read_rows>
            <execution_time>0</execution_time>
        </interval>
    </default>
</quotas>
</yandex>


当然Clickhouse 支持更加丰富的安全策略。大家可以设置不同的quotasprofiles组合不同的用户。

Fluent bit

目前社区日志采集和处理的组件不少,之前elk方案中的logstash,cncf社区中的fluentd,efk方案中的filebeat,以及大数据用到比较多的flume。而Fluent Bit是一款用c语言编写的高性能的日志收集组件,整个架构源于fluentd。官方比较数据如下:

fluent-bit.jpg


通过数据可以看出,fluent bit 占用资源更少。

fluent bit 本身是C语言编写,扩展插件有一定的难度。可能官方考虑到这一点,实现了fluent-bit-go,可以实现采用go语言来编写插件,目前只支持output的编写。

fluent bit 支持lua 编写filter,并且支持Route功能。

可以说,fluent bit 依靠强大的性能和灵活的插件扩展,逐步在日志收集领域占有一席之地。

目前fluent bit 官方没有支持clickhouse 的output 插件。需要我们自己开发。fluent-bit-clickhouse 是我们在实际落地过程中开发的一个插件。欢迎大家使用或是根据实际需求进行二次开发。

因为需要部署到k8s中,所以需要重新打镜像,Dockerfile如下:
FROM golang:1.12 AS build-env
ADD ./  /go/src/github.com/iyacontrol/fluent-bit-clickhouse
WORKDIR /go/src/github.com/iyacontrol/fluent-bit-clickhouse
RUN go build -buildmode=c-shared -o clickhouse.so .

FROM fluent/fluent-bit:1.2.2
COPY --from=build-env /go/src/github.com/iyacontrol/fluent-bit-clickhouse/clickhouse.so /fluent-bit/
CMD ["/fluent-bit/bin/fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.conf", "-e", "/fluent-bit/clickhouse.so"]


通过Dockerfile, 可以看出最终生成了一个.so库,fluent bit 在启动的时候,会加载执行。

部署

通过yaml文件部署到Kubernetes 集群中,mainfest文件如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: k8s-log-agent-config
namespace: kube
labels:
k8s-app: k8s-log-agent
data:
# Configuration files: server, input, filters and output
# ======================================================
fluent-bit.conf: |
[SERVICE]
    Flush         1
    Log_Level     error
    Daemon        off
    Parsers_File  parsers.conf
    HTTP_Server   On
    HTTP_Listen   0.0.0.0
    HTTP_Port     2020

@INCLUDE input-kubernetes.conf
@INCLUDE filter-kubernetes.conf
@INCLUDE output-kubernetes.conf

input-kubernetes.conf: |
[INPUT]
    Name              tail
    Tag               kube.*
    Path              /var/log/containers/*.log
    Parser            docker
    DB                /var/log/flb_kube.db
    Mem_Buf_Limit     5MB
    Skip_Long_Lines   On
    Refresh_Interval  10

filter-kubernetes.conf: |
[FILTER]
    Name                kubernetes
    Match               *
    Kube_URL            https://kubernetes.default.svc.cluster.local:443
    Merge_Log           On
    Annotations         Off
    Kube_Tag_Prefix     kube.var.log.containers.
    Merge_Log_Key       log_processed

[FILTER]
    Name                modify
    Match               *
    Set  cluster  ${CLUSTER_NAME}
output-kubernetes.conf: |
# [OUTPUT]
#     Name            stdout
#     Match           *
# [OUTPUT]
#     Name            es
#     Match           *
#     Host            ${FLUENT_ELASTICSEARCH_HOST}
#     Port            ${FLUENT_ELASTICSEARCH_PORT}
#     Logstash_Format On
#     Retry_Limit     False
[OUTPUT]
    Name            clickhouse
    Match           *


parsers.conf: |
[PARSER]
    Name   apache
    Format regex
    Regex  ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
    Time_Key time
    Time_Format %d/%b/%Y:%H:%M:%S %z

[PARSER]
    Name   apache2
    Format regex
    Regex  ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
    Time_Key time
    Time_Format %d/%b/%Y:%H:%M:%S %z

[PARSER]
    Name   apache_error
    Format regex
    Regex  ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$

[PARSER]
    Name   nginx
    Format regex
    Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
    Time_Key time
    Time_Format %d/%b/%Y:%H:%M:%S %z

[PARSER]
    Name   json
    Format json
    Time_Key time
    Time_Format %d/%b/%Y:%H:%M:%S %z

[PARSER]
    Name         docker
    Format       json
    Time_Key     time
    Time_Format  %Y-%m-%dT%H:%M:%S.%L
    Time_Keep    On

[PARSER]
    Name        syslog
    Format      regex
    Regex       ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
    Time_Key    time
    Time_Format %b %d %H:%M:%S



---

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: k8s-log-agent
namespace: kube-system
labels:
k8s-app: k8s-log-agent
kubernetes.io/cluster-service: "true"
spec:
selector:
matchLabels:
  k8s-app: k8s-log-agent
  kubernetes.io/cluster-service: "true"
template:
metadata:
  labels:
    k8s-app: k8s-log-agent
    kubernetes.io/cluster-service: "true"
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "2020"
    prometheus.io/path: /api/v1/metrics/prometheus
spec:
  containers:
  - name: fluent-bit
    image: iyacontrol/fluent-bit-ck:1.2.2
    imagePullPolicy: Always
    ports:
      - containerPort: 2020
    resources:
      limits:
        cpu: 200m
        memory: 200Mi
      requests:
        cpu: 200m
        memory: 200Mi
    env:
    - name: CLUSTER_NAME
      value: "xxx-cce-prod"
    - name: CLICKHOUSE_HOST
      value: "sg.logs.ck.xxx.service:9000"
    - name: CLICKHOUSE_USER
      value: "admin"
    - name: CLICKHOUSE_PASSWORD
      value: "admin"
    - name: CLICKHOUSE_DATABASE
      value: "scmp"
    - name: CLICKHOUSE_TABLE
      value: "logs"
    - name: NODENAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    volumeMounts:
    - name: varlog
      mountPath: /var/log
    - name: varlibdockercontainers
      mountPath: /var/lib/docker/containers
      readOnly: true
    - name: k8s-log-agent-config
      mountPath: /fluent-bit/etc/
  terminationGracePeriodSeconds: 10
  volumes:
  - name: varlog
    hostPath:
      path: /var/log
  - name: varlibdockercontainers
    hostPath:
      path: /var/lib/docker/containers
  - name: k8s-log-agent-config
    configMap:
      name: k8s-log-agent-config
  serviceAccountName: k8s-log-agent
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule


---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: k8s-log-agent-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: k8s-log-agent-read
subjects:
- kind: ServiceAccount
name: k8s-log-agent
namespace: kube-system

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k8s-log-agent-read
rules:
- apiGroups: [""]
resources:
- namespaces
- pods
verbs: ["get", "list", "watch"]

---


apiVersion: v1
kind: ServiceAccount
metadata:
name: k8s-log-agent
namespace: kube-system


PS:
  • 由于启用了kubernetes filter 插件,所以需要进行RBAC授权。该Filter 会从kube-apiserver读取对应pod的元数据加到logs当中

  • 由于是需要收集多个Kubernetes集群,所以利用modify 插件,对日志增加cluster tag, 便于后期按照集群维度查询和分析。

  • 通过环境变量的方式,将fluent-bit-clickhouse所需的配置参数。


日志展示

除了redash和superset等大数据可视化平台对Clickhouse 有很好的支持。如果单指日志领域,可以采取以下方案:

Grafana

利用table 这种chart即可满足。如果需要更好的效果,Grafana6.0+,已经提供了Logs chart。同时通过插件的方式,也支持Clickhouse作为数据源。

使用grafana-cli工具从命令行安装ClickHouse:
grafana-cli plugins install vertamedia-clickhouse-datasource


然后就可以添加新的数据源,此处我使用了Clickhouse的只读用户。

利用如下查询语句:
SELECT *
FROM scmp.logs_all
LIMIT 1


最终日志展示效果:

log.jpg


Loghouse-dashboard

Loghouse-dashboard 是 loghouse 中的一个专门针对Clickhouse作为日志存储的日志展示项目,具备如下特点:
  • 类似于Papertrail的用户体验。
  • 可自定义的时间范围:从日期至今/从现在到给定时间段(最后一小时,最后一天等)/查找特定时间并显示其周围的日志。
  • 无限滚动旧日志条目。
  • 保存查询以供将来使用。
  • 基本权限(通过指定Kubernetes命名空间来限制为用户显示的条目)。
  • 将当前查询的结果导出为CSV(将支持更多格式)。


实际效果如图:

loghouse.jpg


其实Clickhouse提供了简单的http api接口,对于集成到统一运维平台也比较容易。

结论


  • Clickhouse(可能大多数数据库)并不适合许多小数据高频插入。如果你的日志允许一定的延时,那么选择批量插入。实际上fluent-bit-clickhouse 默认BatchSize是1000,当然大家可以根据自己的实际情况,进行调整。

  • 借助于Clickhouse强大的分析功能,很容易多个维度分析日志,指导我们整个Kubernetes运维工作。比如我们查询标准输出日志条数最多的10个应用。

  • 在特定的场景下,Clickhouse作为Kubernetes日志管理解决方案中的存储的解决方案是一种低成本的方案。我们在实际生产环境中,每天10亿的日志量,使用Elasticsearch 方案需要900G的存储占用,而使用Clickhouse ,只需要不到30G。比如需要查询某个集群某个命名空间下的某个pod的最近10分钟的日志,Clickhouse几乎ms级别返回查询结果。

  • 在其他的日志领域。可以采用clicktail 这个神器。它是Altinity公司基于honeytail开发的一个Go语言的日志解析、传输工具,可以直接解析MySQL慢查询日志、Nginx日志、PG以及MongoDB日志,直接写入Clickhouse,用于后期的分析。以MySQL slowlog为例,会自动做SQL Digest,方便聚合,加上Clickhouse丰富的聚合函数,计算百分比响应时间,非常简单。

0 个评论

要回复文章请先登录注册