Open GarinZ opened 3 years ago
原文:How we fortified Twitter's real time ad spend architecture
推特是广告商与受众接触的一个热门平台。当广告商发起一个新的计划(campaign)时,他们会指定一个广告预算(budget),限制他们将花费多少钱。Twitter的Ad Server校验计划的预算,以确定是否继续出广告。如果没有这种校验,我们可能会在计划达到其预算限制后继续出广告,但却无法对账户扣费(charged)。这种情况我们称为超支或超投(overspend)。
超支可能导致Twitter的收入损失(因为机会成本,比如:本来我们可以在这个广告位(slot)显示其他广告),所以建立可靠的系统来防止超支是非常重要的
广告服务数据流(ad serving pipline)的简单架构图如下:
给定我们需要处理的一定范围的广告事件(Twitter每秒需要处理上百万个跨所有数据中心(data center)的广告事件),在整个数据流中高延迟(latencies)或者硬件故障可能在任何时候发生。如果Spend Cache不能精确累加消耗然后更新计划消耗,那么Ad Server就会拿到过期的消耗信息,继续出一些已经到达最大预算的计划。但广告平台却不能扣减超出预算的余额,与此同时而我们本可以出一些还没有将预算耗尽的计划下的广告,最终导致Twitter收入的损失。
举个例子,假如有一个计划每天的预算是¥100,CPC价格(price-per-click,这里用国内通用术语“CPC价格”替代)是$0.01。在没有任何超支的理想情况下,每天可以为这个计划产出1w次点击。 假如Ads Callback数据流或Live Spend Counter发生故障,导致Spend Cache没有更新,丢了价值$10的广告事件。那么Spend Cache中就会只有$90的消耗,而实际在计划上已经消耗了$100。这个计划就会免费拿到1k次额外点击
Twitter有多个DC(Data Center),每个DC都有一整份广告服务数据流(ad serving pipline)的副本,包含了Ads Callback Pipline、Live Spend Counter、Spend Cache。当用户和广告发生交互行为,广告回调事件就会被路由到一个DC中,然后首先被Ads Callback数据流处理
这就带来一个问题,每个DC都独立计算自己接收到的广告行为(engagment)事件和总消耗,而广告主的预算是跨数据中心的,这意味着每个DC中的消耗信息是不完整的,任何广告主的实际消耗可能被低估了。
为了解决这个问题,我们在回调事件队列(callback event queue)中加入了跨DC复制机制,所以每个DC都处理全量的广告行为事件,这样可以保证每个DC的消耗数据都是准确的。
PS:从图中看就是Ads Callback和Live Spend Counter采用MQ进行通信,原来用的是单个Queue,这个Queue只由同DC的Live Spend Counter消费。改进后增加了N个Queue,N等于DC数,每个Queue由对应DC的Live Spend Counter消费,每个DC中Spend Cache存储的都是全量计划消耗。这种方案可以提供最终一致性。
尽管复制广告交互事件可以为我们提供更好的一致性和更精确的消耗信息,但系统容错性(fault tolarant)仍然不太好。比如在某几周,跨DC复制故障了,就会有一些事件丢了或者延迟了(lagging),那么Spend Cache中的数据就会被污染。Ads Callback数据流会在GC停顿或单DC网络抖动(unreliable network connection)的时候发生事件处理延迟。但既然这是单DC问题,同DC部署的Live Spend Counter和Spend Cache也会有同等比例的延迟,最终导致超支。
之前我们通过停用故障DC的Live Spend Counter来实现故障转移,然后让另一个DC的Live Spend Counter双写Spend Cache(既写自己的Spend Cache,也写故障DC的Spend Cache),直到Ads Callback和Live Spend Counter没有延迟,并追上了当前的事件消费速度。 这种方法可以暂时性的避免超支,但有几个主要缺点:
由于当前架构存在很多问题,我们重新设计了数据流。目标是在更有弹性(resilient)的应对故障、更少的人工介入(operator intervention)。这个方案包含两个主要的组件:
在正常情况下系统和之前的运行方式一样,当本地的Spend Cache数据落后了,Ad Server就可以检测到故障,然后自动使用由远程DC写入的数据集。当故障被解决了或者在两份数据健康状况相同的情况下,Ad Server会自动切换回由本地写入的数据版本
我们决定数据健康的标准是从以下两种常见的故障中总结出来的:
为了构建一个兼顾两种因素的健康检查策略,我们引入一个叫做消耗直方图(spend histogram)的概念
假设我们有一个滑动窗口(rolling window),它描述的是给定时间内每个DC中的Live Spend Counter处理的实时广告交互事件总数。我们将滑动窗口的范围设置为:保留最近1min内每毫秒处理了多少消息,超出滑动窗口范围的累加值就丢弃。这样我们可以根据其中的数据得出一个消耗直方图,直方图描述的是Live Spend Counter在最近60s内处理的事件总数。假如直方图画出来应该是像下面这样:(纵坐标是消息总数,横坐标最近0-60秒) 在ads serving侧可以根据这个直方图和最近的消息时间戳来决定哪一个最佳的消耗数据集。Live Spend Counter会把这个信息作为元数据,然后把这个元数据和消耗数据一起写入到Spend Cache中
Live Spend Counter不会在执行写操作的时候做整个直方图的序列化/反序列化。在写操作之前,它就会持续加总和聚合时间窗口中count值。使用一个近似的count就足够了,它只是用作表示当前DC的Live Spend Count健康状况的signal。为什么用它做signal呢?因为假如Live Spend Counter或Ads Callback出现严重故障,那么这个count就会显著下降。如果故障不严重,那么count值相差也不会很多。
刚刚提到的元数据结构如下所示:
struct SpendHistogram { i64 approximateCount; i64 timestampMilliSecs; }
当处理一个请求的时候,Ad Server读取本地和远程的数据集,然后根据下面的DC选择策略使用Spend Histogram判断使用哪一个消耗数据集
选择数据集的逻辑如下所示:
以上逻辑可以用下面这个真值表(truth table)来描述:
我们使用ts和tc决定切换数据源的阈值。如你所见,在阈值内我们更偏向(bias)使用本地数据集。阈值需要能够让我们在切换数据集之前尽早的检测到故障,为此我们做了一些实验。每次处理广告请求的时候Ad Server都需要执行DC选择策略,所以我们把这个阈值放到本地缓存每秒刷新一次,避免频繁查询导致影响整体性能。
ts
tc
通过下图以可视化的方式展现了从一个DC切换到另一个DC是如何发生的。当前DC1中的LSC发生故障,Ad Server自动选择DC2中的数据集
我们目前讨论只涉及2个DC。通过在跨DC写入(cross data center)组件中引入复制因子(Replication Factor)把这个方法扩展到N个DC。Replication Factor控制的是Live Spend Cache写入的远程DC数。在Ad Server读取链路上逻辑相同,仍然选择最健康的数据集,但需要在一个读操作(批量读)中获取所有数据
举个例子,假设我们把Replication Factor设置为2,DC1中的LSC将会写入DC1、DC2、DC3的Spend Cache;DC2的LSC将会写入DC2、DC3、DC4;DC3的LSC将会写入DC3、DC4、DC1,以此类推。下图展示了3个DC的写入过程。在每个DC中,Ad Server会读取3份Spend Histogram的拷贝从中选择最适合的。基于网络和存储限制,在实际场景中我们设置的Replication Factor = 2。
在完成了这些升级之后,我们立刻发现团队的运维成本负担下降了。我们从过去每个季度都会有几个超支的线上事故,到最近半年事故数已经降到了0。这解放了大量用于运维问题的工程时间,也避免了由于架构问题导致的广告主赔付(returning credits)
通过定义系统健康指标、并根据这些指标设计足够简单且可自动化的的工程方案,我们最终解决了一个影响数据流正确性的问题。这不仅提供了系统容错性和弹性,也解放了工程资源。
张总牛哇
推特是广告商与受众接触的一个热门平台。当广告商发起一个新的计划(campaign)时,他们会指定一个广告预算(budget),限制他们将花费多少钱。Twitter的Ad Server校验计划的预算,以确定是否继续出广告。如果没有这种校验,我们可能会在计划达到其预算限制后继续出广告,但却无法对账户扣费(charged)。这种情况我们称为超支或超投(overspend)。
超支可能导致Twitter的收入损失(因为机会成本,比如:本来我们可以在这个广告位(slot)显示其他广告),所以建立可靠的系统来防止超支是非常重要的
名词解释
背景
广告服务数据流(ad serving pipline)的简单架构图如下:
超支(overspend)
给定我们需要处理的一定范围的广告事件(Twitter每秒需要处理上百万个跨所有数据中心(data center)的广告事件),在整个数据流中高延迟(latencies)或者硬件故障可能在任何时候发生。如果Spend Cache不能精确累加消耗然后更新计划消耗,那么Ad Server就会拿到过期的消耗信息,继续出一些已经到达最大预算的计划。但广告平台却不能扣减超出预算的余额,与此同时而我们本可以出一些还没有将预算耗尽的计划下的广告,最终导致Twitter收入的损失。
举个例子,假如有一个计划每天的预算是¥100,CPC价格(price-per-click,这里用国内通用术语“CPC价格”替代)是$0.01。在没有任何超支的理想情况下,每天可以为这个计划产出1w次点击。 假如Ads Callback数据流或Live Spend Counter发生故障,导致Spend Cache没有更新,丢了价值$10的广告事件。那么Spend Cache中就会只有$90的消耗,而实际在计划上已经消耗了$100。这个计划就会免费拿到1k次额外点击
跨DC一致性(Consistency Across Data Centers)
Twitter有多个DC(Data Center),每个DC都有一整份广告服务数据流(ad serving pipline)的副本,包含了Ads Callback Pipline、Live Spend Counter、Spend Cache。当用户和广告发生交互行为,广告回调事件就会被路由到一个DC中,然后首先被Ads Callback数据流处理
这就带来一个问题,每个DC都独立计算自己接收到的广告行为(engagment)事件和总消耗,而广告主的预算是跨数据中心的,这意味着每个DC中的消耗信息是不完整的,任何广告主的实际消耗可能被低估了。
为了解决这个问题,我们在回调事件队列(callback event queue)中加入了跨DC复制机制,所以每个DC都处理全量的广告行为事件,这样可以保证每个DC的消耗数据都是准确的。
PS:从图中看就是Ads Callback和Live Spend Counter采用MQ进行通信,原来用的是单个Queue,这个Queue只由同DC的Live Spend Counter消费。改进后增加了N个Queue,N等于DC数,每个Queue由对应DC的Live Spend Counter消费,每个DC中Spend Cache存储的都是全量计划消耗。这种方案可以提供最终一致性。
单DC故障(Singele Data Center Failures)
尽管复制广告交互事件可以为我们提供更好的一致性和更精确的消耗信息,但系统容错性(fault tolarant)仍然不太好。比如在某几周,跨DC复制故障了,就会有一些事件丢了或者延迟了(lagging),那么Spend Cache中的数据就会被污染。Ads Callback数据流会在GC停顿或单DC网络抖动(unreliable network connection)的时候发生事件处理延迟。但既然这是单DC问题,同DC部署的Live Spend Counter和Spend Cache也会有同等比例的延迟,最终导致超支。
之前我们通过停用故障DC的Live Spend Counter来实现故障转移,然后让另一个DC的Live Spend Counter双写Spend Cache(既写自己的Spend Cache,也写故障DC的Spend Cache),直到Ads Callback和Live Spend Counter没有延迟,并追上了当前的事件消费速度。 这种方法可以暂时性的避免超支,但有几个主要缺点:
跨DC写入方案(Cross Data Center Writes Solution)
由于当前架构存在很多问题,我们重新设计了数据流。目标是在更有弹性(resilient)的应对故障、更少的人工介入(operator intervention)。这个方案包含两个主要的组件:
在正常情况下系统和之前的运行方式一样,当本地的Spend Cache数据落后了,Ad Server就可以检测到故障,然后自动使用由远程DC写入的数据集。当故障被解决了或者在两份数据健康状况相同的情况下,Ad Server会自动切换回由本地写入的数据版本
如何做数据集的健康检查?
我们决定数据健康的标准是从以下两种常见的故障中总结出来的:
为了构建一个兼顾两种因素的健康检查策略,我们引入一个叫做消耗直方图(spend histogram)的概念
消耗直方图(spend histogram)
假设我们有一个滑动窗口(rolling window),它描述的是给定时间内每个DC中的Live Spend Counter处理的实时广告交互事件总数。我们将滑动窗口的范围设置为:保留最近1min内每毫秒处理了多少消息,超出滑动窗口范围的累加值就丢弃。这样我们可以根据其中的数据得出一个消耗直方图,直方图描述的是Live Spend Counter在最近60s内处理的事件总数。假如直方图画出来应该是像下面这样:(纵坐标是消息总数,横坐标最近0-60秒) 在ads serving侧可以根据这个直方图和最近的消息时间戳来决定哪一个最佳的消耗数据集。Live Spend Counter会把这个信息作为元数据,然后把这个元数据和消耗数据一起写入到Spend Cache中
Live Spend Counter不会在执行写操作的时候做整个直方图的序列化/反序列化。在写操作之前,它就会持续加总和聚合时间窗口中count值。使用一个近似的count就足够了,它只是用作表示当前DC的Live Spend Count健康状况的signal。为什么用它做signal呢?因为假如Live Spend Counter或Ads Callback出现严重故障,那么这个count就会显著下降。如果故障不严重,那么count值相差也不会很多。
刚刚提到的元数据结构如下所示:
当处理一个请求的时候,Ad Server读取本地和远程的数据集,然后根据下面的DC选择策略使用Spend Histogram判断使用哪一个消耗数据集
DC选择策略(Data Center Selection)
选择数据集的逻辑如下所示:
以上逻辑可以用下面这个真值表(truth table)来描述:
我们使用
ts
和tc
决定切换数据源的阈值。如你所见,在阈值内我们更偏向(bias)使用本地数据集。阈值需要能够让我们在切换数据集之前尽早的检测到故障,为此我们做了一些实验。每次处理广告请求的时候Ad Server都需要执行DC选择策略,所以我们把这个阈值放到本地缓存每秒刷新一次,避免频繁查询导致影响整体性能。通过下图以可视化的方式展现了从一个DC切换到另一个DC是如何发生的。当前DC1中的LSC发生故障,Ad Server自动选择DC2中的数据集
扩展到N个DC(Extension for n-DCs)
我们目前讨论只涉及2个DC。通过在跨DC写入(cross data center)组件中引入复制因子(Replication Factor)把这个方法扩展到N个DC。Replication Factor控制的是Live Spend Cache写入的远程DC数。在Ad Server读取链路上逻辑相同,仍然选择最健康的数据集,但需要在一个读操作(批量读)中获取所有数据
举个例子,假设我们把Replication Factor设置为2,DC1中的LSC将会写入DC1、DC2、DC3的Spend Cache;DC2的LSC将会写入DC2、DC3、DC4;DC3的LSC将会写入DC3、DC4、DC1,以此类推。下图展示了3个DC的写入过程。在每个DC中,Ad Server会读取3份Spend Histogram的拷贝从中选择最适合的。基于网络和存储限制,在实际场景中我们设置的Replication Factor = 2。
影响和结论
在完成了这些升级之后,我们立刻发现团队的运维成本负担下降了。我们从过去每个季度都会有几个超支的线上事故,到最近半年事故数已经降到了0。这解放了大量用于运维问题的工程时间,也避免了由于架构问题导致的广告主赔付(returning credits)
通过定义系统健康指标、并根据这些指标设计足够简单且可自动化的的工程方案,我们最终解决了一个影响数据流正确性的问题。这不仅提供了系统容错性和弹性,也解放了工程资源。