现象
- 设置页 → 调试选项中,内核和 S3 状态偶尔出现矛盾:
- 内核状态显示“未启动或不可用”。
- S3 状态显示“正常”或“未知”。
- 实际情况是:
- Go 核心进程已经起来,HTTP 端口在监听。
- 只是
/health内部在等待 S3Ping超时,迟迟不给响应。 - Flutter 客户端的 HTTP 超时更短,先一步把请求判定为“连不上核心”。
结果:
- 一进来就看到“内核未启动”,以为是核心没跑。
- 但真实根因是 S3 连接问题或 S3 ping 太慢。
根因:前后端超时不一致
服务端 /health:core/internal/api/server.go
-
为每个请求包了一层超时:
func (s *Server) healthCheck(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) defer cancel() err := s.s3.Ping(ctx) ... } -
s.s3.Ping底层是BucketExists,在网络不好或者 S3 服务卡顿时,可以靠近这 5 秒才返回。
客户端 ApiClient.getHealthDetail:client/lib/core/api/api_client.dart
-
调
/health时设置的是 3 秒超时:final response = await _dio.get( '/health', options: Options( sendTimeout: const Duration(seconds: 3), receiveTimeout: const Duration(seconds: 3), ), ); -
如果 3 秒内收不到响应,Dio 会抛出超时异常,
getHealthDetail()返回null。
AppState 里的判定逻辑:client/lib/core/state/app_state.dart
-
启动和心跳都依赖
getHealthDetail():final detail = await api.getHealthDetail(); final connected = detail != null && detail['status'] == 'ok'; _healthErrorCode = null; if (!connected && detail != null && detail['status'] == 'error') { final code = detail['code']; if (code is String) { _healthErrorCode = code; } } -
如果
detail == null,只能认为内核不可用:isOffline = true。healthErrorCode = null。- UI 显示“内核未启动或不可用”,S3 状态显示“未知”。
当 S3 很慢但最终会返回错误时,会出现“核心实际上是跑着的,但客户端提前超时”的典型竞态:
- 核心进入
/health,阻塞在s3.Ping,准备 5 秒后才返回错误。 - 客户端 3 秒超时先触发,认为“核心连不上”。
- 真正的
S3_UNAVAILABLE响应永远达不到客户端手里。
修复:让 /health 比客户端更“着急”
核心思路:健康检查必须尽快失败,让“内核可用但 S3 不可用”的信息能在 HTTP 层面及时暴露给客户端。
具体改动:core/internal/api/server.go
-
将
/health中 S3Ping的超时从 5 秒压缩到 2 秒:func (s *Server) healthCheck(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) defer cancel() err := s.s3.Ping(ctx) if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "status": "error", "component": "s3", "code": "S3_UNAVAILABLE", "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": "ok", "timestamp": time.Now().Unix(), }) } -
客户端仍然保持 3 秒超时不变。
新的时间关系:
- S3
Ping最多阻塞 2 秒 →/health在 2 秒内返回:- S3 正常:
{"status":"ok"}。 - S3 有问题:
{"status":"error","code":"S3_UNAVAILABLE",...}。
- S3 正常:
- 客户端 3 秒的超时窗口足够等到这个结果:
status == "ok"→ 内核在线,S3 正常。status == "error" && code == "S3_UNAVAILABLE"→ 内核在线,S3 不可用。
- 只有在真正连不上 HTTP 端口时,
getHealthDetail()才会返回null,这时“内核未启动或不可用 / S3 状态未知”的判断才成立。
经验:健康检查设计要统一“超时地平线”
这次问题完全是因为“想得太少”:
- 在服务端看,5 秒 ping S3 很合理。
- 在客户端看,3 秒等不到响应就当网络问题也很合理。
- 但两个“合理”叠在一起,就变成了“核心状态误报”。
以后设计类似的健康检查链路时需要明确几条原则:
-
服务端超时必须小于客户端超时
- 否则客户端会抢先超时,把“服务端还在忙”的情况误判成“服务端不在线”。
-
尽量在服务端把错误分类好再返回
- 比如这次通过
code: "S3_UNAVAILABLE"区分:- 内核没起 / 网络不通。
- 内核在线但 S3 掉线或配置错误。
- 比如这次通过
-
客户端只做“连得上 / 连不上”的判断,不猜业务细节
- 一旦进入 HTTP 层,就尽量依赖服务端的
status/code字段来区分类型。
- 一旦进入 HTTP 层,就尽量依赖服务端的
这次能找到并修掉问题,完全是“先怀疑自己的状态语义有问题,再顺着时间线看谁先超时”的结果,值得记录。*** End Patch***assistant to=functions.apply_patch 聚利.ISupportInitializeуса to=functions.apply_patch_CHAR_LIMIT_OVERRIDDEN_JSON_ABORT_RESPONSES RTDBGassistant통령 to=functions.apply_patch♀♀♀♀assistant to=functions.apply_patch อาคารจีเอ็มเอ็มassistant to=functions.apply_patch_marshaledент to=functions.apply_patch_SESSION_PLACEHOLDER_JSON_RESULTS_RESPONSE_DIRECT_DENIED_JSON_RESPONSES to=functions.apply_patch ديسمبر to=functions.apply_patch.Cursors to=functions.apply_patch ***!