OpenSearch:安全插件、索引治理与兼容边界
一、是什么
从旧教程切换到可治理的搜索节点
最常见的失败并不是容器拉不下来,而是团队把 OpenSearch 当成“换了名字的 Elasticsearch”:继续使用 admin/admin,让应用持有管理员账号,把 demo 证书带进共享环境,再假设旧客户端和插件可以无条件兼容。OpenSearch 有自己的 Security plugin、OpenSearch Dashboards、Index State Management、快照机制和版本节奏,这些差异会直接影响启动、权限、迁移和恢复。
OpenSearch 的核心状态链是:文档先进入带 mapping 的物理 index,refresh 后才对搜索可见,alias 决定应用实际读写哪一代索引,ISM 决定 rollover 与删除状态,Snapshot repository 决定删除或故障后能否恢复。Security plugin 又在每个入口前校验身份、backend role 和 index 权限。沿这条链路观察,才能区分“REST API 返回 200”“文档已经可搜”“应用切到正确索引”和“数据真的可恢复”。
二、为什么
从搜索目标、安全边界与恢复责任判断是否适合
启动前先检查机器、端口和敏感边界
先在隔离的本地环境证明协议、账号、mapping 和清理链路。Docker Desktop 至少预留 4GB 内存;Linux 宿主确认 vm.max_map_count 不低于 262144。容器内仍使用 9200、9600 和 5601,下面把宿主端口映射为 9201、9601 和 5602,这样可以与本机 Elasticsearch 并存。真实 endpoint、管理员密码、证书私钥和 API key 不进入仓库、截图或终端录屏。
先确认 Docker、Compose、端口和 Linux 内核参数:
docker version
docker compose version
docker ps --format "table {{.Names}}\t{{.Ports}}\t{{.Status}}"Windows:
netstat -ano | findstr ":9201"
netstat -ano | findstr ":5602"Linux / macOS:
lsof -i :9201
lsof -i :5602
sysctl vm.max_map_count如果端口被占用,不要把本地开发脚本指向共享或生产 OpenSearch。先改宿主端口。
OpenSearch 的入口要按使用场景分开:
示例固定使用 3.8.0,这是官方版本历史当前列出的最新正式发行版;OpenSearch 与 Dashboards 的镜像版本必须一致。新项目应从 下载页 和 版本维护策略 选择团队批准版本,再把精确 tag 或 digest 写入锁定文件;不要让 latest 在不同成员机器上解析成不同构建。OpenSearch 2.12 及以上的新安装还必须提供强 OPENSEARCH_INITIAL_ADMIN_PASSWORD,这项要求也适用于 3.x 的 demo security 初始化。
用工作负载排除错误选型
OpenSearch 适合全文检索、相关性排序、日志与事件分析、聚合探索,以及需要近实时索引和水平扩展的搜索入口。它不应替代订单、账户或库存的事务事实库;refresh 之前暂时搜不到刚写入文档是近实时模型的一部分,不是把主库读写语义原样搬过来。若数据量小、查询主要是结构化过滤和关联,先验证关系数据库索引是否足够;若团队依赖 Elasticsearch 专属客户端、插件、API 或 saved objects,则应先做兼容清单,而不是把产品名替换成 OpenSearch。
比较部署入口而不是只比较启动方式
| 入口 | 适合 | 不适合 | 关键边界 |
|---|---|---|---|
| Docker 单容器 | 个人最小验证、快速复现安全插件和 REST API | 团队长期模板、Dashboard 联调 | 自定义 admin 密码、demo 证书、清理命令 |
| Docker Compose | 项目联调、新人一键启动、Dashboards 验证 | 生产集群 | 版本、端口、volume、security demo、healthcheck、清理脚本 |
| 共享开发实例 | 多服务联调、重型组件集中运行 | 个人随意试错、破坏性测试 | owner、角色权限、index prefix、证书轮换、清理窗口 |
| 自建多节点 | 核心业务、强自控、客户现场 | 无 SRE / DBA owner 的小团队 | 节点角色、shard、snapshot、滚动升级、磁盘和告警 |
| 托管 OpenSearch | 快速生产化、减少自建运维 | 数据边界和成本未确认场景 | 账号权限、网络、版本、插件、备份、退出方案 |
| Kubernetes Operator | 成熟 K8S 平台和声明式管理 | 只为本地起服务 | CRD、PVC、PDB、节点亲和、证书和升级 |
三、怎么做
固定安全插件、节点与开发环境
用 Docker 单容器证明安全入口
单容器用于个人验证协议、账号和 index 行为:
docker network create te-opensearch-dev
docker run -d \
--name te-os-single \
--net te-opensearch-dev \
-p 127.0.0.1:9201:9200 \
-p 127.0.0.1:9601:9600 \
-e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=YOUR_STRONG_ADMIN_PASSWORD" \
-e "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" \
opensearchproject/opensearch:3.8.0验证:
export OPENSEARCH_ADMIN_PASSWORD="YOUR_STRONG_ADMIN_PASSWORD"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
https://127.0.0.1:9201-k 只用于本地 demo 证书快速验证。团队模板要收敛为受控 CA 或证书分发,不把跳过 TLS 校验写成默认客户端配置。
清理:
docker rm -f te-os-single
docker network rm te-opensearch-dev用 Docker Compose 固定项目拓扑
项目模板建议放到明确目录:
dev-dependencies/
opensearch/
compose.yaml
.env.example
index/article-v1.json
scripts/verify.sh
scripts/init-index.sh
scripts/clean-index-dry-run.sh
scripts/clean-index-confirmed.sh.env.example:
OPENSEARCH_VERSION=3.8.0
OPENSEARCH_INITIAL_ADMIN_PASSWORD=YOUR_STRONG_ADMIN_PASSWORD
OPENSEARCH_HTTP_PORT=9201
OPENSEARCH_DASHBOARDS_PORT=5602
OPENSEARCH_INDEX_PREFIX=your_project_dev
OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512mcompose.yaml:
services:
opensearch:
image: opensearchproject/opensearch:${OPENSEARCH_VERSION}
container_name: your-project-opensearch
environment:
- cluster.name=your-project-opensearch
- node.name=your-project-opensearch-1
- discovery.type=single-node
- bootstrap.memory_lock=true
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
- OPENSEARCH_JAVA_OPTS=${OPENSEARCH_JAVA_OPTS}
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- opensearch-data:/usr/share/opensearch/data
ports:
- "127.0.0.1:${OPENSEARCH_HTTP_PORT:-9201}:9200"
- "127.0.0.1:9601:9600"
healthcheck:
test:
[
"CMD-SHELL",
"curl --silent --fail -k -u admin:$$OPENSEARCH_INITIAL_ADMIN_PASSWORD https://127.0.0.1:9200 >/dev/null || exit 1"
]
interval: 15s
timeout: 10s
retries: 30
dashboards:
image: opensearchproject/opensearch-dashboards:${OPENSEARCH_VERSION}
container_name: your-project-opensearch-dashboards
depends_on:
opensearch:
condition: service_healthy
environment:
- OPENSEARCH_HOSTS=["https://opensearch:9200"]
ports:
- "127.0.0.1:${OPENSEARCH_DASHBOARDS_PORT:-5602}:5601"
profiles:
- ui
volumes:
opensearch-data:启动:
docker compose --env-file dev-dependencies/opensearch/.env \
-f dev-dependencies/opensearch/compose.yaml \
up -d opensearch启动 Dashboards:
docker compose --env-file dev-dependencies/opensearch/.env \
-f dev-dependencies/opensearch/compose.yaml \
--profile ui \
up -d dashboards几个边界必须写清:
端口只绑定 127.0.0.1。demo 证书和 -k 只服务本地验证。OPENSEARCH_INITIAL_ADMIN_PASSWORD 需要强密码,真实 .env 不提交。
OPENSEARCH_JAVA_OPTS 是开发验证入口,生产资源规划要单独评审。Dashboards 登录使用本地 admin 密码;共享环境不能全员共享 admin。
给共享开发实例建立 owner 和隔离边界
共享开发实例不能照搬个人 Compose。它至少要确认:
owner 是谁,谁能重启、升级、清理 index、轮换证书和密码。TLS 证书来源、有效期、分发位置和轮换窗口。每个项目是否只能访问自己的 index prefix。
Dashboards 是否默认只读,Dev Tools 是否受控。清理脚本是否先 dry-run,再显式确认 prefix。snapshot repository、保留周期和恢复演练是否有人负责。
OpenSearch 开发配置要管住这些对象:
| 配置对象 | 建议 | 原因 |
|---|---|---|
| 版本 | OPENSEARCH_VERSION=3.8.0 或团队批准版本 | 避免镜像漂移 |
| 端口 | 宿主绑定 127.0.0.1:9201 | 防止局域网暴露 |
| 管理员密码 | .env 或 secret,占位值不提交 | 2.12+ 新安装必需 |
| TLS / 证书 | 本地 demo 可 -k;共享环境要受控 CA | 避免把跳过校验写成默认 |
| 数据卷 | named volume | 可 inspect、可安全重置 |
| index prefix | your_project_dev | 支撑权限和清理 |
| Dashboards | profile 启动,账号分层 | UI 不是每次都需要 |
用角色证明允许与拒绝动作
本地单人可以临时用 admin 初始化,但项目模板必须分层:
| 账号 | 用途 | 权限边界 |
|---|---|---|
admin | 本地初始化、紧急救援 | 不给普通开发长期使用 |
| setup 账号 | 创建 index、mapping、template、ISM policy | 只管理项目 prefix |
| app 账号 | 应用写读 | 只写读项目 prefix |
| readonly 账号 | Dashboards / GUI / 排障 | 只读项目 prefix |
| CI 账号 | 初始化、冒烟、清理 | 明确脚本和审计范围 |
权限靠角色拦截,不靠“大家自觉别乱删”。
共享环境至少要做三类低权限验收:
setup 账号能创建 your_project_dev_* 的 template、index、alias 和 ISM policy,但不能操作其他项目前缀。app 账号能对 your_project_dev_* 写入、查询和 bulk,但不能改 cluster settings、snapshot repository、security index。readonly 账号只能查询和进入 Dashboards 只读视图,执行写入、删除、改 mapping、Dev Tools 高危 API 时必须返回 403。
下面只创建角色并映射已经由批准身份源提供的用户,不在命令里创建或保存明文密码。执行 Security REST API 的管理员身份必须被显式允许访问相应 endpoint:
curl --fail -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/_plugins/_security/api/roles/your_project_search_app" \
-H "Content-Type: application/json" \
-d '{
"cluster_permissions": ["cluster_composite_ops"],
"index_permissions": [{
"index_patterns": ["your_project_dev_*"],
"allowed_actions": ["read", "write"]
}]
}'
curl --fail -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/_plugins/_security/api/roles/your_project_search_readonly" \
-H "Content-Type: application/json" \
-d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [{
"index_patterns": ["your_project_dev_*"],
"allowed_actions": ["read"]
}]
}'
curl --fail -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/_plugins/_security/api/rolesmapping/your_project_search_app" \
-H "Content-Type: application/json" \
-d '{"users":["your_project_dev_search_app"],"backend_roles":[],"hosts":[]}'
curl --fail -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/_plugins/_security/api/rolesmapping/your_project_search_readonly" \
-H "Content-Type: application/json" \
-d '{"users":["your_project_dev_search_readonly"],"backend_roles":[],"hosts":[]}'read 和 write 是 Security plugin 的 action group,不是 HTTP 方法白名单。配置后分别用两个低权限身份调用 /_plugins/_security/authinfo,确认实际映射角色;app 对项目 alias 的 bulk 和 search 应成功,但修改 /_cluster/settings 必须返回 403;readonly 的 search 应成功,写入一条 req-forbidden 文档必须返回 403。还要验证两个账号都不能访问其他前缀或 .opendistro_security。权限运行证据要留下账号类型、index pattern、允许动作、拒绝动作、返回码、测试时间和负责人,不能只写“已配置”。
用 index 前缀固定环境身份
建议:
<team>_<project>_<env>_<domain>_vN例如:
arch_blog_dev_article_v1
arch_blog_test_article_v2共享环境里短名字会让权限、清理和恢复都变得危险。
把配置、插件和升级拆成不同变更
配置文件、集群设置、索引设置和插件不是同一种变更。opensearch.yml、JVM 参数、证书路径和节点角色通常随进程启动读取,修改后要滚动重启;Cluster Settings API 写入的 persistent 设置保存在 cluster state,重启后仍然生效,临时排障结束后要显式撤销;refresh interval、replica 等索引设置可以动态调整,但 mapping 中已经存在的字段类型不能原地改写,只能新建 index、reindex 并切 alias。每次变更前先保存当前值,验收后再删除临时设置,避免一次排障永久改变集群行为。
标准镜像已经带有 Security、Index Management、SQL 等插件。先记录每个节点实际加载的插件,不能只看一台机器:
docker exec your-project-opensearch \
/usr/share/opensearch/bin/opensearch-plugin list
: "${OPENSEARCH_URL:?missing OPENSEARCH_URL}"
: "${OPENSEARCH_ADMIN_PASSWORD:?missing OPENSEARCH_ADMIN_PASSWORD}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/_cat/plugins?v"容器里的额外插件应在派生镜像构建阶段安装,不要进入运行中的容器手工修改。OpenSearch 官方核心插件的版本必须与发行版版本匹配,第三方插件还要核对其 descriptor 声明、来源、许可和所请求的 JVM 权限;所有节点必须得到同一插件集合,安装或移除后需要重启。可把强依赖插件写入静态 plugin.mandatory,让缺少插件的节点拒绝启动,而不是带着不完整能力加入集群。
ARG OPENSEARCH_VERSION=3.8.0
FROM opensearchproject/opensearch:${OPENSEARCH_VERSION}
RUN /usr/share/opensearch/bin/opensearch-plugin install --batch analysis-icu升级时先用新 tag 构建并扫描派生镜像,在隔离集群验证插件列表、索引打开、snapshot 恢复、Security role、ISM 和查询回归,再逐节点滚动。回退不是把旧 tag 直接指向已经被新版本写过的数据目录;应保留旧镜像、旧配置、插件清单和升级前 snapshot,失败时停止继续滚动,并按已演练路径恢复到隔离的旧版本集群后切回流量。
让 SEARCH_DEMO 贯穿 mapping、写入与检索
最小验证必须完成:连接、建 index、写入、查询、Dashboards、清理。
设置变量:
export OPENSEARCH_URL="https://127.0.0.1:9201"
export OPENSEARCH_ADMIN_PASSWORD="YOUR_STRONG_ADMIN_PASSWORD"
export OPENSEARCH_INDEX="your_project_dev_article_v1"
export OPENSEARCH_ALIAS="your_project_dev_article"
export SEARCH_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"先确认产品身份和精确版本
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}"预期看到 cluster_name、version.number 和 OpenSearch tagline。这里失败时先看协议、密码、端口、容器健康和旧 volume,不要直接改应用代码。
创建严格 mapping 和第一代物理 index
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/${OPENSEARCH_INDEX}" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"dynamic": "strict",
"properties": {
"id": { "type": "keyword" },
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"tags": { "type": "keyword" },
"updated_at": { "type": "date" }
}
}
}'
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/_aliases" \
-H "Content-Type: application/json" \
-d "{\"actions\":[{\"add\":{\"index\":\"${OPENSEARCH_INDEX}\",\"alias\":\"${OPENSEARCH_ALIAS}\",\"is_write_index\":true}}]}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/_alias/${OPENSEARCH_ALIAS}?pretty"物理 index 表达 schema 代际,alias 表达应用入口。回读结果中必须只有 your_project_dev_article_v1,且 is_write_index=true;应用从此只使用 alias,后面的升级和回滚才不需要改连接配置。
用已知文档证明 refresh 与字段语义
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/${OPENSEARCH_ALIAS}/_bulk?refresh=wait_for" \
-H "Content-Type: application/x-ndjson" \
--data-binary "
{\"index\":{\"_id\":\"article-001\"}}
{\"id\":\"article-001\",\"title\":\"OpenSearch 开发依赖验证\",\"tags\":[\"tool-efficiency\",\"search\"],\"updated_at\":\"${SEARCH_TIMESTAMP}\"}
{\"index\":{\"_id\":\"article-002\"}}
{\"id\":\"article-002\",\"title\":\"OpenSearch 安全插件手册\",\"tags\":[\"security\",\"search\"],\"updated_at\":\"${SEARCH_TIMESTAMP}\"}
{\"index\":{\"_id\":\"article-003\"}}
{\"id\":\"article-003\",\"title\":\"数据库备份恢复演练\",\"tags\":[\"database\",\"recovery\"],\"updated_at\":\"${SEARCH_TIMESTAMP}\"}
"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/${OPENSEARCH_ALIAS}/_count?pretty" \
-H "Content-Type: application/json" \
-d '{"query":{"term":{"tags":"search"}}}'Bulk 响应必须满足 errors=false,而不是只看 HTTP 200;每个 item 的状态也要进入失败日志。因为请求显式使用 refresh=wait_for,后面的精确计数应立即得到 count=2。去掉这个参数后,写入已被 primary 接受却暂时搜不到属于近实时可见性窗口,不应通过循环重复写入来“修复”。
接着用一组正反实验确认字段设计真的生效。title 的 text 主字段用于分词检索,title.keyword 用于精确聚合;tags 是 keyword,不会把 tool-efficiency 拆成多个 token;updated_at 是 date,可以执行范围查询。
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/${OPENSEARCH_ALIAS}/_search?pretty" \
-H "Content-Type: application/json" \
-d '{
"size": 0,
"aggs": {
"titles": { "terms": { "field": "title.keyword" } },
"tags": { "terms": { "field": "tags" } }
}
}'预期 titles 有三个 bucket,tags 中 search 的 doc_count=2,其他四个标签各为 1。若把 title.keyword 改为 title,请求通常会因 text 字段默认未启用 fielddata 而失败,这正是 mapping 对排序和聚合能力的直接影响。
再故意写入一个 mapping 中不存在的字段:
curl -sS -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/${OPENSEARCH_ALIAS}/_doc/bad?refresh=wait_for" \
-H "Content-Type: application/json" \
-o /tmp/opensearch-negative-test.json \
-w "HTTP %{http_code}\n" \
-d "{
\"id\": \"bad\",
\"title\": \"不应写入\",
\"tags\": [\"negative-test\"],
\"updated_at\": \"${SEARCH_TIMESTAMP}\",
\"unexpected_field\": true
}"
cat /tmp/opensearch-negative-test.json预期 HTTP 400,错误中出现 strict_dynamic_mapping_exception。这个失败证明 dynamic: strict 能在入口处阻止脏字段扩散;如果返回 201,应立即检查目标 index 和实际 mapping,不能继续执行后续初始化。
用 Dashboards 复核同一索引身份
启动 Dashboards 后访问:
http://127.0.0.1:5602至少确认:
Dashboards 能连接 OpenSearch。用 data view 指向 ${OPENSEARCH_ALIAS} 后,三条文档、search=2 聚合和 Dev Tools 返回值必须与 curl 一致。页面截图和终端录屏不包含真实生产地址和密码。若 Dashboard 指向物理 index,alias 切换后它会继续展示旧数据,这不是 UI 缓存问题,而是索引身份错误。
只清理当前实验 index
echo "will delete index: ${OPENSEARCH_INDEX}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/_alias/${OPENSEARCH_ALIAS}?pretty"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X DELETE "${OPENSEARCH_URL}/${OPENSEARCH_INDEX}"
curl -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/${OPENSEARCH_INDEX}"再次查询应返回 404。共享实例不能靠手工 Dev Tools 删除,必须走带 prefix 保护的脚本。
完成项目接入与可逆脚本
.env.example:
OPENSEARCH_URL=https://127.0.0.1:9201
OPENSEARCH_USERNAME=your_project_dev_search_app
OPENSEARCH_PASSWORD=YOUR_OPENSEARCH_APP_PASSWORD
OPENSEARCH_INDEX_PREFIX=your_project_dev
OPENSEARCH_CA_CERT=dev-dependencies/opensearch/certs/root-ca.pem
OPENSEARCH_SKIP_TLS_VERIFY=false本地 demo 可临时 -k,但应用默认配置不能长期跳过 TLS 校验。共享环境要由 owner 分发 CA 或配置正式证书。
固化初始化脚本
#!/usr/bin/env bash
set -euo pipefail
: "${OPENSEARCH_URL:?missing OPENSEARCH_URL}"
: "${OPENSEARCH_USERNAME:?missing OPENSEARCH_USERNAME}"
: "${OPENSEARCH_PASSWORD:?missing OPENSEARCH_PASSWORD}"
: "${OPENSEARCH_INDEX:?missing OPENSEARCH_INDEX}"
curl --fail -k \
-u "${OPENSEARCH_USERNAME}:${OPENSEARCH_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/${OPENSEARCH_INDEX}" \
-H "Content-Type: application/json" \
-d @dev-dependencies/opensearch/index/article-v1.json如果脚本需要管理 index、template 或 ISM policy,用 setup 账号;运行时 app 账号只写读业务文档。
固化先预演再确认的清理脚本
dry-run:
#!/usr/bin/env bash
set -euo pipefail
: "${OPENSEARCH_URL:?missing OPENSEARCH_URL}"
: "${OPENSEARCH_USERNAME:?missing OPENSEARCH_USERNAME}"
: "${OPENSEARCH_PASSWORD:?missing OPENSEARCH_PASSWORD}"
: "${OPENSEARCH_INDEX_PREFIX:?missing OPENSEARCH_INDEX_PREFIX}"
echo "target prefix: ${OPENSEARCH_INDEX_PREFIX}_"
curl --fail -k \
-u "${OPENSEARCH_USERNAME}:${OPENSEARCH_PASSWORD}" \
"${OPENSEARCH_URL}/_cat/indices/${OPENSEARCH_INDEX_PREFIX}_*?h=index"确认删除:
#!/usr/bin/env bash
set -euo pipefail
: "${OPENSEARCH_URL:?missing OPENSEARCH_URL}"
: "${OPENSEARCH_USERNAME:?missing OPENSEARCH_USERNAME}"
: "${OPENSEARCH_PASSWORD:?missing OPENSEARCH_PASSWORD}"
: "${OPENSEARCH_INDEX_PREFIX:?missing OPENSEARCH_INDEX_PREFIX}"
: "${CONFIRM_DELETE:?missing CONFIRM_DELETE}"
if [[ "${CONFIRM_DELETE}" != "${OPENSEARCH_INDEX_PREFIX}" ]]; then
echo "refuse to delete: CONFIRM_DELETE must equal ${OPENSEARCH_INDEX_PREFIX}" >&2
exit 1
fi
if [[ "${OPENSEARCH_INDEX_PREFIX}" == "*" || "${OPENSEARCH_INDEX_PREFIX}" == "_all" || ${#OPENSEARCH_INDEX_PREFIX} -lt 6 ]]; then
echo "refuse to delete: unsafe prefix ${OPENSEARCH_INDEX_PREFIX}" >&2
exit 1
fi
curl --fail -k \
-u "${OPENSEARCH_USERNAME}:${OPENSEARCH_PASSWORD}" \
-X DELETE "${OPENSEARCH_URL}/${OPENSEARCH_INDEX_PREFIX}_*"从搜索目标推导 OpenSearch 的业务角色
OpenSearch 解决的是搜索和分析入口问题,不是关系数据库替代品。
| 诉求 | OpenSearch 提供 | 架构代价 |
|---|---|---|
| 全文搜索 | inverted index、scoring、query DSL | mapping 和 analyzer 要提前设计 |
| 近实时查询 | refresh 后可搜 | 不是写后强一致主库 |
| 横向扩展 | shard / replica / node | shard 规划和恢复窗口要治理 |
| 可视化探索 | OpenSearch Dashboards | Dashboard 权限和 Dev Tools 风险 |
| 生命周期治理 | ISM、rollover、delete、snapshot | 策略错误会过早删除或堆积数据 |
| 开源许可 | Apache 2.0 | 仍需看托管、商标、客户交付和合规 |
架构师要回答:它是开发依赖还是业务搜索主链路;数据能否从主库重建;查询是轻搜索、重聚合、日志分析还是向量检索;index 生命周期、snapshot、清理和恢复责任人是谁;是否真的需要 OpenSearch,还是数据库索引、Elasticsearch 或托管服务更合适。
从单节点演进到生产故障域
| 架构 | 适合 | 风险 | 验收重点 |
|---|---|---|---|
| 单节点开发 | 本地、CI、mapping 验证 | 无高可用,replica 会 yellow | 连接、index、清理 |
| 三节点基础集群 | 中小生产搜索 | shard 规划和恢复仍需治理 | health、replica、snapshot |
| 角色分离集群 | 写入、查询、Dashboard 压力变大 | 网络、证书、升级复杂 | cluster manager、data、ingest、coordinating |
| ISM 数据分层 | 日志、审计、事件 | 策略误删或堆积 | rollover、delete、snapshot |
| 托管服务 | 快速生产化 | 成本、插件、网络和版本边界 | 账号、SLA、退出方案 |
角色和节点决定控制面与数据面
OpenSearch cluster 由多个 node 组成。cluster manager 负责 cluster state、选主和 shard 分配;data node 执行存储和查询;ingest node 处理 pipeline;coordinating node 汇总查询结果。节点不是越多越好,节点数会增加通信、分配、证书和升级复杂度。
shard、replica 和 segment 决定恢复代价
index 被拆成 primary shard,replica 提供冗余和读扩展。shard 底层由 Lucene segment 组成。shard 太大恢复慢,shard 太多拖累 cluster state、heap、文件句柄和调度。replica 提升可用性,也增加写入复制和存储成本。
refresh、translog 和近实时决定可见性
OpenSearch 是近实时搜索。refresh=wait_for 适合开发验证,不是所有业务写入默认策略。业务如果要求写后强一致读,应先读主库或做业务补偿。
JVM、文件缓存和磁盘水位决定稳定性
heap 太大挤压文件缓存,太小会 GC 频繁或触发 circuit breaker。磁盘达到水位会影响 shard 分配,严重时触发写入 block。复制 data 目录不是可靠备份。
从 SEARCH_DEMO 追踪索引、生命周期与恢复状态
索引、mapping、template、alias 与 ISM 组成发布链
index 是搜索契约,不只是表名。
团队规则:
核心字段必须显式 mapping。动态字段只用于非核心实验。业务读写走 alias,不写死物理 index。
mapping 变更通过 v2 index、reindex、验证和 alias 切换完成。ISM policy 进入共享环境前必须评审删除和 rollover 条件。
alias 切换前同时记录旧 index、目标 index 和校验结果。切换失败就保持旧 alias;切换后出现查询或字段回归,则把 alias 原子切回旧 index。旧 index 至少保留到回滚窗口结束,不能在切换成功的同一任务里立即删除。
假设审核后的 article-v2.json 新增了业务字段或 analyzer,先创建空的第二代 index,再复制同一组 SEARCH_DEMO 文档:
export OPENSEARCH_INDEX_V2="your_project_dev_article_v2"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/${OPENSEARCH_INDEX_V2}" \
-H "Content-Type: application/json" \
-d @dev-dependencies/opensearch/index/article-v2.json
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/_reindex?refresh=true&wait_for_completion=true" \
-H "Content-Type: application/json" \
-d "{\"source\":{\"index\":\"${OPENSEARCH_INDEX}\"},\"dest\":{\"index\":\"${OPENSEARCH_INDEX_V2}\"}}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/${OPENSEARCH_INDEX_V2}/_count?pretty" \
-H "Content-Type: application/json" \
-d '{"query":{"term":{"tags":"search"}}}'_reindex 的 HTTP 成功不代表逐文档成功。必须检查 failures 为空、created=3,再确认总文档为 3、tags=search 为 2、三个固定 _id 都存在,新字段查询也满足预期。验证通过后,把删除旧 alias 和添加新 alias 放在同一个 _aliases 请求中:
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/_aliases" \
-H "Content-Type: application/json" \
-d "{\"actions\":[
{\"remove\":{\"index\":\"${OPENSEARCH_INDEX}\",\"alias\":\"${OPENSEARCH_ALIAS}\"}},
{\"add\":{\"index\":\"${OPENSEARCH_INDEX_V2}\",\"alias\":\"${OPENSEARCH_ALIAS}\",\"is_write_index\":true}}
]}"这个请求在集群状态中原子生效。切换后立刻通过 alias 重跑三文档和两条 search 标签断言;若业务回归失败,交换上面两个 index 再执行一次即可回切。不要先删 alias 再单独添加,也不要在观察窗口结束前删除 v1。
版本化业务索引与 ISM rollover 不是同一条发布路线。your_project_dev_article_v1 适合人工审核后的 schema 代际;持续追加的日志或事件流应另用 your_project_dev_event-000001 这类递增序号,设置 rollover alias 和 ISM policy。OpenSearch 的 rollover 要求索引名以数字结尾,当前索引又必须是 alias 的 write index;把 _v1 生硬挂到 rollover policy 上会得到校验失败。
ISM 也不是“保存后立刻执行”的定时器。策略按 index age、size 或文档数判断状态迁移,后台任务按间隔检查;达到阈值与真正 rollover 之间存在时间差。3.7 起可以先用 Simulate API 预演 policy 对目标索引的当前状态、下一动作和迁移条件,不修改索引:
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/_plugins/_ism/simulate?pretty" \
-H "Content-Type: application/json" \
-d '{
"policy_id": "your_project_event_retention",
"indices": ["your_project_dev_event-000001"]
}'预演要核对当前 state、下一 action、每个 transition condition 和目标 state;没有删除动作也不等于安全,因为 rollover 失败会让单个索引无限增长。正式附着后,再用 Explain API 证明策略确实管理预期 index:
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/_plugins/_ism/explain/your_project_dev_event-000001?show_policy=true&validate_action=true&pretty"证据要同时包含 policy_id、当前 state、action、失败信息和目标 rollover alias。状态为 red 时 ISM job 不运行;alias 不是 write index、索引名不符合递增序号规则或策略权限不足,也会让 rollover 停住。删除状态进入共享环境前,先在临时前缀缩短阈值演练,并确认快照成功早于删除动作。
查询、写入与节点指标解释运行代价
查询慢先看 query 是否触发大分页、宽聚合、高亮或脚本;mapping 是否让 text 字段参与聚合;shard 是否热点;heap、breaker、thread pool 是否高压;是否处于 snapshot、merge 或 bulk 写入窗口。
写入慢先看 bulk 批大小和并发、refresh、replica、merge、磁盘 IO、routing 热点和客户端 item 级失败。
诊断入口:
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cluster/health?pretty"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/nodes?v"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/shards?v"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_nodes/stats/jvm,indices,thread_pool,fs?pretty"用隔离恢复证明快照可用
| 状态 | 含义 | 开发环境判断 | 生产环境判断 |
|---|---|---|---|
| green | primary 和 replica 都分配 | 正常 | 正常但仍看资源 |
| yellow | primary 分配,replica 未分配 | 单节点常见 | 查副本、节点、分配规则 |
| red | primary 未分配 | 不应忽略 | 优先恢复数据或 snapshot |
Snapshot 才是备份。它从 primary shard 复制数据,进行中的新写入不一定属于同一个一致时间点;快照又是增量的,多个快照可能共享底层数据,所以删除仓库文件会破坏其他快照。创建、列出和删除都应走 Snapshot API。
OpenSearch 3.8 还改变了 S3 repository 的默认服务端加密类型:未显式配置时会按 AES256 发送加密请求头。AWS S3 策略可能因此更符合预期,但某些未配置 KMS 的 S3 兼容实现会拒绝请求。升级前要在 repository 注册或更新时明确评审 server_side_encryption_type,并执行 repository verify;不能因为旧版本快照成功,就假设 3.8 升级后仍能写入同一后端。
在隔离集群准备好 repository 后,可以用“拍快照、改名恢复、校验文档”的闭环证明恢复能力。OPENSEARCH_SNAPSHOT_REPOSITORY 必须是 owner 已注册并授权的仓库,不能临时把生产仓库挂到开发机:
export OPENSEARCH_SNAPSHOT_REPOSITORY="your_project_dev_repo"
export OPENSEARCH_SNAPSHOT="article-v1-smoke"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/${OPENSEARCH_INDEX}/_count?pretty"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/${OPENSEARCH_INDEX}/_count?pretty" \
-H "Content-Type: application/json" \
-d '{"query":{"term":{"tags":"search"}}}'
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X PUT "${OPENSEARCH_URL}/_snapshot/${OPENSEARCH_SNAPSHOT_REPOSITORY}/${OPENSEARCH_SNAPSHOT}?wait_for_completion=true" \
-H "Content-Type: application/json" \
-d "{\"indices\":\"${OPENSEARCH_INDEX}\",\"include_global_state\":false,\"partial\":false}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/_snapshot/${OPENSEARCH_SNAPSHOT_REPOSITORY}/${OPENSEARCH_SNAPSHOT}?pretty"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X POST "${OPENSEARCH_URL}/_snapshot/${OPENSEARCH_SNAPSHOT_REPOSITORY}/${OPENSEARCH_SNAPSHOT}/_restore?wait_for_completion=true" \
-H "Content-Type: application/json" \
-d "{\"indices\":\"${OPENSEARCH_INDEX}\",\"include_global_state\":false,\"include_aliases\":false,\"rename_pattern\":\"(.+)\",\"rename_replacement\":\"restored-\$1\"}"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/restored-${OPENSEARCH_INDEX}/_count?pretty"
curl --fail -k \
-u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
-X GET "${OPENSEARCH_URL}/restored-${OPENSEARCH_INDEX}/_count?pretty" \
-H "Content-Type: application/json" \
-d '{"query":{"term":{"tags":"search"}}}'源索引的基准应是总文档 3、tags=search 文档 2,并保存三个固定 _id 的 _source 摘要。快照状态必须是 SUCCESS,恢复后重复得到 3 和 2,三个 _id、mapping 与关键字段也必须一致,才算数据面恢复成功。然后单独重建并检查 alias、template、Security role 和 ISM policy,因为 include_global_state: false 不会替你恢复这些控制面对象。最后用 readonly 身份查询成功、写入返回 403,记录快照与恢复耗时;确认完成后只删除 restored-* 测试索引,快照保留与删除仍由仓库策略处理。
容量评审至少拆成四笔账:primary 数据量、replica 倍数、merge 与水位预留、快照仓库。粗略起点可以写成“可用数据盘 > primary 预计峰值 x (1 + replica 数) / 目标最高利用率”,再用真实压缩率、segment merge 峰值和恢复窗口修正。提高 replica 会增加存储与写入复制成本;减少 shard 能降低 heap 和文件句柄开销,却会放大单 shard 的恢复时间;托管服务还要把跨区流量、快照、热温节点、专用 cluster manager 和保留周期计入月度成本。没有采样数据时,不要拿单一经验值直接定生产 shard 数。
docker compose --env-file dev-dependencies/opensearch/.env \
-f dev-dependencies/opensearch/compose.yaml ps
docker compose --env-file dev-dependencies/opensearch/.env \
-f dev-dependencies/opensearch/compose.yaml logs --tail=120 opensearch
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cluster/health?pretty"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/nodes?v"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/indices?v"重置个人本地:
docker compose --env-file dev-dependencies/opensearch/.env \
-f dev-dependencies/opensearch/compose.yaml down -v共享环境不能这么做。共享环境清理由 owner 走受控脚本、备份恢复或变更窗口。
四、问题处理
新版容器不接受 admin/admin
现象:容器启动失败或 Dashboards 登录失败。
判断:
docker logs your-project-opensearch --tail=120原因:OpenSearch 2.12+ 新安装要求设置 OPENSEARCH_INITIAL_ADMIN_PASSWORD。
修复:在 .env 设置强密码;个人本地可删除旧 volume 重建;共享环境按 owner 的密码轮换流程处理。
再验证:
curl --fail -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" https://127.0.0.1:9201HTTP / HTTPS 误判
现象:curl http://127.0.0.1:9201 失败。
判断:
curl -v http://127.0.0.1:9201
curl -vk -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" https://127.0.0.1:9201原因:Security demo 默认 HTTPS。修复:开发验证用 https:// 和 -k,共享环境用受控 CA。
Dashboards 连不上 OpenSearch
判断:
docker logs your-project-opensearch-dashboards --tail=120
docker logs your-project-opensearch --tail=120常见原因:OPENSEARCH_HOSTS 协议不对、OpenSearch 未健康、密码或证书不匹配、Dashboards 和 OpenSearch 版本不一致。修复后打开 http://127.0.0.1:5602 验证。
旧 volume 让新密码不生效
现象:改了 .env 的 OPENSEARCH_INITIAL_ADMIN_PASSWORD,仍然登录失败。
判断:
docker volume ls
docker volume inspect your-project_opensearch-data原因:安全配置已经写入数据目录。个人环境可以 down -v,共享环境必须走密码轮换或 securityadmin / API 流程。
mapping 被脏数据污染
判断:
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" \
"${OPENSEARCH_URL}/${OPENSEARCH_INDEX}/_mapping?pretty"修复:开发环境删除 index 重建;共享环境新建 v2 index、reindex、验证、切 alias。
集群 yellow 或 red
判断:
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cluster/health?pretty"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/shards?v"开发单节点可设 number_of_replicas: 0;生产先查分配原因,再决定扩容、恢复节点或从 snapshot 恢复。
bulk 写入出现 429
判断:
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_cat/thread_pool/write?v"
curl -k -u "admin:${OPENSEARCH_ADMIN_PASSWORD}" "${OPENSEARCH_URL}/_nodes/stats/jvm,indices,thread_pool,fs?pretty"修复:降低 bulk 批大小和并发,增加退避重试,记录 item 级失败,必要时调整 refresh 策略。
误连生产
判断:脚本是否打印 URL、cluster name、index prefix;账号是否默认只读;删除脚本是否需要 CONFIRM_DELETE;Dashboards 连接名是否包含环境和权限。
治理:生产连接不进入开发机默认 .env;生产普通账号默认只读;危险操作走受控脚本和变更流程。
固化团队治理与交付清单
OpenSearch 镜像拉取依赖 Docker Hub 或 Amazon ECR。企业网络里要确认代理、镜像仓库和证书:
docker pull opensearchproject/opensearch:3.8.0
docker pull opensearchproject/opensearch-dashboards:3.8.0NO_PROXY 至少包含本机:
NO_PROXY=127.0.0.1,localhost凭证规则:
.env.example 只放占位值,.env 不提交。admin 密码、API key、证书私钥、生产 CA 不进仓库。Dashboards 截图、终端录屏和日志粘贴要脱敏。
发现泄漏后轮换,不只是在 Git 里删除。生产连接默认只读,写入和删除走受控任务。
敏感信息自查:
rg -n "OPENSEARCH_INITIAL_ADMIN_PASSWORD|Authorization:|ApiKey|BEGIN .*PRIVATE KEY|admin:" .命中变量名不一定是泄漏,命中真实值、私钥、生产域名和未脱敏连接串才是风险。
项目模板必须保留:
dev-dependencies/opensearch/compose.yaml
dev-dependencies/opensearch/.env.example
dev-dependencies/opensearch/index/
dev-dependencies/opensearch/scripts/init-index.sh
dev-dependencies/opensearch/scripts/clean-index-dry-run.sh
dev-dependencies/opensearch/scripts/clean-index-confirmed.sh
docs/dependency-setup.md| 角色 | 负责 |
|---|---|
| 搜索 owner | 版本、证书、账号、snapshot、容量和升级 |
| 项目开发 | mapping、初始化脚本、最小验证和清理脚本 |
| 测试 | 共享环境数据准备和回归 |
| 运维 / 平台 | 生产部署、监控、告警、恢复、滚动升级 |
| 安全 / 合规 | 凭证、审计、客户交付和许可边界 |
清理策略:每个项目只能清理自己的 prefix;清理前必须 dry-run;删除必须要求 CONFIRM_DELETE 等于 prefix;共享环境清理记录到变更日志。
用安全、生命周期与恢复门禁预防复发
默认密码变化会拖垮新人接入
旧文章常写 admin/admin,OpenSearch 2.12 及以上的新安装要求 OPENSEARCH_INITIAL_ADMIN_PASSWORD。模板必须锁定实际版本和镜像 digest,.env.example 只保留强密码占位。
Security plugin 不是摆设
为了省事禁用安全插件或全员共享 admin,会让 Dashboards Dev Tools 拥有写入、删除、改 mapping 的能力。本地 demo 可以简化,团队共享环境必须按 role / index prefix 分权。
-k 只能定位问题
本地 demo 使用 -k 可以接受;项目模板要预留 CA 路径,生产和共享环境必须受控证书。
OpenSearch 不是 Elasticsearch 的透明替代
迁移前做 API、客户端、插件、mapping、snapshot、Dashboards saved objects 和性能回归。不要在团队模板里写“二者完全兼容”。
ISM 删除策略会真的删数据
ISM policy 进入共享环境前评审;删除动作要和 snapshot、保留周期、恢复演练绑定。
Snapshot 没恢复演练等于没有备份
至少按季度恢复到临时集群,记录耗时、数据校验、权限校验、alias 和回切方案。
磁盘水位会把可写服务变成只读服务
长期治理要看磁盘、水位、shard 分配、ISM、snapshot 保留和增长趋势,不靠临时删 index。
Dashboard 不是只读浏览器
生产 Dashboards 默认只读;Dev Tools 权限只给 owner;高危操作走脚本和审计。
安装后:
OpenSearch 版本已固定,不使用 latest。OpenSearch、Dashboards、客户端版本和镜像 digest 已记录。Docker Desktop 内存或 Linux vm.max_map_count 满足要求。
宿主端口只绑定 127.0.0.1。OPENSEARCH_INITIAL_ADMIN_PASSWORD 使用强密码,未进入仓库。curl -k -u admin 能返回 200。
Dashboards 能打开并连接本地 OpenSearch。
接入前:
.env.example 只有占位值。应用配置包含 URL、账号、密码、CA / TLS 策略和 index prefix。应用账号不是 admin。
index 使用显式 mapping。最小验证完成建 index、写入、查询、删除。共享环境权限按 prefix 隔离。
共享环境前:
owner、版本、证书、账号、角色和清理窗口明确。Dashboards 普通用户默认只读。index template、mapping、alias 和 ISM 边界已评审。
shard / replica 规划有数据量、查询量和恢复窗口依据。snapshot repository、保留周期、恢复演练和责任人已记录。磁盘水位、heap、GC、thread pool、bulk 失败率有观测入口。
清理脚本先 dry-run,并要求显式确认 prefix。OpenSearch 与 Elasticsearch 兼容边界已验证。
排障时:
先确认协议是 HTTP 还是 HTTPS。再确认 admin 密码、证书、端口和旧 volume。再看 Dashboards 配置和版本。
再看 mapping 是否被动态字段污染。再看 health、shard、磁盘水位、heap 和 thread pool。再看脚本是否指向错误环境或错误 index prefix。
