在电信级计费系统中,RADIUS协议依靠UDP传输,网络设备会在未收到确认时重复发送计费请求。如果不加处理,同一笔上网时长会被记录多次,直接影响出账准确性。Ruby凭借其简洁的集合与哈希操作,非常适合在业务网关或中间件层做轻量重复检测。核心思路是为每个计费包提取稳定指纹,再结合时间窗口决策是否丢弃。

重复检测指纹的选取与碰撞分析
最常见的指纹来源是RADIUS属性中的Acct-Session-Id,它唯一标识一次用户会话。但在实际网络中,部分老旧设备会在会话中断后复用标识,或同一会话分多个计费包上报。如果仅用会话标识判断,会把续费包误认为重复包而丢弃。因此更稳妥的做法是拼接Acct-Session-Id与Event-Timestamp,必要时加上NAS-IP-Address,形成三元组指纹。
在Ruby中我们可以用字符串插值快速生成指纹键。要注意Event-Timestamp单位为秒,若设备时钟存在毫秒级偏差,相同逻辑包可能打出不同秒级时间戳。此时可对时间戳做向下取整到五秒区间,降低误判。下面的代码展示了从请求哈希中提取字段并生成指纹的方法,其中request是解析后的属性散列。
def build_fingerprint(request)
session_id = request['Acct-Session-Id']
nas_ip = request['NAS-IP-Address']
ts = request['Event-Timestamp'].to_i
bucket = ts - (ts % 5)
key = "#{nas_ip}|#{session_id}|#{bucket}"
key
end
上述方式在单机上碰撞概率极低。若集群部署,指纹必须放入共享存储,否则每个节点只看见部分流量,重复包会从其他节点绕过检测。后面会说明如何用Redis统一指纹态。
基于Ruby与Redis的丢弃算法实现
重复包丢弃算法的本质是查询并写入的原子性:先问缓存有无此指纹,有则丢,无则存并设置过期。若拆成两步,高并发时两个相同包可能同时通过查询,造成双写。Ruby的redis库支持pipeline与setnx类命令,但更推荐直接用Redis的SET命令附带NX与EX参数,一条指令完成判断与过期。
以下代码封装了检测逻辑,方法返回true表示应丢弃。我们设置过期时间为三百秒,覆盖设备最大重传周期。若业务允许更长会话,可调整为会话时长加缓冲。注意连接对象应复用,避免每次请求新建TCP。
require 'redis'
class RadiusDedup
def initialize(redis_client)
@redis = redis_client
end
def duplicate?(fp)
# SET key 1 NX EX 300,成功返回OK,已存在返回nil
result = @redis.set(fp, '1', nx: true, ex: 300)
result.nil?
end
end
对于批量重传场景,可以在Ruby侧先对同批包按指纹去重,再调用duplicate?,减少Redis往返。此外应记录丢弃计数,方便运维观察设备健康度。当某NAS的丢弃率突增,往往代表链路丢包而非代码缺陷。
性能权衡与多进程部署注意点
纯内存哈希在单进程Ruby服务里速度最快,但受限于进程内存与重启丢失。若服务重启,短时间内重复包会穿透。用Redis后虽然增加约毫秒级延迟,却换来集群一致性与持久化。在十万级日活场景下,单实例Redis即可支撑,遇到更大流量可按NAS-IP做分片。
多进程模型如Puma或Unicorn,每个worker持有独立Ruby对象,但共享同一Redis,因此算法仍正确。需避免的是在Ruby层用全局变量做缓存却又多进程,那会造成各进程视角不一。下面的表格对比了三种方案差异,帮助按规模选型。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 进程内哈希 | 零网络开销,代码简单 | 重启失效,多进程无效 | 单机测试或极低流量 |
| Redis单实例 | 集群一致,易持久化 | 微秒到毫秒延迟 | 中小规模生产 |
| Redis分片 | 可水平扩展 | 运维复杂 | 千万级请求 |
最后补充一点,RADIUS还有计费响应丢失导致的整个会话重发,此时所有包指纹均新,算法不会丢包但会重复入账。这需要在账单结算侧按会话做聚合,而非仅靠包级丢弃解决。Ruby可在落库前用GROUP BY会话标识二次归并,彻底闭环计费准确性。