虽然大多数人都熟悉Uber,但并非所有人都熟悉优步货运, 自2016年以来一直致力于提供一个平台,将托运人与承运人无缝连接。我们正在简化卡车运输公司的生活,为承运人提供一个平台,使其能够浏览所有可用的货运机会,并通过点击一个按钮进行预订,同时使履行过程更加可扩展和高效。
为托运人提供可靠的服务是优步货运获得他们信任的关键。由于承运人的表现可能会大大影响货运公司服务的可靠性,我们需要对承运人透明,让他们知道我们对他们负责的程度,让他们清楚地了解他们的表现,如果需要,他们可以在哪些方面改进。
为了实现这一目标,优步货运公司开发了承运人记分卡,以显示承运人的几个指标,包括对应用程序的参与度、准时取货/交货、跟踪自动化和延迟取消。通过在承运人应用程序上近乎实时地显示这些信息,我们能够实时向承运人提供反馈,使我们在行业内的大多数竞争对手中脱颖而出。
后台要求
在规模上快速访问新鲜数据是建立可扩展的运营商记分卡后端的关键要求。
考虑的潜在解决方案
一个表存储原始事件,事件更新触发一个异步函数来更新另一个汇总表中的所有相关指标。
Apache Pinot是一个实时、分布式和可扩展的数据存储,旨在以面向用户的分析所需的超低延迟执行分析查询。一个单一的逻辑表(又称混合表)可以被设置为实时和离线摄取,基于 lambda架构.
为了实现对实时数据的精确分析,我们决定使用Lambda架构,利用Kafka、Flink和Pinot。然后,一旦数据生成,我们就用Redis进行缓存,并通过Golang GRPC端点向我们的下游客户(运营商应用程序和网络平台)提供汇总的指标。
Apache Kafka是Uber技术栈的一个基石。我们拥有世界上最大的Kafka部署之一,并且做了大量有趣的工作来确保它的性能和可靠性。随着货运业务的增长,Kafka可以轻松地扩展。
Apache Flink是一个开源框架,用于对数据流进行有状态的计算。在我们的用例中,这对于从其他后端服务消耗原始事件、过滤不相关的事件、将它们映射到持久化的状态、确定性能质量,以及输出到具有共同事件模式的Kafka主题是必要的。Flink可以处理非常大的流量,也有很好的容错能力。
Apache Pinot是一个开源的、分布式的、高度可扩展的OLAP数据存储,它为每秒有数千次并发查询的网络规模的应用提供低查询延迟(即P99延迟在几秒钟之内)。它支持对单个表进行SQL分析。它使用现在流行的Lambda数据架构,从实时流和批处理数据源摄取数据,用于历史数据。
在货运公司的用例中,Pinot使用来自Kafka的实时数据摄取来覆盖过去3天内创建的数据。对于历史数据,Pinot从HDFS摄取,以覆盖从3天前到时间开始的数据。离线摄取管道有内置的回填能力,可以在需要时对以前的数据进行修正。
Apache Pinot提供了丰富的索引优化技术,如倒置、星形树、JSON、排序列等。索引来加速查询性能。例如,在 星型树预聚合索引可以加快查询速度,总结出设施的平均等待时间。快速的查询使承运人在预订货物之前,在承运人的应用程序上查看等待时间,这是一种互动体验。
输出主题提供了一个一般模式,每个事件有一行。这使我们能够在未来添加额外的事件名称选项,以支持未来的需求。它还提供了基础层面的数据点,可用于汇总我们的记分卡所需的所有指标。
下面是一个常用查询的例子,用于提取某一时间窗口内某一承运人完成的工作总数和行驶的里程。
过滤器条款中使用的值是根据客户提供的API请求输入而变化的。
货运后端服务通过一个内部的事件聚合服务将事件数据输出到Kafka。从这个统一的事件流主题,我们可以将这些Kafka事件消费到我们的Flink流处理引擎中。来自这个主题的事件包括诸如预约时间变化、实际到达预约地点的时间、货物状态变化等等。
一旦货件被承运人预订,就会为给定的货件UUID创建一个状态对象。这个货件UUID可以在将来用来检索当前的状态,并参考有关该货件的共同细节。
每当一个里程碑被击中,Kafka消息就会被输出到我们之前讨论的数据模式中的sink主题。里程碑的一个例子是我们的自动跟踪得分。如果一个站点被司机标记为 "到达",而不是被我们的运营团队标记为 "到达",那么auto_arrived_at_stop的值会被输出,布尔值为True。
为了能够重新启动作业,从上次离开的地方继续前进,Flink将创建检查点并将其存储在HDFS中。为了对键入的状态进行处理,状态对象被序列化,然后保存到检查点文件中。当工作重新启动时,状态会从最近的检查点加载,并且对象会被反序列化为Java实例。当我们试图在状态对象中添加一个新的字段时,问题就出现了。工作未能从检查点加载,因为序列化的对象无法被反序列化为新的对象实例。为了解决这个问题,我们利用了Apache AVROᵀᴹ来为状态对象定义一个模式。从这个模式中,AVRO生成的对象可以被安全地序列化和反序列化,即使字段被改变,只要这些改变遵守了模式演化规则.
当我们刚开始在staging中运行我们的Flink作业时,我们一直遇到内存问题,作业会崩溃。我们试图修改这些数值,但要确保我们的工作顺利运行,获得正确的配置并不是一个简单的过程。幸运的是,Uber的一位同事分享了一个关于如何正确配置这些内存设置的非常有用的演示。如果你有兴趣了解更多信息,可以找到该演示文稿 这里如果你有兴趣了解更多。
对于每个混合Pinot表,在引擎盖下有两个物理表:一个用于实时数据,另一个用于离线历史数据。Pino头Broker通过执行离线和实时联合,确保实时表和离线表之间的重叠部分正好被查询到一次。
让我们来看看这个例子,我们有5天的实时数据--3月23日至3月27日,而离线数据已经推送到3月25日,这比实时数据晚了2天。经纪人维持这个时间界限。
假设,我们得到一个对这个表的查询:select sum(metric) from table。代理商将根据这个时间界限把这个查询分成两个查询--一个是离线查询,一个是实时查询。这个查询变成:select sum(metric) from table_REALTIME where date >= Mar 25 and select sum(metric) from table_OFFLINE where date < Mar 25。代理商在将结果返回给客户之前,会合并这两个查询的结果。
查询性能
下面的查询例子是针对Pinot表最常用的一个查询。该查询是一个分析性查询,有聚合、分组和过滤条款。目前的查询量约为每秒40次。在拥有10G数据的表中,Pinot能够提供~250ms的P99查询延迟。这种水平的查询性能为我们的货运应用用户提供了良好的互动体验。
为了实现表的250ms查询延迟,我们在Pinot表上使用两种类型的索引。
倒置索引可以将WHERE子句中相应过滤条件的查询速度提高10倍。
Neutrino是一个主要的查询网关,用于访问Uber的Pinot数据集。它是一个稍微不同的部署 胜利者在这里,每个主机上都运行着一个协调器和一个工作者,并且能够独立运行每个查询。Neutrino是一个托管在Mesos容器上的无状态和可扩展的常规Java微服务。它接受PrestoSQL查询,将其翻译成Pinot查询语言,并将其路由到适当的Pinot集群。本机和Neutrino Presto的主要区别是 在于,Neutrino做了积极的查询推送,以最大化底层存储引擎的利用率。
当用户在移动应用中打开或刷新运营商记分卡时,将同时获取5个指标,这相当于9个Neutrino查询,因为有些指标需要超过一个Neutrino查询。我们的Neutrino查询的P99延迟约为60ms,为了减少Neutrino的流量并改善外部延迟,我们在Neutrino前面添加了一个Redis缓存,用来存储聚合的指标。设置了12小时的TTL,随着新事件的不断涌入,我们使用以下策略来确保缓存的一致性。平均而言,我们能够实现>90%的缓存命中率。
通过让货运司机轻松获得他们当前的绩效分数,我们观察到所有关键指标都有了统计学上的显著提升。
这些性能的改善,仅在2021年就节省了150万美元的成本。
按业绩评级进行的深入调查显示,被评为 "有风险 "的承运人表现出最大的改善。
这不仅从节约成本的角度显示出很高的商业影响,而且从用户体验的角度来看也是如此。以下是一个 推荐书来自Uber货运平台上的一个承运人,她发现这个新功能对她自己的业务有好处
在这篇博客中,我们描述了Uber货运承运人应用程序中承运人记分卡的后端设计和实现,使用了Apache Pinot和Uber的流媒体基础设施。这种新的架构在生成低延迟(~250ms P99和~50ms P50)的分析指标方面效果惊人,否则就需要在多个在线数据存储上进行复杂的查询。我们的服务已经在生产中可靠地运行了一年多,维护费用非常少。
来源:
https://www.toutiao.com/article/7147027316055507459/?log_from=f353fe227cdbf_1664180912743
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!