极狐GitLab 备份故障排查

当您在备份极狐GitLab 时,您可能会遇到如下错误。

secret 文件丢失

如果您没有备份 secret 文件,则必须完成几个步骤才能使极狐GitLab 再次正常工作。

Secret 文件负责存储包含所需敏感信息的列的加密密钥。如果密钥丢失,极狐GitLab 无法解密这些列,从而阻止访问以下项目:

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

  • 作业卡住。
  • 500 错误。

在这种情况下,您必须重置 CI/CD 变量和 Runner 身份验证的所有令牌,这将在以下部分中进行更详细的描述。重置令牌后,您应该能够访问您的项目并且作业再次开始运行。

caution 本节中的步骤可能会导致上述项目的数据丢失

验证所有值都可以解密

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

进行备份

您必须直接修改极狐GitLab 数据才能解决丢失的 secret 文件。

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

禁用用户双因素身份验证(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
  1. 检查 ci_group_variablesci_variables 表格:

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

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

  2. 删除所有变量:

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

    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. 清除项目、群组和整个实例的所有令牌:

    caution 最终的 UPDATE 操作会阻止 runner 来选择新的作业。因此,您必须注册新的 runner。
    -- Clear project tokens
    UPDATE projects SET runners_token = null, runners_token_encrypted = null;
    -- Clear group tokens
    UPDATE namespaces SET runners_token = null, runners_token_encrypted = null;
    -- Clear instance tokens
    UPDATE application_settings SET runners_registration_token_encrypted = null;
    -- Clear key used for JWT authentication
    -- This may break the $CI_JWT_TOKEN job variable:
    -- https://gitlab.com/gitlab-org/gitlab/-/issues/325965
    UPDATE application_settings SET encrypted_ci_jwt_signing_key = null;
    -- Clear runner tokens
    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. 清除待处理作业的所有令牌:

    对于 15.3 及更早版本:

    -- Clear build tokens
    UPDATE ci_builds SET token = null, token_encrypted = null;
    

    对于 15.4 及更高版本:

    -- Clear build tokens
    UPDATE ci_builds SET token_encrypted = null;
    

对于其他功能可以采用类似的策略。通过删除无法解密的数据,极狐GitLab 可以恢复运行,并且可以手动替换丢失的数据。

修复集成和 Webhook

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

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

  1. 进入数据库控制台:

    对于 Linux 软件包(Omnibus):

    sudo gitlab-rails dbconsole --database main
    

    对于自编译安装:

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

    -- truncate web_hooks table
    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 身份运行恢复引起的,该用户无法在恢复过程中为镜像库文件分配正确的所有权。

要让您的镜像库再次工作:

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 错误。例如:

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

此问题会导致备份脚本无法完成。要解决此问题,您必须截断引发问题的文件名。最多允许 246 个字符,包括文件扩展名。

caution 本节中的步骤可能会导致数据丢失。所有步骤必须严格按照给定的顺序进行。

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

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

清理远端上传文件

有一个已知问题导致删除父资源后对象存储上传仍保留,此问题已得到解决。

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

  1. 列出所有可以移动到 lost and found 目录的对象存储上传文件(如果极狐GitLab 数据库中不存在这些文件):

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

    caution 以下操作无法撤回
    bundle exec rake gitlab:cleanup:remote_upload_files RAILS_ENV=production DRY_RUN=false
    

截断数据库引用的文件名

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

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

截断 uploads 表格中的文件名:

  1. 进入数据库控制台:

对于 Linux 软件包(Omnibus),14.2 及更高版本:

   sudo gitlab-rails dbconsole --database main

对于 Linux 软件包(Omnibus),14.1 及更早版本:

   sudo gitlab-rails dbconsole

对于自编译安装,14.2 及更高版本:

   sudo -u git -H bundle exec rails dbconsole -e production --database main

对于自编译安装,14.1 及更早版本:

   sudo -u git -H bundle exec rails dbconsole -e production
  1. 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,
          -- Current filename
          (regexp_match(u.path, '[^\\/:*?"<>|\r\n]+$'))[1] AS current_filename,
          -- New 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,
          -- New path
          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     | loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelits.txt
       new_path         | public/@hashed/loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelits.txt

    其中:

    • current_filename:当前长于 246 个字符的文件名
    • new_filename:已被截断为最多 246 个字符的文件名。
    • new_pathnew_filename 相关的新路径(已截断)。

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

  2. uploads 表中找到的文件从长文件名重命名为新的截断文件名。以下查询将回滚更新,以便您可以在 transaction wrapper 中安全地检查结果:

    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 表中的最后一条记录。

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

    caution 以下操作无法撤回
    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 时恢复数据库备份失败

PostgreSQL 数据库的极狐GitLab 备份包括启用扩展所需的所有 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 更新备份文件。

技术支持

如果您在迁移过程中遇到任何问题,您可以在极狐GitLab 官方论坛上发帖求助,您也可以直接扫描下方二维码咨询专业人员:

jihu-gitlab-support