多租户建模

多租户建模 #

多租户(Multi-tenancy)是指多个用户或组织共享同一个 Easysearch 集群,但各自的数据需要隔离。本页介绍多租户场景下的索引设计、路由策略和容量规划思路。

多租户的两种主要模式 #

模式一:一个用户一个索引 #

通常来说,用户使用 Easysearch 的原因是他们需要添加全文检索或者需要分析一个已经存在的应用。他们创建一个索引来存储所有文档。公司里的其他人也逐渐发现了 Easysearch 带来的好处,也想把他们的数据添加到 Easysearch 中去。

幸运的是,Easysearch 支持多租户,所以每个用户可以在相同的集群中拥有自己的索引。有人偶尔会想要搜索所有用户的文档,这种情况可以通过搜索所有索引实现,但大多数情况下用户只关心它们自己的文档。

一些用户有着比其他人更多的文档,一些用户可能有比其他人更多的搜索次数,所以这种对指定每个索引主分片和副本分片数量能力的需要应该很适合使用"一个用户一个索引"的模式。类似地,较为繁忙的索引可以通过分片分配过滤指定到高配的节点。

提示:不要为每个索引都使用默认的主分片数。想想看它需要存储多少数据。有可能你仅需要一个分片——再多的都只是浪费资源。

大多数 Easysearch 的用户读到这里就已经够了。简单的"一个用户一个索引"对大多数场景都可以满足了。

模式二:共享索引 + 过滤 #

对于例外的场景,你可能会发现需要支持很大数量的用户,都是相似的需求。一个例子可能是为一个拥有几千个邮箱账户的论坛提供搜索服务。一些论坛可能有巨大的流量,但大多数都很小。将一个有着单个分片的索引用于一个小规模论坛已经是足够的了——一个分片可以承载很多个论坛的数据。

我们需要的是一种可以在用户间共享资源的方法,给每个用户他们拥有自己的索引这种印象,而不在小用户上浪费资源。

共享索引的实现 #

我们可以为许多的小用户使用一个大的共享的索引,将用户标识索引进一个字段并且将它用作一个过滤器:

PUT /forums
{
  "settings": {
    "number_of_shards": 10
  },
  "mappings": {
    "properties": {
      "forum_id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      }
    }
  }
}

PUT /forums/_doc/1
{
  "forum_id": "baking",
  "title":    "Easy recipe for ginger nuts",
  ...
}

我们可以把 forum_id 用作一个过滤器来针对单个用户进行搜索。这个过滤器可以排除索引中绝大部分的数据(属于其它用户的数据),缓存会保证快速的响应:

GET /forums/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "title": "ginger nuts"
        }
      },
      "filter": {
        "term": {
          "forum_id": "baking"
        }
      }
    }
  }
}

使用路由优化共享索引 #

这个办法行得通,但我们可以做得更好。来自于同一个用户的文档可以简单地容纳于单个分片,但它们现在被打散到了这个索引的所有十个分片中。这意味着每个搜索请求都必须被转发至所有十个分片的一个主分片或者副本分片。

如果能够保证所有来自于同一个用户的所有文档都被存储于同一个分片可能会是个好想法。

在路由章节中,我们说过一个文档将通过使用如下公式来分配到一个指定分片:

shard = hash(routing) % number_of_primary_shards

routing 的值默认为文档的 _id,但我们可以覆盖它并且提供我们自己自定义的路由值,例如 user_id。所有有着相同 routing 值的文档都将被存储于相同的分片:

PUT /forums/_doc/1?routing=baking
{
  "forum_id": "baking",
  "title":    "Easy recipe for ginger nuts",
  ...
}

当我们搜索一个指定用户的文档时,我们可以传递相同的 routing 值来保证搜索请求仅在存有我们文档的分片上执行:

GET /forums/_search?routing=baking
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "title": "ginger nuts"
        }
      },
      "filter": {
        "term": {
          "forum_id": "baking"
        }
      }
    }
  }
}

多个用户可以通过传递一个逗号分隔的列表来指定 routing 值,然后将每个 user_id 包含于一个 terms 查询:

GET /forums/_search?routing=baking,cooking,recipes
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "title": "ginger nuts"
        }
      },
      "filter": {
        "terms": {
          "forum_id": ["baking", "cooking", "recipes"]
        }
      }
    }
  }
}

使用别名简化多租户管理 #

这种方式从技术上来说比较高效,由于要为每一个查询或者索引请求指定 routingterms 的值看起来有一点的笨拙。索引别名可以帮你解决这些!

可以为每个用户创建一个别名,并设置过滤条件和路由:

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "forums",
        "alias": "baking_forum",
        "filter": {
          "term": {
            "forum_id": "baking"
          }
        },
        "routing": "baking"
      }
    }
  ]
}

这样,用户只需要使用 baking_forum 别名进行查询,不需要关心底层的路由和过滤逻辑。

多租户的容量规划 #

1. 评估每个租户的数据量 #

  • 小租户:数据量小,查询频率低,适合共享索引
  • 大租户:数据量大,查询频率高,适合独立索引

2. 评估查询模式 #

  • 大多数查询只涉及单个租户:适合共享索引 + 路由
  • 经常需要跨租户查询:适合独立索引或共享索引 + 过滤

3. 评估资源需求 #

  • 小租户:共享资源,降低成本
  • 大租户:独立资源,保证性能

安全考虑 #

在多租户场景下,安全隔离非常重要:

  1. 字段级别过滤:使用别名过滤确保用户只能看到自己的数据
  2. 路由隔离:使用路由确保用户数据分布在不同的分片上
  3. 权限控制:在应用层或 Easysearch 安全功能中实现权限控制

小结 #

  • 多租户可以通过"一个用户一个索引"或"共享索引 + 过滤/路由"两种模式实现
  • 小用户适合共享索引,大用户适合独立索引
  • 使用路由可以优化共享索引的查询性能
  • 使用别名可以简化多租户的管理和查询
  • 需要根据数据量、查询模式和资源需求选择合适的模式

下一步可以继续阅读: