bigywater.tech

用日志写入时间代替事件时间,峰值统计会差多少

接着高峰在线人数那篇的一个后续问题:如果没有真实的事件时间字段,直接拿"日志写入时间"当成登录/登出时间来算,行不行?答案不是简单的行或不行——真实实验证明,关键在于登录和登出这两条日志的写入延迟是不是对称的。

真实验证:并发写日志,物理顺序对不上事件发生顺序

200 个用户"本该"按 0,1,2...199 的顺序登录,但每个登录请求的处理都会有一点点不确定的延迟(排队、加锁、调度)——用真实的 Python 多线程模拟:

真实实测
def handle_login(uid, event_time):
    time.sleep(random.uniform(0, 0.005))  # 模拟处理延迟抖动
    with log_lock:
        log_lines.append((uid, event_time))  # 真正写入日志的时刻

# 200 个线程并发调用 handle_login(uid, uid),uid 本身代表"事件本该发生的顺序"
物理写入顺序前20个: [13, 9, 20, 19, 26, 0, 8, 35, 56, 3, 16, 24, 42, 27, 30, 39, 31, 32, 71, 72] 按事件时间排序后前20个: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 物理顺序 == 时间顺序: False 逆序对数量: 3503 / 总记录数 200

200 条记录里有 3503 对是逆序的——只要"事件发生"和"写入日志"这两个动作之间隔着不确定的处理延迟,物理写入顺序就没法保证等于事件真正发生的顺序。这跟并发转账死锁是同一类问题:并发环境下,任何"先发生先记录"的假设都需要真实验证,不能凭直觉认为理所当然。

真实验证:对称抖动几乎无影响,系统性偏差不是

30 万条真实模拟记录,分别测三种"用日志时间代替事件时间"的情形:

真实实测
真实峰值(ground truth): 110460

# 场景A: 登录登出的写入延迟都是 -0.5~+0.5 秒的对称随机抖动
# 场景B: 登录几乎实时写入,登出走异步清理,统一晚 2.5~3.5 秒才写日志
# 场景C: 每 500ms 批量 flush 一次日志
对称随机抖动后的峰值: 110463 (误差: 3) 登出系统性延迟3秒后的峰值: 110475 (误差: 15) 批量flush(500ms)后的峰值: 110460 (误差: 0)

对称抖动和批量 flush 的误差都小到可以忽略——抖动是随机的,登录早一点、登出也早一点,大体上互相抵消;批量 flush 只是把时间"取整"到固定刻度,不会引入方向性偏差。但登出系统性延迟 3 秒,峰值稳定地偏高了 15——这不是随机噪声,是一个固定方向的偏差,不会因为数据量大就被平均抵消。

真实验证:更贴近真实场景,系统性偏差被放大到 5%

上面用的会话时长动辄几万秒,几秒钟的延迟自然显得微不足道。换成更真实的场景:一次登录潮里,8000 个用户在 5 分钟内集中登录,会话时长 1~10 分钟:

真实实测
真实峰值(会话1~10分钟): 6603

logged_records = [(uid, lo, hi + DELAY) for uid, lo, hi in real_records]  # 登出延迟 DELAY 秒才写日志
登出延迟3秒写日志后的峰值: 6645 (误差 +42, +0.64%) 登出延迟30秒写日志后的峰值: 6959 (误差 +356, +5.39%)

延迟越大,误差越大,而且方向永远是正的——"登出写晚了"意味着系统会误以为这些用户还在线,多算一段时长,这是一个稳定的正向偏差,不是能被大数据量平均掉的随机噪声。会话越短(相对延迟占比越大)、延迟越大,失真越严重。

真实验证:一个从未真实发生过的"幽灵峰值"

把上面的偏差落实成一个具体到"秒"的小例子:A、B、C 三个用户真实在线区间是 A=[10,20) B=[15,30) C=[22,35)——A 在 20 秒时就已经真实登出,跟 C(22秒才上线)根本没有同时在线过。真实峰值只有 2(发生在 A、B 重叠的 [15,20))。但如果登出统一晚 5 秒才写日志(A 的登出被记成 25 而不是 20):

真实实测
真实区间: A=[10,20)  B=[15,30)  C=[22,35)
日志区间(登出+5秒): A=[10,25)  B=[15,35)  C=[22,40)
真实峰值(按真实时间算): 2 日志峰值(按日志时间算): 3 ← A 明明已经走了,日志却以为 A、B、C 三人在 [22,25) 同时在线!

问题就出在 t=22 C 登录的那一刻:真实世界里 A 早就走了(真实登出在 t=20),但日志记录里 A 的登出被拖到了 t=25 才写进去,所以从日志的视角看,[22,25) 这段时间"A、B、C 都在线"——一个真实世界里从没发生过的时刻,凭空被算成了峰值。这不是理论上的担忧,是可以真实构造出来的具体反例。

一句话总结: 能不能拿日志写入时间代替真实事件时间,取决于登录和登出这两条写入路径的延迟是不是对称的。对称的随机抖动确实"统计意义上够用",可以不深究;但只要两边写入路径不一样(最常见的就是"登出走异步清理/批量任务,登录走同步写入"这种设计),就会引入一个方向固定、随延迟增大而增大的系统性偏差——而且偏差的方向永远是让峰值偏高,因为"该下线的还没被记下线"。这种偏差不会被大数据量平均掉,必须先搞清楚登录登出两条日志的写入延迟是否对称,再决定能不能这么简化。
0 / 0

3 个用户:实线是真实在线区间,斜纹尾巴是"登出延迟5秒"多出来的幽灵区间

0 20 40
真实在线(实线) 日志误差尾巴(斜纹)
0
真实峰值
0
日志算出的峰值

当前事件

—

点击"下一步"或"播放"开始。

☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电
京ICP备2026067502号-1京公网安备11010502063487号