沃尔玛针对高峰流量扩展其库存预订API处理能力 - Shanawaaz


当顾客在Walmart.com网站或移动应用程序上下订单时,会有一个库存预订电话。这捕获了对顾客购物车中的商品的需求。在感恩节假期或任何销售活动(如PS5或Xbox活动)期间,库存预订请求的数量会显著增加。在这篇文章中,我想解释一下我们是如何克服扩展问题的,现在能够无缝地处理高峰流量。

在进行优化之前,让我们了解一下可售库存是如何计算的。

可售库存=在手库存-正在处理的订单

上述公式简单来说就是计算可销售的单位。在手存货是根据存货摄入量更新的。库存信息可以来自沃尔玛商店、配送中心或市场接口。Demand/Orders In Progress(正在进行的订单)是根据库存预订的数量来更新的。

{
"id": "5a3adr7sehfe4y9a8ezb80da8sch6afz",
"sId": "65CED97DC02248478EAF8242FC808601-ss-1",//partition key
"onHand" : 10,
"ordersInProgress" : 4,
"availableToSell" : 6
...other fields omitted
}

库存预订服务的第一个设计很简单。它是一个横向扩展的API,每个实例都试图执行保留命令(执行命令和应用事件的概念是事件源模式的一部分),并将执行的保留事件写入事件集合(CosmosDB容器)。一旦保留事件被写入,那么快照服务将在库存快照上应用保留事件。这意味着来自保留事件的数量被汇总到库存快照的ordersInProgress字段中(见上面的JSON)。

通过这种方法,我们很快注意到,当API调用的数量增加时,会出现大量的写入竞争。这意味着如果有一个以上的实例试图在同一个分区键上写入保留的事件,他们将在冲突中重试。

为了克服这种写竞争,我们在新的设计中利用了散点收集技术:
库存预订API被分割成主预订API和行预订API:

  1. 当主预订API收到预订请求时,它将拆分由sellingId分组的预订行,sellingId是数据库的分区键,并调用行预订API。
  2. 主预订API中的每个行项目都被路由行预订API,并带有一个自定义的HTTP头。这个HTTP头由Istio sidecar识别,以使用一致的基于哈希的目的地规则将请求路由到同一Kubernetes pod。

通过这种设计上的改变,我们确保只有一个实例收到对同一分区键的请求。
然后在同一个pod内,传入的请求被路由到运行MailBoxProcessor的同一个线程。这种技术是用来实现内存并发的。使用这个请求,试图将写到分布式数据存储(CosmosDB)中一个分区process/threads的路由数量减少到一个。

虽然我们可以通过这种设计减少写的争夺,但我们遇到的下一个瓶颈是被写入数据库的单一实例的保留事件的数量。
CosmosDB把每个分区的吞吐量定为10,000 RUs,作为上限。这意味着,如果每个保留事件文档的写入要使用5个RU,那么我们的写入吞吐量将被限制在10,000/5=2,000个订单/秒(OPS)。这意味着对于任何项目,我们只能采取2000个OPS或每分钟12万个订单(OPM)。

为了克服这个每个分区的写入限制,我们创建的另一个优化是对保留事件进行批处理,并创建一个单一的 batchReserved 事件,可以将多个保留作为一个文件写入。这意味着我们可以将我们的写入吞吐量提高到2000多OPS。我们已经对它进行了压力测试,以处理超过5000个OPS或超过30万个OPM。

我想强调的另一个优化是快照缓存。在执行储备命令之前,必须从数据库中读取当前状态或分区键的快照。由于快照读取是发生在每一个预订调用中,并且只有一个实例负责处理单个分区键的预订,所以在该实例中维护了一个带有TTL的内存快照缓存。并且有一个异步后台进程保持这个快照缓存与数据库的同步。这种优化减少了快照读取的总数,降低了预订服务的读取RU成本。

总结
总结一下,下面是可以应用于任何写量大的API的所有优化的列表。

  1. 分散--用一个粘性sticky会话来收集API请求,所以数据库分区总是由同一个实例来处理。
  2. 在内存中并发,使用带有邮箱的actor模式,将单个分区的处理限制在单个线程中。这也有助于对相同分区请求的批量处理。
  3. 内存中的快照状态缓存,以减少读取的次数。

这些优化之所以能够实现,是因为我们这个了不起的团队的联合努力。