解决 Alpine Linux 上的 .NET Core 原生库加载问题

本文详细描述了在Alpine Linux 3.17上运行.NET Core 3.1和.NET 5应用时遇到的e_sqlite3原生库加载问题,通过ldd、LD_DEBUG等工具逐步排查,最终发现是运行时ID解析问题,并通过设置DOTNET_RUNTIME_ID环境变量解决。

解决 Alpine Linux 上的 .NET Core 原生库加载问题

背景:.NET 10 需要 Alpine 3.17+

我们在为 Datadog .NET 追踪器添加 .NET 10 临时支持时遇到了这个问题。.NET 10 不再支持我们之前使用的 alpine:3.14 版本。

在 alpine:3.14 上运行 .NET 10 会在运行时失败,出现以下错误:

1
2
3
4
5
Failed to load /usr/share/dotnet/host/fxr/10.0.0-preview.5.25277.114/libhostfxr.so, error:
Error relocating /usr/share/dotnet/host/fxr/10.0.0-preview.5.25277.114/libhostfxr.so: 
_ZSt28__throw_bad_array_new_lengthv: symbol not found

The library libhostfxr.so was found, but loading it from /usr/share/dotnet/host/fxr/10.0.0-preview.5.25277.114/libhostfxr.so failed

我们随后确认这是预期的:.NET 10 更新了其支持矩阵,现在需要 Alpine 3.17 或更高版本。在确认可以更新基础镜像而不会破坏任何内容后,我们将构建和测试镜像更新为使用 alpine:3.17。这时我们开始遇到本文重点讨论的问题。

问题:无法加载共享库 ’e_sqlite3'

作为 CI 测试的一部分,我们运行数百个不同的示例应用程序,涵盖各种包版本组合和目标框架。更新到新版本的 Alpine 后,我们开始在一个特定应用程序 Samples.Microsoft.Data.Sqlite 中遇到问题。

这个问题出现在两个特定的目标框架中:netcoreapp3.1 和 net5.0,并出现以下错误:

1
2
3
4
5
6
7
8
9
Unhandled exception. System.DllNotFoundException: Unable to load shared library 'e_sqlite3' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: Error loading shared library libe_sqlite3: No such file or directory
    at SQLitePCL.SQLite3Provider_e_sqlite3.NativeMethods.sqlite3_libversion_number()
    at SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
    at SQLitePCL.raw.SetProvider(ISQLite3Provider imp)
    at SQLitePCL.Batteries_V2.Init()
    at SQLitePCL.Batteries.Init()
    at Samples.Microsoft.Data.Sqlite.Program.OpenConnection() in D:\a\_work\1\s\tracer\test\test-applications\integrations\Samples.Microsoft.Data.Sqlite\Program.cs:line 29
    at Samples.Microsoft.Data.Sqlite.Program.Main() in D:\a\_work\1\s\tracer\test\test-applications\integrations\Samples.Microsoft.Data.Sqlite\Program.cs:line 17
    at Samples.Microsoft.Data.Sqlite.Program.<Main>()

这个应用程序使用 Microsoft.Data.Sqlite 包,该包传递引用各种 SQLite 包。问题是:为什么库无法加载 libe_sqlite3?为什么它只影响 .NET Core 3.1 和 .NET 5?

缩小问题范围

解决这个问题的一个困难是我们同时改变了多个变量:我们将基础镜像更新为 alpine:3.17,并且我们也在使用 .NET 10 预览版 SDK 进行构建。

我们怀疑更新后的 Alpine 基础镜像是问题所在。可能的原因有很多,比如缺少原生依赖项,但在缩小焦点之前,我们想将问题隔离到 Alpine。

为了确认我们的怀疑,我们使用了从主分支 CI 中获取的示例构建,并在 alpine:3.14 和 alpine:3.17 上运行它,没有附加 .NET 追踪器,针对 .NET Core 3.1 运行。

结果是:

  • 在 alpine:3.14 上,应用程序运行没有问题
  • 在 alpine:3.17 上,应用程序崩溃,出现"无法加载共享库 ’e_sqlite3’ 或其依赖项之一"

好的,所以问题肯定是新的 alpine:3.17 镜像。现在尝试理解为什么这是个问题。

使用 ldd 检查缺失的依赖项

此时我们知道应用程序本身是正确的,并且 libe_sqlite3 存在且位于正确的位置,因为它在 alpine:3.14 上工作。那么为什么库无法加载呢?

我首先在 alpine:3.17 项目中运行 ldd(列出动态依赖项)命令,传入 SQLite 库的路径:

1
2
3
$ ldd ./runtimes/linux-musl-x64/native/libe_sqlite3.so
  /lib/ld-musl-x86_64.so.1 (0x741dab2ae000)
  libc.so => /lib/ld-musl-x86_64.so.1 (0x741dab2ae000)

这表明 SQLite 库仅链接到 musl 的 libc,并且该库存在。所以这意味着问题可能不是由于缺少依赖项。

使用 LD_DEBUG 和 LD_LIBRARY_PATH 调试原生库加载问题

接下来我尝试按照错误消息的建议操作:

1
为了帮助诊断加载问题,请考虑设置 LD_DEBUG 环境变量

LD_DEBUG 变量是 Linux 动态链接器的一个功能,允许转储有关其运行方式的信息。这对调试很有用,你可以传递很多选项给它。在下面的示例中,我使用了 libs 选项,它显示原生库搜索路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ LD_DEBUG=libs ls
295:	find library=libselinux.so.1 [0]; searching
295:	 search cache=/etc/ld.so.cache
295:	  trying file=/lib/x86_64-linux-gnu/libselinux.so.1
295:	
295:	find library=libc.so.6 [0]; searching
295:	 search cache=/etc/ld.so.cache
295:	  trying file=/lib/x86_64-linux-gnu/libc.so.6
295:	
295:	find library=libpcre2-8.so.0 [0]; searching
295:	 search cache=/etc/ld.so.cache
295:	  trying file=/lib/x86_64-linux-gnu/libpcre2-8.so.0
295:	
295:	calling init: /lib64/ld-linux-x86-64.so.2
295:	calling init: /lib/x86_64-linux-gnu/libc.so.6
295:	calling init: /lib/x86_64-linux-gnu/libpcre2-8.so.0
295:	calling init: /lib/x86_64-linux-gnu/libselinux.so.1
295:	initialize program: ls
295:	transferring control: ls
295:	

不幸的是,使用 LD_DEBUG=libs dotnet Samples.Microsoft.Data.Sqlite.dll 运行我们的应用程序没有显示任何有趣的信息。这是因为 libe_sqlite.so 原生库不是作为动态依赖项加载的,所以 LD_DEBUG 没有给我们任何信息。相反,SQLite 库是由 .NET 运行时显式加载的,所以是运行时无法加载该库。

另一个用于调试链接问题的有用工具是 strace,它提供对所有系统调用的洞察。

接下来我尝试的是显式设置 LD_LIBRARY_PATH 以包含 libe_sqlite.so 的路径。LD_LIBRARY_PATH 是一组额外的路径,用于搜索动态链接的库,除了标准位置之外。作为一种 hack,我尝试设置该变量以包含 SQLite 库所在的目录:

1
2
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/project/runtimes/linux-musl-x64/native/ \
  dotnet Samples.Microsoft.Data.Sqlite.dll

果然,它工作了!示例成功找到了 libe_sqlite.so 库,并正确运行!所以在这一点上,我们相当确定这纯粹是 .NET 在 alpine:3.17 上运行时找不到原生库的问题,而不是加载库本身的问题。

根本原因:.NET 运行时 ID 解析

那么这里发生了什么?在这一点上,我的最佳猜测本质上是 .NET Core 3.1 和 .NET 5 根本不支持 alpine:3.17,基于以下事实:

  • Alpine 3.17 直到 2022 年才发布
  • .NET Core 3.1 于 2019 年发布,.NET 5 于 2020 年发布

更重要的是,Microsoft 为 alpine:3.17 添加了 dockerfile,但只针对 .NET 6+ 添加。Microsoft 从未更新 .NET Core 3.1 镜像以使用 alpine:3.17,即使他们当时仍在更新 3.1。

所以即使 .NET Core 3.1 和 .NET 5 正式支持 Alpine 3.13+,似乎原生库查找规则在 Alpine 3.17 上被破坏了。

这基本上总结了问题:对于 .NET Core 3.1 和 .NET 5,在 Alpine 3.17+ 上的原生库查找规则被破坏了。

我实际上忘记了在早期版本的 .NET Core 中,有硬编码的列表将像 alpine:3.17 这样的发行版名称映射到运行时 ID,比如 linux-musl-x64。如果映射缺失,那么 .NET 不知道使用哪个运行时 ID,而不是选择正确的 linux-musl-x64 运行时 ID,它会回退到 linux-x64。选择错误的运行时 ID 就是应用程序之前失败的原因。

这个由于缺少运行时 ID 条目导致在 Alpine 上原生失败的问题实际上是一个反复出现的问题:

  • 为 Alpine 3.15 添加 RID
  • 为 Alpine 3.16 添加 RID
  • 为 alpine-3.17 + alpine-{armv6,x86,s390x,ppc64le} 添加 RID
  • 为 Alpine 3.18 添加 RID

最终,为 Alpine 添加了一个新的回退,明确为未知的 Alpine 版本使用 linux-musl-x64 工件,所以这对 .NET 7+ 不应该是一个问题。

解决方案:DOTNET_RUNTIME_ID

好的,现在我们理解了问题发生的原因。但是我们如何修复它呢?

幸运的是,.NET 主机允许通过环境变量 DOTNET_RUNTIME_ID 显式设置运行时 ID。如果设置了此变量,运行时会优先使用它而不是通常的回退,所以即使这些旧的运行时版本也可以在较新版本的 Alpine 上工作。

所以在这种情况下,我们可以设置 DOTNET_RUNTIME_ID=linux-musl-x64,应用程序完美运行:

1
$ DOTNET_RUNTIME_ID=linux-musl-x64 dotnet Samples.Microsoft.Data.Sqlite.dll

问题解决了!所以解决方案最终非常简单,但我认为描述我们缩小问题范围的过程是值得的。也许它会帮助那些(像我一样)忘记了这是怎么回事的人😅

总结

在本文中,我逐步介绍了我们如何解决在 Alpine Linux 3.17 上运行 .NET Core 3.1 和 .NET 5 时的错误:无法加载共享库 ’e_sqlite3’ 或其依赖项之一。

我描述了问题本身——无法加载 SQLite 原生库——问题发生的环境,我们尝试隔离问题的事情,最终的根本原因,以及我们如何解决它。

最终,我们将问题追溯到 .NET 运行时中的一个回退路径,该路径会导致运行时在较新版本的 Alpine 上使用 linux-x64 运行时 ID 而不是所需的 linux-musl-x64。这个回退路径在较新版本的 .NET 中已修复,但在旧版本中,你必须通过设置 DOTNET_RUNTIME_ID=linux-musl-x64 来强制运行时使用正确的运行时 ID。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计