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

排查 GitLab 备份问题

当您备份 GitLab 时,可能会遇到以下问题。

当密钥文件丢失时

如果您没有备份密钥文件,您必须完成几个步骤才能让 GitLab 正常工作。

密钥文件负责存储包含必需敏感信息的列的加密密钥。如果密钥丢失,GitLab 将无法解密这些列,导致无法访问以下项目:

在 CI/CD 变量和 Runner 认证等情况下,您可能会遇到意外行为,例如:

  • 任务卡住。
  • 500 错误。

在这种情况下,您必须重置所有 CI/CD 变量和 Runner 认证的令牌,以下各节将详细说明。重置令牌后,您应该能够访问您的项目,任务将再次开始运行。

本节中的步骤可能会导致上述项目中的数据丢失。如果您是 Premium 或 Ultimate 客户,请考虑提交支持请求

验证所有值都可以解密

您可以使用 Rake 任务来确定数据库是否包含无法解密的值。

创建备份

您必须直接修改 GitLab 数据来绕过丢失的密钥文件问题。

在尝试任何更改之前,请务必创建完整的数据库备份。

禁用用户双因素认证 (2FA)

启用 2FA 的用户无法登录 GitLab。在这种情况下,您必须为所有用户禁用 2FA,之后用户必须重新激活 2FA。

重置 CI/CD 变量

  1. 进入数据库控制台:

    对于 Linux 包(Omnibus):

    sudo gitlab-rails dbconsole --database main

    对于自编译安装:

    sudo -u git -H bundle exec rails dbconsole -e production --database main
  2. 检查 ci_group_variablesci_variables 表:

    SELECT * FROM public."ci_group_variables";
    SELECT * FROM public."ci_variables";

    这些是您需要删除的变量。

  3. 删除所有变量:

    DELETE FROM ci_group_variables;
    DELETE FROM ci_variables;
  4. 如果您知道要从中删除变量的特定组或项目,可以在 DELETE 语句中包含 WHERE 子句来指定:

    DELETE FROM ci_group_variables WHERE group_id = <GROUPID>;
    DELETE FROM ci_variables WHERE project_id = <PROJECTID>;

您可能需要重新配置或重启 GitLab 才能使更改生效。

重置 Runner 注册令牌

  1. 进入数据库控制台:

    对于 Linux 包(Omnibus):

    sudo gitlab-rails dbconsole --database main

    对于自编译安装:

    sudo -u git -H bundle exec rails dbconsole -e production --database main
  2. 清除项目、组和整个实例的所有令牌:

    最后的 UPDATE 操作会阻止 Runner 能够接收新任务。您必须注册新的 Runner。

    -- 清除项目令牌
    UPDATE projects SET runners_token = null, runners_token_encrypted = null;
    -- 清除组令牌
    UPDATE namespaces SET runners_token = null, runners_token_encrypted = null;
    -- 清除实例令牌
    UPDATE application_settings SET runners_registration_token_encrypted = null;
    -- 清除用于 JWT 认证的密钥
    -- 这可能会破坏 $CI_JWT_TOKEN 任务变量:
    -- https://gitlab.com/gitlab-org/gitlab/-/issues/325965
    UPDATE application_settings SET encrypted_ci_jwt_signing_key = null;
    -- 清除 Runner 令牌
    UPDATE ci_runners SET token = null, token_encrypted = null;

重置待处理的流水线任务

  1. 进入数据库控制台:

    对于 Linux 包(Omnibus):

    sudo gitlab-rails dbconsole --database main

    对于自编译安装:

    sudo -u git -H bundle exec rails dbconsole -e production --database main
  2. 清除所有待处理任务的令牌:

    对于 GitLab 15.3 及更早版本:

    -- 清除构建令牌
    UPDATE ci_builds SET token = null, token_encrypted = null;

    对于 GitLab 15.4 及更晚版本:

    -- 清除构建令牌
    UPDATE ci_builds SET token_encrypted = null;

可以对剩余功能采用类似策略。通过删除无法解密的数据,GitLab 可以恢复运行,丢失的数据可以手动替换。

修复集成和 Webhook

如果您丢失了密钥,集成设置Webhook 设置页面可能会显示 500 错误消息。当您尝试访问具有先前配置的集成或 Webhook 的项目中的仓库时,丢失的密钥也可能产生 500 错误。

修复方法是截断受影响的表(包含加密列的表)。这将删除您配置的所有集成、Webhook 和相关元数据。在删除任何数据之前,您应该确认密钥是根本原因。

  1. 进入数据库控制台:

    对于 Linux 包(Omnibus):

    sudo gitlab-rails dbconsole --database main

    对于自编译安装:

    sudo -u git -H bundle exec rails dbconsole -e production --database main
  2. 截断以下表:

    -- 截断 web_hooks 表
    TRUNCATE integrations, chat_names, issue_tracker_data, jira_tracker_data, slack_integrations, web_hooks, zentao_tracker_data, web_hook_logs CASCADE;

容器注册表未恢复

如果您将备份从使用容器注册表的环境恢复到未启用容器注册表的新安装环境,则容器注册表不会恢复。

要同时恢复容器注册表,您需要在恢复备份之前在新环境中启用它

从备份恢复后容器注册表推送失败

如果您使用容器注册表,在 Linux 包(Omnibus)实例上恢复备份后,向注册表推送可能会失败。

这些失败在注册表日志中提及权限问题,类似于:

level=error
msg="response completed with error"
err.code=unknown
err.detail="filesystem: mkdir /var/opt/gitlab/gitlab-rails/shared/registry/docker/registry/v2/repositories/...: permission denied"
err.message="unknown error"

此问题是由恢复过程以无特权用户 git 身份运行引起的,该用户在恢复过程中无法为注册表文件分配正确的所有权(问题 #62759)。

要让您的注册表恢复正常工作:

sudo chown -R registry:registry /var/opt/gitlab/gitlab-rails/shared/registry/docker

如果您更改了注册表的默认文件系统位置,请对您的自定义位置运行 chown,而不是 /var/opt/gitlab/gitlab-rails/shared/registry/docker

备份因 Gzip 错误而失败

运行备份时,您可能会收到 Gzip 错误消息:

sudo /opt/gitlab/bin/gitlab-backup create
...
Dumping ...
...
gzip: stdout: Input/output error

Backup failed

如果发生这种情况,请检查以下内容:

  • 确认 Gzip 操作有足够的磁盘空间。使用默认策略的备份在创建备份期间通常需要实例大小一半的可用磁盘空间。
  • 如果使用 NFS,请检查是否设置了挂载选项 timeout。默认值为 600,将其更改为较小值会导致此错误。

备份因 File name too long 错误而失败

在备份过程中,您可能会收到 File name too long 错误(问题 #354984)。例如:

Problem: <class 'OSError: [Errno 36] File name too long:

此问题阻止备份脚本完成。要解决此问题,您必须截断导致问题的文件名。允许的最大长度为 246 个字符,包括文件扩展名。

本节中的步骤可能会导致数据丢失。所有步骤必须严格按照给定顺序执行。 如果您是 Premium 或 Ultimate 客户,请考虑提交支持请求

截断文件名以解决错误涉及:

  • 清理未在数据库中跟踪的远程上传文件。
  • 截断数据库中的文件名。
  • 重新运行备份任务。

清理远程上传文件

已知问题导致对象存储上传在父资源被删除后仍然存在。此问题已解决

要修复这些文件,您必须清理所有存储在存储中但未在 uploads 数据库表中跟踪的远程上传文件。

  1. 列出所有对象存储上传文件,如果它们不存在于 GitLab 数据库中,可以将其移动到丢失和找到目录:

    bundle exec rake gitlab:cleanup:remote_upload_files RAILS_ENV=production
  2. 如果您确定要删除这些文件并删除所有未引用的上传文件,请运行:

    以下操作是不可逆的。

    bundle exec rake gitlab:cleanup:remote_upload_files RAILS_ENV=production DRY_RUN=false

截断数据库引用的文件名

您必须截断数据库引用的导致问题的文件名。数据库引用的文件名存储在:

  • uploads 表中。
  • 找到的引用中。从其他数据库表和列中找到的任何引用。
  • 文件系统上。

截断 uploads 表中的文件名:

  1. 进入数据库控制台:

    对于 Linux 包(Omnibus):

    sudo gitlab-rails dbconsole --database main

    对于自编译安装:

    sudo -u git -H bundle exec rails dbconsole -e production --database main
  2. 搜索 uploads 表中文件名超过 246 个字符的记录:

    以下查询按 0 到 10000 的批次选择文件名超过 246 个字符的 uploads 记录。这提高了具有数千条记录的大型 GitLab 实例上的性能。

    CREATE TEMP TABLE uploads_with_long_filenames AS
    SELECT ROW_NUMBER() OVER(ORDER BY id) row_id, id, path
    FROM uploads AS u
    WHERE LENGTH((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1]) > 246;
    
    CREATE INDEX ON uploads_with_long_filenames(row_id);
    
    SELECT
       u.id,
       u.path,
       -- 当前文件名
       (regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1] AS current_filename,
       -- 新文件名
       CONCAT(
          LEFT(SPLIT_PART((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1], '.', 1), 242),
          COALESCE(SUBSTRING((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1] FROM '\.(?:.(?!\.))+$'))
       ) AS new_filename,
       -- 新路径
       CONCAT(
          COALESCE((regexp_match(u.path, '(.*\/).*'))[1], ''),
          CONCAT(
             LEFT(SPLIT_PART((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1], '.', 1), 242),
             COALESCE(SUBSTRING((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1] FROM '\.(?:.(?!\.))+$'))
          )
       ) AS new_path
    FROM uploads_with_long_filenames AS u
    WHERE u.row_id > 0 AND u.row_id <= 10000;

    输出示例:

    -[ RECORD 1 ]----+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    id               | 34
    path             | public/@hashed/loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisit.txt
    current_filename | loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisit.txt
    new_filename     | loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsed.txt
    new_path         | public/@hashed/loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsed.txt

    其中:

    • current_filename:超过 246 个字符的文件名。
    • new_filename:已截断为最多 246 个字符的文件名。
    • new_path:考虑 new_filename(已截断)的新路径。

    验证批次结果后,您必须使用以下数字序列(10000 到 20000)更改批次大小(row_id)。重复此过程,直到到达 uploads 表中的最后一条记录。

  3. uploads 表中找到的长文件名重命名为新的截断文件名。以下查询回滚更新,以便您可以在事务包装器中安全地检查结果:

    CREATE TEMP TABLE uploads_with_long_filenames AS
    SELECT ROW_NUMBER() OVER(ORDER BY id) row_id, path, id
    FROM uploads AS u
    WHERE LENGTH((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1]) > 246;
    
    CREATE INDEX ON uploads_with_long_filenames(row_id);
    
    BEGIN;
    WITH updated_uploads AS (
       UPDATE uploads
       SET
          path =
          CONCAT(
             COALESCE((regexp_match(updatable_uploads.path, '(.*\/).*'))[1], ''),
             CONCAT(
                LEFT(SPLIT_PART((regexp_match(updatable_uploads.path, '[^\/:*?"<>|\r\n]+$'))[1], '.', 1), 242),
                COALESCE(SUBSTRING((regexp_match(updatable_uploads.path, '[^\/:*?"<>|\r\n]+$'))[1] FROM '\.(?:.(?!\.))+$'))
             )
          )
       FROM
          uploads_with_long_filenames AS updatable_uploads
       WHERE
          uploads.id = updatable_uploads.id
       AND updatable_uploads.row_id > 0 AND updatable_uploads.row_id  <= 10000
       RETURNING uploads.*
    )
    SELECT id, path FROM updated_uploads;
    ROLLBACK;

    验证批次更新结果后,您必须使用以下数字序列(10000 到 20000)更改批次大小(row_id)。重复此过程,直到到达 uploads 表中的最后一条记录。

  4. 验证先前查询中的新文件名是否符合预期。如果您确定要将先前步骤中找到的记录截断为 246 个字符,请运行以下操作:

    以下操作是不可逆的。

    CREATE TEMP TABLE uploads_with_long_filenames AS
    SELECT ROW_NUMBER() OVER(ORDER BY id) row_id, path, id
    FROM uploads AS u
    WHERE LENGTH((regexp_match(u.path, '[^\/:*?"<>|\r\n]+$'))[1]) > 246;
    
    CREATE INDEX ON uploads_with_long_filenames(row_id);
    
    UPDATE uploads
    SET
    path =
       CONCAT(
          COALESCE((regexp_match(updatable_uploads.path, '(.*\/).*'))[1], ''),
          CONCAT(
             LEFT(SPLIT_PART((regexp_match(updatable_uploads.path, '[^\/:*?"<>|\r\n]+$'))[1], '.', 1), 242),
             COALESCE(SUBSTRING((regexp_match(updatable_uploads.path, '[^\/:*?"<>|\r\n]+$'))[1] FROM '\.(?:.(?!\.))+$'))
          )
       )
    FROM
    uploads_with_long_filenames AS updatable_uploads
    WHERE
    uploads.id = updatable_uploads.id
    AND updatable_uploads.row_id > 0 AND updatable_uploads.row_id  <= 10000;

    完成批次更新后,您必须使用以下数字序列(10000 到 20000)更改批次大小(updatable_uploads.row_id)。重复此过程,直到到达 uploads 表中的最后一条记录。

截断找到的引用中的文件名:

  1. 检查这些记录是否被某处引用。一种方法是转储数据库并搜索父目录名和文件名:

    1. 要转储您的数据库,可以使用以下命令作为示例:

      pg_dump -h /var/opt/gitlab/postgresql/ -d gitlabhq_production > gitlab-dump.tmp
    2. 然后您可以使用 grep 命令搜索引用。结合父目录和文件名可能是个好主意。例如:

      grep public/alongfilenamehere.txt gitlab-dump.tmp
  2. 使用从查询 uploads 表获得的新文件名替换那些长文件名。

截断文件系统上的文件名。您必须手动将文件系统中的文件重命名为从查询 uploads 表获得的新文件名。

重新运行备份任务

完成所有先前步骤后,重新运行备份任务。

当之前启用了 pg_stat_statements 时恢复数据库备份失败

GitLab 对 PostgreSQL 数据库的备份包含所有 SQL 语句,用于启用数据库中之前启用的扩展。

pg_stat_statements 扩展只能由具有 superuser 角色的 PostgreSQL 用户启用或禁用。由于恢复过程使用具有有限权限的数据库用户,它无法执行以下 SQL 语句:

DROP EXTENSION IF EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public;

当尝试在没有 pg_stats_statements 扩展的 PostgreSQL 实例中恢复备份时,会显示以下错误消息:

ERROR: permission denied to create extension "pg_stat_statements"
HINT: Must be superuser to create this extension.
ERROR: extension "pg_stat_statements" does not exist

当尝试在启用了 pg_stats_statements 扩展的实例中恢复时,清理步骤失败并显示类似于以下内容的错误消息:

rake aborted!
ActiveRecord::StatementInvalid: PG::InsufficientPrivilege: ERROR: must be owner of view pg_stat_statements
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:42:in `block (4 levels) in <top (required)>'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:41:in `each'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:41:in `block (3 levels) in <top (required)>'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/backup.rake:71:in `block (3 levels) in <top (required)>'
/opt/gitlab/embedded/bin/bundle:23:in `load'
/opt/gitlab/embedded/bin/bundle:23:in `<main>'
Caused by:
PG::InsufficientPrivilege: ERROR: must be owner of view pg_stat_statements
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:42:in `block (4 levels) in <top (required)>'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:41:in `each'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:41:in `block (3 levels) in <top (required)>'
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/backup.rake:71:in `block (3 levels) in <top (required)>'
/opt/gitlab/embedded/bin/bundle:23:in `load'
/opt/gitlab/embedded/bin/bundle:23:in `<main>'
Tasks: TOP => gitlab:db:drop_tables
(See full trace by running task with --trace)

防止转储文件包含 pg_stat_statements

为了防止扩展包含在作为备份包一部分的 PostgreSQL 转储文件中,在任何架构中启用扩展,但 public 架构除外:

CREATE SCHEMA adm;
CREATE EXTENSION pg_stat_statements SCHEMA adm;

如果扩展之前在 public 架构中启用,请将其移动到新架构:

CREATE SCHEMA adm;
ALTER EXTENSION pg_stat_statements SET SCHEMA adm;

要更改架构后查询 pg_stat_statements 数据,请在视图名称前加上新架构:

SELECT * FROM adm.pg_stat_statements limit 0;

为了使其与期望它在 public 架构中启用的第三方监控解决方案兼容,您需要将其包含在 search_path 中:

set search_path to public,adm;

修复现有的转储文件以删除对 pg_stat_statements 的引用

要修复现有的备份文件,请进行以下更改:

  1. 从备份中提取以下文件:db/database.sql.gz

  2. 解压文件或使用能够处理压缩文件的编辑器。

  3. 删除以下行或类似的行:

    CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public;
    COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed';
  4. 保存更改并重新压缩文件。

  5. 使用修改后的 db/database.sql.gz 更新备份文件。