http://www.zenlife.tk/bid-budget-control.md
广告实时竞价这边,出价是要涉及到钱的。客户在我们这边建一个广告投放活动。这个活动有预算,如果预算用完了,那么这个活动就不应该继续参与竞价了。预算控制是竞价服务其中一个模块。
那么现在存在什么问题呢?根本控制不住!预算会超!这让我们很不好跟客户交待。
为什么会超呢?活动预算相关的数据是在另一个数据库中,从数据库到竞价服务,数据是有延迟的。竞价服务这边并不知道某活动的预算已经用完了,继续出价。由于竞价请求的QPS很高,稍微一点点的延迟,就超预算了。
这个问题还要从架构演化谈起。
据说起初(我还没来这里之前)竞价服务是每次去活动数据库中取数据。这样数据是一致的,但是竞价服务对响应时间要求非常高,取一条数据要去访问数据库一次数据库根本没法玩,即使是redis也不行。
活动这一部分的数据量并不大,所以后来就在竞价进程内部做了个活动数据库,然后做一个dispatch服务负责周期性地从数据库同步到竞价进程内,相当于是做一个本地缓存。
预算数值变化的流程是这样的:
- 由dispatch将活动数据同步到竞价进程中
- 预算没超,竞价进程让活动参与竞价(读)
- 获胜通知的进程与数据库交互,修改预算(写)
- 如此反复
这里有几个点需要注意。一个是数据流动是异步的,修改预算并不是由竞价进程执行,竞得后的处理是其它进程。另一个是竞价进程是分布的,有许许多多进程。我们要尽量的让竞价进程是"无状态"的,它是一个计算型服务,性能方面要求很高。前面有ngnix做负载均衡,把竞价请求转发到各个竞价进程中。
这个流程,数据延迟不可避免,于是预算控制就控不住了。
说到底,这是一个性能跟一致性的矛盾,为了竞价进程的性能,必须本地缓存数据。本地缓存数据之后,每个竞价进程不知道其它竞价进程对数据的修改。另外,反馈也是异步的过程,最终竞价进程拿到的都是非真实的预算在做决策。
说一说解决,只是提下思路。
如果是从业务层面想办法,可以用一个概率算法,在快要接近预算耗尽时,就降低此活动出价的概率,这样子速度慢下来,就可以缓解预算不会超得太多。
架构层想办法,现在的同步是由dispatch周期性地拉。如果能改成一个主动推,实时性就会提高。获胜通知那边,如果发现预算已经不多了,除了写数据库,同时还向竞价服务推更新消息。
http://www.zenlife.tk/%E5%86%8D%E7%9C%8B%E9%A2%84%E7%AE%97%E6%8E%A7%E5%88%B6%E6%A8%A1%E5%9D%97.md
这几天看了一下redis集群,老大让看看把这部分数据迁到集群中。一看吓一跳:算钱相关的,居然redis用作存储,这胆子也是够肥的!
数据量其实很小,不过之前还是业务层做的hash分到多个redis实例中的,但是没有主从。如果redis挂了,预算就完全不受控,然后就没法跟客户交待了。
首先明确一点:redis是一个AP的系统,不保证C,而且...是会丢数据的。如果切换到redis集群可以解决的是容错和可用性问题。但是不会解决的是一致性以及数据不丢。
redis为什么会丢数据?从单机的角度,写aof是周期性的,只有fsync刷盘之后,数据才是真正的持久化了,期间进程挂掉或者突然断什么等意外都可能导致数据丢失。升到集群之后,由于是先回复请求,再异步写副本的,数据也会丢。比如说master回复client写ok,然后master挂了,数据还没有写到slave。接着slave提升为master,这次写就丢失了。
对redis集群做了一个简单的测试,跟单个实例相比,读的性能基本上没有损失。但是写操作性能降低很大。
因为要多一份异步写副本操作,平均到每个实例的处理能力下降了好多。比如在我们内网机器上测试单实例的set操作,不开pipeline,无slave,大概是7-8w的qps。测试3主3从的redis集群,整集群的吞吐量大概才12w,平均到每个实例不到4w了。3个master的CPU跑满之后,slave的CPU大概在40~50%的样子。
回头再看我们的业务,每投出一次广告,都要修改预算数据。而读是周期性(比如10秒)由dispatch读出,再走消息队列同步到竞价服务那边。所以,这是一个写远高于读场景。
总结一下,换到集群是满足了可用性。但是一致性方面,不适合算钱的业务。另外业务模型上面主要是写操作对redis集群也不算啥优势,虽然以现在的的压力完全没任何问题。做肯定是能做,不过这里并不是一个比较适合redis集群的场景。
http://www.zenlife.tk/dsp-challenge.md
DSP,全称是Demand Side Platform,即需求方平台。虽然我喜欢开玩笑称DSP是比ASP高端一点点的东西,但其实完全不是一个东西啦。我们的客户是广告主,即那些想投放广告的人。
但是我们并没有广告位,我们是参与竞价,购买广告位曝光机会,然后帮客户将广告放出去。你们看到的那些讨厌的小广告,就是我们帮客户放的啦...
这里又引出两个名词,一个是AdExchange,即广告交易中心。另一个是RTB,实时竞价。这是一种新兴的广告模式,AdExchange那边掌握着广告位曝光机会,用户打开网页,但是看到什么样的广告,目前是未知的。广告交易中心就是拿着这次广告展示的机会去拍卖。每一次的用户访问广告位,就会产生一条竞价请求。AdExchange会将这条竞价请求发给所有接入的DSP,然后各个DSP会出价,购买这次的广告位曝光机会。谁出价高,说就可以抢到,最终将自己的广告展示给用户。
我认为DSP最核心的部分,一块是算法,另一块是RTB。算法重要性不言而喻,实现的是精准广告的基础。如果我知道用户信息,就可以知道为他展示什么的广告。比如,某用户昨天刚在淘宝上看过某首饰,那么就为他推荐首饰广告(所以那些总是弹出很黄很暴力的广告的,你们可以去面壁了,你看你都访问了些什么)。至于RTB这边,就是系统性能方面的一些挑战了。
用户每次打开带广告的网页,就会产生竞价请求过来,到达我们系统。想想同时有多少人在上网,就知道这边的请求量有多么恐怖。我们每天要处理70-100亿的请求数。如果你没什么感性的认识,可以类比一下抢火车票。N多用户同时进来,然后系统就挂了。但是我们又有些不同的地方,首先,我们流量是一直都很大的,不是说某个时刻秒杀一下。其次,我们对响应要求很苛刻。不像秒杀场景,顶多排个队,用户感觉慢就慢呗,实在处理不过来的时候直接给404,只要保证一致性就好了,反正系统不出错,不挂掉,用户体验不爽就骂呗,像12306被骂着也习惯了。
一般AdExchange竞价请求后,要求DSP在120ms以内给予回复,进行出价。如果超时了,就判定默认不出价。如果经常超时,说明这个系统做的烂,那么AdExchange就会对它降级,给的流量就少,DSP就赚不到钱咯。
所以实时竞价这一块是DSP关键之一。说到这个120ms,是指从AdExchange发出消息,到AdExchange收到DSP返回的消息,那么中间还有一次网络来回的开销。为了在网络抖动的情况下也能成功竞价,那么留给DSP的实际处理时间在大概30ms以内。
所以实时竞价这一块是DSP关键之一。说到这个120ms,是指从AdExchange发出消息,到AdExchange收到DSP返回的消息,那么中间还有一次网络来回的开销。为了在网络抖动的情况下也能成功竞价,那么留给DSP的实际处理时间在大概30ms以内。
响应时间波动是很难应对的,因为各种因素累积起来,比如跑着的另外的进行占用CPU过高了,影响了竞价服务的进程。比如某个周期性的任务占了网络IO,这些是无法控制的。实际上目前我们现在的系统平均处理时间大概在10ms以内,这样在发生波动也能尽量保证不超时。亮出秘密武器:我们系统用Go语言写的。
Go语言很好用,不用花多少力气就可以轻松发挥出多核CPU的优势,并且在网络处理方面做得非常好。但是这并不是免费的午餐,Go会有垃圾回收,这时会Stop the world。无疑会造成响应时间波动。
Go语言很好用,不用花多少力气就可以轻松发挥出多核CPU的优势,并且在网络处理方面做得非常好。但是这并不是免费的午餐,Go会有垃圾回收,这时会Stop the world。无疑会造成响应时间波动。
cookie map的存储是另一块难点。AdExchange那边会有一个用户cookie,而我们这边也会有一个用户cookie,cookie map做的就是一个映射关系把两者联系起来。这样就可以通过AdExchange那边的cookie,就查到我们的用户cookie,进而通过这个cookie查询用户特征数据库,继而进行更精准的广告投放。这中间涉及了两次的数据库查询。
竞价流程中还会有一个频次控制会查一次数据库,所以每一次请求会有三次数据库查询。如果Go的垃圾回收有5ms左右的影响(实际上如果对象过多以后远不止),要求系统处理时间10ms以内,那么留给一次查询时间就要在1ms。
竞价流程中还会有一个频次控制会查一次数据库,所以每一次请求会有三次数据库查询。如果Go的垃圾回收有5ms左右的影响(实际上如果对象过多以后远不止),要求系统处理时间10ms以内,那么留给一次查询时间就要在1ms。
这里引入两个问题,每一个是redis集群的管理,redis本身的分布式是做得很差的。另一个是存储量,全内存肯定是吃不消的,然而我们的使用场景又对响应时间和tps要求特别高。有没有什么好的方案呢?SSD也许是一个方向