github.com/polarismesh/polaris@v1.17.8/plugin/healthchecker/redis/checker_redis.go (about) 1 /** 2 * Tencent is pleased to support the open source community by making Polaris available. 3 * 4 * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. 5 * 6 * Licensed under the BSD 3-Clause License (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * https://opensource.org/licenses/BSD-3-Clause 11 * 12 * Unless required by applicable law or agreed to in writing, software distributed 13 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 14 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 15 * specific language governing permissions and limitations under the License. 16 */ 17 18 package heartbeatredis 19 20 import ( 21 "context" 22 "encoding/json" 23 "fmt" 24 "strconv" 25 "strings" 26 "sync/atomic" 27 "time" 28 29 commonlog "github.com/polarismesh/polaris/common/log" 30 "github.com/polarismesh/polaris/common/redispool" 31 commontime "github.com/polarismesh/polaris/common/time" 32 "github.com/polarismesh/polaris/common/utils" 33 "github.com/polarismesh/polaris/plugin" 34 ) 35 36 var log = commonlog.GetScopeOrDefaultByName(commonlog.HealthcheckLoggerName) 37 38 // 把操作记录记录到日志文件中 39 const ( 40 // PluginName plugin name 41 PluginName = "heartbeatRedis" 42 // Sep separator to divide id and timestamp 43 Sep = ":" 44 // Servers key to manage hb servers 45 Servers = "servers" 46 // CountSep separator to divide server and count 47 CountSep = "|" 48 ) 49 50 // RedisHealthChecker 心跳检测redis 51 type RedisHealthChecker struct { 52 // 用于写入心跳数据的池 53 hbPool redispool.Pool 54 // 用于检查回调的池 55 checkPool redispool.Pool 56 cancel context.CancelFunc 57 statis plugin.Statis 58 suspendTimeSec int64 59 } 60 61 // Name plugin name 62 func (r *RedisHealthChecker) Name() string { 63 return PluginName 64 } 65 66 // Initialize initialize plugin 67 func (r *RedisHealthChecker) Initialize(c *plugin.ConfigEntry) error { 68 redisBytes, err := json.Marshal(c.Option) 69 if err != nil { 70 return fmt.Errorf("fail to marshal %s config entry, err is %v", PluginName, err) 71 } 72 var config redispool.Config 73 if err = json.Unmarshal(redisBytes, &config); err != nil { 74 return fmt.Errorf("fail to unmarshal %s config entry, err is %v", PluginName, err) 75 } 76 r.statis = plugin.GetStatis() 77 var ctx context.Context 78 ctx, r.cancel = context.WithCancel(context.Background()) 79 r.hbPool = redispool.NewRedisPool(ctx, &config, r.statis) 80 r.hbPool.Start() 81 r.checkPool = redispool.NewRedisPool(ctx, &config, r.statis) 82 r.checkPool.Start() 83 if err = r.registerSelf(); err != nil { 84 return fmt.Errorf("fail to register %s to redis, err is %v", utils.LocalHost, err) 85 } 86 return nil 87 } 88 89 func (r *RedisHealthChecker) registerSelf() error { 90 localhost := utils.LocalHost 91 resp := r.checkPool.Sdd(Servers, []string{localhost}) 92 return resp.Err 93 } 94 95 // Destroy plugin destroy 96 func (r *RedisHealthChecker) Destroy() error { 97 if nil != r.cancel { 98 r.cancel() 99 } 100 return nil 101 } 102 103 // Type for health check plugin, only one same type plugin is allowed 104 func (r *RedisHealthChecker) Type() plugin.HealthCheckType { 105 return plugin.HealthCheckerHeartbeat 106 } 107 108 // HeathCheckRecord 心跳记录 109 type HeathCheckRecord struct { 110 LocalHost string 111 CurTimeSec int64 112 Count int64 113 } 114 115 // IsEmpty 是否空对象 116 func (h *HeathCheckRecord) IsEmpty() bool { 117 return len(h.LocalHost) == 0 && h.CurTimeSec == 0 118 } 119 120 // Serialize 序列化成字符串 121 func (h *HeathCheckRecord) Serialize(compatible bool) string { 122 if compatible { 123 return fmt.Sprintf("1%s%d%s%s%s%d", Sep, h.CurTimeSec, Sep, h.LocalHost, CountSep, h.Count) 124 } 125 return fmt.Sprintf("%d%s%s%s%d", h.CurTimeSec, Sep, h.LocalHost, CountSep, h.Count) 126 } 127 128 func parseHeartbeatValue(value string, startIdx int) (host string, curTimeSec int64, count int64, err error) { 129 tokens := strings.Split(value, Sep) 130 if len(tokens) < startIdx+2 { 131 return "", 0, 0, fmt.Errorf("invalid redis value %s", value) 132 } 133 lastHeartbeatTimeStr := tokens[startIdx] 134 lastHeartbeatTime, err := strconv.ParseInt(lastHeartbeatTimeStr, 10, 64) 135 if err != nil { 136 return "", 0, 0, err 137 } 138 host = tokens[startIdx+1] 139 curTimeSec = lastHeartbeatTime 140 countSepIndex := strings.LastIndex(host, CountSep) 141 var countValue int64 142 if countSepIndex > 0 && countSepIndex < len(host) { 143 countStr := host[countSepIndex+1:] 144 countValue, err = strconv.ParseInt(countStr, 10, 64) 145 if err != nil { 146 return "", 0, 0, err 147 } 148 } 149 return host, curTimeSec, countValue, nil 150 } 151 152 // Deserialize 反序列为对象 153 func (h *HeathCheckRecord) Deserialize(value string, compatible bool) error { 154 if len(value) == 0 { 155 return nil 156 } 157 var err error 158 if compatible { 159 h.LocalHost, h.CurTimeSec, h.Count, err = parseHeartbeatValue(value, 1) 160 } else { 161 h.LocalHost, h.CurTimeSec, h.Count, err = parseHeartbeatValue(value, 0) 162 } 163 return err 164 } 165 166 // String 字符串化 167 func (h HeathCheckRecord) String() string { 168 return fmt.Sprintf("{LocalHost=%s, CurTimeSec=%d}", h.LocalHost, h.CurTimeSec) 169 } 170 171 // Report process heartbeat info report 172 func (r *RedisHealthChecker) Report(ctx context.Context, request *plugin.ReportRequest) error { 173 value := &HeathCheckRecord{ 174 LocalHost: request.LocalHost, 175 CurTimeSec: request.CurTimeSec, 176 Count: request.Count, 177 } 178 179 log.Debugf("[Health Check][RedisCheck]redis set key is %s, value is %s", request.InstanceId, *value) 180 resp := r.hbPool.Set(request.InstanceId, value) 181 if resp.Err != nil { 182 log.Errorf("[Health Check][RedisCheck]addr:%s:%d, id:%s, set redis err:%s", 183 request.Host, request.Port, request.InstanceId, resp.Err) 184 return resp.Err 185 } 186 return nil 187 } 188 189 // Query queries the heartbeat time 190 func (r *RedisHealthChecker) Query(ctx context.Context, request *plugin.QueryRequest) (*plugin.QueryResponse, error) { 191 resp := r.checkPool.Get(request.InstanceId) 192 if resp.Err != nil { 193 log.Errorf("[Health Check][RedisCheck]addr:%s:%d, id:%s, get redis err:%s", 194 request.Host, request.Port, request.InstanceId, resp.Err) 195 return nil, resp.Err 196 } 197 value := resp.Value 198 queryResp := &plugin.QueryResponse{ 199 Exists: resp.Exists, 200 } 201 if len(value) == 0 { 202 return queryResp, nil 203 } 204 heathCheckRecord := &HeathCheckRecord{} 205 err := heathCheckRecord.Deserialize(value, resp.Compatible) 206 if err != nil { 207 log.Errorf("[Health Check][RedisCheck]addr is %s:%d, id is %s, parse %s err:%v", 208 request.Host, request.Port, request.InstanceId, value, err) 209 return nil, err 210 } 211 queryResp.Server = heathCheckRecord.LocalHost 212 queryResp.LastHeartbeatSec = heathCheckRecord.CurTimeSec 213 queryResp.Count = heathCheckRecord.Count 214 return queryResp, nil 215 } 216 217 const maxCheckDuration = 500 * time.Second 218 219 func (r *RedisHealthChecker) skipCheck(instanceId string, expireDurationSec int64) bool { 220 suspendTimeSec := r.SuspendTimeSec() 221 localCurTimeSec := commontime.CurrentMillisecond() / 1000 222 if suspendTimeSec > 0 && localCurTimeSec >= suspendTimeSec && localCurTimeSec-suspendTimeSec < expireDurationSec { 223 log.Infof("[Health Check][RedisCheck]health check redis suspended, "+ 224 "suspendTimeSec is %d, localCurTimeSec is %d, expireDurationSec is %d, id %s", 225 suspendTimeSec, localCurTimeSec, expireDurationSec, instanceId) 226 return true 227 } 228 recoverTimeSec := r.checkPool.RecoverTimeSec() 229 // redis恢复期,不做变更 230 if recoverTimeSec > 0 && localCurTimeSec >= recoverTimeSec && localCurTimeSec-recoverTimeSec < expireDurationSec { 231 log.Infof("[Health Check][RedisCheck]health check redis on recover, "+ 232 "recoverTimeSec is %d, localCurTimeSec is %d, expireDurationSec is %d, id %s", 233 suspendTimeSec, localCurTimeSec, expireDurationSec, instanceId) 234 return true 235 } 236 return false 237 } 238 239 // Check Report process the instance check 240 func (r *RedisHealthChecker) Check(request *plugin.CheckRequest) (*plugin.CheckResponse, error) { 241 var startTime = time.Now() 242 defer func() { 243 var timePass = time.Since(startTime) 244 if timePass >= maxCheckDuration { 245 log.Warnf("[Health Check][RedisCheck]check %s cost %s duration, greater than max %s duration", 246 request.InstanceId, timePass, maxCheckDuration) 247 } 248 }() 249 queryResp, err := r.Query(context.Background(), &request.QueryRequest) 250 if err != nil { 251 return nil, err 252 } 253 lastHeartbeatTime := queryResp.LastHeartbeatSec 254 checkResp := &plugin.CheckResponse{ 255 LastHeartbeatTimeSec: lastHeartbeatTime, 256 } 257 curTimeSec := request.CurTimeSec() 258 if r.skipCheck(request.InstanceId, int64(request.ExpireDurationSec)) { 259 checkResp.StayUnchanged = true 260 return checkResp, nil 261 } 262 // 出现时间倒退,不对心跳状态做变更 263 if curTimeSec < lastHeartbeatTime { 264 log.Infof("[Health Check][RedisCheck]time reverse, curTime is %d, last heartbeat time is %d, id %s", 265 curTimeSec, lastHeartbeatTime, request.InstanceId) 266 checkResp.StayUnchanged = true 267 return checkResp, nil 268 } 269 // 正常进行心跳中 270 checkResp.Regular = true 271 if curTimeSec-lastHeartbeatTime >= int64(request.ExpireDurationSec) { 272 // 心跳超时 273 checkResp.Healthy = false 274 if request.Healthy { 275 log.Infof("[Health Check][RedisCheck]health check expired, "+ 276 "last hb timestamp is %d, curTimeSec is %d, expireDurationSec is %d instanceId %s", 277 lastHeartbeatTime, curTimeSec, request.ExpireDurationSec, request.InstanceId) 278 } else { 279 checkResp.StayUnchanged = true 280 } 281 } else { 282 // 心跳恢复 283 checkResp.Healthy = true 284 if !request.Healthy { 285 log.Infof("[Health Check][RedisCheck]health check resumed, "+ 286 "last hb timestamp is %d, curTimeSec is %d, expireDurationSec is %d instanceId %s", 287 lastHeartbeatTime, curTimeSec, request.ExpireDurationSec, request.InstanceId) 288 } else { 289 checkResp.StayUnchanged = true 290 } 291 } 292 log.Debugf("[Health Check][RedisCheck]instanceId is %s, healthy is %v", request.InstanceId, checkResp.Healthy) 293 return checkResp, nil 294 } 295 296 // Delete delete the target id 297 func (r *RedisHealthChecker) Delete(ctx context.Context, id string) error { 298 resp := r.checkPool.Del(id) 299 return resp.Err 300 } 301 302 // Suspend checker for an entire expired interval 303 func (r *RedisHealthChecker) Suspend() { 304 curTimeMilli := commontime.CurrentMillisecond() / 1000 305 log.Infof("[Health Check][RedisCheck] suspend checker, start time %d", curTimeMilli) 306 atomic.StoreInt64(&r.suspendTimeSec, curTimeMilli) 307 } 308 309 // SuspendTimeSec get suspend time in seconds 310 func (r *RedisHealthChecker) SuspendTimeSec() int64 { 311 return atomic.LoadInt64(&r.suspendTimeSec) 312 } 313 314 func (r *RedisHealthChecker) DebugHandlers() []plugin.DebugHandler { 315 return []plugin.DebugHandler{} 316 } 317 318 func init() { 319 d := &RedisHealthChecker{} 320 plugin.RegisterPlugin(d.Name(), d) 321 }