Как стать автором
Обновить

Комментарии 32

Почему если GO, то сразу высокая производительность? В статье нет никакого сравнения с другими решениями

Может производительность не кода, а привыкшего к языку программиста? Тогда все сошлось бы...

У вас Go из какого века? :) В атомике нет булов, можно только эмулировать их через инты.

Вы правы, Aerospike Go client internal/atomic/bool.go
import "sync/atomic"

//AtomicBool implements a synchronized boolean value
type AtomicBool struct {
	val int32
}

// NewAtomicBool generates a new AtomicBoolean instance.
func NewAtomicBool(value bool) *AtomicBool {
	var i int32
	if value {
		i = 1
	}
	return &AtomicBool{
		val: i,
	}
}

// Get atomically retrieves the boolean value.
func (ab *AtomicBool) Get() bool {
	return atomic.LoadInt32(&(ab.val)) != 0
}

// Set atomically sets the boolean value.
func (ab *AtomicBool) Set(newVal bool) {
	var i int32
	if newVal {
		i = 1
	}
	atomic.StoreInt32(&(ab.val), int32(i))
}

traefik (написанный на go) уступает nginx на 10%
использую на проде везде. он из коробки читает конфиги из docker labels или consul. Без reload и тп, крайне удобно (если сравнивать с конфигогенерацией для nginx и reload, который в части кейсов не заходит)

А есть пруф про 10%? Интересно почитать.
Половина функционала в балансировщике nginx доступна только в plus, так что: почему бы и нет?
Как-то безумно, по-моему, защищать булевскую переменную мьютексом при том, что в Go есть atomic exchange, работающий с целыми числами.

В общем и целом, я бы считал для каждого бакенда количество запросов, уже туда отправленных, но с ответами, назад не полученными. И отправлял бы следующий запрос тому бакенду, на котором в данный момент висит меньше необслуженных запросов. А round robin крутил бы лишь при одинаковом их количестве.

Я бы вообще не пытался round robin с общей переменной использовать. Имхо, в данном случае порядок не принципиален, можно ничего не синхронизовывать и в каждом потоке держать свой маленький round robin (с разным порядком серверов, если хочется избежать "плохих" случаев).

Порядок не принципиален, но лучше бы более-менее поровну дергать все бакенды, а не какой-нибудь «любимый». А то забытый нами бакенд тихо помрет, а мы об этом и не узнаем. А у «любимого» на SSD-ке дырка протрется от натуги.

Для всех любителей использовать атомик в любых ситуациях в документации есть предупреждение:


These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory.

Понятно что это перестраховка, но часто смысла трогать атомик нет — скорости взятия мутекса в ~15нс достаточно бывает.

Дело не в наносекундах. Ненужный мьютекс глаза мозолит и усложняет чтение кода.

Я бы поспорил. Как по мне атомик с указателями выглядит хуже. А с мутексом четко видно критическую секцию которая заблокирована.


Сам я использую атомики только для счетчиков статистики если нет необходимости оптимизировать до наносекунд.

НЛО прилетело и опубликовало эту надпись здесь
Операция присваивания не атомарная. И что хуже, глядя с одного ядра CPU, можно подумать, что присваивание уже произошло, а глядя с другого — нет.

Что худшего может произойти в данном конкретном случае, я не знаю, но вообще, если считать присваивание атомарным в многопоточной программе, произойти может очень трудноуловимый баг.
НЛО прилетело и опубликовало эту надпись здесь
Процессор работает с памятью не байтами, а кэш-лайнами. Размер кэш-лайна, в зависимости от модели процессора, может быть и 16 байт, а может и 64. И чтобы изменить байт, bool или int64, процессор считывает целый кэш-лайн, меняет в нем данные и записывает назад. Причем не сразу, а когда-нибудь. Поэтому обычная операция присваивание не атомарна.

Атомные операции содержат дополнительные инструкции процессора (инструкции в широком смысле, у интела это может быть префикс к команде), и процессор делает вид, что конкретно эта операция произошла атомарно. Причем на многоядерной/многопроцессорной машине это «деланье вида» включает в себя нетривиальное взаимодействие с другими ядрами/процессорами на предмет синхронизации кэшей.

Поскольку вся эта дополнительная деятельность чего-то стоит, обычное присваивание не атомарно.
Можно ввести метрику которая учитывает не только «есть ответ» — «нет ответа», но и время ответа. Не все же запросы к бекенду одинаковые. Какие то запросы тяжелые, какие то более легкие. И раз мы ставим на входе LB, значит у нас есть нагрузка и много запросов. На больших данных, такая метрика покажет более объективную картинку по каждому серверу и даже поможет более равномерно распределить % тяжелых запросов по серверам.
Балансировщику трудно заранее определить, какие запросы тяжелые, а какие — нет. Но по длинне очереди необслуженных запросов он может оценить загруженность бакенда.
Тут идея в том, чтобы был приоритет у бэкенд сервера основанный на статистике его скорости обработки запросов. Тот, который отвечает быстрее остальных (допустим в интервале «последние 30 минут»), тот и имеет бОльшую вероятность получить очередной запрос в работу.
UPD: причем можно оперировать разными метриками, от «время получения первого байта» до «полной отдачи всего ответа». Надо смотреть какой размер пейлоада генерится бекендом. Там может быть 2 байта, а могут быть динамические хтмл страницы по 300кб.
Дополнительная сложность очевидна, а в чем добавленная стоимость — не очевидно
профит:

1. Если эта метрика у конкретного сервера растет, он получает меньше запросов. Если сам сервер в порядке и ему просто не повезло с % тяжелых запросов, он рано или поздно вздохнет свободней, его метрика придет в порядок.

2. Пул бекендов может состоять из совершенно разных по производительности серверов. Данная метрика сделает за нас распределение потока запросов между ними в той же пропорции, как и их производительность. Разве нет?

3. Достаточно легко делается мониторинг как балансера, так и серверов. Для первоначальной оценки нагрузки/производительности (за интервал час два три… и так далее) мы получаем более чем достаточно данных.
Мне кажется в функции GetNextPeer вместо nil лучше возвращать error.
// GetNextPeer returns next active peer to take a connection
func (s *ServerPool) GetNextPeer() *Backend {
  // loop entire backends to find out an Alive backend
  next := s.NextIndex()
  l := len(s.backends) + next // start from next and move a full cycle
  for i := next; i < l; i++ {
    idx := i % len(s.backends) // take an index by modding with length
    // if we have an alive backend, use it and store if its not the original one
    if s.backends[idx].IsAlive() {
      if i != next {
        atomic.StoreUint64(&s.current, uint64(idx)) // mark the current one
      }
      return s.backends[idx]
    }
  }
  return nil
}


Тогда в функции lb вместо проверки на nil будет идиоматическая для Go проверка на error:

// lb load balances the incoming request
func lb(w http.ResponseWriter, r *http.Request) {
  peer := serverPool.GetNextPeer()
  if peer != nil {
    peer.ReverseProxy.ServeHTTP(w, r)
    return
  }
  http.Error(w, "Service not available", http.StatusServiceUnavailable)
}
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
а fasthttp не пробовали прикрутить?
Подскажите, есть ли смысл в использовании такой констркукции с ticker и select в healthCheck? Если написать проще, будет ли хуже с точки зрения шедулера?
func healthCheck() {
	for {
		time.Sleep(time.Second * 20)
		log.Println("Starting health check...")
		serverPool.HealthCheck()
		log.Println("Health check completed")
	}
}
тут получается, что вы запускаете проверку, через 20 секунд после предыдущей
c Ticker — раз в 20 секунд
если у вас опрос серверов идет 5 минут (ну мало ли и у вас их тысячи), то в вашем случае, если первый сервер умер сразу после ответа «OK» чекеру, то балансер об этом узнает только через 5 минут + 20 секунд (время когда for сработает в след раз)
select, кстати, в приведенном коде нигде не нужен. Во всех случаях ожидаем на единственном канале и без default.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий