github.com/mailru/activerecord@v1.12.2/pkg/activerecord/cluster.go (about) 1 package activerecord 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "hash" 8 "hash/crc32" 9 "math/rand" 10 "strconv" 11 "strings" 12 "sync" 13 "sync/atomic" 14 "time" 15 ) 16 17 // Интерфейс которому должен соответствовать билдер опций подключения к конретному инстансу 18 type OptionInterface interface { 19 GetConnectionID() string 20 InstanceMode() ServerModeType 21 } 22 23 // Тип и константы для выбора инстанса в шарде 24 type ShardInstanceType uint8 25 26 const ( 27 MasterInstanceType ShardInstanceType = iota // Любой из описанных мастеров. По умолчанию используется для rw запросов 28 ReplicaInstanceType // Любой из описанных реплик 29 ReplicaOrMasterInstanceType // Любая реплика если есть, если нет то любой мастер. По умолчанию используется при селекте 30 ) 31 32 // Тип и константы для определения режима работы конкретного инстанса. 33 type ServerModeType uint8 34 35 const ( 36 ModeMaster ServerModeType = iota 37 ModeReplica 38 ) 39 40 // Структура используется для описания конфигурации конктретного инстанса 41 type ShardInstanceConfig struct { 42 Timeout time.Duration 43 Mode ServerModeType 44 PoolSize int 45 Addr string 46 User string 47 Password string 48 } 49 50 // Структура описывающая инстанс в кластере 51 type ShardInstance struct { 52 ParamsID string 53 Config ShardInstanceConfig 54 Options interface{} 55 Offline bool 56 } 57 58 // Структура описывающая конкретный шард. Каждый шард может состоять из набора мастеров и реплик 59 type Shard struct { 60 Masters []ShardInstance 61 Replicas []ShardInstance 62 curMaster int32 63 curReplica int32 64 } 65 66 // Функция выбирающая следующий доступный инстанс мастера в конкретном шарде 67 func (s *Shard) NextMaster() ShardInstance { 68 length := len(s.Masters) 69 switch length { 70 case 0: 71 panic("no master configured") 72 case 1: 73 master := s.Masters[0] 74 if master.Offline { 75 panic("no available master") 76 } 77 78 return master 79 } 80 81 // Из-за гонок при большом кол-ве недоступных инстансов может потребоватся много попыток найти доступный узел 82 attempt := 10 * length 83 84 for i := 0; i < attempt; i++ { 85 newVal := atomic.AddInt32(&s.curMaster, 1) 86 newValMod := newVal % int32(len(s.Masters)) 87 88 if newValMod != newVal { 89 atomic.CompareAndSwapInt32(&s.curMaster, newVal, newValMod) 90 } 91 92 master := s.Masters[newValMod] 93 if master.Offline { 94 continue 95 } 96 97 return master 98 } 99 100 //nolint:gosec 101 // Есть небольшая вероятность при большой нагрузке и большом проценте недоступных инстансов можно залипнуть на доступном узле 102 // Чтобы не паниковать выбираем рандомный узел 103 return s.Masters[rand.Int()%length] 104 } 105 106 // Инстанс выбирающий следующий доступный инстанс реплики в конкретном шарде 107 func (s *Shard) NextReplica() ShardInstance { 108 length := len(s.Replicas) 109 if length == 1 && !s.Replicas[0].Offline { 110 return s.Replicas[0] 111 } 112 113 // Из-за гонок при большом кол-ве недоступных инстансов может потребоватся много попыток найти доступный узел 114 attempt := 10 * length 115 116 for i := 0; i < attempt; i++ { 117 newVal := atomic.AddInt32(&s.curReplica, 1) 118 newValMod := newVal % int32(len(s.Replicas)) 119 120 if newValMod != newVal { 121 atomic.CompareAndSwapInt32(&s.curReplica, newVal, newValMod) 122 } 123 124 replica := s.Replicas[newValMod] 125 if replica.Offline { 126 continue 127 } 128 129 return replica 130 } 131 132 //nolint:gosec 133 // Есть небольшая вероятность при большой нагрузке и большом проценте недоступных инстансов поиск может залипнуть на недоступном узле 134 // Чтобы не паниковать выбираем рандомный узел 135 return s.Replicas[rand.Int()%length] 136 } 137 138 // Instances копия списка конфигураций всех инстансов шарды. В начале списка следуют мастера, потом реплики 139 func (c *Shard) Instances() []ShardInstance { 140 instances := make([]ShardInstance, 0, len(c.Masters)+len(c.Replicas)) 141 instances = append(instances, c.Masters...) 142 instances = append(instances, c.Replicas...) 143 144 return instances 145 } 146 147 // Тип описывающий кластер. Сейчас кластер - это набор шардов. 148 type Cluster struct { 149 m sync.RWMutex 150 shards []Shard 151 hash hash.Hash 152 } 153 154 func NewCluster(shardCnt int) *Cluster { 155 return &Cluster{ 156 m: sync.RWMutex{}, 157 shards: make([]Shard, 0, shardCnt), 158 hash: crc32.NewIEEE(), 159 } 160 } 161 162 // NextMaster выбирает следующий доступный инстанс мастера в шарде shardNum 163 func (c *Cluster) NextMaster(shardNum int) ShardInstance { 164 c.m.RLock() 165 defer c.m.RUnlock() 166 167 return c.shards[shardNum].NextMaster() 168 } 169 170 // NextMaster выбирает следующий доступный инстанс реплики в шарде shardNum 171 func (c *Cluster) NextReplica(shardNum int) (ShardInstance, bool) { 172 c.m.RLock() 173 defer c.m.RUnlock() 174 175 for _, replica := range c.shards[shardNum].Replicas { 176 if replica.Offline { 177 continue 178 } 179 180 return c.shards[shardNum].NextReplica(), true 181 } 182 183 return ShardInstance{}, false 184 185 } 186 187 // Append добавляет новый шард в кластер 188 func (c *Cluster) Append(shard Shard) { 189 c.m.Lock() 190 defer c.m.Unlock() 191 192 c.shards = append(c.shards, shard) 193 194 c.hash.Reset() 195 for i := 0; i < len(c.shards); i++ { 196 for _, instance := range c.shards[i].Instances() { 197 c.hash.Write([]byte(instance.ParamsID)) 198 } 199 } 200 } 201 202 // ShardInstances копия всех инстансов из шарды shardNum 203 func (c *Cluster) ShardInstances(shardNum int) []ShardInstance { 204 c.m.Lock() 205 defer c.m.Unlock() 206 207 return c.shards[shardNum].Instances() 208 } 209 210 // Shards кол-во доступных шард кластера 211 func (c *Cluster) Shards() int { 212 return len(c.shards) 213 } 214 215 // SetShardInstances заменяет инстансы кластера в шарде shardNum на инстансы из instances 216 func (c *Cluster) SetShardInstances(shardNum int, instances []ShardInstance) { 217 c.m.Lock() 218 defer c.m.Unlock() 219 220 shard := c.shards[shardNum] 221 shard.Masters = shard.Masters[:0] 222 shard.Replicas = shard.Replicas[:0] 223 for _, shardInstance := range instances { 224 switch shardInstance.Config.Mode { 225 case ModeMaster: 226 shard.Masters = append(shard.Masters, shardInstance) 227 case ModeReplica: 228 shard.Replicas = append(shard.Replicas, shardInstance) 229 } 230 } 231 232 c.shards[shardNum] = shard 233 } 234 235 // Equal сравнивает загруженные конфигурации кластеров на основе контрольной суммы всех инстансов кластера 236 func (c *Cluster) Equal(c2 *Cluster) bool { 237 if c == nil { 238 return false 239 } 240 241 if c2 == nil { 242 return false 243 } 244 245 return bytes.Equal(c.hash.Sum(nil), c2.hash.Sum(nil)) 246 } 247 248 // Тип используемый для передачи набора значений по умолчанию для параметров 249 type MapGlobParam struct { 250 Timeout time.Duration 251 PoolSize int 252 } 253 254 // Конструктор который позволяет проинициализировать новый кластер. В опциях передаются все шарды, 255 // сколько шардов, столько и опций. Используется в случаях, когда информация по кластеру прописана 256 // непосредственно в декларации модели, а не в конфиге. 257 // Так же используется при тестировании. 258 func NewClusterInfo(opts ...clusterOption) *Cluster { 259 cl := NewCluster(0) 260 261 for _, opt := range opts { 262 opt.apply(cl) 263 } 264 265 return cl 266 } 267 268 // Констркуктор позволяющий проинициализировать кластер их конфигурации. 269 // На вход передаётся путь в конфиге, значения по умолчанию, и ссылка на функцию, которая 270 // создаёт структуру опций и считает контрольную сумму, для того, что бы следить за их изменением в онлайне. 271 func GetClusterInfoFromCfg(ctx context.Context, path string, globs MapGlobParam, optionCreator func(ShardInstanceConfig) (OptionInterface, error)) (*Cluster, error) { 272 cfg := Config() 273 274 shardCnt, exMaxShardOK := cfg.GetIntIfExists(ctx, path+"/max-shard") 275 if !exMaxShardOK { 276 shardCnt = 1 277 } 278 279 cluster := NewCluster(shardCnt) 280 281 globalTimeout, exGlobalTimeout := cfg.GetDurationIfExists(ctx, path+"/Timeout") 282 if exGlobalTimeout { 283 globs.Timeout = globalTimeout 284 } 285 286 globalPoolSize, exGlobalPoolSize := cfg.GetIntIfExists(ctx, path+"/PoolSize") 287 if !exGlobalPoolSize { 288 globalPoolSize = 1 289 } 290 291 globs.PoolSize = globalPoolSize 292 293 if exMaxShardOK { 294 // Если используется много шардов 295 for f := 0; f < shardCnt; f++ { 296 shard, err := getShardInfoFromCfg(ctx, path+"/"+strconv.Itoa(f), globs, optionCreator) 297 if err != nil { 298 return nil, fmt.Errorf("can't get shard %d info: %w", f, err) 299 } 300 301 cluster.Append(shard) 302 } 303 } else { 304 // Когда только один шард 305 shard, err := getShardInfoFromCfg(ctx, path, globs, optionCreator) 306 if err != nil { 307 return nil, fmt.Errorf("can't get shard info: %w", err) 308 } 309 310 cluster.Append(shard) 311 } 312 313 return cluster, nil 314 } 315 316 // Чтение информации по конкретному шарду из конфига 317 func getShardInfoFromCfg(ctx context.Context, path string, globParam MapGlobParam, optionCreator func(ShardInstanceConfig) (OptionInterface, error)) (Shard, error) { 318 cfg := Config() 319 ret := Shard{ 320 Masters: []ShardInstance{}, 321 Replicas: []ShardInstance{}, 322 } 323 324 shardTimeout := cfg.GetDuration(ctx, path+"/Timeout", globParam.Timeout) 325 shardPoolSize := cfg.GetInt(ctx, path+"/PoolSize", globParam.PoolSize) 326 327 user, _ := cfg.GetStringIfExists(ctx, path+"/user") 328 password, _ := cfg.GetStringIfExists(ctx, path+"/password") 329 330 // информация по мастерам 331 master, exMaster := cfg.GetStringIfExists(ctx, path+"/master") 332 if !exMaster { 333 master, exMaster = cfg.GetStringIfExists(ctx, path) 334 if !exMaster { 335 return Shard{}, fmt.Errorf("master should be specified in '%s' or in '%s/master' and replica in '%s/replica'", path, path, path) 336 } 337 } 338 339 if master != "" { 340 for _, inst := range strings.Split(master, ",") { 341 if inst == "" { 342 return Shard{}, fmt.Errorf("invalid master instance options: addr is empty") 343 } 344 345 shardCfg := ShardInstanceConfig{ 346 Addr: inst, 347 Mode: ModeMaster, 348 PoolSize: shardPoolSize, 349 Timeout: shardTimeout, 350 User: user, 351 Password: password, 352 } 353 354 opt, err := optionCreator(shardCfg) 355 if err != nil { 356 return Shard{}, fmt.Errorf("can't create instanceOption: %w", err) 357 } 358 359 ret.Masters = append(ret.Masters, ShardInstance{ 360 ParamsID: opt.GetConnectionID(), 361 Config: shardCfg, 362 Options: opt, 363 }) 364 } 365 } 366 367 // Информация по репликам 368 replica, exReplica := cfg.GetStringIfExists(ctx, path+"/replica") 369 if exReplica { 370 for _, inst := range strings.Split(replica, ",") { 371 if inst == "" { 372 return Shard{}, fmt.Errorf("invalid slave instance options: addr is empty") 373 } 374 375 shardCfg := ShardInstanceConfig{ 376 Addr: inst, 377 Mode: ModeReplica, 378 PoolSize: shardPoolSize, 379 Timeout: shardTimeout, 380 User: user, 381 Password: password, 382 } 383 384 opt, err := optionCreator(shardCfg) 385 if err != nil { 386 return Shard{}, fmt.Errorf("can't create instanceOption: %w", err) 387 } 388 389 ret.Replicas = append(ret.Replicas, ShardInstance{ 390 ParamsID: opt.GetConnectionID(), 391 Config: shardCfg, 392 Options: opt, 393 }) 394 } 395 } 396 397 return ret, nil 398 } 399 400 // Структура для кеширования полученных конфигураций. Инвалидация происходит посредством 401 // сравнения updateTime сданной труктуры и самого конфига. 402 // Используется для шаринга конфигов между можелями если они используют одну и ту же 403 // конфигурацию для подключений 404 type DefaultConfigCacher struct { 405 lock sync.RWMutex 406 container map[string]*Cluster 407 updateTime time.Time 408 } 409 410 // Конструктор для создания нового кешера конфигов 411 func NewConfigCacher() *DefaultConfigCacher { 412 return &DefaultConfigCacher{ 413 lock: sync.RWMutex{}, 414 container: make(map[string]*Cluster), 415 updateTime: time.Now(), 416 } 417 } 418 419 // Получение конфигурации. Если есть в кеше и он еще валидный, то конфигурация берётся из кешаб 420 // если в кеше нет, то достаём из конфига и кешируем. 421 func (cc *DefaultConfigCacher) Get(ctx context.Context, path string, globs MapGlobParam, optionCreator func(ShardInstanceConfig) (OptionInterface, error)) (*Cluster, error) { 422 cc.lock.RLock() 423 conf, ex := cc.container[path] 424 confUpdateTime := cc.updateTime 425 cc.lock.RUnlock() 426 427 // Если конфигурация не найдена в кеше или конфигурация была обновлена, то перегружаем конфигурацию 428 if !ex || confUpdateTime.Sub(Config().GetLastUpdateTime()) < 0 { 429 cc.lock.Lock() 430 newConf, err := GetClusterInfoFromCfg(ctx, path, globs, optionCreator) 431 if err != nil { 432 cc.lock.Unlock() 433 434 return nil, fmt.Errorf("can't get config: %w", err) 435 } 436 437 // если конфигурация поменялась, то обновляем её в кеше 438 if !newConf.Equal(conf) { 439 conf = newConf 440 cc.container[path] = conf 441 cc.updateTime = time.Now() 442 } 443 444 cc.lock.Unlock() 445 } 446 447 return conf, nil 448 }