组织
组织计划旨在实现 GitLab.com 和 GitLab 自托管版本之间的功能对等。
合并群组和项目
组织计划的一个方面是合并群组和项目,解决它们之间的功能差异。某些功能(如 epic)仅在群组级别可用。某些功能(如 issue)仅在项目级别可用。其他功能(如 milestone)则在群组和项目级别都可用。
我们收到许多请求,要求将功能添加到群组或项目级别。在不同级别之间移动功能存在多个问题:
- 需要工程时间来移动这些功能。
- 需要 UX 开销来维护功能可用性的心理模型。
- 会产生冗余代码。
当功能从一个级别(项目、群组或实例)复制到另一个级别时,这些副本之间通常存在细微的差异。这些差异在需要修复时会增加额外的工程时间,因为修复必须复制到多个位置。这些差异也会导致在不同地方使用功能时产生不同的用户体验。
解决此问题的方案是将群组和项目合并到单个实体 namespace 中。这项解决方案的工作分为几个阶段,并在 epic 6473 中进行跟踪。
如何规划与群组和项目命名空间交互的功能
目前,系统中的每个项目在 namespaces 表中都有记录。这使得可以使用通用接口创建在群组和项目之间共享的功能。可以使用 concerns 机制添加共享行为。由于 Namespace 模型还负责 UserNamespace 方法,因此不建议使用 Namespace 模型来实现项目和群组的共享行为。
基于资源的功能
要迁移基于资源的功能,需要支持现有功能。这可以通过两个阶段实现。
第一阶段 - 设置
- 链接到命名空间表
- 向表中添加一列
- 例如,在 issues 中,
project id指向 projects 表。我们需要建立到namespaces表的链接。 - 修改代码,使任何新记录已经包含正确的数据
- 回填数据
第二阶段 - 先决工作
- 调查权限模型以及相关的性能问题。
- 需要检查并保持权限不变。
- 调查哪些模型需要支持命名空间,以支持您在第一阶段迁移的功能。
- 调整 CRUD 服务和 API(REST 和 GraphQL)以指向您在第一阶段添加的新列。
- 考虑获取资源时的性能。
引入新功能在很大程度上取决于每个团队和功能。
与设置相关的功能
目前,NamespaceSettings 支持级联设置。通过创建 ProjectNamespace,我们可以使用此框架确保某些设置也适用于项目级别。
在处理设置时,我们需要确保:
- 它们不用于
join查询或修改这些查询。 - 考虑更新设置。
- 如果我们想从项目迁移到项目命名空间,我们遵循与第一阶段描述的类似数据库过程。
组织与单元
对于 Cells 项目,GitLab 将依赖组织。一个单元将托管一个或多个组织。当发出请求时,HTTP Router Service 会将其路由到正确的单元。
为所有组织表定义分片键
所有具有以下 gitlab_schema 的表都被视为组织级别:
gitlab_main_cellgitlab_cigitlab_secgitlab_main_user
所有新创建的组织级别表都需要在相应表的 db/docs/ 文件中定义一个 sharding_key。
分片键的目的在 组织隔离蓝图 中有文档说明,但简而言之,此列用于提供一种标准方式来确定哪个组织拥有数据库中的特定行。该列将来将用于强制执行不跨组织边界的数据约束。它还将用于提供在单元之间迁移数据的统一方式。
外键的实际名称可以是任何值,但它必须引用 projects 或 groups 中的行。所选的 sharding_key 列必须为非空。
允许多个 sharding_key 且列为可空,前提是该表有一个检查约束,正确确保表中的至少一个键必须为非空。有关创建这些约束的说明,请参见 多列的 NOT NULL 约束。
以下是有效的分片键示例:
-
表条目仅属于一个项目:
sharding_key: project_id: projects -
表条目属于一个项目,外键为
target_project_id:sharding_key: target_project_id: projects -
表条目仅属于一个命名空间/群组:
sharding_key: namespace_id: namespaces -
表条目仅属于一个命名空间/群组,外键为
group_id:sharding_key: group_id: namespaces -
表条目属于一个命名空间或一个项目:
sharding_key: project_id: projects namespace_id: namespaces -
(仅适用于
gitlab_main_user)表条目仅属于一个用户:sharding_key: user_id: user
分片键必须不可变
sharding_key 的选择应该始终是不可变的。这是因为分片键列将用作计划的 Org Mover 的索引,以及组织数据的 隔离强制执行。对 sharding_key 的任何变更都可能导致读取不一致的数据。
因此,如果您的功能需要允许用户在项目或群组/命名空间之间移动数据的用户体验,那么您可能需要重新设计移动功能以创建新行。这方面的示例可以在 移动 issue 功能 中看到。此功能实际上不会更改现有 issues 行的 project_id 列,而是创建一个新的 issues 行并在数据库中从原始 issues 行创建链接。如果存在特别具有挑战性的现有功能需要允许移动数据,您需要尽早联系 Tenant Scale 团队,讨论如何管理分片键的选项。
使用 namespace_id 作为分片键
namespaces 表中的行可以引用 Group、ProjectNamespace 或 UserNamespace。UserNamespace 类型也称为个人命名空间。
使用 namespace_id 作为分片键是一个不错的选择,除非 namespace_id 引用的是 UserNamespace。因为用户不一定有相关的 namespace 记录,所以这个分片键可以为 NULL。分片键不应有 NULL 值。
为项目和命名空间使用相同的分片键
开发人员也可以选择仅对属于项目的表使用 namespace_id,这些表的功能是按照 合并群组和项目蓝图 开发的。在这种情况下,namespace_id 需要是 ProjectNamespace 的 ID,而不是命名空间所属的群组。
使用 organization_id 作为分片键
通常,project_id 或 namespace_id 是最常见的分片键。但是,有些表不属于项目或命名空间。
在这种情况下,organization_id 可以作为分片键选项,前提是遵循以下指南:
sharding_key列仍然需要是 不可变的。- 仅对根级别模型(例如
namespaces)添加organization_id,而不是叶级别模型(例如issues)。 - 确保此类表不包含与群组或项目相关的数据(或属于群组/项目的记录)。相反,使用
project_id或namespace_id。 - 行数很多的表不是好的候选者,因为如果我们将实体移动到不同的组织,我们需要重写每一行,这可能很昂贵。
- 当有其他表引用此表时,如果引用表的记录被移动到不同的组织,应用程序应继续工作。
如果您认为 organization_id 是分片键的最佳选择,请寻求 Tenant Scale 团队的批准。这至关重要,因为它会影响数据迁移,并可能需要重新考虑分片键的选择。
例如,请参阅 此 issue,它将 organization_id 作为分片键添加到现有表中。
有关使用组织开发的更多信息,请参阅 组织
向现有表添加分片键
请参阅以下 指南。
定义 desired_sharding_key 以自动回填 sharding_key
我们需要为数百个没有分片键的表回填 sharding_key。此过程将涉及创建一个合并请求,如 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136800,以添加新列、从数据库中的相关表回填数据,然后创建后续的合并请求以添加索引、外键和非空约束。
为了最小化开发人员的重复工作量,我们引入了一种简洁的声明性方式来描述如何为特定表回填 sharding_key。此内容稍后将用于自动化以创建所有必要的合并请求。
desired_sharding_key 的示例添加在 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139336 中,如下所示:
--- # db/docs/security_findings.yml
table_name: security_findings
classes:
- Security::Finding
# ...
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: scanner_id
table: vulnerability_scanners
table_primary_key: id # 可选。默认为 'id'
sharding_key: project_id
belongs_to: scanner要了解此 YAML 数据将如何使用,您可以将其映射到我们在 GraphQL 中手动创建的合并请求 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136800。想法是自动创建这个。YAML 的内容指定了父表及其 sharding_key,以便在批量后台迁移中回填。它还指定了一个 belongs_to 关系,该关系将被添加到模型中以在 before_save 中自动填充 sharding_key。
当父表也有 desired_sharding_key 时定义 desired_sharding_key
默认情况下,desired_sharding_key 配置将验证所选的 sharding_key 是否存在于父表中。但是,如果父表也有 desired_sharding_key 配置并且本身正在等待回填,您需要包含 awaiting_backfill_on_parent 字段。例如:
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: package_file_id
table: packages_package_files
table_primary_key: id # 可选。默认为 'id'
sharding_key: project_id
belongs_to: package_file
awaiting_backfill_on_parent: true可能存在边缘情况,这种 desired_sharding_key 结构不适合回填 sharding_key。在这种情况下,拥有该表的团队需要创建必要的合并请求以手动添加 sharding_key。
免除某些表具有分片键
可以通过在表的数据库字典文件中添加
exempt_from_sharding: true来免除某些表具有分片键。这可以用于:
- JiHu 特定表,因为它们在 .com 数据库上没有任何数据。!145905
- 标记为即将删除的表,如
operations_feature_flag_scopes。!147541。 这些表应尽快删除。
不要将 exempt_from_sharding 用于任何其他目的。被免除的表破坏了我们的隔离工作,并在组织和单元项目的后期引入问题。
当表被免除分片键要求时,它们也不会显示在我们的 进度仪表板 中。
被免除的表不得有外键或松散外键引用,因为这可能导致目标单元的数据库在数据移动时出现外键违规。请参阅 #471182 了解示例和可能的解决方案。
确保应用程序层存在分片键
当您定义分片键时,必须确保它在应用程序层被填充。每个 ApplicationRecord 模型都包含一个辅助方法 populate_sharding_key,它提供了定义分片键逻辑的便捷方式,以及相应的匹配器来测试您的分片键逻辑。例如:
# 在 model.rb 中
populate_sharding_key :project_id, source: :merge_request, field: :target_project_id
# 在 model_spec.rb 中
it { is_expected.to populate_sharding_key(:project_id).from(:merge_request, :target_project_id) }查看更多 辅助方法示例 和 RSpec 匹配器示例。
使用 Current.organization 将请求映射到组织
应用程序需要知道如何将传入的请求映射到组织。映射逻辑封装在 Gitlab::Current::Organization 中。此映射的结果存储在一个名为 Current 的 ActiveSupport::CurrentAttributes 实例中。然后您可以使用 Current.organization 方法访问当前组织。
Current.organization 的可用性
由于此映射依赖于 HTTP 请求,Current.organization 仅在请求层可用。您可以在以下位置使用它:
- 继承自
ApplicationController的 Rails 控制器 - GraphQL 查询和变更
- Grape API 端点(需要 使用辅助方法)
在这些请求层中,可以安全地假设 Current.organization 不是 nil。
您不能在以下位置使用 Current.organization:
- Rake 任务
- Cron 作业
- Sidekiq 工作线程
此限制由 RuboCop 规则强制执行。对于这些情况,从相关数据派生组织 ID 或将其作为参数传递。
为依赖 Current.organization 的代码编写测试
如果您需要为 RSpec 提供 current_organization,可以使用 with_current_organization 共享上下文。这将创建一个 current_organization 方法,该方法将由 Gitlab::Current::Organization 类返回
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MyController, :with_current_organization do
let(:project) { create(:project, organization: current_organization) }
subject { project.organization }
it {is_expected.to eq(current_organization) }
end在 Grape API 中的使用
并非所有 Grape API 端点都提供 Current.organization。使用 set_current_organization 辅助方法来设置 Current.organization:
module API
class SomeAPIEndpoint < ::API::Base
before do
set_current_organization # 这将设置 Current.organization
end
# ... api 逻辑 ...
end
end默认组织
不要依赖默认组织。只有一个单元可以访问默认组织,其他单元无法访问它。
默认组织最初用于在引入组织数据结构时分配现有数据。但是,应用程序不再依赖默认组织。不要创建或分配默认组织对象。
默认组织仅在 GitLab.com 上可用,直到所有数据都分配给新组织为止。对默认组织的硬编码依赖在单元中不起作用。所有单元都应被视为相同。
组织数据源
组织有两个目的:
- 数据的逻辑分组(例如:一个用户属于一个或多个组织)
- Cells 的分片键
对于数据建模目的,不需要冗余的 organization_id 属性。例如,projects 表有一个 organization_id 列。从规范化的角度来看,这是不必要的,因为项目属于一个命名空间,而命名空间属于一个组织。
但是,为了分片的目的,我们违反了这个规范化规则。具有父子关系的表仍在父表和子表上定义 organization_id。
要填充 organization_id 列,请按优先顺序使用以下方法:
- 从相关数据派生。例如,子群组可以使用分配给父群组的组织。
Current.organization。这在请求层可用,可以传递给 Sidekiq 工作线程。- 询问用户。在某些情况下,需要更新 UI 并应包含选择组织的方式。