【问题标题】:Architectural issue with Tomcat cluster environmentTomcat 集群环境的架构问题
【发布时间】:2012-12-23 11:57:42
【问题描述】:

我正在开发一个我们有身份验证机制的项目。我们在身份验证机制中遵循以下步骤。

  1. 用户打开浏览器并在文本框中输入他/她的电子邮件,然后单击登录按钮。
  2. 请求发送到服务器。我们生成一个随机字符串(例如,123456)并发送通知到用户的Android/iPhone,并借助wait()方法使当前线程等待。
  3. 用户在手机上输入密码,然后点击手机上的提交按钮。
  4. 一旦用户单击提交按钮,我们就会使 Web 服务访问服务器并传递先前生成的字符串(例如 123456)和密码。
  5. 如果之前输入的电子邮件密码正确,我们将调用notify() 方法到之前等待的线程并发送success作为响应,然后用户进入我们的系统。李>
  6. 如果密码与之前输入的电子邮件不正确,我们将调用notify() 方法到之前等待的线程并发送失败作为响应并向用户显示无效凭据消息。

一切正常,但最近我们转移到了集群环境。我们发现有些线程即使在用户回复后也没有得到通知,并且等待时间不受限制。

服务器方面,我们使用的是Tomcat 5.5,我们关注The Apache Tomcat 5.5 Servlet/JSP Container 制作tomcat集群环境。

回答 :: 可能的问题和解决方案

可能的问题是集群环境中的多个 JVM。现在我们还将集群的 Tomcat URL 连同生成的字符串一起发送到用户 Android 应用程序。

当用户点击回复按钮时,我们将生成的字符串与集群的 Tomcat URL 一起发送,因此在这种情况下,两个请求都将发送到同一个 JVM,并且工作正常。

但我想知道是否有针对上述问题的单一解决方案。

此解决方案存在问题。 如果集群 Tomcat 崩溃会怎样?负载均衡器会向第二个集群 Tomcat 发送请求,同样的问题会再次出现。

【问题讨论】:

  • 当用户收到一个额外的密码 (OTP) 并使用他的常规密码并且 OTP 可以登录时,这在我看来就像一个网上银行解决方案。为什么需要通过电话登录?电子邮件中的点击可以将他重定向到登录页面,他可以在其中输入他的常规密码和将同时到达他的手机的 OTP。这种方式不需要等待/通知。
  • @Andras Kerekes:假设登录可以通过密码、面部认证、语音认证、指纹认证来完成,这些都可以在手机上而不是在网络应用程序上完成。所以通过这种方式,我们的认证系统会更加成熟,并且网络上没有密码之类的东西,所以我们更加安全。未来我们计划将第三方认证系统作为插件制作。
  • 在这种情况下,他输入电子邮件地址的登录页面可以定期查询服务器是否通过电话登录成功/失败。我认为将应用服务器工作线程保持在等待状态不是一个好主意。如果有很多用户,应用程序可能会由于缺少工作线程而变得无响应。

标签: java tomcat web-applications architecture cluster-computing


【解决方案1】:

您的问题的根本原因是 Java EE 被设计为以不同的方式工作 - 尝试阻塞/等待服务线程是重要的禁忌之一。我将首先给出原因,以及之后如何解决问题。

Java EE(Web 层和 EJB 层)旨在能够扩展到非常大的规模(集群中的数百台计算机)。然而,为了做到这一点,设计师必须做出以下假设,这是对如何编码的具体限制:

  • 交易是:

    1. 短暂的(例如,不要阻塞或等待超过一秒左右的时间)
    2. 彼此独立(例如线程之间没有通信)
    3. 对于 EJB,由容器管理
  • 所有用户状态都保存在特定的数据存储容器中,包括:

    1. 通过例如 JDBC 访问的数据存储。您可以使用传统的 SQL 数据库或 NoSQL 后端
    2. 有状态会话 bean,如果您使用 EJB。将这些视为将其字段持久保存到数据库的 Java Bean。有状态会话 bean 由容器管理
    3. Web session 这是一个键值存储(有点像 NoSQL 数据库,但没有缩放或搜索功能),它在特定用户的会话中保存数据。它由 Java EE 容器管理并具有以下属性:

      1. 如果节点在集群中崩溃,它将自动重新定位
      2. 用户可以拥有多个当前网络会话(即在两个不同的浏览器上)
      3. 当用户通过注销结束会话时,或者当会话处于非活动状态的时间超过可配置的超时时间时,Web 会话结束。
      4. 存储的所有值必须可序列化,以便在集群中的节点之间持久化或传输。

如果我们遵循这些规则,Java EE 容器可以成功管理集群,包括关闭节点、启动新节点和迁移用户会话,而无需任何特定的开发人员代码。开发人员编写图形界面和业务逻辑 - 所有“管道”都由可配置的容器功能管理。

此外,在运行时,Java EE 容器可以由一些非常复杂的软件监控和管理,这些软件可以跟踪实时系统上的应用程序性能和行为问题。

嗯,这就是理论。实践表明有一些非常重要的限制被遗漏了,这导致了 AOSP 和代码注入技术,但那是另一回事了

[网上有很多关于这个的讨论。一个专注于 EJB 的文章在这里:Why is spawning threads in Java EE container discouraged? 对于 Tomcat 等 Web 容器也是如此]

很抱歉这篇文章 - 但这对您的问题很重要。由于线程的限制,您不应阻止 Web 请求等待另一个稍后的请求。

当前设计的另一个问题是,如果用户与网络断开连接、电量耗尽或只是决定放弃,会发生什么?大概你会超时,但过了多长时间?对一些客户来说,这可能还为时过早,这会导致满意度问题。如果超时时间过长,您最终可能会阻塞 Tomcat 中的 所有 个工作线程,并且服务器将冻结。这会使您的组织面临拒绝服务攻击。

编辑:在更详细的算法描述发布后改进了建议。

尽管上面讨论了阻止网络工作线程的不良做法以及可能的拒绝服务,但很明显,用户会看到一个小的时间窗口来对 Android 手机上的通知做出反应,并且可以将其保持在合理范围内以增强安全性。这个时间窗口也可以保持在 Tomcat 的响应超时以下。所以可以使用线程阻塞的方式。

有两种方法可以解决这个问题:

  1. 将解决方案的重点转移到客户端 - 在浏览器上使用 Javascript 轮询服务器
  2. 集群中节点之间的通信允许节点接收来自Android App的授权响应来解除阻塞阻塞servlet响应的节点。

对于方法 1,浏览器通过 Javascript 通过 AJAX 调用对 Tomcat 上的 Web 服务进行轮询;如果 Android 应用经过身份验证,AJAX 调用将返回 True。优点:客户端,服务器上的最小实现,服务器上没有线程阻塞。缺点:在等待期间,您必须进行频繁的调用(可能每秒一个 - 用户不会注意到这种延迟),这相当于 大量 调用和服务器上的一些额外负载。

对于方法2,还有一个选择:

  1. 使用Object.wait() 阻塞线程,可选择将节点 ID、IP 或其他标识符存储在共享数据存储中:如果是这样,接收 Android 应用授权的节点需要:

    1. 要么找到当前阻塞的节点,要么向集群中的所有节点广播
    2. 对于上面 1. 中的每个节点,发送一条消息来标识要解除阻止的用户会话。该消息可以通过以下方式发送:

      1. 在每个节点上都有一个仅限内部的 servlet - 这由执行 Android 应用程序授权的 servlet 调用。内部 servlet 将在正确的线程上调用 Object.notify
      2. 使用 JMS 发布-订阅消息队列向集群的所有成员广播。每个节点都是一个订阅者,收到通知后将在正确的线程上调用Object.notify()
  2. 轮询数据存储,直到线程被授权继续:在这种情况下,Android 应用程序需要做的就是将状态保存在 SQL 数据库中

【讨论】:

  • 非常感谢您的努力。阅读答案后,一件事很清楚设计不正确,我的服务器将冻结。请给我写一些解决上述问题的方法。我将修改我的问题并编写我们在应用程序中遵循的步骤。也许您可以获得有关确切用例的更多信息。
  • 太棒了 - 得到了反馈。现在是午夜了,所以我不是处于最佳回答状态。我希望在接下来的 12 小时内这样做。
  • 添加了内存中跨 jvm 缓存的概念,例如 Terracotta - 感谢 Luigi R. Viggiano
  • 改进了解决方案实施的细节
  • 真的很有帮助,我已经向我的高级经理建议了两种方法,让我们看看高级经理或架构的决定是什么。我们非常感谢您和 stackoverflow 社区。​​span>
【解决方案2】:

使用等待/通知可能很棘手。请记住,任何线程都可以随时暂停。所以可以在wait之前调用notify,这种情况下wait会永远阻塞。

在您的情况下,我不希望出现这种情况,因为您涉及到用户交互。但是对于您正在执行的同步类型,请尝试使用信号量。创建一个数量为 0(零)的信号量。等待的线程调用acquire(),它会阻塞直到另一个线程调用release()。

以这种方式使用 Semaphore 比等待/通知您描述的任务更加健壮。

【讨论】:

    【解决方案3】:

    考虑使用内存网格,以便集群中的实例可以共享状态。我们使用Hazelcast 在实例之间共享数据,因此如果响应到达不同的实例,它仍然可以处理它。

    例如您可以使用值为 1 的分布式倒计时锁存器来设置线程在发送消息后等待,并且当响应从客户端到达单独的实例时它可以减少,该实例可以将锁存器减少到 0 让运行第一个线程.

    【讨论】:

      【解决方案4】:

      您的集群部署意味着集群中的任何节点都可以收到任何响应。

      为 Web 应用程序使用线程等待/通知可能会累积大量可能无法通知的线程,这可能会导致内存泄漏或创建大量阻塞线程。这最终可能会影响服务器的可靠性。

      更强大的解决方案是将请求发送到 android 应用并存储用户请求的当前状态以供以后处理并完成 HTTP 请求。要存储您可以考虑的状态:

      • 所有 tomcat 节点都连接到的
      • 一个可以跨tomcat节点工作的java缓存解决方案,比如

      此状态将对您的 tomcat 集群中的所有节点可见。

      当来自 android 应用程序的回复到达不同的节点时,恢复您的线程正在执行的状态并继续在该节点上处理。

      如果应用程序的 UI 正在等待来自服务器的响应,您可以考虑使用 请求来轮询来自服务器的响应状态。处理 android 应用程序响应的节点不需要与处理 UI 请求的节点相同。

      【讨论】:

      • 感谢@pd40 的回复。我们使用 ajax 和数据库方法设计应用程序,但被客户拒绝。说“对数据库和服务器造成大量不必要的打击,并且响应不会是实时的”。然后我们来等待并通知解决方案,这种方法是实时的,不会对服务器和数据库造成不必要的打击。但问题是服务器负载增加,因为我们在服务器上等待线程 40 秒,其次是多个 JVM 问题。
      • 我不太明白你的解释。等待客户端的线程仍然需要访问服务器,不是吗?那么您的客户如何想象它在不接触服务器的情况下工作?
      • 线程不在此解决方案中等待。该状态一直持续到 android 应用程序响应为止。任何等待 Android 应用程序的 UI 都会轮询响应。
      • @pd40 对于 android 应用程序,我们也对 iPhone 使用类似的谷歌云消息传递机制 (GCM)。所以我们有推送机制来向用户手机发送通知,而不是拉机制。
      • 我修改了我的问题并添加了更多步骤和细节,以便社区可以轻松理解用例和需求。
      【解决方案5】:

      在 Web 服务环境中使用 Thread.wait 是一个巨大的错误。相反,维护一个用户/令牌对的数据库并定期将它们过期。

      如果您需要集群,请使用可集群的数据库。我会推荐像 memcached 这样的东西,因为它在内存中(而且速度很快)并且开销很低(键/值对非常简单,所以你不需要 RDBMS 等)。 memcached 已经为您处理了令牌过期问题,因此它看起来非常合适。

      我认为用户名 -> 令牌 -> 密码策略是不必要的,特别是因为您有两个不同的组件共享相同的 2 因素身份验证责任。我认为您可以进一步降低复杂性,减少用户的困惑,并节省一些 SMS 发送费用。

      与您的网络服务的交互很简单:

      1. 用户使用用户名 + 密码登录您的网站
      2. 如果主要身份验证(用户名/密码)成功,则生成令牌并将userid=token 插入到内存缓存中
      3. 将令牌发送到用户的手机
      4. 向用户展示“输入令牌”页面
      5. 用户通过电话收到令牌并将其输入到表单中
      6. 根据用户 ID 从 memcached 中获取令牌值。如果匹配,则将 memcached 中的令牌过期并认为第二因素成功
      7. 无论您想在 memcached 中设置多少时间,令牌都会自动过期

      上述解决方案不存在线程问题,它可以扩展到支持您自己的软件所需的尽可能多的 JVM。

      【讨论】:

      • 感谢您的努力。用户只能从网站输入他/她的电子邮件,网页上没有密码。当我们向用户 android/iPhone 发送通知时,我们会通过面部认证、语音认证、指纹认证或简单的文本密码等多种方式匹配密码。但是密码或匹配一词仅在用户的手机上。这是我们的应用程序“Web 应用程序上的无密码身份验证”的优点或功能。
      【解决方案6】:

      在分析您的问题后,我得出的结论是,确切的问题是集群环境中的多个 JVM。

      【讨论】:

        【解决方案7】:

        确切的问题在于集群环境。两个请求都不会发送到同一个 JVM。但是我们知道,当前一个线程正在等待时,普通/简单通知在同一个 JVM 上工作。

        您应该尝试执行这两个请求(第一个请求,当用户从 Android 应用程序回复时的第二个请求)。

        【讨论】:

          【解决方案8】:

          恐怕,但线程无法迁移经典的 Java EE 集群。

          您必须重新考虑您的架构以不同方式实现等待/通知(无连接)。

          或者,您可以尝试使用terracotta.org。看起来这允许在多台机器上集群整个JVM 进程。也许这是您唯一的解决方案。

          阅读Introduction to OpenTerracotta中的快速介绍。

          【讨论】:

            【解决方案9】:

            我想问题是,您的第一个线程在 JVM 1 中向用户的 Android 应用程序发送通知,当用户回复时,控制权转到 JVM 2。这就是主要问题。

            不知何故,两个线程都可以访问同一个 JVM 以应用等待和通知逻辑。

            【讨论】:

              【解决方案10】:

              解决方案:

              为所有等待线程创建单点联系。因此,在集群环境中,所有线程都将在第三个 JVM(单点联系)上等待,因此所有请求(任何集群 Tomcat)都将联系同一个 JVM 以等待和通知逻辑,因此没有线程将无限期等待。如果有回复,那么如果同一个对象已经等待并且正在第二次被通知,那么线程将被通知。

              【讨论】:

              • 在 JEE 容器(如 Tomcat)中生成线程并不是一个好习惯——这就是 JEE 容器的用途。那些额外的线程不受容器控制,可能导致容器无法关闭,不会被容器重启,也无法移动到集群的其他成员。改用其他 JEE 机制:会话持久性或数据存储。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-10-27
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-05-25
              相关资源
              最近更新 更多