github.com/thanos-io/thanos@v0.32.5/pkg/cacheutil/redis_client.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package cacheutil
     5  
     6  import (
     7  	"context"
     8  	"crypto/tls"
     9  	"net"
    10  	"strings"
    11  	"time"
    12  	"unsafe"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/prometheus/client_golang/prometheus/promauto"
    19  	"github.com/redis/rueidis"
    20  	"gopkg.in/yaml.v3"
    21  
    22  	"github.com/thanos-io/thanos/pkg/extprom"
    23  	"github.com/thanos-io/thanos/pkg/gate"
    24  	"github.com/thanos-io/thanos/pkg/model"
    25  	thanos_tls "github.com/thanos-io/thanos/pkg/tls"
    26  )
    27  
    28  var (
    29  	// DefaultRedisClientConfig is default redis config.
    30  	DefaultRedisClientConfig = RedisClientConfig{
    31  		DialTimeout:            time.Second * 5,
    32  		ReadTimeout:            time.Second * 3,
    33  		WriteTimeout:           time.Second * 3,
    34  		MaxGetMultiConcurrency: 100,
    35  		GetMultiBatchSize:      100,
    36  		MaxSetMultiConcurrency: 100,
    37  		SetMultiBatchSize:      100,
    38  		TLSEnabled:             false,
    39  		TLSConfig:              TLSConfig{},
    40  		MaxAsyncConcurrency:    20,
    41  		MaxAsyncBufferSize:     10000,
    42  	}
    43  )
    44  
    45  // TLSConfig configures TLS connections.
    46  type TLSConfig struct {
    47  	// The CA cert to use for the targets.
    48  	CAFile string `yaml:"ca_file"`
    49  	// The client cert file for the targets.
    50  	CertFile string `yaml:"cert_file"`
    51  	// The client key file for the targets.
    52  	KeyFile string `yaml:"key_file"`
    53  	// Used to verify the hostname for the targets. See https://tools.ietf.org/html/rfc4366#section-3.1
    54  	ServerName string `yaml:"server_name"`
    55  	// Disable target certificate validation.
    56  	InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
    57  }
    58  
    59  // RedisClientConfig is the config accepted by RedisClient.
    60  type RedisClientConfig struct {
    61  	// Addr specifies the addresses of redis server.
    62  	Addr string `yaml:"addr"`
    63  
    64  	// Use the specified Username to authenticate the current connection
    65  	// with one of the connections defined in the ACL list when connecting
    66  	// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
    67  	Username string `yaml:"username"`
    68  	// Optional password. Must match the password specified in the
    69  	// requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
    70  	// or the User Password when connecting to a Redis 6.0 instance, or greater,
    71  	// that is using the Redis ACL system.
    72  	Password string `yaml:"password"`
    73  
    74  	// DB Database to be selected after connecting to the server.
    75  	DB int `yaml:"db"`
    76  
    77  	// DialTimeout specifies the client dial timeout.
    78  	DialTimeout time.Duration `yaml:"dial_timeout"`
    79  
    80  	// ReadTimeout specifies the client read timeout.
    81  	ReadTimeout time.Duration `yaml:"read_timeout"`
    82  
    83  	// WriteTimeout specifies the client write timeout.
    84  	WriteTimeout time.Duration `yaml:"write_timeout"`
    85  
    86  	// MaxGetMultiConcurrency specifies the maximum number of concurrent GetMulti() operations.
    87  	// If set to 0, concurrency is unlimited.
    88  	MaxGetMultiConcurrency int `yaml:"max_get_multi_concurrency"`
    89  
    90  	// GetMultiBatchSize specifies the maximum size per batch for mget.
    91  	GetMultiBatchSize int `yaml:"get_multi_batch_size"`
    92  
    93  	// MaxSetMultiConcurrency specifies the maximum number of concurrent SetMulti() operations.
    94  	// If set to 0, concurrency is unlimited.
    95  	MaxSetMultiConcurrency int `yaml:"max_set_multi_concurrency"`
    96  
    97  	// SetMultiBatchSize specifies the maximum size per batch for pipeline set.
    98  	SetMultiBatchSize int `yaml:"set_multi_batch_size"`
    99  
   100  	// TLSEnabled enable tls for redis connection.
   101  	TLSEnabled bool `yaml:"tls_enabled"`
   102  
   103  	// TLSConfig to use to connect to the redis server.
   104  	TLSConfig TLSConfig `yaml:"tls_config"`
   105  
   106  	// If not zero then client-side caching is enabled.
   107  	// Client-side caching is when data is stored in memory
   108  	// instead of fetching data each time.
   109  	// See https://redis.io/docs/manual/client-side-caching/ for info.
   110  	CacheSize model.Bytes `yaml:"cache_size"`
   111  
   112  	// MasterName specifies the master's name. Must be not empty
   113  	// for Redis Sentinel.
   114  	MasterName string `yaml:"master_name"`
   115  
   116  	// MaxAsyncBufferSize specifies the queue buffer size for SetAsync operations.
   117  	MaxAsyncBufferSize int `yaml:"max_async_buffer_size"`
   118  
   119  	// MaxAsyncConcurrency specifies the maximum number of SetAsync goroutines.
   120  	MaxAsyncConcurrency int `yaml:"max_async_concurrency"`
   121  }
   122  
   123  func (c *RedisClientConfig) validate() error {
   124  	if c.Addr == "" {
   125  		return errors.New("no redis addr provided")
   126  	}
   127  
   128  	if c.TLSEnabled {
   129  		if (c.TLSConfig.CertFile != "") != (c.TLSConfig.KeyFile != "") {
   130  			return errors.New("both client key and certificate must be provided")
   131  		}
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  type RedisClient struct {
   138  	client rueidis.Client
   139  
   140  	config RedisClientConfig
   141  
   142  	// getMultiGate used to enforce the max number of concurrent GetMulti() operations.
   143  	getMultiGate gate.Gate
   144  	// setMultiGate used to enforce the max number of concurrent SetMulti() operations.
   145  	setMultiGate gate.Gate
   146  
   147  	logger           log.Logger
   148  	durationSet      prometheus.Observer
   149  	durationSetMulti prometheus.Observer
   150  	durationGetMulti prometheus.Observer
   151  
   152  	p *asyncOperationProcessor
   153  }
   154  
   155  // NewRedisClient makes a new RedisClient.
   156  func NewRedisClient(logger log.Logger, name string, conf []byte, reg prometheus.Registerer) (*RedisClient, error) {
   157  	config, err := parseRedisClientConfig(conf)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	return NewRedisClientWithConfig(logger, name, config, reg)
   163  }
   164  
   165  // NewRedisClientWithConfig makes a new RedisClient.
   166  func NewRedisClientWithConfig(logger log.Logger, name string, config RedisClientConfig,
   167  	reg prometheus.Registerer) (*RedisClient, error) {
   168  
   169  	if err := config.validate(); err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	if reg != nil {
   174  		reg = prometheus.WrapRegistererWith(prometheus.Labels{"name": name}, reg)
   175  	}
   176  
   177  	var tlsConfig *tls.Config
   178  	if config.TLSEnabled {
   179  		userTLSConfig := config.TLSConfig
   180  
   181  		tlsClientConfig, err := thanos_tls.NewClientConfig(logger, userTLSConfig.CertFile, userTLSConfig.KeyFile,
   182  			userTLSConfig.CAFile, userTLSConfig.ServerName, userTLSConfig.InsecureSkipVerify)
   183  
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  
   188  		tlsConfig = tlsClientConfig
   189  	}
   190  
   191  	clientSideCacheDisabled := false
   192  	if config.CacheSize == 0 {
   193  		clientSideCacheDisabled = true
   194  	}
   195  
   196  	clientOpts := rueidis.ClientOption{
   197  		InitAddress:       strings.Split(config.Addr, ","),
   198  		ShuffleInit:       true,
   199  		Username:          config.Username,
   200  		Password:          config.Password,
   201  		SelectDB:          config.DB,
   202  		CacheSizeEachConn: int(config.CacheSize),
   203  		Dialer:            net.Dialer{Timeout: config.DialTimeout},
   204  		ConnWriteTimeout:  config.WriteTimeout,
   205  		DisableCache:      clientSideCacheDisabled,
   206  		TLSConfig:         tlsConfig,
   207  	}
   208  
   209  	if config.MasterName != "" {
   210  		clientOpts.Sentinel = rueidis.SentinelOption{
   211  			MasterSet: config.MasterName,
   212  		}
   213  	}
   214  
   215  	client, err := rueidis.NewClient(clientOpts)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	c := &RedisClient{
   221  		client: client,
   222  		config: config,
   223  		logger: logger,
   224  		p:      newAsyncOperationProcessor(config.MaxAsyncBufferSize, config.MaxAsyncConcurrency),
   225  		getMultiGate: gate.New(
   226  			extprom.WrapRegistererWithPrefix("thanos_redis_getmulti_", reg),
   227  			config.MaxGetMultiConcurrency,
   228  			gate.Gets,
   229  		),
   230  		setMultiGate: gate.New(
   231  			extprom.WrapRegistererWithPrefix("thanos_redis_setmulti_", reg),
   232  			config.MaxSetMultiConcurrency,
   233  			gate.Sets,
   234  		),
   235  	}
   236  	duration := promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
   237  		Name:    "thanos_redis_operation_duration_seconds",
   238  		Help:    "Duration of operations against redis.",
   239  		Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 3, 6, 10},
   240  	}, []string{"operation"})
   241  	c.durationSet = duration.WithLabelValues(opSet)
   242  	c.durationSetMulti = duration.WithLabelValues(opSetMulti)
   243  	c.durationGetMulti = duration.WithLabelValues(opGetMulti)
   244  
   245  	return c, nil
   246  }
   247  
   248  // SetAsync implement RemoteCacheClient.
   249  func (c *RedisClient) SetAsync(key string, value []byte, ttl time.Duration) error {
   250  	return c.p.enqueueAsync(func() {
   251  		start := time.Now()
   252  		if err := c.client.Do(context.Background(), c.client.B().Set().Key(key).Value(rueidis.BinaryString(value)).ExSeconds(int64(ttl.Seconds())).Build()).Error(); err != nil {
   253  			level.Warn(c.logger).Log("msg", "failed to set item into redis", "err", err, "key", key, "value_size", len(value))
   254  			return
   255  		}
   256  		c.durationSet.Observe(time.Since(start).Seconds())
   257  	})
   258  }
   259  
   260  // SetMulti set multiple keys and value.
   261  func (c *RedisClient) SetMulti(data map[string][]byte, ttl time.Duration) {
   262  	if len(data) == 0 {
   263  		return
   264  	}
   265  	start := time.Now()
   266  	sets := make(rueidis.Commands, 0, len(data))
   267  	ittl := int64(ttl.Seconds())
   268  	for k, v := range data {
   269  		sets = append(sets, c.client.B().Setex().Key(k).Seconds(ittl).Value(rueidis.BinaryString(v)).Build())
   270  	}
   271  	for _, resp := range c.client.DoMulti(context.Background(), sets...) {
   272  		if err := resp.Error(); err != nil {
   273  			level.Warn(c.logger).Log("msg", "failed to set multi items from redis", "err", err, "items", len(data))
   274  			return
   275  		}
   276  	}
   277  	c.durationSetMulti.Observe(time.Since(start).Seconds())
   278  }
   279  
   280  // GetMulti implement RemoteCacheClient.
   281  func (c *RedisClient) GetMulti(ctx context.Context, keys []string) map[string][]byte {
   282  	if len(keys) == 0 {
   283  		return nil
   284  	}
   285  	start := time.Now()
   286  	results := make(map[string][]byte, len(keys))
   287  
   288  	if c.config.ReadTimeout > 0 {
   289  		timeoutCtx, cancel := context.WithTimeout(ctx, c.config.ReadTimeout)
   290  		defer cancel()
   291  		ctx = timeoutCtx
   292  	}
   293  
   294  	// NOTE(GiedriusS): TTL is the default one in case PTTL fails. 8 hours should be good enough IMHO.
   295  	resps, err := rueidis.MGetCache(c.client, ctx, 8*time.Hour, keys)
   296  	if err != nil {
   297  		level.Warn(c.logger).Log("msg", "failed to mget items from redis", "err", err, "items", len(resps))
   298  	}
   299  	for key, resp := range resps {
   300  		if val, err := resp.ToString(); err == nil {
   301  			results[key] = stringToBytes(val)
   302  		}
   303  	}
   304  	c.durationGetMulti.Observe(time.Since(start).Seconds())
   305  	return results
   306  }
   307  
   308  // Stop implement RemoteCacheClient.
   309  func (c *RedisClient) Stop() {
   310  	c.p.Stop()
   311  	c.client.Close()
   312  }
   313  
   314  // stringToBytes converts string to byte slice (copied from vendor/github.com/go-redis/redis/v8/internal/util/unsafe.go).
   315  func stringToBytes(s string) []byte {
   316  	return *(*[]byte)(unsafe.Pointer(
   317  		&struct {
   318  			string
   319  			Cap int
   320  		}{s, len(s)},
   321  	))
   322  }
   323  
   324  // parseRedisClientConfig unmarshals a buffer into a RedisClientConfig with default values.
   325  func parseRedisClientConfig(conf []byte) (RedisClientConfig, error) {
   326  	config := DefaultRedisClientConfig
   327  	if err := yaml.Unmarshal(conf, &config); err != nil {
   328  		return RedisClientConfig{}, err
   329  	}
   330  	return config, nil
   331  }