Commit d71e20f0d601548d303e334a6887a1e3e95807e3

Authored by tangwang
1 parent 5f7d7f09

索引同步,用于性能测试

docs/ES/Reindex_from_remote_注意事项.md 0 → 100644
@@ -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 === &#39;loading&#39;) { @@ -84,8 +84,8 @@ if (document.readyState === &#39;loading&#39;) {
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(&#39;load&#39;, function() { @@ -93,8 +93,8 @@ window.addEventListener(&#39;load&#39;, 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 // 初始化分面面板
scripts/reindex_from_remote_tenant_170_to_0.sh 0 → 100755
@@ -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\":{}}}'"