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  }