目录
简介
作为高性能 OLAP 数据库,ClickHouse 被用于许多用例,包括 时间序列 数据的实时分析。其多样化的用例促使了大量 分析函数 的出现,这些函数有助于查询大多数数据类型。这些查询功能和高压缩率使得越来越多的用户利用 ClickHouse 存储可观测性数据。这些数据通常有三种形式:日志、指标和追踪。在本系列博文中,我们将探讨如何收集、最佳存储、查询和可视化这些“支柱”。
在本文中,我们从日志以及收集和查询的可能性开始。我们已尝试确保示例可以重现。我们还注意到,代理对 ClickHouse 特定数据类型的支持在不断发展,本文代表了截至 2023 年 1 月的生态系统的当前状态。因此,我们始终鼓励用户查看文档和链接的问题。
虽然我们的示例假设用户必须从 Kubernetes 集群收集日志的现代架构,但这些建议和意见并非 Kubernetes 依赖,同样适用于自管理服务器或其他容器编排系统。我们使用我们的开发云环境进行测试,每天从大约 20 个节点生成约 100GB 的日志。请注意,我们没有努力调整代理或测量其资源开销 - 我们建议用户在生产部署之前研究或执行此操作。本文还侧重于数据收集,提出模式和数据模型,但将优化留到以后的文章中。
对于我们的示例,我们将数据存储在 ClickHouse Cloud 服务中,您可以在其中几分钟内在免费试用版中启动集群,让我们处理基础设施并让您开始查询!
注意: 本文中使用的所有可重现的配置示例都可以在此存储库中查阅
架构
大多数代理使用通用的架构模式来大规模收集可观测性数据,从而推广代理和聚合器的概念。对于小型部署,后者可以忽略不计,代理部署在靠近其数据源的位置,并负责通过 HTTP 或原生协议直接将数据发送到 ClickHouse。在 Kubernetes 环境中,这意味着将代理部署为 Daemonset。这会将 代理 Pod 部署到每个 K8s 节点,负责收集其他容器的日志(通常从磁盘读取)。
对于不需要高持久性或可用性以及少量代理且配置更改摩擦较小的用户,此架构已足够。但是,用户应注意,这可能会导致许多小的插入,尤其是在代理配置为频繁刷新数据时,例如,因为需要及时提供数据以进行分析和问题识别。在这种情况下,用户应考虑配置代理以使用 异步插入,以避免 零件过多 导致的常见问题。
较大的部署引入了聚合器或网关概念。这旨在配置靠近其数据源的轻量级代理,仅负责将数据转发到聚合器。这降低了中断现有服务的可能性。聚合器负责处理诸如丰富、过滤和确保应用模式等步骤,以及批量处理和可靠地交付到 ClickHouse。此聚合器通常部署为 Deployment 或 Statefulset,并且可以根据需要复制以实现高可用性。
除了最大限度地减少潜在关键服务数据源的负载外,此架构还允许将数据批量处理并作为更大的块插入到 ClickHouse 中。此属性非常重要,因为它与 ClickHouse 插入最佳实践 一致。
上述架构简化了企业架构,实际上,企业架构还需要考虑数据应缓冲在何处、负载均衡、高可用性、复杂路由以及分离您的记录(存档)和分析系统的需求。Vector 文档 此处 详细介绍了这些概念。虽然是 Vector 特有的,但此处的原则适用于讨论的其他代理。这些架构的一个重要特性是,代理和聚合器也可以是异构的,技术的混合很常见,尤其是在收集不同类型的数据时,因为某些代理擅长不同的可观测性支柱。
代理
在 ClickHouse,我们的用户倾向于四种主要的代理技术:Open Telemetry Collector、Vector、FluentBit 和 Fluentd。后两者具有相同的起源和许多相同的概念。为了简洁起见,我们探讨了 FluentBit,它更轻量级,足以在 Kubernetes 中收集日志,但 Fluentd 也是一种有效的方法。这些代理可以承担聚合器或收集器的角色,并且可以 一起使用(有一些限制)。尽管它们使用不同的术语,但它们都使用具有可插拔输入、过滤器/处理器和输出的通用架构。ClickHouse 要么作为官方输出得到支持,要么通过通用的 HTTP 支持实现集成。
但是,在下面的初始示例中,我们在聚合器和收集器角色中都部署了每个代理。我们利用每个代理的官方 Helm chart 来获得简单的入门体验,注意重要的配置更改,并共享 values.yaml
文件。
我们的示例对聚合器使用单个副本,尽管可以轻松地使用多个副本进行部署并进行负载均衡以提高性能和容错能力。所有代理都支持使用 Kubernetes 元数据丰富日志,这对于未来的分析至关重要,例如 Pod 名称、容器 ID 和日志来源节点。注释 和 标签 也可以包含在日志条目中(FluentBit 和 Vector 默认启用)。这些通常是稀疏的,但可能数量众多(数百个);生产架构应评估其价值并对其进行过滤。我们建议对这些使用 Map 类型,以避免列爆炸,这具有 查询影响。
所有代理都需要在聚合器角色中进行调整(通过 resources YAML 键),以避免 OOM 问题并跟上我们的吞吐量(每天约 100GB)。您的里程可能会因聚合器数量和日志吞吐量而异,但在大型环境中几乎总是需要调整资源。
Open Telemetry (OTEL) 收集器(alpha 版)
OpenTelemetry 是一组用于检测、生成、收集和导出可观测性数据的工具、API 和 SDK。除了提供大多数流行语言的代理外,用 Golang 编写的 Collector 组件 还提供了如何接收、处理和导出可观测性数据的供应商无关实现。通过支持多种输入格式(如 Prometheus 和 OTLP)以及包括 ClickHouse 在内的各种导出目标,OTEL Collector 可以提供集中的处理网关。Collector 使用术语 接收器、处理器 和 导出器 来表示其三个阶段,并使用 网关 来表示聚合器实例。
虽然 Collector 更常用于网关/聚合器,处理诸如批量处理和重试之类的任务,但 Collector 也可以部署为 代理本身。OTLP 代表 Open Telemetry 数据标准,用于网关和代理实例之间的通信,可以通过 gRPC 或 HTTP 进行。正如我们将在下面看到的,Vector 和 FluentBit 也支持此协议。
ClickHouse 支持
ClickHouse 在 OTEL 导出器中通过 社区贡献 得到支持,支持日志和追踪(指标的 PR 正在审核中)。与 ClickHouse 的通信通过优化的原生格式和协议以及官方 Go 客户端进行。
在使用 Open Telemetry Collector 之前,用户应考虑以下几点
- 代理使用的 ClickHouse 数据模型和模式是硬编码的。在撰写本文时,无法更改使用的类型或编解码器。通过在部署连接器之前创建表来缓解此问题,从而强制执行您的模式。
- 导出器未与核心 OTEL 发行版一起分发,而是作为通过
contrib
镜像的扩展。实际上,这意味着在 Helm chart 中使用正确的 docker 镜像。 - 导出器处于 alpha 版,尽管我们收集了超过 1TB 的日志没有遇到任何问题,但用户应遵守 Open Telemetry 提供的建议。OTEL 的日志用例仍然相对较新,不如 Fluent Bit 或 Vector 产品成熟。
Kubernetes 部署
如果仅收集日志,官方 Helm chart 代表最简单的部署方式。在以后的文章中,当我们检测应用程序时,operator 提供自动检测功能和其他部署模式,例如,作为 Sidecar。但是,对于日志,基本 chart 就足够了。有关安装和配置 chart 的完整详细信息,请参见 此处,包括部署网关和代理以及示例配置的步骤。
请注意,导出器还支持 ClickHouse 的原生 TTL 功能 用于数据管理,并依赖于按日期进行分区(由模式强制执行)。我们在 我们的示例中将 TTL 设置为 0,禁用数据过期,但这代表一个有用的功能,也是日志中的常见要求,可以轻松地在其他代理的模式中使用。
数据和模式
我们之前的示例已将聚合器配置为将数据发送到 otel.otel_logs
表。我们可以通过简单的 SELECT 确认数据已成功收集。
SELECT * FROM otel.otel_logs LIMIT 1 FORMAT Vertical Row 1: ────── Timestamp: 2023-01-04 17:27:29.880230118 TraceId: SpanId: TraceFlags: 0 SeverityText: SeverityNumber: 0 ServiceName: Body: {"level":"debug","ts":1672853249.8801103,"logger":"activity_tracker","caller":"logging/logger.go:161","msg":"Time tick; Starting fetch activity"} ResourceAttributes: {'k8s.container.restart_count':'0','k8s.pod.uid':'82bc65e2-145b-4895-87fc-4a7db48e0fd9','k8s.container.name':'scraper-container','k8s.namespace.name':'ns-fuchsia-qe-86','k8s.pod.name':'c-fuchsia-qe-86-server-0'} LogAttributes: {'log.file.path':'/var/log/pods/ns-fuchsia-qe-86_c-fuchsia-qe-86-server-0_82bc65e2-145b-4895-87fc-4a7db48e0fd9/scraper-container/0.log','time':'2023-01-04T17:27:29.880230118Z','log.iostream':'stderr'} 1 row in set. Elapsed: 0.302 sec. Processed 16.38 thousand rows, 10.59 MB (54.18 thousand rows/s., 35.02 MB/s.)
请注意,收集器对模式有自己的看法,包括强制执行特定的编解码器。虽然这些代表了通用情况的明智选择,但它阻止用户根据自己的需求调整配置,例如,修改表的排序键以适应用户特定的访问模式。
该模式使用 PARTITION BY 来辅助 TTL。具体而言,这允许有效地删除一天的数据。它可能会对查询产生正面和负面影响。数据跳过 Bloom 索引的使用是一个高级主题,我们将其推迟到以后关于模式优化的文章中。此处对 Kubernetes 和日志属性使用 Map 类型会影响我们的查询语法。
SHOW CREATE TABLE otel.otel_logs CREATE TABLE otel.otel_logs ( `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)), `TraceId` String CODEC(ZSTD(1)), `SpanId` String CODEC(ZSTD(1)), `TraceFlags` UInt32 CODEC(ZSTD(1)), `SeverityText` LowCardinality(String) CODEC(ZSTD(1)), `SeverityNumber` Int32 CODEC(ZSTD(1)), `ServiceName` LowCardinality(String) CODEC(ZSTD(1)), `Body` String CODEC(ZSTD(1)), `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1, INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1 ) ENGINE = MergeTree PARTITION BY toDate(Timestamp) ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId) SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
Vector(beta 版)
Vector 是 DataDog 维护的开源(在 Mozilla Public License 版本 2.0 下获得许可)可观测性数据管道工具,支持收集、转换和路由日志、指标和追踪数据。它旨在与供应商无关,并支持多种 输入 和 输出,包括 OTLP 协议,使其可以充当 Open Telemetry 代理的聚合器。Vector 用 Rust 编写,在其 3 阶段管道中使用术语 sources、transforms 和 sinks。它代表了功能丰富的日志收集解决方案,并且在 ClickHouse 社区中越来越受欢迎。
ClickHouse 支持
ClickHouse 在 Vector 中通过 专用 sink(目前为 Beta 版)得到支持,通信通过 使用 JSON 格式的 HTTP 协议 和 插入时批量处理的请求 进行。虽然不如其他协议性能高,但这会将数据处理卸载到 ClickHouse,并简化调试网络流量。虽然强制执行数据模型,但用户必须创建目标表并选择其类型和编码。skip_unknown_fields
选项允许用户创建包含可用列子集表。这将导致忽略目标表中不存在的任何列。下面我们在 vector
数据库中创建一个目标表,涵盖所有 post 列,包括从 Kubernetes 丰富添加的列。目前,我们使用针对按容器名称过滤优化的表排序键。以后的文章将讨论优化此模式。
CREATE database vector CREATE TABLE vector.vector_logs ( `file` String, `timestamp` DateTime64(3), `kubernetes_container_id` LowCardinality(String), `kubernetes_container_image` LowCardinality(String), `kubernetes_container_name` LowCardinality(String), `kubernetes_namespace_labels` Map(LowCardinality(String), String), `kubernetes_pod_annotations` Map(LowCardinality(String), String), `kubernetes_pod_ip` IPv4, `kubernetes_pod_ips` Array(IPv4), `kubernetes_pod_labels` Map(LowCardinality(String), String), `kubernetes_pod_name` LowCardinality(String), `kubernetes_pod_namespace` LowCardinality(String), `kubernetes_pod_node_name` LowCardinality(String), `kubernetes_pod_owner` LowCardinality(String), `kubernetes_pod_uid` LowCardinality(String), `message` String, `source_type` LowCardinality(String), `stream` Enum('stdout', 'stderr') ) ENGINE = MergeTree ORDER BY (`kubernetes_container_name`, timestamp)
默认情况下,Vector 的 Kubernetes 日志输入 使用 .
在列名中创建列,例如,kubernetes.pod_labels
。我们 不建议在 Map 列名中使用点,并且可能会弃用其使用,因此请使用 _
。转换在聚合器中实现此目的(见下文)。请注意,我们还获得了命名空间和节点标签。
Kubernetes 部署
同样,我们通过使用官方 chart 将 Helm 用作首选安装方法。聚合器和代理的完整安装详细信息以及示例配置请参见此处。除了将输出源更改为 ClickHouse 之外,主要更改是需要使用 remap 转换,该转换使用 Vector Remap Language (VRL) 来确保列使用 _
作为分隔符,而不是 .
。
数据
我们可以通过一个简单的查询来确认日志数据正在插入
SELECT * FROM vector.vector_logs LIMIT 1 FORMAT Vertical Row 1: ────── file: /var/log/pods/argocd_argocd-application-controller-0_33574e53-966a-4d54-9229-205fc2a4ea03/application-controller/0.log timestamp: 2023-01-05 12:12:50.766 kubernetes_container_id: kubernetes_container_image: quay.io/argoproj/argocd:v2.3.3 kubernetes_namespace_labels: {'kubernetes.io/metadata.name':'argocd','name':'argocd'} kubernetes_node_labels: {'beta.kubernetes.io/arch':'amd64','beta.kubernetes.io/instance-type':'r5.xlarge'...} kubernetes_container_name: application-controller kubernetes_pod_annotations: {'ad.agent.com/application-controller.check_names':'["openmetrics"]...'} kubernetes_pod_ip: 10.1.3.30 kubernetes_pod_ips: ['10.1.3.30'] kubernetes_pod_labels: {'app.kubernetes.io/component':'application-controller'...} kubernetes_pod_name: argocd-application-controller-0 kubernetes_pod_namespace: argocd kubernetes_pod_node_name: ip-10-1-1-210.us-west-2.compute.internal kubernetes_pod_owner: StatefulSet/argocd-application-controller kubernetes_pod_uid: 33574e53-966a-4d54-9229-205fc2a4ea03 message: {"level":"info","msg":"Ignore '/spec/preserveUnknownFields' for CustomResourceDefinitions","time":"2023-01-05T12:12:50Z"} source_type: kubernetes_logs stream: stderr
Fluent Bit
Fluent Bit 是日志和指标处理器和转发器。FluentBit 历史以来一直专注于日志并用 C 编写以最大限度地减少任何开销,旨在轻量级和快速。该代码最初由 TreasureData 开发,但长期以来已作为 Cloud Native Computing Foundation 项目在 Apache 2.0 许可下开源。它被 多家云提供商 采用为一流公民,它提供了与上述工具相当的 输入、处理和输出 功能。
FluentBit 为其管道使用 输入、解析器/过滤器 和 输出(以及超出本文范围的 缓冲区 和 路由器 概念)。聚合器实例称为 聚合器。
ClickHouse 支持
luentBit 没有 ClickHouse 专用输出,而是依赖于通用的 HTTP 支持。这效果很好,并且依赖于以 JSONEachRow 格式插入数据。但是,用户需要对此方法持谨慎态度,因为输出不执行任何批量处理。因此,需要对 FluentBit 进行适当的配置,以避免大量的小插入和 “零件过多”问题。用户应注意,Fluent bit 将所有内容存储在块中。这些 块 的数据结构为标签,有效负载大小最大为 2MB。使用 Kubernetes 时,每个容器都输出到由动态标签标识的单独文件。该标签还用于读取各个块。这些块由代理按标签独立刷新到聚合器,根据 刷新间隔。聚合器保留任何下游路由需求的标签信息。它将对其自己的刷新间隔设置用于每个标签,以确定写入 ClickHouse 的操作。因此,用户有两种选择
- 在代理和聚合器上配置较大的刷新间隔,即至少 10 秒。这可能有效,但也可能导致惊群效应,从而导致对 ClickHouse 的插入激增。但是,如果间隔足够大,内部合并应能跟上。
- 配置输出以使用 ClickHouse 的异步插入 - 如果您不部署聚合器实例,则尤其推荐这样做。这将导致 ClickHouse 缓冲插入,并且是处理此写入模式的 推荐方法。可以调整异步插入的行为,这对 Fluent Bit 的交付保证有影响。具体而言,设置
wait_for_async_insert
控制写入是否在写入缓冲区 (0) 时或在写入为实际数据部分(并且可用于读取查询)时确认。值为 1 可提供更大的交付保证,但可能会降低吞吐量。请注意,Fluent Bit 偏移量管理和前进基于输出的确认。对于wait_for_async_insert
,值为 0 可能意味着在数据完全处理之前确认数据,即,随后可能会发生故障,导致数据丢失。在某些情况下,这可能是可以接受的。另请注意设置async_insert_max_data_size
和async_insert_busy_timeout_ms
,它们控制缓冲区的确切刷新行为。
在没有对 ClickHouse 的明确理解的情况下,用户必须在部署之前预先创建表。与 Vector 类似,这会将模式决策留给用户。FluentBit 创建一个嵌套的 JSON 模式,深度大于 1。这可能包含数百个字段,因为为每个唯一的标签或注释创建一个唯一的列。我们之前的帖子建议对 kubernetes
列使用 JSON 类型。这会将列创建推迟到 ClickHouse,并允许根据数据创建动态子列。这提供了很好的入门体验,但并非最佳选择,因为用户无法使用编解码器或在表的排序键中使用特定的子列(除非使用 JSONExtract),从而导致更差的压缩和更慢的查询。在没有对标签和注释使用进行控制的环境中,也可能导致列爆炸。此外,此功能目前是实验性的。此模式更优化的方法是将标签和注释移动到 Map 类型 - 这方便地将 kubernetes
列减少到深度为 1。这需要我们在处理器管道中稍微修改数据结构,并产生以下模式。
CREATE TABLE fluent.fluent_logs ( `timestamp` DateTime64(9), `log` String, `kubernetes` Map(LowCardinality(String), String), `host` LowCardinality(String), `pod_name` LowCardinality(String), `stream` LowCardinality(String), `labels` Map(LowCardinality(String), String), `annotations` Map(LowCardinality(String), String) ) ENGINE = MergeTree ORDER BY (host, pod_name, timestamp)
Kubernetes 部署
之前的博文 详细讨论了部署 Fluent Bit 以将 Kubernetes 日志收集到 ClickHouse。这篇文章侧重于部署仅代理架构,没有聚合器。一般配置仍然适用,但有一些差异可以改进模式并引入聚合器。
聚合器和代理的完整安装详细信息可以在 此处 找到,以及示例配置。关于配置的一些重要细节
- 我们使用不同的 Lua 脚本 将特定字段从
kubernetes
键移到根目录,从而允许在排序键中使用这些字段。我们还将注释和标签移到根目录。这允许将它们声明为 Map 类型,并从稍后的 压缩 统计信息中排除,因为它们非常稀疏。此外,这意味着我们的kubernetes
列只有一层嵌套,也可以声明为 Map。 - 聚合器输出指定在 URI 中使用 async_inserts。我们将此与 5 秒的刷新间隔 结合使用。在我们的示例中,我们没有指定
wait_for_async_insert=1
,但可以根据需要将其作为参数附加。
数据
我们可以通过一个简单的查询来确认日志数据正在插入
SELECT * FROM fluent.fluent_logs LIMIT 1 FORMAT Vertical Row 1: ────── timestamp: 2023-01-05 13:11:36.452730318 log: 2023.01.05 13:11:36.452588 [ 41 ] {} RaftInstance: Receive a append_entries_request message from 1 with LastLogIndex=298734, LastLogTerm=17, EntriesLength=0, CommitIndex=298734 and Term=17 kubernetes: {'namespace_name':'ns-chartreuse-at-71','container_hash':'609927696493.dkr.ecr.us-west-2.amazonaws.com/clickhouse-keeper@sha256:e9efecbef9498dea6ddc029a8913dc391c49c7d0b776cb9b1c767cdb1bf15489',...} host: ip-10-1-3-9.us-west-2.compute.internal pod_name: c-chartreuse-at-71-keeper-2 stream: stderr labels: {'controller-revision-hash':..}
互操作性和堆栈选择
我们之前的示例将假设代理和聚合器都使用相同的技术。通常,这并非最佳选择,或者仅仅是因为组织标准或代理中缺少对特定数据类型的支持而不可能实现。例如,如果您正在使用 Open Telemetry 语言代理进行追踪,则可能会部署 OTEL Collector 作为聚合器。在这种情况下,您可以选择 Fluent Bit 作为首选日志收集代理(因为它在这种数据类型方面更成熟),但继续使用 OTEL 收集器作为聚合器以获得一致的数据模型。
幸运的是,作为更广泛的 Open Telemetry 项目的一部分而推广的 OTLP 协议 以及对转发协议(Fluent Bit 的首选通信标准)的支持在某些情况下允许互操作性。
Vector 支持这些协议作为源,并且可以充当 Fluent Bit 和 Open Telemetry Collector 的日志聚合器。但是,它不支持将这些协议作为 sink,这使得在已经部署了 OTEL 收集器或 Fluent Bit 的环境中部署为代理具有挑战性。请注意,Vector 对您应该用 Vector 替换堆栈的哪些组件 有强烈的看法。
Fluent Bit 最近添加了 OTLP 支持作为输入 和 输出,可能允许与 OTEL 收集器高度互操作(OTEL 收集器也支持 转发协议作为接收器)。Fluent Bit 作为日志收集代理,通过转发或 OTEL 协议发送到 OTEL 收集器,已经变得越来越流行,尤其是在 Open Telemetry 已经成为标准的环境中。
注意:在撰写本文时,我们在 Fluent Bit 的 OTLP 输入和输出方面 遇到了问题,尽管我们预计很快会解决此问题。
我们在下面总结了当前日志收集的兼容性状态,并链接到示例 Helm 配置,其中包含有关已知问题的详细信息,这些问题可以与上述类似地使用。请注意,这仅适用于日志收集。
当代理配置为聚合器以接收来自不同技术的事件时,生成的数据模式将与等效的同构架构不同。上面的链接显示了生成模式的示例。如果需要一致的模式,用户可能需要使用每个代理的转换功能。
压缩
在 ClickHouse 中存储日志数据的主要好处之一是其出色的压缩:这是其面向列的设计和可配置编解码器的产物。以下查询显示,根据聚合器,我们先前收集的数据的压缩率范围为 14 倍到 30 倍。这些代表非优化的模式(尽管默认 OTEL 模式是明智的),因此可以通过调整实现进一步的压缩。精明的读者会注意到,我们排除了 Kubernetes 标签和注释,这些标签和注释默认情况下是为 Fluent Bit 和 Vector 部署添加的,而不是由 OTEL 收集器添加的(OTEL 收集器支持此功能,但需要 其他配置)。此数据高度稀疏且压缩效果非常好,因为大多数注释都存在于 Pod 的一小部分子集中。这会扭曲压缩比(增加压缩比),因为大多数值都为空,因此我们选择排除它们 - 好消息是压缩后它们占用很少的空间。
SELECT database, table, formatReadableSize(sum(data_compressed_bytes)) AS compressed_size, formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size, round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio FROM system.columns WHERE (database IN ('fluent', 'vector', 'otel')) AND (name NOT LIKE '%labels%') AND (name NOT LIKE '%annotations%') GROUP BY database, table ORDER BY database ASC, table ASC ┌─database─┬─table───────┬─compressed_size─┬─uncompressed_size─┬─ratio─┐ │ fluent │ fluent_logs │ 2.43 GiB │ 80.23 GiB │ 33.04 │ │ otel │ otel_logs │ 5.57 GiB │ 78.51 GiB │ 14.1 │ │ vector │ vector_logs │ 3.69 GiB │ 77.92 GiB │ 21.13 │ └──────────┴─────────────┴─────────────────┴───────────────────┴───────┘
我们将在后续的文章中探讨这些压缩率变化的原因,但即使是初步尝试,上述压缩率也显示出相对于其他解决方案的巨大潜力。这些模式可以被标准化,并且可以实现与代理无关的可比较的压缩率,因此这些结果不应用于比较代理。
注解高压缩示例
SELECT name, table, formatReadableSize(sum(data_compressed_bytes)) AS compressed_size, formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size, round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio FROM system.columns WHERE (database IN ('fluent', 'vector', 'otel')) AND ((name LIKE '%labels%') OR (name LIKE '%annotations%')) GROUP BY database, table, name ORDER BY database ASC, table ASC ┌─name────────────────────────┬─table───────┬─compressed_size─┬─uncompressed_size─┬──ratio─┐ │ labels │ fluent_logs │ 2.95 MiB │ 581.31 MiB │ 196.93 │ │ annotations │ fluent_logs │ 14.97 MiB │ 7.17 GiB │ 490.57 │ │ kubernetes_pod_annotations │ vector_logs │ 36.67 MiB │ 23.97 GiB │ 669.29 │ │ kubernetes_node_labels │ vector_logs │ 18.67 MiB │ 4.18 GiB │ 229.55 │ │ kubernetes_pod_labels │ vector_logs │ 6.89 MiB │ 1.88 GiB │ 279.92 │ │ kubernetes_namespace_labels │ vector_logs │ 3.91 MiB │ 468.14 MiB │ 119.62 │ └─────────────────────────────┴─────────────┴─────────────────┴───────────────────┴────────┘
我们将在未来的文章中探讨这一点,但建议您先阅读 Optimizing ClickHouse with Schemas and Codecs 作为过渡阅读材料。
查询和可视化日志
常用查询
日志数据实际上是时间序列数据,ClickHouse 提供了许多函数来辅助查询。我们在 最近的博客文章 中广泛介绍了这些函数,其中大多数查询概念都适用。大多数仪表板和调查都需要按时间聚合以绘制时间序列图表,然后根据服务器/Pod 名称或错误代码进行后续过滤。我们下面的示例使用 Vector 收集的日志,但这些示例可以适用于收集类似字段的其他代理数据。
按 Pod 名称显示一段时间内的日志
在这里,我们按自定义间隔分组,并使用填充来填充缺失的组。根据需要进行调整。有关更多详细信息,请参阅我们最近的博客文章。
SELECT toStartOfInterval(timestamp, toIntervalDay(1)) AS time, kubernetes_pod_name AS pod_name, count() AS c FROM vector.vector_logs GROUP BY time, pod_name ORDER BY pod_name ASC, time ASC WITH FILL STEP toIntervalDay(1) LIMIT 5 ┌────────────────time─┬─pod_name──────────────────────────────────────────┬─────c─┐ │ 2023-01-05 00:00:00 │ argocd-application-controller-0 │ 8736 │ │ 2023-01-05 00:00:00 │ argocd-applicationset-controller-745c6c86fd-vfhzp │ 9 │ │ 2023-01-05 00:00:00 │ argocd-notifications-controller-54495dd444-b824r │ 15137 │ │ 2023-01-05 00:00:00 │ argocd-repo-server-d4787b66b-ksjps │ 2056 │ │ 2023-01-05 00:00:00 │ argocd-server-58dd79dbbf-wbthh │ 9 │ └─────────────────────┴───────────────────────────────────────────────────┴───────┘ 5 rows in set. Elapsed: 0.270 sec. Processed 15.62 million rows, 141.97 MB (57.76 million rows/s., 524.86 MB/s.)
查询 Pod 在特定时间窗口内的日志
SELECT timestamp, kubernetes_pod_namespace AS namespace, kubernetes_pod_name AS pod, kubernetes_container_name AS container, message FROM vector.vector_logs WHERE (kubernetes_pod_name = 'argocd-application-controller-0') AND ((timestamp >= '2023-01-05 13:40:00.000') AND (timestamp <= '2023-01-05 13:45:00.000')) ORDER BY timestamp DESC LIMIT 2 FORMAT Vertical Row 1: ────── timestamp: 2023-01-05 13:44:41.516 namespace: argocd pod: argocd-application-controller-0 container: application-controller message: W0105 13:44:41.516636 1 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+ Row 2: ────── timestamp: 2023-01-05 13:44:09.515 namespace: argocd pod: argocd-application-controller-0 container: application-controller message: W0105 13:44:09.515884 1 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+ 2 rows in set. Elapsed: 0.219 sec. Processed 1.94 million rows, 21.59 MB (8.83 million rows/s., 98.38 MB/s.)
查询 Map 类型
上述许多代理生成类似的模式,并对 Kubernetes 注解和标签使用 Map 数据类型。用户可以使用 map notation 来访问嵌套键,此外,如果过滤或选择这些列,还可以使用专门的 ClickHouse map functions。
SELECT kubernetes_pod_labels['statefulset.kubernetes.io/pod-name'] AS statefulset_pod_name, count() AS c FROM vector.vector_logs WHERE statefulset_pod_name != '' GROUP BY statefulset_pod_name ORDER BY c DESC LIMIT 10 ┌─statefulset_pod_name────────┬──────c─┐ │ c-snow-db-40-keeper-2 │ 587961 │ │ c-coral-cy-94-keeper-0 │ 587873 │ │ c-ivory-es-35-keeper-2 │ 587331 │ │ c-feldspar-hh-33-keeper-2 │ 587169 │ │ c-steel-np-64-keeper-2 │ 586828 │ │ c-fuchsia-qe-86-keeper-2 │ 583358 │ │ c-canary-os-78-keeper-2 │ 546849 │ │ c-salmon-sq-90-keeper-1 │ 544693 │ │ c-claret-tk-79-keeper-2 │ 539923 │ │ c-chartreuse-at-71-keeper-1 │ 538370 │ └─────────────────────────────┴────────┘ 10 rows in set. Elapsed: 0.343 sec. Processed 16.98 million rows, 3.15 GB (49.59 million rows/s., 9.18 GB/s.) // use groupArrayDistinctArray to list all pod label keys SELECT groupArrayDistinctArray(mapKeys(kubernetes_pod_annotations)) FROM vector.vector_logs LIMIT 10 ['clickhouse.com/chi','clickhouse.com/namespace','release','app.kubernetes.io/part-of','control-plane-id','controller-revision-hash','app.kubernetes.io/managed-by','clickhouse.com/replica','kind','chart','heritage','cpu-request','memory-request','app.kubernetes.io/version','app','clickhouse.com/ready','clickhouse.com/shard','clickhouse.com/settings-version','control-plane','name','app.kubernetes.io/component','updateTime','clickhouse.com/app','role','pod-template-hash','app.kubernetes.io/instance','eks.amazonaws.com/component','clickhouse.com/zookeeper-version','app.kubernetes.io/name','helm.sh/chart','k8s-app','statefulset.kubernetes.io/pod-name','clickhouse.com/cluster','component','pod-template-generation']
查找包含特定字符串的日志的 Pod
可以通过 ClickHouse string and regex 函数对日志行进行模式匹配,如下所示
SELECT kubernetes_pod_name, count() AS c FROM vector.vector_logs WHERE message ILIKE '% error %' GROUP BY kubernetes_pod_name ORDER BY c DESC LIMIT 5 ┌─kubernetes_pod_name──────────────────────────────────────────┬───c─┐ │ falcosidekick-ui-redis-0 │ 808 │ │ clickhouse-operator-clickhouse-operator-helm-dc8f5789b-lb88m │ 48 │ │ argocd-repo-server-d4787b66b-ksjps │ 37 │ │ kube-metric-forwarder-7df6d8b686-29bd5 │ 22 │ │ c-violet-sg-87-keeper-1 │ 22 │ └──────────────────────────────────────────────────────────────┴─────┘ 5 rows in set. Elapsed: 0.578 sec. Processed 18.02 million rows, 2.79 GB (31.17 million rows/s., 4.82 GB/s.)
使用正则表达式查找存在问题的 Pod
SELECT kubernetes_pod_name, arrayCompact(extractAll(message, 'Cannot resolve host \\((.*)\\)')) AS cannot_resolve_host FROM vector.vector_logs WHERE match(message, 'Cannot resolve host') LIMIT 5 FORMAT PrettyCompactMonoBlock ┌─kubernetes_pod_name─────┬─cannot_resolve_host──────────────────────────────────────────────────────────────────────────┐ │ c-violet-sg-87-keeper-0 │ ['c-violet-sg-87-keeper-1.c-violet-sg-87-keeper-headless.ns-violet-sg-87.svc.cluster.local'] │ │ c-violet-sg-87-keeper-0 │ ['c-violet-sg-87-keeper-2.c-violet-sg-87-keeper-headless.ns-violet-sg-87.svc.cluster.local'] │ │ c-violet-sg-87-keeper-0 │ ['c-violet-sg-87-keeper-1.c-violet-sg-87-keeper-headless.ns-violet-sg-87.svc.cluster.local'] │ │ c-violet-sg-87-keeper-0 │ ['c-violet-sg-87-keeper-2.c-violet-sg-87-keeper-headless.ns-violet-sg-87.svc.cluster.local'] │ │ c-violet-sg-87-keeper-0 │ ['c-violet-sg-87-keeper-1.c-violet-sg-87-keeper-headless.ns-violet-sg-87.svc.cluster.local'] │ └─────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────┘ 5 rows in set. Elapsed: 0.690 sec. Processed 18.04 million rows, 2.76 GB (26.13 million rows/s., 3.99 GB/s.)
优化性能
由上述代理生成的数据的查询性能将主要取决于表创建期间定义的排序键。这些排序键应与您的典型工作流程和访问模式相匹配。确保您在工作流程中通常用于过滤的列存在于 ORDER BY 表声明中。这些列的排序还应考虑它们各自的基数,以确保可以在 ClickHouse 中使用最佳的过滤算法。在大多数情况下,按照基数递增的顺序对列进行排序。对于日志,这通常意味着首先放置服务器或 Pod 名称,然后是时间戳:但这再次取决于您计划如何过滤。通常不建议在一个键中使用超过 3-4 列,并且价值不大。相反,请考虑加速查询的替代方法,如 Supercharging your ClickHouse queries 和 Working with time series data in Clickhouse 文章中所讨论的那样。
Map 类型在本篇文章中的许多模式中都很常见。此类型要求值和键具有相同的类型 - 这对于 Kubernetes 标签来说已经足够了。请注意,当查询 Map 类型的子键时,将加载整个父列。如果 Map 有许多键,这可能会导致显著的查询性能损失。如果您需要频繁查询特定键,请考虑将其移动到根目录下的专用列中。
请注意,我们目前发现 OTEL 收集器的默认表模式和排序键可能会使某些查询在数据集变大后变得昂贵,尤其是当您的访问模式与键不匹配时。用户应根据其工作流程评估模式并提前创建表以避免这种情况。
OTEL 模式为使用分区通过 TTL 管理数据提供了灵感。这与日志数据尤其相关,日志数据通常只需要保留几天,之后就可以删除。请注意,分区可能会对查询性能产生正面或负面影响:如果大多数查询命中单个分区,则可以提高查询性能。相反,如果查询通常命中多个分区,则可能会导致性能下降。
最后,即使您的访问模式偏离了排序键,线性扫描在 ClickHouse 中也非常快,这使得大多数查询仍然实用。未来的文章将更详细地探讨优化日志的模式和排序键。
可视化工具
我们目前推荐使用 Grafana 和 官方 ClickHouse 插件来可视化和探索日志数据。之前的文章 和 视频 深入探讨了这个插件。
我们之前使用 Fluent Bit 的 博客文章 演示了在 Grafana 中可视化来自 Kubernetes 的日志数据。该仪表板可以从 这里 下载,并按照如下所示 导入到 Grafana 中 - 请注意仪表板 ID 17284
。如何针对特定的代理选择进行调整留给读者自行完成。

此仪表板的只读版本在此处可用 here
结论
这篇博文展示了如何使用代理和技术的组合轻松地收集日志并将其存储在 ClickHouse 中。虽然我们使用了现代 Kubernetes 架构来说明这一点,但这些工具同样适用于更传统的自管理服务器或容器编排系统。我们还介绍了查询以及可能的互操作性方法和挑战。为了进一步阅读,我们鼓励用户探索本文之外的主题,例如代理如何处理队列、反压以及它们承诺的交付保证。我们将在以后的文章中探讨这些主题,并在探索如何使用生命周期功能优化模式和管理数据之前,将指标和跟踪数据添加到我们的 ClickHouse 实例中。