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 }