极光笔记 | DSP 高并发应用实践

作者:极光高级工程师 ——陈策
导言
广告业务简介

SSP:供应方平台,互联网有大量拥有用户流量的媒体网站和 APP,他们可以通过发布广告进行流量变现。而 SSP 就是可以实现流量变现的媒体服务平台,媒体可以通过它进行广告资源的管理,分配,定价,筛选。
DSP: 需求放平台,互联网有大量的广告主需要寻找优质的媒体和用户来推广自己的产品,并产生收益。而 DSP 就是为广告主提供服务的平台,他们可以在平台上设置自己的广告、目标受众、地域、出价等。
ADX:广告交易平台,连接买方和卖方,ADX 以拍卖的方式为各个 DSP 提供 SSP 委托管理的广告位和流量并最终决定竞得者。
广告核心竞价流程

用户 A 访问媒体
媒体向 ADX 请求广告展示给用户 A
ADX 向各个 DSP 发起竞价请求,询问是否需要竞价并传输用户 A 的设备信息、广告位信息等。
DSP 的竞价引擎接收到请求后,将用户数据和投放需求进行匹配,决定是否参与出价并作出响应,返回广告物料。这个过程必须在 100ms 内返回,否则会被 ADX 自动判定为放弃此次竞价。
ADX 接收到所有 DSP 的出价响应后由竞价决策服务进行竞拍,价高着得。并返回广告物料给 APP。
用户 A 看到 APP 的广告,当前竞价流程结束。
下面主要介绍极光 DSP 系统实战。
DSP 业务系统架构

DSP 的高可用网络架构

内部服务使用 kubernetes 集群进行管理,可做到如下几点:
1. 水平扩缩容
通过 k8s 管理平台对应用进行扩容和缩容。
2. 故障转移
当 node 节点关机或挂掉后,node 节点上的服务会自动转移到另一个 node 节点上,这个过程所有服务不中断。
3. 服务发现和负载均衡
当发生扩缩容时,无需配置自动实现服务发现和负载均衡。
4. 资源调度
当 node 节点上的 cpu、内存不够用的时候,可以扩充 node 节点,新建的 pod 就会被 kube-schedule 调度到新扩充的 node 节点上。
DSP 高性能技术框架及服务治理
极光 DSP 的技术框架选用了 vert.x-web, vert.x-grpc 并通过集成 springcloud 微服务组件对服务进行治理。
Vert.x
vert.x 是基于事件模型的异步非阻塞框架,具有异步编程和高并发的特性,能够充分利用多核 CPU 的优势,支持海量连接。工作原理如下:

Vert.x 中主要有两种线程:Event Loop 线程 和 Worker 线程。其中,Event Loop 线程结合了 Netty 的 EventLoop,用于处理事件。每一个 EventLoop 都与唯一的线程相绑定,这个线程就叫 Event Loop 线程。Event Loop 线程不能被阻塞,否则事件将无法被处理,它是典型的异步步编程模型。为了充分利用多核 CPU 的性能,Vert.x 中提供了一组 Event Loop 线程。每个 Event Loop 线程都可以处理事件。为了保证线程安全,防止资源争用,Vert.x 保证了某一个 Handler 总是被同一个 Event Loop 线程执行,这样不仅可以保证线程安全,而且还可以在底层对锁进行优化提升性能。所以,只要开发者遵循 Vert.x 的线程模型,开发者就不需要再担心线程安全的问题,这是非常方便的。

Verticle 是 vert.x 的部署单元,网络请求通过 acceptor 线程将请求放到事件队列中,然后 verticle 进行处理。verticle 可以被 deploy 多个实例,每个 verticle 从属于一个 event loop 线程,无论有多少请求处理,都只会被 这个 event loop 线程来处理。Acceptor 线程也会轮询将事件发放给不通的 verticle 实例。

一个应用程序通常是由在同一个 Vert.x 实例中同时运行的许多 Verticle 实例组合而成。不同的 Verticle 实例通过向 Event Bus 上发送消息来相互通信。
总的来说,它以非阻塞 IO 的思想来实现高性能,非阻塞 IO 的实现,基于 Event Loop Vertical 和 Worker Vertical 的分离,在 Vert.x 中,Event Loop 用于接收,并将短业务操作交由其内部的 Vertical 来处理,该模块是非阻塞的,这样可以保证请求的处理效率;阻塞任务通过 Vert.x 的事件机制脱离当前线程,转移到 Worker Vertical 中执行,并将执行结果返回给 Event Loop Vertical。这一过程完成的核心是 Event Bus,Event Bus 中注册了所有的事件,通过事件匹配完成事件转移和结果返回,从而将整个流程衔接起来。相比传统的阻塞模型,相同的资源配置下,它能处理更高的并发。
当前极光实时竞价服务一个实例配置 6c,12G。Verticle 类型使用 standard verticle,服务启动发布 12 个 verticle 实例,内部使用 future 进行异步编程,对于 IO 阻塞操作(如 redis,rpc)采用异步回调的同时设置超时时间。经测试,响应时间在 100ms 内,一个实例最高能处理到 2800 左右 的 qps。
Grpc
特点:
1. Grpc 基于 http2 协议实现,在性能上比 http1 高出许多。
2. 使用 protobuf 进行数据传输,传递的数据量相比 json 要少很多
3. 支持异步请求
4. 允许在客户端取消 rpc 通信或者设置超时时间
5. 支持多语言
调用模型

Grpc 原理
gRPC 的客户端请求消息由 Netty Http2ConnectionHandler 接入,由 gRPC 服务端负责将 PB 消息(或者 JSON)反序列化为 POJO 对象,然后通过服务定义查询到该消息对应的接口实例,发起本地 Java 接口调用,调用完成之后,将响应消息序列化为 PB(或者 JSON),通过 HTTP2 Frame 发送给客户端。
gRPC 线程模型由 Netty 线程 + gRPC 应用线程组成。
Netty I/O 线程负责 gRPC 请求消息的读取、响应消息的发送,HTTP/2 协议消息的编码和解码,NettyServerHandler 的调度。
gRPC service 线程负责将 gRPC 请求消息(PB 码流)反序列化为接口的请求参数对象,将接口响应对象序列化为 PB 码流,gRPC 服务端接口实现类调用。
gRPC 的线程模型遵循 Netty 的线程分工原则,即:协议层消息的接收和编解码由 Netty 的 I/O(NioEventLoop) 线程负责;后续应用层的处理由应用线程负责,防止由于应用处理耗时而阻塞 Netty 的 I/O 线程。
服务治理

缓存设计
1、创意倒排索引
这部分数据主要是指运营人员创建的广告活动、创意。当 ADX 用户流量过来时,如何快速的匹配到对应的广告活动,极光 DSP 采用倒排索引以及二级缓存的设计。
倒排索引即通过用户流量的特征去匹配对应的广告活动 id,如:匹配定向地域在 shenzheng,媒体为 com.a 的广告活动,缓存数据类型采用 Set,如:

二级缓存
二级缓存是指本地应用缓存和 redis 缓存。当运营人员添加广告活动后,同步任务每一分钟会将广告活动从 mysql 按倒排方式写入在 redis 集群,数据类型为 Set。
如:广告活动 1001 配置定向地域在 shenzheng,媒体为 com.a;广告活动 1002 配置定向地域在 guangzhou,媒体为 com.a。则 redis 中存储如下:

当流量过来时,直接从本地应用缓存中获取,若获取不到,先设置空集合,防止高并发导致本地缓存击穿,同时从 redis 中获取并更新本地缓存,缓存有效期设置为 1min。
当然由于我们每一次都先从本地缓存获取,若查询不到会先设置空集合再查询 redis,虽然保证了我们广告检索的高性能,但同时也丢失了部分并发请求匹配成功的机会。我们的考量是在海量的流量下,具有部分相同特征的流量非常多,因而选择了性能,放弃部分由于并发导致第一次匹配失败的机会,后续可能会有一些优化。业务流程图如下:


2、人群包
作为 DSP 来说,人群包的准确性很大程度决定了广告的投放精准度。同时不同的广告主有着不同的定向人群,1 个人群包量级一般在几千万 到几亿,极光 DSP 目前在用人群包有 100 个左右,为了保证 RTB 在人群过滤时的高性能,人群包数据都是预加载到 redis 集群,定时进行更新。
Redis 存储数据类型为 Set,如:

注:key 保存的是 md5 后的设备号再转成 16 进制字节数组。
另由于人群包数量较大,按当前存储方式,极光 DSP 人群包占用 redis 内存约在 500G 左右,我们使用了 100 个实例的 redis 集群(主从)来进行存储,如:

3、人群标签
RTB 中人群标签的作用主要有两点:
1. 提高广告投放的精准度
2. 用于 CTR 预估模型进行出价
人群标签的存储类似人群包也是预加载到 redis 集群,定时进行更新。
由于人群包标签数量也非常巨大,为了减少 redis 集群存储,极光 DSP 采用 hash 数据类型对人群标签进行存储同时标签 value 使用 bitset。如:

使用 hash 数据类型主要基于以下两个方面的考虑:
1. 减少存储 key
Redis 使用全局 hash 表来保存所有的键值对,hash 表每一项都是 dictEntry 结构体,用来指向一个键值对。DictEntry 结构中有三个 8 字节指针,分别指向 key,value,下一个 dictEntry。若减少 key 能降低这部分元数据的存储。


2. Hash 底层数据结构使用压缩列表或者 hash 表,只有当 field 个数大于 hash-max-ziplist-entries 或者单个 value 大于 hash-max-ziplist-value 时,hash 底层数据结构才会使用 hash 表。故只要将 field 个数以及 value 大小控制好,使用压缩列表存储可以减少存储空间。
RTA 策略设计
极光 DSP 中 RTA 是指在广告检索过程中实时询问广告主当前流量是否匹配的功能。但是由于机房位置,广告主接口性能等问题并不是所有 RTA 接口都能做到实时并满足 RTB 的性能要求,故我们采用了实时+离线设计方案(如下图)。即当给广告主配置实时 RTA 标识时,走实时方案,否则走离线方案。

异步方案:RTB 先从 redis 中查询 RTA 缓存结果,若不存在,则根据 RTA 配置策略判断当前用户流量是否匹配,同时将 RTA 询问请求异步发送 kafka,由 RTA 处理模块消费消息询问对应广告主结果,最终缓存到 redis 中,当用户流量第二次过来就能直接从 redis 中拿到对应结果了。

同步方案:RTB 实时请求广告主 RTA 接口询问当前用户流量是否匹配同时设置超时时间为 30ms。若超时,则按 RTA 配置策略来决策当前流量是否匹配。
总结
极光 DSP 每天接收 100 亿级流量而稳定运行,SLA 保持在 99.9%,主要依赖以上高可用的网络架构,高性能技术框架以及高效的缓存、业务设计的支撑。当然我们也还有很多需要改善的地方如:
1. 为进一步提升服务的可用性可部署同城双活;
2. 对 k8s 集群节点按业务模块分类,将各业务模块服务部署到指定分类节点,进一步提升节点 cpu 和 memery 的利用率;
3. 人群包大 redis 存储的优化。
关于极光
极光(Aurora Mobile,纳斯达克股票代码:JG)成立于 2011 年,是中国领先的移动开发者服务提供商,专注于为开发者提供稳定高效的消息推送、一键认证以及流量变现等服务,助力开发者的运营、增长与变现。同时,极光的行业应用已经拓展至行业洞察、金融风控与商业地理服务,助力各行各业优化决策、提升效率。
版权声明: 本文为 InfoQ 作者【极光JIGUANG】的原创文章。
原文链接:【http://xie.infoq.cn/article/ec4a4fa234483c379bd583e51】。文章转载请联系作者。
评论