使用最佳实践部署Java Web应用程序以实现最小停机时间?

58

当部署一个大的Java网络应用程序(> 100 MB .war)时,我目前使用以下部署过程:

  • 在开发机上展开应用程序.war文件。
  • 从开发机器rsync:ed扩展的应用程序到现场环境。
  • 在rsync之后重新启动现场环境中的应用程序服务器。尽管这一步骤并不是必需的,但我发现在部署时重启应用程序服务器可以避免由于频繁的类加载而导致“java.lang.OutOfMemoryError:PermGen space”。

该方法的优点:

  • rsync减少了从开发机器到现场环境发送的数据量。上传整个.war文件需要超过十分钟,而rsync只需几秒钟。

该方法的缺点:

  • rsync正在运行时应用程序上下文被重启,因为文件已更新。理想情况下,在rsync完成后应该发生重新启动,而不是在其仍在运行时。
  • 应用程序服务器的重新启动会导致约两分钟的停机时间。

我希望找到具有以下特性的部署过程:

  • 部署过程中最小化停机时间。
  • 最小化数据上传所需的时间。
  • 如果部署过程是应用程序服务器特定的,则应用程序服务器必须是开源的。

问题:

  • 给出所述要求,最优部署过程是什么?

3
我认为这应该是一个“社区维基”。 - Nathan Campos
12
Nathan:为什么?这是一个我需要答案的技术问题。也许我没有理解“社区wiki”周围的某些规则。 - knorv
3
只是出于好奇,你的Web应用程序中有什么东西如此沉重? - Pascal Thivent
3
Pascal Thivent说:Grails加上静态文件(图形)和一些外部依赖项很快就累积到了超过100 MB。 - knorv
1
knorv,你尝试过在服务器JVM上调整内存/永久代空间了吗? - James McMahon
18个回答

31

更新:

自从这篇答案第一次写出来以后,一个更好的方法已经出现了,可以在不停机的情况下将war文件部署到tomcat上。在最近的版本中,你可以在war文件名中包含版本号。例如,你可以同时部署文件ROOT##001.warROOT##002.war到同一个上下文中。在##之后的所有内容都被tomcat解释为版本号,而不是上下文路径的一部分。Tomcat将保持您应用程序的所有版本运行,并为新请求和会话提供服务,对于已完全启动的最新版本,它将优雅地完成旧请求和会话。版本号也可以通过tomcat管理器甚至catalina ant任务来指定。更多信息请点击此处

原始答案:

Rsync在压缩文件上往往效果不佳,因为其增量传输算法会查找文件中的更改,而未压缩文件的微小更改可能会大幅改变结果压缩版本。因此,如果网络带宽成为瓶颈,使用rsync传输未压缩的war文件而不是压缩版本可能是明智的选择。

使用Tomcat管理器应用程序进行部署有什么问题吗?如果你不想直接从远程位置将整个war文件上传到Tomcat管理器应用程序中,你可以将其(由于上述原因未压缩)rsync到生产环境中的占位符位置,重新打包为war文件,然后在本地将其交给管理器。Tomcat带有一个很好的ant任务,允许您使用Tomcat管理器应用程序进行脚本化部署。

你没有提到你的方法中存在的另一个缺陷:当你的应用程序部分部署(在rsync操作期间)时,你的应用程序可能处于不一致状态,其中更改的接口可能不同步,新/更新的依赖项可能不可用等。此外,根据你的rsync作业所需的时间长短,你的应用程序可能实际上会多次重启。你知道你可以和应该关闭Tomcat中监听文件变化并重启的行为吗?这实际上不推荐用于生产系统。你可以始终使用Tomcat管理器应用程序手动或ant脚本重启应用程序。

当然,在重新启动期间,你的应用程序将对用户不可用。但是,如果你非常关心可用性,你肯定在负载均衡器后面拥有冗余的Web服务器。在部署更新的war文件时,你可以将负载均衡器暂时发送所有请求到其他Web服务器,直到部署完成。对于其他Web服务器,请重复以上步骤。


2
问题在于:未压缩文件中微小的本地更改可能导致压缩文件中非常大的差异,即如果网络带宽是瓶颈,并且通常存在许多文件的小差异,则rsync将不得不传输更多数据,这可能导致总体速度较慢的结果。 - Michael Borgwardt
1
@knorv:你说的可能是对的。虽然rsync使用增量传输算法(http://samba.anu.edu.au/ftp/rsync/rsync.html),但压缩往往会改变整个文件的结构,这使得rsync的增量传输算法有些无效(http://zsync.moria.org.uk/paper200501/ch01s03.html)。如果选择在rsync之前解压缩文件,则至少要使用-z选项,告诉rsync在传输之前压缩数据。 - Asaph
@knorv:你在使用rsync时是否带有“-z”选项?我认为这会对你有所帮助。 - Asaph
3
通过使用网络解决停机时间的做法得到了加分。是的,这意味着将新版本发布到生产需要更长时间,但如果最小化停机时间很重要,这是唯一可行的方法。您甚至可以在同一主机上的不同端口上启动新版本作为单独的tomcat进程,然后切换网络流量以连接到该端口,并在旧版本的连接关闭后关闭旧版本。当然,这对于进程崩溃或机器死机的情况没有帮助。 - Zac Thompson
@Asaph 感谢您提供的更新信息。在相同的方式下,版本化Maven构建的最佳方法是什么? - gkiko
显示剩余10条评论

20
已经注意到,使用rsync在推送更改到WAR文件时效果不佳。原因是,WAR文件本质上是ZIP文件,并且默认情况下创建时会对成员文件进行压缩。在未压缩之前对成员文件进行的小更改会导致ZIP文件中的大规模差异,从而使rsync的增量传输算法无效。
一个可能的解决方案是使用`jar -0...`创建原始的WAR文件。`-0`选项告诉`jar`命令在创建WAR文件时不要压缩成员文件。当`rsync`比较WAR文件的新旧版本时,增量传输算法应该能够创建小的差异。然后安排rsync以压缩形式发送差异(或原始文件);例如使用`rsync -z...`或在其下使用压缩数据流/传输。
编辑:根据WAR文件的结构如何,也可能需要使用`jar -0...`创建组件JAR文件。这适用于经常发生更改(或仅是重新构建)的JAR文件,而不是稳定的第三方JAR文件。
理论上,此过程应比发送常规WAR文件有显着的改进。实际上我没有尝试过,所以我不能保证它会起作用。
缺点是部署的WAR文件将显着变大。这可能会导致更长的Web应用程序启动时间,但我认为其影响会很小。
完全不同的方法是查看WAR文件,看看是否可以确定可能(几乎)永远不会更改的库JAR。将这些JAR从WAR文件中取出,并将它们单独部署到Tomcat服务器的`common/lib`目录中;例如使用`rsync`。

6
把库移动到共享目录的一个巨大问题是,如果它们持有 Web 应用程序内对象的引用,则会防止 JVM 释放 Web 应用程序使用的空间,从而导致 PermGen 耗尽。请注意,必须确保在将库移到共享目录之前,没有任何库持有对 Web 应用程序内对象的引用。 - kdgregory
但是,如果共享库没有静态变量来保存对Web应用程序对象的引用,那么第二种方法就可以了,对吗? - Stephen C
当然。但是你怎么知道呢?例如,JDK的Introspector类缓存类定义,这意味着如果你从Web应用程序中使用它,你必须在重新部署时显式地清除缓存。但是如果你的共享编组库在底层使用Introspector呢? - kdgregory
“但是你怎么知道呢?”通过手动或自动检查代码。 (编写一个实用程序来检查JAR文件中的类是否存在潜在问题的静态变量是可行的。) - Stephen C

13

在任何需要考虑停机时间的环境中,您肯定会运行某种服务器集群以通过冗余增加可靠性。我会将其中一台主机从集群中取出,进行更新,然后再将其放回到集群中。如果您有无法在混合环境中运行的更新(例如需要在数据库上进行不兼容模式更改),则必须至少暂时关闭整个站点。关键是先启动替换进程,然后再停止原始进程。

以Tomcat为例-您可以使用CATALINA_BASE来定义一个目录,其中包含所有Tomcat工作目录,与可执行代码分开。每次部署软件时,我都会部署到一个新的基础目录,以便将新代码驻留在旧代码旁边的磁盘上。然后,我可以启动指向新基础目录的另一个Tomcat实例,启动和运行所有内容,然后将负载平衡器中的旧进程(端口号)与新进程交换。

如果我担心在切换过程中保留会话数据,我可以设置系统,使得每个主机都有一个同伴,用于复制会话数据。我可以删除其中一个主机,对其进行更新,然后将其重新启动,以便它重新获取会话数据,然后切换这两个主机。如果集群中有多个成对的主机,我可以删除所有成对主机中的一半,然后进行批量切换,或者可以一个成对一个成对地执行,具体情况取决于发布要求、企业要求等。个人而言,我更喜欢允许最终用户偶尔失去活动会话,而不是试图在会话保持完整的情况下进行升级。

在 IT 基础设施、发布流程复杂性和开发人员工作量之间进行权衡。如果您的集群足够大且您的愿望足够强烈,那么很容易设计一个系统,可以在大多数更新中实现零停机时间切换。由于更新后的软件通常无法适应旧模式,因此大型模式更改通常会强制实际停机时间,而且您可能无法通过将数据复制到新的数据库实例、执行模式更新然后将服务器切换到新的数据库来摆脱这种情况,因为在新的数据库从旧数据库克隆之后写入旧数据库的任何数据都将被忽略。当然,如果您有资源,您可以让开发人员修改新应用程序以使用所有更新的表名称,并在现有数据库上放置触发器,以便正确地根据先前版本写入旧表的数据更新新表(或者可能使用视图来模拟来自另一个架构的一个方案)。启动新的应用程序服务器并将它们插入集群中。如果您有开发资源可供使用,则可以采取许多措施来最小化停机时间。

也许减少软件升级停机时间最有用的机制是确保您的应用程序可以在只读模式下运行。这将为您的用户提供一些必要的功能,但会让您具备进行需要数据库修改等系统全局更改的能力。将您的应用程序置于只读模式,然后克隆数据、更新模式、启动新的应用程序服务器对新的数据库进行操作,然后切换负载均衡器以使用新的应用程序服务器。您唯一需要的停机时间就是切换到只读模式所需的时间和修改负载均衡器配置所需的时间(大部分负载均衡器可以在没有任何停机时间的情况下处理它)。


为了给这个答案添加一些更新的信息...Tomcat可以将会话持久化到数据库。此外,使用负载均衡技术进行热交换到新版本有时被称为蓝绿部署 - Pixelstix

10

我的建议是使用rsync带有爆炸版本,但部署一个war文件。

  1. 在生产环境中创建临时文件夹,其中包含Web应用程序的爆炸版本。
  2. 同步爆炸版本。
  3. 在生产环境机器的临时文件夹中创建war文件。
  4. 使用临时文件夹中的新文件替换服务器部署目录中的旧文件。

在JBoss容器(基于Tomcat)中推荐使用将旧war文件替换为新war文件,因为它是原子和快速操作,并确保当部署人员启动整个应用程序时,应用程序完全处于已部署状态。


这样做可以避免我对楼主的做法最担心的问题,即非原子更新。 - kdgregory
是的,在开发模式下,展开版本和热部署很好,但在生产环境中最好使用WAR包。 - cetnar

8

您是否可以在Web服务器上制作当前Web应用程序的本地副本,将其同步到该目录,然后甚至使用符号链接,在一个“步骤”中将Tomcat指向新的部署,而不会有太多停机时间?


4

2
JavaRebel现在被称为JRebel。 - Thierry Roy
2
对于使用JRebel技术进行生产级更新,有一个名为LiveRebel的工具。 - toomasr

4
您将提取的war文件同步的方法很好,同时重新启动也是不错的选择,因为我认为生产服务器不应启用热部署。所以,唯一的缺点就是需要重启服务器时会有停机时间,对吗?
我假设您的应用程序的所有状态都保存在数据库中,因此您没有问题,可以让一些用户在一个应用程序服务器实例上工作,而另一些用户则在另一个应用程序服务器实例上工作。如果是这样,
运行两个应用程序服务器:启动第二个应用程序服务器(它监听其他TCP端口),并在那里部署您的应用程序。部署后,更新Apache httpd的配置(mod_jk或mod_proxy)以指向第二个应用程序服务器。优雅地重新启动Apache httpd进程。这样,您将没有停机时间,并且新的用户和请求会自动重定向到新的应用程序服务器。
如果您可以利用应用程序服务器的集群和会话复制支持,即使是当前已登录的用户,第二个应用程序服务器启动后也会平稳过渡,因为第二个应用程序服务器会在启动时立即进行重新同步。然后,在第一个服务器没有访问时,关闭它。

4

这取决于您的应用程序架构。

我的一个应用程序位于负载均衡代理后面,在那里我执行分阶段部署 - 有效消除了停机时间。


1
+1. 这是我们使用的解决方案。只需稍微动一下脑筋,您就可以确保运行混合版本 N 和版本 N-1 的服务器集群能够正常运行。然后只需将其中一个服务器离线,升级它,然后将其重新上线。运行一段时间以确保没有问题,然后对另一半服务器执行相同的操作。这样运行几天,以便您有一个回退位置,然后转换其余部分。 - paxdiablo

2
如果静态文件是您的大型WAR的一个重要组成部分(100Mo相当大),那么将它们放在WAR之外,并在应用服务器前部署在Web服务器(如Apache)上可能会加快速度。此外,Apache通常比Servlet引擎更擅长提供静态文件服务(即使大多数Servlet引擎在这方面取得了显著进展)。
因此,不要生成一个臃肿的WAR,而是进行瘦身处理,生成:
- 一个包含Apache静态文件的大型ZIP - 一个较小的Servlet引擎WAR
可选地,在使WAR变得更轻的过程中更进一步:如果可能的话,请在应用程序服务器级别部署不经常更改的Grails和其他JAR。
如果成功制作出轻量级的WAR,我不会费心去同步目录而不是存档文件。
这种方法的优点:
1. 静态文件可以在Apache上进行热“部署”(例如使用指向当前目录的符号链接,解压新文件,更新符号链接,完成)。 2. WAR将更加轻巧,部署所需时间更短。
这种方法的缺点:
1. 增加了一个服务器(Web服务器),因此增加了一些复杂性。 2. 您需要更改构建脚本(在我看来不是什么大问题)。 3. 您需要更改rsync逻辑。

1

只需使用2个或更多的tomcat服务器,并在其上使用代理。该代理可以是apache/nignix/haproxy。

现在,在每个代理服务器中,“in”和“out” url都配置了端口。

首先,将war文件复制到tomcat中,而无需停止服务。一旦部署了war文件,它就会自动被tomcat引擎打开。

请注���,在server.xml中Host节点内,交叉检查unpackWARs="true"和autoDeploy="true"

看起来像这样

  <Host name="localhost"  appBase="webapps"
        unpackWARs="true" autoDeploy="true"
        xmlValidation="false" xmlNamespaceAware="false">

现在查看Tomcat的日志。如果没有错误,这意味着它已经成功启动。
现在测试所有的API。
现在来到你的代理服务器。
只需更改背景URL映射为新的WAR名称即可。由于向Apache/Nginx/HAProxy等代理服务器注册的时间非常短,因此您将感受到最小的停机时间。
请参考--https://developers.google.com/speed/pagespeed/module/domains进行URL映射。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接