8

我正在研究我们有身份验证机制的项目。我们在身份验证机制中遵循以下步骤。

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

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

对于服务器,我们使用的是 Tomcat 5.5,我们遵循Apache Tomcat 5.5 Servlet/JSP Container来制作 tomcat 集群环境。

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

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

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

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

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

4

10 回答 10

9

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

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

  • 交易是:

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

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

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

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

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

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

[关于这个,网上有很多讨论。一个专注于 EJB 的文章在这里:为什么不鼓励在 Java EE 容器中生成线程?对于 Tomcat 等 Web 容器也是如此]

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

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

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

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

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

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

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

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

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

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

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

于 2012-12-27T09:16:54.670 回答
1

使用等待/通知可能会很棘手。请记住,任何线程都可以随时挂起。因此可以在等待之前调用通知,在这种情况下等待将永远阻塞。

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

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

于 2012-12-23T12:03:58.017 回答
1

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

于 2012-12-23T12:08:51.907 回答
1

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

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

于 2012-12-23T13:17:38.427 回答
1

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

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

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

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

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

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

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

于 2012-12-26T12:00:40.007 回答
1

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

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

于 2012-12-27T05:38:11.527 回答
1

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

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

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

与您的 Web 服务的交互很简单:

  1. 用户使用用户名+密码登录您的网站
  2. 如果主要身份验证(用户名/密码)成功,则生成一个令牌并将userid=token插入 memcached
  3. 将令牌发送到用户的手机
  4. 向用户呈现“输入令牌”页面
  5. 用户通过电话接收令牌并将其输入表格
  6. 根据用户的 id 从 memcached 中获取令牌值。如果匹配,则过期 memcached 中的令牌并认为第二因素成功
  7. 令牌将在您想在 memcached 中设置的任何时间后自动过期

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

于 2012-12-27T21:16:01.967 回答
1

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

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

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

阅读OpenTerracotta 简介 中的快速介绍。

于 2012-12-28T01:12:37.530 回答
0

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

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

于 2012-12-23T12:18:10.767 回答
0

解决方案:

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

于 2012-12-23T13:29:06.570 回答