海量日志分析:一天内最大在线人数与最长持续时间计算方案
在用户行为分析、服务器资源调度、产品运营监控等场景中,"一天内的最大在线人数" 和 "维持最大在线人数的最长持续时间" 是核心指标 ------ 比如游戏运营需要知道峰值在线人数以扩容服务器,视频平台需要根据峰值时长优化带宽分配。但当日志量达到百万、甚至亿级时,"逐秒遍历统计" 的暴力方案会彻底失效,必须设计基于 "事件排序" 的高效算法。
本文以日志格式{userid, login_time, logout_time}(时间单位:秒,范围 0~86399,代表一天 24 小时)为例,完整讲解两大问题的解决思路与落地代码。
一、核心思路:从 "暴力遍历" 到 "事件点排序"
先明确一个关键观察:用户的在线状态变化只发生在 "登录时间" 和 "登出时间" 两个节点,中间时间段的在线状态是连续的。基于此,我们可以将每条日志转换成两个 "状态事件",再通过排序和遍历事件计算在线人数,彻底规避 "逐秒统计" 的性能瓶颈。
事件转换规则
对任意一条日志(user1, 100, 200)(user1 在第 100 秒登录,第 200 秒登出):
- 登录事件:(time=100, type=+1) → 第 100 秒时,在线人数 + 1;
- 登出事件:(time=200, type=-1) → 第 200 秒时,在线人数 - 1。
关键排序原则
当多个事件的time相同时,需按 "登出事件优先" 排序,避免计算错误:
- 例:事件 A(100, -1)(登出)和事件 B(100, +1)(登录),先处理 A 再处理 B;
- 原因:若用户 C 在 100 秒登出,用户 D 在 100 秒登录,正确的在线人数变化是 "C 登出(人数 - 1)→ D 登录(人数 + 1)",若先处理登录会导致 100 秒时人数多算 1 次。
二、问题(1):计算一天内的最大在线人数
算法步骤
- 日志预处理:过滤异常日志(如login_time > logout_time、login_time < 0、logout_time > 86399,异常日志直接丢弃或修正,如登出时间超 24 小时按 86399 处理);
- 事件转换:将每条有效日志转换为 "登录 + 1" 和 "登出 - 1" 两个事件;
- 事件排序:按time升序排序,time相同则 "登出事件(type=-1)" 排在 "登录事件(type=+1)" 前面;
- 遍历计算:初始化current_online=0、max_online=0,遍历排序后的事件,累加current_online,并实时更新max_online;
- 输出结果:max_online即为一天内的最大在线人数。
单机版代码实现(Python)
适用于日志量≤1000 万条的场景(Python 处理 1000 万事件排序约需 10~20 秒):
python
def calculate_max_online(logs):
"""
计算一天内的最大在线人数
:param logs: 日志列表,每条日志格式为 (userid, login_time, logout_time)
:return: max_online(最大在线人数)
"""
events = []
# 1. 日志预处理与事件转换
for userid, login, logout in logs:
# 过滤异常日志
if login < 0 or logout < login or logout > 86399:
continue
# 登录事件(type=1),登出事件(type=-1)
events.append((login, 1))
events.append((logout, -1))
# 2. 事件排序:time升序,time相同则登出事件(-1)在前
# 排序key:(time, type),因为-1 < 1,所以相同time时-1排在前面
events.sort(key=lambda x: (x[0], x[1]))
# 3. 遍历计算在线人数
current_online = 0
max_online = 0
for time, delta in events:
current_online += delta
# 更新最大在线人数(注意:current_online可能为负,需取非负后比较)
if current_online > max_online:
max_online = current_online
return max_online
# 测试示例
if __name__ == "__main__":
# 测试日志:(userid, login_time, logout_time)
test_logs = [
("user1", 100, 200), # 100秒登录,200秒登出
("user2", 150, 250), # 150秒登录,250秒登出
("user3", 180, 300), # 180秒登录,300秒登出
("user4", 200, 280), # 200秒登录,280秒登出(与user1登出时间相同)
]
print("最大在线人数:", calculate_max_online(test_logs)) # 输出3(180~200秒时,user1、user2、user3同时在线)
分布式方案(Spark)
当日志量超 1 亿条时,单机内存无法承载,需用 Spark 进行分布式处理,核心逻辑与单机版一致,只是通过 RDD/DataFrame 实现并行计算:
kotlin
import org.apache.spark.sql.SparkSession
object MaxOnlineCalculator {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("MaxOnlineCalculator")
.master("yarn") // 生产环境用yarn或k8s
.getOrCreate()
import spark.implicits._
// 1. 读取日志(假设日志存储在HDFS,格式为userid,login_time,logout_time)
val logsDF = spark.read
.option("header", "false")
.csv("hdfs://path/to/logs.csv")
.toDF("userid", "login_time", "logout_time")
.select(
$"userid",
$"login_time".cast("long"),
$"logout_time".cast("long")
)
// 2. 预处理+事件转换(并行执行)
val eventsDF = logsDF
.filter("login_time >= 0 and logout_time >= login_time and logout_time <= 86399")
.flatMap(row => {
val login = row.getAs[Long]("login_time")
val logout = row.getAs[Long]("logout_time")
// 返回两个事件:(time, delta)
Seq((login, 1L), (logout, -1L))
})
.toDF("time", "delta")
// 3. 事件排序(分布式排序,按time和delta排序)
val sortedEvents = eventsDF
.orderBy($"time", $"delta") // delta=-1排在1前面,符合排序原则
// 4. 遍历计算最大在线人数(用reduceByKey累加,避免全局洗牌)
// 这里用累加器实现全局计数(适用于需实时更新最大值的场景)
val maxOnlineAcc = spark.sparkContext.longAccumulator("maxOnline")
var currentOnline = 0L
sortedEvents.foreach(row => {
val time = row.getAs[Long]("time")
val delta = row.getAs[Long]("delta")
currentOnline += delta
if (currentOnline > maxOnlineAcc.value) {
maxOnlineAcc.add(currentOnline - maxOnlineAcc.value)
}
})
// 输出结果
println(s"一天内最大在线人数:${maxOnlineAcc.value}")
spark.stop()
}
}
三、问题(2):计算维持最大在线人数的最长持续时间
核心难点
最大在线人数可能在一天内出现多次(如早高峰 9 点和晚高峰 20 点都达到峰值),需找到 "每次峰值持续的时间段",计算每个时间段的时长,最终取最大值。
关键观察
- 峰值时间段的特征:current_online == max_online,且时间段为[start_time, end_time)(左闭右开,因为end_time是下一个事件的时间,事件发生时在线人数会变化);
- 时长计算:end_time - start_time(单位:秒);
- 例:事件序列为(180, +1)(人数 3,达峰值)→ (200, -1)(人数 2,峰值结束),则峰值时间段是[180, 200),时长 20 秒。
算法步骤
- 先按问题(1)的步骤计算出max_online;
- 重新遍历排序后的事件,记录每次current_online达到max_online的start_time;
- 当current_online从max_online下降时,记录end_time(当前事件的时间),计算时长end_time - start_time,并更新 "最长持续时间";
- 若遍历到最后一个事件时current_online仍为max_online,则end_time取 86399(一天结束时间),计算时长。
单机版代码实现(Python)
ini
def calculate_max_online_duration(logs):
"""
计算一天内最大在线人数的最长持续时间
:param logs: 日志列表,每条日志格式为 (userid, login_time, logout_time)
:return: (max_online, max_duration) (最大在线人数,最长持续时间,单位:秒)
"""
# 步骤1:生成排序后的事件(同问题1)
events = []
for userid, login, logout in logs:
if login < 0 or logout < login or logout > 86399:
continue
events.append((login, 1))
events.append((logout, -1))
# 排序:time升序,相同time时登出事件在前
events.sort(key=lambda x: (x[0], x[1]))
# 步骤2:先计算max_online(同问题1)
current_online = 0
max_online = 0
for time, delta in events:
current_online += delta
if current_online > max_online:
max_online = current_online
# 步骤3:计算最长峰值持续时间
current_online = 0
max_duration = 0
peak_start = None # 峰值开始时间
for i in range(len(events)):
time, delta = events[i]
prev_online = current_online # 事件发生前的在线人数
current_online += delta
# 情况1:事件后进入峰值状态(prev_online < max_online,current_online == max_online)
if prev_online < max_online and current_online == max_online:
peak_start = time
# 情况2:事件后退出峰值状态(prev_online == max_online,current_online < max_online)
elif prev_online == max_online and current_online < max_online:
if peak_start is not None:
# 峰值结束时间为当前事件时间
duration = time - peak_start
if duration > max_duration:
max_duration = duration
peak_start = None # 重置峰值开始时间
# 情况3:遍历到最后一个事件,且仍处于峰值状态
if i == len(events) - 1 and current_online == max_online and peak_start is not None:
duration = 86399 - peak_start # 结束时间为一天的最后一秒
if duration > max_duration:
max_duration = duration
return max_online, max_duration
# 测试示例
if __name__ == "__main__":
test_logs = [
("user1", 100, 200),
("user2", 150, 250),
("user3", 180, 300),
("user4", 200, 280),
("user5", 220, 280), # 220~280秒时,user2、user3、user4、user5同时在线(峰值4)
]
max_online, max_duration = calculate_max_online_duration(test_logs)
print(f"最大在线人数:{max_online},最长持续时间:{max_duration}秒") # 输出:最大4,最长60秒(220~280秒)
分布式方案优化
在 Spark 中计算最长持续时间,需避免 "全局遍历"(会导致数据集中到一个节点),可通过 "窗口函数" 标记峰值时间段,再计算每个时间段的时长:
- 用lag窗口函数获取上一个事件的time和current_online,标记当前事件是否处于峰值;
- 按 "是否峰值" 分组,计算每个峰值组的min(time)(start_time)和max(time)(end_time);
- 计算每个组的时长end_time - start_time,取最大值;
- 若最后一个事件仍处于峰值,需补充计算86399 - max(time)。
四、边界场景处理:避免计算错误
- 用户瞬间登录登出:login_time == logout_time(如(user1, 500, 500)),转换为两个事件(500, +1)和(500, -1),排序后先处理 - 1,current_online先 + 1 再 - 1,不影响最大人数(相当于未在线);
- 登出时间超一天:如logout_time=90000,按 86399 处理(一天的最后一秒),避免事件时间超出范围;
- 空日志或全异常日志:返回max_online=0,max_duration=0;
- 全天维持峰值:如所有用户登录时间 = 0,登出时间 = 86399,此时max_duration=86399秒(24 小时)。
五、性能优化建议
- 日志预处理过滤:提前过滤异常日志(如用 Flink 实时过滤写入 HDFS),减少后续计算的数据量;
- 事件去重:若同一用户在同一时间有多次登录登出(如日志重复),可先按(userid, login_time, logout_time)去重,避免重复生成事件;
- 时间粒度优化:若业务允许按 "分钟" 统计(而非秒),可将时间转换为分钟(time = time // 60),事件数量减少 60 倍,性能提升显著;
- 内存优化:单机场景用numpy数组存储事件(替代 Python 列表),减少内存占用;分布式场景用 Spark 的Kryo序列化,提升数据传输效率。
总结
处理海量日志的 "最大在线人数" 和 "最长持续时间",核心是 "将连续状态转换为离散事件":
- 用 "事件点排序法" 替代 "逐秒遍历",时间复杂度从 O (86400) 降为 O (n log n)(n 为事件数),适配海量数据;
- 排序时遵循 "登出优先" 原则,避免同一时间点的计算错误;
- 计算最长持续时间需关注 "峰值的开始与结束节点",用左闭右开区间计算时长;
- 单机场景适合中小数据量,分布式场景(Spark/Flink)适合亿级日志,需兼顾并行计算与全局状态更新。
这套方案不仅适用于用户在线统计,还可扩展到 "服务器连接数峰值""接口调用峰值" 等类似场景 ------ 只要是 "连续状态随离散事件变化" 的问题,都能复用 "事件排序 + 遍历计算" 的思路。