healthcheck.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "sync/atomic"
  8. "time"
  9. "lc/common/protocol"
  10. "lc/common/util"
  11. )
  12. const (
  13. phase1Timeout = 30 * time.Second
  14. phase2Delay = 5 * time.Minute
  15. phase2Window = 5 * time.Minute
  16. )
  17. // HealthCheckRunner 健康检测运行器
  18. type HealthCheckRunner struct {
  19. marker *DeployMarker
  20. running int32
  21. phase2Done int32
  22. }
  23. var _hcRunner *HealthCheckRunner
  24. // StartHealthCheckIfNeeded 启动时检查部署标记,如有则运行健康检测
  25. func StartHealthCheckIfNeeded() {
  26. markerPath := filepath.Join(util.GetPath(0), "deploy_marker.json")
  27. data, err := os.ReadFile(markerPath)
  28. if err != nil {
  29. // 检查是否有回滚标记
  30. rollbackPath := filepath.Join(util.GetPath(0), "rollback_marker.json")
  31. if rbData, err := os.ReadFile(rollbackPath); err == nil {
  32. var marker DeployMarker
  33. json.Unmarshal(rbData, &marker)
  34. util.GetTagLog().Infof("sys", "检测到回滚标记,已回滚到版本:%s", marker.Version)
  35. reportRollback(marker)
  36. os.Remove(rollbackPath)
  37. }
  38. return
  39. }
  40. var marker DeployMarker
  41. if err := json.Unmarshal(data, &marker); err != nil {
  42. util.GetTagLog().Errorf("sys", "解析部署标记失败,err=%v", err)
  43. return
  44. }
  45. util.GetTagLog().Infof("sys", "检测到部署标记,开始健康检测 version=%s", marker.Version)
  46. _hcRunner = &HealthCheckRunner{
  47. marker: &marker,
  48. }
  49. go func() {
  50. time.Sleep(3 * time.Second) // 等待 MQTT 初始化
  51. _hcRunner.runPhase1()
  52. }()
  53. }
  54. func (o *HealthCheckRunner) runPhase1() {
  55. if !atomic.CompareAndSwapInt32(&o.running, 0, 1) {
  56. return
  57. }
  58. defer atomic.StoreInt32(&o.running, 0)
  59. util.GetTagLog().Infof("sys", "HealthCheck:Phase 1 开始 version=%s", o.marker.Version)
  60. deadline := time.Now().Add(phase1Timeout)
  61. checks := []protocol.HealthCheck{
  62. o.checkMQTT(deadline),
  63. o.checkRedis(deadline),
  64. }
  65. allOK := true
  66. for _, c := range checks {
  67. if c.Status != "ok" {
  68. allOK = false
  69. break
  70. }
  71. }
  72. o.reportPhase(1, allOK, checks)
  73. if !allOK {
  74. util.GetTagLog().Errorf("sys", "HealthCheck:Phase 1 失败,开始回滚")
  75. o.rollback(checks)
  76. return
  77. }
  78. util.GetTagLog().Infof("sys", "HealthCheck:Phase 1 全部通过,清理部署标记")
  79. os.Remove(filepath.Join(util.GetPath(0), "deploy_marker.json"))
  80. DeployResultSave(o.marker.Version, 1, allOK, checks)
  81. go func() {
  82. time.Sleep(phase2Delay)
  83. o.runPhase2()
  84. }()
  85. }
  86. func (o *HealthCheckRunner) checkMQTT(deadline time.Time) protocol.HealthCheck {
  87. start := time.Now()
  88. check := protocol.HealthCheck{Name: "mqtt"}
  89. for time.Now().Before(deadline) {
  90. mgr := GetMQTTMgr()
  91. if mgr.Cloud != nil && mgr.Cloud.IsConnected() {
  92. check.Status = "ok"
  93. check.Detail = "MQTT Broker 已连接"
  94. check.DurationMs = int(time.Since(start).Milliseconds())
  95. util.GetTagLog().Infof("sys", "HealthCheck:MQTT检测通过")
  96. return check
  97. }
  98. time.Sleep(1 * time.Second)
  99. }
  100. check.Status = "timeout"
  101. check.Detail = "MQTT Broker 不可达,请检查 Server/User/Password 配置、网络连通性"
  102. check.DurationMs = int(time.Since(start).Milliseconds())
  103. util.GetTagLog().Errorf("sys", "HealthCheck:MQTT检测失败")
  104. return check
  105. }
  106. func (o *HealthCheckRunner) checkRedis(deadline time.Time) protocol.HealthCheck {
  107. start := time.Now()
  108. check := protocol.HealthCheck{Name: "redis"}
  109. for time.Now().Before(deadline) {
  110. if redisEdgeData != nil {
  111. if _, err := redisEdgeData.Ping().Result(); err == nil {
  112. check.Status = "ok"
  113. check.Detail = "Redis PING 正常"
  114. check.DurationMs = int(time.Since(start).Milliseconds())
  115. util.GetTagLog().Infof("sys", "HealthCheck:Redis检测通过")
  116. return check
  117. }
  118. }
  119. time.Sleep(2 * time.Second)
  120. }
  121. check.Status = "timeout"
  122. check.Detail = "Redis 连接失败,请检查 Redis Server/Password 配置"
  123. check.DurationMs = int(time.Since(start).Milliseconds())
  124. util.GetTagLog().Errorf("sys", "HealthCheck:Redis检测失败")
  125. return check
  126. }
  127. func (o *HealthCheckRunner) runPhase2() {
  128. atomic.StoreInt32(&o.phase2Done, 1)
  129. util.GetTagLog().Infof("sys", "HealthCheck:Phase 2 开始 version=%s", o.marker.Version)
  130. checks := []protocol.HealthCheck{
  131. o.checkSerial(),
  132. o.checkModbusData(),
  133. }
  134. allOK := true
  135. for _, c := range checks {
  136. if c.Status != "ok" {
  137. allOK = false
  138. break
  139. }
  140. }
  141. o.reportPhase(2, allOK, checks)
  142. DeployResultSave(o.marker.Version, 2, allOK, checks)
  143. if !allOK {
  144. util.GetTagLog().Warnf("sys", "HealthCheck:Phase 2 存在告警,不回滚")
  145. } else {
  146. util.GetTagLog().Infof("sys", "HealthCheck:Phase 2 全部通过")
  147. }
  148. }
  149. func (o *HealthCheckRunner) checkSerial() protocol.HealthCheck {
  150. start := time.Now()
  151. check := protocol.HealthCheck{Name: "serial"}
  152. sc := GetSerialMgr()
  153. failedPorts := sc.GetFailedPorts()
  154. if len(failedPorts) == 0 {
  155. check.Status = "ok"
  156. check.Detail = "所有串口打开正常"
  157. } else {
  158. check.Status = "fail"
  159. parts := make([]string, len(failedPorts))
  160. for i, p := range failedPorts {
  161. parts[i] = fmt.Sprintf("%d", p)
  162. }
  163. check.Detail = "串口打开失败: " + strings.Join(parts, ", ")
  164. }
  165. check.DurationMs = int(time.Since(start).Milliseconds())
  166. return check
  167. }
  168. func (o *HealthCheckRunner) checkModbusData() protocol.HealthCheck {
  169. start := time.Now()
  170. check := protocol.HealthCheck{Name: "modbus"}
  171. deviceCount := 0
  172. hasData := false
  173. mapRtuUploadManager.Range(func(key, value interface{}) bool {
  174. mgr, ok := value.(*RtuUploadManager)
  175. if !ok {
  176. return true
  177. }
  178. deviceCount++
  179. mgr.DataLock.Lock()
  180. lastDataTime := mgr.Datatime
  181. mgr.DataLock.Unlock()
  182. if time.Since(lastDataTime) < phase2Window {
  183. hasData = true
  184. return false
  185. }
  186. return true
  187. })
  188. if deviceCount == 0 {
  189. check.Status = "ok"
  190. check.Detail = fmt.Sprintf("无Modbus设备配置,跳过采集检测")
  191. } else if hasData {
  192. check.Status = "ok"
  193. check.Detail = fmt.Sprintf("设备数据采集正常(%d台设备)", deviceCount)
  194. } else {
  195. check.Status = "fail"
  196. check.Detail = fmt.Sprintf("%d台设备5分钟内无数据上报,请检查设备配置和物模型是否正确加载", deviceCount)
  197. }
  198. check.DurationMs = int(time.Since(start).Milliseconds())
  199. return check
  200. }
  201. func (o *HealthCheckRunner) reportPhase(phase int, success bool, checks []protocol.HealthCheck) {
  202. var obj protocol.Pack_HealthCheck
  203. seq := GetNextUint64()
  204. if str, err := obj.EnCode(appConfig.GID, appConfig.GID, seq, o.marker.Version, phase, success, checks); err == nil {
  205. topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_HEALTH)
  206. GetMQTTMgr().Publish(topic, str, 0, ToCloud)
  207. util.GetTagLog().Infof("sys", "HealthCheck:Phase %d 结果已上报 success=%v", phase, success)
  208. }
  209. }
  210. func (o *HealthCheckRunner) rollback(checks []protocol.HealthCheck) {
  211. cwd, _ := os.Getwd()
  212. targetPath := filepath.Join(cwd, appname)
  213. backupPath := targetPath + ".bak"
  214. rbMarker := DeployMarker{
  215. Version: o.marker.Version,
  216. Timestamp: time.Now().Unix(),
  217. Action: "rollback",
  218. }
  219. rbContent, _ := json.MarshalToString(rbMarker)
  220. os.WriteFile(filepath.Join(util.GetPath(0), "rollback_marker.json"), []byte(rbContent), os.ModePerm)
  221. os.Remove(filepath.Join(util.GetPath(0), "deploy_marker.json"))
  222. DeployResultSave(o.marker.Version, 1, false, checks)
  223. if _, err := os.Stat(backupPath); os.IsNotExist(err) {
  224. util.GetTagLog().Errorf("sys", "HealthCheck:备份文件不存在,无法自动回滚")
  225. return
  226. }
  227. if err := os.Rename(backupPath, targetPath); err != nil {
  228. util.GetTagLog().Errorf("sys", "HealthCheck:回滚失败,err=%v", err)
  229. return
  230. }
  231. os.Chmod(targetPath, 0755)
  232. util.GetTagLog().Infof("sys", "HealthCheck:回滚成功,退出进程")
  233. time.Sleep(500 * time.Millisecond)
  234. os.Exit(2)
  235. }
  236. func reportRollback(marker DeployMarker) {
  237. var obj protocol.Pack_DeployAck
  238. seq := GetNextUint64()
  239. if str, err := obj.EnCode(appConfig.GID, appConfig.GID, seq, marker.Version, false,
  240. "已自动回滚到版本 "+marker.Version); err == nil {
  241. topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_ACK)
  242. GetMQTTMgr().Publish(topic, str, 0, ToCloud)
  243. }
  244. }
  245. // ---- 部署结果持久化 ----
  246. type DeployResult struct {
  247. Version string `json:"version"`
  248. Timestamp int64 `json:"timestamp"`
  249. Phase1 *DeployPhaseResult `json:"phase1,omitempty"`
  250. Phase2 *DeployPhaseResult `json:"phase2,omitempty"`
  251. }
  252. type DeployPhaseResult struct {
  253. Success bool `json:"success"`
  254. Checks []protocol.HealthCheck `json:"checks"`
  255. }
  256. var _deployResult *DeployResult
  257. func DeployResultSave(version string, phase int, success bool, checks []protocol.HealthCheck) {
  258. if _deployResult == nil || _deployResult.Version != version {
  259. _deployResult = &DeployResult{
  260. Version: version,
  261. Timestamp: time.Now().Unix(),
  262. }
  263. }
  264. pr := &DeployPhaseResult{Success: success, Checks: checks}
  265. if phase == 1 {
  266. _deployResult.Phase1 = pr
  267. } else {
  268. _deployResult.Phase2 = pr
  269. }
  270. content, _ := json.MarshalToString(_deployResult)
  271. resultPath := filepath.Join(util.GetPath(0), "deploy_result.json")
  272. os.WriteFile(resultPath, []byte(content), os.ModePerm)
  273. }
  274. func DeployResultGet() *DeployResult {
  275. if _deployResult != nil {
  276. return _deployResult
  277. }
  278. resultPath := filepath.Join(util.GetPath(0), "deploy_result.json")
  279. data, err := os.ReadFile(resultPath)
  280. if err != nil {
  281. return nil
  282. }
  283. var result DeployResult
  284. if err := json.Unmarshal(data, &result); err != nil {
  285. return nil
  286. }
  287. _deployResult = &result
  288. return &result
  289. }