以最少的停机时间部署 Java Web 应用程序的最佳实践?
-
08-07-2019 - |
题
当部署大型 Java Web 应用程序(>100 MB .war)时,我当前使用以下部署过程:
- 应用程序 .war 文件在开发计算机上本地扩展。
- 扩展的应用程序从开发计算机 rsync:ed 到实时环境。
- rsync完成后,现场环境的应用服务器会重新启动。这一步并不是严格需要的,但我发现在部署时重新启动应用程序服务器可以避免“java.lang.OutOfMemoryError:由于频繁的类加载,PermGen 空间”。
这种方法的好处是:
- rsync 最大限度地减少了从开发计算机发送到实时环境的数据量。上传整个 .war 文件需要十多分钟,而 rsync 需要几秒钟。
这种方法的缺点:
- 当 rsync 运行时,应用程序上下文会重新启动,因为文件已更新。理想情况下,重新启动应在 rsync 完成后进行,而不是在 rsync 仍在运行时进行。
- 应用程序服务器重新启动会导致大约两分钟的停机时间。
我想找到具有以下属性的部署过程:
- 部署过程中的停机时间最少。
- 上传数据所花费的时间最少。
- 如果部署过程是特定于应用程序服务器的,则应用程序服务器必须是开源的。
问题:
- 考虑到上述要求,最佳的部署流程是什么?
解决方案
人们注意到,当将更改推送到 WAR 文件时,rsync 不能很好地工作。原因是 WAR 文件本质上是 ZIP 文件,并且默认情况下使用压缩成员文件创建。对成员文件(压缩前)的微小更改会导致 ZIP 文件中出现较大的差异,从而导致 rsync 的增量传输算法无效。
一种可能的解决方案是使用 jar -0 ...
创建原始 WAR 文件。这 -0
选项告诉 jar
命令在创建 WAR 文件时不压缩成员文件。那么,当 rsync
比较 WAR 文件的新旧版本,增量传输算法应该能够创建小的差异。然后安排 rsync 以压缩形式发送 diff(或原始文件);例如使用 rsync -z ...
或下面的压缩数据流/传输。
编辑:根据 WAR 文件的结构,可能还需要使用 jar -0 ...
创建组件 JAR 文件。这适用于经常更改(或简单重建)的 JAR 文件,而不是稳定的第 3 方 JAR 文件。
理论上,这个过程应该比发送常规 WAR 文件有显着的改进。实际上我还没有尝试过这个,所以我不能保证它会起作用。
缺点是部署的 WAR 文件会明显更大。这可能会导致 web 应用程序启动时间更长,但我怀疑效果是微乎其微的。
一种完全不同的方法是查看您的 WAR 文件,看看您是否可以识别可能(几乎)永远不会更改的库 JAR。将这些JAR从WAR文件中取出,并分别部署到Tomcat服务器的 common/lib
目录;例如使用 rsync
.
其他提示
更新:
自从这个答案第一次写出来,一种更好的零停机时间部署到tomcat的方式就出现了。在最新版本的 tomcat 中,您可以在 war 文件名中包含版本号。例如,您可以部署文件 ROOT##001.war
和 ROOT##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 服务器,直到部署结束。冲洗并重复您的其他网络服务器。
在任何需要考虑停机时间的环境中,您肯定会运行某种服务器集群,以通过冗余来提高可靠性。我将从集群中取出一台主机,对其进行更新,然后将其放回集群中。如果您的更新无法在混合环境中运行(例如,数据库上需要进行不兼容的架构更改),您将不得不关闭整个站点,至少暂时关闭。诀窍是在删除原始文件之前启动替换进程。
以 tomcat 为例 - 您可以使用 CATALINA_BASE 定义一个目录,在该目录中可以找到所有 tomcat 的工作目录,与可执行代码分开。每次部署软件时,我都会部署到一个新的基本目录,以便我可以将新代码驻留在磁盘上旧代码旁边。然后,我可以启动另一个指向新基目录的 tomcat 实例,启动并运行所有内容,然后在负载均衡器中将旧进程(端口号)与新进程交换。
如果我担心在交换机上保留会话数据,我可以设置我的系统,使每个主机都有一个伙伴来复制会话数据。我可以删除其中一台主机,更新它,将其重新启动,以便它备份会话数据,然后切换两台主机。如果集群中有多个对,我可以删除所有对的一半,然后进行大规模切换,或者我可以一次进行一对,具体取决于版本的要求、企业的要求等。然而,就我个人而言,我更愿意允许最终用户偶尔丢失活动会话,而不是尝试在会话完好无损的情况下进行升级。
这都是 IT 基础设施、发布流程复杂性和开发人员工作量之间的权衡。如果您的集群足够大并且您的愿望足够强烈,那么设计一个可以在大多数更新中无需停机的情况下进行更换的系统就很容易。大型架构更改通常会导致实际停机,因为更新的软件通常无法适应旧架构,并且您可能无法摆脱将数据复制到新的数据库实例、执行架构更新,然后将服务器切换到新数据库的情况,因为从旧数据库克隆新数据库后,您将错过写入旧数据库的任何数据。当然,如果您有资源,您可以要求开发人员修改新应用程序,以便为所有更新的表使用新表名称,并且您可以在实时数据库上放置触发器,这将使用数据正确更新新表:它由先前版本写入旧表(或者可能使用视图来模拟一种模式与另一种模式)。启动新的应用程序服务器并将它们交换到集群中。如果您有开发资源来构建游戏,您可以玩大量游戏,以最大限度地减少停机时间。
也许减少软件升级期间停机时间的最有用机制是确保您的应用程序可以在只读模式下运行。这将为您的用户提供一些必要的功能,但让您能够进行需要数据库修改等的系统范围的更改。将您的应用程序置于只读模式,然后克隆数据、更新架构、针对新数据库启动新应用程序服务器,然后切换负载均衡器以使用新应用程序服务器。您唯一的停机时间是切换到只读模式所需的时间以及修改负载均衡器配置所需的时间(其中大多数可以在没有任何停机时间的情况下处理它)。
我的建议是将 rsync 与分解版本一起使用,但部署一个 war 文件。
- 在实时环境中创建临时文件夹,您将在其中拥有 web 应用程序的分解版本。
- Rsync 爆炸版本。
- rsync 成功后,在实时环境计算机的临时文件夹中创建一个 war 文件。
- 将服务器部署目录中的旧 war 替换为临时文件夹中的新 war。
建议在 JBoss 容器(基于 Tomcat)中用新的 war 替换旧的 war,因为它是一种原子且快速的操作,并且可以肯定,当部署程序启动时,整个应用程序将处于已部署状态。
难道您不能在 Web 服务器上制作当前 Web 应用程序的本地副本,rsync 到该目录,然后甚至可能使用符号链接,一次性将 Tomcat 指向新的部署,而无需太多停机时间吗?
您对提取的战争进行 rsync 的方法非常好,重新启动也是如此,因为我认为生产服务器不应该启用热部署。那么,唯一的缺点是需要重新启动服务器时的停机时间,对吧?
我假设您的应用程序的所有状态都保存在数据库中,因此某些用户在一个应用程序服务器实例上工作而其他用户在另一个应用程序服务器实例上工作时没有问题。如果是这样,
运行两个应用程序服务器: :启动第二个应用程序服务器(它侦听其他 TCP 端口)并在那里部署您的应用程序。部署后,更新 Apache httpd 的配置(mod_jk 或 mod_proxy)以指向第二个应用程序服务器。正常重启 Apache httpd 进程。这样您就不会出现停机情况,并且新用户和请求会自动重定向到新的应用程序服务器。
如果您可以利用应用程序服务器的集群和会话复制支持,那么对于当前登录的用户来说甚至会很顺利,因为第二个应用程序服务器将在启动后立即重新同步。然后,当没有对第一台服务器的访问时,将其关闭。
这取决于您的应用程序架构。
我的一个应用程序位于负载平衡代理后面,我在其中执行交错部署 - 有效地消除了停机时间。
热部署 Java EAR 以最大限度地减少或消除服务器上应用程序的停机时间 或者 如何使用 Jboss Tools Eclipse 插件在 Jboss 中“热”部署 war 依赖 可能有一些选择适合您。
部署到集群,无需停机 也很有趣。
JavaRebel 有 热代码部署 也。
如果静态文件是大型 WAR 的重要组成部分(100Mo 相当大),那么将它们放在 WAR 之外并将它们部署在 Web 服务器上(例如Apache)放在应用程序服务器前面可能会加快速度。最重要的是,Apache 在提供静态文件方面通常比 Servlet 引擎做得更好(即使它们中的大多数在该领域取得了重大进展)。
因此,与其产生一场巨大的战争,不如节食并生产:
- 一个包含 Apache 静态文件的大 ZIP
- servlet 引擎的较少的 WAR。
或者,进一步简化 WAR 的过程:如果可能,在应用程序服务器级别部署 Grails 和其他不经常更改的 JAR(这可能是大多数情况)。
如果你成功地产生了一个更轻的 WAR,我就不会费心去同步目录而不是档案。
这种方法的优点:
- 静态文件可以在 Apache 上热“部署”(例如使用指向当前目录的符号链接,解压缩新文件,更新符号链接,然后瞧)。
- WAR 将变得更薄,部署时间也将更短。
这种方法的缺点:
- 多了一台服务器(Web 服务器),因此这增加了(一点)更多的复杂性。
- 您需要更改构建脚本(在我看来这没什么大不了的)。
- 您需要更改 rsync 逻辑。
我不确定这是否回答了您的问题,但我只会分享我在我所做的几个项目中使用或遇到的部署过程。
与您类似,我不记得进行过全面的战争重新部署或更新。大多数时候,我的更新仅限于几个jsp文件,也许是一个库,一些类文件。我能够管理和确定哪些是受影响的工件,通常,我们将这些更新与更新脚本一起打包在 zip 文件中。我将运行更新脚本。该脚本执行以下操作:
- 备份将被覆盖的文件,可能备份到具有今天日期和时间的文件夹。
- 解压我的文件
- 停止应用程序服务器
- 将文件移过来
- 启动应用服务器
如果停机是一个问题(通常是这样),我的项目通常是高可用性的,即使它们不共享状态而是使用提供粘性会话路由的路由器。
我很好奇的另一件事是,为什么需要 rsync?您应该能够通过在暂存/开发环境中确定所需的更改来了解所需的更改,而不是实时执行增量检查。在大多数情况下,您必须调整 rsync 以忽略文件,例如定义生产服务器使用的资源的某些属性文件,如数据库连接、smtp 服务器等。
我希望这是有帮助的。
您的 PermSpace 设置为多少?我希望看到这种增长,但是 应该 收集旧类后就下去吗?(或者类加载器仍然存在吗?)
大胆思考,您可以 rsync 到单独的版本或日期命名的目录。如果容器支持符号链接,您可以 SIGSTOP 根进程,通过符号链接切换上下文的文件系统根,然后 SIGCONT 吗?
至于早期的上下文重启。所有容器都有配置选项来禁用类文件或静态资源更改的自动重新部署。您可能无法在 web.xml 更改上禁用自动重新部署,因此该文件是最后一个更新的文件。因此,如果您禁用自动重新部署并将 web.xml 更新为最后一个,您将看到上下文重新启动 后 整个更新。
我们将新版本的 web 应用程序上传到一个单独的目录,然后将其与正在运行的版本交换,或者使用符号链接。例如,我们在 tomcat webapps 目录中有一个名为“myapp”的符号链接,它指向当前名为“myapp-1.23”的 webapp。我们将新的 webapp 上传到“myapp-1.24”。一切准备就绪后,停止服务器,删除符号链接并创建一个指向新版本的新符号链接,然后再次启动服务器。
为了提高性能,我们在生产服务器上禁用自动重新加载,但即便如此,以非原子方式更改 Web 应用程序中的文件可能会导致问题,因为静态文件甚至 JSP 页面的更改方式可能会导致链接损坏或更糟。
实际上,Web 应用程序实际上位于共享存储设备上,因此集群、负载平衡和故障转移服务器都具有相同的可用代码。
您的情况的主要缺点是上传将花费更长的时间,因为您的方法允许 rsync 仅传输修改或添加的文件。您可以先将旧的 webapp 文件夹复制到新的文件夹,然后 rsync 到该文件夹,如果它有显着的差异,并且确实是一个问题。
Tomcat 7 有一个很好的功能,称为“并行部署“这是专为此用例设计的。
要点是将 .war 展开到一个目录中,可以直接在 webapps/ 下,也可以在符号链接下。应用程序的后续版本位于名为 app##version
, , 例如 myapp##001
和 myapp##002
. 。Tomcat 将处理转到旧版本的现有会话以及转到新版本的新会话。
问题是你必须 非常 小心 PermGen 泄漏。对于使用大量 PermGen 的 Grails 来说尤其如此。VisualVM 是你的朋友。
只需使用 2 个或多个带有代理的 tomcat 服务器即可。该代理可以是 apache/nignix/haproxy。
现在,在每个代理服务器中,都有“in”和“out”url,并配置了端口。
首先在tomcat中复制你的war,不要停止服务。一旦部署了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 映射更改为新战争的名称即可。由于注册像 apache/nignix/haProxy 这样的代理服务器花费的时间非常少,因此您会感觉停机时间最短
参考 - https://developers.google.com/speed/pagespeed/module/domains 用于映射 url
您正在使用 Resin,Resin 内置了对 Web 应用程序版本控制的支持。
http://www.caucho.com/resin-4.0/admin/deploy.xtp#VersioningandGracefulUpgrades
更新:它的看门狗进程也可以帮助解决永久空间问题。
不是“最佳实践”,而是我刚刚想到的。
通过 DVCS(例如 git)部署 web 应用程序怎么样?
这样你就可以让 git 找出要传输到服务器的文件。如果结果被破坏,您还有一个很好的方法可以退出,只需执行还原即可!
我编写了一个 bash 脚本,它采用一些参数并在服务器之间同步文件。对于较大的档案,可以大大加快 rsync 传输速度: