容器镜像仓库故障排查

在深入研究以下部分之前,查看一些基本的故障排除信息:

  1. 检查以确保 Docker 客户端和极狐GitLab 服务器上的系统时钟已同步(例如通过 NTP)。

  2. 如果您使用的是 S3 支持的镜像库,请仔细检查 IAM 权限和 S3 凭证(包括区域)是否正确。有关更多详细信息,请参阅示例 IAM 策略

  3. 检查镜像库日志(例如/var/log/gitlab/registry/current)和极狐GitLab 生产日志是否有错误(例如/var/log/gitlab/gitlab-rails/production.log)。您或许可以在那里找到线索。

将自签名证书与容器镜像库结合使用

如果您在容器镜像库中使用自签名证书,则在 CI 作业期间可能会遇到如下问题:

Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority

运行该命令的 Docker 守护进程需要由公认的 CA 签名的证书,因此出现上述错误。

虽然不支持在容器镜像库中使用开箱即用的自签名证书,但可以通过指示 Docker 守护进程信任自签名证书,挂载 Docker 守护进程并在极狐GitLab Runner 的 config.toml 文件中设置 privileged = false。设置 privileged = true 优先于 Docker 守护进程:

  [runners.docker]
    image = "ruby:2.6"
    privileged = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

Docker 登录尝试失败并显示:’token signed by untrusted key’

Registry 依赖极狐GitLab 来验证凭据。如果镜像库验证登录尝试失败,您会收到以下错误消息:

# docker login gitlab.company.com:4567
Username: user
Password:
Error response from daemon: login attempt to https://gitlab.company.com:4567/v2/ failed with status: 401 Unauthorized

更具体地说,这出现在 /var/log/gitlab/registry/current 日志文件中:

level=info msg="token signed by untrusted key with ID: "TOKE:NL6Q:7PW6:EXAM:PLET:OKEN:BG27:RCIB:D2S3:EXAM:PLET:OKEN""
level=warning msg="error authorizing context: invalid token" go.version=go1.12.7 http.request.host="gitlab.company.com:4567" http.request.id=74613829-2655-4f96-8991-1c9fe33869b8 http.request.method=GET http.request.remoteaddr=10.72.11.20 http.request.uri="/v2/" http.request.useragent="docker/19.03.2 go/go1.12.8 git-commit/6a30dfc kernel/3.10.0-693.2.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.2 \(linux\))"

GitLab 使用证书密钥对两侧的内容为 Registry 加密身份验证令牌。此消息表示这些内容未匹配。

检查正在使用的文件:

  • grep -A6 'auth:' /var/opt/gitlab/registry/config.yml

    ## Container Registry Certificate
       auth:
         token:
           realm: https://<example_url>/jwt/auth
           service: container_registry
           issuer: omnibus-gitlab-issuer
      -->  rootcertbundle: /var/opt/gitlab/registry/gitlab-registry.crt
           autoredirect: false
    
  • grep -A9 'Container Registry' /var/opt/gitlab/gitlab-rails/etc/gitlab.yml

    ## Container Registry Key
       registry:
         enabled: true
         host: gitlab.company.com
         port: 4567
         api_url: http://127.0.0.1:5000 # internal address to the registry, will be used by GitLab to directly communicate with API
         path: /var/opt/gitlab/gitlab-rails/shared/registry
    -->  key: /var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key
         issuer: omnibus-gitlab-issuer
         notification_secret:
    

这些 openssl 命令的输出应该匹配,证明 cert-key 对是匹配的:

/opt/gitlab/embedded/bin/openssl x509 -noout -modulus -in /var/opt/gitlab/registry/gitlab-registry.crt | /opt/gitlab/embedded/bin/openssl sha256
/opt/gitlab/embedded/bin/openssl rsa -noout -modulus -in /var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key | /opt/gitlab/embedded/bin/openssl sha256

如果证书的两部分不匹配,请删除文件并运行 gitlab-ctl reconfigure 以重新生成证书对。如果存在,则使用 /etc/gitlab/gitlab-secrets.json 中的现有值重新创建证书对。要生成新的证书对,请在运行 gitlab-ctl reconfigure 之前删除 /etc/gitlab/gitlab-secrets.json 中的 registry 部分。

如果您使用自己的证书覆盖了自动生成的自签名对,并确保它们的内容匹配,则可以删除 /etc/gitlab/gitlab-secrets.json 中的 registry 部分并运行 gitlab -ctl reconfigure

推送大镜像时,AWS S3 出现极狐GitLab 镜像库错误

在极狐GitLab 镜像库中使用 AWS S3 时,推送大镜像时可能会发生错误。在镜像库日志中查看以下错误:

level=error msg="response completed with error" err.code=unknown err.detail="unexpected EOF" err.message="unknown error"

要解决该错误,请在镜像库配置中指定一个 chunksize 值。 从 25000000 (25MB) 和 50000000 (50MB) 之间的值开始。

::Tabs

:::TabTitle Linux 软件包 (Omnibus)

  1. 编辑 /etc/gitlab/gitlab.rb

    registry['storage'] = {
      's3' => {
        'accesskey' => 'AKIAKIAKI',
        'secretkey' => 'secret123',
        'bucket'    => 'gitlab-registry-bucket-AKIAKIAKI',
        'chunksize' => 25000000
      }
    }
    
  2. 保存文件并重新配置极狐GitLab 以使更改生效。

:::TabTitle 自编译(源)

  1. 编辑 config/gitlab.yml

    storage:
      s3:
        accesskey: 'AKIAKIAKI'
        secretkey: 'secret123'
        bucket: 'gitlab-registry-bucket-AKIAKIAKI'
        chunksize: 25000000
    
  2. 保存文件并重新启动极狐GitLab 以使更改生效。

::EndTabs

支持较旧的 Docker 客户端

极狐GitLab 附带的 Docker 容器镜像库默认禁用 schema1 manifest。如果您仍在使用较旧的 Docker 客户端(1.9 或更早版本),您可能会在推送镜像时遇到错误。

您可以添加配置选项以实现向后兼容性。

::Tabs

:::TabTitle Linux 软件包 (Omnibus)

  1. 编辑 /etc/gitlab/gitlab.rb

    registry['compatibility_schema1_enabled'] = true
    
  2. 保存文件并重新配置极狐GitLab 以使更改生效。

::Tabs

:::TabTitle Linux 软件包 (Omnibus)

  1. 编辑您在部署镜像库 时创建的 YAML 配置文件。 添加以下代码段:

    compatibility:
        schema1:
            enabled: true
    
  2. 重新启动镜像库以使更改生效。

::EndTabs

Docker 连接错误

当群组、项目或分支名称中存在特殊字符时,可能会发生 Docker 连接错误。特殊字符包括:

  • 前置下划线
  • 后置连字符/破折号
  • 双连字符/破折号

要解决此问题,您可以更改群组路径、更改项目路径或更改分支名称。另一种选择是创建推送规则,在实例级别防止这种情况。

镜像推送错误

docker login 工作正常时,尝试推送镜像出现错误或 “retrying” 循环,NGINX 转发到镜像库的 header 可能存在问题。默认推荐的 NGINX 配置应该可以解决这个问题,但它可能发生在 SSL 被卸载到第三方反向代理的自定义设置中。

一个简单的解决方案是在镜像库中启用相对 URL。

::Tabs

:::TabTitle Linux 软件包 (Omnibus)

  1. 编辑 /etc/gitlab/gitlab.rb

    registry['env'] = {
      "REGISTRY_HTTP_RELATIVEURLS" => true
    }
    
  2. 保存文件并重新配置极狐GitLab 以使更改生效。

:::TabTitle 自编译(源)

  1. 编辑您在部署镜像库时创建的 YAML 配置文件。 添加以下代码段:

    http:
        relativeurls: true
    
  2. 保存文件并重新启动极狐GitLab 以使更改生效。

::EndTabs

启用 Registry debug 服务器

您可以使用容器镜像库调试服务器来诊断问题。调试端点可以监控指标和运行状况,以及进行分析。

caution 可以从调试端点获得敏感信息。在生产环境中必须锁定对调试端点的访问。

可以通过在 gitlab.rb 配置中设置镜像库调试地址,启用可选的 debug 服务器。

registry['debug_addr'] = "localhost:5001"

添加设置后,重新配置极狐GitLab 以应用更改。

使用 curl 从调试服务器请求调试输出:

curl "localhost:5001/debug/health"
curl "localhost:5001/debug/vars"

启用镜像仓库 Prometheus 指标

如果启用了调试服务器,您还可以启用 Prometheus 指标。此端点公开与几乎所有镜像仓库操作相关的高度详细的数据。

registry['debug'] = {
  'prometheus' => {
    'enabled' => true,
    'path' => '/metrics'
  }
}

使用 curl 从调试服务器请求 Prometheus 输出:

curl "localhost:5001/debug/metrics"

tag 的名称为空

如果在使用 AWS DataSync 将镜像仓库数据拷贝至 S3 存储桶,或者在存储桶之间互相拷贝,则在目标存储桶的每个容器仓库的根路径中会创建一个空的元数据对象。这会导致镜像库将这些文件解释为在极狐GitLab UI 和 API 中没有名称的标签。

要解决此问题,您可以采取以下两种方法之一:

  • 使用 AWS CLI rm命令从每一个受影响的仓库根目录中删除空对象。特别注意尾部的 /,并确保不要使用 --recursive 选项:

    aws s3 rm s3://<bucket>/docker/registry/v2/repositories/<path to repository>/
    
  • 使用 AWS CLI sync 命令将存储桶中的所有内容复制到另一个存储桶。这将删除空对象,但会创建一个新存储桶,您必须配置镜像库以使用它。

高级故障排查

我们用一个具体的例子来说明如何诊断 S3 设置的问题。

调查清理策略

如果您不确定您的清理策略为何删除或未删除标签,请通过从 Rails 控制台运行以下脚本逐行执行该策略。 这可以帮助诊断策略问题。

repo = ContainerRepository.find(<project_id>)
policy = repo.project.container_expiration_policy

tags = repo.tags
tags.map(&:name)

tags.reject!(&:latest?)
tags.map(&:name)

regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{policy.name_regex}\\z")
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{policy.name_regex_keep}\\z")

tags.select! { |tag| regex_delete.match?(tag.name) && !regex_retain.match?(tag.name) }

tags.map(&:name)

now = DateTime.current
tags.sort_by! { |tag| tag.created_at || now }.reverse! # Lengthy operation

tags = tags.drop(policy.keep_n)
tags.map(&:name)

older_than_timestamp = ChronicDuration.parse(policy.older_than).seconds.ago

tags.select! { |tag| tag.created_at && tag.created_at < older_than_timestamp }

tags.map(&:name)
  • 脚本构建要删除的标签列表(tags)。
  • tags.map(&:name) 打印要删除的标签列表。这可能是一个漫长的操作。
  • 在每个过滤器之后,检查 tags 列表,查看它是否包含要销毁的预期标签。

推送时遇到 Unexpected 403 error

用户尝试启用 S3 支持的镜像库。 docker login 步骤进行得很顺利。但是,在推送镜像时,输出显示:

The push refers to a repository [s3-testing.myregistry.com:5050/root/docker-test/docker-image]
dc5e59c14160: Pushing [==================================================>] 14.85 kB
03c20c1a019a: Pushing [==================================================>] 2.048 kB
a08f14ef632e: Pushing [==================================================>] 2.048 kB
228950524c88: Pushing 2.048 kB
6a8ecde4cc03: Pushing [==>                                                ] 9.901 MB/205.7 MB
5f70bf18a086: Pushing 1.024 kB
737f40e80b7f: Waiting
82b57dbc5385: Waiting
19429b698a22: Waiting
9436069b92a3: Waiting
error parsing HTTP 403 response body: unexpected end of JSON input: ""

这个错误是模棱两可的,因为不清楚 403 是来自极狐GitLab Rails 应用程序、Docker Registry 还是其它组件。在这种情况下,由于我们知道登录成功了,大概就需要查看客户端和 Registry 的通信。

在 Docker 文档中 描述了 Docker 客户端和 Registry 之间的 REST API。通常会使用 Wireshark 或 tcpdump 来捕获流量并查看哪里出了问题。但是,由于 Docker 客户端和服务器之间的所有通信都是通过 HTTPS 完成的,因此即使您知道私钥,也很难快速解密流量。我们能做些什么呢?

一种方法是通过设置不安全镜像库 来禁用 HTTPS。 这可能会引入安全漏洞,仅建议用于本地测试。如果您有一个生产系统并且不能或不想这样做,还有另一种方法:使用 mitmproxy,作为中间人代理。

mitmproxy

mitmproxy 允许您在客户端和服务器之间放置代理以检查所有流量。 一个问题是您的系统需要信任 mitmproxy SSL 证书才能使其工作。

以下安装说明假设您正在运行 Ubuntu:

  1. 安装 mitmproxy
  2. 运行 mitmproxy --port 9000 来生成它的证书。输入 CTRL-C 退出。
  3. ~/.mitmproxy 安装证书到你的系统:

    sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
    sudo update-ca-certificates
    

如果成功,输出应表明已添加证书:

Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d....done.

要验证证书是否已正确安装,请运行:

mitmproxy --port 9000

此命令在端口 9000 上运行 mitmproxy。在另一个窗口中,运行:

curl --proxy "http://localhost:9000" "https://httpbin.org/status/200"

如果一切设置正确,信息将显示在 mitmproxy 窗口上,并且 curl 命令不会生成任何错误。

使用代理运行 Docker 守护进程

要使 Docker 通过代理连接,您必须使用适当的环境变量启动 Docker 守护进程。最简单的方法是关闭 Docker(例如sudo initctl stop docker),然后手动运行 Docker。 以 root 身份运行:

export HTTP_PROXY="http://localhost:9000"
export HTTPS_PROXY="https://localhost:9000"
docker daemon --debug

此命令启动 Docker 守护进程并通过 mitmproxy 代理所有连接。

运行 Docker 客户端

现在我们已经运行了 mitmproxy 和 Docker,我们可以尝试登录并推送容器镜像。 您可能需要以 root 身份运行才能执行此操作。 例如:

docker login s3-testing.myregistry.com:5050
docker push s3-testing.myregistry.com:5050/root/docker-test/docker-image

在上面的示例中,我们在 mitmproxy 窗口上看到以下跟踪:

mitmproxy output from Docker

上图中显示了:

  • 最初的 PUT 请求通过 201 状态代码正常运行。
  • 201 将客户端重定向到 S3 存储桶。
  • 对 AWS 存储桶的 HEAD 请求报告了 403 Unauthorized。

这强烈表明 S3 用户没有权利执行 HEAD 请求的权限。 解决办法:再次检查 IAM权限。 一旦设置了正确的权限,错误就会消失。

缺失 gitlab-registry.key 阻止容器镜像库删除

如果您禁用实例的 Container Registry 并尝试删除具有容器仓库的项目,则会发生以下错误:

Errno::ENOENT: No such file or directory @ rb_sysopen - /var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key

在这种情况下,请按照下列步骤操作:

  1. gitlab.rb 中临时启用 Container Registry 的实例范围设置:

    gitlab_rails['registry_enabled'] = true
    
  2. 保存文件并重新配置极狐GitLab 使更改生效。
  3. 再次尝试删除。

如果使用常用方法仍然无法移除仓库,可以使用极狐GitLab Rails 控制台强制移除项目:

# Path to the project you'd like to remove
prj = Project.find_by_full_path(<project_path>)

# The following will delete the project's container registry, so be sure to double-check the path beforehand!
if prj.has_container_registry_tags?
  prj.container_repositories.each { |p| p.destroy }
end

镜像仓库服务在 IPv6 地址而不是 IPv4 地址上监听

如果 localhost 主机名解析到 IPv6 回环地址(::1)上,而极狐GitLab 期望镜像仓库服务在 IPv4 回环地址(127.0.0.1)上可用,则可能会看到以下错误:

request: "GET /v2/ HTTP/1.1", upstream: "http://[::1]:5000/v2/", host: "registry.example.com:5005"
[error] 1201#0: *13442797 connect() failed (111: Connection refused) while connecting to upstream, client: x.x.x.x, server: registry.example.com, request: "GET /v2/<path> HTTP/1.1", upstream: "http://[::1]:5000/v2/<path>", host: "registry.example.com:5005"

要解决此错误,在 /etc/gitlab/gitlab.rb 中将 registry['registry_http_addr'] 更改为 IPv4 地址。例如:

registry['registry_http_addr'] = "127.0.0.1:5000"