data-download-oom-2026-09-22.md 6.5 KB

DataDownload 内存 OOM 排查记录(2026-09-22)

状态:根因已定位并实锤,修复进行中(未完成)。 影响服务:fwh-toolbox-server(cgroup 内存上限 2GB)。 测试站点:10060001A00F22520081(时区 America/Chicago,-0500/-0600 且有 DST)。

一、现象

  • 创建 DataDownload 任务(尤其勾选「事件&告警」/「MQTT 日志」)后,后端进程内存在数秒内从 ~12MB 飙到 2GB,被 cgroup OOM killer 杀掉,随后 systemd 重启,反复循环(NRestarts 持续增长)。
  • 现象反常点:
    1. OOM 前 3 秒内无任何「上游返回 / 分页请求」业务日志;
    2. 数据量本身不大(PowerFlow 单日 ~20KB,正常);
    3. 多个 item 并发时更快触发。

二、根因(已实锤)

时间分片 periods 构建死循环 → periods 切片无限 append → OOM。

位置:

  • service/repositories/api/fwh_pro/fwh_eventAlarmLog.go 的 QueryEventOrAlarmListOnTimeRange
  • service/repositories/api/fwh_pro/fwh_mqttLog.go 的 QueryMqttLogListOnTimeRange

两函数内「按天拆分时间段」的游标推进逻辑:

currTime = dayEnd.Add(time.Second).Truncate(24 * time.Hour)          // Truncate 按 UTC 零点对齐
currTime = time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0,0,0,0, currTime.Location())

当传入的 startTime/endTime 带非 UTC 时区(如 America/Chicago,-0500)时:

  • dayEnd = 2026-09-19T23:59:59-0500,.Add(1s) = 2026-09-20T00:00:00-0500 = 2026-09-20T05:00:00 UTC;
  • .Truncate(24h) 按 UTC 零点截断 → 2026-09-20T00:00:00 UTC = 2026-09-19T19:00:00-0500(回退到 19 号晚上);
  • 再 time.Date(...,-0500) → 2026-09-19T00:00:00-0500;
  • currTime 从 09-19 变回 09-19 00:00,未前进 → currTime.Before(endTime) 恒成立 → 死循环。

验证日志实锤(每 500 个 period 打一次):

periods: 1443000  curr: 2026-09-19T00:00:00-0500  end: 2026-09-19T23:59:59-0500  ← curr 停滞不前
(数秒内 periods 涨到 144 万+,仍在增长)

三、为什么 BatchDownload 没出错(关键对比)

  • QueryEventOrAlarmListOnTimeRange / QueryMqttLogListOnTimeRange 是 fwh_pro 共享函数,多个模块调用(fwhTaskSiteAnalysis、fwhTaskBatchDownload、dataDownloadService、mcp/fwh_api、fwhQueueOld)。
  • BatchDownload (fwhTaskBatchDownload.go:55) 用 time.Parse(time.DateOnly, ...) 解析日期 → 结果是 UTC 时间。函数内 Truncate(24h) 也按 UTC 对齐,两者一致,游标正常前进,不触发。
  • DataDownload (dataDownloadService.go ExecuteStructuredDataDownloadItem) 用 time.ParseInLocation(time.DateOnly, item.Date, loc) 解析,loc = 站点本地时区(Chicago)→ 带非 UTC 时区 → 触发 Truncate(24h) 回退死循环。

结论:时间分片本不该带时区概念(就是「某段日期」的字面值)。BatchDownload 传 UTC 字面值恰好规避;DataDownload 传本地时区时间踩中共享函数里 Truncate(24h) 的 UTC 对齐陷阱。

四、修复方案(遵循「共享函数非自身必要不改」原则)

方向:不改被多模块共用的 fwh_pro 查询函数逻辑;在 DataDownload 调用侧把传给上游查询的时间剥离时区(转为同字面值的 UTC),与 BatchDownload 行为一致。

理由:上游查询只把时间 Format("2006-01-02 15:04:05") 成字符串发给 FWH;UTC 与 Chicago 解析出的 2026-09-14 00:00:00 字面值相同,查询语义不变,但避开 Truncate(24h) 时区陷阱。

注意:ExecuteStructuredDataDownloadItem 里的 start/end(本地时区)仍用于 Frozen(D+2) 判断(!time.Now().In(loc).Before(start.AddDate(0,0,2))),不能改动它们本身;只新增一对「字面值 UTC」时间专用于上游查询。

五、修改进展

已完成(service/services/dataDownloadService.go ExecuteStructuredDataDownloadItem):

  • 在 start/end 定义后新增 startQuery := stripTimeLocation(start) / endQuery := stripTimeLocation(end)(含说明注释);
  • mqtt / event 两个 case 的上游查询传参从 start,end 改为 startQuery,endQuery。

待完成(下次接续):

  1. 补上 stripTimeLocation 辅助函数(当前编译会因未定义而失败)。建议实现:把时间按字面 y/m/d/h/m/s 重建为 UTC:

    func stripTimeLocation(t time.Time) time.Time {
       if t.IsZero() { return t }
       return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC)
    }
    

    放在 parseRemoteTime(约 422 行)附近。

  2. 在两个共享查询函数上补注释(限制条件),避免再次踩坑:说明「startTime/endTime 必须为不含时区(UTC)的字面时间;内部按天拆分用 time.Truncate(24h) 按 UTC 对齐,传入非 UTC 时区会导致 periods 死循环 OOM」。

  3. 去掉临时验证日志(periods 持续增长 Warn),保留「periods 构建完成」Debug(可选)。

  4. go build ./...(相关包)+ go test ./services 验证。

  5. 清理毒任务(Mongo 活动态 item/task 置 cancelled + 清 asynq data-download:* 队列与 data-download:* 锁),重新部署 fwh-toolbox-server,重测 event&告警 + mqtt。

六、附带发现的其他问题(本次已处理/待办)

  • [已修] EMS 类型解析:dataDownloadBinaryService.go 用 strconv.Atoi("EmsRunLog") 失败。已加 resolveEmsLogType(字符串枚举→数字映射,兼容数字),与 BatchDownload 的 mapEmsLogType 一致。
  • [待办] UpdateLog detail 上游 500:selectIotFirmwareOrderDetailList 对某 orderId 返回 500,导致整个 update_log item 失败重试。建议单个 order detail 失败时跳过并记 Warn,不整体失败。
  • [遗留防护] mqtt 分页游标停滞(同一秒 ≥1000 条):已加 Error 日志 + 返回,作为极端防护(非当前根因)。
  • 已加大量过程日志:controller API 入口、plan/item/aggregate、请求租约获取/占用/丢失、各 kind 上游返回条数、二进制/FaultRecord 各步、HTTP 响应体字节数、分页请求前后。级别:Info=里程碑/API,Debug=规模细节,Warn=上游失败/重试,Error=最终失败。

七、并发放大(次要因素,非根因)

worker Concurrency: 8(dataDownloadQueue.go)。periods 死循环下,多个 item 并发只是让 OOM 更快;根因修复后并发不再是问题。是否降并发可后续评估,非必须。