如何在一个可扩展、高并发和容错的系统中一个一个地处理来自同一个客户端的多个 API 调用
how to process multiple API calls from the same client one by one in a scalable, highly concurrent and fault tolerant system
我们有 Web 服务 API 来支持一千万台设备上的客户端 运行。通常客户每天调用服务器一次。那是每秒看到大约 116 个客户端。对于每个客户端(每个客户端都有唯一的 ID),它可能会同时进行多个 APIs 调用。但是,Server 只能一个接一个地处理来自同一个客户端的 API 个调用。因为,那些 API 调用将更新后端 Mongodb 数据库中该客户端的相同文档。例如:需要更新最后一次上线时间和本客户端文档中嵌入的其他文档。
我的一个解决方案是将同步块放在代表此客户端唯一 ID 的 "intern" 对象上。这将只允许来自同一客户端的一个请求同时获得锁并被处理。此外,来自其他客户端的请求也可以同时处理。但是,此解决方案需要打开负载平衡器的 "stickiness"。这意味着负载均衡器将在预设时间间隔(例如 15 分钟)内将来自同一 IP 地址的所有请求路由到特定服务器。我不确定这是否对整个系统设计的稳健性有任何影响。我能想到的一件事是,有些客户端可能会发出更多的请求,导致负载不均衡(造成热点)。
解决方案 #1:
Interner<Key> myIdInterner = Interners.newWeakInterner();
public ResponseType1 processApi1(String clientUniqueId, RequestType1 request) {
synchronized(myIdInterner.intern(new Key(clientUniqueId))) {
// code to process request
}
}
public ResponseType2 processApi2(String clientUniqueId, RequestType2 request) {
synchronized(myIdInterner.intern(new Key(clientUniqueId))) {
// code to process request
}
}
这个解决方案你可以看我的另外一个问题:
我想到的第二个解决方案是以某种方式锁定该客户端的文档 (Mongodb)(我还没有找到一个很好的例子来做到这一点)。然后,我不需要触摸负载均衡器设置。但是,我对这种方法感到担忧,因为我认为与解决方案 #1 相比,性能(到 Mongodb 服务器的往返行程和忙等待?)会差很多。
解决方案 #2:
public ResponseType1 processApi1(String clientUniqueId, RequestType1 request) {
try {
obtainDocumentLock(new Key(clientUniqueId));
// code to process request
} finally {
releaseDocumentLock(new Key(clientUniqueId));
}
}
public ResponseType2 processApi2(String clientUniqueId, RequestType2 request) {
try {
obtainDocumentLock(new Key(clientUniqueId));
// code to process request
} finally {
releaseDocumentLock(new Key(clientUniqueId));
}
}
我相信这在可扩展和高并发的系统中是非常普遍的问题。你如何解决这个问题?还有其他选择吗?我想要实现的是能够一次处理来自同一客户端的那些请求的一个请求。请注意,仅控制 read/write 对数据库的访问是行不通的。解决方案需要控制整个请求的独占处理。
例如,有两个请求:请求#1 和请求#2。请求 #1 读取客户端文档,更新子文档 #5 的一个字段,并将整个文档保存回来。请求 #2 读取同一个文档,更新子文档 #8 的一个字段,然后将整个文档保存回来。此时,我们会得到一个 OptimisticLockingFailureException,因为我们使用来自 spring-data-mongodb 的 @Version 注解来检测版本冲突。因此,必须在任何时候只处理来自同一客户端的一个请求。
P.S。在选择解决方案 #1(锁定单个 process/instance 并打开负载均衡器粘性)或解决方案 #2(分布式锁)以实现可扩展和高并发系统设计方面的任何建议。目标是支持每秒上百个客户端并发访问系统的千万级客户端。
为什么不在 Mongodb 中创建一个处理队列,您可以在其中提交客户端请求文档,然后使用它们的另一个服务器进程生成结果文档,客户端等待...同步数据使用 clientId,并避免在 API 提交步骤中使用 activity。客户端提交的第二部分 activity(完成后)仅轮询 Mongodb 以查找已使用的记录以查找其 API / ClientID 和一些作业标签。这样,您可以扩展 API 提交,并在单独的服务器上单独扩展 API 消费活动等
在您的解决方案中,您正在根据客户 ID 进行锁定拆分,以便两个客户可以同时处理服务。唯一的问题是粘性会话。一种解决方案是使用分布式锁,这样您就可以将任何请求分派给任何服务器,然后服务器获得锁进程。只有一个考虑因素是它涉及远程调用。我们正在使用 hazelcast/Ignite,它对于平均节点数来说工作得很好。
Hazelcast
一个明显的方法就是在您的终端实施完整的乐观锁定算法。
也就是说,当有并发修改时,您有时会得到 OptimisticLockingFailureException
,但这没关系:只需重新阅读文档并再次开始失败的修改。您将获得与使用锁定相同的效果。本质上,您正在利用 MongoDB 中已经内置的并发控制。这还有一个好处,如果多个事务不冲突(例如,一个是读取,或者它们写入不同的文档),则可以从同一个客户端通过多个事务,从而有可能增加系统的并发性。另一方面,您必须实现重试逻辑。
如果你确实想在每个客户端(或每个文档或其他任何东西)的基础上锁定并且你的服务器是一个单一的进程(这是你建议的方法所暗示的)你只需要一个有效的锁管理器在任意 String
键上,其中有 包括你提到的 Interner
一个。
我们有 Web 服务 API 来支持一千万台设备上的客户端 运行。通常客户每天调用服务器一次。那是每秒看到大约 116 个客户端。对于每个客户端(每个客户端都有唯一的 ID),它可能会同时进行多个 APIs 调用。但是,Server 只能一个接一个地处理来自同一个客户端的 API 个调用。因为,那些 API 调用将更新后端 Mongodb 数据库中该客户端的相同文档。例如:需要更新最后一次上线时间和本客户端文档中嵌入的其他文档。
我的一个解决方案是将同步块放在代表此客户端唯一 ID 的 "intern" 对象上。这将只允许来自同一客户端的一个请求同时获得锁并被处理。此外,来自其他客户端的请求也可以同时处理。但是,此解决方案需要打开负载平衡器的 "stickiness"。这意味着负载均衡器将在预设时间间隔(例如 15 分钟)内将来自同一 IP 地址的所有请求路由到特定服务器。我不确定这是否对整个系统设计的稳健性有任何影响。我能想到的一件事是,有些客户端可能会发出更多的请求,导致负载不均衡(造成热点)。
解决方案 #1:
Interner<Key> myIdInterner = Interners.newWeakInterner();
public ResponseType1 processApi1(String clientUniqueId, RequestType1 request) {
synchronized(myIdInterner.intern(new Key(clientUniqueId))) {
// code to process request
}
}
public ResponseType2 processApi2(String clientUniqueId, RequestType2 request) {
synchronized(myIdInterner.intern(new Key(clientUniqueId))) {
// code to process request
}
}
这个解决方案你可以看我的另外一个问题:
我想到的第二个解决方案是以某种方式锁定该客户端的文档 (Mongodb)(我还没有找到一个很好的例子来做到这一点)。然后,我不需要触摸负载均衡器设置。但是,我对这种方法感到担忧,因为我认为与解决方案 #1 相比,性能(到 Mongodb 服务器的往返行程和忙等待?)会差很多。
解决方案 #2:
public ResponseType1 processApi1(String clientUniqueId, RequestType1 request) {
try {
obtainDocumentLock(new Key(clientUniqueId));
// code to process request
} finally {
releaseDocumentLock(new Key(clientUniqueId));
}
}
public ResponseType2 processApi2(String clientUniqueId, RequestType2 request) {
try {
obtainDocumentLock(new Key(clientUniqueId));
// code to process request
} finally {
releaseDocumentLock(new Key(clientUniqueId));
}
}
我相信这在可扩展和高并发的系统中是非常普遍的问题。你如何解决这个问题?还有其他选择吗?我想要实现的是能够一次处理来自同一客户端的那些请求的一个请求。请注意,仅控制 read/write 对数据库的访问是行不通的。解决方案需要控制整个请求的独占处理。
例如,有两个请求:请求#1 和请求#2。请求 #1 读取客户端文档,更新子文档 #5 的一个字段,并将整个文档保存回来。请求 #2 读取同一个文档,更新子文档 #8 的一个字段,然后将整个文档保存回来。此时,我们会得到一个 OptimisticLockingFailureException,因为我们使用来自 spring-data-mongodb 的 @Version 注解来检测版本冲突。因此,必须在任何时候只处理来自同一客户端的一个请求。
P.S。在选择解决方案 #1(锁定单个 process/instance 并打开负载均衡器粘性)或解决方案 #2(分布式锁)以实现可扩展和高并发系统设计方面的任何建议。目标是支持每秒上百个客户端并发访问系统的千万级客户端。
为什么不在 Mongodb 中创建一个处理队列,您可以在其中提交客户端请求文档,然后使用它们的另一个服务器进程生成结果文档,客户端等待...同步数据使用 clientId,并避免在 API 提交步骤中使用 activity。客户端提交的第二部分 activity(完成后)仅轮询 Mongodb 以查找已使用的记录以查找其 API / ClientID 和一些作业标签。这样,您可以扩展 API 提交,并在单独的服务器上单独扩展 API 消费活动等
在您的解决方案中,您正在根据客户 ID 进行锁定拆分,以便两个客户可以同时处理服务。唯一的问题是粘性会话。一种解决方案是使用分布式锁,这样您就可以将任何请求分派给任何服务器,然后服务器获得锁进程。只有一个考虑因素是它涉及远程调用。我们正在使用 hazelcast/Ignite,它对于平均节点数来说工作得很好。 Hazelcast
一个明显的方法就是在您的终端实施完整的乐观锁定算法。
也就是说,当有并发修改时,您有时会得到 OptimisticLockingFailureException
,但这没关系:只需重新阅读文档并再次开始失败的修改。您将获得与使用锁定相同的效果。本质上,您正在利用 MongoDB 中已经内置的并发控制。这还有一个好处,如果多个事务不冲突(例如,一个是读取,或者它们写入不同的文档),则可以从同一个客户端通过多个事务,从而有可能增加系统的并发性。另一方面,您必须实现重试逻辑。
如果你确实想在每个客户端(或每个文档或其他任何东西)的基础上锁定并且你的服务器是一个单一的进程(这是你建议的方法所暗示的)你只需要一个有效的锁管理器在任意 String
键上,其中有 Interner
一个。