- 作业产物使用过多的磁盘空间
- 作业产物上传失败,错误 500
- 作业产物上传失败,
500 Internal Server Error (Missing file)
- 作业产物在使用 Windows 挂载时上传失败,
FATAL: invalid argument
- 使用配额显示不正确的产物存储使用情况
- 产物下载流程图
413 Request Entity Too Large
错误
{{< details >}}
- Tier: 基础版, 专业版, 旗舰版
- Offering: 私有化部署
{{< /details >}}
在管理作业产物时,你可能会遇到以下问题。
作业产物使用过多的磁盘空间
作业产物可能会比预期更快地填满你的磁盘空间。可能的原因包括:
- 用户配置了作业产物的过期时间长于必要时间。
- 运行的作业数量和因此生成的产物数量高于预期。
- 作业日志比预期大,并且随着时间的推移积累。
- 文件系统可能会耗尽 inode,因为产物维护留下了空目录。清除孤立产物文件的 Rake 任务可以删除这些文件。
- 产物文件可能被留在磁盘上,并且没有被维护删除。运行清除孤立产物文件的 Rake 任务以删除这些文件。这个脚本应该总能找到需要做的工作,因为它还会删除空目录(见上文)。
- 产物维护在极狐GitLab 15.0 到 15.2 中被显著更改,你可能需要启用一个功能标志来使用更新的系统。
- 启用了从最近成功的作业中保留最新产物的功能。
在这些和其他情况下,确定最占用磁盘空间的项目,找出使用最多空间的产物类型,并在某些情况下手动删除作业产物以回收磁盘空间。
产物维护
产物维护是识别哪些产物已过期并可以删除的过程。
极狐GitLab 15.0 到 15.2 中禁用的维护
产物维护在极狐GitLab 15.0 中得到了显著改进,默认情况下通过功能标志引入。默认情况下,在极狐GitLab 15.3 中启用了这些标志。
如果在极狐GitLab 15.0 到极狐GitLab 15.2 中产物维护似乎不起作用,你应该检查功能标志是否已启用。
要检查功能标志是否已启用:
-
启动一个 Rails 控制台。
-
检查功能标志是否已启用。
Feature.enabled?(:ci_detect_wrongly_expired_artifacts) Feature.enabled?(:ci_update_unlocked_job_artifacts) Feature.enabled?(:ci_job_artifacts_backlog_work)
-
如果任何功能标志被禁用,请启用它们:
Feature.enable(:ci_detect_wrongly_expired_artifacts) Feature.enable(:ci_update_unlocked_job_artifacts) Feature.enable(:ci_job_artifacts_backlog_work)
这些更改包括在应该保留时将产物从 unlocked
切换到 locked
。
状态为 unknown
的产物
在更新维护之前创建的产物状态为 unknown
。在它们过期后,这些产物不会被新的维护处理。
你可以检查数据库以确认你的实例是否有状态为 unknown
的产物:
-
启动一个数据库控制台:
{{< tabs >}}
{{< tab title=”Linux package (Omnibus)” >}}
sudo gitlab-psql
{{< /tab >}}
{{< tab title=”Helm chart (Kubernetes)” >}}
# Find the toolbox pod kubectl --namespace <namespace> get pods -lapp=toolbox # Connect to the PostgreSQL console kubectl exec -it <toolbox-pod-name> -- /srv/gitlab/bin/rails dbconsole --include-password --database main
{{< /tab >}}
{{< tab title=”Docker” >}}
sudo docker exec -it <container_name> /bin/bash gitlab-psql
{{< /tab >}}
{{< tab title=”Self-compiled (source)” >}}
sudo -u git -H psql -d gitlabhq_production
{{< /tab >}}
{{< /tabs >}}
-
运行以下查询:
select expire_at, file_type, locked, count(*) from ci_job_artifacts where expire_at is not null and file_type != 3 group by expire_at, file_type, locked having count(*) > 1;
如果返回记录,则表示有产物无法被维护作业处理。例如:
expire_at | file_type | locked | count
-------------------------------+-----------+--------+--------
2021-06-21 22:00:00+00 | 1 | 2 | 73614
2021-06-21 22:00:00+00 | 2 | 2 | 73614
2021-06-21 22:00:00+00 | 4 | 2 | 3522
2021-06-21 22:00:00+00 | 9 | 2 | 32
2021-06-21 22:00:00+00 | 12 | 2 | 163
状态为 2
的产物为 unknown
。
清理 unknown
产物
处理所有 unknown
产物的 Sidekiq worker 在极狐GitLab 15.3 及更高版本中默认启用。它分析上面数据库查询返回的产物并确定哪些应该是 locked
或 unlocked
。如果需要,产物将由该 worker 删除。
在极狐GitLab 私有化部署中可以启用该 worker:
-
启动一个 Rails 控制台。
-
检查功能是否已启用。
Feature.enabled?(:ci_job_artifacts_backlog_work)
-
如果需要,启用该功能:
Feature.enable(:ci_job_artifacts_backlog_work)
该 worker 每七分钟处理 10,000 个 unknown
产物,或者大约 24 小时内处理两百万个。
有一个相关的 ci_job_artifacts_backlog_large_loop_limit
功能标志,导致 worker 处理 unknown
产物出现问题。不建议使用该标志。
@final
产物没有从对象存储中删除
在极狐GitLab 16.1 及更高版本中,产物直接上传到其最终存储位置,即 @final
目录,而不是先使用临时位置。
极狐GitLab 16.1 和 16.2 中的一个问题导致产物没有从对象存储中删除在它们过期时。过期产物的清理过程不会从 @final
目录中移除产物。此问题在极狐GitLab 16.3 及更高版本中已修复。
运行过极狐GitLab 16.1 或 16.2 一段时间的极狐GitLab 实例管理员可能会看到产物使用的对象存储增加。按照以下步骤检查并删除这些产物。
删除文件是一个两阶段过程:
列出孤立的作业产物
{{< tabs >}}
{{< tab title=”Linux package (Omnibus)” >}}
sudo gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
{{< /tab >}}
{{< tab title=”Docker” >}}
docker exec -it <container-id> bash
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
要么写入挂载在容器中的持久卷中,要么在命令完成时将输出文件复制出会话。
{{< /tab >}}
{{< tab title=”Self-compiled (source)” >}}
sudo -u git -H bundle exec rake gitlab:cleanup:list_orphan_job_artifact_final_objects RAILS_ENV=production
{{< /tab >}}
{{< tab title=”Helm chart (Kubernetes)” >}}
# find the pod
kubectl get pods --namespace <namespace> -lapp=toolbox
# open the Rails console
kubectl exec -it -c toolbox <toolbox-pod-name> bash
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
命令完成时,将文件复制出会话到持久存储。
{{< /tab >}}
{{< /tabs >}}
Rake 任务有一些适用于所有类型极狐GitLab 部署的附加功能:
- 可以中断扫描对象存储。进度记录在 Redis 中,用于从该点继续扫描产物。
- 默认情况下,Rake 任务生成一个 CSV 文件:
/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv
-
设置环境变量以指定不同的文件名:
# Packaged GitLab sudo su - FILENAME='custom_filename.csv' gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
- 如果输出文件已经存在(默认或指定的文件),它会将条目附加到文件中。
-
每行包含
object_path,object_size
字段,以逗号分隔,没有文件头。例如:35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201
删除孤立的作业产物
{{< tabs >}}
{{< tab title=”Linux package (Omnibus)” >}}
sudo gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
{{< /tab >}}
{{< tab title=”Docker” >}}
docker exec -it <container-id> bash
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
- 命令完成时,将输出文件复制出会话,或将其写入已挂载的卷。
{{< /tab >}}
{{< tab title=”Self-compiled (source)” >}}
sudo -u git -H bundle exec rake gitlab:cleanup:delete_orphan_job_artifact_final_objects RAILS_ENV=production
{{< /tab >}}
{{< tab title=”Helm chart (Kubernetes)” >}}
# find the pod
kubectl get pods --namespace <namespace> -lapp=toolbox
# open the Rails console
kubectl exec -it -c toolbox <toolbox-pod-name> bash
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
- 当命令完成时,将文件复制出会话到持久存储。
{{< /tab >}}
{{< /tabs >}}
以下内容适用于所有类型的极狐GitLab 部署:
- 使用
FILENAME
变量指定输入文件名。默认情况下,脚本查找:/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv
- 脚本删除文件时,它会输出一个 CSV 文件,包含已删除的文件:
- 文件与输入文件位于同一目录
- 文件名前缀为
deleted_from--
。例如:deleted_from--orphan_job_artifact_final_objects.csv
。 -
文件中的行是:
object_path,object_size,object_generation/version
,例如:35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201,1711616743796587
列出具有特定过期时间(或无过期时间)产物的项目和构建
使用 Rails 控制台,你可以找到具有以下情况的作业产物的项目:
- 没有过期日期。
- 过期日期超过未来 7 天。
类似于删除产物,使用以下示例时间范围并根据需要进行更改:
7.days.from_now
10.days.from_now
2.weeks.from_now
3.months.from_now
1.year.from_now
以下每个脚本也限制搜索结果为 50 个结果,使用 .limit(50)
,但此数字也可以根据需要更改:
# Find builds & projects with artifacts that never expire
builds_with_artifacts_that_never_expire = Ci::Build.with_downloadable_artifacts.where(artifacts_expire_at: nil).limit(50)
builds_with_artifacts_that_never_expire.find_each do |build|
puts "Build with id #{build.id} has artifacts that don't expire and belongs to project #{build.project.full_path}"
end
# Find builds & projects with artifacts that expire after 7 days from today
builds_with_artifacts_that_expire_in_a_week = Ci::Build.with_downloadable_artifacts.where('artifacts_expire_at > ?', 7.days.from_now).limit(50)
builds_with_artifacts_that_expire_in_a_week.find_each do |build|
puts "Build with id #{build.id} has artifacts that expire at #{build.artifacts_expire_at} and belongs to project #{build.project.full_path}"
end
按存储的作业产物总大小列出项目
通过在 Rails 控制台中运行以下代码,列出按存储的作业产物总大小排序的前 20 个项目:
include ActionView::Helpers::NumberHelper
ProjectStatistics.order(build_artifacts_size: :desc).limit(20).each do |s|
puts "#{number_to_human_size(s.build_artifacts_size)} \t #{s.project.full_path}"
end
你可以通过修改 .limit(20)
来更改列出的项目数量为你想要的数量。
列出单个项目中最大的产物
通过在 Rails 控制台中运行以下代码,列出单个项目中 50 个最大的作业产物:
include ActionView::Helpers::NumberHelper
project = Project.find_by_full_path('path/to/project')
Ci::JobArtifact.where(project: project).order(size: :desc).limit(50).map { |a| puts "ID: #{a.id} - #{a.file_type}: #{number_to_human_size(a.size)}" }
你可以通过修改 .limit(50)
来更改列出的作业产物数量为你想要的数量。
列出单个项目中的产物
列出单个项目的产物,按产物大小排序。输出包括:
- 创建产物的作业 ID
- 产物大小
- 产物文件类型
- 产物创建日期
- 产物的磁盘位置
p = Project.find_by_id(<project_id>)
arts = Ci::JobArtifact.where(project: p)
list = arts.order(size: :desc).limit(50).each do |art|
puts "Job ID: #{art.job_id} - Size: #{art.size}b - Type: #{art.file_type} - Created: #{art.created_at} - File loc: #{art.file}"
end
要更改列出的作业产物数量,请更改 limit(50)
中的数字。
删除旧的构建和产物
{{< alert type=”warning” >}}
这些命令永久删除数据。在生产环境中运行它们之前,你应该先在测试环境中尝试,并备份实例,以便在需要时恢复。
{{< /alert >}}
删除项目的旧产物
此步骤还会删除用户选择保留的产物:
project = Project.find_by_full_path('path/to/project')
builds_with_artifacts = project.builds.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
Ci::JobArtifacts::DeleteService.new(build).execute
end
batch.update_all(artifacts_expire_at: Time.current)
end
在极狐GitLab 15.3 及更早版本中,使用以下代码代替:
project = Project.find_by_full_path('path/to/project')
builds_with_artifacts = project.builds.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
build.artifacts_expire_at = Time.current
build.erase_erasable_artifacts!
end
end
全实例删除旧产物
此步骤还会删除用户选择保留的产物:
builds_with_artifacts = Ci::Build.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
Ci::JobArtifacts::DeleteService.new(build).execute
end
batch.update_all(artifacts_expire_at: Time.current)
end
在极狐GitLab 15.3 及更早版本中,使用以下代码代替:
builds_with_artifacts = Ci::Build.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
build.artifacts_expire_at = Time.current
build.erase_erasable_artifacts!
end
end
删除项目的旧作业日志和产物
project = Project.find_by_full_path('path/to/project')
builds = project.builds
admin_user = User.find_by(username: 'username')
builds.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
print "Ci::Build ID #{build.id}... "
if build.erasable?
Ci::BuildEraseService.new(build, admin_user).execute
puts "Erased"
else
puts "Skipped (Nothing to erase or not erasable)"
end
end
end
全实例删除旧作业日志和产物
builds = Ci::Build.all
admin_user = User.find_by(username: 'username')
builds.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |build|
print "Ci::Build ID #{build.id}... "
if build.erasable?
Ci::BuildEraseService.new(build, admin_user).execute
puts "Erased"
else
puts "Skipped (Nothing to erase or not erasable)"
end
end
end
在极狐GitLab 15.3 及更早版本中,将 Ci::BuildEraseService.new(build, admin_user).execute
替换为 build.erase(erased_by: admin_user)
。
1.year.ago
是 Rails ActiveSupport::Duration
方法。从较长的时间开始,以减少意外删除仍在使用的产物的风险。根据需要重新运行删除操作,使用较短的时间段,例如 3.months.ago
、2.weeks.ago
或 7.days.ago
。
方法 erase_erasable_artifacts!
是同步的,执行时产物会立即被删除;
它们不会被安排到后台队列中。
删除产物不会立即回收磁盘空间
删除产物时,过程分为两个阶段:
-
标记为准备删除:
Ci::JobArtifact
记录从数据库中删除,并转换为具有未来pick_up_at
时间戳的Ci::DeletedObject
记录。 -
从存储中移除:产物文件保留在磁盘上,直到
Ci::ScheduleDeleteObjectsCronWorker
worker 处理Ci::DeletedObject
记录并物理删除文件。
删除是有意限制的,以防止系统资源过载:
- worker 每小时运行一次,在第 16 分钟标记。
- 它按批次处理对象,最大并发作业数为 20。
- 每个删除的对象都有一个
pick_up_at
时间戳,用于确定何时变得 符合物理删除的条件
对于大规模删除,物理清理可能需要相当长的时间 才能完全回收磁盘空间。对于非常大的删除,清理可能需要几天时间。
如果你需要快速回收磁盘空间,可以加快产物删除。
加快产物移除
如果你需要在删除大量产物后快速回收磁盘空间, 你可以绕过标准的调度限制并加快删除过程。
{{< alert type=”warning” >}}
这些命令会给系统带来很大的负担,如果你正在删除大量产物。
{{< /alert >}}
# Set the pick_up_date to the current time on all artifacts
# This will mark them for immediate deletion
Ci::DeletedObject.update_all(pick_up_at: Time.current)
# Get the count of artifacts marked for deletion
Ci::DeletedObject.where("pick_up_at < ?", Time.current)
# Delete the artifacts from disk
while Ci::DeletedObject.where("pick_up_at < ?", Time.current).count > 0
Ci::DeleteObjectsService.new.execute
sleep(10)
end
# Get the count of artifacts marked for deletion (should now be zero)
Ci::DeletedObject.count
删除旧的流水线
{{< alert type=”warning” >}}
这些命令永久删除数据。在生产环境中运行它们之前, 考虑寻求支持工程师的指导。你还应该先在测试环境中尝试它们 并备份实例,以便在需要时恢复。
{{< /alert >}}
删除流水线还会删除该流水线的:
- 作业产物
- 作业日志
- 作业元数据
- 流水线元数据
删除作业和流水线元数据有助于减少数据库中 CI 表的大小。 CI 表通常是实例数据库中最大的表。
删除项目的旧流水线
project = Project.find_by_full_path('path/to/project')
user = User.find(1)
project.ci_pipelines.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |pipeline|
puts "Erasing pipeline #{pipeline.id}"
::Ci::DestroyPipelineService.new(pipeline.project, user).execute(pipeline)
end
end
全实例删除旧流水线
user = User.find(1)
Ci::Pipeline.where("finished_at < ?", 1.year.ago).each_batch do |batch|
batch.each do |pipeline|
puts "Erasing pipeline #{pipeline.id} for project #{pipeline.project_id}"
::Ci::DestroyPipelineService.new(pipeline.project, user).execute(pipeline)
end
end
作业产物上传失败,错误 500
如果你使用对象存储来存储产物,而作业产物上传失败,请查看:
-
作业日志中的错误消息类似于:
WARNING: Uploading artifacts as "archive" to coordinator... failed id=12345 responseStatus=500 Internal Server Error status=500 token=abcd1234
-
workhorse 日志中的错误消息类似于:
{"error":"MissingRegion: could not find region configuration","level":"error","msg":"error uploading S3 session","time":"2021-03-16T22:10:55-04:00"}
在这两种情况下,你可能需要将 region
添加到作业产物的对象存储配置。
作业产物上传失败,500 Internal Server Error (Missing file)
在整合对象存储中,包含文件夹路径的存储桶名称不受支持。
例如,bucket/path
。如果存储桶名称中包含路径,你可能会收到类似以下的错误:
WARNING: Uploading artifacts as "archive" to coordinator... POST https://gitlab.example.com/api/v4/jobs/job_id/artifacts?artifact_format=zip&artifact_type=archive&expire_in=1+day: 500 Internal Server Error (Missing file)
FATAL: invalid argument
如果作业产物在使用整合对象存储时上传失败并出现上述错误,请确保你为每种数据类型使用单独的存储桶。
作业产物在使用 Windows 挂载时上传失败,FATAL: invalid argument
如果你使用 CIFS 的 Windows 挂载来存储作业产物,当 runner 尝试上传产物时,你可能会看到 invalid argument
错误:
WARNING: Uploading artifacts as "dotenv" to coordinator... POST https://<your-gitlab-instance>/api/v4/jobs/<JOB_ID>/artifacts: 500 Internal Server Error id=1296 responseStatus=500 Internal Server Error status=500 token=*****
FATAL: invalid argument
要解决此问题,你可以尝试:
- 切换到 ext4 挂载而不是 CIFS。
- 升级到至少包含许多与 CIFS 文件租约相关的重要错误修复的 Linux 内核 5.15。
- 对于较旧的内核,使用
nolease
挂载选项来禁用文件租约。
使用配额显示不正确的产物存储使用情况
有时,产物存储使用情况显示的总存储空间不正确。要为实例中的所有项目重新计算产物使用统计信息,你可以运行此后台脚本:
gitlab-rake gitlab:refresh_project_statistics_build_artifacts_size[https://example.com/path/file.csv]
https://example.com/path/file.csv
文件必须列出你希望重新计算产物存储使用情况的所有项目的项目 ID。使用此格式为文件:
PROJECT_ID
1
2
在脚本运行时,产物使用值可能会波动为 0
。重新计算后,使用情况应按预期显示。
产物下载流程图
以下流程图说明了作业产物如何工作。这些 图假设对象存储已配置为作业产物。
禁用代理下载
当proxy_download
设置为 false
时,极狐GitLab
将 runner 重定向到使用预签名 URL 从对象存储下载产物。通常情况下,runner 从源直接获取数据会更快,因此通常推荐此配置。
它还应减少带宽使用,因为数据不必被极狐GitLab 获取并发送给 runner。然而,它确实需要给予 runner 直接访问对象存储的权限。
请求流程如下所示:
在此图中:
-
首先,runner 尝试通过使用
GET /api/v4/jobs/:id/artifacts
端点获取作业产物。runner 在第一次尝试时附加direct_download=true
查询参数以指示它能够直接从对象存储下载。可以在 runner 配置中通过FF_USE_DIRECT_DOWNLOAD
功能标志禁用直接下载。此标志默认设置为true
。 -
runner 使用 HTTP 基本身份验证发送 GET 请求使用
gitlab-ci-token
用户名和自动生成的 CI/CD 作业令牌作为密码。此令牌由极狐GitLab 生成并在作业开始时提供给 runner。 -
GET 请求传递给极狐GitLab API,它在数据库中查找令牌并找到触发作业的用户。
-
在步骤 5-8 中:
-
如果用户有权访问构建,则极狐GitLab 生成一个预签名 URL 并发送 302 重定向,
Location
设置为该 URL。runner 跟随 302 重定向并下载产物。 -
如果找不到作业或用户没有权访问作业,则 API 返回 401 未授权。
如果 runner 收到以下 HTTP 状态码,则不会重试:
- 200 OK
- 401 未授权
- 403 禁止
- 404 未找到
然而,如果 runner 收到其他状态码,例如 500 错误,它会再次尝试下载产物两次,每次尝试之间间隔 1 秒。随后的尝试省略
direct_download=true
。 -
启用代理下载
如果 proxy_download
为 true
,极狐GitLab 始终从对象存储获取产物并将数据发送给 runner,即使 runner 发送 direct_download=true
查询参数。代理下载可能是可取的,如果 runner 具有受限的网络访问。
以下图类似于禁用代理下载的示例,除了在步骤 6-9 中,极狐GitLab 不会向 runner 发送 302 重定向。相反,极狐GitLab 指示 Workhorse 获取数据并将其流式传输回 runner。从 runner 的角度来看,最初的 GET 请求到 /api/v4/jobs/:id/artifacts
直接返回二进制数据。
413 Request Entity Too Large
错误
如果产物太大,作业可能会失败并出现以下错误:
Uploading artifacts as "archive" to coordinator... too large archive <job-id> responseStatus=413 Request Entity Too Large status=413" at end of a build job on pipeline when trying to store artifacts to <object-storage>.
你可能需要:
- 增加最大产物大小。
- 如果你使用 NGINX 作为代理服务器,请增加文件上传大小限制,默认限制为 1 MB。在 NGINX 配置文件中设置更高的
client-max-body-size
值。