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  }