胶印分页有什么好处? 设计基于游标的并行Web API

2021-01-04 10:53:57

最近,我正在编写一个小程序来备份多年来在各种社交平台中存储的数据。在进行Goodreads时,我想起了它的API有点奇怪,因为它使用了分页2的偏移量从今天的标准来看,这被认为是不好的做法。尽管使用方便,但偏移量很难在后端保持高性能,并且具有主要的不良特性,即插入或删除的任何项目都将整个结果集移到所有后续页面上。 Goodreads的API已有很长的历史了,大概是因为它的事实,就像许多现代API一样,仅凭此事实就使用了偏移量。

最初,我的程序执行了应做的工作,然后天真地一页一页地迭代页面。它先获取第一页,然后获取第二页,然后获取第三页,并继续进行直到找到包含空结果的页面。这种方法效果很好,但是Goodreads API的其他怪癖之一是它运行缓慢。 API调用花费了很长时间,以至于我的程序只花了35秒来迭代几百个对象。

幸运的是,胶印分页确实具有一个非常明显的优势-完全易于并行化。基于光标的分页对于客户端来说很难并行化,因为在接收到上一页之前,它不知道要发送下一页的光标。该API会发回结果列表以及指定下一步操作的游标,例如“这里有一些结果,现在在/ objects?starting_after = tok_123下一页”。胶印分页并不是这样,在这里我不仅可以同时请求第1页,还可以同时请求第2、3和4页。

我将程序重构为使用简单的分而治之的策略。选择多个使用者,将页面空间分成相等的部分,然后让每个使用者沿着其块中的页面前进:

const numSegments = 6var互斥量同步。RWMutexvar读数[] * Readingvar wg sync.WaitGroupwg.Add(numSegments)for i:= 1;我< = numSegments; i ++ {segmentNum:= i go func(){page:= segmentNum for {pageReadings,err:= fetchGoodreadsPage(& conf,client,page)if err!= nil {...}如果len(apiReviews)< 1 {break} Mutex.Lock()读数= append(readings,pageReadings ...)Mutex.Unlock()页面+ = numSegments} wg.Done()}()} wg.Wait()

运行时间的减少非常适合教科书。有了6个使用者,程序从35秒减少到6秒。哇,胶印分页肯定很棒。每个人都应该使用它。

但是Goodreads在这里有点不正常。如果所有现代API都使用游标分页,那么偏移量并行化对我们没有帮助吗?

嗯,尽管基于游标的分页并没有像offset那样容易并行化,但是仍然可以使用类似的原理。

关键是允许至少指定一个其他过滤器,该过滤器将允许用户将总搜索空间分解为可并行化的部分。例如,Stripe的API允许许多列表终结点根据创建资源的时间进行过滤:

每个列表端点都是完全基于游标的,但是客户可以通过将他们感兴趣的总时间轴分为N个消费者的N个部分来进行分而治之,然后让每个请求列表都具有较高和较低的时间限制。每个使用者都有一个单独的游标,他们愉快地沿自己的细分进行分页,没有重复。

在后端实施此方法仍然非常有效,因为即使使用了其他过滤器,也很容易确保列表仍可以使用索引。就像使用无过滤游标一样,不需要其他与​​偏移量有关的记帐。对客户而言,这对服务器来说是最快的。

值得注意的是,使用诸如创建的时间戳之类的方法可以达到此目的,但这并不是完美的选择,因为它要求消费者发现自己的上下限,并且当对象在各个对象之间分布不均匀时,进行并行化将是一个挑战。时间线。假设您在2018年创建了100个对象,在2019年创建了1000个对象,在2020年创建了100万个对象。您无法将2018-2020年划分成相等的块,并期望工作能够很好地并行化。

一个真正友好的API提供程序可能会引入诸如针对每个用户和对象类型的计数器之类的计数器,该计数器会随着创建的新对象的增加而大致增加,并允许用户对此进行过滤。然后,客户可以检查最大界限,根据消费者数量进行一些简单划分,然后开始工作。

1最近,从Goodreads导出数据变得越来越重要,因为他们计划完全淘汰API访问。

2更具体地说,它实际上是基于页面的(?page = 1),但在后端与基于偏移的功能相同。