Комментарии 32
У вас 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))
}
UPD: увидел, что ради веселья. А есть тесты производительности?
traefik (написанный на go) уступает nginx на 10%
использую на проде везде. он из коробки читает конфиги из docker labels или consul. Без reload и тп, крайне удобно (если сравнивать с конфигогенерацией для nginx и reload, который в части кейсов не заходит)
В общем и целом, я бы считал для каждого бакенда количество запросов, уже туда отправленных, но с ответами, назад не полученными. И отправлял бы следующий запрос тому бакенду, на котором в данный момент висит меньше необслуженных запросов. А round robin крутил бы лишь при одинаковом их количестве.
Я бы вообще не пытался round robin с общей переменной использовать. Имхо, в данном случае порядок не принципиален, можно ничего не синхронизовывать и в каждом потоке держать свой маленький round robin (с разным порядком серверов, если хочется избежать "плохих" случаев).
Для всех любителей использовать атомик в любых ситуациях в документации есть предупреждение:
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нс достаточно бывает.
Что худшего может произойти в данном конкретном случае, я не знаю, но вообще, если считать присваивание атомарным в многопоточной программе, произойти может очень трудноуловимый баг.
Атомные операции содержат дополнительные инструкции процессора (инструкции в широком смысле, у интела это может быть префикс к команде), и процессор делает вид, что конкретно эта операция произошла атомарно. Причем на многоядерной/многопроцессорной машине это «деланье вида» включает в себя нетривиальное взаимодействие с другими ядрами/процессорами на предмет синхронизации кэшей.
Поскольку вся эта дополнительная деятельность чего-то стоит, обычное присваивание не атомарно.
UPD: причем можно оперировать разными метриками, от «время получения первого байта» до «полной отдачи всего ответа». Надо смотреть какой размер пейлоада генерится бекендом. Там может быть 2 байта, а могут быть динамические хтмл страницы по 300кб.
1. Если эта метрика у конкретного сервера растет, он получает меньше запросов. Если сам сервер в порядке и ему просто не повезло с % тяжелых запросов, он рано или поздно вздохнет свободней, его метрика придет в порядок.
2. Пул бекендов может состоять из совершенно разных по производительности серверов. Данная метрика сделает за нас распределение потока запросов между ними в той же пропорции, как и их производительность. Разве нет?
3. Достаточно легко делается мониторинг как балансера, так и серверов. Для первоначальной оценки нагрузки/производительности (за интервал час два три… и так далее) мы получаем более чем достаточно данных.
// 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)
}
func healthCheck() {
for {
time.Sleep(time.Second * 20)
log.Println("Starting health check...")
serverPool.HealthCheck()
log.Println("Health check completed")
}
}
c Ticker — раз в 20 секунд
если у вас опрос серверов идет 5 минут (ну мало ли и у вас их тысячи), то в вашем случае, если первый сервер умер сразу после ответа «OK» чекеру, то балансер об этом узнает только через 5 минут + 20 секунд (время когда for сработает в след раз)
Пишем на Go простой балансировщик