
我们做了几年的互联网风控系统踩过的坑比写过的代码还多。今天把整个架构从数据采集到实时决策这条链路掰开揉碎讲一遍重点讲清楚每一层为什么这么设计而不是只贴一张架构图就完事。之前公司内部好几次复盘发现大部分线上故障都不是模型不行而是基建和架构细节出了问题所以这篇会更侧重那些常规文档里不会明确写的取舍和坑。这套架构解决的核心问题是如何在几十毫秒内对线上请求完成一次风险判定判定依赖的数据可能横跨埋点日志、业务库、第三方数据源还要让模型决策和规则决策在同一个引擎里协同工作。系统适合的读者是准备自建风控平台的中大型团队、正在做风控系统重构的架构师或者想了解风控内幕的后端开发。1. 整体架构设计先搞清楚边界再动手写代码1.1 风控的核心矛盾带宽、延迟与准确率的三方博弈风控系统跟普通业务系统最大的区别在于它永远在跟对抗方赛跑。业务系统追求的是功能完备风控系统追求的是在有限预算内做最优决策。这里说的预算不是钱是时间预算和计算预算。每次请求进入风控引擎你只有几十毫秒去决定放行、拦截还是人工审核这个时间窗口内要完成数据拉取、特征计算、规则匹配、模型打分、策略命中判定等一系列动作。很多人刚做风控时会犯一个认知错误总觉得准确率越高越好。实际上一味追求准确率会把系统拖垮。比如你为了算一个更精确的设备指纹特征引入了一个需要300毫秒才能返回的SDK采集结果那整个链路就被这一个特征锁死了。我见过最夸张的一个案例是某团队为了在风控里用上实时多头借贷查询把一个接口的RT拖到了800毫秒结果业务方直接说宁可不做风控也不想损失转化率最后只能把这个特征改成异步补齐、下一轮请求再生效。所以在设计架构的第一天就要把全量实时和部分实时部分异步的边界画清楚。风险决策链路上的数据只有那些对本次判定有决定性影响的才值得同步等待其余的全部走异步或离线路径。1.2 分层设计采集、计算、决策、存储各司其职我们最终落地的架构分为四层数据接入层、特征计算层、决策引擎层和存储与回流层。每一层的职责边界非常明确不允许跨层调用。数据接入层只做一件事把各种各样的原始数据变成统一格式的标准化事件。不管进来的是Nginx访问日志、App埋点、业务库的binlog、还是第三方黑名单数据进到这一层之后全部转成统一的Event结构。特征计算层负责把标准化事件加工成可用的特征。这一层内部又拆成实时计算子层和离线计算子层。实时计算子层主要处理滑动窗口、计数器、频率类特征离线子层跑的是复杂的聚合特征和模型样本。决策引擎层是整个系统的心脏。规则引擎负责跑硬规则模型服务负责跑软性评分编排器负责把规则和模型的输出按策略组合起来输出最终决策。存储与回流层管所有数据落地。热数据放Redis或内存里支撑高并发查询温数据放ClickHouse用于分析离线样本落到Hive供模型训练。为什么必须分层因为风控系统迭代极其频繁。业务团队每周都可能提新的策略需求如果所有逻辑都揉在一个大泥球里改一个策略就要全链路回归测试谁也扛不住。分层之后规则引擎可以独立热更新模型服务可以独立灰度数据层可以独立扩容互不干扰。注意分层不是目的隔离变化才是。你只需要让每一层的变更尽可能不波及相邻层。再强调一个容易被忽略的点每一层之间必须定义清晰的数据契约。我们团队是直接用Protobuf定义全套接口协议所有层之间的数据传输都必须经过协议校验非法字段直接丢弃并告警。这一步在早期看起来是浪费时间到了后期有几十个下游方依赖我们数据时你就会庆幸当初做了这层约束。2. 数据采集层数据进不来后面全是无米之炊2.1 三类核心数据源同步接口、异步消息、离线批处理风控需要的数据按实时性要求可以分成三类。第一类是同步接口数据要求毫秒级返回比如设备指纹、注册信息、当前请求的IP和User-Agent。这类数据直接在风控请求的主链路上通过RPC调用获取。第二类是异步消息数据允许秒级延迟比如用户最近5分钟的点击行为、下单记录、支付回调这些通过消息队列以异步方式进入系统经过流式计算后更新特征。第三类是离线批处理数据小时级或天级更新比如历史订单的聚合统计、社交关系图谱、逾期记录等这些通过定时的ETL任务灌入特征库。那这三类数据在架构上是完全分开的吗其实不是。我们一开始也试图三条链路各搞一套代码后来发现维护成本太高了。最终统一成一个数据接入平台只是把不同的实时性要求抽象成不同的接入通道。同步接口走HTTP/HSF直连异步数据走Kafka离线任务走调度平台三个通道进入平台之后统一做解析、清洗、标准化。这里有个很关键的实践细节数据源接入的时候一定要保留原始快照不要只留清洗后的结果。因为风控策略经常需要回溯当时那条数据到底长什么样这个问题在投诉处理、策略复盘时频繁出现。我们当时的做法是将原始报文完整存储在对象存储里保留30天索引只存数据字典和关键字段这样既不影响查询性能也不至于把存储成本顶爆。2.2 数据标准化从原始报文到统一Event的九九八十一难数据标准化比你想象中麻烦得多。你可能觉得不就是字段映射嘛但真实场景里你会遇到同一个字段在这个接口里叫userId在那个接口里叫user_id在另一个接口里还可能是uid时间字段有的传Unix时间戳有的传ISO8601字符串还有的传的是带时区的字符串一转换就出八小时偏差。我们的做法是建立了一个字段映射中心用配置化的方式管理所有源字段到标准字段的映射关系。每个接入方到平台注册数据源的时候需要填写一张映射表平台自动生成解析代码。对于时间字段统一用标准的毫秒级Unix时间戳存储转换逻辑全部收敛到一个公共组件里不允许业务团队自己写时间转换。数据质量问题一定不能只在入口处解决。我们对每条进入系统的Event都计算一个数据质量分数包括字段完整性、取值合法性、时间戳新鲜度等维度。这个分数会作为特征的一部分传给下游。比如一个设备指纹事件如果它的质量分很低说明这个指纹可能是被篡改或者采集不全的那它对应的置信度就要打折扣。这个做法帮我们解决了很多仿冒设备绕过的案例。2.3 链路压力与削峰填谷采集层的流量治理采集层还有一个很容易被忽略的任务流量治理。对C端产品来说流量是有波动的大促期间峰值可能是日常流量的十倍。如果采集层不做削峰填谷下游计算层就会被冲垮。我们用的是双缓冲加流量控制。所有同步接口进来的请求先进入一个内存队列由分发线程按下游节点的处理能力控制速率超出的部分直接丢弃并返回降级响应。降级不是拒绝而是返回数据未采集完整的标记让决策引擎知道本次决策可能缺少某些特征从而自动调整策略阈值。异步消息通道则利用Kafka的分区机制做负载均衡每个业务类型单独一个Topic消费端按分区消费任何一个消费者出问题最多影响一个分区的数据。这里特别提醒一下绝对不要让多个业务共用同一个Kafka Topic。我们早期为了省事把注册、登录、交易三类事件全扔进一个Topic结果某次交易类事件流量暴涨把注册和登录的事件全挤压了引起了一轮大规模误判。3. 特征计算层实时与离线的天壤之别3.1 特征分层按照实时性和重要性给特征分等级特征计算是整个风控系统的技术含量所在。特征可以简单粗暴地分成实时特征、近线特征和离线特征三类不能一股脑全做实时也不能把离线当实时用。实时特征要求在微秒到毫秒级别完成计算常见的有频率类特征比如一个IP在5分钟内请求了多少次、序列类特征用户最近10笔交易金额的序列、状态类特征用户当前是否在处罚名单里。这类特征的共同点是依赖的数据量小、状态有限、计算逻辑直接。近线特征允许秒到分钟级延迟常见的是各种滑动窗口聚合比如最近一小时内设备关联了多少个账号这类特征需要的时间跨度大实时算的话内存开销不可接受。我们通常用Flink的窗口计算来做结果写入KV存储决策时直接读取。离线特征则处理天级的全局聚合比如用户的30天活跃天数、平均客单价、地理位置的常用区域等。这些特征用离线ETL每天凌晨算一次结果推送到在线特征库供全天使用。3.2 Flink实时计算与状态管理窗口是个技术活实时计算这一层我们选了Flink作为主力引擎选型理由不复杂状态管理、窗口机制、精确一次语义这三点Flink做的是最成熟的。做实时风控最烦的事情是机器宕机导致状态丢失Flink的Checkpoint机制能自动恢复状态这是省心活命的关键。窗口计算里最常用的滑动窗口比如统计过去10分钟内登录失败的次数。这里有个非常容易出bug的地方事件时间跟处理时间不一致。客户端上报的事件本身可能延迟到达如果你按处理时间也就是消息到达Flink的时间做窗口统计就会出现明明是同一时刻的事件被划进不同窗口的偏差。解决方法是做水位线Watermark管理让窗口按照事件时间计算允许一定程度的延迟数据。但这个延迟容忍度不能设得太大否则窗口关闭太晚特征迟迟算不出来同样会影响实时性。我们当时的经验是延迟容忍度设为窗口长度的一半比较合理对10分钟的窗口允许最多5分钟的数据延迟到达超过的直接丢弃。状态存储上Flink的RocksDB状态后端是生产环境的标准选择。刚开始我们用内存状态后端感觉很快但在状态量超过几个GB之后GC问题会拖垮整个任务。换成RocksDB后状态存储和计算分离了虽然单次状态访问多了序列化开销但整体的稳定性和扩容能力大幅提升。3.3 特征存储选型在线特征和离线特征别放一个库特征计算完要存到哪供决策引擎使用这是另一个影响全局性能的点。常见的做法是实时特征和近线特征存Redis或Tair这类KV数据库以user_id或device_id为key一次查询能批量取出几百个特征。我们用的是Tair主要是考虑到它跟Redis协议兼容但支持持久化和多级存储比裸Redis在容灾上更有保障。这里要给个暴论所有特征能不能在几十毫秒内取出来基本决定了决策引擎的RT。特征数量一大网络IO就成了瓶颈。我们做过压测500个特征的单次批量读取在纯Redis模式下大概需要5-8毫秒在Tair持久化模式下要12-15毫秒。这看起来是小事但乘上每秒几万的调用量就会变成巨大的资源消耗。解决思路是把特征做本地缓存。决策引擎每台机器上部署了一份热特征缓存只缓存最近30分钟内有访问过的特征命中率能做到90%以上。请求进来时先查本地缓存没命中再查远程KV。这个优化把特征读取的RT从平均10毫秒降到了接近1毫秒整个决策链路RT预算一下子宽裕了很多。3.4 特征管理与血缘没有元数据的特征是灾难最后还要聊一下特征管理。几百上千个特征分布在不同的计算链路里如果没有人记录每个特征的含义、来源、口径、负责人用不了多久就没人知道某个特征到底代表什么了。我们自建了一个特征元数据中心每个特征在上面注册时必须填写特征名、所属业务域、来源数据、计算逻辑、负责人、使用方、灰度状态。特征有变更时必须走评审流程使用方要收到变更通知。这一套管下来最大的作用不是监控而是避免了大量特征口径改了但下游没人知道的事故。我印象很深的一次线上问题运营同事调整了一个活动规则导致用户领取优惠券次数这个特征的计算口径变了但负责策略的同事不知道还在按照旧口径配了一个阈值结果大量正常用户被拦截造成了严重的客诉。4. 实时决策引擎把规则、模型和编排揉在一起4.1 规则引擎选型自研还是开源这是个好问题决策引擎是风控架构的心脏。说到规则引擎估计很多人第一反应是用Drools这类开源产品。我们最开始也是用的Drools用着用着发现越来越别扭——风控规则的形态和业务规则差异很大风控规则需要精确到千分位的阈值控制、需要支持规则之间复杂的优先级和组合关系、需要支持规则的灰度发布和AB测试Drools在这几方面都比较吃力。后来我们选择了自研规则引擎核心是一个基于Groovy脚本的规则解析器配上可视化的规则配置界面。自研的理由倒不是要炫技而是风控规则的本质是对特征做条件判断后给动作逻辑本身不复杂复杂的是规则的组织编排和运营效率。自研之后规则可以做到毫秒级热更新运营同学在界面上改一个阈值10秒后全量生效不用再发版重启了。4.2 规则组织与命中判定应然与实然之间还有优先级规则引擎除了能跑规则怎么组织规则也有讲究。我们把规则分为三个层级全局规则、场景规则、用户级别规则。全局规则适用所有请求比如黑名单命中直接拦截场景规则区分注册、登录、下单、支付等不同业务场景各自有独立的规则集用户级别规则是针对高风险用户的个性化策略通常是名单形式的。一个请求进来引擎先取全局规则全部跑一遍如果有任何一条命中且动作是拦截就直接返回不再往下走。全局规则过了再取场景规则场景规则按优先级排序同一个命中结果里优先级高的规则说了算。最后再查用户级别规则一般是查名单命中黑名单就升级策略。优先级设计看着简单真做起来全是坑。曾经有个场景规则A说转账金额大于1000需要人工审核规则B说该用户是白名单用户直接放行如果优先级设置反了白名单用户就被规则A拦截了。为了保证这类需求不被搞错我们要求每个规则在配置时必须声明优先级和冲突策略冲突策略包括高优先级覆盖和并存且取严并且引擎在启动时必须做一次规则冲突检测把明显矛盾的配置直接拦下。4.3 模型服务接入从离线训练到在线打分风控引擎光有硬规则是不够的。硬规则是开或关的二元决策模型却能输出一个0到1的连续风险分对应可能风险很可能风险高度风险。我们在决策链路里同时挂了多个模型反欺诈模型、逾期风险模型、设备异常模型等每个模型输出一个分作为规则里的一个普通特征参与后续判断。模型服务的接入方式我们踩过几次坑。最开始是让规则引擎直接HTTP调用模型服务单次调用要15毫秒几个模型串下来就出去了50毫秒决策链路RT直接超标。后来改成并行调用用一个CompletableFuture把所有模型请求同时发出去等待最慢的那个返回整体RT反而变成20毫秒左右因为模型服务本身只要5-10毫秒并行之后基本就是最慢模型的时间。模型服务内部也做了升级放弃了裸Python服务改成基于Java的ONNX Runtime来加载模型。模型训练仍然用Python训练完成导出ONNX格式Java服务直接加载执行。这样服务化能力大幅提升QPS上去了GC问题也基本没有了。如果一个模型跑着跑着效果变差了还能在模型管理平台上快速回滚到上一个版本整个发布过程不需要重启服务。4.4 决策编排器把规则和模型的输出组装成最终结论规则引擎和模型服务都完成之后还需要一个环节把这些输出组装起来这就是决策编排器。它读取规则引擎的命中列表和模型服务的评分结果按照策略树的逻辑组合出最终决策通过、拒绝、人工审核、增加验证码、提升OTP等级。编排器内部维护着一棵策略树每个叶子节点是一个决策动作每个中间节点是一个判断条件判断条件可以引用规则的命中结果、模型的分值区间、特征的取值组合。这棵树是可视化配置的运营同学可以自己调整树上一个分支的判断逻辑不用写代码。这里想特别提醒一下决策编排器一定要设计成无状态服务。无状态意味着任意一台机器都可以处理任何请求扩容缩容都很方便某台机器宕机也不会影响整体决策。我们所有决策结果都会写一份审计日志记录这次决策用了哪些特征、哪些规则、模型打了多少分、为什么输出这个结论方便后续客诉处理和责任追溯。5. 存储与数据闭环短期决策与长期演进的根基5.1 冷热分离存储与决策审计日志的检索随着业务量增长你很快就会遇到存储和查询的瓶颈。风控系统对存储的需求是明显分层的热数据要支持毫秒级查询温数据要支持秒级检索冷数据只需要能低成本保存。决策审计日志是最典型的例子。我们每天产生数十亿条决策日志每条一KB出头一个月就是几TB。如果全放ClickHouse里查询虽然快但存储成本扛不住。所以我们做了冷热分离当天的日志放ClickHouse的Hot节点7天前的日志自动搬迁到冷节点90天前的直接归档到对象存储只保留缩小版的摘要数据在ClickHouse里。查询时如果业务要查3个月前的具体决策详情先从摘要数据查到对象存储的路径再从对象存储拉取明细。因为对象存储带宽有限我们限定这类查询只能走异步任务不能同步返回。实际操作里大多数需要回溯的都是最近几天的数据很少真的要去翻90天前的明细这个取舍很划算。5.2 样本回流与模型迭代让系统越跑越聪明风控系统不能只做一个静态的判官它得从每一次决策中学习。决策引擎每做一次判定系统都要记录下这次判定的特征向量、规则命中、模型输出以及最终结果。一段时间之后我们用这些数据组装成有监督训练的样本集。样本里需要标注正样本和负样本但风控的标注跟一般机器学习不一样没有真实风险发生的决策样本你不能全标成正样本因为有可能是误判放行。我们的标注策略是多信号交叉比如一笔支付订单如果最后发生了拒付或退款那决策时点对应的样本就要标为负样本如果用户后续主动联系客服说是本人操作那标记就要纠正为正样本。这个标注的质量直接影响模型迭代的效果比模型调参重要得多。模型更新频率上我们走的是双周迭代节奏。每两周跑一次离线训练效果评估通过之后进入影子模式跑一天影子模式下模型只打分不影响决策把打分结果跟线上模型对比确认没有明显的分布偏移之后正式灰度。这个流程能保证模型一直在适应最新的风险变化同时不会因为一次训练异常把线上模型搞坏。6. 常见问题与排查技巧实录6.1 数据延迟导致误判时间对齐才是最大隐形杀手线上出过最诡异的一个问题同一批用户白天风控判定正常到了凌晨突然大量被拦截第二天白天又自动恢复。排查了很久最后发现是离线特征更新链路出了问题。我们每日的离线特征计算任务在凌晨两点开始跑正常情况下应该在三小时内完成但那天因为上游Hive任务延迟特征更新被推迟到了六点才完成。这导致凌晨两点到六点之间决策引擎读取到的最近30天活跃天数还是前一天的值部分用户的活跃特征偏低模型打出的风险分偏高直接触发了拦截阈值。这个案例的教训是要对所有特征加上时间戳标记并且决策引擎在读取时要校验数据新鲜度发现用的是过期特征时要么跳过该特征要么自动调整策略阈值。后来我们把所有特征读取统一加了一层时效包装器任何特征查询都带上数据生成时间这个字段特征使用方必须声明自己接收的时效范围。自那以后这类问题基本绝迹。6.2 规则膨胀导致性能劣化从700条规则说起规则数量会随着业务需求不断膨胀。我们最夸张的时期线上同时跑着700多条规则每次决策把所有规则都跑一遍单次决策RT飙升到了80毫秒。排查下来真正的问题不在于规则本身跑得慢而在于很多规则访问了同一个特征KV重复查询导致IO放大。解决方案是做一个特征预取优化。引擎在决策前先扫描所有可能命中的规则涉及的变量自动拉取所有所需特征到本地再进入规则计算阶段。这样网络IO从每条规则独立查一次变成了整个决策只查一次。配合特征本地缓存700条规则的场景下RT从80毫秒降到了15毫秒左右。如果你的规则也越来越多看看是不是存在同样的重复查询问题。6.3 模型灰度上线时AB实验的对照组设计模型灰度上线时需要做AB实验实验的对照组是旧模型实验组是新模型。但风控模型跟推荐模型不一样风控模型直接影响用户体验挂了新模型之后用户被拦截了他根本不会给你第二次机会。所以风控模型灰度期不能只比对风险分还要同时对比误杀率——即在最终没有被确认为风险的真实用户中模型给过高风险分的比例是多少。我们的做法是让新模型先进入影子模式运行至少48小时影子模式下新模型打分完全不影响决策只默默记录打分结果。影子期结束后把影子打分和线上实际决策结果做一个离线回放算出新旧模型在误杀率和召回率上的差异。只有当新模型的误杀率不高于旧模型5%的情况下才允许开始5%的流量灰度。这个流程虽然保守但确实帮我们躲过了好几次潜在的灾难性灰度事故。6.4 一个特别容易被忽视的点时钟偏差最后说一个特别基础但容易出事的问题时钟偏差。分布式环境下不同机器的时钟可能存在几十毫秒甚至几秒的偏差。对一般业务来说这不算事但对风控系统来说严重的时钟偏差会让事件时间排序出错——明明是用户先登录后下单由于日志所在机器的时钟慢了系统会以为是先下单后登录评判特征全乱套。我们在所有机器上强制部署了NTP同步并且采集层给每个事件都打了双时间戳机器接收时间戳和业务事件时间戳。判断依赖逻辑时只用业务事件时间戳不信任机器接收时间戳。后来有一次跟一个外部数据供应商对接发现他们的数据时间戳是另一个时区的经过转换后差了整整13个小时当时就是靠这套双时间戳机制排查出来的。我这几年的切身体会是风控系统表面拼的是模型和规则实际拼的是工程细节。数据延迟、时钟偏差、规则膨胀、模型误杀任何一个环节出了纰漏都会在线上以客诉和资损的形式加倍朝你讨回来。架构上不需要追求花哨的组件把数据从采集到决策的每一跳都做扎实把时间对齐、特征管理和灰度机制这些基本功练好系统的稳定性自然就起来了。