
互联网广告系统本身是一个对稳定性和可靠性要求极高的系统,每天面对数十亿级别的请求,广告投放主多样的投放方式变化与用户关注点及兴趣频繁的更新,同时对时效性要求严格,而作为电商广告的计费系统,则要求更加严格, 从打点到计费任何一环节出现问题,都会带来巨大的经济损失和平台信任度危机,涉及到商家账户资金,系统实时反作弊和防刷,亿级别点击(曝光)等高效稳定账务扣费,数据的强一致性和最终一致性的保证及全链路高效可靠的监控..
本次主要介绍下蘑菇街广告计费系统的持续优化改进中,系统容灾方面做的一些事情
一.计费系统介绍:
广告对很多互联网公司营收占有非常重要的比重,对互联网广告大家应该不陌生,日常pc和app应该见过很多(弹窗广告,视频广告等),电商来说也是非常重要的营收方式,那么这些广告是怎样计费的,先来了解下几种常见的计费模式
CPC(Cost Per Click ) : 按点击计费的商业产品,对于电商,常用于站内广告资源位,用于推广商品
CPS (CostPer Sale ) :按成交计费的商业产品,站外引流,站外长尾流量,用于推广商品/店铺
CPM(Cost Per Mille, 或者Cost Per Thousand;Cost Per Impressions) : 按曝光计费的商业产品,站内banner位等资源,用于推广品牌店铺
其他还有比较常用的,如:CPD(按天计费),CPT(按时间计费),CPA(按激活或者行为等).
广告计费系统的数据流,如下图所示:

数据来源:web端用户对广告触发行为产生数据,主要是广告资源位上广告的曝光,点击等
数据收集:用于接收web产生的广告数据,为了保证下游系统的稳定性,盗刷及恶意流量一般这层要给拦截住,核心必须保证性能高效及数据防刷功能
数据处理:广告系统计费最核心的功能部分,主要是计费数据的处理,涉及到计费策略,计费金额核算,反作弊及对余额或者预算不足广告进行上下架等操作
数据存储:主要是对处理过的流水,账务及广告数据的持久化
和数据流结构对应,我们来了解下蘑菇街现在广告计费系统,整体架构如下,其中billingWorker是处理计费数据最核心的程序,unionLogAgent是自研支持规则定制和流量切分的数据收集程序,后面会对每个部分的技术及设计做重点的介绍.

二.系统容灾设计
计费系统的每个数据都和钱息息相关,系统不稳定,数据不可靠直接损失的是钱,主要面临着以下的核心问题, 怎么样保证系统与数据的可靠与稳定,是我们系统在容灾设计上需要去重点考虑的点:

这些问题我们是怎样考虑的了?接下来主要从数据/系统/链路完整性及监控方面重点介绍蘑菇街广告计费系统的容灾设计.
1.数据流
数据流面临的问题:
1.恶意流量攻击,异常数据对系统的冲击
2.数据回溯,快速定位失败数据
3.链路数据的一致性,数据丢失或者重复
面临着数亿级别曝光和点击,既要保证数据的一致性,不发送漏扣或多扣,又要保证系统的稳定可靠,整体在设计阶段必须重点考虑系统的容灾,先介绍下我们在数据采集阶段的设计.
▶数据采集
数据采集阶段,从整体的设计上,必须满足三个目标:
1.采集程序的性能要高效,尽量做到业务无关性,能弹性面对我们的热点流量
2.我们期望在最上层就将恶意流量拦截掉,避免对下游冲击,面对恶意盗刷或攻击(人为或者第三方程序刷点击,刷曝光,爬虫),能快速灵活定制化我们的规则拦截无效流量
3. 保证系统的稳定及能快速恢复,并且做到简单快速并保证无数据丢失
最开始在技术选型方面,主要考虑以下3种方案:

最终采用方案1-2,优点是 :
1.nginx只管接收数据, 日志落盘和收集解耦,不会有任何业务逻辑,能保证性能的高效
2.unionlogagent独立开发,完全自己维护,并提供防刷模块,支持规则定义及流量拆分,可以有效拦截高频恶意流量
3. 定制优化kafka客户端,对于每一条消息无论成功或失败都进行ack操作,确保不会出现数据丢失的可能
4. unionlogagent会实时持久化成功最小offset,出现异常能快速恢复并能快速自动回溯数据
方案0:公司默认的方案0,最大的问题是unionlog采用php,由于php线程切换导致性能不会很高,同时利用共享内存缓冲, 共享内存容量有限,面对热点流量冲击时,会造成共享内存满,数据丢失..同时logagent采用scala的kafka客户端,当客户端超时,消息重试3次仍失败,会丢弃而且业务无感知,可能造成数据丢失
方案2:数据收集和下发耦合,收集程序出现异常及重启过程,也会造成数据丢失,同时依赖web容器,环境重,性能无法比拟单纯nginx
其中unionlogagent主要的架构图如下:

防刷模块:主要是根据时间跨度,MD5校验和小黑屋来拦截异常流量
时间跨度,曝光和点击时间差或点击数据与当前时间差较大,就认为是异常流量,重点针对爬虫或者单接口攻击
MD5校验,会有前端用户身份标示及后端动态口令,保证数据的有效性,能有效拦截各种盗刷及恶意分享产生的行为
小黑屋更多和反作弊结合,根据用户某段时间的行为做黑名单处理
构造MD5唯一ID,这个是非常重要的点, 会对每条日志生成唯一身份标示,对日志追踪有非常重要的作用
unionlogagent还通过一套ACK机制,会持久最小成功offset的持久化,能支持下游链路灾备的切换,保证系统的容灾及数据的快速恢复及回溯
▶数据缓冲
数据缓冲部分的设计就比较简单,主要的消息中间件是用kafka, 通过redis做灾备(轻量级,出问题恢复时间及数据量可控,redismq能稳定支持)

之所以选用kafka,主要是kafka对于数据的持久性可靠性能保证,同时容错性做得非常好,比如kafka我们设置的副本数是3个,4台机器,就算集群中2台机器出现问题, 整个集群也能正常运行,同时在高并发和高吞吐上kafka也能很好的满足
这里要着重补充一点,为了保证kafka整个集群分区数据的一致性,我们将kafka的ack等级设置为all,保证异常情况就是出现主从分区切换,也不会出现数据丢失
▶数据处理
计费系统最核心的部分就是数处理,最核心功能是消息分发的负载均衡 及 消费端异常的接管,同时能支持很好的水平扩展, 这部分的设计,最终选用方案1
我们利用了kafka本身有partition的机制及容灾方面非常好的设计,基于kafka+zk的方式实现动态的负载均衡和调度,整个过程根据partition和billingWorker的数量,采用固定策略做自动负载分配及接管,无需人工参与,异常情况系统也具有自愈功能

当有新的partition进行扩展或者billingWorker出现异常或者扩容,系统能自动重新分配partition和消费的billingworker,支持系统自愈和平滑扩容的功能.
▶数据一致性
链路比较长,核心主要在两个阶段:数据收集和数据处理.中间面临着很多问题:每个节点的故障,kafka分区的切换,计费程序的故障,DB操作失败等等
出现这些问题时,怎样保证数据不丢失不重复,从全局能保证数据的一致性, 异常数据能够被系统感知并快速消费,因此只有在每个阶段消费成功的offset能够被追溯, 在出问题时才能快速定点恢复异常数据
要保证全局的一致性,那必须先保证局部的一致性,因此我们设计了两套ack机制来保证我们的数据一致..两套ACK机制的核心思想如下:

最小成功offset持久化保存,能支持失败数据快速回溯,利用fail队列支持重试机制,通过数据收集和数据处理阶段的ACK机制,通过局部数据的一致性,最终保证全局数据的一致性
2.系统容灾
系统稳定性面临的问题
1.系统幂等,扣费时效性及热点数据处理
2.系统大流量的冲击,系统的雪崩
3.自身AB及不停服切换,平滑升级
▶系统幂等性
为了保证数据具有去重功能,过程中需要对于失败数据的重复消费,灵活快速回溯各种数据,而且最终要避免多扣,那么扣费系统需要“幂等”功能,对于有状态环节,同一数据结果必须幂等,实现计费链路去重逻辑.

系统上为了实现幂等,首先必须保证每条数据的唯一性,刚刚在数据采集阶段也说过,日志的生产端的时候,就会构造单条扣费日志唯一性主键(GUID).
其次整个数据流上要做到无状态,整个系统有状态的环节,其实主要有两部分,一是:反作弊程序, 主要是由于反作弊规则中单用户对单广告有类似次数限制的规则,二是:计费程序,要保证这个阶段中间任何一环节,重试机制都需要加入去重逻辑,保证程序的幂等
讨论过很多次,从稳定又简单的方案考虑,我们最终选用缓存的方式保证反作弊唯一结果,DB的唯一索引来保证计费的去重逻辑
▶扣费时效性
效果广告计费是个非常实时的过程,对时效性要求非常高,出现延迟会导致投放时间差的漏洞,也会造成平台的损失(缓存点击),那么必须保证扣费的时效性
首先系统层面, 导致系统低效的原因主要如下:
1.第三方依赖(DB,redis)链接及数据传输网络开销
2.第三方逻辑耦合在主流程中,占用主链路的开销
3. 线程的频繁创建与销毁,频繁的上下文切换
4. 热点数据的分布不均匀,不能高效的利用CPU
针对这几个问题,我们的主要改进如下:
Ø减少不必要的依赖:强依赖转弱依赖
1、第三方依赖异步化(kafka/sentry/数据统计),保证主流程安全,同时提高主流程效率。
2、弱依赖埋好开关,在出现性能瓶颈时可以及时关闭。
3、连接池应用及网络部署优化,减少中间网络开销
Ø根据业务特点,找到相对合适的上下文切换
CPU密集型:线程数=2 *核数
IO密集型:线程数=核数/(1-阻塞系数) 阻塞系数=链路中IO占用的时间占比
除了系统外,扣费时效性上,在实际投放过程中,我们还面临着热点数据的挑战 ,存在热点商家或者热点流量,一种情况很多大商家全部被分到同一个线程队列中处理,则会导致某些线程阻塞而某些线程空闲,未能最大化并发效果, 处理不好同时还有可能造成系统雪崩;
1.过程中如果只是以特定的线程来处理一个队列,由于点击在商家维度的不均衡性,一定会出现某些队列堵塞。
2. 大队列堵塞会导致大量的缓存点击,会影响收入
针对这个问题,其实我们的解决方案核心思想很简单,就是:分而治之
1.按照userId的方式进行hash队列拆分,分拆合适粒度的队列,多队列并行,消费程序(billingWorker)分布式部署分配接管队列消费
2. 线程和队列分离,按队列数据长度,动态分配线程,提高热点数据消费效率.
▶限流降级方案
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
缓存:提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹
降级:暂时屏蔽掉非核心流程,避免对主流程造成影响,待高峰或者问题解决后再打开
限流:一个时间窗口内的的请求进行限速来保护系统,常见的限流算法有:令牌桶、漏桶、计数器也可以进行粗暴限流实现
而有些场景并不能用缓存和降级来解决,比如稀缺资源(流量飙升的会场,活动页广告)、写服务(如大量扣费,冲击下游),因此需有一种手段来限制这些场景的并发/请求量,即限流
计费系统主要采用三级限流保护:
一级:接入层限流
数据总入口,根据入口流量及系统流量瓶颈,进行限流。
二级:系统级限流
程序本身计费逻辑限流,保证队列不会出现堵塞:在程序数据拉取过程中,判断计费队列中的消息数量,比如当数量大于某个阈值(10000)时,限制流量进入(如:当前队列数据10240个,在QPS 4500时计费队列平均10000个),同时控制总量,避免内存队列堆积大量数据。
当出现断电式故障时,可以快速确定丢失的数量,进行数据恢复。
三级:重试机制限流
程序拉取上游数据限流:当程序下游雪崩时,程序会出现大量异常,异常数据会存储入fail队列,计费程序将根据异常的出现次数,fail队列的长度,决定是否继续从上游获取数据、是否进行不进行重试,走fail队列消费逻辑进行数据恢复。
▶系统自身AB&平滑上线
为保证系统的7*24小时服务,对于新改造的系统也需要保证稳定性,并支持小流量平滑上线 ,同时对于新的业务模式做一些计费验证,因此利用unionlogagent在数据收集阶段做流量切分服务,能够支持自定义规则的流量切分,保证我们的计费程序能做自身和业务的AB,同时也能小流量平滑上线,新程序出现故障时可以快速切回,主要的设计方案如下:

很多时候除了数据流层面和系统层面的问题,还面临整个链路完整性的问题,要保证后端接收到的数求是OK,那怎么去确认前端发送请求的链路没有丢失?后端业务数据处理没有异常? 前端链路是否出现流量劫持,是否出现第三方拦截?因此链路完整性也非常重要
同时系统必须具备一个非常重要的功能:监控,除了防范与未然,还需要更快的发现系统存在的问题,并能更好的定位问题,计费容灾下部分,将重点分享下此部分.