TL;DR:通过重新启动服务器重新部署(检查最终更新)。
这是一个老问题,但还没有找到解决方案,而且目前我不知道该去哪里寻找。
我们在两台服务器上部署了三个 Java(Spring,非启动)Web 应用:一个应用位于部署在 Tomcat9(端口 80)上的 GCP Compute Engine 上,另外两个应用位于我们内部服务器上的一个 Tomcat8.5 实例上(端口 8080 从端口 80 重定向)。两个系统都具有 Mysql8,并使用几乎相同的配置来连接到它:DataSource 用于主数据库,ConnectionProvider 用于租户。
问题是,重新部署时,池中的一些旧连接(HikariCP)不会被终止,而其他连接则会被终止。保留的这些连接来自我们的多租户连接提供商。可以说,主租户会正确地终止旧连接。这当然会导致当我们拥有太多连接时,我们会用完它们,从而引发 SQLException。我通过增加连接数来解决这个问题,但这不是解决方案。
我们通过不借助 GUI 更新 war 文件来重新部署。我确信这就是导致问题的原因,但并不能真正解释为什么有些连接可以正确关闭,而其他连接却不能。
我尝试过的:
- 我看过与此相关的答案(主要是关于 PHP 的),其中 Mysql 连接在工作完成后仍处于休眠状态。我也尝试了这些问题中提供的修复方法,因为它们似乎也适合我的情况。例如将
wait_timeout
和减少interactvive_timeout
到 30 分钟。 - 我们的 HikariCP 配置在 10 分钟后空闲连接,并且有
maxLifetime
15 分钟空闲时间。即使过了几个小时,连接也不会关闭,并且它们实际上会在 30 分钟后刷新。我的意思是查询显示的时间SELECT * FROM information_schema.processlist GROUP BY db;
上升到 1799(甚至更少),然后回到 0。为什么?我知道系统当时没有被用户使用,并且日志显示 HikariCP 只知道 4 个连接(我配置的连接),而不是有时最多 20 个处于“活动”状态的连接。
我们使用的是 Spring Data JPA,因此所有连接管理均由 Hibernate/JPA 处理。连接也由 Hikari 正确刷新,因此我认为代码中的连接未处于打开状态。
到目前为止,我确信这不是 Hikari 的问题(我指的是我们的配置)。这让我相信数据库配置有些奇怪,或者我们只是没有正确重新部署。
我相信,如果我重建服务器布局(请原谅我词汇量不足),让两个 Web 应用程序都位于各自的 Tomcat 实例中,并使用 Apache 或 Nginx 作为代理,这个问题就会消失。我已经在测试环境中完成了此配置,而且我一直想这样做,但很难证明我的职位发生了这样的变化(我几乎是一个不太初级的后端开发人员,不知怎么就负责了这件事)。即便如此,这也是一个很大的变化,我需要花几天时间处理更多的事情,而且我真的宁愿(正确地)修复当前配置,也不愿重建服务器。
其他选项是安排服务器 + 数据库重启。我们的系统是区域性的,而且我们仍然有少数用户在正常工作时间工作,因此他们永远不会注意到每天凌晨 3 点的重启。我只是不喜欢这样做,而且在我看来,这和max_connections
每隔几天盲目增加一样低效。
还有一个选项是重建我们处理多个租户的方式。我们正在使用 ConnectionProvider,这些连接是“有故障”的连接。我见过几个使用 DataSource 的其他方法的例子,我知道 DataSource 没有这个问题,因为“主”数据库连接在重新部署时会按预期关闭。即便如此,我仍然认为这是一个配置问题。
由于我的经验不足,而且需要研究的东西太多,我猜我可能忽略了文档中的某些内容,或者我并不真正了解我接触过的配置。尽管我迷茫了,但我还是来寻求其他人在这方面的经验。我还应该研究其他东西吗?我也设置过了,slow_query_logs
但几天后文件仍然是空的。
有人遇到过这种问题吗?如果您需要有关我们的结构或部署的更多信息,请随时提出请求。您可能已经猜到了,我们是一家仍在学习这方面知识的小公司。
更新:
我在后端推出了一些额外的方法,这些方法可能有助于处理额外的连接。有些方法没有被覆盖,而且由于我们是从另一个类扩展而来的,所以超级方法可能不起作用。这些方法专门针对访问连接的数据结构。
此外,经过一次重新部署后,我看到连接数从 4 个增加到了 8 个(预期:第一次部署时的 4 个,重新部署时的额外 4 个),但几个小时后连接数下降到了 6 个。我希望一切都结束了,但第二天我们又有了那 8 个连接。
更糟糕的是,今天我有机会重新启动一些服务,并尝试仅重新启动数据库服务。一开始,它似乎将连接数降低到每个租户的预期 4 个,但过了一段时间,它又上升到重新启动前的相同值。这告诉我连接被 Tomcat 劫持了(?),这意味着文档中可能有一些内容可以解决此行为。我还没有找到合适的关键字来找到它,但我的赌注是在上下文、领域或一个阀门上。
如果我找不到任何东西,我将推出一个自定义的 ConnectionProvider,它是我从 扩展而来的EntityManagerFactoryBean
。在此,我设置了一个stop()
方法,该方法触发一个@PreDestroy
方法,该方法使用租户连接访问数据结构,并使用 Hikari 自己的方法手动关闭它们。理论上,这是我从代码中可以关闭这些连接的最多方法。如果这不起作用,而且我在 Tomcat 的文档中也找不到任何东西,我需要说出来并在计划重启或重建服务器 + “适当的重新部署”(停止、更新、启动)之间进行选择。
更新2:
昨天我尝试使用上次更新中描述的方法和另一种方法手动关闭连接ServletContextListener
。但都没有成功,而且我发现close()
HikariCPs 连接提供程序中的方法没有引用连接,所以真是糟糕。我还决定尝试在 bean 中动态生成 ConnectionProviders,并使用它正确的 close/destroy 方法,但由于我考虑使用的方法不适合这样做,所以我将部分放弃这个想法。
接下来:从 更改为ConnectionProviders
。DataSouces
如果这有效,那么我们可以像往常一样继续重新部署。我将尝试我想到的三种方法(如果连接在重新部署时出现相同的问题):设置一种@PreDestroy
手动迭代 DataSources 映射并关闭所有相关连接的方法,动态生成并将所有DataSource
s 注册为 bean(可能使用接口或其他东西“分组”它们,以便可以MultiTenantResolver
使用它或采用第一种方法但关闭 中的连接ServletContextListener
。
我发现的另一件事是,连接被保持在比 webapps 上下文更高的级别。这是关键信息,但老实说,我不太明白为什么一个应用程序的一组连接没有像另一个应用程序那样关闭,以及为什么 Tomcat 不会让这些线程/连接在超时后消失。此信息的来源是StackOverflow 上的这个问题。
我设法默默地“从服务器上切出一块”并在测试环境中设置了一个个人测试环境。由于我技术上负责这项工作,并努力修复生产中目前出现的问题,我认为我这样做是合理的。
我可能会尝试在 SO 中询问以及 HikariCPs Google Group,尽管目的不同,但都是为了使我的问题与两个社区相关。
更新 3
从 ConnectionProvider 更改为 DataSource 解决了一半的问题,但带来了新的、更令人困惑的错误:
- 虽然大多数池在重新部署时都正确初始化为 4 个连接,但其中两个池仍保持旧行为(原始部署中的 4 个 + 新部署中的 4 个),而一个不知何故在重新部署时最终变成了 12 个。即原始的 4 个、重新部署中的 4 个以及一些随机的额外 4 个。
- 在使用该系统测试任何异常行为时,我注意到每次更换租户时,都会创建一个新池。后来我发现,实际上,启动时只创建了两个池,其他每个池仅在请求时才创建。这确实没问题,但我仍有一个租户在启动时有一些随机连接,这些连接在使用该特定数据库时继续进行。
然后我尝试了所有选项并在关机期间手动关闭连接,但我不能说这些都有效。
看来我只需要改变服务器的工作方式。我有点惊讶,无论我怎么研究,似乎都找不到答案,而且我很沮丧,在我投入了这么多时间之后,所有问题都可能被解决为一个批处理文件,通过关闭、替换和重新启动来处理重新部署。
Hikari 文档中指出,对于热部署(以及通过扩展进行的热重新部署),需要关闭连接,但它谈论的是数据源,而不是 ConnectionProvider。此时,我甚至考虑放弃 Hikari 而选择其他解决方案,但我也觉得这是不必要的,也是我沮丧的结果。
无论如何,我想我会继续尝试一些东西。剩下的留给我尝试的东西不多了。
更新 4:
好吧,我终于放弃了。我和需要谈的人谈过,实际上得到了一个完成其他工作的最后期限,包括对我们的服务器进行小规模检修。这也是我开始研究这个问题的部分原因。无论如何,鉴于这个最后期限,并且我还没有找到解决方案,我将重建服务器结构:我将使用代理服务器为每个应用程序提供不同安全端口的 Tomcat 实例。这样,客户端就不需要更改任何内容。在里面,我将为项目负责人提供部署脚本,这些脚本将更新他们的部署分支,生成更新的 WAR,停止他们特定的 Tomcat 服务,清理以前的版本,添加新版本并再次启动 Tomcat 服务。这样我就不需要担心连接了,最终为每个项目提供所需的独立性并自动化部署,以尽可能避免错误。
说实话,以这种方式结束确实有点糟糕,但我们并不总是赢,对吧?