32

Windows 容器无 gdipuls.dll 解决方案

 3 years ago
source link: https://pzy.io/archives/2020/8/windows-container-no-gdiplus.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近,我将本站(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 可执行文件。
uQbMvqm.png!mobile .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

当然,如果你直接把软件解压放在 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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK