
简介《OCS计费原理与实现排版后》是一份面向电信运营商计费领域的技术文档适合业务需求分析、系统架构设计、开发与运维人员阅读。内容从离线计费演进到实时在线计费的背景讲起先交代系统定位与导读再进入扫盲篇详细解释TAG、业务用量、费用项、产品、订购、入帐关系、模板与策略等核心术语随后围绕事件驱动的计费机制梳理事件捕获、事件处理、账单生成与结算的完整流程并细化到预处理、批价等关键环节。文档还介绍了计费引擎、数据库、消息中间件等实现组件覆盖额度控制与安全性设计帮助读者理解OCS从业务规则到系统落地的整体架构。资源为单个doc文档体积约1.87MB内容排版清晰、目录完整既可作新人入门材料也可作方案设计与技术评审的参考资料。目前已有297人浏览学习。1. OCS计费到底在解决什么问题先看清它和离线计费的本质差别现实中的计费系统不是月底算账就完事的。用户在套餐额度里打电话、刷视频时网络侧必须实时做出决定这个请求放不放行放行后立刻扣多少这就是在线计费系统OCSOnline Charging System要干的事。OCS计费与实现的核心是解决三件事给网元批多少配额、从账户扣多少余额、以及两边最终怎么对得上账。离线计费可以容忍延迟几小时出账OCS却必须在几百毫秒内响应否则语音接不通、数据包被丢弃业务直接翻车。这套逻辑在电信语音、移动数据、物联网流量包乃至云上API计量计费里都通用做业务支撑系统或网元控制面的人基本绕不开它。2. OCS的架构骨架从配额鉴权到扣费的消息闭环2.1 核心组件拆解CTF、OCF与ABMF的职责边界一个能落地的OCS实现大体沿用3GPP定义的那套分工网络侧有CTF计费触发功能计费中心里有OCF在线计费功能和ABMF账户余额管理功能。我先讲清三者边界因为在自研项目里边界划不清楚后面改造成本非常高。CTF一般部署在网元附近负责监控业务事件。比如语音软交换里CTF监控呼叫开始、呼叫结束数据网关里它监控每个会话的上行下行流量。CTF不直接管钱它只做两件事向OCF发起信用控制请求然后按OCF下发的授权结果执行放行或阻断。OCF是决策面。它接收CTF的信用控制请求消息调用费率引擎和余额查询决定本次授予多少配额、以什么费率计费然后通过应答消息把决定返回。OCF还会维护会话状态——哪些用户正在使用服务、已经用了多少、配额有效期到什么时候。多数自研项目里OCF是一个无状态服务状态放Redis方便水平扩展。ABMF则是账户和余额的数据面保存账户ID、余额、冻结金额、计费会话关联信息。行业里常见做法是ABMF不直接暴露给网元只对OCF提供余额扣减、冻结、解冻和冲正接口。ABMF不复杂但它每个操作都是热点并发控制要点全在这一层。组件位置职责典型存储CTF网络侧触发计费事件、执行策略无状态跟随网元OCF计费中心信用控制决策、会话管理Redis缓存ABMF数据面余额扣减、冻结、冲正数据库集群从协议上看CTF与OCF之间用的是Diameter协议族的Ro接口信令面上就是CCR/CCA消息对如果还需要离线话单就用Rf接口把CDR交给后处理系统。很多团队第一次上手时以为要自己写一套协议其实不用直接复用成熟的开源Diameter协议栈做编解码即可。真正的业务复杂度不在协议栈而在状态管理和余额事务。2.2 计费会话生命周期CCR-I/CCR-U/CCR-T怎么流转一次语音通话或一条数据连接在OCS里对应一个“计费会话”。会话开始、中间、结束这三个阶段分别对应三对消息CCR-I初始请求CTF发现新业务事件请求配额。OCF验余额、算费率、下发配额。CCR-U更新请求CTF的配额快用完上报已用量并请求新的配额。CCR-U是会话心跳机制的关键稳定程度直接决定计费正确性。CCR-T终止请求业务结束CTF上报最终用量OCF关闭会话并做最终结算。需要熟悉两个核心AVPRequested-Service-Unit和Granted-Service-Unit分别表示CTF要多少和OCF批多少。里面可再分CC-Time、CC-Total-Octets、CC-Input-Octets等子单元分别对应按时长、按总流量、按上下行流量计费的场景。字段含义典型值CC-Request-Type初始化/更新/终止1/2/3Requested-Service-Unit本次请求的配额“CC-Time: 60”Granted-Service-Unit本次授予的配额“CC-Time: 120”Validity-Time配额有效期30~300秒Result-Code成功2001余额不足40122xxx/3xxxFinal-Unit-Indication最后一笔配额标记true/false时序可以这样理解语音呼叫开始时CTF发送CCR-IOCF查余额后授予120秒。到100秒时CTF觉得配额快用完发送CCR-U上报已用100秒此时OCF按新余额再授予。如果用户挂断CTF发送CCR-T上报最终用量OCF终止会话。配额没有用完的部分按预扣模型做返还过期配额则在Validity-Time到期后由网元主动终止或申请续期。这里容易误以为CCR-U只能等到配额用完才触发实际不是。OCF通过Validity-Time限定了配额的“保质期”强制CTF必须定期续发。也就是说即使配额没用完只要有效期到了CTF也得发起CCR-U。这其实就是一个标准的“心跳机制实现”会话保活和配额续期是同一件事消息节奏由OCF控制。把这个看透后再处理超时、断链、重试就都有抓手了。3. 配额管理是计费的心脏授信、余额与单元换算3.1 配额计算与余额扣减在OCS实现中配额不是一个拍脑袋的数字它是余额、费率和系统策略共同作用的结果。常见做法是先定义计费单元时间计费按秒记CC-Time流量计费按字节记CC-Total-Octets事件计费按条记。然后再把余额按单价换算成可授权的配额。授信的计算式可以写成授权配额 min余额除以单价单次授权上限CTF请求量。这个公式有三个要素缺一个都会出问题不除以单价用户可能透支几千元不设单次上限CTF请求1GB就把这个月全部授出不尊重CTF请求量可能给一个只需要10秒的连接授了120秒后面还要做退款。实际项目里语音场景常见的首次授权是60到120秒续期授权是30到60秒数据流量场景首次授权是1到5MB续期在1MB左右。这些参数不是拍出来的要看网元的CCR-U触发频率。我们曾经给某数据网关配了10MB的首次配额用户短视频刚开始就连续触发续期反而把CCR-U频率带高了。后面我一般会把首次授权压到用户无感的最低阈值既不让消息风暴也不透支余额。配额扣减的模式业内常见有两种模式授权时结束时风险适用场景预扣/冻结模式从余额扣授权全额按用量退还会话失控导致冻结资金挂账需冲正语音、实时互动后扣/账单模式不扣只检查余额按用量一次性扣会话期间可能超额并发读改写冲突低值短会话、事件计费使用预扣模式时ABMF需要支持“冻结”“扣减”“解冻”“冲正”四种操作。这是计费和普通订单系统最不同的地方订单扣款只有一次计费却有“预扣后再返还”的状态变换只要某个环节忘记返还账户里就会积压一笔永远用不上的“空气钱”。3.2 时延、并发与一致性OCS计费延迟直接决定用户体验。CCR发出后如果200毫秒才收到应答用户没有感知但网元侧的等待资源、内存、超时定时器全被占住端到端超过500毫秒的极端情况会直接丢会话。业界一般把OCF与ABMF之间的交互控制在30到80毫秒整个信用控制往返控制在200到300毫秒。要做到这一点ABMF不能每笔扣款都开一次数据库事务。我见过的做法里最稳的是把余额的检查、扣减放到一条Redis的Lua脚本里脚本内同时完成“扣减”和“返还”的原子操作数据库只做异步落账和对账。热点账户是这类方案路上的核心坎一个热门账户在同一瞬间可能被几百个会话同时扣减如果单线程Lua脚本批量执行延迟会被拉高。后面我在第5章会专门讲这个坑。一致性的第一个问题是消息重复。Diameter协议里CCR带了CC-Request-Number同一个会话的每一条消息序号都会递增。OCF必须把“会话编号请求序号请求摘要”缓存起来发现重发时直接返回之前的应答否则用户会被扣两次钱。一致性的第二个问题是计费会话状态的持久化。OCF进程如果崩了内存里的会话状态全部丢失用户正在进行的语音会话会因为没有配额续期而被掐断。常见做法是把会话状态写入Redis并带过期时间同时把“授权记录”打在数据库落账日志里。OCF重启后从Redis恢复活跃会话对无法恢复的会话做强制终止并把此刻还没扣的授权回补。另一个很多人忽略的一致性问题是CTF上报的用量和OCF自己记录的耗时或流量可能不一致。CTF可能因为网元重启丢失部分用量所以OCS一般会“以己方计费周期为准”CTF上报值只作为明细留档不做最终结算的唯一依据。听上去像“信你一半”但这恰恰是长期打磨出来的血泪经验如果完全信任网元上报的Usage出账对不上时你能查的依据就少了一半。4. 用代码跑通一个最小OCS流程从CCR构建到CCA解析4.1 一个最小可运行的OCS原型消息构建与授信我不会一上来就让你在工程里从零写一套完整Diameter协议栈那是重复发明轮子。常见做法是先用一份最简单的消息结构把业务逻辑跑顺验证“配额申请—授信—扣款—返还”这条链路再把对应部分替换为开源协议栈的编解码层。原型里有三个基础件CCR消息、余额单元、OCF处理器。先把消息和AVP写出来import uuid # ---------- 消息结构 ---------- def make_ccr(cc_type, session_id, requestedNone, usedNone, prev_grant0, req_num1): 构建一条信用控制请求消息。 cc_type: 1INITIAL_REQUEST, 2UPDATE_REQUEST, 3TERMINATION_REQUEST return { code: 272, # Diameter Credit-Control命令码 session_id: session_id, # 一次计费会话的唯一标识 cc_type: cc_type, req_num: req_num, # 消息序号用于幂等 requested: requested or {}, # 本次请求的配额 used: used or {}, # 本次已用的配额 prev_grant: prev_grant, # 上一次授权的配额用于返还 }这段代码构建了一条最小CCR。code是Diameter信用控制的命令码272session_id对应一次计费会话的唯一标识在真正的协议栈里它是一个FQDN格式的字符串。req_num对应CC-Request-Number后面做幂等判断时要拿它来判断消息是否重复。requested和used是AVP容器我用Python字典简化实际协议栈里会换成结构化对象。接下来写余额扣减和授信逻辑。用全局变量模拟ABMF账户后续可以替换成Redis或数据库# ---------- ABMF余额与授信 ---------- ACCOUNT_BALANCE 30.0 # 用户余额单位元 RATE_PER_SEC 0.05 # 0.05元/秒模拟语音通话费率 MAX_GRANT_SEC 120 # 单次授权上限秒 MIN_GRANT_SEC 10 # 最小授权阈值低于则视为余额不足 def query_balance(): return ACCOUNT_BALANCE def reserve_and_grant(requested_sec): 预扣配额对应的金额并返回可授权的秒数。 global ACCOUNT_BALANCE can_afford int(ACCOUNT_BALANCE / RATE_PER_SEC) # 余额能撑多久 grant min(requested_sec, can_afford, MAX_GRANT_SEC) if grant MIN_GRANT_SEC: return 0 ACCOUNT_BALANCE - grant * RATE_PER_SEC # 预扣 return grant def handle_ccr(msg): CCR分发器根据cc_type决定处理路径。 global ACCOUNT_BALANCE if msg[cc_type] 1: # CCR-I初始授权 req msg[requested].get(time, 60) grant reserve_and_grant(req) if grant 0: return {cca_type: 1, result: 4012, granted: 0, final_unit: True} return {cca_type: 1, result: 2001, granted: grant, final_unit: (grant MAX_GRANT_SEC)} if msg[cc_type] 2: # CCR-U续期并返还上一次未用量 prev msg[prev_grant] or 0 used msg[used].get(time, 0) refund max(0, (prev - used)) * RATE_PER_SEC ACCOUNT_BALANCE refund # 续期本质是一次新的初始授权 return handle_ccr(make_ccr(1, msg[session_id], requested{time: 60}, req_nummsg[req_num] 1)) if msg[cc_type] 3: # CCR-T完全结束退回全部剩余授权 prev msg[prev_grant] or 0 used msg[used].get(time, 0) refund max(0, (prev - used)) * RATE_PER_SEC ACCOUNT_BALANCE refund return {cca_type: 3, result: 2001, granted: 0}授信核心逻辑是reserve_and_grant先算余额可支撑的秒数再取“请求量、承受能力、单次上限”三个值里的最小值。grant小于最小阈值时直接返回4012余额不足避免授出1秒2秒这种没有意义的碎片。预扣发生在授权时返还发生在CCR-U或CCR-T时这样余额始终只反映“已授权未结算”的资金。代码里没有考虑同一个会话重复发送CCR-I的情况。真实OCS里CCR-I在一个会话中只能出现一次如果CTF重复发初始请求OCF要能识别并拒绝。为了看到完整流程再补一个驱动脚本# ---------- 模拟一次完整的语音通话 ---------- session_id str(uuid.uuid4()) # 1. 话务开始请求60秒 cca1 handle_ccr(make_ccr(1, session_id, requested{time: 60})) print(初始授信:, cca1[granted], s, 余额:, round(query_balance(), 2)) assert cca1[result] 2001 # 2. 用了40秒发起续期应退回(60-40)*0.051.0元再请求新的60秒 cca2 handle_ccr(make_ccr(2, session_id, requested{time: 60}, used{time: 40}, prev_grant60)) print(续期授信:, cca2[granted], s, 余额:, round(query_balance(), 2)) # 3. 通话结束上报最终用量50秒退回(60-50)*0.050.5元 cca3 handle_ccr(make_ccr(3, session_id, used{time: 50}, prev_grant60)) print(最终余额:, round(query_balance(), 2))预期输出能算出这样一条线初始授信后余额30减3元变27续期时退还1元到28再预扣3元到25结束时退还0.5元到25.5。用这个简单例子做基准之后引入并发和持久化时可以快速判断逻辑是否已经被破坏。4.2 四个必调的参数与它们的影响最小原型里有四个参数需要盯紧它们直接决定消息频率和透支风险参数初始值调高的影响调低的影响RATE_PER_SEC0.05元/秒授信秒数缩短CCR-U频率变高透支风险增加MAX_GRANT_SEC120秒单次授权大CCR-U频率低资金占用长消息风暴系统压力变大MIN_GRANT_SEC10秒能过滤极小额度请求可能拒绝低余额用户体验变差Validity-Time30~300秒配额寿命长续期少透支窗口变大会话频繁续期心跳压力大特别提醒一点CCR-U的续期不是“用完再报告”而是“到点上报一次用量、同时请求下一段配额”。因此OCF在收到CCR-U时要做两步动作先返还上一轮未用配额再预扣新一轮配额。这个“返还加预扣”的双写操作必须原子完成否则余额会出现短暂的虚高或虚低。原型里用全局变量模拟ABMF严格说是有问题的一旦改成并发全局变量的读改写就不是原子的。真正落地时我一般会把ACCOUNT_BALANCE替换成Redis的字符串Key把reserve_and_grant替换成一段Lua脚本保证“查询—扣减—返回授权”在单线程里执行这样能去掉大部分并发扣款的坑。5. OCS落地避坑最容易翻车的5个场景5.1 授权前没做余额预扣超授权导致欠费现象用户余额只剩0.10元费率是1元/MB系统却一次性授权了100MB。用户跑完这个流量后余额变成负数。原因授信逻辑只查了余额“够不够本次请求”没有做预扣。等到会话结束再按用量扣款期间其他并发会话已经把余额用掉最终扣款超过实际余额。解决授权前先按授信额度做预扣会话结束后返还未用部分。如果确实需要后扣模式则必须给ABMF加行级锁并在授权时冻结等额信用。用Redis Lua实现最小方案-- reserve.lua local balance tonumber(redis.call(GET, KEYS[1]) or 0) local max_quota math.floor(balance / tonumber(ARGV[1])) local quota math.min(tonumber(ARGV[2]), max_quota, tonumber(ARGV[3])) if quota 0 then return 0 end redis.call(DECRBY, KEYS[1], quota * tonumber(ARGV[1])) return quota这段脚本把“查余额、算上限、扣款、返回授权”放在一个原子步骤里四个操作不会因为并发而互相覆盖。KEYS[1]是账户余额KeyARGV[1]是单价ARGV[2]是请求配额ARGV[3]是单次上限。这就是前面reserve_and_grant的并发版本。别在Java或Python代码里做“先查再扣”两个操作之间一定有时间窗那个时间窗就是翻车的窗口。5.2 CCR-U超时导致配额失效现象日志里没有扣款异常但用户反馈“正在直播时断网恢复后立刻好”。网元侧看到的是应答下发成功可是配额过了有效期才到达。原因OCF通过Validity-Time限定了配额有效期CTF要在到期前发起下一轮CCR-U。当续期队列出现堆积或者CTF处理线程被阻塞从有效期截止到下一轮应答到达之间有几十秒的窗口网元严格按“没配额就断”执行。解决给CCR-U独立的处理通道不要和CCR-I、CCR-T混在同一个消费者里OCF侧对续期消息的处理逻辑要足够轻只做“返还加预扣”不落明细明细异步写。同时在网元侧设置合理的配额过期宽容值业界常见做法是允许0.5到1秒的过度容忍窗口窗口内CTF还来得及把上次配额用完。更深一层会话“心跳机制实现”要支持合并如果同一个会话的CCR-U在队列里积压了多条消费者只处理最新一条旧消息直接丢弃并返回超时。5.3 重发导致双扣现象同一用户短时间内出现两条金额完全一样的余额流水授权时间差毫秒级用户实际并没有发起两次通信。原因CCR-I或CCR-U在网络抖动时超时CTF重发同一请求但OCF没做幂等判断把它当成新请求又扣了一次。解决OCF维护一个会话缓存Key为“会话ID加请求序号”收到消息时先查缓存命中便不再扣款直接把上一次的应答原样返回。缓存过期时间要大于网络重发的最大周期。遇到无法确定是否已应答的场景宁可再次返回上一次的应答也不要再次扣款。这个坑我在压测时真踩过。当时把超时时间从2秒改成500毫秒压测脚本的重发风暴瞬间翻倍账务库里多出几十条重复扣款流水。从那以后“幂等优先宁可重复应答不可重复扣款”就成了OCS开发的第一原则。5.4 热点账户一行余额把整个OCS拖垮现象充值活动期间某账户余额Key的读写热度极高数据库行锁等待时间飙升周边数百用户的计费延迟全被放大。原因ABMF以账户ID为粒度加锁同一账户的所有扣减只能串行执行。热点账户是计费系统的典型冲击场景每次大促、抽奖、流量包秒杀都会碰到。解决对账户做“预拆分”把一个主账户拆成多个子账户授权时优先扣减最大余额的子账户返还时回到原子账户。更实用的一招是热点账户下“短配额”把单次授权从120秒砍到10秒让锁持有时间变短从而降低等待时延。配合监控活动前就要把热点账户的请求日志单独采样压测时先验证短配额和分片都生效再放量。5.5 Final-Unit-Indication只提示不关闭会话堆积现象余额仅够几十秒OCF已经下发final_unittrue且granted0CTF却没有终止会话依然继续发起CCR-UOCF反复返回失败计费会话状态始终不退场。原因CTF在处理Final-Unit-Indication时的逻辑分支太少只知道配额用完要续期不知道“最后一笔配额”意味着应该让业务主动下线。OCF侧也没把收到final后仍继续请求的会话兜底关闭。解决CTF状态机里必须识别final_unittrue且granted0直接进入终止流程OCF侧对“已经给过final却仍收到CCR-U”的会话直接返回4012并强制关闭会话释放所有计费状态。两边都做处理才是双保险只靠一侧迟早出问题。6. 上线前验证用模拟器和压力脚本守住计费正确性OCS的业务逻辑藏在几百行状态机代码里最怕的就是“改一个参数隔两天才在账上显出来”。所以上线前必须先做三类验证功能正确性、并发压力、故障恢复。第一类验证用模拟器把计费场景完整跑一遍按顺序发送“初始—续期—终止”每一步断言余额跟公式对得上。我把这套脚本叫“迷你题库”像刷题一样每次改动后都回归一遍。例如def verify_voice_charge(): balance_before query_balance() sess str(uuid.uuid4()) c1 handle_ccr(make_ccr(1, sess, requested{time: 60})) c2 handle_ccr(make_ccr(2, sess, requested{time: 60}, used{time: 40}, prev_grant60)) c3 handle_ccr(make_ccr(3, sess, used{time: 50}, prev_grant60)) cost 50 * RATE_PER_SEC # 最终确认计费50秒 assert abs(query_balance() - (balance_before - cost)) 1e-9这个断言的边界情况很实际如果续期时返还逻辑漏了最终余额会偏低如果终止请求忘记返还余额会偏高如果最终用量取错断言立刻失败。任何一次改动后只要跑一遍这套脚本配额返还那条链路有没有被破坏一眼可见。第二类并发压测把1000个不同会话同时打进来观察账户余额是否出现负数、扣款流水是否与授权总量对得上。压测时重点不要看平均时延要看P99。我一般把目标设在80毫秒以下超了就回到参数里找原因单次授权是不是设太大、热点账户是不是没拆分、CCR-U处理通道是不是和高负载任务混在一起。第三类是故障注入让OCF进程在会话中途重启再检查Redis里的会话是否按过期时间被清理、有没有“僵尸会话”把授权永远冻结。做完这三类验证才敢让OCS接真实流量。从我的经历看OCS这类系统最贵的不是开发成本而是出账后的查账成本。有次上线新计费套餐对账脚本晚了一个月才补进流水线结果上线第二天就有一批配额返还数据对不上人力核了整整一天。后来把对账脚本放进发布流水线才彻底止住这类问题。对账脚本就是计费系统的后悔药越早准备越便宜。希望帮到你。本文还有配套的精品资源点击获取