Help us learn about your current experience with the documentation. Take the survey.

GitHub importer 开发者文档

GitHub importer 是一个使用 Sidekiq 的并行导入器。

先决条件

  • 处理 github_importergithub_importer_advance_stage 队列的 Sidekiq workers(默认启用)。
  • Octokit(用于与 GitHub API 交互)。

代码结构

导入器的代码库分为以下目录:

  • lib/gitlab/github_import:此目录包含大部分代码,例如用于导入资源的类。
  • app/workers/gitlab/github_import:此目录包含 Sidekiq workers。
  • app/workers/concerns/gitlab/github_import:此目录包含一些被各个 Sidekiq workers 重复使用的模块。

架构概览

当导入 GitHub 项目时,工作被分为不同的阶段,每个阶段由一组执行的 Sidekiq 作业组成。在每个阶段之间,会安排一个作业来定期检查当前阶段的所有工作是否完成,如果是,则将导入过程推进到下一阶段。处理此作业的 worker 称为 Gitlab::GithubImport::AdvanceStageWorker

阶段

1. RepositoryImportWorker

此 worker 调用 Projects::ImportService.new.execute, 该服务调用 importer.execute

在此上下文中,importerGitlab::ImportSources.importer(project.import_type) 的实例, 对于 github 导入类型,它映射到 ParallelImporter

ParallelImporter 为下一个 worker 安排作业。

2. Stage::ImportRepositoryWorker

此 worker 导入仓库和 wiki,完成后安排下一阶段。

3. Stage::ImportBaseDataWorker

此 worker 导入基础数据,如标签、里程碑和发布。这些工作在单线程中完成,因为执行速度足够快,不需要并行执行。

4. Stage::ImportPullRequestsWorker

此 worker 导入所有拉取请求。对于每个拉取请求,都会为 Gitlab::GithubImport::ImportPullRequestWorker worker 安排一个作业。

5. Stage::ImportCollaboratorsWorker

此 worker 仅导入直接仓库协作者,不包括外部协作者。对于每个协作者,我们为 Gitlab::GithubImport::ImportCollaboratorWorker worker 安排一个作业。

此阶段是可选的(由 Gitlab::GithubImport::Settings 控制),默认选择。

6. Stage::ImportIssuesAndDiffNotesWorker

此 worker 导入所有问题和拉取请求评论。对于每个问题,我们为 Gitlab::GithubImport::ImportIssueWorker worker 安排一个作业。对于拉取请求评论,我们改为为 Gitlab::GithubImport::DiffNoteImporter worker 安排作业。

此 worker 并行处理问题和 diff notes,因此我们不需要安排单独的阶段并等待前一个阶段完成。

问题与拉取请求分开导入,因为只有 “issues” API 包含问题和拉取请求的标签。在同一 worker 中导入问题和设置标签链接,无需单独遍历 API 数据,从而减少了导入项目所需的 API 调用次数。

7. Stage::ImportIssueEventsWorker

此 worker 导入所有问题和拉取请求事件。对于每个事件,我们为 Gitlab::GithubImport::ImportIssueEventWorker worker 安排一个作业。

我们可以通过单个阶段导入问题和拉取请求事件,这是由于 GitHub API 的特定特性。看起来在底层,问题和拉取请求在 GitHub 中存储在同一个表中。因此,它们具有全局唯一的 ID,所以:

  • 每个拉取请求都是一个问题。
  • 问题不是拉取请求。

因此,问题和拉取请求在大多数相关事物上具有通用的 API。

为了使用时间线事件端点导入 pull request review requests,事件必须按顺序处理。鉴于导入 workers 不保证执行顺序,pull request review requests 事件最初被放置在 Redis 有序列表中。随后,它们由 Gitlab::GithubImport::ReplayEventsWorker 按顺序消费。

8. Stage::ImportAttachmentsWorker

此 worker 导入链接在 Markdown 内部的笔记附件。对于项目中包含 Markdown 文本的每个实体,我们安排以下作业:

  • 对于每个发布,安排 Gitlab::GithubImport::Importer::Attachments::ReleasesImporter 作业
  • 对于每个笔记,安排 Gitlab::GithubImport::Importer::Attachments::NotesImporter 作业
  • 对于每个问题,安排 Gitlab::GithubImport::Importer::Attachments::IssuesImporter 作业
  • 对于每个合并请求,安排 Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter 作业

每个作业:

  1. 遍历特定记录内的所有附件链接
  2. 下载附件
  3. 将旧链接替换为指向 GitLab 的新生成链接

这是一个可选阶段,可能会消耗额外的导入时间(由 Gitlab::GithubImport::Settings 控制)。

9. Stage::ImportProtectedBranchesWorker

此 worker 导入受保护分支规则。对于 GitHub 上存在的每个规则,我们为 Gitlab::GithubImport::ImportProtectedBranchWorker 安排一个作业。

每个作业比较 GitHub 和 GitLab 中的分支保护规则,并将最严格的规则应用到 GitLab 中的分支。

10. Stage::FinishImportWorker

此 worker 通过执行一些清理工作(如刷新任何缓存)并将标记导入为已完成来完成导入过程。

推进阶段

推进阶段有两种方式:

  • 直接安排下一阶段的 worker。
  • Gitlab::GithubImport::AdvanceStageWorker 安排一个作业,当当前阶段的所有工作完成后,它将推进阶段。

第一种方法仅应由在单线程中执行所有工作的 workers 使用,而 AdvanceStageWorker 应用于其他所有情况。

第一种方法的示例是 ImportBaseDataWorker 如何直接调用 PullRequestWorker 在此处

第二种方法的示例是 PullRequestsWorker 在其自身工作完成后如何调用 AdvanceStageWorker 在此处

当您安排作业时,AdvanceStageWorker 会获得项目 ID、Redis 键列表和下一阶段的名称。Redis 键(由 Gitlab::JobWaiter 生成)用于检查当前阶段是否已完成。如果阶段尚未完成,AdvanceStageWorker 会重新安排自身。一个阶段完成后,或在上次调用后有更多作业完成时,AdvanceStageworker 会刷新导入 JID(更多内容见下文)并安排下一阶段的 worker。

为了减少安排的 AdvanceStageWorker 作业数量,此 worker 在决定下一步操作前会短暂等待作业完成。对于小项目,这可能会稍微减慢导入过程,但也减轻了整个系统的压力。

刷新导入作业 ID

GitLab 包含一个名为 Gitlab::Import::StuckProjectImportJobsWorker 的 worker,它定期运行,如果项目导入超过 24 小时未刷新,则将其标记为失败。对于 GitHub 项目,这带来了一些问题:导入大型项目可能需要几天时间,具体取决于我们达到 GitHub 速率限制的频率(更多内容见下文),但我们不希望 Gitlab::Import::StuckProjectImportJobsWorker 因此将我们的导入标记为失败。

为防止这种情况发生,我们定期刷新导入的过期时间。这是通过将导入作业的 JID 存储在数据库中,然后在导入过程中的各个阶段刷新此 JID 的 TTL 来实现的。这通过调用 ProjectImportState#refresh_jid_expiration 或使用 RefreshImportJidWorker 并传入当前 worker 的 jid 来完成。通过刷新此 TTL,我们可以确保只要我们仍在执行工作,我们的导入就不会被标记为失败。

GitHub 速率限制

GitHub 的速率为每小时 5,000 次 API 调用。导入项目所需的请求数量主要由项目涉及的唯一用户数量决定(例如,问题作者),因为我们需要用户的电子邮件地址将他们映射到 GitLab 用户。其他数据如问题页面和评论通常只需要几十次请求即可导入。

我们通过以下方式处理速率限制:

  1. 达到速率限制后,我们自动重新安排作业,使其在速率限制重置之前不会执行。
  2. 我们将 GitHub 用户到 GitLab 用户的映射缓存在 Redis 中。

有关用户缓存的更多信息见下文。

缓存用户查找

在将 GitHub 用户映射到 GitLab 用户时,我们可能需要(在最坏情况下)执行:

  1. 一次 API 调用来获取用户的电子邮件地址。
  2. 两次数据库查询来查看是否存在对应的 GitLab 用户。一次查询尝试基于 GitHub 用户 ID 查找用户,而第二次查询用于使用 GitHub 电子邮件地址查找用户。

为了避免用户不匹配,从 GitHub Enterprise 导入时不执行基于 GitHub 用户 ID 的搜索。

由于此过程相当昂贵,我们将这些查找的结果缓存在 Redis 中。对于每个查找的用户,我们存储五个键:

  • 一个将 GitHub 用户名映射到其电子邮件地址的 Redis 键。
  • 一个将 GitHub 电子邮件地址映射到 GitLab 用户 ID 的 Redis 键。
  • 一个将 GitHub 用户 ID 映射到 GitLab 用户 ID 的 Redis 键。
  • 一个将 GitHub 用户名映射到 ETAG 标头的 Redis 键。
  • 一个指示是否已为项目完成电子邮件查找的 Redis 键。

我们缓存两种类型的查找:

  • 正向查找,表示我们找到了 GitLab 用户 ID。
  • 负向查找,表示我们没有找到 GitLab 用户 ID。缓存此查找可防止我们对已知不在 GitLab 数据库中的用户执行相同的工作。

这些键的过期时间为 24 小时。当检索正向查找的缓存时,我们会自动刷新 TTL。负向查找的 TTL 永远不会刷新。

如果电子邮件查找返回空或负向结果,则对每个项目使用缓存的 ETAG 在标头中进行一次 条件请求。条件请求不计入 GitHub API 速率限制。

由于此缓存层,新注册的 GitLab 账户可能无法链接到其对应的 GitHub 账户。但是,这在缓存键过期或导入新项目后会得到解决。

用户缓存查找在项目之间共享。这意味着导入的项目越多,所需的 GitHub API 调用就越少。

相关代码位于:

  • lib/gitlab/github_import/user_finder.rb
  • lib/gitlab/github_import/caching.rb

增加 Sidekiq 中断

当 Sidekiq 进程关闭时,它会等待一段时间让正在运行的作业完成,然后中断它们。中断会终止作业并重新排队。我们的 vendored sidekiq-reliable-fetcher gem 将中断限制为 3 次,之后作业不再重新排队并被永久终止。已被中断的作业会在 Kibana 中记录 json.interrupted_count

此限制可防止在 Sidekiq 重启之间的时间内永远无法完成的作业。

对于大型导入,我们的 GitHub 阶段 workers(在 Stage:: 命名空间中)需要数小时才能完成。默认情况下,导入有失败的风险,因为 sidekiq-reliable-fetcher 在这些 workers 完成之前永久停止它们。

可以从上次中断处继续工作的阶段 workers 可以通过调用 .resumes_work_when_interrupted!sidekiq-reliable-fetcher 的中断限制增加到 20

module Gitlab
  module GithubImport
    module Stage
      class MyWorker
        resumes_work_when_interrupted!

        # ...
      end
    end
  end
end

重启时不能完全恢复工作的阶段 workers 不应调用此方法。例如,一个跳过已导入对象但每次都从头开始循环的 worker。

能够完全恢复工作的阶段 workers 的示例是执行以下服务的 workers:

sidekiq_options dead: false

通常当 worker 的重试次数用尽时,它们会进入 Sidekiq dead set,可以由实例管理员重试。

GithubImport::Queue 设置 Sidekiq worker 选项 dead: false 以防止这种情况发生在 GitHub importer workers 上。

原因是:

  • dead set 有最大限制,如果对象导入 workers(包含 ObjectImporter 的)大量失败,它们可能会 spam dead set 并将其他 workers 推出。
  • 阶段 workers(包含 StageMethods 的) 在重试次数用尽时失败导入,因此重试将保证是 无操作

映射标签和里程碑

为了减轻数据库压力,在设置问题和合并请求的标签和里程碑时,我们不查询数据库。相反,我们在导入标签和里程碑时缓存这些数据,然后在将它们分配给问题/合并请求时重用此缓存。与用户查找类似,这些缓存键在 24 小时未使用后自动过期。

与用户查找缓存不同,这些标签和里程碑缓存限定在正在导入的项目范围内。

相关代码位于:

  • lib/gitlab/github_import/label_finder.rb
  • lib/gitlab/github_import/milestone_finder.rb
  • lib/gitlab/cache/import/caching.rb

日志

可以在 logs/importer.log 文件中检查导入进度。每个相关的导入都记录了 "import_type": "github""project_id"

最后一条日志条目报告获取和导入的对象数量:

{
  "message": "GitHub project import finished",
  "duration_s": 347.25,
  "objects_imported": {
    "fetched": {
      "diff_note": 93,
      "issue": 321,
      "note": 794,
      "pull_request": 108,
      "pull_request_merged_by": 92,
      "pull_request_review": 81
    },
    "imported": {
      "diff_note": 93,
      "issue": 321,
      "note": 794,
      "pull_request": 108,
      "pull_request_merged_by": 92,
      "pull_request_review": 81
    }
  },
  "import_source": "github",
  "project_id": 47,
  "import_stage": "Gitlab::GithubImport::Stage::FinishImportWorker"
}

指标仪表板

为了评估 GitHub importer 的健康状况,GitHub importer 仪表板提供了随时间获取与导入的对象总数信息。