我们有一个想要升级的 Debian 软件包。它会安装一组 Java jar,这些 jar 作为 systemd 服务运行。
软件包可以在运行时升级吗?我的理解是,是的,这是可能的。
这是如何工作的?整个包是否加载到内存中,然后在仍在内存中运行时替换 jar?
为什么我可以在该软件包仍在运行时升级它?
答案1
在 Unix 世界中,文件系统将文件名和文件内容分开,只要您有适当的权限,删除文件名总是允许的。当没有更多引用(文件名和文件句柄)时,文件内容将被删除。
程序dpkg
不知道或不关心正在运行的服务。升级软件包的工作方式如下:
- 解压新文件,并附加到
.dpkg-new
文件名 - 为旧文件创建硬链接,附加后缀
.dpkg-old
(这将创建额外的引用) - 将新文件重命名为正确的名称,覆盖旧文件名的引用。
- 删除带有后缀的文件
.dpkg-old
。任何不再引用的文件内容都将在此删除。
这减少了出现错误的可能性,因为仅在第三步中触及未加后缀的文件名,而步骤 1 和 2 最有可能出现错误(例如磁盘已满),未加后缀的文件名始终引用旧名称或新名称,并且包不一致的时间保持在最短。
对于正在运行且保持文件打开的服务,这意味着它们将继续使用旧文件内容,但如果它们关闭并重新打开文件,它们将获得新内容。
对于传统的 Unix 服务来说,这不是一个大问题,因为它们主要由可执行文件和共享库组成,所有这些都保持开放,因为它们是映射到进程地址空间的内存中,所以升级软件包不会对它们造成丝毫干扰。
对于 Java 程序,它们是否受此影响取决于 Java 解释器是否在升级期间关闭并重新打开 JAR 文件。通常,打包者会采取安全措施,在升级之前关闭服务,然后在升级之后重新启动,而不是冒险让服务将不同版本的类加载到同一个服务实例中。
所以答案是:这取决于服务和 JVM。
Java 代码通常是加载的而不是内存映射的,因此通常不需要保持文件句柄打开,并且正在运行的服务将使用它最初加载的代码,而不关心更改的 JAR 文件。
不过,它通常不会将整个 JAR 存档加载到内存中,而只会在使用时复制单个类的代码。通常,此代码随后会通过 JIT 编译器运行,服务会在编译后的版本上运行,因此内存中的代码与磁盘上的版本没有任何相似之处。
理想情况下,JVM 会将它们使用的 JAR 文件映射到它们的地址空间,这将保持对该文件的引用打开并与进程关联,这既允许它们从一组一致的 JAR 文件中加载类,又允许在升级后汇总哪些服务正在使用过时的文件来工作。
此报告是 systemd 服务单元生成的进程列表,这些进程具有指向没有名称的文件的句柄(因此一旦关闭所有这些句柄,其内容将被删除)。其准确性取决于实际保持文件句柄打开的进程(对于 C 程序而言,这是因为文件被映射到进程地址空间中)。
如果你想查看服务进程(或任何进程)当前打开了哪些文件,请使用lsof(8)命令来获取列表。
总结:对于 Java 来说,如果您的服务不会一直加载和卸载类,那么它基本上是安全的(在这种情况下,该服务有什么问题?)。
答案2
当您升级当前正在运行的服务时,位于内存中的旧版本不会同时升级。旧版本将继续在内存中运行,直到停止或重新启动。下次运行服务时,它会将升级后的版本加载到内存中。
至于为什么允许你这样做,嗯......这是开发人员的问题。如果 dpkg/apt 在升级软件包时提醒用户哪些服务正在运行,那当然会很好。我知道如果 dpkg 需要重新启动服务,它会提醒用户,但如果服务已经在运行,它就不会提醒用户。