目录
简介
ClickHouse 作为高性能 OLAP 数据库,用于许多用例,包括时间序列数据的实时分析。ClickHouse 用例的多样性推动了大量分析函数的开发,这些函数有助于查询大多数数据类型。这些查询功能和高压缩率越来越多地导致用户利用 ClickHouse 存储可观测性数据。这些数据采用三种常见形式:日志、指标和跟踪。在本系列博文中,我们将探讨如何收集、最佳存储、查询和可视化这些“支柱”。
在本篇文章中,我们将从日志开始,探索收集和查询日志的可能性。我们已尽力确保示例可以复现。我们还要注意,ClickHouse 对特定数据类型的代理支持不断发展,本文所述内容代表了截至 2023 年 1 月的生态系统现状。因此,我们始终鼓励用户查看文档和相关问题。
虽然我们的示例假设现代架构,其中用户必须从 Kubernetes 集群中收集日志,但这些建议和建议并非依赖于 Kubernetes,同样适用于自管理服务器或其他容器编排系统。我们使用我们的开发云环境进行测试,每天从大约 20 个节点生成约 100 GB 的日志。请注意,我们没有努力调整代理或衡量其资源开销 - 我们建议用户在生产部署之前进行研究或实践。本文还重点介绍数据收集,提出架构和数据模型,但将优化留到下一篇文章中。
在我们的示例中,我们存储数据在ClickHouse Cloud服务中,您可以在几分钟内免费试用一个集群,让我们处理基础设施并让您开始查询!
注意:本文中所有可复现的配置示例都可以在此存储库中找到。
架构
大多数代理使用一种常见的架构模式来大规模收集可观测性数据,推广了代理和聚合器概念。对于小型部署,可以忽略后者,代理部署在靠近其数据源的位置,负责处理数据并通过 HTTP 或本机协议直接将数据发送到 ClickHouse。在 Kubernetes 环境中,这意味着将代理部署为Daemonset。这将向每个 K8s 节点部署一个代理 pod,负责收集其他容器的日志(通常从磁盘读取)。
对于不需要高耐用性或可用性以及代理数量较少且配置更改摩擦较小的用户来说,这种架构就足够了。但是,用户应该注意,这可能会导致许多小的插入操作,尤其是如果代理被配置为频繁刷新数据,例如,由于需要立即提供数据以进行分析和问题识别。在这种情况下,用户应该考虑配置代理以使用异步插入,以避免过多部分带来的常见问题。
大型部署引入了聚合器或网关概念。这旨在将轻量级代理配置在靠近其数据源的位置,仅负责将数据转发到聚合器。这减少了中断现有服务的可能性。聚合器负责处理步骤,例如丰富、过滤和确保应用架构,以及批处理和可靠地交付到 ClickHouse。聚合器通常部署为Deployment或Statefulset,并且可以复制以实现高可用性(如果需要)。
除了最大程度地减少对潜在关键服务的源数据的负载外,这种架构还允许将数据批处理并以更大的块插入 ClickHouse。此属性非常重要,因为它与 ClickHouse 的插入最佳实践相一致。
上述架构简化了企业架构,在实际应用中,还需要考虑数据存储位置、负载平衡、高可用性、复杂路由以及将记录(存档)系统和分析系统分开的需求。Vector 文档此处详细介绍了这些概念。虽然是特定于 Vector 的,但这些原则适用于讨论的其他代理。这些架构的一个重要特点是代理和聚合器也可以是异构的,混合使用常见技术,尤其是在收集不同类型的数据时,因为某些代理在不同的可观测性支柱方面表现出色。
代理
在 ClickHouse,我们的用户倾向于使用四种主要的代理技术:Open Telemetry 收集器、Vector、FluentBit 和 Fluentd。后两者具有相同的来源和许多相同的概念。为了简洁起见,我们将探讨 FluentBit,它更轻量级,足以用于 Kubernetes 中的日志收集,但 Fluentd 将是一种有效的方法。这些代理可以充当聚合器或收集器角色,并且可以一起使用(存在一些限制)。虽然它们使用不同的术语,但它们都利用了具有可插拔输入、过滤器/处理器和输出的通用架构。ClickHouse 作为官方输出得到支持,或者通过通用 HTTP 支持实现集成。
然而,在下面的初始示例中,我们将每个代理都部署在聚合器和收集器角色中。我们利用每个代理的官方 Helm 图表来提供一个简单的入门体验,注意重要的配置更改,并共享 values.yaml
文件。
我们的示例使用单个副本作为聚合器,尽管这些副本可以轻松地部署多个副本并进行负载平衡以提高性能和容错能力。所有代理都支持使用 Kubernetes 元数据丰富日志,这对于将来的分析至关重要,例如日志的来源 Pod 名称、容器 ID 和节点。注释 和 标签 也可以包含在日志条目中(默认情况下在 FluentBit 和 Vector 中启用)。这些通常是稀疏的,但可能很多(数百个);生产环境应该评估其价值并对其进行过滤。我们建议对这些使用 Map 类型,以避免列爆炸,这会对 查询产生影响。
所有代理都需要在聚合器角色中进行调整(通过 资源 YAML 键),以避免 OOM 问题并跟上我们的吞吐量(每天约 100GB)。具体情况可能有所不同,具体取决于聚合器的数量和日志吞吐量,但在大型环境中几乎总是需要调整资源。
Open Telemetry (OTEL) 收集器 (alpha)
OpenTelemetry 是一个用于对可观察性数据进行检测、生成、收集和导出的一组工具、API 和 SDK。除了在大多数流行语言中提供代理外,Collector 组件(用 Golang 编写)提供了如何接收、处理和导出可观察性数据的与供应商无关的实现。通过支持多种输入格式(如 Prometheus 和 OTLP),以及各种导出目标(包括 ClickHouse),OTEL Collector 可以提供一个集中式处理网关。Collector 使用术语 接收器、处理器 和 导出器 来表示其三个阶段,并使用 网关 来表示聚合器实例。
虽然 Collector 更常用于网关/聚合器,处理诸如批处理和重试之类的任务,但它也可以部署为 代理本身。OTLP 代表了 Open Telemetry 数据标准,用于网关和代理实例之间的通信,可以通过 gRPC 或 HTTP 进行。正如我们将在下面看到的那样,此协议也受 Vector 和 FluentBit 支持。
ClickHouse 支持
ClickHouse 通过 社区贡献 在 OTEL 导出器中得到支持,并支持日志和跟踪(PR 正在审核中,用于指标)。与 ClickHouse 的通信通过官方 Go 客户端,通过优化的本机格式和协议进行。
在使用 Open Telemetry Collector 之前,用户应该考虑以下几点
- 代理使用的 ClickHouse 数据模型和模式是硬编码的。截至撰写本文时,无法更改使用的类型或编解码器。通过在部署连接器之前创建表来缓解此问题,从而强制执行模式。
- 导出器不与核心 OTEL 分发一起分发,而是作为扩展通过
contrib
映像分发。实际上,这意味着在 Helm 图表中使用正确的 Docker 映像。 - 导出器处于 alpha 阶段,尽管我们收集了超过 1 TB 的日志没有遇到任何问题,但用户应遵循 Open Telemetry 提供的建议。OTEL 的日志用例仍然相对较新,成熟度不如 Fluent Bit 或 Vector 产品。
Kubernetes 部署
如果只收集日志,官方 Helm 图表 代表了最简单的部署方式。在将来的文章中,当我们对应用程序进行检测时,操作员 提供自动检测功能和其他部署模式,例如作为 sidecar。但是,对于日志,基本图表就足够了。有关安装和配置图表的完整详细信息,请参见 此处,包括部署网关和代理以及示例配置的步骤。
请注意,导出器还支持 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。具体来说,这允许有效地删除一天的数据。它可能会对查询产生积极和消极的影响。数据跳过布隆索引的使用是一个高级主题,我们将在以后有关模式优化的文章中介绍。在此处使用 Map 类型来表示 Kubernetes 和日志属性会影响我们的查询语法。
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 公共许可证 2.0 版下授权)可观察性数据管道工具,支持收集、转换和路由日志、指标和跟踪数据。它旨在与供应商无关,并支持多种 输入 和 输出,包括 OTLP 协议,使其能够作为 Open Telemetry 代理的聚合器运行。Vector 用 Rust 编写,使用术语 源、转换 和 接收器 来表示其 3 阶段管道。它代表了一个功能丰富的日志收集解决方案,在 ClickHouse 社区中越来越受欢迎。
ClickHouse 支持
ClickHouse 通过 专用接收器(目前处于 Beta 阶段)在 Vector 中得到支持,通信通过 使用 JSON 格式的 HTTP 协议 以及 插入时的批处理请求 进行。虽然性能不如其他协议,但这将数据处理卸载到 ClickHouse,并简化了调试网络流量。虽然会强制执行数据模型,但用户必须创建目标表并选择其类型和编码。一个 skip_unknown_fields
选项允许用户创建具有可用列子集的表。这将导致目标表中不存在的任何列被忽略。下面我们在 vector
数据库中创建一个目标表,涵盖所有发布后的列,包括从 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 部署
同样,我们使用 Helm 作为我们的首选安装方法,通过使用官方图表。聚合器和代理的完整安装详细信息,以及示例配置,请参见此处。除了将输出源更改为 ClickHouse 之外,主要的更改是需要使用重新映射转换,该转换使用 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 许可证的形式开源。Fluent Bit 被 多个云提供商 采用为一等公民,它提供了与上述工具相当的 输入、处理和输出 功能。
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。这可能会包含数百个字段,因为为每个唯一的标签或注释创建了一个唯一的列。我们之前的帖子建议使用 JSON 类型 用于此 kubernetes
列。这将列创建推迟到 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 收集器作为聚合器。在这种情况下,你可以选择 Fluent Bit 作为你首选的日志收集代理(因为它在这类数据类型方面更加成熟),但继续使用 OTEL 收集器作为聚合器,以保持一致的数据模型。
幸运的是,OTLP 协议(作为更广泛的 Open Telemetry 项目的一部分推广)以及对转发协议的支持(Fluent Bit 首选的通信标准)在某些情况下允许互操作性。
Vector 支持这些协议作为源,可以充当 Fluent Bit 和 Open Telemetry 收集器的日志聚合器。但是,它不支持这些协议作为接收器,这使得在已经部署了 OTEL 收集器或 Fluent Bit 的环境中将其部署为代理变得具有挑战性。请注意,Vector 对应该用 Vector 替换的堆栈组件有很强的见解。
Fluent Bit 最近添加了 OTLP 作为输入 和 输出,可能允许与 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 │ └─────────────────────────────┴─────────────┴─────────────────┴───────────────────┴────────┘
我们在以后的文章中会对此进行探讨,但建议你阅读 用模式和编解码器优化 ClickHouse 作为过渡阅读材料。
查询和可视化日志
常用查询
日志数据实际上是时间序列数据,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 类型
上述许多代理生成类似的模式,并使用 Map 数据类型用于 Kubernetes 注释和标签。用户可以使用 map 符号 来访问嵌套键,以及专门的 ClickHouse map 函数,如果要过滤或选择这些列。
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 字符串和正则表达式 函数对日志行进行模式匹配,如下所示
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 列之后,键中的列通常不建议使用,并且提供的价值很小。相反,请考虑本文中讨论的加速查询的替代方法,例如 加速你的 ClickHouse 查询 和 在 Clickhouse 中使用时间序列数据。
Map 类型在本文中的许多模式中很普遍。这种类型要求值和键具有相同的类型 - 足够用于 Kubernetes 标签。请注意,当查询 Map 类型的子键时,将加载整个父列。如果 map 具有很多键,这会导致严重的查询性能损失。如果你需要频繁地查询特定键,请考虑将其移到根目录的专用列中。
请注意,我们目前发现 OTEL 收集器的默认表模式和排序键会使一些查询变得昂贵,尤其是在数据集变大后,特别是如果你的访问模式与键不匹配。用户应该根据自己的工作流程评估模式,并在提前创建表格以避免这种情况。
OTEL 模式为使用 TTL 管理数据提供了一种使用分区的灵感。这与日志数据尤其相关,因为通常只需要保留几天,然后就可以删除这些数据。请注意,分区可能会对查询性能产生积极或消极的影响:如果大多数查询命中单个分区,查询性能会得到改善。相反,如果查询通常命中多个分区,可能会导致性能下降。
最后,即使你的访问模式偏离了你的排序键,线性扫描在 ClickHouse 中仍然非常快,这使得大多数查询仍然切实可行。以后的文章将更详细地探讨如何优化日志的模式和排序键。
可视化工具
我们目前建议使用 Grafana 通过 官方 ClickHouse 插件 可视化和探索日志数据。之前的文章 和 视频 深入探讨了此插件。
我们之前的博客文章使用 Fluent Bit 演示了如何在 Grafana 中可视化来自 Kubernetes 的日志数据。此仪表盘可以从这里下载,并导入到 Grafana,如下所示 - 请注意仪表盘 ID 为 17284
。将此方法应用于特定代理选择留给读者自己。
此仪表盘的只读版本可从这里获取。
结论
本文展示了如何使用代理和技术组合轻松收集日志并将其存储在 ClickHouse 中。虽然我们使用现代 Kubernetes 架构来说明这一点,但这些工具同样适用于更传统的自管理服务器或容器编排系统。我们还讨论了查询以及可能的互操作性方法和挑战。为了进一步阅读,我们鼓励用户探索本文以外的主题,例如代理如何处理队列、背压以及它们承诺的交付保证。我们将在以后的文章中探讨这些主题,并将指标和跟踪数据添加到我们的 ClickHouse 实例中,然后再探讨如何优化架构以及使用生命周期功能管理数据。