что такое сервер тестирование
Отработка методики тестирования серверов, часть первая
В нашей конференции недавно открылся раздел «Серверы», посвященный аппаратному обеспечению серверов, а так же программному и аппаратному обеспечению систем хранения и резервного копирования данных. Полагаю, в этом разделе часто будет возникать вопрос «а какой же из А, В, С, D лучше?». В последующей серии статей я предлагаю к обсуждению методику тестирования производительности серверов для баз данных.
То есть сервер баз данных (учитываем, что эта машина обслуживает не пару десятков человек) это многопроцессорная (2, 4, 8 процессоров) машина, обслуживающая несколько сотен человек и хранящая довольно большой объем информации в своей базе. Поэтому дисковая подсистема является тоже критичным местом. К тому же от нее требуется безотказность работы и часто возможность горячей замены поврежденных винчестеров. Поэтому в подобных серверах обычно используются дисковые массивы RAID пятого уровня и жесткие диски на SCSI шине. Оперативная память тоже никогда не бывает лишней (она используется и операционной системой и самой базой данных). Используется память с коррекцией ошибок и ее объем начинается от полутора гигабайт и выше.
В общем, вы уже поняли, что это далеко не домашняя машина на p4 3 ГГц, 160 Гб SATA HDD, 512 Мб DDR памяти и GeForce FX 5900. Кстати говоря, вышеописанному серверу видеокарта не нужна совсем.
Если с методикой тестирования производительности дисковой подсистемы вопрос пока остается открытым, то обсуждение методики тестирование скорости обработки данных (а точнее говоря количество транзакций в секунду) можно начинать.
Очевидно, что если на каком то этапе произойдет сбой, то первый клиент может потерять деньги, а второй не получить их. Другими словами, деньги растворяться в киберпространстве. Будет еще интереснее, если мы поменяем шаги 3,4 местами с шагами 1,2. При сбое второй клиент может получить деньги неоткуда. Поэтому транзакции очень важны. В современном мире можно найти множество примеров, где они используются.
OSDL Database Test 1 (OSDL-DBT-1) представляет собой Интернет-тест производительности транзакций. Он имитирует активность пользователей, просматривающих и покупающих товары в интерактивном книжном магазине. OSDL-DBT-1 — реализация спецификаций теста TPC-W ™. Результаты теста включают количество транзакций в секунду, степень загрузки ЦП, активности ввода-вывода и использования памяти. Основным является показатель BT количество bogotransactions (синтетических транзакций) в секунду.
OSDL Database Test 2 это тест производительности оперативной обработки транзакций. Он имитирует работу оптовой фирмы по продаже запасных деталей, в которой несколько пользователей работают с БД, обновляют информацию о клиентах и проверяют наличие товара на складе. OSDL-DBT-2 реализация спецификаций теста TPC-C ™. Результаты теста включают количество транзакций в секунду, степень загрузки ЦП, активности ввода-вывода и использования памяти.
OSDL Database Test 3 (OSDL-DBT-3) этот тест имитирует средства поддержки принятия решений. Он включает нерегламентированные запросы и параллельное изменение данных. OSDL-DBT-3 реализация спецификаций теста TPC-H ™.
В этой статье подробно остановлюсь на тесте OSDL-DBT-1.
Проект OSDL Database Test 1 (OSDL-DBT-1) нацелен на разработку легкого в использовании теста обработки транзакций для ОС Linux и ПО с открытым исходным кодом с возможностью удобного обмена результатами с другим разработчиками. Данный тест является упрощенной производной спецификации TPC-W ™ от TPC. TPC-W используется в данном случае как шаблон, так как считается, что он имитирует нагрузку, достаточную для оптимизации производительности.
TPC-W имитирует активность пользователей, просматривающих веб-страницы и осуществляющих покупки в интерактивном книжном магазине. OSDL-DBT-1 использует характеристики нагрузки TPC-W для создания упрощенного инструмента исследования узких мест системы и измерения относительных повышений производительности, выполненных разработчиками.
Необходимо помнить, что результаты OSDL-DBT-1 нельзя сравнивать с результатами теста TPC-W. TPC требует, чтобы все опубликованные результаты удовлетворяли строгим правилам публикации и аудита, гарантирующих честное сравнение с конкурирующими тестами. Правила TPC также требуют указания стоимостей и доступности продуктов, использованных для тестирования. Следовать этим правилам в открытых разработках непрактично, поэтому результаты теста OSDL-DBT-1 не имеют никакого отношения к результатам теста TPC-W Benchmark.
Что такое TPC-W?
TPC-W определяет коммерческую деятельность интерактивного книжного магазина. Типичный комплект TPC-W включает эмуляторы удаленных браузеров (RBE), веб-серверы и базу данных. Подробное описание теста TPC-W есть на веб-сайте TPС.
Одна веб-страница представляет одно взаимодействие. Каждое взаимодействие может включать в себя один или более обмен между тестируемой системой и эмулированным браузером. Обмены могут включать в себя запросы и передачу файлов cookie, HTML-страниц, изображений и т.д. Эмулированные браузеры работают в соответствии с определенными правилами перехода между страницами, которые имитируют поведение реального пользователя и гарантируют, что доступ к 14 страницам удовлетворяет требованиям TPC-W «Web Interaction Mix», определяющим процентный диапазон каждой транзакции.
При получении запроса от RBE, веб-серверы обращаются к веб-страницам, динамически их обновляют и отсылают обратно. Серверы коммерческого веб-сайта обычно разделены на группы по назначению. Например, сервер изображений обслуживает файлы «.gif» и «.jpg», HTTP-сервер и сервер приложений исполняет бизнес-логику и работает с базой данных, а кэширующий сервер работает с кэшируемыми объектами. Для имитации поиска по сайту спецификация TPC-W предоставляет коммерчески доступную подсистему текстового поиска, которая создает и управляет статическими индексами вне базы данных. TPC-W также требует наличие эмулятора платежного шлюза, имитирующего работу с кредитными картами.
База данных состоит из множества таблиц различных размеров, имеющих сложные взаимосвязи. Транзакции базы данных должны поддерживать свойства ACID. В число свойств ACID входит атомарность, непротиворечивость, автономность и долговечность. Более подробное описание содержится в разделах спецификации TPC-W.
На Рис.1 приведена типичная архитектура TPC-W.
Что такое OSDL-DBT-1?
OSDL-DBT-1 представляет собой набор тестов на основе транзакций. Он нагружает базу данных в соответствии со спецификацией TPC-W. Тест включает в себя базу данных, сервер управления транзакциями и драйвер.
На Рис.2 показаны компоненты OSDL-DBT-1.
Драйвер OSDL-DBT-1 выполняет задачи, сходные с задачами RBE в TPC-W. Он создает и управляет эмулированными пользователями, которые следуют логике, сходной с логикой браузера в тесте TPC-W, но создают вместо HTTP-запросов структуры данных.
В отличие от теста TPC-WTM, использующего веб-серверы для сетевых объектов, тест OSDL-DBT-1 работает с сервером управления транзакциями, который упрощает тестирование и полностью исключает уровень веб-серверов.
Находясь на среднем уровне, сервер управления транзакциями соединяет драйвер с базой данных и осуществляет управление транзакциями. Взаимодействие с базой данных происходит через ODBC.
Базы данных в тестах OSDL-DBT-1 и TPC-W имеют, по существу, одинаковые таблицы с одинаковыми описаниями и следуют одним и тем же правилам заполнения. Хранимые процедуры исполняют одну и ту же бизнес-логику. Некоторые из хранимых процедур OSDL-DBT-1 возвращают меньше данных, чем определено для TPC-W.
Архитектура OSDL-DBT-1
Тест OSDL-DBT-1 состоит из трех компонентов: драйвер (driver), сервер управления транзакциями (transaction management server) и база данных. Первые два компонента написаны на языке C и используют интерфейс ODBC для работы. В качестве базы данных был взять сторонний продукт SAP DB (версии 7.3). Тест разрабатывался под RedHat Linux 7.2, но может использоваться на всех стандартных Linux ОС.
Драйвер непосредственно нагружает базу данных. Он представляет собой многопоточную программу, в которой каждый поток выполняет действия одного пользователя. Драйвер скомпилирован в два отдельных двоичных файла. Первый из них (dbdriver_p1) связан с интерфейсом ODBC и взаимодействует с базой данных напрямую, в обход менеджера транзакций. Этот драйвер можно использовать для простого функционального тестирования хранимых процедур. Второй двоичный файл (dbdriver_p2) связан с сокет-интерфейсом и взаимодействует с сервером управления транзакциями. Данный драйвер играет главную роль в тестировании производительности.
Сервер управления транзакциями представляет собой ПО среднего уровня. Он получает от драйвера запросы на транзакции, доставляет запросы базе данных и возвращает их результаты драйверу. Сервер управления транзакциями настроен на создание определенного количества соединений с базой данных для работы с большим количеством отдельных эмулированных пользователей. Это обеспечивает большую реалистичность загрузки систем.
На Рис.3 показан сервер управления транзакциями и его связь с драйвером и базой данных:
При запуске сервера управления транзакциями создается определенное число потоков DoTxn, каждый из которых открывает соединение с базой данных и ожидает поступление элементов в очередь транзакций.
Прослушивание выделенного порта на предмет входящих соединений выполняется одним потоком. При попытке эмулированного пользователя создать соединение прослушивающий поток создает поток DoConnection для обработки запроса.
DoConnection получает запрос от эмулированного пользователя, добавляет его к очереди транзакций, оповещает DoTxn о том, что очередь не пуста и ждет завершения транзакции.
DoTxn забирает запрос из очереди транзакций, обращается к базе данных и уведомляет DoConnection о выполнении транзакции. После этого DoConnection отсылает результаты эмулированному пользователю.
База данных
База данных состоит из таблиц, индексов и хранимых процедур. Таблицы содержат информацию о товарах интерактивного книжного магазина. Хранимые процедуры выполняют запросы. Индексы создаются для ускорения выполнения запросов. С помощью базы данных эмулированные пользователи могут создавать запросы о лидерах продаж, новых книгах, книгах конкретных авторов и т.д.
Методика тестирования тестом OSDL-DBT-1
Вообще говоря, такой компьютер позицируется в качестве мощной графической станции, но мы его используем в качестве серверного стенда для отработки методики. В конце цикла статей этот компьютер будет рассмотрен более подробно уже на отработанной методике тестирования серверов.
На сервер устанавливается RedHat Linux 7.3 (с версией 9.0 используемая версия SAP DB базы, рекомендуемая разработчиками OSDL тестов, работает некорректно).
Из RPM-пакетов устанавливается SAP DB версии 7.3.0.25, все ее настройки остаются по умолчанию.
Далее разархивируется и собирается DBT-1 тест (версии 1.2) с поддержкой SAP DB базы (тест уже умеет работать с PostgreSQL, но вышла лишь единственная его версия 0.1).
Общий размер базы данных при вышезаданных параметрах получается около 2,4 гигабайт.
Максимальное количество процессоров, которое может задействовать ядро БД при обработке запросов.
Для ускорения доступа, создаются два RAW устройства
/usr/bin/raw /dev/raw/rawX /dev/sdaX
Устройства используются для хранения логов и данных текущей базы.
После чего тест запускается на выполнение (примерно на час). Строка запуска скрипта:
./run_dbt1.sh /home/sapdb/dbt1/tmp/res
После окончания теста и перед началом нового, база данных восстанавливается из бекапов, а сервер перегружается для чистоты эксперимента.
Результаты
Результаты OSDL DBT-1 представлены в виде большого количества текстовых файлов. Основной показателем является количество BTs (боготранзакций в секунду). Interaction % Avg. Response Time (s) Admin Confirm 0.09 0.274 Admin Request 0.10 0.259 Best Sellers 4.95 1.103 Buy Confirm 1.18 0.565 Buy Request 2.55 0.586 Customer Registration 2.94 0.000 Home 16.69 0.505 New Products 4.98 1.125 Order Display 0.65 0.554 Order Inquiry 0.74 0.470 Product Detail 16.92 0.467 Search Request 19.88 0.478 Search Results 16.92 0.684 Shopping Cart 11.41 0.510 59.3 bogotransactions per second 68.5 minute duration total bogotransactions 243754 total errors 0
Хорошо видно, что в данном случае процессоры были загружены лишь наполовину. Для полной загрузки возможно потребуется увеличить количество EU (эмулируемых пользователей), а так же размер самой базы данных (items). При увеличении количества пользователей мы сталкиваемся с ограничением glibc и библиотеки pthread, которое не позволяет эмулировать больше, чем примерно 900 EU с одной машины. В этом случае придется запускать несколько программ dbdriver и appServer на разных машинах.
Выводы
Был рассмотрен первый тест из пакета OSDL-DBT. В следующих статьях будет рассмотрены остальные два, а так же завершен тюнинг их параметров. Полагаю, подобные тесты заслуживают внимания и годятся для тестирования мощных рабочих станций и серверов.
Тестовый сервер для команды разработчиков
Вводная
Варианты решений
1. Один сервер, много хостов
Самый простой вариант. Используем тот же тестовый сервер, только разработчику нужно создавать хост под каждую ветку/проект и вносить его в конфигурацию nginx/apache2.
2. Каждому разработчику по серверу!
Выделяем каждому по серверу и разработчик сам отвечает за свое хозяйство.
3. Контейнеризация — docker, kubernetes
Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в среде виртуализации на уровне операционной системы. Позволяет «упаковать» приложение со всем его окружением и зависимостями в контейнер, который может быть перенесён на любую Linux-систему с поддержкой cgroups в ядре, а также предоставляет среду по управлению контейнерами.
Внедрение docker
При использовании gitlab очень часто попадались на глаза настройки AutoDevOps, kubernetes. Плюс бородатые дядьки на различных meetup рассказывают как у них круто все работает с kubernetes. Поэтому было принято решение попробовать развернуть кластер на своих мощностях, был выпрошен сервер (а тестовый трогать нельзя, там люди тестируют) и понеслась!
Так как опыта у меня с kubernetes 0, делось все по мануалу с попыткой понять как все эти кластера работают. Спустя некоторое время мне удалось поднять кластер, но потом пошли проблемы с сертификатами, ключами, да и вообще с трудностью развертывания. Мне же нужно было решение проще, чтобы научить своих коллег с этим работать (например, тот же отпуск не хочется проводить сидящим в скайпе и помогающим с настройкой). Поэтому kubernetes был оставлен в покое. Оставался сам docker и нужно было найти решение для маршрутизации контейнеров. Так как их можно было поднять на разных портах, то можно было бы использовать тот же nginx для внутреннего перенаправления. Называется это обратный прокси сервер.
Обратный прокси-сервер — тип прокси-сервера, который ретранслирует запросы клиентов из внешней сети на один или несколько серверов, логически расположенных во внутренней сети. При этом для клиента это выглядит так, будто запрашиваемые ресурсы находятся непосредственно на прокси-сервере.
Обратный прокси-сервер
Чтобы не изобретать велосипед, я начал искать готовые решения. И оно нашлось — это traefik.
Запускаю его через docker-compose.yml
Здесь мы сообщаем прокси, что нужно слушать порты 80,443 и 8080 (веб морда прокси), монтируем сокет докера, файл конфигурации и папку с сертификатами. Для удобства именования тестовых сайтов, мы решили сделать локальную доменную зону *.test. При обращении к любому сайту на ней, пользователь попадает на наш тестовый сервер. Поэтому сертификаты в папке traefik самоподписаные, но он так поддерживает Let’s Encrypt.
Перед стартом нужно создать в докере сеть proxy (можете назвать по своему).
Это будет сеть для связи traefik с контейнерами php сайтов. Поэтому указываем ее в параметре networks сервиса и в networks всего файла указав в параметре external: true.
Тут все довольно просто — указываем точки входа http и https трафика, не забудьте поставить insecureSkipVerify = true если сертификаты локальные. В секции entryPoints.https.tls можно не указывать сертификаты, тогда traefik подставит свой сертификат.
Можно запустить сервис
Если перейти по адресу site.test, то выдаст ошибку 404, так как этот домен не привязан ни к какому контейнеру.
Упаковываем приложения в docker
Теперь нужно настроить контейнер с приложением, а именно:
1. указать в сетях сеть proxy
2. добавить labels с конфигурацией traefik
Ниже приведена конфигурация одного из приложений
В сервисе app, в секции сети нужно указать proxy и default, это значит что он будет доступен в двух сетях, как видно из конфигурации я не пробрасываю порты наружу, все идет внутри сети.
Далее конфигурируем labels
В общей секции networks нужно указать external: true
Константу TEST_DOMAIN нужно заменить на домен, например, site.test
Теперь если зайти на домены site.test, crm.site.test, bonus.site.test можно увидеть рабочий сайт. А на домене pma.site.test будет phpmyadmin для удобной работы с бд.
Настройка GitLab
Создаем обработчик заданий, для этого запускаем
Указываем url gitlab, токен и через что будет выполняться задание (executors). Так как у меня тестовый и gitlab находятся на разных серверах, то выбираю ssh executor. Нужно будет указать адрес сервера и логин/пароль для подключения по ssh.
Runner можно сделать прикрепленным к одному или нескольким проектам. Так как у меня логика работы везде одинаковая, поэтому был создан shared runner (общий для всех проектов).
И последний штрих это создать файл конфигурации CI
В данной конфигурации описаны 2 этапа — build и clear. Этап build имеет 2 варианта выполнения — build_develop и build_prod
Gitlab строит понятную диаграмму выполнения процесса. В моем примере все процессы стартуют вручную (параметр when: manual). Сделано это для того, чтобы разработчик после разворачивания тестового сайта, мог делать pull своих правок в контейнер без пересборки всего контейнера. Еще одна причина это наименование доменов — site$CI_PIPELINE_ID.test, где CI_PIPELINE_ID — номер процесса запустившего сборку. То есть отдали на проверку сайт с доменом site123.test и чтобы внести горячие правки, сразу заливаются изменения в контейнер самим разработчиком.
Небольшая особенность работы ssh executor. При подключении к серверу создается папка вида
Поэтому была добавлена строчка
В ней мы поднимаемся на папку выше и копируем проект в папку с номером процесса. Так можно разворачивать несколько веток одного проекта. Но в настройках обработчика нужно поставить галку Lock to current projects, так обработчик не будет пытаться развернуть несколько веток одновременно.
Уборка мусора
Время от времени нужно удалять не используемые контейнеры, чтобы не занимать место, для этого в кроне висит скрипт с таким содержанием
выполняется раз в день.
Заключение
Данное решение помогло нам существенно оптимизировать тестирование и выпуск новых фич. Готов ответить на вопросы, конструктивная критика принимается.
Бонус
Для того чтобы не собирать каждый раз образы из Dockerfile, можно хранить их локальном реестре докера.
В данном варианте не используется аутентификация, это не безопасный способ (. ), но для хранения не критичных образов нам подходит.
Можно настроить gitlab для просмотра
После этого в gitlab появляется список образов
Как я решил протестировать нагрузочную способность web сервера
Это была вступительная минутка юмора, а если серьёзно то вопрос о тестировании web сервера стоял уже давно. Какого-то комплексного решения не было, поэтому я решил сделать хоть какие то первые шаги. Получить первые цифры. Благодаря которым можно было бы уже предметно разговаривать и продумывать дальнейшую стратегию.
Посмотреть, как работает можно, например здесь – бесплатный сервис хранения ссылок http://linkin.link. Про него я писал.
Вступление
Собственно, как тестировать web сервер? Если посмотреть на проблему в лоб – то веб сервер должен отдавать все страницы, которые были запрошены клиентами. И желательно отдавать быстро.
Конечно, есть множество других сценариев. Ведь web сервер не работает сам по себе, как правило, он как минимум работает с какой-нибудь БД, связь с которой — это отдельная большая тема. Опять же могут быть множество отдельных сервисов – авторизационных и прочих. Перед web сервером может стоять балансировщик нагрузок. Всё это может размещается в множестве виртуальных контейнеров, которые опять же могут быть на разных физических серверах. В общем это целый зоопарк каналов передачи данных – каждый из которых может стать узким горлышком.
Начинать надо с малого и для начала решил проверить самый простой и очевидный сценарий. Есть web сервер. Делаем на него запросы. Все запросы должны вернутся без ошибок и вовремя.
Задача поставлена. Тестировать решил так. На одной машине под windows 10 запускаю web сервер. На web сервере запущен сайт. На сайте размещено куча js, сss, mp4 файлов и, собственно, html страничка. Для простоты я просто взял страницу из готового сайта.
Чем досить сервер? Тут 2 пути – скачать что-то готовое или написать свой велосипед. Я решил остановится на втором варианте. И этот выбор я сделал по нескольким причинам.
Во-первых, это интереснее. Во-вторых, своя программа обладает большой гибкость, любой тестовый сценарий можно внедрять, как только он пришёл в голову. Да и, по собственным размышлениям, это займёт меньше времени чем поиск другой программы, скачивания, изучения интерфейса. А потом ещё столкнуться с фактом, что тот функционал, который есть, не подходит.
Httpdos
Изначально использовал TcpClient но по каким-то причинам он выдавал почти сразу на старте множество ошибочных загрузок. Не стал разбираться, а спустился пониже. Реализовал отправку на socket.
В программу добавил таблицу – url, количество удачных скачиваний, количество ошибок, размер закачки.
Ошибки считал по 2 сценариям. Все exception – связанные с открытием, отсылкой, приёмом данных. Собирательно если хоть, где что-то не так – убиваем socket, инкрементируем счётчик ошибок. Ещё добавил проверку длины данных. При первой закачке страницы – запоминается длина данных, все повторные закачки – сверяются с этим числом. Отклонение – считается ошибкой. Можно было бы добавить и что-то другое – но для начала мне и этого достаточно.
Всё для эксперимента готово. Запускаем web сервер. Запускаем httpdos – так назвал программу, чтобы дальше можно было использовать это короткое название. И вижу следующую картину.
Тут я уточню некоторые технические данные. Количество потоков делающие запросы получилось 1200. Эта цифра – количество url прочитанных из файла urls.txt, плюс я решил умножить все запросы в 20 раз. Все цифры взял из головы на момент написания программы. В любой момент можно поставить любые другие по желанию. Преимущество велосипеда.
Откуда такая цифра? По моим размышлениям она зависит от многих обстоятельств. Самая очевидное – это какого размера сами запросы. Как я писал выше – я просто скачиваю все данные с определённой web страницы, которая была взята из стороннего web проекта. И там много mp4 файлов, размер которых под 3 мегабайта. Если уменьшить размер запросов, например скачивать только css – наверняка количество обработанных запросов увеличится. Мне даже стало интересно, и я начал играть с исходным кодом как со стороны web сервера, так и со стороны httpdos. Там есть куча различных таймеров, буферов и прочего. Я смотрел, как то или иное изменение, окажет влияние на скорость.
Так же существенное влияние на скорость оказывало, то что web сервер и httpdos был запущен в режиме отладки в Visual Studio.
Решил параллельно с httpdos сделать загрузки js файла через браузер. Файл закачивается мгновенно. Т.е. httpdos не оказывает существенного влияния на web сервер.
Продолжаю эксперименты. Решил запустить три программы httpdos одновременно. И понаблюдать за результатами. То, что я увидел, и я является причиной появления этой статьи.
А у видел я, что по каким-то причинам спустя примерно минуту начали появляться ошибки загрузок.
И я начал расследование.
Такого поведения просто не должно быть!
Забегая вперёд, скажу, что даже не это побудило меня к дальнейшему расследованию. Ну пусть по каким-то причинам пару запросов из сотен тысяч не дойдут. Из моей практики – браузеры делают большое число одних и тех же запросов пока не получат запрошенные данные. Клиенты этого даже не заметят.
То, что произошло дальше заставило меня напрячься.
В процессе экспериментов я и так и сяк изгалялся над httpdos. И один сценарий привёл к неожиданным результатам.
Если запустить не менее 3 программ. Думаю, что такое количество связанно с количеством одновременных запросов в секунду. Начать dos атаку. Дождаться появления ошибок. Потом резко закрыть программы. То слушающий socket web сервера просто умирает! Вот так – нету никакой ошибки на стороне сервера. Все потоки работают. А socket ничего не принимает. Это уже ни в какие ворота.
Эксперименты показали, что socket оживал примерно через 4 минуты, но, если dos атаку проводить долго – socket умирал навсегда, по крайней мере я минут 15 ждал оживления, а дальше уже и не интересно было. Такого поведения просто не должно быть!
Эксперимент был проведён множество раз – результат всегда один и тот же.
Если перезапускать web сервер, т.е. получается мы пересоздаём слушающий socket, то socket начинал принимать клиентов сразу.
Первый шаг
Невозможно установить соединение, потому что целевой компьютер активно отказался от него. Обычно это происходит в результате попытки подключиться к службе, которая неактивна на чужом хосте, то есть к службе, на которой не запущено серверное приложение.
Ну… информативно(сарказм) однако. Вроде как это стоит понимать, что socket, к которому мы хотим подключится как бы и нету.
И что делать далее? В таком случае начинаю искать хоть какие-то зацепки, хоть какие-то отклонения от ожидаемого поведения. Поэтому придумываю кучу сценариев, обкладываю их отладчиками, трассировкой, сниффингом и прочим и наблюдаю.
Пару слов о socket
Как видно – для работы с слушающим socket нужно 3 действия. Создали. Слушаем. Принимаем клиентов. Схема настолько простая что программисту не остаётся никаких способов для манёвров, ну или возможностей отстрелить себе ногу.
Предполагается что используй вот так (куда ещё проще?) и не иначе – и всё у тебя должно работать. Но как показывает практика – есть нюансы.
Хоть схема использования и проста, я всё же решил провести пару экспериментов что бы отсеять весь лишний код из подозрений. Первое что я сделал это очистил код от лишних действий и поставил точку остановки сразу после конструкции:
Затем подвесил socket. Попытались присоединится клиентом. Программа висела в вышеприведённой строчки кода. Тут мы увидели, что проблема не в коде после socket, а где то внутри socket.
Дальше я решил поэкспериментировать с настройками открытия socket. Собственно, в своё время все настройки и так были исследованы, и оставлены только те, что нужно. Но в свете последних событий – никто не уйдёт от (подозрений) экспериментов.
Под подозрения попали следующие настройки.
Конечно, если подумать – ни одно из этих свойств не должно вызывать эффект, описанный выше, но в принципе вообще ничего не должно убивать socket. Но что-то же убивает. Да и провести этот эксперимент занимает десятки секунд, гораздо меньше, чем я этот текст пишу.
И вообще было проведено большое число разных спонтанных экспериментов. Пришла в голову идея. Изменил код. Запустил. Проверил. Забыл. Десятки секунд, не более.
Что касается тайм аутов. В коде сервера реализованы различные таймауты, но на более высоком уровне. В приёмнике есть таймауты на приём шапки http, определения длины body, расчёт времени на приём body исходя из длины body и сценария наихудшего канала связи, например 1024 кб/c.
Примерно такие же правила и на отправку.
И если таймауты выходят – socket клиента закрывается и удаляется. Для socket вызывается shutdown и close. Всё как и предписывает microsoft.
Поставил таймауты для resive/transmite 2000ms. Не помогло. Идём далее.
Что это за параметр? Wiki говорит:
Получает или задает значение, задающее время существования (TTL) IP-пакетов
Т.е. при прохождении очередного шлюза параметр уменьшается на 1. При достижении 0 – пакет удаляется. И вроде это не наш случай. Потому как наша система вся на localhost. Но! При гугление я нашёл следующую информацию.
Там было сказано, что windows от этого параметра рассчитывает двойное время нахождения socket в режиме TIME_WAIT. Про этот режим более подробно ниже.
Поставил ttl в минимально возможное значение. Не помогло.
Утечка sockets?
Далее я подумал – сервер открывает каждую секунду около 6000 тысяч sockets и закрывает их. И всё это крутится на кучи асинхронных Task. Вдруг количество открытых сокетов и закрытых не совпадает? И есть, некая утечка sockets?
Быстро набросав код – принцип которого заключается в инкременте глобальной переменной при открытии socket и декременте при закрытии и вывода этого числа в консоль.
Весь дополнительный код состоял из 3 блоков:
Разумеется, каждый блок размещается в нужном месте.
Запустив программы, я получил следующее:
Видео можно разделить на три части. Запуск. Рабочее состояние – длится около 3 минут. В этом состоянии ни одной ошибки. И первое появление ошибки. Собственно, вторую часть я вырезал как не интересную.
Как же нам интерпретировать результат? Во-первых, мы видим число примерно 400 выводящееся каждую секунду.
Как работает веб сервер? Идеальный сервер приняв клиента мгновенно формирует ответ, отправляет его клиенту и мгновенно закрывает socket (хотя в реальности socket не закрывается а ожидает следующих запросов, ну условимся что так работает идеальный сервер). Поэтому количество открытых socket в идеальном сервере каждую секунду было бы 0. Но тут мы видим, что каждую секунду у нас примерно 400 обрабатывающихся клиентов. Что ж, для неидеального сервера вполне норма. Вообще количество одновременных клиентов в нашем сервере задаётся глобальной настройкой. В данном случае 10000 – что значительно выше 400.
Так же мы видим, что периодически подпрыгивающее значение до 1000-2000. Связанно это может быть с чем угодно. При желании можно и это выяснить. Может сборщик мусора, может что ещё. Но, собственно, ничего криминального в этом нет.
И вот появились первые ошибки. Я начал закрывать программы. На видео виден характерный момент – при закрытии второй программы, в последней оставшейся программе, посыпались ошибки открытия socket. Это в очередной раз показал своё лицо баг – который мы так пытаемся отловить, пока безуспешно. Одна радость – повторяется он регулярно, поэтому хоть есть возможность поиска. Сама же эта ошибка нам пока ничего не даёт.
Главная цель текущего эксперимента – сопоставить количество открытых и закрытых socket. Окончательная цифра 0. Всё, как и должно быть. Ладно идём дальше.
Динамический диапазон портов
Далее расследование завело меня в область настроек windows. Должны же быть какие-то настройки для работы tcp/ip стека? Гугление мне подкинуло множество, но особо я хотел бы остановится на наиболее подходящих к нашему случаю.
Если выделять тысячи портов в секунду – то можно и исчерпать их всех.
Такой способ выдаёт socket с портом из динамического пула windows. Т.е. windows сама решает какой порт тебе выдать. Я поменял этот код на другой – где я сам указывал порты. Но первый запуск показал, что диапазон выбранных мною портов был нашпигован как изюм открытыми портами, плюс доступ к некоторым портам выдавал странные ошибки о некорректном доступе. Это заставляло меня усложнять программу – делать код поиска открытых портов
Поискав готовое решение я увидел, что это не простой способ – в одну строчку. И я не стал развивать эту тему дальше. Windows виднее кокой мне порт дать.
В дальнейшем я и убедился в своей правоте. Как оказалось Microsoft изменила этот диапазон, и он составляет 49152-65535. И того где-то 15k портов. Явно меньше, чем 65535
Поэтому первая настройка – увеличиваем этот диапазон.
Я применил следующую команду:
netsh int ipv4 set dynamicport tcp start=10000 num= 55535
Команду запускаем под администратором. Проверить значение можно командой:
netsh int ipv4 show dynamicport tcp
Изменить то изменили – но как проверить, что диапазон действительно расширен? Да и нужно ещё узнать повлияла ли эта настройка на наш баг.
Делаем вывод на стороне сервер, всех портов подключившихся клиентов. Ну и пытаемся увидеть какие-нибудь отклонения от нормы. Вдруг кусок выпадет из списка?
Что показало нам запуск? При старте одной программы вылетела ошибка одного подключения. Не будем на это обращать внимание. А то так можно закопаться по уши в отладке. Дальше работало всё, как и ожидалось. Во-первых, мы чётко увидели, что диапазон выделяемых портов действительно стал 10000-65535. Во-вторых, порты выделяются последовательно без каких-то видимых провалов. Достигнув максимума, цикл повторяется. Я прогнал несколько циклов – никаких отклонений.
Далее я начал закрывать программы – и при закрытии второй программы – серверный socket завис. Порты перестали выделятся. Во второй программе httpdos посыпались ошибки открытия socket.
Выводы – проблема не в портах. По крайней мере порты явно не заканчиваются.
TIME WAIT
А что у нас есть вообще по протоколу TCP? Идём сюда и внимательно читаем.
Под подозрение попадает одно состояние TIME WAIT. Это одно из стояний, в котором может находится socket, т.е. пара ip+port. После его закрытия.
Получается, что мы закрыли socket – но он ещё будет недоступен. Поиск показал, что это время находится в районе 4 минут.
Если бы мы играли в игру холодно-горячо, то я бы заорал горячо! Горячо! Похоже это именно наш случай.
Но зачем такое странное поведение. Опять же из справки получил пояснение. Т.к. протокол tcp/ip гуляет по сети пакетами, а сами пакеты могут пойти по разным маршрутам. И вообще застрять на некоторое время на каком-нибудь шлюзе, может получится ситуация, когда мы открыли socket, поработали с удалённым сервером. Закрыли socket. Потом этот socket открывает другая программа – а ей начинают валится пакеты от предыдущей сессии. Как раз время таймаута и выбрано что бы отставшие все пакеты в сети удалились как мусорные.
OK, а можно ли изменить этот параметр? Оказывается в Windows есть целая ветка реестра, отвечающая за параметры tcp протокола.
Нас интересуют пока 2 параметра:
Цель установить минимальное значение. Минимальное значение 30. Что означает что через 30 секунд порт опять будет доступен. Устанавливаем. Ну а далее наша стандартная проверка.
Запускаем сервер. Запускаем клиенты. Досим. Дожидаемся отказа socket. Потом запускаем консоль и вводим команду:
Такое ощущение что весь выделенный диапазон портов находится в состоянии TIME WAIT. Это пока соответствует ожиданиям. Ждём 30с. Повторяем команду. Листинг уменьшился в разы. Все порты с WAIT TIME пропали.
Проверяем серверный socket на оживление. Глух ((( А такие надежды были на эту настройку.
Впрочем, определённые выводы сделать можно. Настройка наша работает. Пауза 30с. Оставляем – полезная настройка для нагруженного сервера.
Wireshark
Решил посмотреть, что нам покажет Wireshark. Кто не знает — это мощнейший анализатор всевозможных протоколов, в том числе и tcp/ip. До недавнего времени в программе не была реализована функция прослушки localhost и приходилось производить некоторые танцы с бубном. Ставить виртуальную сетевую карту. Трафик пускать через неё. А Wireshark уже мог подключатся к прослушке этой карты.
Недавно подвезли возможность прослушки localhost – что очень облегчило работу.
Далее всё стандартно. Запускаем сервер, клиентов. Убиваем слушающий socket.
Запускаем Wireshark – ставим фильтр на 80 порт. Открываем браузер – пытаемся закачать файл javascript.
Вот какое непотребство мы увидели в сниффере:
Видно, наш запрос. Браузер с порта 36036 пытается достучатся к порту 80. Выставляет флаг SYN. Это стандартно. Но вот с порта 80 нам возвращается флаг RST — оборвать соединения, сбросить буфер. Всё. И так по кругу.
Вывод. Wireshark – нам особо не помощник. Разве только мы увидели, что слушающий socket не мёртв совсем, а отвечает. Он работает по какой-то своей внутренней логике, а не просто умер.
Журнал windows
Решил посмотреть – может что в журнале событий windows есть что-то интересное. Для этого захожу в журнал.
Панель управления-> Администрирование-> Управление компьютером-> Служебные программы-> Просмотр событий-> Журналы Windows
Либо запустите eventvwr.msc
Там, разумеется, есть миллионы записей. И чтобы не копаться в этих миллионах, я делаю следующее. Убиваю socket. Захожу в журнал – и чищу подразделы. Захожу в браузер – пытаюсь скачать файл. Захожу в журнал – и смотрю что там только что появилось.
К сожалению, никаких событий, кусающихся моей проблемы, не появилось. Я даже нашёл такой раздел Microsoft/Windows/TCPIP. Но кроме одинокой записи – “работает”, там ничего не было.
Финал?
Поиск в интернете всей доступной информации по проблеме периодически выдавал мне страницы подобной этим:
И ещё во многих других местах.
Отсюда я сделала 2 вывода – я со своей проблемой не одинок, и как минимум периодически с ней сталкиваются и другие.
Есть ещё 2 параметра TCP стека с которыми можно поэкспериментировать.
Поиск по этим параметрам для Windows 10 ничего не дал. Только для Windows 2003-2008 Server. Может плохо искал (наверняка). Но я всё же решил их проверить.
Установил следующие значения:
TcpMaxDataRetransmissions REG_DWORD: 00000005 (hex)
Перезагрузился. Повторил в который раз все процедуры. И….
Socket жив. Всё проверив несколько раз, путём добавления и удаления из реестра параметров с последующей перезагрузкой компьютера. Все эксперименты показали, что с параметрами – socket остаётся жив. Это всё?
В этом месте был завершающий текст с выводами и размышления на тему почему именно эти параметры помогли.
На следующий день, я решил закрепить полученную информацию. Запускаю эксперимент – socket мёртв. Мы вернулись к тому, с чего начинали!
Эти параметры оказались бесполезны в решении нашей проблемы. Но я их всё-таки оставил – потому как по смыслу они полезные.
Финал
Продолжая эксперименты, я обнаружил в коде один хитрый перехватчик исключений на слушающим socket. Точка остановки в этом месте показала, что при возникновении нашего случая – сюда начали валится куча exception со следующим сообщением:
Удаленный хост принудительно разорвал существующее подключение.
В этом exсeption была паузу. С пояснением – исключение на socket случай неординарный, например socket занят другим приложением, делаем паузу что бы всё утряслось и в лог не сыпалось миллионы сообщений.
Как оказалось не неординарный.
Картина сложилась
Получается, что приёмный socket набрал большое число входящих подключений, которые оборвались, и больше не принимает новые подключения пока поток висел в exception на паузе.
Выводы
Ну что ж. Я, конечно, ожидал что геройски одержу победу над некоей эпичной ошибкой. А всё оказалось очень банально. Собственно, как всегда.
Все эксперименты у меня заняли где-то полтора дня. Эту статью я писал намного больше.
Из плюсов – цель эксперимента достигнута. Сервер провёл часы под нагрузкой, вряд ли я бы столько тестировал его, не будь этой ошибки. Плюс я отдельно проверил некоторые важные механизмы работы сервера. Т.к. искал ошибку – эта проверка была интересна. Если бы просто проверял – то это была бы наискучнейшая работа.
Узнал новую информацию по поводу работы сети – эти знания пригодятся впоследствии при решения очередного непонятного бага.
Почему у меня заняло так много времени на поиск банального Delay? Я думал над этим и пришёл к выводу, что все эксперименты уводили в сторону от ответа. Сложившееся картина показывала нам что умирал именно socket. Переставал принимать подключения. Спустя время оживал. Все сниферы показывали, что подключений не происходит.
Опять же было неправильное представление, что серверный socket работает в отдельном потоке. И в любом случае должен принимать клиентов. Поэтому я и скал проблему в настройках socket и tcp стека. Главная проблема – я думал поток висит на socket. А в нашем аварийном случае он висел на exeption в Delay.