Commit d71e20f0d601548d303e334a6887a1e3e95807e3
1 parent
5f7d7f09
索引同步,用于性能测试
Showing
4 changed files
with
289 additions
and
27 deletions
Show diff stats
| @@ -0,0 +1,167 @@ | @@ -0,0 +1,167 @@ | ||
| 1 | +# Reindex from Remote 注意事项(官方文档要点) | ||
| 2 | + | ||
| 3 | +基于 Elasticsearch 官方文档整理的 **Reindex from remote** 要点,用于从远程 ES 集群(如 8.x)迁移数据到本机集群(如 9.x)。 | ||
| 4 | + | ||
| 5 | +## 官方文档入口 | ||
| 6 | + | ||
| 7 | +- [Reindex API (current)](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html) | ||
| 8 | +- [Reindex from remote (upgrade)](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/reindex-upgrade-remote.html) | ||
| 9 | +- [REST API: Reindex](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reindex-indices) | ||
| 10 | + | ||
| 11 | +--- | ||
| 12 | + | ||
| 13 | +## 必须注意的事项 | ||
| 14 | + | ||
| 15 | +### 1. 目标集群白名单(必须) | ||
| 16 | + | ||
| 17 | +> If reindexing from a remote cluster into a cluster using Elastic Stack, you must **explicitly allow the remote host** using the **`reindex.remote.whitelist`** node setting on the **destination** cluster. | ||
| 18 | + | ||
| 19 | +- **在目标集群(本机 ES)** 的 `elasticsearch.yml` 中配置,不是源集群。 | ||
| 20 | +- 格式:只写 **host:port**,多个用逗号分隔;不写协议。例如: | ||
| 21 | + ```yaml | ||
| 22 | + reindex.remote.whitelist: "120.76.41.98:9200" | ||
| 23 | + ``` | ||
| 24 | +- 修改后需重启**执行 reindex 的节点**(通常是协调节点)才能生效。 | ||
| 25 | + | ||
| 26 | +### 2. 目标索引需事先创建 | ||
| 27 | + | ||
| 28 | +- Reindex **不会**复制源索引的 **settings / mappings**。 | ||
| 29 | +- 目标索引的 mapping、分片数、副本数等需在调用 `_reindex` **之前**在本机创建好。 | ||
| 30 | +- 可用本项目的 `mappings/search_products.json` 在本机创建同名索引。 | ||
| 31 | + | ||
| 32 | +### 3. 权限要求 | ||
| 33 | + | ||
| 34 | +- **源集群(远程)** 用于认证的用户(如 `source.remote.username`)需要: | ||
| 35 | + - 集群权限:`monitor` | ||
| 36 | + - 源索引权限:`read` | ||
| 37 | +- **目标集群(本机)** 执行 reindex 的用户需要: | ||
| 38 | + - 目标索引:`write` | ||
| 39 | + - 若需自动创建目标索引:`auto_configure` 或 `create_index` 或 `manage` | ||
| 40 | + | ||
| 41 | +### 4. 源文档必须开启 _source | ||
| 42 | + | ||
| 43 | +- Reindex 依赖文档的 `_source` 字段;若源索引禁用了 `_source`,无法 reindex。 | ||
| 44 | + | ||
| 45 | +### 5. 远程 Reindex 不支持 Slicing | ||
| 46 | + | ||
| 47 | +- 文档明确说明:**Reindexing from remote clusters does not support manual or automatic slicing.** | ||
| 48 | +- 不能通过 `slices` 或 `slice` 做并行加速,只能单任务拉取。 | ||
| 49 | + | ||
| 50 | +### 6. 远程拉取时的缓冲区与 batch size | ||
| 51 | + | ||
| 52 | +- 从远程 reindex 时,目标集群使用 **on-heap buffer**,默认最大约 **100MB**。 | ||
| 53 | +- 若单文档很大,需在 `source` 里调小 **`size`**(每批文档数),例如 `"size": 500` 或 `200`,避免 OOM。 | ||
| 54 | +- 默认 `size` 为 1000。 | ||
| 55 | + | ||
| 56 | +### 7. max_docs 与 conflicts | ||
| 57 | + | ||
| 58 | +- 用 **`max_docs`** 可限制只迁移前 N 条(注意:与 scroll 顺序不保证严格一致,但数量正确)。 | ||
| 59 | +- 若设置 **`conflicts: "proceed"`**,在遇到版本冲突时仍会继续,但可能从源多读一些文档直到成功写入 `max_docs` 条。 | ||
| 60 | + | ||
| 61 | +### 8. 建议在源索引为 green 时执行 | ||
| 62 | + | ||
| 63 | +- 官方建议在源索引状态为 green 时 reindex,否则节点宕机等可能导致失败。 | ||
| 64 | +- 若使用 `wait_for_completion=false`,可通过 Task API 查进度;重试时可能需要先删掉目标索引中部分数据或设置 `conflicts=proceed`。 | ||
| 65 | + | ||
| 66 | +### 9. 超时(可选) | ||
| 67 | + | ||
| 68 | +- `source.remote.socket_timeout`、`connect_timeout` 可调大,默认约 30s;大批量或网络慢时可适当增加。 | ||
| 69 | + | ||
| 70 | +--- | ||
| 71 | + | ||
| 72 | +## 示例:从远程 tenant_170 同步 10000 条到本机 tenant_0 | ||
| 73 | + | ||
| 74 | +- **源**:远程 `search_products_tenant_170`(约 39731 条) | ||
| 75 | +- **目标**:本机索引 `search_products_tenant_0`,只同步 **10000 条** | ||
| 76 | + | ||
| 77 | +### 步骤 1:在本机 ES 配置白名单 | ||
| 78 | + | ||
| 79 | +在**本机** ES 的 `elasticsearch.yml` 中添加(或合并到已有 `reindex.remote.whitelist`): | ||
| 80 | + | ||
| 81 | +```yaml | ||
| 82 | +reindex.remote.whitelist: "120.76.41.98:9200" | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +保存后重启本机 ES(或至少重启会执行 reindex 的节点)。 | ||
| 86 | + | ||
| 87 | +### 步骤 2:在本机创建目标索引 | ||
| 88 | + | ||
| 89 | +使用本项目 mapping 创建索引 `search_products_tenant_0`(若已存在且结构一致可跳过)。例如用 API: | ||
| 90 | + | ||
| 91 | +```bash | ||
| 92 | +# 本机 ES(按需加 -u user:pass) | ||
| 93 | +curl -X PUT 'http://localhost:9200/search_products_tenant_0?pretty' \ | ||
| 94 | + -H 'Content-Type: application/json' \ | ||
| 95 | + -d @mappings/search_products.json | ||
| 96 | +``` | ||
| 97 | + | ||
| 98 | +或通过项目代码:`create_index_if_not_exists(es_client, "search_products_tenant_0", load_mapping())`。 | ||
| 99 | + | ||
| 100 | +### 步骤 3:在本机执行 Reindex(请求发往本机 ES) | ||
| 101 | + | ||
| 102 | +以下请求是发给**本机 ES**(例如 `http://localhost:9200`),由本机去拉远程数据。 | ||
| 103 | + | ||
| 104 | +```bash | ||
| 105 | +# 请求发往本机 ES(ES 9.x 将 wait_for_completion 放在 query 参数) | ||
| 106 | +curl -X POST 'http://localhost:9200/_reindex?wait_for_completion=true&pretty' \ | ||
| 107 | + -H 'Content-Type: application/json' \ | ||
| 108 | + -d '{ | ||
| 109 | + "max_docs": 10000, | ||
| 110 | + "source": { | ||
| 111 | + "remote": { | ||
| 112 | + "host": "http://120.76.41.98:9200", | ||
| 113 | + "username": "essa", | ||
| 114 | + "password": "4hOaLaf41y2VuI8y" | ||
| 115 | + }, | ||
| 116 | + "index": "search_products_tenant_170", | ||
| 117 | + "size": 500 | ||
| 118 | + }, | ||
| 119 | + "dest": { | ||
| 120 | + "index": "search_products_tenant_0" | ||
| 121 | + } | ||
| 122 | +}' | ||
| 123 | +``` | ||
| 124 | + | ||
| 125 | +说明: | ||
| 126 | + | ||
| 127 | +- `max_docs: 10000`:最多写入 10000 条到目标。 | ||
| 128 | +- `source.remote`:远程 ES 地址与认证(仅本机连远程时使用,不会把密码发到远程)。 | ||
| 129 | +- `source.index`:远程索引名。 | ||
| 130 | +- `source.size`:每批从远程拉取的文档数,500 可降低大文档时本机内存压力。 | ||
| 131 | +- `dest.index`:本机目标索引名。 | ||
| 132 | +- `wait_for_completion: true`:同步等待完成;数据量大可改为 `false`,用返回的 `task_id` 查进度:`GET _tasks/<task_id>`。 | ||
| 133 | + | ||
| 134 | +### 步骤 4:校验条数 | ||
| 135 | + | ||
| 136 | +```bash | ||
| 137 | +curl -X GET 'http://localhost:9200/search_products_tenant_0/_count?pretty' \ | ||
| 138 | + -H 'Content-Type: application/json' \ | ||
| 139 | + -d '{"query":{"match_all":{}}}' | ||
| 140 | +``` | ||
| 141 | + | ||
| 142 | +预期约 10000 条(若未设 `max_docs` 则会与源索引条数一致)。 | ||
| 143 | + | ||
| 144 | +### 一键脚本(可选) | ||
| 145 | + | ||
| 146 | +项目内提供了脚本,可自动创建目标索引并执行上述 reindex(默认 10000 条,目标 `search_products_tenant_0`): | ||
| 147 | + | ||
| 148 | +```bash | ||
| 149 | +# 确保本机 .env 中 ES_HOST 指向本机 ES(如 http://localhost:9200) | ||
| 150 | +chmod +x scripts/reindex_from_remote_tenant_170_to_0.sh | ||
| 151 | +./scripts/reindex_from_remote_tenant_170_to_0.sh | ||
| 152 | +``` | ||
| 153 | + | ||
| 154 | +可通过环境变量覆盖:`REMOTE_ES_HOST`、`REMOTE_ES_USER`、`REMOTE_ES_PASS`、`MAX_DOCS`、`LOCAL_ES_HOST`。详见脚本注释。 | ||
| 155 | + | ||
| 156 | +--- | ||
| 157 | + | ||
| 158 | +## 小结 | ||
| 159 | + | ||
| 160 | +| 项目 | 说明 | | ||
| 161 | +|------|------| | ||
| 162 | +| 白名单 | 在**目标(本机)** ES 的 `elasticsearch.yml` 中配置 `reindex.remote.whitelist` | | ||
| 163 | +| 目标索引 | 事先在本机创建好 mapping/settings | | ||
| 164 | +| 远程权限 | 源集群用户需 `monitor` + 源索引 `read` | | ||
| 165 | +| 限条数 | 使用 `max_docs`,例如 10000 | | ||
| 166 | +| 大批/大文档 | 适当调小 `source.size`(如 500) | | ||
| 167 | +| 并行 | 远程 reindex 不支持 `slices` | |
frontend/index.html
| @@ -73,9 +73,10 @@ | @@ -73,9 +73,10 @@ | ||
| 73 | </div> | 73 | </div> |
| 74 | <div class="tenant-input-wrapper"> | 74 | <div class="tenant-input-wrapper"> |
| 75 | <label for="tenantSelect">tenant ID:</label> | 75 | <label for="tenantSelect">tenant ID:</label> |
| 76 | - <select id="tenantSelect" onchange="onTenantIdChange()"> | 76 | + <input type="text" id="tenantSelect" list="tenantList" placeholder="选择或输入 tenant ID" onchange="onTenantIdChange()"> |
| 77 | + <datalist id="tenantList"> | ||
| 77 | <!-- 选项将通过 JavaScript 动态填充 --> | 78 | <!-- 选项将通过 JavaScript 动态填充 --> |
| 78 | - </select> | 79 | + </datalist> |
| 79 | </div> | 80 | </div> |
| 80 | <div class="tenant-input-wrapper"> | 81 | <div class="tenant-input-wrapper"> |
| 81 | <label for="skuFilterDimension">sku_filter_dimension:</label> | 82 | <label for="skuFilterDimension">sku_filter_dimension:</label> |
frontend/static/js/app.js
| @@ -84,8 +84,8 @@ if (document.readyState === 'loading') { | @@ -84,8 +84,8 @@ if (document.readyState === 'loading') { | ||
| 84 | 84 | ||
| 85 | // 备用初始化:如果上面的初始化失败,在 window.onload 时再试一次 | 85 | // 备用初始化:如果上面的初始化失败,在 window.onload 时再试一次 |
| 86 | window.addEventListener('load', function() { | 86 | window.addEventListener('load', function() { |
| 87 | - const tenantSelect = document.getElementById('tenantSelect'); | ||
| 88 | - if (tenantSelect && tenantSelect.options.length === 0) { | 87 | + const tenantList = document.getElementById('tenantList'); |
| 88 | + if (tenantList && tenantList.options.length === 0) { | ||
| 89 | console.log('Retrying tenant select initialization on window.load...'); | 89 | console.log('Retrying tenant select initialization on window.load...'); |
| 90 | initTenantSelect(); | 90 | initTenantSelect(); |
| 91 | } | 91 | } |
| @@ -93,8 +93,8 @@ window.addEventListener('load', function() { | @@ -93,8 +93,8 @@ window.addEventListener('load', function() { | ||
| 93 | 93 | ||
| 94 | // 最后尝试:延迟执行,确保所有脚本都已加载 | 94 | // 最后尝试:延迟执行,确保所有脚本都已加载 |
| 95 | setTimeout(function() { | 95 | setTimeout(function() { |
| 96 | - const tenantSelect = document.getElementById('tenantSelect'); | ||
| 97 | - if (tenantSelect && tenantSelect.options.length === 0) { | 96 | + const tenantList = document.getElementById('tenantList'); |
| 97 | + if (tenantList && tenantList.options.length === 0) { | ||
| 98 | console.log('Final retry: Initializing tenant select after delay...'); | 98 | console.log('Final retry: Initializing tenant select after delay...'); |
| 99 | if (typeof getAvailableTenantIds === 'function') { | 99 | if (typeof getAvailableTenantIds === 'function') { |
| 100 | initTenantSelect(); | 100 | initTenantSelect(); |
| @@ -111,11 +111,12 @@ function handleKeyPress(event) { | @@ -111,11 +111,12 @@ function handleKeyPress(event) { | ||
| 111 | } | 111 | } |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | -// 初始化租户下拉框 | 114 | +// 初始化租户输入框(带 162/170 等候选,可自行填写任意 tenant ID) |
| 115 | function initTenantSelect() { | 115 | function initTenantSelect() { |
| 116 | const tenantSelect = document.getElementById('tenantSelect'); | 116 | const tenantSelect = document.getElementById('tenantSelect'); |
| 117 | - if (!tenantSelect) { | ||
| 118 | - console.error('tenantSelect element not found'); | 117 | + const tenantList = document.getElementById('tenantList'); |
| 118 | + if (!tenantSelect || !tenantList) { | ||
| 119 | + console.error('tenantSelect or tenantList element not found'); | ||
| 119 | return; | 120 | return; |
| 120 | } | 121 | } |
| 121 | 122 | ||
| @@ -128,25 +129,19 @@ function initTenantSelect() { | @@ -128,25 +129,19 @@ function initTenantSelect() { | ||
| 128 | const availableTenants = getAvailableTenantIds(); | 129 | const availableTenants = getAvailableTenantIds(); |
| 129 | console.log('Available tenants:', availableTenants); | 130 | console.log('Available tenants:', availableTenants); |
| 130 | 131 | ||
| 131 | - if (!availableTenants || availableTenants.length === 0) { | ||
| 132 | - console.warn('No tenant IDs found in configuration'); | ||
| 133 | - return; | ||
| 134 | - } | ||
| 135 | - | ||
| 136 | - // 清空现有选项 | ||
| 137 | - tenantSelect.innerHTML = ''; | 132 | + // 清空 datalist 现有选项 |
| 133 | + tenantList.innerHTML = ''; | ||
| 138 | 134 | ||
| 139 | - // 添加选项 | ||
| 140 | - availableTenants.forEach(tenantId => { | ||
| 141 | - const option = document.createElement('option'); | ||
| 142 | - option.value = tenantId; | ||
| 143 | - option.textContent = tenantId; | ||
| 144 | - tenantSelect.appendChild(option); | ||
| 145 | - }); | ||
| 146 | - | ||
| 147 | - // 设置默认值 | ||
| 148 | - if (availableTenants.length > 0) { | ||
| 149 | - tenantSelect.value = availableTenants.includes('170') ? '170' : availableTenants[0]; | 135 | + if (availableTenants && availableTenants.length > 0) { |
| 136 | + availableTenants.forEach(tenantId => { | ||
| 137 | + const option = document.createElement('option'); | ||
| 138 | + option.value = tenantId; | ||
| 139 | + tenantList.appendChild(option); | ||
| 140 | + }); | ||
| 141 | + // 设置默认值(仅当输入框为空时) | ||
| 142 | + if (!tenantSelect.value.trim()) { | ||
| 143 | + tenantSelect.value = availableTenants.includes('170') ? '170' : availableTenants[0]; | ||
| 144 | + } | ||
| 150 | } | 145 | } |
| 151 | 146 | ||
| 152 | // 初始化分面面板 | 147 | // 初始化分面面板 |
| @@ -0,0 +1,99 @@ | @@ -0,0 +1,99 @@ | ||
| 1 | +#!/bin/bash | ||
| 2 | +# | ||
| 3 | +# 从远程 ES 的 search_products_tenant_170 同步 10000 条到本机 search_products_tenant_0。 | ||
| 4 | +# 请求发往本机 ES,由本机去拉远程数据;需在本机 elasticsearch.yml 配置 reindex.remote.whitelist。 | ||
| 5 | +# | ||
| 6 | +# 用法: | ||
| 7 | +# ./scripts/reindex_from_remote_tenant_170_to_0.sh | ||
| 8 | +# | ||
| 9 | +# 环境变量(可选): | ||
| 10 | +# LOCAL_ES_HOST 本机 ES 地址,用于创建索引和发送 _reindex(默认从 .env 的 ES_HOST 读取,应为本机) | ||
| 11 | +# REMOTE_ES_HOST 远程 ES 地址(默认 http://120.76.41.98:9200) | ||
| 12 | +# REMOTE_ES_USER 远程 ES 用户名(默认 essa) | ||
| 13 | +# REMOTE_ES_PASS 远程 ES 密码(默认 4hOaLaf41y2VuI8y) | ||
| 14 | +# MAX_DOCS 同步条数(默认 10000) | ||
| 15 | +# | ||
| 16 | + | ||
| 17 | +set -e | ||
| 18 | + | ||
| 19 | +cd "$(dirname "$0")/.." | ||
| 20 | +PROJECT_ROOT="$(pwd)" | ||
| 21 | + | ||
| 22 | +# 加载 .env | ||
| 23 | +# shellcheck source=scripts/lib/load_env.sh | ||
| 24 | +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" | ||
| 25 | +load_env_file "${PROJECT_ROOT}/.env" | ||
| 26 | + | ||
| 27 | +# 本机 ES(发 _reindex 请求的目标) | ||
| 28 | +LOCAL_ES_HOST="${LOCAL_ES_HOST:-${ES_HOST:-http://localhost:9200}}" | ||
| 29 | +ES_USERNAME="${ES_USERNAME:-}" | ||
| 30 | +ES_PASSWORD="${ES_PASSWORD:-}" | ||
| 31 | +ES_INDEX_NAMESPACE="${ES_INDEX_NAMESPACE:-}" | ||
| 32 | + | ||
| 33 | +# 远程 ES(数据源) | ||
| 34 | +REMOTE_ES_HOST="${REMOTE_ES_HOST:-http://120.76.41.98:9200}" | ||
| 35 | +REMOTE_ES_USER="${REMOTE_ES_USER:-essa}" | ||
| 36 | +REMOTE_ES_PASS="${REMOTE_ES_PASS:-4hOaLaf41y2VuI8y}" | ||
| 37 | + | ||
| 38 | +MAX_DOCS="${MAX_DOCS:-10000}" | ||
| 39 | +SOURCE_INDEX="search_products_tenant_170" | ||
| 40 | +DEST_INDEX="${ES_INDEX_NAMESPACE}search_products_tenant_0" | ||
| 41 | +MAPPING_FILE="${PROJECT_ROOT}/mappings/search_products.json" | ||
| 42 | + | ||
| 43 | +# 本机 curl 认证 | ||
| 44 | +AUTH_PARAM="" | ||
| 45 | +if [ -n "$ES_USERNAME" ] && [ -n "$ES_PASSWORD" ]; then | ||
| 46 | + AUTH_PARAM="-u ${ES_USERNAME}:${ES_PASSWORD}" | ||
| 47 | +fi | ||
| 48 | + | ||
| 49 | +echo "本机 ES: $LOCAL_ES_HOST" | ||
| 50 | +echo "远程 ES: $REMOTE_ES_HOST" | ||
| 51 | +echo "源索引: $SOURCE_INDEX" | ||
| 52 | +echo "目标索引: $DEST_INDEX" | ||
| 53 | +echo "同步条数: $MAX_DOCS" | ||
| 54 | +echo "" | ||
| 55 | + | ||
| 56 | +# 1. 若目标索引不存在,则创建 | ||
| 57 | +if ! curl -s $AUTH_PARAM "${LOCAL_ES_HOST}/${DEST_INDEX}" -o /dev/null -w "%{http_code}" | grep -q 200; then | ||
| 58 | + echo "创建目标索引: $DEST_INDEX" | ||
| 59 | + if [ ! -f "$MAPPING_FILE" ]; then | ||
| 60 | + echo "错误: mapping 文件不存在: $MAPPING_FILE" | ||
| 61 | + exit 1 | ||
| 62 | + fi | ||
| 63 | + curl -X PUT "${LOCAL_ES_HOST}/${DEST_INDEX}" \ | ||
| 64 | + -H "Content-Type: application/json" \ | ||
| 65 | + $AUTH_PARAM \ | ||
| 66 | + -d @"${MAPPING_FILE}" \ | ||
| 67 | + -w "\nHTTP: %{http_code}\n" -s | tail -1 | ||
| 68 | + echo "" | ||
| 69 | +else | ||
| 70 | + echo "目标索引已存在: $DEST_INDEX,将写入数据(可能覆盖同 id 文档)" | ||
| 71 | +fi | ||
| 72 | + | ||
| 73 | +# 2. Reindex from remote(JSON 中的密码用 env 传入,避免 shell 转义) | ||
| 74 | +echo "执行 Reindex from remote(最多 $MAX_DOCS 条)..." | ||
| 75 | +export REMOTE_ES_HOST REMOTE_ES_USER REMOTE_ES_PASS SOURCE_INDEX DEST_INDEX MAX_DOCS | ||
| 76 | +# ES 9.x 将 wait_for_completion 放在 query 参数,不在 body | ||
| 77 | +curl -X POST "${LOCAL_ES_HOST}/_reindex?wait_for_completion=true&pretty" \ | ||
| 78 | + -H "Content-Type: application/json" \ | ||
| 79 | + $AUTH_PARAM \ | ||
| 80 | + -d @- <<EOF | ||
| 81 | +{ | ||
| 82 | + "max_docs": ${MAX_DOCS}, | ||
| 83 | + "source": { | ||
| 84 | + "remote": { | ||
| 85 | + "host": "${REMOTE_ES_HOST}", | ||
| 86 | + "username": "${REMOTE_ES_USER}", | ||
| 87 | + "password": "${REMOTE_ES_PASS}" | ||
| 88 | + }, | ||
| 89 | + "index": "${SOURCE_INDEX}", | ||
| 90 | + "size": 500 | ||
| 91 | + }, | ||
| 92 | + "dest": { | ||
| 93 | + "index": "${DEST_INDEX}" | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | +EOF | ||
| 97 | + | ||
| 98 | +echo "" | ||
| 99 | +echo "完成。校验条数: curl $AUTH_PARAM '${LOCAL_ES_HOST}/${DEST_INDEX}/_count?pretty' -H 'Content-Type: application/json' -d '{\"query\":{\"match_all\":{}}}'" |