27

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

 4 years ago
source link: https://www.tuicool.com/articles/mY3aIzQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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,意味着更小的磁盘和内存使用。

方案设计

qIrMfeM.png!web

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

  • 每个Kubernetes集群通过DaemonSet方式部署Flunet bit。负责收集日志并写到Clickhouse集群中。关于Flunet bit 需要定制开发Clickhouse的output插件,这将在下面详细讲述。
  • 由于Clickhouse 出色的写入性能,目前我们没有使用kafka。
  • Clickhouse 集群部署,需要zk集群做一致性表数据复制。

而clickhouse 的集群示意图如下:

JFfYvuq.png!web

  • 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 < 1 or c - d > 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。官方比较数据如下:

JvUBVrF.jpg!web

通过数据可以看出,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

最终日志展示效果:

yu22ayj.jpg!web

Loghouse-dashboard

Loghouse-dashboard 是 loghouse 中的一个专门针对Clickhouse作为日志存储的日志展示项目,具备如下特点:

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

实际效果如图:

EbIjEvf.jpg!web

加粗文字

其实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丰富的聚合函数,计算百分比响应时间,非常简单。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK