Uber将其自主研发的搜索索引系统切换到了OpenSearch,原因是针对大规模流式数据处理,引入了基于拉取机制的数据导入框架。这一变革的目的是为了提升实时索引任务的可靠性、应对压力波动的能力以及系统的恢复能力。由于产品需求的变化,维护自有搜索平台的成本和复杂性日益增加,尤其是在数据结构演变、相关性调整以及多地区数据一致性保障等方面存在诸多挑战。
Uber的搜索基础设施支持行程查找、配送选项选择以及基于位置的查询功能,能够近乎实时地处理连续产生的事件流。其原有的自主研发搜索平台采用基于推送机制的数据导入方式,即上游服务直接将数据写入集群。虽然这种方案在小规模应用中效果良好,但在面对突发流量或系统故障时,就会出现数据写入失败或需要重复尝试的情况。
而基于拉取机制的数据导入方式将责任转移给了OpenSearch集群。各个数据分片会从Kafka或Kinesis这类持久化数据流中获取数据,这些数据流起到了缓冲的作用,使得数据导入过程能够以可控的速度进行,同时系统也能有效处理压力波动,并支持数据重放以实现故障恢复。Uber的工程师们表示,这种方案大大减少了在高流量峰值期间索引任务出现的失败次数,也简化了系统的运维流程。以往那些会导致数据分片队列堵塞的突发流量,现在都被每个数据分片对应的有限容量队列所吸收,从而提升了系统的吞吐量和稳定性。
在高流量峰值期间,基于推送机制与基于拉取机制的数据导入方式对比(来源:Uber技术博客)
基于拉取机制的数据处理流程包含多个相互协作的组件。事件数据会被生成到Kafka或Kinesis的主题中,每个数据分片会被映射到一个特定的数据流分区中,从而确保数据能够被准确重放。数据消费者会从这些数据流中读取消息,并将它们放入阻塞队列中,这种设计使得数据消费过程与数据处理过程相互分离,同时也支持并行写入操作。消息会由专门的线程进行处理,这些线程会先对消息进行验证、转换,然后再生成索引请求,最后将这些请求传递给数据导入引擎。该引擎会直接将处理后的数据写入Lucene数据库,同时会记录已处理的数据位置信息,以便在系统出现故障时能够准确恢复数据。
基于拉取的数据流式导入架构(来源:Uber技术博客)
据Uber工程师介绍,基于拉取的导入方式还提供了细粒度的操作控制功能。外部版本控制机制能够确保顺序混乱的消息不会覆盖最新的更新内容,而“至少一次处理”机制则能保证数据的一致性。运营人员可以配置相应的故障处理策略:在“丢弃策略”下,错误消息会被直接删除;而在“重试策略”下,这些消息会被无限次地重新尝试处理。API还允许用户暂停、恢复数据导入操作,或将导入位置重置到特定值,从而帮助团队在系统出现故障后及时处理积压的任务。
Uber支持两种数据导入模式。其中,“分段复制模式”仅会在主分片上导入数据,而副本分片则会下载已完成的数据段,这种方式虽然能降低CPU使用率,但会导致数据可见性略有延迟;“全活跃模式”则会在所有分片上同时进行数据导入,因此能够提供近乎实时的数据可见性,不过这种模式的计算成本也会相应增加。
基于拉取的数据导入机制是Uber高度可用、多区域搜索架构的核心。每个区域的OpenSearch集群都会从全球范围内聚合的Kafka主题中获取数据,从而构建出完整且最新的索引。这样的设计能够确保数据的冗余性、全局一致性以及无缝的故障切换能力,因此用户无论身处世界哪个角落,都能使用到一致性的搜索服务,同时系统也能保持高度的可用性。
基于拉取的索引构建模型(来源:Uber技术博客)
Uber正在逐步将所有搜索相关功能迁移到OpenSearch的基于拉取的数据导入机制上,从而朝着一个云原生的、可扩展的架构方向发展。同时,Uber也会继续优化这一平台,并为OpenSearch社区做出贡献。