这张照片是由 Pawel Czerwinski 在 Unsplash 上分享的。
如果你用过 Elasticsearch、OpenSearch、Loki 或 VictoriaLogs,并且好奇为什么你的系统需要很多内存或全文搜索速度很慢,那么这篇文章可能对你来说很有趣。
日志的定义是什么让我们假设一下这些日志通常由 Fluentbit 发送,并且包含多个日志字段,然后被送入集中的日志存储库。
{
"@timestamp": "2024-10-18T21:11:52.237412Z",
"message": "错误详情如下: 名称 = ErrorInfo 原因 = 缺少IAM权限 域名 = iam.googleapis.com 元数据 = map[permission:logging.logEntries.create]",
"kubernetes_annotations.EnableNodeJournal": "false",
"kubernetes_annotations.EnablePodSecurityPolicy": "false",
"kubernetes_annotations.SystemOnlyLogging": "false",
"kubernetes_annotations.components.gke.io/component-name": "fluentbit",
"kubernetes_annotations.components.gke.io/component-version": "1.30.2-gke.3",
"kubernetes_annotations.monitoring.gke.io/path": "/api/v1/metrics/prometheus",
"kubernetes_container_hash": "gke.gcr.io/fluent-bit-gke-exporter@sha256:0ef2fab2719444d5b5b0817cf4512f24a347c521d9db9c5e3f85eb4fdcf9a187",
"kubernetes_container_image": "sha256:81082ddf27934f981642f2d8e615f763cc15c08414baa0e908a674ccb116dfcb",
"kubernetes_container_name": "fluentbit",
"kubernetes_docker_id": "f39c349b5368b3abde7c22f97f3f8547c202228e725bb5ef620f399e2a5e67af",
"kubernetes_host": "gke-sandbox-sandbox-pool-4cpu-b-4dc82194-l0an",
"kubernetes_labels.component": "fluentbit-gke",
"kubernetes_labels.controller-revision-hash": "68cfcc69c",
"kubernetes_labels.k8s-app": "fluentbit-gke",
"kubernetes_labels.kubernetes.io/cluster-service": "true",
"kubernetes_labels.pod-template-generation": "24",
"kubernetes_namespace_name": "kube-system",
"kubernetes_pod_id": "7d75e660-9fcf-4b6a-b860-210293b5eda6",
"kubernetes_pod_name": "fluentbit-gke-jt7wb",
"stream": "stderr"
}
这些日志文件包括以下字段:
@timestamp
字段,包含日志生成的时间。message
字段,包含纯文本的日志消息。- 其他 Kubernetes 相关字段,这些字段用来标识给定日志条目对应的源容器。
这种日志条目一般长度在 1KiB-2KiB
之间。
Elasticsearch 为每个摄入的日志条目分配一个唯一的 ID(例如,存储普通日志条目的文件中的偏移量)。然后它将每个日志字段拆分成词(即标记)。例如,message
字段的值 error details: name = ErrorInfo
被拆分为 error
、details
、name
和 ErrorInfo
单词(即标记)。然后它将这些标记持久化到 倒排索引 中——这是一个从 (字段名称; 标记)
到日志条目 ID
的映射关系。例如,上面的 message
字段会被转换为倒排索引中的四个条目:
(消息; 错误信息)
->ID
(消息; 详细信息)
->ID
(消息; 名称)
->ID
(消息; ErrorInfo)
->ID
典型的token长度约为5–10字节。因此,我们可以估计Elasticsearch需要为每个长度为1KiB
的输入的日志条目创建大约125个倒排索引条目。因此,Elasticsearch为每个长度为1KiB
的10亿条日志创建了大约1250亿个条目。这些条目通常以'postings'这种紧凑形式存储。
(字段; token)
对应于 [ID_1, ID_2, … ID_N]
例如,所有 kubernetes_container_name=fluentbit
字段的标记最终都会合并成一个单一的倒排索引条目,在所有包含该字段的日志中。
(kubernetes_container_name; fluentbit) 对应 [ID_1, ID_2, … ID_N] (表示某个容器的ID列表)
如果日志数量达到1.25亿,那么 [ID_1, ID_2, … ID_N]
列表将包含1.25亿个项目。每个ID通常是64位整数,所以这些项目大约占用 125M*8 = 1GB
。
那么,用于存储十亿个1KiB
日志(每个日志包含125个标记)的倒排索引大小至少为1B*125*8 = 1TB
(不包括(field_name; token)
对所需的存储空间)。Elasticsearch需要存储原始日志,以便在查询结果中显示它们。十亿个1KiB
日志需要占用1B*1KiB = 1TiB
的存储空间。因此,所需的总存储空间为1TB + 1TiB = 2TiB
。Elasticsearch可以通过roaring bitmaps对倒排索引进行压缩。它也可以压缩原始日志。这可能会将所需的磁盘空间减少数倍,但仍太大了 :()
Elasticsearch 使用倒排索引进行快速全文搜索。当你搜索某个词(即 token)在某个字段时,它会在倒排索引中,使用二分搜索快速定位 (field_name; token)
对的位置,然后通过它们的 ID 逐个读取原始日志条目并从存储中读出。这就是为什么 Elasticsearch 在全文搜索中表现出如此出色的查询速度!
查询Elasticsearch日志是否有缺点?有的:
- 当你搜索某个字段值,该值出现在大量的日志中时,Elasticsearch 在查询时需要读取 巨大的 候选列表文档。例如,如果你搜索
kubernetes_container_name=fluentbit
字段的日志,该字段存在于 1.25 亿条日志中,那么 Elasticsearch 需要从相应的倒排索引候选列表文档中读取 *125M8=1GiB 的 8 字节日志 ID。这样的查询可能非常耗时。或者,为了稍微提高查询速度,它们可能需要占用 大量的 RAM** 来缓存所有需要的候选列表文档。不幸的是,这种情况在 Elasticsearch 的典型生产环境中非常普遍 :( - 当查询返回的日志数量过多时,Elasticsearch 可能需要从存储系统的随机位置读取这些日志。这在低 IOPS 的存储系统上可能会特别慢。例如,典型的 HDD 每秒提供 100 至 200 次随机读取操作。这意味着如果没有缓存在 RAM 中,Elasticsearch 可能需要多达
10K logs / 100 iops = 100 秒
来从存储系统读取并返回10K
条匹配的日志。
我们来回顾一下:
- Elasticsearch 拥有出色的全文搜索性能,这归功于倒排索引,可以在所有日志字段中执行搜索。
- 处理中等和大量日志(例如超过一太字节)所需的存储空间巨大。
- 查询中等和大量日志(例如超过一太字节)时,Elasticsearch 需要大量的内存以确保较快的速度。
洛基会提取除了 message
和 @timestamp
之外的日志字段,按字段名称进行排序,然后根据这些字段构建日志流标签集(如):例如 log stream labelset
。
{
kubernetes_annotations.启用节点日志="false",
kubernetes_annotations.启用Pod安全策略="false",
kubernetes_annotations.仅系统日志="false",
kubernetes_annotations.components.gke.io/组件名称="fluentbit",
kubernetes_annotations.components.gke.io/组件版本="1.30.2-gke.3",
kubernetes_annotations.monitoring.gke.io/路径="/api/v1/metrics/prometheus",
kubernetes_container_hash="gke.gcr.io/fluent-bit-gke-exporter@sha256:0ef2fab2719444d5b5b0817cf4512f24a347c521d9db9c5e3f85eb4fdcf9a187",
kubernetes_container_image="sha256:81082ddf27934f981642f2d8e615f763cc15c08414baa0e908a674ccb116dfcb",
kubernetes_container_name="fluentbit",
kubernetes_docker_id="f39c349b5368b3abde7c22f97f3f8547c202228e725bb5ef620f399e2a5e67af",
kubernetes_host="gke-sandbox-sandbox-pool-4cpu-b-4dc82194-l0an",
kubernetes_labels.组件="fluentbit-gke",
kubernetes_labels.controller_revision_hash="68cfcc69c",
kubernetes_labels.k8s应用="fluentbit-gke",
kubernetes_labels.kubernetes.io/集群服务="true",
kubernetes_labels.pod模板生成="24",
kubernetes命名空间名称="kube-system",
kubernetes_pod_id="7d75e660-9fcf-4b6a-b860-210293b5eda6",
kubernetes_pod_name="fluentbit-gke-jt7wb",
stream="标准错误"
}
此标签集唯一标识了一个从单个来源(在这种情况下是 Kubernetes 容器)接收的日志流。Loki 对每个日志流仅存储一份此标签集。Loki 将此标签集放入倒排索引中,以便使用日志流过滤器快速定位匹配的日志流。与 Elasticsearch 不同,日志流标签集的倒排索引要小得多,因为它存储的是每个日志流的信息,而不是每个单独日志的信息,并且日志流的数量通常很小(例如,在最坏的情况下有几百万个日志流,而日志条目可能有数十亿个)。
Loki 按日志流分组所有的 message
字段,在每个流中,Loki 按 @timestamp
排序日志,最后以压缩形式存入持久化存储。按流分组日志消息能提高压缩率,因为每个日志流通常包含相似的日志。这使得在典型生产案例中,Loki 可以实现 5x-10x 的压缩率。例如,拥有每条 1KiB
大小的十亿条日志可能只需占用 100GiB
的存储空间。日志流标签集的倒排索引所需存储空间通常可以忽略不计,因为它通常远小于 100GiB
的存储空间。
如您所见,Loki 所需的存储空间比 Elasticsearch 少得多(最多可少 10 倍),以存储相同数量的日志。这对于需要扫描大量已存日志的重型分析查询来说是个好消息,因为 Loki 从存储中读取的数据比 Elasticsearch 少得多。由于较小的倒排索引,Loki 还需要更少的 RAM(最多可少 10 倍)以确保良好的查询性能。
洛基有啥缺点吗? 有:
- 它对于“大海捞针式”的查询提供了非常差的性能,这种查询是在大量日志数据中搜索特定的词或短语。这是因为它需要读取、解压然后扫描所有日志消息以找到给定的词或短语。例如,如果你在一个包含十亿条日志消息(每条消息大小为 1KiB)的集合中搜索某个唯一的
trace_id=7d75e660–9fcf-4b6a-b860-210293b5eda6
,那么Loki需要扫描1B*1KiB=1TiB
的数据量。当然,由于优秀的压缩比,它可能只需要读取100GiB
的数据,但这依然无法避免在快速SSD和NVMe磁盘上的查询时间过慢的问题。 - 它对于具有高基数字段的结构化日志的支持极其有限,这些字段包含大量独特的值,例如
user_id
,trace_id
,ip
等。如果你将此类字段存储到日志流标签集,则Loki将会因为倒排索引急剧膨胀而消耗所有的RAM。此外,这也将显著增加磁盘 I/O 并降低查询速度,因为其并没有为处理大量日志流进行优化。
回顾一下:
- Loki需要的存储空间和RAM比Elasticsearch少得多。
- 在Loki中进行全文搜索查询通常会慢很多(比Elasticsearch慢1000倍左右)。
- Loki对包含高基数字段的结构化日志支持不佳。
VictoriaLogs 将每个日志字段以类似于 Elasticsearch 的方式拆分成单词(也称为标记),但不会像 Elasticsearch 那样创建倒排索引。相反,它会从这些标记中创建 Bloom 过滤器。这些 Bloom 过滤器用于快速跳过那些查询中没有提到的单词对应的数据块。例如,如果搜索某个独特的短语,如 trace_id=7d75e660–9fcf-4b6a-b860-210293b5eda6
,则大多数数据块将不会被读取,而只读取、解包并检查少量包含该短语的数据块。这有助于提高这种“大海捞针”式查询的性能。
维多利亚日志中的布隆过滤器只需要为日志中看到的每个唯一词项保留2字节,而Elasticsearch中的倒排索引则至少需要为日志中看到的每个词项保留8字节。通常,唯一词项的数量远小于词项的总数。一个典型的日志条目包含多达5个唯一词项,而其余的词项则在日志条目之间重复出现。因此,十亿条日志条目包含1B * 5 = 50亿
个唯一词项,这些词项被存储为50亿 * 2 字节/词项 = 10GB
。
因此,布隆滤波器通常比倒排索引小10到100倍(对于相同的词集)。这减少了存储空间和RAM的需求,从而使得VictoriaLogs在数据摄取和查询方面比Elasticsearch更有效率。这也减少了在执行大量查询时的磁盘读取。
VictoriaLogs 也有类似 Loki 的 日志流 的概念。不同之处在于,VictoriaLogs 默认不将日志字段放入日志流标签集中。相反,它依赖于日志输送器通过 _stream_fields
查询参数或 VL-Stream-Fields
HTTP 请求头提供的日志流字段集,根据 这些文档 中的说明。这使得高效存储和查询具有高基数字段(如 user_id
、trace_id
或 ip
)的结构化日志成为可能。
VictoriaLogs 每个日志字段的数据都单独存储在物理独立的存储区域(即类似于 ClickHouse 的column-oriented storage)。这在查询时减少了需要读取的数据量,因为只读取了所需的字段数据。这也有助于提高各字段数据的压缩率,从而降低了存储空间需求。
维多利亚日志软件中有啥缺点吗? 有的,比如:
- 对于一些简单的查询,如选择少量的日志条目,VictoriaLogs 的全文搜索速度会比 Elasticsearch 更慢。这是因为 VictoriaLogs 需要从布隆过滤器中读取更多的数据,而 Elasticsearch 只需要读取倒排索引中的少量数据。而对于涉及多个过滤条件的复杂查询,VictoriaLogs 在这些复杂查询上的表现通常优于 Elasticsearch。这是因为 Elasticsearch 需要读取的倒排索引项的大小开始超过 VictoriaLogs 需要读取的布隆过滤器的大小。
- VictoriaLogs 目前尚不支持 facets。
让我们来个简短的回顾:
- VictoriaLogs 使用布隆过滤器来提高全文搜索的性能,同时保持较低的存储空间使用(比 Elasticsearch 少多达 15 倍的存储空间)和较低的 RAM 大小需求(比 Elasticsearch 少多达 30 倍)。不过,简单的查询可能仍然比 Elasticsearch 更慢一些 :(
- VictoriaLogs 支持类似于 Grafana Loki 的日志流。这使得对日志流的快速查询变得可能。
- VictoriaLogs 使用列式存储来进一步减少存储空间使用。这在处理大量日志的密集查询时也减少了读取带宽的使用。
这几个开源的日志解决方案——Elasticsearch、Loki 和 VictoriaLogs 这些工具——各有优缺点。我试着用简单明了的方式来解释这些工具如何存储和检索日志。希望这些信息能帮助您选出最适合您需求的日志解决方案。如果有不确定的地方,可以同时试用几种工具,看看哪一种最适合您的具体需求。
这篇文章没有提到非技术方面的问题,例如操作上的复杂性(配置、部署和维护)、基础设施成本、查询语言的易用程度、与其他解决方案的兼容性、文档的质量等。我建议从每个解决方案的快速入门文档开始。
完全坦白:我是VictoriaLogs的主要开发者,写这篇文章时尽量保持了公正。
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章