把 Elasticsearch 做成 SaaS 产品的搜索与日志后端时,多租户是绕不开的架构问题:一套集群服务成百上千个租户,如何保证 A 租户查不到 B 租户的数据、A 租户的突发流量不拖垮 B 租户、单租户的容量增长不会挤爆整个集群。隔离不是单一开关,而是从数据、权限、资源三个层面叠加的分层设计。本文按「数据怎么分 → 权限怎么控 → 资源怎么限 → 集群怎么规划」的顺序,讲清多租户隔离的完整方案与取舍。
1. 多租户模型与隔离维度
一句话总结: 多租户隔离分数据隔离、权限隔离、资源隔离三个维度,分别解决「看不到」「查不到」「抢不到」三类问题。
1.1 三类隔离需求
- 数据隔离:租户的数据物理或逻辑上分开存放,避免互相污染与误删。
- 权限隔离:租户只能访问自己的数据,包括文档级与字段级的细粒度控制。
- 资源隔离:租户的查询与写入压力互不影响,防止「吵闹邻居」拖垮集群。
三者缺一不可。只做权限不做资源隔离,恶意或低效查询仍能拖垮集群;只做资源隔离不做数据隔离,安全边界形同虚设。
1.2 隔离强度光谱
从弱到强,常见方案大致是:共享索引 + 字段过滤(最弱)→ 共享索引 + 文档级安全 → 每租户独立索引 → 每租户独立别名与路由 → 独立集群(最强)。强度越高,运维成本与资源开销越大。
1.3 选型依据
租户数量、数据敏感度、SLA 要求、成本预算共同决定方案。小租户多、数据不敏感的 SaaS 适合共享索引 + DLS;租户少、数据敏感、SLA 严格的企业客户适合独立索引甚至独立集群。混合方案也常见:大客户独立索引,小客户共享。
2. 按租户分索引与路由
一句话总结: 按租户分索引提供最强的数据边界,自定义路由则在共享索引内把同一租户的数据收敛到少数分片,兼顾边界与开销。
2.1 每租户独立索引
最直接的做法是每个租户一组索引,用命名前缀区分:
PUT /tenant-a-logs-000001
{
"aliases": {
"tenant-a-logs": { "is_write_index": true }
},
"settings": { "number_of_shards": 1, "number_of_replicas": 1 }
}
优点是边界清晰、可单独设 ILM 与权限、可单独迁走;缺点是租户数多时索引与分片数量爆炸,集群元数据与开销陡增。
2.2 共享索引加租户字段
另一种做法是全部租户共享索引,用 tenant_id 字段区分:
{
"properties": {
"tenant_id": { "type": "keyword" },
"@timestamp": { "type": "date" },
"message": { "type": "text" }
}
}
查询必须带 tenant_id 过滤。优点是索引数可控,缺点是任何遗漏过滤都会导致越权,安全依赖应用层纪律,风险较高。
2.3 自定义路由收敛分片
共享索引时,用 _routing 把同一租户的数据落到同一分片:
curl -X POST "localhost:9200/logs/_doc?routing=tenant-a" -H "Content-Type: application/json" -d'
{ "tenant_id": "tenant-a", "message": "hello" }
'
查询时指定相同 routing,ES 只查目标分片,避免全分片广播:
curl -X GET "localhost:9200/logs/_search?routing=tenant-a" -H "Content-Type: application/json" -d'
{ "query": { "term": { "tenant_id": "tenant-a" } } }
'
这是「共享索引 + 隔离效果」的折中方案,租户内查询只扫一个分片,性能接近独立索引。
2.4 路由键的选择
路由键基数决定分片分布的均匀度。用租户 ID 作路由键,租户数远大于分片数时分布较均匀;租户数少于分片数则会浪费分片。大租户可以再按时间二次路由(tenant-a-2026-10),避免单租户数据全部堆在一个分片。
2.5 索引模板批量管理
租户索引多时,靠索引模板统一设置分片数、ILM 与映射,避免逐个创建:
PUT /_index_template/tenant-logs
{
"index_patterns": ["tenant-*-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"index.lifecycle.name": "tenant-logs-policy"
}
}
}
2.6 方案对比
| 方案 | 数据边界 | 分片开销 | 适用租户规模 |
|---|---|---|---|
| 独立索引 | 强 | 高 | 数十到数百 |
| 共享索引加路由 | 中 | 低 | 数千到数万 |
| 共享索引加字段 | 弱 | 最低 | 数万以上 |
| 独立集群 | 最强 | 最高 | 少量大客户 |
3. 文档级与字段级安全
一句话总结: 文档级安全(DLS)用查询语句限定租户可见文档,字段级安全(FLS)控制字段可见性,两者由角色定义,在查询执行前自动注入。
3.1 文档级安全
在角色里用 query 限定可见文档,ES 会把该查询与用户查询合并(AND):
PUT /_security/role/tenant_a_role
{
"indices": [
{
"names": ["tenant-a-*"],
"privileges": ["read"],
"query": {
"term": { "tenant_id": "tenant-a" }
}
}
]
}
用户即便显式查询其他租户数据,也只会返回 tenant_id = tenant-a 的文档。
3.2 共享索引下的 DLS
共享索引是 DLS 的典型场景:一个索引服务全部租户,靠角色里的 query 做行级隔离。这让索引数保持可控,同时不牺牲安全。代价是每个查询都要额外拼接过滤条件,且 DLS 会禁用请求缓存(因为缓存结果不能跨用户共享)。
3.3 字段级安全
字段级安全控制用户能看到哪些字段:
{
"names": ["tenant-a-*"],
"privileges": ["read"],
"field_security": {
"grant": ["@timestamp", "message", "level"],
"except": ["internal_notes"]
}
}
grant 与 except 组合使用:先授予一批,再排除敏感字段。被排除的字段在 _source、聚合、排序中一律不可见,返回结果里直接缺失。
3.4 动态模板与字段级安全配合
FLS 依赖字段名。如果映射里字段名随租户变化(如 field_a_tenant1),维护角色会很麻烦。设计上应保持字段名统一,用值而非字段名区分租户,这样一份角色模板就能覆盖所有租户。
3.5 DLS 的注意事项
- DLS 只在读路径生效,写入权限要单独控制。
- 聚合与聚合下钻同样受 DLS 限制,无法绕过。
- DLS 查询本身要高效,
term优于wildcard,否则每个请求都背上额外开销。 - 与别名结合使用,让角色只授予别名权限,避免直接暴露底层索引名。
3.6 安全边界的兜底
DLS/FLS 是应用层之上的第二道防线,不应作为唯一防线。应用层仍应在查询里显式带租户条件,DLS 作为「即使应用写错也不会泄露」的兜底。纵深防御比单点依赖更可靠。
4. 索引模板与配额
一句话总结: 配额管理从索引数量、分片数量、磁盘容量三个维度限制单租户的占用,防止个别租户耗尽集群资源。
4.1 索引与分片配额
Elasticsearch 没有原生的「每租户分片配额」功能,需要在治理层实现:定期巡检各租户的索引数与分片数,超限告警;或用索引模板限制单索引分片数,从源头控制总量。
GET /_cat/indices/tenant-*?v&h=index,pri,rep,docs.count,store.size
按租户前缀聚合统计,形成配额看板。
4.2 磁盘容量配额
用 ILM 的 min_age 与保留策略间接控制容量:租户的保留期越短,占用的磁盘越少。对大租户可单独设更短的保留期或更激进的降冷策略,把成本压下来。
4.3 索引模板的治理作用
模板统一分片数与副本数,避免租户自定义导致分片爆炸:
PUT /_index_template/tenant-default
{
"index_patterns": ["tenant-*"],
"priority": 1,
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"index.refresh_interval": "30s"
}
}
}
refresh_interval 调大能显著降低写入侧开销,对日志类租户尤其有效。
4.4 命名规范与生命周期
统一命名规范(tenant-<id>-<type>-<date>)是治理的前提,配额统计、权限授予、ILM 绑定都依赖命名前缀。命名混乱会让自动化治理无从下手。
4.5 配额超限的处理
超限不应直接拒绝写入(会导致数据丢失),而应告警 + 人工介入,或走「先降冷再删除」的自动策略。硬性拒绝只在极端滥用时启用,且要有明确的通知机制。
5. 搜索限流与线程池隔离
一句话总结: 资源隔离靠搜索限流与线程池两层实现,限流控制入口速率,线程池控制节点内部并发,二者共同防止吵闹邻居。
5.1 搜索限流
搜索限流按节点与请求类型限制并发,超出部分排队或拒绝:
PUT /_cluster/settings
{
"persistent": {
"search.throttled": false
}
}
更细的做法是按角色与用户限流,用 xpack.security 的限流能力或网关层(如 Nginx、API Gateway)按租户配额做前置限流。网关层限流的好处是与业务身份绑定,能按租户维度精确控制。
5.2 线程池模型
Elasticsearch 的搜索、写入、refresh、merge 各有独立线程池,默认大小按 CPU 核数计算:
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected,completed
搜索线程池默认 CPU × 3 / 2 + 1,写入线程池默认 CPU 核数。rejected 计数持续增长说明池已饱和,请求被丢弃。
5.3 线程池能否按租户隔离
Elasticsearch 不支持为不同租户划分独立线程池,线程池是节点级共享的。因此「线程池隔离」实际是「避免单个租户耗尽线程池」:通过限流把慢查询挡在外面,通过查询优化降低单请求耗时,通过副本与节点扩展提高整体吞吐。
5.4 慢查询租户的治理
用慢查询日志按租户维度统计(在查询里带租户标识,日志里能区分),找出高频慢查询的租户,推动其优化查询或升级套餐。这是「隔离」之外更根本的治理手段——降低总消耗比切分资源更有效。
5.5 副本分流
给只读副本单独挂节点,把 BI 类重查询导向副本,与写入和在线搜索分开。这是物理层面的资源隔离,代价是更多节点。对租户分层(大客户独占副本)也常这样实现。
5.6 网关层隔离实践
在 Elasticsearch 前面放一层网关,按租户身份做:限流(QPS/并发)、熔断(慢查询超时)、缓存(热点查询)、审计(记录查询来源)。这层是资源隔离最灵活的位置,能实现集群内无法做到的按租户策略。
6. 共享集群治理与容量规划
一句话总结: 共享集群的治理目标是「可观测、可配额、可迁移」,容量规划要预留足够的故障余量与增长空间。
6.1 可观测性
按租户维度采集指标:查询量、慢查询数、索引数与分片数、磁盘占用、线程池拒绝数。没有租户维度的可观测性,配额与限流都无从谈起。可以用索引前缀聚合,或在网关层打标。
6.2 容量规划公式
总容量 ≈ 租户数 × 单租户平均数据量 × 增长系数 × 副本系数。规划时要考虑:冷热分层能压多少成本、ILM 保留期如何影响总量、峰值查询并发需要多少节点。经验上集群磁盘使用率长期不超过 70%,留出 merge 与故障恢复的余量。
6.3 分片规划
分片总数控制在「节点数 × 20」以内,单分片 10~50GB。租户数多时,共享索引 + 路由是控制分片数的关键手段。分片过多会拖慢集群状态更新与恢复,是共享集群最常见的隐患。
6.4 大租户的处理
大租户应从共享池中迁出:独立索引、独立 ILM、必要时独立节点组甚至独立集群。判断标准是「该租户的资源占用是否显著影响其他租户」,一旦触及就启动迁移,而不是等它拖垮集群。
6.5 集群拆分策略
当单集群的租户数与负载达到瓶颈,可以按维度拆分:按地域拆(数据合规与延迟)、按业务线拆(日志与搜索分离)、按租户等级拆(VIP 与普通)。拆分后要统一治理层(统一限流、统一监控、统一权限),避免运维碎片化。
6.6 治理流程化
把租户接入、配额授予、监控告警、超限处理、迁移升级做成标准流程与自动化脚本。多租户治理的复杂度随租户数线性甚至超线性增长,靠人工运维必然失控。
7. 隔离方案选型与演进
一句话总结: 隔离方案应随租户规模与业务阶段演进,从共享走向分离,而非一步到位。
7.1 阶段化演进路径
- 起步期:共享索引 + 字段过滤 + DLS,成本最低,快速上线。
- 成长期:引入自定义路由收敛分片,加网关限流,建立租户维度监控。
- 成熟期:大租户迁独立索引,租户分层,配额与限流精细化。
- 规模化期:集群按维度拆分,统一治理层,自动化运营。
7.2 不要过早优化
租户数少时引入复杂的分片隔离与配额体系,只会增加运维负担。隔离强度应与实际风险匹配:数据不敏感的日志类租户,共享索引加 DLS 已足够;涉及隐私的租户才需要独立索引与更严格的 FLS。
7.3 安全与成本的平衡
隔离越强,成本越高。用「数据敏感度 × 租户价值」做二维分档,对高敏感高价值租户用强隔离,对低敏感租户用共享方案,把资源花在刀刃上。
7.4 迁移的可操作性
设计之初就要考虑「租户从共享迁到独立」的路径:命名规范统一、别名抽象、应用只认别名不认索引名。这样迁移只需切换别名指向,应用无感知。前期多花一点抽象成本,后期迁移就能平滑完成。
8. 总结
| 环节 | 要点 |
|---|---|
| 隔离维度 | 数据、权限、资源三层缺一不可 |
| 分索引 | 边界最强但分片开销高,适合少量大租户 |
| 自定义路由 | 共享索引内收敛到少数分片,性能接近独立 |
| 文档级安全 | 角色里 query 限定可见文档,读路径生效 |
| 字段级安全 | grant 与 except 控制字段可见性 |
| 配额治理 | 索引数、分片数、磁盘三维度限制 |
| 限流 | 网关层按租户限流,集群内线程池共享 |
| 容量规划 | 磁盘不超 70%,分片数受节点数约束 |
| 演进路径 | 共享起步,按规模逐步分离,迁移靠别名 |
多租户隔离的本质是在「安全」与「成本」之间找平衡点,而这个平衡点随业务阶段移动。先用共享方案快速上线,再用监控数据驱动分层与迁移,比一开始就设计完美架构更务实。权限体系的细节见《安全加固与访问控制:从角色到审计》,分片与副本的规划见《集群分片与高可用架构》,索引模板与生命周期的配合见《索引生命周期与滚动》。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。