Упрощенная балансировка нагрузки NGINX с помощью Loadcat
Опубликовано: 2022-03-11Веб-приложения, предназначенные для горизонтального масштабирования, часто требуют наличия одного или нескольких узлов балансировки нагрузки. Их основной целью является справедливое распределение входящего трафика между доступными веб-серверами. Возможность увеличить общую мощность веб-приложения, просто увеличив количество узлов и адаптировав балансировщики нагрузки к этому изменению, может оказаться чрезвычайно полезной в рабочей среде.
NGINX — это веб-сервер, предлагающий высокопроизводительные функции балансировки нагрузки, помимо многих других своих возможностей. Некоторые из этих функций доступны только в рамках их модели подписки, но бесплатная версия с открытым исходным кодом по-прежнему очень многофункциональна и поставляется с наиболее важными функциями балансировки нагрузки из коробки.
В этом руководстве мы изучим внутреннюю механику экспериментального инструмента, который позволяет вам на лету настроить экземпляр NGINX для работы в качестве балансировщика нагрузки, абстрагировавшись от всех мельчайших подробностей файлов конфигурации NGINX, предоставив аккуратный веб-интерфейс. на основе пользовательского интерфейса. Цель этой статьи — показать, как легко начать создавать такой инструмент. Стоит отметить, что проект Loadcat во многом вдохновлен NodeBalancers от Linode.
NGINX, серверы и апстримы
Одним из самых популярных применений NGINX является обратное проксирование запросов от клиентов к приложениям веб-сервера. Хотя веб-приложения, разработанные на таких языках программирования, как Node.js и Go, могут быть самодостаточными веб-серверами, наличие обратного прокси-сервера перед реальным серверным приложением дает множество преимуществ. Блок «сервер» для такого простого варианта использования в файле конфигурации NGINX может выглядеть примерно так:
server { listen 80; server_name example.com; location / { proxy_pass http://192.168.0.51:5000; } }
Это заставило бы NGINX прослушивать порт 80 для всех запросов, которые указывают на example.com, и передавать каждый из них какому-либо приложению веб-сервера, работающему по адресу 192.168.0.51:5000. Мы также могли бы использовать здесь петлевой IP-адрес 127.0.0.1, если сервер веб-приложений работал локально. Обратите внимание, что в приведенном выше фрагменте отсутствуют некоторые очевидные настройки, которые часто используются в конфигурации обратного прокси-сервера, но они сохранены для краткости.
Но что, если мы хотим сбалансировать все входящие запросы между двумя экземплярами одного и того же сервера веб-приложений? Именно здесь становится полезной директива «upstream». В NGINX с помощью директивы upstream можно определить несколько внутренних узлов, между которыми NGINX будет распределять все входящие запросы. Например:
upstream nodes { server 192.168.0.51:5000; server 192.168.0.52:5000; } server { listen 80; server_name example.com; location / { proxy_pass http://nodes; } }
Обратите внимание, как мы определили «восходящий» блок, названный «узлы», состоящий из двух серверов. Каждый сервер идентифицируется IP-адресом и номером порта, который они прослушивают. При этом NGINX становится балансировщиком нагрузки в своей простейшей форме. По умолчанию NGINX будет распределять входящие запросы в циклическом режиме, где первый будет проксироваться на первый сервер, второй — на второй сервер, третий — на первый сервер и так далее.
Однако NGINX может предложить гораздо больше, когда дело доходит до балансировки нагрузки. Это позволяет вам определять веса для каждого сервера, помечать их как временно недоступные, выбирать другой алгоритм балансировки (например, есть такой, который работает на основе хэша IP-адреса клиента) и т. д. Все эти функции и директивы конфигурации хорошо задокументированы на nginx.org. . Кроме того, NGINX позволяет изменять и перезагружать файлы конфигурации «на лету» практически без перерыва.
Настраиваемость NGINX и простые конфигурационные файлы позволяют очень легко адаптировать его ко многим потребностям. В Интернете уже существует множество учебных пособий, в которых рассказывается, как именно настроить NGINX в качестве балансировщика нагрузки.
Loadcat: инструмент настройки NGINX
Есть что-то завораживающее в программах, которые вместо того, чтобы делать что-то самостоятельно, настраивают другие инструменты, чтобы сделать это за них. На самом деле они мало что делают, кроме как, возможно, принимают пользовательские данные и генерируют несколько файлов. Большинство преимуществ, которые вы получаете от этих инструментов, на самом деле являются функциями других инструментов. Но они, безусловно, облегчают жизнь. Пытаясь настроить балансировщик нагрузки для одного из моих собственных проектов, я задался вопросом: почему бы не сделать что-то подобное для NGINX и его возможностей балансировки нагрузки?
Loadcat родился!
Loadcat, созданный с помощью Go, все еще находится в зачаточном состоянии. На данный момент инструмент позволяет настроить NGINX только для балансировки нагрузки и терминации SSL. Он предоставляет пользователю простой веб-интерфейс. Вместо того, чтобы рассматривать отдельные функции инструмента, давайте взглянем на то, что находится под ним. Однако имейте в виду, что если кому-то нравится работать с конфигурационными файлами NGINX вручную, такой инструмент может оказаться малоценным.
Есть несколько причин выбора Go в качестве языка программирования для этого. Одним из них является то, что Go создает скомпилированные двоичные файлы. Это позволяет нам создавать и распространять или развертывать Loadcat в виде скомпилированного двоичного файла на удаленных серверах, не беспокоясь об устранении зависимостей. Что-то, что значительно упрощает процесс настройки. Конечно, бинарник предполагает, что NGINX уже установлен и для него существует юнит-файл systemd.
Если вы не инженер Go, не беспокойтесь. Go довольно прост и интересен для начала. Более того, сама реализация очень проста, и вы сможете легко следовать ей.
Структура
Инструменты сборки Go накладывают несколько ограничений на то, как вы можете структурировать свое приложение, а все остальное оставляете на усмотрение разработчика. В нашем случае мы разбили вещи на несколько пакетов Go в зависимости от их назначения:
- cfg: загружает, анализирует и предоставляет значения конфигурации
- cmd/loadcat: основной пакет, содержит точку входа, компилируется в двоичный файл
- data: содержит «модели», использует встроенное хранилище ключей/значений для постоянства
- feline: содержит основные функции, например, создание файлов конфигурации, механизм перезагрузки и т. д.
- ui: содержит шаблоны, обработчики URL и т. д.
Если мы внимательно посмотрим на структуру пакета, особенно в пакете feline, то заметим, что весь специфичный для NGINX код хранится в подпакете feline/nginx. Это сделано для того, чтобы остальная часть логики приложения оставалась общей и расширялась поддержка других балансировщиков нагрузки (например, HAProxy) в будущем.
Входная точка
Начнем с основного пакета для Loadcat, который находится в «cmd/loadcatd». Основная функция, точка входа в приложение, делает три вещи.
func main() { fconfig := flag.String("config", "loadcat.conf", "") flag.Parse() cfg.LoadFile(*fconfig) feline.SetBase(filepath.Join(cfg.Current.Core.Dir, "out")) data.OpenDB(filepath.Join(cfg.Current.Core.Dir, "loadcat.db")) defer data.DB.Close() data.InitDB() http.Handle("/api", api.Router) http.Handle("/", ui.Router) go http.ListenAndServe(cfg.Current.Core.Address, nil) // Wait for an “interrupt“ signal (Ctrl+C in most terminals) }
Для простоты и облегчения чтения кода весь код обработки ошибок был удален из приведенного выше фрагмента (а также из фрагментов далее в этой статье).
Как видно из кода, мы загружаем файл конфигурации на основе флага командной строки «-config» (по умолчанию «loadcat.conf» в текущем каталоге). Далее мы инициализируем пару компонентов, а именно основной пакет feline и базу данных. Наконец, мы запускаем веб-сервер для графического веб-интерфейса.
Конфигурация
Загрузка и анализ файла конфигурации, пожалуй, самая простая часть. Мы используем TOML для кодирования информации о конфигурации. Для Go доступен удобный пакет синтаксического анализа TOML. Нам нужно очень мало информации о конфигурации от пользователя, и в большинстве случаев мы можем определить разумные значения по умолчанию для этих значений. Следующая структура представляет собой структуру файла конфигурации:
struct { Core struct { Address string Dir string Driver string } Nginx struct { Mode string Systemd struct { Service string } } }
А вот как может выглядеть типичный файл «loadcat.conf»:
[core] address=":26590" dir="/var/lib/loadcat" driver="nginx" [nginx] mode="systemd" [nginx.systemd] service="nginx.service"
Как мы видим, существует сходство между структурой конфигурационного файла в формате TOML и структурой , показанной над ним. Пакет конфигурации начинается с установки некоторых разумных значений по умолчанию для определенных полей структуры , а затем анализирует файл конфигурации поверх него. Если ему не удается найти файл конфигурации по указанному пути, он создает его и сначала выгружает в него значения по умолчанию.
func LoadFile(name string) error { f, _ := os.Open(name) if os.IsNotExist(err) { f, _ = os.Create(name) toml.NewEncoder(f).Encode(Current) f.Close() return nil } toml.NewDecoder(f).Decode(&Current) return nil }
Данные и постоянство
Знакомьтесь, Болт. Встроенное хранилище ключей/значений, написанное на чистом Go. Он поставляется в виде пакета с очень простым API, поддерживает транзакции «из коробки» и работает невероятно быстро.
В данных пакета у нас есть структуры , представляющие каждый тип объекта. Например, у нас есть:
type Balancer struct { Id bson.ObjectId Label string Settings BalancerSettings } type Server struct { Id bson.ObjectId BalancerId bson.ObjectId Label string Settings ServerSettings }
… где экземпляр Balancer представляет один балансировщик нагрузки. Loadcat эффективно позволяет балансировать запросы для нескольких веб-приложений через один экземпляр NGINX. Каждый балансировщик может иметь за собой один или несколько серверов, где каждый сервер может быть отдельным внутренним узлом.
Поскольку Bolt является хранилищем ключей и значений и не поддерживает расширенные запросы к базе данных, у нас есть логика на стороне приложения, которая делает это за нас. Loadcat не предназначен для настройки тысяч балансировщиков с тысячами серверов в каждом из них, поэтому, естественно, этот наивный подход прекрасно работает. Кроме того, Bolt работает с ключами и значениями, которые представляют собой фрагменты байтов, поэтому мы кодируем структуры BSON перед их сохранением в Bolt. Реализация функции, которая извлекает список структур Balancer из базы данных, показана ниже:
func ListBalancers() ([]Balancer, error) { bals := []Balancer{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { bal := Balancer{} bson.Unmarshal(v, &bal) bals = append(bals, bal) } return nil }) return bals, nil }
Функция ListBalancers запускает транзакцию только для чтения, перебирает все ключи и значения в корзине «балансировщиков», декодирует каждое значение в экземпляр структуры Balancer и возвращает их в виде массива.

Хранить балансир в ведре почти так же просто:
func (l *Balancer) Put() error { if !l.Id.Valid() { l.Id = bson.NewObjectId() } if l.Label == "" { l.Label = "Unlabelled" } if l.Settings.Protocol == "https" { // Parse certificate details } else { // Clear fields relevant to HTTPS only, such as SSL options and certificate details } return DB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) p, err := bson.Marshal(l) if err != nil { return err } return b.Put([]byte(l.Id.Hex()), p) }) }
Функция Put присваивает некоторые значения по умолчанию определенным полям, анализирует прикрепленный SSL-сертификат в настройках HTTPS, начинает транзакцию, кодирует экземпляр структуры и сохраняет его в корзине с идентификатором балансировщика.
При анализе SSL-сертификата две части информации извлекаются с использованием стандартного пакетного кодирования / pem и сохраняются в SSLOptions в поле « Настройки »: DNS-имена и отпечаток пальца.
У нас также есть функция, которая ищет серверы по балансировщику:
func ListServersByBalancer(bal *Balancer) ([]Server, error) { srvs := []Server{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("servers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { srv := Server{} bson.Unmarshal(v, &srv) if srv.BalancerId.Hex() != bal.Id.Hex() { continue } srvs = append(srvs, srv) } return nil }) return srvs, nil }
Эта функция показывает, насколько наивен наш подход. Здесь мы эффективно читаем всю корзину «серверов» и отфильтровываем ненужные объекты перед возвратом массива. Но опять же, это работает просто отлично, и нет никакой реальной причины что-то менять.
Функция Put для серверов намного проще, чем функция структуры Balancer , поскольку она не требует большого количества строк кода, устанавливающих значения по умолчанию и вычисляемые поля.
Управление NGINX
Прежде чем использовать Loadcat, мы должны настроить NGINX для загрузки сгенерированных файлов конфигурации. Loadcat создает файл «nginx.conf» для каждого балансировщика в каталоге по идентификатору балансировщика (короткая шестнадцатеричная строка). Эти каталоги создаются в каталоге «out» по адресу cwd
. Поэтому важно настроить NGINX для загрузки этих сгенерированных файлов конфигурации. Это можно сделать с помощью директивы include внутри блока http:
Отредактируйте /etc/nginx/nginx.conf и добавьте следующую строку в конце блока «http»:
http { include /path/to/out/*/nginx.conf; }
Это заставит NGINX сканировать все каталоги, найденные в «/path/to/out/», искать файлы с именем «nginx.conf» в каждом каталоге и загружать каждый найденный.
В нашем базовом пакете feline мы определяем драйвер интерфейса. Любая структура , предоставляющая две функции Generate и Reload с правильной подписью, считается драйвером.
type Driver interface { Generate(string, *data.Balancer) error Reload() error }
Например, структура Nginx в пакетах feline/nginx:
type Nginx struct { sync.Mutex Systemd *dbus.Conn } func (n Nginx) Generate(dir string, bal *data.Balancer) error { // Acquire a lock on n.Mutex, and release before return f, _ := os.Create(filepath.Join(dir, "nginx.conf")) TplNginxConf.Execute(f, /* template parameters */) f.Close() if bal.Settings.Protocol == "https" { // Dump private key and certificate to the output directory (so that Nginx can find them) } return nil } func (n Nginx) Reload() error { // Acquire a lock on n.Mutex, and release before return switch cfg.Current.Nginx.Mode { case "systemd": if n.Systemd == nil { c, err := dbus.NewSystemdConnection() n.Systemd = c } ch := make(chan string) n.Systemd.ReloadUnit(cfg.Current.Nginx.Systemd.Service, "replace", ch) <-ch return nil default: return errors.New("unknown Nginx mode") } }
Generate можно вызвать со строкой, содержащей путь к выходному каталогу и указатель на экземпляр структуры Balancer . Go предоставляет стандартный пакет для создания текстовых шаблонов, который драйвер NGINX использует для создания окончательного файла конфигурации NGINX. Шаблон состоит из «восходящего» блока, за которым следует «серверный» блок, сгенерированный в зависимости от того, как настроен балансировщик:
var TplNginxConf = template.Must(template.New("").Parse(` upstream {{.Balancer.Id.Hex}} { {{if eq .Balancer.Settings.Algorithm "least-connections"}} least_conn; {{else if eq .Balancer.Settings.Algorithm "source-ip"}} ip_hash; {{end}} {{range $srv := .Balancer.Servers}} server {{$srv.Settings.Address}} weight={{$srv.Settings.Weight}} {{if eq $srv.Settings.Availability "available"}}{{else if eq $srv.Settings.Availability "backup"}}backup{{else if eq $srv.Settings.Availability "unavailable"}}down{{end}}; {{end}} } server { {{if eq .Balancer.Settings.Protocol "http"}} listen {{.Balancer.Settings.Port}}; {{else if eq .Balancer.Settings.Protocol "https"}} listen {{.Balancer.Settings.Port}} ssl; {{end}} server_name {{.Balancer.Settings.Hostname}}; {{if eq .Balancer.Settings.Protocol "https"}} ssl on; ssl_certificate {{.Dir}}/server.crt; ssl_certificate_key {{.Dir}}/server.key; {{end}} location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://{{.Balancer.Id.Hex}}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; } } `))
Reload — это еще одна функция в структуре Nginx , которая заставляет NGINX перезагружать файлы конфигурации. Используемый механизм основан на настройке Loadcat. По умолчанию предполагается, что NGINX — это служба systemd, работающая как nginx.service, так что [sudo] systemd reload nginx.service
будет работать. Однако вместо выполнения команды оболочки он устанавливает соединение с systemd через D-Bus, используя пакет github.com/coreos/go-systemd/dbus.
Веб-интерфейс
Со всеми этими компонентами мы добавим простой пользовательский интерфейс Bootstrap.
Для этих основных функций достаточно нескольких простых обработчиков маршрутов GET и POST:
GET /balancers GET /balancers/new POST /balancers/new GET /balancers/{id} GET /balancers/{id}/edit POST /balancers/{id}/edit GET /balancers/{id}/servers/new POST /balancers/{id}/servers/new GET /servers/{id} GET /servers/{id}/edit POST /servers/{id}/edit
Переход по каждому отдельному маршруту может быть не самым интересным, так как это в значительной степени страницы CRUD. Не стесняйтесь заглянуть в код пользовательского интерфейса пакета, чтобы увидеть, как реализованы обработчики для каждого из этих маршрутов.
Каждая функция-обработчик представляет собой процедуру, которая либо:
- Извлекает данные из хранилища данных и отвечает визуализированными шаблонами (используя полученные данные).
- Анализирует входящие данные формы, вносит необходимые изменения в хранилище данных и использует пакет feline для повторной генерации файлов конфигурации NGINX.
Например:
func ServeServerNewForm(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) TplServerNewForm.Execute(w, struct { Balancer *data.Balancer }{ Balancer: bal, }) } func HandleServerCreate(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) r.ParseForm() body := struct { Label string `schema:"label"` Settings struct { Address string `schema:"address"` } `schema:"settings"` }{} schema.NewDecoder().Decode(&body, r.PostForm) srv := data.Server{} srv.BalancerId = bal.Id srv.Label = body.Label srv.Settings.Address = body.Settings.Address srv.Put() feline.Commit(bal) http.Redirect(w, r, "/servers/"+srv.Id.Hex()+"/edit", http.StatusSeeOther) }
Все, что делает функция ServeServerNewForm , — это извлекает балансировщик из хранилища данных и отображает шаблон, в данном случае TplServerList , который извлекает список соответствующих серверов с помощью функции Servers на балансировщике.
Функция HandleServerCreate , с другой стороны, анализирует входящую полезную нагрузку POST из тела в структуру и использует эти данные для создания и сохранения новой структуры сервера в хранилище данных перед использованием пакета feline для повторного создания файла конфигурации NGINX для балансировщика.
Все шаблоны страниц хранятся в файле «ui/templates.go», а соответствующие HTML-файлы шаблонов можно найти в каталоге «ui/templates».
Пробуем
Развернуть Loadcat на удаленном сервере или даже в вашей локальной среде очень просто. Если вы используете Linux (64-разрядную версию), вы можете получить архив с готовым бинарным файлом Loadcat из раздела «Релизы» репозитория. Если вы чувствуете себя немного предприимчивым, вы можете клонировать репозиторий и самостоятельно скомпилировать код. Хотя опыт в этом случае может быть немного разочаровывающим , поскольку компиляция программ Go на самом деле не является проблемой. И если вы используете Arch Linux, то вам повезло! Для удобства был собран пакет для дистрибутива. Просто скачайте его и установите с помощью менеджера пакетов. Необходимые шаги более подробно описаны в файле проекта README.md.
После того, как вы настроили и запустили Loadcat, укажите в веб-браузере «http://localhost:26590» (при условии, что он работает локально и прослушивает порт 26590). Затем создайте балансировщик, создайте пару серверов, убедитесь, что что-то прослушивает эти определенные порты, и вуаля, у вас должен быть баланс нагрузки входящих запросов NGINX между этими работающими серверами.
Что дальше?
Этот инструмент далек от совершенства, и на самом деле это довольно экспериментальный проект. Инструмент даже не охватывает все основные функции NGINX. Например, если вы хотите кэшировать ресурсы, обслуживаемые внутренними узлами на уровне NGINX, вам все равно придется вручную изменять файлы конфигурации NGINX. И это то, что делает вещи захватывающими. Здесь многое можно сделать, и это именно то, что будет дальше: охватить еще больше функций балансировки нагрузки NGINX — основных и, возможно, даже тех, которые может предложить NGINX Plus.
Попробуйте Loadcat. Проверьте код, разветвите его, измените его, поиграйте с ним. Кроме того, сообщите нам, если вы создали инструмент, который настраивает другое программное обеспечение, или использовали тот, который вам действительно нравится, в разделе комментариев ниже.