tl;dr:可以在同一个 NuGet 缓存上同时构建多个 .NET 应用程序吗?
我有一个 Visual Studio 解决方案,其中包含要在 Docker 中托管的多个库和多个应用程序。我希望我的构建尽可能快,因此我涉及尽可能多的缓存。这是通过两种方式实现的:首先,仅将必要的 .csproj 文件复制到各自的目录中,只要我不编辑任何 .csproj,就可以缓存该层。此副本不能使用通配符,因为下一步需要 BuildKit。
然后在每个
--mount=type=cache,id=nuget,target=/root/.nuget/packages
指令之前使用 RUN
,以便在构建之间在主机上拥有可重用的 NuGet 缓存:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app
# Copy all required (transitive) project references
COPY src/SampleApp.Common/SampleApp.Common.csproj ./SampleApp.Common/SampleApp.Common.csproj
COPY src/SampleApp.Data/SampleApp.Data.csproj ./SampleApp.Data/SampleApp.Data.csproj
# Then copy ourselves
COPY src/SampleApp.Api/SampleApp.Api.csproj ./SampleApp.Api/SampleApp.Api.csproj
# Restore as distinct layers
# Mount NuGet cache dir as cache in Docker for faster subsequent builds.
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet restore SampleApp.Api --runtime linux-x64
# Copy all required (transitive) project sources
COPY src/SampleApp.Common ./SampleApp.Common
COPY src/SampleApp.Data ./SampleApp.Data
# Then copy ourselves
COPY src/SampleApp.Api SampleApp.Api
# Build and publish the release (needs cached packages to copy on build)
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
dotnet publish SampleApp.Api \
--no-restore \
--runtime linux-x64 \
--self-contained false \
--configuration Release \
--output ./Publish/SampleApp.Api/
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/Publish/SampleApp.Api .
ENTRYPOINT ["dotnet", "SampleApp.Api.dll"]
现在这可行了,尽管维护较大项目的 Dockerfile 变得很麻烦(添加新的库项目需要在多个 Dockerfile 中编辑多行),但是当需要同时构建多个项目时就会出现问题。
命令:
docker compose up -d --build
一段时间后,显示多个带有这样的 Dockerfile 的项目:
=> 错误 [sampleapp_api build-env 16/30] RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages dotnet Restore SampleApp.Api --runtime linux-x64
#12 34.56 /usr/share/dotnet/sdk/6.0.302/NuGet.targets(130,5):错误:找不到文件“/root/.nuget/packages/microsoft.aspnetcore.app.runtime.linux” -x64/6.0.7/fgp1y1vi.2tu'。 [/app/SampleApp.Api/SampleApp.Api.csproj]
第二次运行时,两个项目同时失败:
/usr/share/dotnet/sdk/6.0.302/NuGet.targets(130,5):错误:找不到文件“/root/.nuget/packages/runtime.any.system.runtime.interopservices/4.1”。 0/q0z4zwua.tf3 /usr/share/dotnet/sdk/6.0.302/NuGet.targets(130,5):错误:找不到文件'/root/.nuget/packages/runtime.any.system.runtime.interopservices/4.1.0 /xpvko1tv.2o3
可能是因为third项目正在那个时间恢复该库。这是因为多个构建使用相同的 NuGet 缓存:
--mount=type=cache,id=nuget,target=/root/.nuget/packages
并且由于 NuGet 在每个构建上使用随机文件名提取包的内容,从而清除其中已有的内容,因此在并行构建中,一个构建会清除另一个构建的 NuGet 缓存,从而导致上述构建(恢复)错误。
现在有几种解决方法,但我都不喜欢:
--mount=type=cache,id=nuget-sampleapp-api
、...,id=nuget-sampleapp-web
、...)。这将炸毁磁盘上的缓存,并且一些 Docker 构建将很高兴地消耗掉几 GB 的空间。 NVMe 空间很昂贵。docker compose build sampleapp_api
,... sampleapp_web
,...)。每次更新共享项目时,我都必须对每个应用程序映像执行此操作。或者编写脚本。更多的脚本,更多的维护,更多的非标准构建步骤,不喜欢它。它超越了并行构建的目的。还有其他建议吗?
在与这个确切的问题斗争了很长时间之后,我终于弄清楚了为什么这不起作用以及如何解决它。
此评论引导我找到解决方案:https://github.com/NuGet/Home/issues/7060#issuecomment-732065148
因此,如果正在运行并发恢复操作,这些操作使用不同的临时路径,但共享相同的全局包缓存,则缓存根本不受锁定机制的真正保护
NuGet 使用临时文件夹 (
/tmp/NuGetScratch
),您可以在此处阅读:https://learn.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache -folders 安装此临时目录以及全局包缓存可以修复问题。
确切的问题似乎是 NuGet 使用临时目录作为恢复进程之间的锁定机制,安装临时目录允许在从 Docker 并行恢复时发挥作用。
此时,我在进行 NuGet 恢复时只需像这样安装所有 NuGet 缓存文件夹:
RUN \
--mount=type=cache,target=/root/.nuget/packages \
--mount=type=cache,target=/root/.local/share/NuGet/v3-cache \
--mount=type=cache,target=/root/.local/share/NuGet/plugins-cache \
--mount=type=cache,target=/tmp/NuGetScratch \
dotnet restore
为了避免竞争条件,我们需要缓存 nuget 使用的所有文件夹。要找出 nuget 正在使用哪些文件夹,请运行
dotnet nuget locals all --list
。在 Linux Docker 容器中,运行 docker run -it mcr.microsoft.com/dotnet/sdk:6.0.414-jammy dotnet nuget locals all --list
时,您将得到以下结果:
http-cache: /root/.local/share/NuGet/http-cache
global-packages: /root/.nuget/packages/
temp: /tmp/NuGetScratchroot
plugins-cache: /root/.local/share/NuGet/plugin-cache
这些是需要缓存的路径。
在我的构建脚本中,我以防御性的方式解决了这个问题。我使用 ENV 设置了控制上述路径的环境变量。我们还挂载整个 /root/.local/NuGet 文件夹。最终结果如下所示:
ENV NUGET_PACKAGES=/root/.nuget/packages
ENV NUGET_HTTP_CACHE_PATH=/root/.local/share/NuGet/v3-cache
ENV NUGET_SCRATCH=/tmp/NuGetScratchroot
ENV NUGET_PLUGINS_CACHE_PATH=/root/.local/share/NuGet/plugins-cache
RUN \
--mount=type=cache,target=/root/.nuget/packages \
--mount=type=cache,target=/root/.local/share/NuGet \
--mount=type=cache,target=/tmp/NuGetScratch \
dotnet restore