# 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` 两函数内「按天拆分时间段」的游标推进逻辑: ```go 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: ```go 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 更快;根因修复后并发不再是问题。是否降并发可后续评估,非必须。