воскресенье, 16 июня 2019 г.

Контроль количества процессов приложений uWSGI через Zabbix

Задумался как-то о том, чтобы подобрать подходящее количество рабочих процессов для каждого из приложений, работающих под управлением uwsgi. Если рабочих процессов не хватает, то приложение работает не в полную силу и часть поступающих запросов встаёт в очередь до освобождения рабочего процесса. Если же процессов слишком много, то часть из них окажутся неиспользуемыми и будут просто занимать свободную оперативную память. Чтобы задать подходящее количество рабочих процессов, нужно понять, сколько обычно их используется в действительности.

В uwsgi новых версий есть множество разных способов контролировать его работу, но в имеющейся у меня версии 1.2.3 из репозитория Debian Wheezy поддерживается не так-то много вариантов для его контроля. Я остановился на варианте с механизмом Stats Server.

Настройка сокет-файлов

Чтобы включить Stats Server для каждого из приложений, нужно добавить соответствующую опцию в конфигурацию каждого из приложений в файлах /etc/uwsgi/apps-available/. Однако, чтобы не редактировать много файлов конфигурации, можно изменить файл с настройками по умолчанию. Копируем из файла /usr/share/uwsgi/conf/default.ini настройки в файл /etc/uwsgi/uwsgi.ini и добавляем опцию:
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
В результате у меня получился файл с таким содержимым:
[uwsgi]
autoload = true
master = true
workers = 2
no-orphans = true
pidfile = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/pid
socket = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/socket
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
chmod-socket = 660
log-date = true
uid = www-data
gid = www-data
Теперь пропишем использование этого файла с настройками по умолчанию в файле /etc/default/uwsgi, прописав в него соответствующую настройку:
INHERITED_CONFIG=/etc/uwsgi/uwsgi.ini
Теперь нужно перезапустить все приложения:
# /etc/init.d/uwsgi restart
В каталогах приложений /var/run/uwsgi/app/<приложение>/ должны появиться файлы stats.

Скрипт обнаружения и опроса

К сожалению, права доступа к этому файлу точно такие же, как у файла сокета и поменять их через файл конфигурации приложения нельзя, поэтому для доступа к файлу статистики от имени пользователя zabbix придётся либо воспользоваться sudo, либо включить пользователя zabbix в группу www-data. Я воспользовался sudo. Однако, прежде чем перейти к настройкам sudo и Zabbix-агента, нужно написать скрипт, с помощью которого Zabbix-агент будет извлекать необходимые данные из сокетов статистики.

У меня получился скрипт на языке Python со следующим содержимым:
#!/usr/bin/python

import sys, os, socket, json

def discover():
    data = []
    for app in os.listdir('/run/uwsgi/app/'):
        data.append({'{#UWSGI_APP}': app})
    data = {'data': data}
    return json.dumps(data)

def read_stats(unix_socket):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try:
        s.connect(unix_socket)
    except socket.error:
        return None

    data = ''
    while True:
        buf = s.recv(4096)
        if len(buf) < 1:
            break
        data += buf
    s.close()

    data = data.decode('UTF-8')
    return json.loads(data)

def analyze_stats(data):
    listen_queue = data['listen_queue']
    listen_queue_errors = data['listen_queue_errors']
    
    total = 0
    idle = 0
    busy = 0
    for worker in data['workers']:
        total += 1
        if worker['status'] == 'idle':
            idle += 1
        elif worker['status'] == 'busy':
            busy += 1

    pidle = idle * 100.0 / total
    pbusy = busy * 100.0 / total

    return listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy

if __name__ == '__main__':
    if len(sys.argv) == 2:
        if sys.argv[1] == 'discover':
            print discover()
    elif len(sys.argv) == 3:
        app = sys.argv[2]
        data = read_stats('/run/uwsgi/app/%s/stats' % app)
        if data is None:
            print 'No stats'
            sys.exit()

        listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy = analyze_stats(data)
        if sys.argv[1] == 'listen_queue':
            print listen_queue
        elif sys.argv[1] == 'listen_queue_errors':
            print listen_queue_errors
        elif sys.argv[1] == 'total':
            print total
        elif sys.argv[1] == 'idle':
            print idle
        elif sys.argv[1] == 'busy':
            print busy
        elif sys.argv[1] == 'pidle':
            print pidle
        elif sys.argv[1] == 'pbusy':
            print pbusy
Я поместил этот скрипт в файл /etc/zabbix/uwsgi.py. Не забудьте поменять права доступа к файлу:
# chown root:root /etc/zabbix/uwsgi.py
# chmod ugo=rx /etc/zabbix/uwsgi.py

Настройка sudo и Zabbix-агента

Теперь дадим пользователю zabbix права на запуск этого скрипта от имени пользователя root. Запускаем visudo и добавляем в файл конфигурации две строчки:
Defaults:zabbix !requiretty
zabbix ALL=(www-data:ALL) NOPASSWD:/etc/zabbix/uwsgi.py *
Отредактируем файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf:
UserParameter=uwsgi.discover,/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py discover
UserParameter=uwsgi.total[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py total $1
UserParameter=uwsgi.idle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py idle $1
UserParameter=uwsgi.busy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py busy $1
UserParameter=uwsgi.pidle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pidle $1
UserParameter=uwsgi.pbusy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pbusy $1
UserParameter=uwsgi.listen_queue[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue $1
UserParameter=uwsgi.listen_queue_errors[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue_errors $1
И перезапустим Zabbix-агента, чтобы новые настройки вступили в силу:
# /etc/init.d/zabbix-agentd restart

Шаблоны Zabbix

Я подготовил два шаблона для Zabbix 3.4:
В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения, который находит все имеющиеся сокет-файлы для статистики приложений. Обнаружение происходит раз в час:

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

Пример графика

Приведу пример графика с реального сервера:

Из графиков видно, что приложения maps и bottle иногда используют 100% рабочих процессов, а вот приложение ncc постоянно использует не более 30% рабочих процессов.

воскресенье, 9 июня 2019 г.

Исправление ручного закрытия проблем в Zabbix 3.4

В Zabbix версии 3.2 появилась возможность закрывать проблемы вручную, которая бывает очень полезной, если невозможно сформулировать выражение, при котором проблема должна закрываться автоматически. Например, это может оказаться полезно в триггерах, которые срабатывают при появлении определённой строчке в каком-либо журнале, или если информация о проблеме поступает в виде трапа в элемент данных типа Zabbix-траппер или SNMP trap, особенно если сообщения о восстановлении не предусмотрены или потерялись.

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

Для ручного закрытия проблемы нужно найти проблему и перейти по ссылке в колонке подтверждения. Например, ниже показана страница просмотра проблем с одним из таких триггеров:

Для ручного закрытия проблемы нужно написать сообщение, объясняющее причину ручного закрытия проблемы, и отметить соответствующую галочку:

Однако, реализована эта функция оказалась довольно странным образом: чтобы воспользоваться галочкой, нужно иметь права редактирования группы узлов, на одном из которых может сработать проблема, требующая ручного закрытия. Даже если предположить, что пользователь, желающий закрыть проблему, не захочет менять настройки наблюдаемых узлов или удалять их, у него остаётся возможность закрыть любую проблему, которая ему не нравится, если у него есть доступ на редактирование соответствующего узла. Для этого нужно всего лишь зайти в настройки триггера, который породил проблему, выставить галочку, разрешающую ручное закрытие, закрыть проблему и вернуть галочку обратно. На мой взгляд, польза от такой настройки сомнительна, потому что пользователь, имеющий доступ к редактированию настроек, и раньше мог закрыть любую проблему. Для этого ему нужно было всего лишь временно поменять выражение в триггере и вернуть его обратно, или удалить и создать триггер заново. Новая галочка ручного закрытия лишь дала возможность сделать это чуть-чуть корректнее :)

Итак, думаю что вам уже стало понятно, что лучше дать возможность всем закрывать видимые ими проблемы вручную, чем давать им доступ на редактирование настроек узлов. Для этого пересоберём пакет для Debian, внеся в исходные тексты веб-интерфейса соответствующие исправления.

Добавим официальные репозитории с файлами для сборки пакетов в файл /etc/apt/sources.list:
deb-src http://repo.zabbix.com/zabbix/3.4/debian stretch main
Установим ключ репозитория Zabbix и обновим список доступных пакетов:
# wget http://repo.zabbix.com/zabbix-official-repo.key -O - | apt-key add -
# apt-get update
Установим необходимые для сборки пакетов Zabbix зависимости и скачаем файлы для сборки пакетов:
# apt-get build-dep zabbix
# apt-get source zabbix
Ставим пакеты с инструментами, которые пригодятся нам для пересборки пакета:
# apt-get install dpkg-dev devscripts fakeroot
Переходим в каталог zabbix-3.4.12-1+stretch, в который распаковались исходные тексты пакета, скачиваем и накладываем заплатку:
# cd zabbix-3.4.12-1+stretch
# wget http://stupin.su/files/zabbix3_4_12_manual_close_event_acknowledge.diff
# patch -Np0 < zabbix3_4_12_manual_close_event_acknowledge.diff
Текст самой заплатки:
Description: Fixed manual closing problems for users without rights to edit trigger
Fixed manual closing problems for users without rights to edit trigger

Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2019-03-01>

--- zabbix-3.4.12-1+wheezy.orig/frontends/php/app/controllers/CControllerAcknowledgeEdit.php
+++ zabbix-3.4.12-1+wheezy/frontends/php/app/controllers/CControllerAcknowledgeEdit.php
@@ -132,7 +132,6 @@ class CControllerAcknowledgeEdit extends
                        'output' => [],
                        'triggerids' => $triggerids,
                        'filter' => ['manual_close' => ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED],
-                       'editable' => true,
                        'preservekeys' => true
                ]);
--- zabbix-3.4.12-1+wheezy.orig/app/controllers/CControllerAcknowledgeCreate.php        2019-03-01 16:15:28.931300319 +0500
+++ zabbix-3.4.12-1+wheezy/app/controllers/CControllerAcknowledgeCreate.php     2019-03-01 16:15:14.211550047 +0500
@@ -87,7 +87,6 @@
                                'output' => [],
                                'triggerids' => $triggerids,
                                'filter' => ['manual_close' => ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED],
-                               'editable' => true,
                                'preservekeys' => true
                        ]);
--- zabbix-3.4.12-1+wheezy.orig/include/classes/api/services/CEvent.php 2019-03-01 16:18:17.636438283 +0500
+++ zabbix-3.4.12-1+wheezy/include/classes/api/services/CEvent.php      2019-03-01 16:18:24.272325613 +0500
@@ -860,7 +860,6 @@
                        'eventids' => $eventids,
                        'source' => EVENT_SOURCE_TRIGGERS,
                        'object' => EVENT_OBJECT_TRIGGER,
-                       'editable' => true
                ]);
 
                if ($events_count != count($events)) {
Если всё прошло успешно, то можно внести комментарии к изменениям. Запускаем команду для редактирования журнала изменений:
# dch -i
И приводим последнюю запись к подобному виду:
zabbix (1:3.4.12-1+stretch-p2) UNRELEASED; urgency=low

  * Non-maintainer upload.
  * Fixed manual closing problems for users without rights to edit trigger

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 07 Mar 2019 11:04:14 +0500
Теперь можно собрать двоичные пакеты и исправленный пакет с исходными текстами:
# dpkg-buildpackage -us -uc -rfakeroot
В каталоге выше появятся собранные пакеты, которые можно установить при помощи dpkg. Нас прежде всего интересует пересобранный пакет с веб-интерфейсом Zabbix:
# dpkg -i zabbix-frontend-php_3.4.12-1+stretch-p2_all.deb

воскресенье, 2 июня 2019 г.

Пересборка libiksemel для решения проблемы JABBER tls handshake failed в Zabbix

После обновления операционной системы Debian с Wheezy до Stretch перестала работать отправка сообщений через Jabber. При обновлении пакет libiksemel3 обновился с версии 1.2-4 до версии 1.4-3. В журнале сервера Zabbix /var/log/zabbix/zabbix_server.log обнаружились строчки вида:
1769:20190422:083154.224 JABBER: [monitoring@jabber.server.ru/xxx] tls handshake failed
В интернете можно найти массу примеров того, как люди справляются с подобными проблемами:
Как видно, большинство статей ограничивается обходными решениями. Лишь в третьей статье сделана попытка справиться с источником проблем. Проблема эта, правда, была другая и её решение тоже не было корректным: вместо увеличения таймаута автор статьи по-сути просто отключил проверку таймаута.

Поскольку раньше отправка сообщений в Jabber у меня работала без нареканий, то я сразу заподозрил, что в библиотеке libiksemel, при помощи которой Zabbix отправляет сообщения в Jabber, повысились требования к безопасности используемых протоколов и шифров. Для начала я решил узнать, какие протоколы и шифры поддерживает Jabber-сервер.

Jabber-сервер по умолчанию принимает подключения от клиентов на TCP-порт 5222. Через этот порт принимаются как подключения без шифрования, так и с шифрованием. При подключении клиент узнаёт о возможностях сервера и может согласовать с ним шифрование данных, если сервер такую возможность позволяет. К сожалению, я не знаю, как проверить список поддерживаемых протоколов и шифров, подключившись на этот порт.

Однако, для совместимости со старыми клиентами Jabber-сервер обычно принимает подключения также и на порт 5223, на котором сразу после подключения нужно согласовать шифрование, а затем через зашифрованное соединение уже обмениваться данными по протоколу XMPP.

libiksemel для установки защищённых подключений использует библиотеку GNU TLS. Попробуем воспользоваться утилитой командной строки gnutls-cli-debug, использующей эту библиотеку, для проверки возможностей Jabber-сервера. Для этого установим пакет gnutls-bin:
# apt-get install gnutls-bin
Вызываем утилиту для получения списка возможностей Jabber-сервера:
$ gnutls-cli-debug -p 5223 jabber.server.ru
Утилита выводит следующую информацию:
Warning: getservbyport(5223) failed. Using port number as service.
GnuTLS debug client 3.5.8
Checking jabber.server.ru:5223
                             for SSL 3.0 (RFC6101) support... yes
                        whether we need to disable TLS 1.2... no
                        whether we need to disable TLS 1.1... no
                        whether we need to disable TLS 1.0... no
                        whether %NO_EXTENSIONS is required... no
                               whether %COMPAT is required... no
                             for TLS 1.0 (RFC2246) support... yes
                             for TLS 1.1 (RFC4346) support... no
                                  fallback from TLS 1.1 to... TLS 1.0
                             for TLS 1.2 (RFC5246) support... no
                                  fallback from TLS 1.6 to... failed (server requires fallback dance)
              for inappropriate fallback (RFC7507) support... yes
                               for certificate chain order... sorted
                  for safe renegotiation (RFC5746) support... yes
                     for Safe renegotiation support (SCSV)... yes
                    for encrypt-then-MAC (RFC7366) support... no
                   for ext master secret (RFC7627) support... no
                           for heartbeat (RFC6520) support... no
                       for version rollback bug in RSA PMS... no
                  for version rollback bug in Client Hello... no
            whether the server ignores the RSA PMS version... no
whether small records (512 bytes) are tolerated on handshake... yes
    whether cipher suites not in SSL 3.0 spec are accepted... yes
whether a bogus TLS record version in the client hello is accepted... yes
         whether the server understands TLS closure alerts... no
            whether the server supports session resumption... no
                      for anonymous authentication support... no
                      for ephemeral Diffie-Hellman support... no
                   for ephemeral EC Diffie-Hellman support... no
                             for curve SECP256r1 (RFC4492)... no
                             for curve SECP384r1 (RFC4492)... no
                             for curve SECP521r1 (RFC4492)... no
           for curve X25519 (draft-ietf-tls-rfc4492bis-07)... no
                  for AES-128-GCM cipher (RFC5288) support... no
                  for AES-128-CCM cipher (RFC6655) support... no
                for AES-128-CCM-8 cipher (RFC6655) support... no
                  for AES-128-CBC cipher (RFC3268) support... yes
             for CAMELLIA-128-GCM cipher (RFC6367) support... no
             for CAMELLIA-128-CBC cipher (RFC5932) support... yes
                     for 3DES-CBC cipher (RFC2246) support... yes
                  for ARCFOUR 128 cipher (RFC2246) support... yes
            for CHACHA20-POLY1305 cipher (RFC7905) support... no
                                       for MD5 MAC support... yes
                                      for SHA1 MAC support... yes
                                    for SHA256 MAC support... no
                              for ZLIB compression support... no
                     for max record size (RFC6066) support... no
                for OCSP status response (RFC6066) support... no
              for OpenPGP authentication (RFC6091) support... no
Как видно, сервер поддерживает только протоколы SSL 3.0 и TLS 1.0, а протоколы TLS 1.1 и TLS 1.2 не поддерживаются. Не понял я только, откуда в выводе утилиты взялся TLS 1.6.

Теперь распакуем пакет с исходными текстами, чтобы проверить, какие настройки используются:
# apt-get source libiksemel3
В файле src/stream.c имеется строчка:
const char *priority_string = "SECURE256:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2";
Видно, что Jabber-сервер поддерживает только протоколы SSL3 и TLS1.0, а libiksemel требует, чтобы сервер поддерживал как минимум TLS1.2. Безопасность - это, конечно, хорошо, но, как мне кажется, не стоит бежать впереди паровоза и выставлять настройки более жёсткие, чем это принято в библиотеке GNU TLS по умолчанию.

Прежде чем менять строку приоритетов и пересобирать пакет, можно протестировать новую строку приоритетов при помощи такой команды:
$ gnutls-cli --priority 'NORMAL:-VERS-SSL3.0' -p 5223 jabber.server.ru
Если подключение устанавливается успешно с указанной строкой приоритетов, можно продолжать.

Я поменял строку приоритетов следующим образом:
const char *priority_string = "NORMAL:-VERS-SSL3.0";
Вы можете оставить там только слово NORMAL, т.к. я добавил запрет использовать протокол SSL3.0 для того, чтобы принудить клиента использовать протокол TLS1.0, поддерживаемый используемым мной сервером Jabber. Подробнее о формате строки приоритетов можно почитать, например, здесь: 7.9 Priority strings

Подтверждаем изменения, формируя из них заплату:
# dpkg-source --commit
В качестве имени заплаты я указал строчку downgrade_gnutls_priority_string_for_jabber.server.ru

У меня получился такая заплатка:
Description: Downgrade GNU TLS priority string for jabber.server.ru
 Downgrade GNU TLS priority string for jabber.server.ru
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: 2019-04-22

--- libiksemel-1.4.orig/src/stream.c
+++ libiksemel-1.4/src/stream.c
@@ -63,7 +63,7 @@ tls_pull (iksparser *prs, char *buffer,
 static int
 handshake (struct stream_data *data)
 {
-       const char *priority_string = "SECURE256:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2";
+       const char *priority_string = "NORMAL:-VERS-SSL3.0";
        int ret;

        if (gnutls_global_init () != 0)
Запускаем команду для обновления журнала изменений пакета:
# dch -i
Добавляем такую запись:
libiksemel (1.4-3+b1u1) UNRELEASED; urgency=medium

  * Downgrade GNU TLS priority string for jabber.server.ru

 -- Vladimir Stupin   Mon, 22 Apr 2019 10:29:11 +0300
Осталось собрать пакеты с исходными текстами и двоичные пакеты:
# dpkg-buildpackage -us -uc -rfakeroot
Можно установить libiksemel из собранных пакетов, перезапустить сервер Zabbix и проверить отправку сообщений в Jabber. У меня всё заработало.