解决 Alpine Linux 上的 .NET Core 原生库加载问题
背景:.NET 10 需要 Alpine 3.17+
我们在为 Datadog .NET 追踪器添加 .NET 10 临时支持时遇到了这个问题。.NET 10 不再支持我们之前使用的 alpine:3.14 版本。
在 alpine:3.14 上运行 .NET 10 会在运行时失败,出现以下错误:
|
|
我们随后确认这是预期的:.NET 10 更新了其支持矩阵,现在需要 Alpine 3.17 或更高版本。在确认可以更新基础镜像而不会破坏任何内容后,我们将构建和测试镜像更新为使用 alpine:3.17。这时我们开始遇到本文重点讨论的问题。
问题:无法加载共享库 ’e_sqlite3'
作为 CI 测试的一部分,我们运行数百个不同的示例应用程序,涵盖各种包版本组合和目标框架。更新到新版本的 Alpine 后,我们开始在一个特定应用程序 Samples.Microsoft.Data.Sqlite 中遇到问题。
这个问题出现在两个特定的目标框架中:netcoreapp3.1 和 net5.0,并出现以下错误:
|
|
这个应用程序使用 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 库的路径:
|
|
这表明 SQLite 库仅链接到 musl 的 libc,并且该库存在。所以这意味着问题可能不是由于缺少依赖项。
使用 LD_DEBUG 和 LD_LIBRARY_PATH 调试原生库加载问题
接下来我尝试按照错误消息的建议操作:
|
|
LD_DEBUG 变量是 Linux 动态链接器的一个功能,允许转储有关其运行方式的信息。这对调试很有用,你可以传递很多选项给它。在下面的示例中,我使用了 libs 选项,它显示原生库搜索路径:
|
|
不幸的是,使用 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 库所在的目录:
|
|
果然,它工作了!示例成功找到了 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
,应用程序完美运行:
|
|
问题解决了!所以解决方案最终非常简单,但我认为描述我们缩小问题范围的过程是值得的。也许它会帮助那些(像我一样)忘记了这是怎么回事的人😅
总结
在本文中,我逐步介绍了我们如何解决在 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。