目前有哪些从 OpenSearch 迁移到 Elasticsearch® 的选项?
OpenSearch 是 Elasticsearch 7.10 的一个分支,最近与自身的分歧越来越大,导致功能集不同,性能也不同,如此基准测试所示(提示:它目前比 Elasticsearch 慢得多)。
鉴于这两个解决方案之间的差异,无法从 OpenSearch 还原快照,也无法从远程重新索引,因此我们唯一的选择是使用介于两者之间的东西,该东西将从 OpenSearch 读取并写入 Elasticsearch。
本博客将向您展示从 OpenSearch 迁移到 Elasticsearch 以获得更好的性能和更少的磁盘使用量是多么容易!
10 亿条日志
我们将使用我们用于基准测试的部分数据集,该数据集在磁盘上占用大约半个 TB 的空间,包括副本,并且跨越一周(2023 年 1 月 1 日至 7 日)。
我们总共有 1,009,165,775 个文档,这些文档在 OpenSearch 中占用 453.5GB 的空间,包括副本。每个文档为 241.2KB。当我们启用 Elasticsearch 中的一些优化功能时,这将非常重要,这些优化功能将在不牺牲性能的情况下大幅减小此总大小!
这 10 亿条日志数据集分布在九个索引中,这些索引是我们称为 logs-myapplication-prod 的数据流的一部分。根据最佳分片大小调整实践,我们有大约 25GB 大小的主分片。GET _cat/indices 显示了我们要处理的索引
index docs.count pri rep pri.store.size store.size
.ds-logs-myapplication-prod-000049 102519334 1 1 22.1gb 44.2gb
.ds-logs-myapplication-prod-000048 114273539 1 1 26.1gb 52.3gb
.ds-logs-myapplication-prod-000044 111093596 1 1 25.4gb 50.8gb
.ds-logs-myapplication-prod-000043 113821016 1 1 25.7gb 51.5gb
.ds-logs-myapplication-prod-000042 113859174 1 1 24.8gb 49.7gb
.ds-logs-myapplication-prod-000041 112400019 1 1 25.7gb 51.4gb
.ds-logs-myapplication-prod-000040 113362823 1 1 25.9gb 51.9gb
.ds-logs-myapplication-prod-000038 110994116 1 1 25.3gb 50.7gb
.ds-logs-myapplication-prod-000037 116842158 1 1 25.4gb 50.8gb
OpenSearch 和 Elasticsearch 集群都具有相同的配置:3 个节点,具有 64GB RAM 和 12 个 CPU 核心。就像在基准测试中一样,集群在 Kubernetes 中运行。
将数据从 A 移动到 B
通常,如果集群是彼此兼容的版本,则将数据从一个 Elasticsearch 集群移动到另一个集群就像快照和还原一样容易;如果您需要实时同步和最大程度地减少停机时间,则可以使用从远程重新索引。当从 OpenSearch 迁移数据到 Elasticsearch 时,这些方法不适用,因为这两个项目从 7.10 分支开始就产生了很大的分歧。但是,有一种方法可行:滚动。
滚动
滚动涉及使用外部工具(例如 Logstash®)从源集群读取数据并将其写入目标集群。此方法提供了高度的自定义功能,允许我们在迁移过程中根据需要转换数据。以下是使用 Logstash 的几个优点
- 易于并行化:编写可以从索引的不同“切片”读取的并发作业非常容易,这实际上最大化了我们的吞吐量。
- 排队:Logstash 会在发送之前自动对文档进行排队。
- 自动重试:如果在数据传输过程中发生故障或错误,Logstash 将自动尝试重新发送数据;此外,它会停止频繁查询源集群,直到重新建立连接,所有这些都无需人工干预。
滚动允许我们进行初始搜索,并不断从 Elasticsearch 中提取批处理结果,直到没有更多结果为止,这类似于关系数据库中“游标”的工作方式。
滚动搜索通过冻结构成索引的段来创建时间快照,直到发出请求的时间,从而防止这些段合并。因此,滚动看不到在发出初始搜索请求后对索引所做的任何更改。
迁移策略
在没有优化的情况下,从 A 读取和在 B 中写入可能会很慢,因为它涉及分页遍历结果,将每个批处理通过网络传输到 Logstash,然后 Logstash 会将文档组装到另一个批处理中,然后再将这些批处理再次通过网络传输到 Elasticsearch,其中文档将被索引。因此,当涉及到如此庞大的数据集时,我们必须非常高效,并在我们可以的地方提取每一位性能。
让我们从事实开始——我们对需要传输的数据了解多少?我们在数据流中有九个索引,每个索引大约有 1 亿个文档。让我们只用其中一个索引进行测试,并测量索引速率,以了解迁移需要多长时间。可以通过激活 Elastic® 中的监控功能,然后导航到要检查的索引来查看索引速率。
深度滚动
传输日志行的最简单方法是让 Elasticsearch 滚动浏览整个数据集,并在完成后稍后检查。这里我们将介绍我们的前两个变量:PAGE_SIZE 和 BATCH_SIZE。前者是我们每次查询时将从源中提取的记录数,后者是 Logstash 将组装在一起并写入目标索引的文档数。
对于如此庞大的数据集,随着这种深度分页的进行,滚动速度会减慢。索引速率从 6,000 个文档/秒开始,然后稳定下降到 700 个文档/秒,因为分页变得非常深入。在没有任何优化的情况下,迁移 10 亿个文档将需要 19 天的时间!我们可以做得更好!
切片我很好
我们可以通过使用一种称为切片滚动的方法来优化滚动,我们在其中将索引分成不同的切片以独立使用它们。
在这里,我们将介绍我们最后两个变量:SLICES 和 WORKERS。切片数量不能太少,因为性能会随着时间的推移而急剧下降,而且切片数量也不能太大,因为维护滚动的开销会抵消较小搜索的好处。
让我们首先迁移一个索引(在我们的九个索引中),并使用不同的参数来查看哪个组合可以为我们提供最高的吞吐量。
切片 | 页面大小 | 工作线程 | 批处理大小 | 平均索引速率 |
3 | 500 | 3 | 500 | 13,319 个文档/秒 |
3 | 1,000 | 3 | 1,000 | 13,048 个文档/秒 |
4 | 250 | 4 | 250 | 10,199 个文档/秒 |
4 | 500 | 4 | 500 | 12,692 个文档/秒 |
4 | 1,000 | 4 | 1,000 | 10,900 个文档/秒 |
5 | 500 | 5 | 500 | 12,647 个文档/秒 |
5 | 1,000 | 5 | 1,000 | 10,334 个文档/秒 |
5 | 2,000 | 5 | 2,000 | 10,405 个文档/秒 |
10 | 250 | 10 | 250 | 14,083 个文档/秒 |
10 | 250 | 4 | 1,000 | 12,014 个文档/秒 |
10 | 500 | 4 | 1,000 | 10,956 个文档/秒 |
看起来我们有一组很好的候选方案,可以在 12K 到 14K 个文档/秒之间最大化单个索引的吞吐量。但这并不意味着我们已经达到了上限。即使搜索操作是单线程的,并且每个切片都会触发顺序搜索操作来读取数据,但这并不能阻止我们并行读取多个索引。
默认情况下,打开的最大滚动数是 500——可以使用 search.max_open_scroll_context 集群设置更新此限制,但默认值对于此特定迁移足够了。
让我们迁移
准备我们的目标索引
我们将创建一个名为 logs-myapplication-reindex 的数据流来写入数据,但在索引任何数据之前,让我们确保我们的索引模板和索引生命周期管理配置已正确设置。索引模板充当创建新索引的蓝图,允许您定义应在索引中一致应用的各种设置。
索引生命周期管理策略
索引生命周期管理 (ILM) 同样至关重要,因为它可以自动管理索引的整个生命周期。使用 ILM,您可以定义策略,以确定数据应保留多长时间、何时应将其滚动到新索引中以及何时应删除或存档旧索引。我们的策略非常简单
PUT _ilm/policy/logs-myapplication-lifecycle-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_primary_shard_size": "25gb"
}
}
},
"warm": {
"min_age": "0d",
"actions": {
"forcemerge": {
"max_num_segments": 1
}
}
}
}
}
}
索引模板(并节省 23% 的磁盘空间)
既然我们在这里,我们将继续启用合成源,这是一项巧妙的功能,允许我们存储和丢弃原始 JSON 文档,同时仍然可以在需要时从存储的字段重建它。
在我们的示例中,启用合成源使得存储效率显着提高了 23.4%,将存储单个文档所需的大小从 OpenSearch 中的 241.2KB 减少到 Elasticsearch 中的 185KB。
因此,我们的完整索引模板是
PUT _index_template/logs-myapplication-reindex
{
"index_patterns": [
"logs-myapplication-reindex"
],
"priority": 500,
"data_stream": {},
"template": {
"settings": {
"index": {
"lifecycle.name": "logs-myapplication-lifecycle-policy",
"codec": "best_compression",
"number_of_shards": "1",
"number_of_replicas": "1",
"query": {
"default_field": [
"message"
]
}
}
},
"mappings": {
"_source": {
"mode": "synthetic"
},
"_data_stream_timestamp": {
"enabled": true
},
"date_detection": false,
"properties": {
"@timestamp": {
"type": "date"
},
"agent": {
"properties": {
"ephemeral_id": {
"type": "keyword",
"ignore_above": 1024
},
"id": {
"type": "keyword",
"ignore_above": 1024
},
"name": {
"type": "keyword",
"ignore_above": 1024
},
"type": {
"type": "keyword",
"ignore_above": 1024
},
"version": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"aws": {
"properties": {
"cloudwatch": {
"properties": {
"ingestion_time": {
"type": "keyword",
"ignore_above": 1024
},
"log_group": {
"type": "keyword",
"ignore_above": 1024
},
"log_stream": {
"type": "keyword",
"ignore_above": 1024
}
}
}
}
},
"cloud": {
"properties": {
"region": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"data_stream": {
"properties": {
"dataset": {
"type": "keyword",
"ignore_above": 1024
},
"namespace": {
"type": "keyword",
"ignore_above": 1024
},
"type": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"ecs": {
"properties": {
"version": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"event": {
"properties": {
"dataset": {
"type": "keyword",
"ignore_above": 1024
},
"id": {
"type": "keyword",
"ignore_above": 1024
},
"ingested": {
"type": "date"
}
}
},
"host": {
"type": "object"
},
"input": {
"properties": {
"type": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"log": {
"properties": {
"file": {
"properties": {
"path": {
"type": "keyword",
"ignore_above": 1024
}
}
}
}
},
"message": {
"type": "match_only_text"
},
"meta": {
"properties": {
"file": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"metrics": {
"properties": {
"size": {
"type": "long"
},
"tmin": {
"type": "long"
}
}
},
"process": {
"properties": {
"name": {
"type": "keyword",
"ignore_above": 1024
}
}
},
"tags": {
"type": "keyword",
"ignore_above": 1024
}
}
}
}
}
构建自定义 Logstash 镜像
我们将在此迁移中使用容器化的 Logstash,因为两个集群都位于 Kubernetes 基础设施上,因此只需启动一个 Pod 即可与两个集群通信,这更加方便。
由于 OpenSearch 不是官方的 Logstash 输入,我们必须构建一个包含 logstash-input-opensearch 插件的自定义 Logstash 镜像。让我们使用 docker.elastic.co/logstash/logstash:8.16.1 的基础镜像,并安装该插件即可。
FROM docker.elastic.co/logstash/logstash:8.16.1
USER logstash
WORKDIR /usr/share/logstash
RUN bin/logstash-plugin install logstash-input-opensearch
编写 Logstash 管道
现在我们有了 Logstash Docker 镜像,我们需要编写一个从 OpenSearch 读取并写入 Elasticsearch 的管道。
输入
input {
opensearch {
hosts => ["os-cluster:9200"]
ssl => true
ca_file => "/etc/logstash/certificates/opensearch-ca.crt"
user => "${OPENSEARCH_USERNAME}"
password => "${OPENSEARCH_PASSWORD}"
index => "${SOURCE_INDEX_NAME}"
slices => "${SOURCE_SLICES}"
size => "${SOURCE_PAGE_SIZE}"
scroll => "5m"
docinfo => true
docinfo_target => "[@metadata][doc]"
}
}
让我们分解最重要的输入参数。这些值在这里都表示为环境变量。
- hosts: 指定 OpenSearch 集群的主机和端口。在本例中,它连接到端口 9200 上的“os-cluster”。
- index: 指定要从 OpenSearch 集群中检索日志的索引。在本例中,它是 “logs-myapplication-prod”,这是一个包含实际索引的数据流(例如,.ds-logs-myapplication-prod-000049)。
- size: 指定每个请求中要检索的最大日志数。
- scroll: 定义在 OpenSearch 服务器上保持搜索上下文打开的时间。在本例中,它设置为 “5m”,这意味着必须在五分钟内回答每个请求并请求新的“页面”。
- docinfo 和 docinfo_target: 这些设置控制是否应将文档元数据包含在 Logstash 输出中以及应将其存储在哪里。在本例中,文档元数据存储在 [@metadata][doc] 字段中——这很重要,因为文档的 _id 也将用作目标 id。
如果您是从位于不同基础设施(单独的云提供商)中的集群迁移,则强烈建议使用 ssl 和 ca_file。如果您的 TLS 证书由公共机构签名,则无需指定 ca_file,如果您使用的是 SaaS 并且您的端点可以通过 Internet 访问,则很可能是这种情况。在这种情况下,只需要 ssl => true 就足够了。在我们的例子中,我们所有的 TLS 证书都是自签名的,因此我们还必须提供证书颁发机构 (CA) 证书。
(可选的)过滤器
如果我们愿意,我们可以使用此过滤器来删除或更改要写入 Elasticsearch 的文档,但我们不会这样做,因为我们希望按原样迁移文档。我们只删除 Logstash 在所有文档中包含的额外元数据字段,例如 “@version” 和 “host”。我们还删除了原始的 “data_stream”,因为它包含源数据流名称,该名称在目标中可能不同。
filter {
mutate {
remove_field => ["@version", "host", "data_stream"]
}
}
输出
输出非常简单——我们将把我们的数据流命名为 logs-myapplication-reindex,并且我们使用原始文档中的 document_id 作为 document_id,以确保没有重复的文档。在 Elasticsearch 中,数据流名称遵循 <type>-<dataset>-<namespace> 的约定,因此我们的 logs-myapplication-reindex 数据流的 “myapplication” 作为数据集, “prod” 作为命名空间。
elasticsearch {
hosts => "${ELASTICSEARCH_HOST}"
user => "${ELASTICSEARCH_USERNAME}"
password => "${ELASTICSEARCH_PASSWORD}"
document_id => "%{[@metadata][doc][_id]}"
data_stream => "true"
data_stream_type => "logs"
data_stream_dataset => "myapplication"
data_stream_namespace => "prod"
}
部署 Logstash
我们有几种部署 Logstash 的选项:可以 从命令行本地部署,作为 systemd 服务,通过 docker 或在 Kubernetes 上部署。
由于我们的两个集群都部署在 Kubernetes 环境中,我们将部署 Logstash 作为 Pod,引用我们之前创建的 Docker 镜像。让我们将我们的管道放入 ConfigMap 以及一些配置文件(pipelines.yml 和 config.yml)。
在下面的配置中,我们将 SOURCE_INDEX_NAME、SOURCE_SLICES、SOURCE_PAGE_SIZE、LOGSTASH_WORKERS 和 LOGSTASH_BATCH_SIZE 方便地公开为环境变量,因此您只需填写它们即可。
apiVersion: v1
kind: Pod
metadata:
name: logstash-1
spec:
containers:
- name: logstash
image: ugosan/logstash-opensearch-input:8.10.0
imagePullPolicy: Always
env:
- name: SOURCE_INDEX_NAME
value: ".ds-logs-benchmark-dev-000037"
- name: SOURCE_SLICES
value: "10"
- name: SOURCE_PAGE_SIZE
value: "500"
- name: LOGSTASH_WORKERS
value: "4"
- name: LOGSTASH_BATCH_SIZE
value: "1000"
- name: OPENSEARCH_USERNAME
valueFrom:
secretKeyRef:
name: os-cluster-admin-password
key: username
- name: OPENSEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: os-cluster-admin-password
key: password
- name: ELASTICSEARCH_USERNAME
value: "elastic"
- name: ELASTICSEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: es-cluster-es-elastic-user
key: elastic
resources:
limits:
memory: "4Gi"
cpu: "2500m"
requests:
memory: "1Gi"
cpu: "300m"
volumeMounts:
- name: config-volume
mountPath: /usr/share/logstash/config
- name: etc
mountPath: /etc/logstash
readOnly: true
volumes:
- name: config-volume
projected:
sources:
- configMap:
name: logstash-configmap
items:
- key: pipelines.yml
path: pipelines.yml
- key: logstash.yml
path: logstash.yml
- name: etc
projected:
sources:
- configMap:
name: logstash-configmap
items:
- key: pipeline.conf
path: pipelines/pipeline.conf
- secret:
name: os-cluster-http-cert
items:
- key: ca.crt
path: certificates/opensearch-ca.crt
- secret:
name: es-cluster-es-http-ca-internal
items:
- key: tls.crt
path: certificates/elasticsearch-ca.crt
---
apiVersion: v1
kind: ConfigMap
metadata:
name: logstash-configmap
data:
pipelines.yml: |
- pipeline.id: reindex-os-es
path.config: "/etc/logstash/pipelines/pipeline.conf"
pipeline.batch.size: ${LOGSTASH_BATCH_SIZE}
pipeline.workers: ${LOGSTASH_WORKERS}
logstash.yml: |
log.level: info
pipeline.unsafe_shutdown: true
pipeline.ordered: false
pipeline.conf: |
input {
opensearch {
hosts => ["os-cluster:9200"]
ssl => true
ca_file => "/etc/logstash/certificates/opensearch-ca.crt"
user => "${OPENSEARCH_USERNAME}"
password => "${OPENSEARCH_PASSWORD}"
index => "${SOURCE_INDEX_NAME}"
slices => "${SOURCE_SLICES}"
size => "${SOURCE_PAGE_SIZE}"
scroll => "5m"
docinfo => true
docinfo_target => "[@metadata][doc]"
}
}
filter {
mutate {
remove_field => ["@version", "host", "data_stream"]
}
}
output {
elasticsearch {
hosts => "https://es-cluster-es-http:9200"
ssl => true
ssl_certificate_authorities => ["/etc/logstash/certificates/elasticsearch-ca.crt"]
ssl_verification_mode => "full"
user => "${ELASTICSEARCH_USERNAME}"
password => "${ELASTICSEARCH_PASSWORD}"
document_id => "%{[@metadata][doc][_id]}"
data_stream => "true"
data_stream_type => "logs"
data_stream_dataset => "myapplication"
data_stream_namespace => "reindex"
}
}
就这样。
几个小时后,我们成功地将 10 亿个文档从 OpenSearch 迁移到 Elasticsearch,甚至节省了 23% 以上的磁盘存储空间!既然我们已经在 Elasticsearch 中有了日志,那么如何从中提取实际的业务价值呢?日志包含如此有价值的信息——我们不仅可以用 AIOPS 做各种有趣的事情,比如 自动分类 这些日志,还可以提取 业务指标 并 检测异常,不妨尝试一下。
OpenSearch | Elasticsearch | |||||
索引 | 文档 | 大小 | 索引 | 文档 | 大小 | 差异 |
.ds-logs-myapplication-prod-000037 | 116842158 | 27285520870 | logs-myapplication-reindex-000037 | 116842158 | 21998435329 | 21.46% |
.ds-logs-myapplication-prod-000038 | 110994116 | 27263291740 | logs-myapplication-reindex-000038 | 110994116 | 21540011082 | 23.45% |
.ds-logs-myapplication-prod-000040 | 113362823 | 27872438186 | logs-myapplication-reindex-000040 | 113362823 | 22234641932 | 22.50% |
.ds-logs-myapplication-prod-000041 | 112400019 | 27618801653 | logs-myapplication-reindex-000041 | 112400019 | 22059453868 | 22.38% |
.ds-logs-myapplication-prod-000042 | 113859174 | 26686723701 | logs-myapplication-reindex-000042 | 113859174 | 21093766108 | 23.41% |
.ds-logs-myapplication-prod-000043 | 113821016 | 27657006598 | logs-myapplication-reindex-000043 | 113821016 | 22059454752 | 22.52% |
.ds-logs-myapplication-prod-000044 | 111093596 | 27281936915 | logs-myapplication-reindex-000044 | 111093596 | 21559513422 | 23.43% |
.ds-logs-myapplication-prod-000048 | 114273539 | 28111420495 | logs-myapplication-reindex-000048 | 114273539 | 22264398939 | 23.21% |
.ds-logs-myapplication-prod-000049 | 102519334 | 23731274338 | logs-myapplication-reindex-000049 | 102519334 | 19307250001 | 20.56% |
有兴趣试用 Elasticsearch 吗?开始我们的 14 天免费试用。
本文中描述的任何特性或功能的发布和时间安排均由 Elastic 自行决定。任何当前不可用的特性或功能可能不会按时交付,甚至根本不会交付。