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

当密钥文件丢失时

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

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

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

  • 卡住的作业。
  • 500 错误。

在这种情况下,你必须重置所有 CI/CD 变量和 runner 身份验证的令牌,具体步骤在以下部分中详细描述。重置令牌后,你应该可以访问你的项目,并且作业将重新开始运行。

{{< alert type=”warning” >}}

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

{{< /alert >}}

验证所有值是否可以解密

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

进行备份

你必须直接修改极狐GitLab 数据以解决丢失的密钥文件问题。

{{< alert type=”warning” >}}

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

{{< /alert >}}

禁用用户双因素认证 (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. 清除项目、群组和整个实例的所有令牌:

    {{< alert type=”warning” >}}

    最后的 UPDATE 操作将阻止 runners 接受新作业。你必须注册新的 runners。

    {{< /alert >}}

    -- 清除项目令牌
    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 恢复运行,并且可以手动替换丢失的数据。

修复集成和 webhooks

如果你丢失了密钥,集成设置webhooks 设置页面可能会显示 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 运行,无法在恢复过程中为注册表文件分配正确的所有权。

要让你的注册表再次工作:

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,将其更改为较小的值会导致此错误。

备份因 文件名过长 错误而失败

在备份期间,你可能会遇到 文件名过长 错误。例如:

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

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

{{< alert type=”warning” >}}

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

{{< /alert >}}

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

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

清理远程上传的文件

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

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

  1. 列出所有可以移动到“失物招领”目录的对象存储上传文件(如果它们在极狐GitLab 数据库中不存在):

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

    {{< alert type=”warning” >}}

    以下操作是不可逆的

    {{< /alert >}}

    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 个字符的文件名:

    以下查询按批次选择文件名超过 246 个字符的 uploads 记录,范围为 0 到 10000。这样可以提高在具有数千条记录的表的大型极狐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     | loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelits.txt
       new_path         | public/@hashed/loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelitsedvulputatemisitloremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaauctorelits.txt

    其中:

    • current_filename: 超过 246 个字符的文件名。
    • new_filename: 被截断到 246 个字符的文件名。
    • new_path: 考虑了 new_filename(截断)的新路径。

    在验证批处理结果后,你必须使用以下数字序列更改批处理大小 (row_id)(10000 到 20000)。重复此过程,直到到达 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;
    

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

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

    {{< alert type=”warning” >}}

    以下操作是不可逆的

    {{< /alert >}}

    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;
    

    在完成批处理更新后,你必须使用以下数字序列更改批处理大小 (updatable_uploads.row_id)(10000 到 20000)。重复此过程,直到到达 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/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/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