Blame view

docs/工作总结-微服务性能优化与架构.md 19.7 KB
208e079a   tangwang   TODO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
  # 工作总结:微服务性能优化与架构
  
  本文档汇总**三个微服务的性能优化****架构设计****性能测试基线**,便于汇报与后续复现。
  
  ---
  
  ## 一、完成三个微服务的性能优化
  
  ### 1. 文本向量(Embedding)
  
  **调研与选型**:对 TEI(Text Embeddings Inference)、vLLM 与原有 SentenceTransformers 方案进行对比评估。
  
  | 方案 | 特点 | 结论 |
  |------|------|------|
  | **SentenceTransformers** | 本地 Python 进程,单机推理,无独立服务化 | 此前用于 Qwen3-Embedding-0.6B,扩展性与运维性一般 |
  | **vLLM** | 高吞吐推理框架,更适合生成式与重排混合场景 | 纯 embedding 场景通常不作为首选 |
  | **TEI** | HuggingFace 官方 embedding 专用推理服务,Docker 部署 | **当前最优选型** |
  
  **当前方案**:以 **TEI** 为文本向量后端,模型为 `Qwen/Qwen3-Embedding-0.6B`,embedding 服务(端口 **6005**)将 `POST /embed/text` 请求转发至 TEI(默认端口 **8080**)。
  
  **具体配置与脚本**
  - **配置**`config/config.yaml` → `services.embedding.backend: "tei"`,`services.embedding.backends.tei.base_url: "http://127.0.0.1:8080"`、`model_id: "Qwen/Qwen3-Embedding-0.6B"`、`timeout_sec: 20`
  - **启动**`scripts/start_tei_service.sh`(Docker 容器);环境变量:`TEI_DEVICE=cuda`(默认)、`TEI_PORT=8080`、`TEI_MODEL_ID=Qwen/Qwen3-Embedding-0.6B`、`TEI_VERSION=1.9`、`TEI_MAX_BATCH_TOKENS=4096`、`TEI_MAX_CLIENT_BATCH_SIZE=24`、`TEI_DTYPE=float16`;T4 自动选镜像 `ghcr.io/huggingface/text-embeddings-inference:turing-1.9`
  - **编排**`./scripts/service_ctl.sh start tei` 或 `start embedding`(embedding 会校验 TEI `/health` 后再就绪)。
  
  **工程化收益**
  - **独立服务**:TEI 以 Docker 容器运行,与主程序 `.venv` 解耦;embedding 使用 `.venv-embedding`,便于独立扩缩容与升级。
  - **协议统一**:与 `local_st`(SentenceTransformers)可插拔,通过 `config.yaml` 的 `services.embedding.backend` 或环境变量 `EMBEDDING_BACKEND` 切换。
  - **T4 适配**:按 GPU 架构自动选择镜像(T4 用 `turing-*`,Ampere+ 用 `cuda-*`);`TEI_MAX_BATCH_TOKENS`、`TEI_MAX_CLIENT_BATCH_SIZE`、`TEI_DTYPE` 已按短文本场景调优。
  - **健康检查与编排**`service_ctl.sh` 对 tei 校验容器存在且 `GET http://127.0.0.1:8080/health` 成功;embedding 启动前会等待 TEI 健康。
  
  详见:`docs/TEI_SERVICE说明文档.md`、`docs/QUICKSTART.md` §4。
  
  ---
  
  ### 2. 重排(Reranker)
  
  **后端**:vLLM 部署 **Qwen3-Reranker-0.6B**,对外提供 `POST /rerank`(query + docs → scores),端口 **6007**
  
  **针对 T4 的优化(已落地配置)**
  - **精度**`dtype: "float16"`,降低显存与计算量。
  - **Prefix Caching**`enable_prefix_caching: true`,对重复前缀(如相同 query)做缓存,减少重复计算。
  - **CUDA 图**`enforce_eager: false`(默认),利用 vLLM 的 CUDA graph 降低 kernel 启动开销。
  - **按文档长度分批**`sort_by_doc_length: true`,请求内先按文档长度排序再按 `infer_batch_size` 分批推理,减少 padding 浪费;`length_sort_mode: "char"`(更快,短文本推荐)或 `"token"`(更精确)。
  - **参数搜索结论**:在 T4、1000-doc 口径下对 `infer_batch_size` 做了 24/32/48/64 对比;**单请求延迟(c=1)** 上 `infer_batch_size=64` 最优,故当前默认 `infer_batch_size: 64`;`max_model_len: 256` 满足 query+doc 短文本场景;`gpu_memory_utilization: 0.36` 与 T4 16GB 匹配。
  
  **具体配置**`config/config.yaml` → `services.rerank.backends.qwen3_vllm`):
  ```yaml
  model_name: "Qwen/Qwen3-Reranker-0.6B"
  engine: "vllm"
  max_model_len: 256
  tensor_parallel_size: 1
  gpu_memory_utilization: 0.36
  dtype: "float16"
  enable_prefix_caching: true
  enforce_eager: false
  infer_batch_size: 64
  sort_by_doc_length: true
  length_sort_mode: "char"
  instruction: "Given a shopping query, rank product titles by relevance"
  ```
  环境变量覆盖:`RERANK_BACKEND`、`RERANKER_SERVICE_URL`、`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH` 等。启停:`./scripts/service_ctl.sh start reranker`,健康:`curl -sS http://127.0.0.1:6007/health`
  
  详见:`reranker/DEPLOYMENT_AND_TUNING.md`、`reranker/README.md`
  
  ---
  
  ### 3. 图片向量(Image Embedding)
  
  **方案****clip-as-service**(CN-CLIP,ViT-H-14),由独立服务提供图片向量化能力。
  
  **具体内容**
  - **端口**:clip-as-service 默认 **51000**`CNCLIP_PORT`);文本走 TEI(8080),图片走 clip-as-service。
  - **API**:embedding 服务(6005)统一暴露 `POST /embed/text` 与 `POST /embed/image`;图片请求由 `embeddings/server.py` 按配置调用实现 `ImageEncoderProtocol` 的后端(clip-as-service 或本地 CN-CLIP)。
  - **环境与启停**:CN-CLIP 使用独立虚拟环境 `.venv-cnclip`;启动 `scripts/start_cnclip_service.sh`,或 `./scripts/service_ctl.sh start cnclip`;设备可通过 `CNCLIP_DEVICE=cuda`(默认)或 `cpu` 指定。
  - **配置**:图片后端在 `config/config.yaml` 的 `services.embedding` 下配置(若存在 image 相关 backend);clip-as-service 的 flow 配置在 `third-party/clip-as-service/server/torch-flow-temp.yml`
  
  详见:`docs/CNCLIP_SERVICE说明文档.md`、`embeddings/README.md`
  
  ---
  
  ### 4. 翻译(Translation)
  
  **背景**:原使用 DeepL,后迁移至 **qwen-mt**(如 `qwen-mt-flash`)。qwen-mt 云端限速约 **RPM=60(每分钟 60 请求)**,此前未做大商品量压测,未暴露问题;高并发索引或查询场景下易触限。
  
  **当前方案**
  - **迁移至 qwen-flash**:在配置中将翻译改为 **LLM provider + qwen-flash 模型**,由 DashScope 兼容 API 调用,可配置化切换。
  - **可配置化(具体配置)**
    - **入口**`config/config.yaml` → `services.translation`;`provider: "llm"` 时使用 `providers.llm`,`model: "qwen-flash"`,`timeout_sec: 30`,`base_url` 可选(为空则用 `DASHSCOPE_BASE_URL`);环境变量 `DASHSCOPE_API_KEY` 注入 Key。
    - **Provider 取值**`provider` 可为 `http`(走翻译服务 6006)、`qwen-mt`(直连 qwen-mt-flush 等)、`deepl`(DeepL API)、`llm`(对话模型 qwen-flash 等);工厂函数 `providers/translation.py` 的 `create_translation_provider(query_config)` 根据 `get_translation_config()` 解析结果返回对应实现。
    - **调用位置**:QueryParser(`query/query_parser.py`)与 Indexer(`indexer/incremental_service.py`、`indexer/indexing_utils.py`)均通过 `create_translation_provider(...)` 获取实例,不写死 URL 或模型名。
  - **缓存**`services.translation.cache` 支持 `key_prefix: "trans:v2"`、`ttl_seconds`、`sliding_expiration` 等,翻译结果写 Redis,减轻重复请求对限速的影响。
  - **场景支撑**:在线索引(indexer)与 query 请求(QueryParser)共用同一套 provider 配置;可按环境或租户通过修改 `config.yaml` 或环境变量切换 provider/model。
  - **待配合****金伟侧对索引侧翻译调用做流量控制**(限流/排队/批量聚合),避免索引高峰打满 qwen 限速,影响在线 query 翻译。
  
  ---
  
  ### 5. 内容理解字段(支撑 Suggest)
  
  **能力**:支持根据商品标题批量生成 **qanchors**(锚文本)、**semantic_attributes****tags**,供索引与 suggest 使用。
  
  **具体内容**
  - **接口**`POST /indexer/enrich-content`(Indexer 服务端口 **6004**)。请求体为 `items` 数组,每项含 `spu_id`、`title`(必填)及可选多语言标题等;单次请求最多 **50 条**,建议批量调用。响应 `results` 与 `items` 一一对应,每项含 `spu_id`、`qanchors`(按语言键,如 `qanchors.zh`、`qanchors.en`,逗号分隔短语)、`semantic_attributes`、`tags`
  - **索引侧**:微服务组合方式下,调用方先拿不含 qanchors/tags 的 doc,再调用本接口补齐后写入 ES 的 `qanchors.{lang}` 等字段;索引 transformer(`indexer/document_transformer.py`、`indexer/product_annotator.py`)内也可在构建 doc 时调用内容理解逻辑,写入 `qanchors.{lang}`
  - **Suggest 侧**`suggestion/builder.py` 从 ES 商品索引读取 `_source: ["id", "spu_id", "title", "qanchors"]`,对 `qanchors.{lang}` 用 `_split_qanchors` 拆成词条,以 `source="qanchor"` 加入候选,排序时 `qanchor` 权重大于纯 title(`add_product("qanchor", ...)`);suggest 配置中 `sources: ["query_log", "qanchor"]` 表示候选来源包含 qanchor。
  - **实现与依赖**:内容理解内部使用大模型(需 `DASHSCOPE_API_KEY`),支持多语言与 Redis 缓存(如 `product_anchors`);逻辑与 `indexer/product_annotator` 一致。
  
  **状态**:内容理解字段已接入索引与 suggest 链路;依赖内容理解(qanchors/tags)的**全量数据尚未全部完成一轮**,后续需持续跑满并校验效果。
  
  详见:`indexer/ANCHORS_AND_SEMANTIC_ATTRIBUTES.md`、`docs/搜索API对接指南.md`(内容理解接口)、`api/routes/indexer.py`(enrich-content 路由)。
  
  ---
  
  ## 二、架构
  
  ### 1. Provider 与动态选择翻译
  
  - **设计**:参考 `docs/系统设计文档.md`、`docs/DEVELOPER_GUIDE.md`,翻译/向量/重排均采用 **Provider + Backend** 解耦;配置单一来源为 `config/config.yaml` 的 `services` 块,环境变量可覆盖。
  - **翻译(具体实现)**
    - **工厂**`providers/translation.py` 的 `create_translation_provider(query_config)`;内部调用 `config/services_config.get_translation_config()` 得到 `provider` 与 `providers.<name>` 参数。
    - **分支**`provider in ("qwen-mt", "direct", "local", "inprocess")` → 使用 `query/qwen_mt_translate.py` 的 `Translator`(model 如 qwen-mt-flush);`provider == "http"` 或 `"service"` → `HttpTranslationProvider`(base_url 为翻译服务 6006,model 如 qwen);`provider == "llm"` → `query/llm_translate.py` 的 `LLMTranslatorProvider`(model 如 qwen-flash,base_url 可选);`provider == "deepl"` → `query/deepl_provider.py` 的 `DeepLProvider`
    - **调用方**`query/query_parser.py`(搜索前翻译)、`indexer/incremental_service.py`、`indexer/indexing_utils.py`(索引时翻译)均通过上述工厂获取实例,不写死 URL 或模型名。
  - **效果**:仅改 `config.yaml` 的 `services.translation.provider` 及对应 `providers.<name>` 即可切换 DeepL、qwen-mt、qwen-flash(llm)、HTTP 翻译服务等。
  
  ### 2. 服务的监控与拉起机制
  
  - **脚本**`scripts/service_ctl.sh` 统一负责各服务的生命周期与监控;依赖 `scripts/lib/load_env.sh` 与项目根目录 `.env`
  - **服务与端口**
    - 核心:backend **6002**、indexer **6004**、frontend **6003**
    - 可选:embedding **6005**、translator **6006**、reranker **6007**、tei **8080**、cnclip **51000**
    - 端口可由环境变量覆盖:`API_PORT`、`INDEXER_PORT`、`FRONTEND_PORT`、`EMBEDDING_PORT`、`TRANSLATION_PORT`、`RERANKER_PORT`、`TEI_PORT`、`CNCLIP_PORT`
  - **命令**
    - `./scripts/service_ctl.sh start [service...]` 或 `start all`(all = tei cnclip embedding translator reranker backend indexer frontend,按依赖顺序);`stop`、`restart` 同参数;`status` 默认列出所有服务。
    - 启动时:backend/indexer/frontend/embedding/translator/reranker 会写 pid 到 `logs/<service>.pid`,并执行 `wait_for_health`(GET `http://127.0.0.1:<port>/health`);reranker 健康重试 90 次,其余 30 次;TEI 校验 Docker 容器存在且 `/health` 成功;cnclip 无 HTTP 健康则仅校验进程/端口。
  - **监控常驻**
    - `./scripts/service_ctl.sh monitor-start <targets>` 启动后台监控进程,将 targets 写入 `logs/service-monitor.targets`,pid 写入 `logs/service-monitor.pid`,日志追加到 `logs/service-monitor.log`
    - 轮询间隔 `MONITOR_INTERVAL_SEC` 默认 **10** 秒;连续 **3** 次(`MONITOR_FAIL_THRESHOLD`)健康失败则触发重启;重启冷却 `MONITOR_RESTART_COOLDOWN_SEC` 默认 **30** 秒;每小时最多重启 `MONITOR_MAX_RESTARTS_PER_HOUR` 默认 **6** 次;超限时调用 `scripts/wechat_alert.py` 告警(若存在)。
  - **日志**:各服务按日滚动到 `logs/<service>-<date>.log`,通过 `scripts/daily_log_router.sh` 与 `LOG_RETENTION_DAYS`(默认 30)控制保留。
  
  详见:`scripts/service_ctl.sh` 内注释及 `docs/Usage-Guide.md`
  
  ### 3. Suggest 索引与增量
  
  - **当前**:suggest 索引由**全量脚本触发**,入口为 `main.py build-suggestions`,或封装脚本 `scripts/build_suggestions.sh <tenant_id> [args...]`
    - **全量示例**`./scripts/build_suggestions.sh 162 --mode full --days 30 --publish-alias`(重建建议索引,近 30 天数据,并发布别名)。
    - **增量示例**`./scripts/build_suggestions.sh 162 --mode incremental --overlap-minutes 30`(按 watermark 增量更新);脚本内部调用 `main.py build-suggestions --tenant-id <id> ...`
    - 构建逻辑在 `suggestion/builder.py` 的 `SuggestionIndexBuilder`:从 ES 商品索引(含 `title`、`qanchors`)与查询日志等拉取数据,写入 versioned 建议索引并切换 alias。
  - **尚未完成的“增量机制”**:指**自动/事件驱动的增量**(如商品变更或日志写入时自动刷新建议索引);当前 incremental 模式为“按 watermark 再跑一次构建”,仍为脚本主动触发,非持续增量流水线。
  - **依赖**:suggest 候选依赖商品侧 **内容理解字段**(qanchors/tags);`sources: ["query_log", "qanchor"]` 表示候选来自查询日志与 qanchor;当前内容理解未全量跑完一轮,suggest 数据会随全量重建逐步完善。
  
  详见:`suggestion/builder.py`、`suggestion/ARCHITECTURE_V2.md`、`main.py`(build-suggestions 子命令)。
  
  ---
  
  ## 三、性能测试报告摘要
  
  以下数据来自 `docs/性能测试报告.md`,测试时间 **2026-03-12**,环境:**8 vCPU**(Intel Xeon Platinum 8255C @ 2.50GHz)、**约 15Gi 可用内存**;租户 **162** 文档数约 **53**(search/suggest/rerank 与文档规模相关)。压测工具:`scripts/perf_api_benchmark.py`,场景×并发矩阵,每档 **20s**
  
  **复现命令(四场景×四并发)**
  ```bash
  cd /data/saas-search
  .venv/bin/python scripts/perf_api_benchmark.py \
    --scenario backend_search,backend_suggest,embed_text,rerank \
    --concurrency-list 1,5,10,20 \
    --duration 20 \
    --tenant-id 162 \
    --backend-base http://127.0.0.1:6002 \
    --embedding-base http://127.0.0.1:6005 \
    --translator-base http://127.0.0.1:6006 \
    --reranker-base http://127.0.0.1:6007 \
    --output perf_reports/2026-03-12/perf_matrix_report.json
  ```
  执行前需启动服务:`./scripts/service_ctl.sh start embedding translator reranker backend`,并通过各端口 `/health` 检查。Reranker 单独 386-docs 口径复现见性能测试报告 §7.4 与 §13。
  
  ### 3.1 向量化服务(embed_text)
  
  | 并发 | 请求数 | 成功率 | 吞吐(RPS) | Avg(ms) | P95(ms) | Max(ms) |
  |-----:|-------:|-------:|----------:|--------:|--------:|--------:|
  | 1    | 966   | 100%  | 48.27     | 20.63   | 23.41   | 49.80   |
  | 5    | 1796  | 100%  | 89.57     | 55.55   | 69.62   | 109.84  |
  | 10   | 2095  | 100%  | 104.42    | 95.22   | 117.66  | 152.48  |
  | 20   | 2393  | 100%  | 118.70    | 167.37  | 212.21  | 318.70  |
  
  **结论**:随并发提升吞吐持续增长,延迟平滑上升,扩展性较好。
  
  ---
  
  ### 3.2 Reranker(rerank)
  
  口径:query 固定 `wireless mouse`,每次请求 **386 docs**,句长 15–40 词随机(从 1000 词池采样);配置 `rerank_window=384`。复现命令:
  ```bash
  .venv/bin/python scripts/perf_api_benchmark.py \
    --scenario rerank --duration 20 --concurrency-list 1,5,10,20 --timeout 60 \
    --rerank-dynamic-docs --rerank-doc-count 386 --rerank-vocab-size 1000 \
    --rerank-sentence-min-words 15 --rerank-sentence-max-words 40 \
    --rerank-query "wireless mouse" --rerank-seed 20260312 \
    --reranker-base http://127.0.0.1:6007 \
    --output perf_reports/2026-03-12/rerank_realistic/rerank_386docs.json
  ```
  
  | 并发 | 请求数 | 成功率 | 吞吐(RPS) | Avg(ms) | P95(ms) | Max(ms) |
  |-----:|-------:|-------:|----------:|--------:|--------:|--------:|
  | 1    | 14    | 100%  | 0.67      | 1498.64 | 1799.25 | 2160.96 |
  | 5    | 15    | 100%  | 0.62      | 8011.99 | 9725.61 | 9726.02 |
  | 10   | 20    | 100%  | 0.61      | 16217.12| 18043.05| 18050.04|
  | 20   | 20    | 100%  | 0.60      | 33252.35| 33456.74| 33480.14|
  
  **结论**:在 386 docs/请求 的真实口径下,吞吐约 **0.6 rps**,延迟随并发明显上升(并发 20 时 P95 约 33.5s),是当前整条链路的**最重瓶颈**。与 DashScope 云后端对比:单并发 vLLM 延迟更优;并发 5+ 时云后端吞吐更高(约 1.3x–6x),在线高并发场景可优先考虑云后端。
  
  ---
  
  ### 3.3 搜索(backend_search)
  
  | 并发 | 请求数 | 成功率 | 吞吐(RPS) | Avg(ms) | P95(ms) | Max(ms) |
  |-----:|-------:|-------:|----------:|--------:|--------:|--------:|
  | 1    | 160   | 100%  | 7.98      | 124.89 | 228.06 | 345.49 |
  | 5    | 161   | 100%  | 7.89      | 628.91 | 1271.49| 1441.02|
  | 10   | 181   | 100%  | 8.78      | 1129.23| 1295.88| 1330.96|
  | 20   | 161   | 100%  | 7.63      | 2594.00| 4706.44| 4783.05|
  
  **结论**:吞吐约 **8 rps** 平台化,延迟随并发上升明显,符合“检索 + 向量 + 重排”重链路特征。多租户补测(文档数 500–10000,见报告 §12)表明:文档数越大,RPS 下降、延迟升高;tenant 0(10000 doc)在并发 20 出现部分 ReadTimeout(成功率 59.02%),需注意 timeout 与容量规划;补测命令示例:`for t in 0 1 2 3 4; do .venv/bin/python scripts/perf_api_benchmark.py --scenario backend_search --concurrency-list 1,5,10,20 --duration 20 --tenant-id $t --output perf_reports/2026-03-12/search_tenant_matrix/tenant_${t}.json; done`
  
  ---
  
  ### 3.4 Suggest(backend_suggest)
  
  | 并发 | 请求数 | 成功率 | 吞吐(RPS) | Avg(ms) | P95(ms) | Max(ms) |
  |-----:|-------:|-------:|----------:|--------:|--------:|--------:|
  | 1    | 3502  | 100%  | 175.09    | 5.68    | 8.70    | 15.98   |
  | 5    | 4168  | 100%  | 208.10    | 23.93   | 36.93   | 59.53   |
  | 10   | 4152  | 100%  | 207.25    | 48.05   | 59.45   | 127.20  |
  | 20   | 4190  | 100%  | 208.99    | 95.20   | 110.74  | 181.37  |
  
  **结论**:吞吐高且稳定(**约 200+ rps**),对并发友好,与 search/rerank 相比压力较小。
  
  ---
  
  ## 四、总结
  
  | 维度 | 要点 |
  |------|------|
  | **Embedding** | TEI 替代 SentenceTransformers/vLLM 作为文本向量后端,兼顾性能与工程化(Docker、配置化、T4 调优);图片向量由 clip-as-service 承担。 |
  | **Reranker** | vLLM + Qwen3-Reranker-0.6B,针对 T4 做 float16、prefix caching、CUDA 图、按长度分批及 batch/长度参数搜索;高并发场景可选用 DashScope 云后端。 |
  | **翻译** | 因 qwen-mt 限速(RPM≈60),迁移至可配置的 qwen-flash 等方案,支撑在线索引与 query;需金伟侧对索引做流量控制。 |
  | **内容理解** | 提供 qanchors/tags 等字段生成接口,支撑 suggest 与检索增强;全量一轮尚未完全跑满。 |
  | **架构** | Provider 动态选择翻译;service_ctl 统一监控与拉起;suggest 目前全量脚本触发,增量待做。 |
  | **性能基线** | 向量化扩展性良好;reranker 为整链瓶颈(386 docs 约 0.6 rps);search 约 8 rps;suggest 约 200+ rps。 |
  
  **关键文件与复现**
  - 配置:`config/config.yaml`(services、rerank、query_config)、`.env`(端口与 API Key)。
  - 脚本:`scripts/service_ctl.sh`(启停与监控)、`scripts/perf_api_benchmark.py`(压测)、`scripts/build_suggestions.sh`(suggest 构建)。
  - 完整步骤与多租户/rerank 对比见:`docs/性能测试报告.md`