Windows 容器无 gdipuls.dll 解决方案

最近,我将本站(pzy.io)应用转移到 Windows Container 后,上传图片 API 会发生 500 错误,通过查询容器日志快速定位到问题:

System.TypeInitializationException: The type initializer for 'Gdip' threw an exception. 
---> System.DllNotFoundException: Unable to load DLL 'gdiplus.dll' or one of its dependencies: The specified module could not be found. (0x8007007E) 
  at System.Drawing.SafeNativeMethods.Gdip.GdiplusStartup(IntPtr& token, StartupInput& input, StartupOutput& output) 
  at System.Drawing.SafeNativeMethods.Gdip..cctor() 

原因是该 API 在上传图片时会将图片添加水印,也就是说要对图片进行处理,发生了错误:[System.DllNotFoundException: Unable to load DLL 'gdiplus.dll': The specified module could not be found. ]。 但奇怪的是在我自己电脑执行是正常的,进入容器后就不正常了。

问题原因分析

我以 Unable to load DLL 'gdiplus.dll'  为关键字查詢,能找到一大堆关于.NET Core 与 Linux 相关的讨论结果,大部分提供的方法都是执行apt-get install -y libgdiplus或者brew install mono-libgdiplus,把gdiplus.dll安装进去就能解决了等等。

但我用的是 Windows 容器。找了好久才了解,原来问题是出在 Nano Server Based Image 并未包含gdiplus.dll,所以当我们以 microsoft/dotnet:3.1-aspnetcore-runtime 为 Based Image 来封装容器时,就会出现以上错误。知道原因后,我有了一个想法,是否能模仿 Linux 的作法,把gdiplus.dll封装到项目中。 

在 NuGet 上有两个微软官方放上去的 System.Drawing 组件。测试結果只是更新项目組件到较新版本的 System.Drawing,关键是找不到 gdiplus.dll  问题还是沒解决。

但为何在我电脑是正常的?通过多次查询得知,仅当在 Nano Server Based Image 时gdiplus.dll被移除了,那么意思是 Server Core Base Image 中还存在?

那就试试吧。目前微软官方 https://hub.docker.com/r/microsoft/dotnet/ 为了性能以及减少大小,都是以 Nano Server 为基础来制作 Images。并没有 Server Core 的版本,既然没有,那我就自己来做一个。

我打算把 ASP.NET Core Runtime 安装到 Server Core 基础镜像包中。除非特別理由,微软官方应用程序的镜像包通常最终以 Nano Server  为基础镜像来提供。它们分别是:

以最新的 Tag 2004 来看,nanoserver:2004 解压后约 156 MB,servercore:2004 解压后约 4.63 GB。

读者可以想象一下,从 Server Core 到 Nano Server  这中间被剔除了多少东西!常有人说:”Nano Server 单纯只是含 Network I/O 与 Disk I/O 的系统“。但这就可能造成上述无gdipuls.dll的原因。所以在这里,我需要 Windows Server Core + .NET Core Runtime 来提供 .NET Core 应用程序的图片添加水印功能。同时,以此为例,学习如何客制化自己的 Windows 基础镜像包以及一些注意事项。

客制化基础镜像包

在基础镜像中安装软件,通常希望是单纯的可执行文件,而不是setup.exe这类的会有互动画面要 [下一步] 安装步骤。就像我们开发的项目一样,最后只是把编译出来的 DLL 复制进容器一样,越单纯越好。

以安装 .NET Core 为例,在 Windows 下载区域,你可以看微软均提供 Installer 和 Binaries: 

  • Installer:有互动安装画面的安装应用程序。
  • Binaries:.zip 压缩文档,内含编译好的 .exe 可执行文件。
.NET Core 下载
.NET Core 下载

按照我的需求区分,看最终是否要执行 Web,可以选择以下两个:

  • ASP.NET Core Binaries
  • .NET Core Binaries

以目前非自动化方式,只需要下载 .NET Core Runtime 的 .zip 文件,在 Windows Server Core 基础镜像中解压 .zip 文件档,这样就算完成了。接下来只要通过docker commit就能产生出含 .NET Core Runtime 的 Windows Server Core 基础镜像包。

.NET Core Runtime 版本更新速度并不慢。如果打算可以通过 Dockerfile 来进行自动化生成含 .NET Core Runtime 的 Windows Server Core  基础镜像包,那么可以使用以下面的脚本来操作:

# escape=` 
FROM mcr.microsoft.com/windows/servercore:1809 
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] 
# Source from https://github.com/dotnet/dotnet-docker/blob/11a446f2826c2b8c51baa774584ff3f28ba0e88e/src/aspnet/3.1/nanoserver-2004/amd64/Dockerfile 
# Install ASP.NET Core Runtime 
RUN $aspnetcore_version = '3.1.7'; ` 
   Invoke-WebRequest -OutFile aspnetcore.zip https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/$aspnetcore_version/aspnetcore-runtime-$aspnetcore_version-win-x64.zip; ` 
   $aspnetcore_sha512 = 'f330c8b02699340503d4129626c0290097dc79d5d5cf97941ce9344f78de5e9bd3cba1a726b56753db0cac9db8b531c21335a3ea04dc740f09e2e5327e9f423e'; ` 
   if ((Get-FileHash aspnetcore.zip -Algorithm sha512).Hash -ne $aspnetcore_sha512) { ` 
       Write-Host 'CHECKSUM VERIFICATION FAILED!'; ` 
       exit 1; ` 
   }; ` 
   ` 
   Expand-Archive aspnetcore.zip -DestinationPath dotnet; ` 
   Remove-Item -Force aspnetcore.zip 

上述只是通过 PowerShell 的协助执行下载和解压文件,以完成 .NET Core Runtime 的安裝工作。到这里并未结束,我自己安装或设置的软件路径不会在 PATH 环境变量中,也就是说,如果这样直接发出去,那么使用者必须指定绝对路径才能调用到正确的应用程序。例如:C:\Program Files\dotnet\dotnet.exe,这将为后续使用的开发者带来不便。

对于 Server Core 为基础镜像包的 Dockerfile,只要加上一行RUN, 并通过 PowerShell 将应用程序路径加入到 PATH 变量,这样开发者不论在什么地方,都能确保调用dotnet.exe时都能正常执行。

RUN $Env:PATH = 'C:\dotnet;' + $Env:PATH; ` 
   [Environment]::SetEnvironmentVariable('PATH', $Env:PATH, [EnvironmentVariableTarget]::Machine) 

如果是 Nano Server 为基础镜像的 Dockerfile,则通过以下方式来加入应用程序路径到 PATH 环境变量,需要注意的是,Nano Server 容器需要先切换至高权限的 ContainerAdministrator 才能进行 PATH 设置。

USER ContainerAdministrator 
RUN setx /M PATH "%PATH%;C:\Program Files\dotnet" 
USER ContainerUser

综上,客制化镜像包时要注意几点:

  • 软件尽量是绿色安装较佳;
  • 如果软件就是需要setup.exe来安装,也不用怕,通常都会有提供[静态安装]的选择,可查一下文件;
  • 最好设置 PATH 环境变量。

当然,如果你直接把软件解压放在C:\ 底下来执行,跳过 PATH 设置这一步骤也可能正常执行。不过,我个人还是不建议这样做,最好按照Windows 原始设计来配置,以免出现莫名的问题。

客制化基础镜像包完整示例:https://github.com/zhiyongpeng/dockerfiles/tree/master/Windows/servercore

读者朋友也可以使用我已生成好的 aspnetcore 基础镜像, 通过docker pull zhiyongpeng/aspnetcore:<tag> 或在 Dockerfile 里 FROM 指定此镜像就能获取 Server Core + dotnet-3.x-aspnetcore-runtime 的镜像。

全文完,相关参考资料:

  1. Windows Container 之.NET CORE 找不到 gdiplus.dll 解决方案
  2. 制作 Windows Container 镜像注意事项
  3. https://github.com/kkbruce/dockerfiles-windows

《Windows 容器无 gdipuls.dll 解决方案》的相关评论

发表评论

必填项已用 * 标记,邮箱地址不会被公开。