多租户与资源隔离:分索引路由、文档级安全与配额治理

系统讲解 Elasticsearch 多租户隔离方案:按租户分索引与自定义路由、文档级与字段级安全、索引模板与配额管理、搜索限流与线程池隔离,以及共享集群的容量规划与治理。

把 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%,分片数受节点数约束
演进路径共享起步,按规模逐步分离,迁移靠别名

多租户隔离的本质是在「安全」与「成本」之间找平衡点,而这个平衡点随业务阶段移动。先用共享方案快速上线,再用监控数据驱动分层与迁移,比一开始就设计完美架构更务实。权限体系的细节见《安全加固与访问控制:从角色到审计》,分片与副本的规划见《集群分片与高可用架构》,索引模板与生命周期的配合见《索引生命周期与滚动》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍