在程序化音频广告领域,101毫秒意味着零收入。这不仅是性能问题,更是生存问题。
我们运营的音频广告DSP每日处理约3.5亿次请求——每周近20亿次。每个请求必须在100毫秒内完成接收、评估和响应。超时即流标。一旦达到101毫秒,我们的服务形同虚设。
这一切由三人工程师团队完成,月均云成本控制在1万美元以内。该团队全面负责竞价基础设施、应用层、数据库和运维。
实时竞价生态浅析对于非广告技术领域从业者,程序化广告依赖实时竞价(RTB)运行:
- 供应方平台(SSP):如AdsWizz或Triton等平台,聚合数千个应用和发布商的广告库存——包括播客播放器、流媒体应用等音频界面。
- 需求方平台(DSP):如我们所在的系统,代表广告主实时决策是否参与特定广告展示位的竞价。
当听众打开播客应用产生广告位时,应用会向SSP发送请求。SSP随即向多个DSP发出竞价请求。我们的系统接收请求后,评估用户画像和活跃广告活动,计算出价并返回响应。全程必须在100毫秒内完成。
延迟在这里不仅是指标,更是产品核心价值。
认清这一点后,传统Web应用架构显然不再适用。我们无法承担微服务间的网络跳转开销,更无法承受庞大Kubernetes集群带来的云成本。要想存活,必须优先为约束条件进行设计。
这些约束并非理论假设。早期使用云服务商赠金时,我们并未重视效率问题。赠金耗尽后,每个架构决策都开始产生真实成本。这促使我们重新设计兼顾延迟、成本和运维简洁性的系统。
下文介绍的架构方案,使三人团队能规模化运营全球RTB系统——既控制成本又保障系统稳定性。
1. 地理优先过滤:基于网络物理特性的设计我们对接的主要音频SSP(Triton、AdsWizz、Magnite(前身为Rubicon)和MCTV)服务器分布于美国、欧洲和东南亚。而承载印度广告主投放逻辑的应用层部署在印度。
仅区域间网络往返延迟就达240毫秒,尚未计算任何应用逻辑处理时间。这已超出允许延迟预算的两倍以上。
解决方案直接但不容妥协:我们在四个战略级AWS区域部署竞价集群:美国西部(俄勒冈)、欧盟中部(法兰克福)、亚太(新加坡和香港)。
这确保SSP发送竞价请求时,网络传输延迟可忽略不计(通常为个位数毫秒),为实际计算留出充足的100毫秒预算。
2. 单体-微服务混合架构
我们最终采用介于传统单体与微服务之间的架构。不再将服务分散部署,而是在单台虚拟机上共置紧密耦合的小型服务集群,通过全局负载均衡器横向扩展这些VM。
每个VM作为独立节点,内部构成微型分布式系统:
- Nginx:单一入口容器,承担内部流量调度职责。
- 竞价服务:3-5个Node.js竞价服务副本在同一机器运行,充分压榨CPU资源。
- Redis:与代码同机部署的本地Redis实例。
这种设计天然实现数据本地性。竞价服务无需跨网络获取数据,直接通过本地地址读取。同时提升资源密度——采用大型VM(如c6i.xlarge)并在多个工作进程间共享内存数据集,获得更优的内存-CPU比例,在最大化吞吐量的同时减少资源浪费。
3. 快速中止逻辑设计要实现100毫秒响应,仅靠代码优化远远不够。系统必须快速判断何时终止处理。
大部分竞价请求最终不会中标。若让这些请求触及状态存储、数据库或写入路径,将快速耗尽延迟预算。我们将竞价服务构建为按"失败概率"排序的决策树,优先执行成本最低的检查项:
检查流程大致如下:
- 地理检查:大量竞价在此环节失败,简单比较后立即返回结果。
- 操作系统与设备检查:通过地理检查后验证平台与设备限制。
- 媒体渠道/应用定向:校验应用是否匹配允许的广告库存。
- 状态化逻辑:仅当请求通过以上过滤后,才触达用户层级数据、预算控制等需要协调的重型路径。
通过快速中止机制,我们能在微秒级拒绝90%的无效流量,将CPU资源留给真正可能转化的竞价请求。
4. 数据架构:Redis存储规则,Aerospike存储画像根据访问模式和数据"陈旧度"成本,我们将数据架构分为两个层级:
规则数据:本地Redis
每个节点在本地Redis存储"热"元数据(活跃广告活动与预算上限)。定时任务采用拉取模式,每60秒从中心API获取最新数据。
在此规模下,60秒的数据延迟存在超支风险:若多个节点基于相同陈旧预算数据同时操作,可能超出客户分配的预算额度。
我们通过制动机制应对:将集群规模作为预算控制参数。随着活跃竞价节点数量增加,每个节点动态缩减可使用的预算份额。即使节点使用陈旧数据,其本地限制已根据当前集群规模进行节制。支出会平滑减缓而非突然飙升,系统在下次刷新周期中自动收敛。
用户画像数据:Aerospike(SSD优势)
用户层级数据(画像查询、频次控制、重定向)需要亚毫秒级访问海量可变数据集。传统关系型数据库无法在此成本下承担该负载。
我们选用Aerospike作为该层解决方案,因其专为SSD主存储优化,而非将完整工作集存放于内存。这在保持硬件成本远低于纯内存方案(如ElastiCache)的同时,提供可预测的延迟表现。
为控制该层成本,我们严格执行数据清理策略:
- 10天有效期规则:仅保留最近10天活跃用户数据。
- 千万量级封顶:主动修剪数据集,仅保留最活跃的数百万用户数据。
用户停止互动后,其数据立即被清除。通过严格执行这一策略,我们确保了存储成本的稳定,同时竞价系统专注于高价值用户。
5. 架构级异步:解耦业务负担初始版本尝试代码层面的异步:立即发送竞价响应后,让Node.js函数在后台继续执行日志写入MongoDB的任务。
陷阱所在:流量存在波峰波谷。高峰期即使竞价响应已发送,Node.js容器仍保持"悬浮"状态——维持连接并消耗CPU周期完成数据库写入。这导致自动扩展器启动更多VM处理写入积压,最终使数据库因连接数过载而崩溃。
解决方案:从"异步代码"转向架构级异步。引入Kafka作为竞价服务与写入路径间的缓冲层。现在竞价服务将消息推送至Kafka后立即释放资源,写入操作由独立消费者集群以可控节奏处理。
6. 性能基准测试:用数据代替猜测在此规模下,盲目选择实例类型成本极高。我们通过实际测试替代理论推测:
我们提出假设:竞价服务是受计算限制还是内存限制?每个节点运行多少工作进程最合理?实例规格如何影响延迟?通过测试不同AWS实例系列与规格,调整单节点运行的竞价服务数量,验证这些假设。
对数据层采用相同方法,测试不同机型在真实负载下对MongoDB的性能表现,优先考虑稳定延迟而非峰值吞吐量。
所有测试均通过模拟生产环境QPS的负载测试进行验证,测量响应时间和长尾延迟。部分假设成立,更多假设被推翻。
结论很简单:在100毫秒预算下,实测数据永远优于理论推测。
7. 云成本管控:如何将账单控制在1万美元初创公司往往不是死于流量激增,而是死于云赠金耗尽。对我们而言,成本控制不是事后考虑,而是系统设计的重要组成部分。
部分节省来自商务谈判:通过经销商结算获得统一量贩折扣。针对带宽成本,我们承诺月度约1PB的数据传输量,使得CDN价格较按需费率降低60%以上。在当前流量规模下,这一承诺显著影响成本结构。
我们还进行职责分离:无状态竞价服务器使用深度折扣的竞价实例,而有状态数据库采用稳定的长期实例。
在此规模下,每日存储数亿条记录如同定时炸弹。详细竞价日志仅保留数天而非数月。需长期保存的数据提前聚合(按小时而非按事件),转移至更经济、易查询的存储层。合规或历史审计所需数据则推入冷存储,成本比标准层级降低80-90%。
但最大的节省并非来自折扣,而是利用率提升。
多数系统为"安全"起见将CPU利用率维持在20-30%。我们的系统无需如此。由于架构具备容错能力且设计为优雅降载,我们将CPU利用率提升至80%。不为空闲周期付费——每个核心都在参与竞价。
将效率视为首要约束条件,这种思维方式确保即使流量增长,成本也能保持稳定。
8. 三人运维哲学:"学费"理念三人团队运维全球级系统的前提是让系统而非人力承担主要负荷。我们将运维设计为消除关键路径中的人工干预,尽可能避免单点故障。同时打破职能壁垒——每位工程师都能驾驭全技术栈——确保运维知识不会成为单点故障。
基础设施默认可丢弃。若节点异常,相较于原地排查,直接替换实例的恢复速度更快,且每个实例都源自已知的清洁状态。
可观测性至关重要,但仅靠仪表板无法帮助小团队。我们依赖可操作警报(通过Datadog),仅在需要人工介入时触发。区域集群减速或消费者滞后时会收到告警,其余流程均实现自动化。
但自动化并非万能。我们仍会不时支付"学费"。
早期某次事件中,负责刷新预算状态的背景任务静默失败。竞价服务持续基于陈旧数据运营,对本应结束的广告活动继续出价——典型的"僵尸竞价"场景。我们向客户退款后,通过Datadog监控的心跳检查机制加固系统。现在预算 freshness 成为显式指标,静默本身就会触发警报。若竞价服务未收到有效更新,会自动失败关闭。
小团队犯错不可避免,但重复犯错可以避免。
总结思考该系统的每个决策单独看并不新奇。其精妙之处在于这些决策产生的复合效应。
网络物理特性设定边界。本地化与快速中止逻辑守护100毫秒通路。架构级异步消解流量峰值。数据测量取代直觉判断。成本纪律与高利用率保障经济性。运维设计确保人类仅在系统无法自主恢复时介入。
这不仅是关于广告技术或特定技术栈的故事,更是提醒我们:在规模面前,约束不是障碍而是设计输入。当把延迟、成本和人力视为首要需求时,产生的架构往往会更简洁而非更复杂。
我们的目标不是构建令人惊叹的系统,而是构建能够存活下去的系统。
随时随地看视频