Привет, Хабр! Вы наверное тоже с любопытством наблюдаете за «эпопеей Американского Дикого Запада по распределению земельных участков — доскачи первым и воткни флаг чтобы застолбить» или возможно даже участвуете в ней. Точнее в ее современном варианте — успей первым подать заявление на госуслугах для получения денежных средств на детей или получи пропуск на выход из дома. Смотря на все это, хочется поделиться опытом нашей команды по тестированию и участию в подготовке регионального портала услуг к предоставлению услуги «Запись в первый класс». Она тоже очень похожа на хабраэффект и, думаю, была близка к тому, что пару дней назад проиcходило с федеральным порталом gosuslugi.ru, но на региональном масштабе.

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

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

image

В процессе подготовки к пиковому проведению записи в первый класс работали несколько команд, т.к. в неё вовлечены, так или иначе: оператор ЦОД, эксплуатант и разработчик информационной системы регионального портала, эксплуатант и разработчик интегрированной информационной системы образования, техническая поддержка пользователей регионального портала. Мы в iondv выполняем последнюю задачу, независимо отслеживая работоспособность портала и поддерживая пользователей.

Наша роль в подготовке – организация тестирования и рекомендаций по конфигурациям кеширования в nginx, ну и мы готовили инструкцию для пользователей с рекомендованным «поведением».

Для тех кто не знает о проблеме записи в 1-й класс
Во многих больших городах существует несколько муниципальных или региональных школ, в которые сложно попасть. Причины разные. Некоторые школы стабильно выпускают детей с высокими результатами по ЕГЭ, другие используют особые программы образования, третьи дают какие-то высоко ценимые родителями навыки (например английский язык или упор на математику, а иногда — особо известный хороший учитель начальных классов), четвертые находятся в районах, где кол-во домов приписанных к школе велико, школа сама небольшая, а другая ближайшая находится за большой магистралью или в получасе ходьбы ребёнком. При том, чтобы ребёнок был зачислен в эту школу, он должен быть зарегистрирован в домах, которые отнесены к школе – это не останавливает родителей, они готовых покупать возможность прописки или даже квартиры. Поэтому ажиотаж подогревается ещё и уже произведенными затратами времени и денег.

Во многих регионах запись в 1-й класс организуют в электронной форме параллельно с записью в очной, но смещённой по времени. Например в Хабаровске в этом году электронная запись открылась в 0:00 минут, а прием в в школах открылся в 10:00 26 января. Очевидно, что подаваться очно – это уже изначально оказаться в конце очереди. В прошлом году запись стартовала одновременно в 10:00 утра, но это не решало проблему очередей — родители всё равно дежурили, опасаясь что портал ляжет или что-то сломается.

Обычно запись в традиционной форме осуществлялась в виде круглосуточных дежурств и ведением очередей. По ссылке можно посмотреть опыт г. Хабаровска в 2017 г. с фотографиями и отзывами родителей. Иногда такая ситуация даже провоцировала драки родителей в очередях, бывало родители организовывали ЧОП, чтобы отсечь параллельные очереди. А некоторые директора вызывали ОМОН для наведения порядка перед школой.

Услуга на востребованный ресурс как техническая задача


Проблема в услугах, подобных «записи в 1-й класс» в интегральном показателе нагрузки (или вероятности запросов) стремящемся к «дельта функции» (?-функция, функция Дирака) – хорошо видна на графиках в виде пики. В этот момент происходит кратный рост обращений в краткий период времени.

?-функция и пиковая статистика запросов

Наш опыт говорит, что основная задача подготовки вовсе не в том, чтобы увеличить ресурсы. Задача — максимально уменьшить потенциальное кол-во запросов в секунду, растянуть их на какой-то период и подготовить систему к оставшейся нагрузке. При этом надо найти и ускорить узкие места — это даст самый большой эффект в соответствии с принципами теории ограниченных систем (принципы Голдратта). И иначе именно самое узкое место откажет. Вся система должна работать от него: принцип «барабан-буфер-веревка».

Физически невозможно в 10-ти минутных песочных часах просыпать весь объем за 1 минуту – очевидно, что они разрушатся. Аналогично и для предоставления услуги. Никого не удивляет — когда перед МФЦ очередь и скандалы на получение услуги, но всех удивляет — почему портал лег.

Есть разные модели поведения обработки нагрузки из теории массового обслуживания:

  • можно ставить пользователей в ожидание, т.е. наращивать очередь;
  • можно просто отказывать в обслуживании тем, кто пришёл позже, например, пока не будут обработаны предыдущие;
  • можно пытаться бесконечно наращивать производительность.

Целесообразность где-то посередине. Ведь для услуги с ограниченным ресурсом, предоставляемой регионом или государством, важна не только скорость, но и, в первую очередь, сохранение социальной справедливости – т.е. равные условия у всех. В тоже время – если пользователь не получил то, что ему нужно, он инициирует новый запрос. В этом случае запросы растут лавиной, формируя модель атаки «эффект собачьей стаи» (dog-pile effect, cache stampede, hit miss storm) – пользователь запрос уже отменил и инициировал новый, а предыдущий ещё в очереди стоит на обработку.

Этот процесс усиливает то, что в подаче участвуют целыми семьями — папа и мама заполняют заявления одновременно, и часто подают заявления несколько раз для надежности. А кроме того, часто ещё и в нескольких вкладках и нескольких браузерах. Поэтому ожидаемую пиковую нагрузку обычно имеет смысл умножать в 2-3 раза, от количества тех, кто фактически подает заявления в подобных услугах.

Отступление про справедливость
Иногда, осмысляя очередной «опыт», я вспоминаю участие в марафонах и разыгрывание ограниченного количества мест в лотерею. Что будет справедливей? Сейчас «справедливость» больше работает на тех, кто разбирается в ИТ. Мы имеем преимущество. Банально если есть время — можно просто написать скрипт для автозаполнения таких полей, да навык копипастле более отработан. Имея это преимущество мы считаем такую ситуацию «более справедливой». Но это не так для других категорий заявителей. И это цифровое неравенство не убрать проведением интеренет в деревни.

Организация оказания услуги


Мы сделали расчет ожидаемого количества заявителей на основе комбинации данных об общем количестве поданных заявлений в прошлому году и поминутным данным по другим регионам. Обычно пик заявлений приходится на 5-10 минуты, в том числе потому что первые три-пять минут порталы почти не отвечают, а позже пользователи заполняют форму от 1 до 5 минут (не удивляйтесь многие даже в таких «нервных» условиях заполняют с телефона).

Примерная модель расчета для условных 1000 заявлений в час такая:

  • пик с 5 по 10 минуту с момента начала и будет подано 80% заявлений по правилу Паретто
  • условно планируем 160 заявлений в минуту или 3 заявления в секунду.

По факту первая подача произошла через одну минуту и 45 секунд, а пик заявлений шёл с 4 минуты.

Для уменьшения нагрузки на ЕСИА и на систему от генерирования сессий авторизации в инструкции предложили пользователям авторизоваться заранее и удлинили время жизни сессии. По факту 50% были авторизованы за 1 час, а ~90% за полчаса. Мы сталкивались ранее с тем, что пользователи начинали заходить на портал за 10 минут до начала услуги — и авторизация начинала работать нестабильно. Сложно сказать почему. Возможно причина в том, что когда в Москве проводятся технические работы ночью — у нас в Хабаровске как раза начало рабочего дня.

Отступление про инструкцию и организационные мероприятия
На главной странице были размещены баннеры с прямой ссылкой на форму и инструкцию.

В самой инструкции были прописаны все прямые ссылки на форму, чтобы избежать использования «ресурсоемких ресурсов» поиска и каталогов с описаниями. Т.е. организационно маршрутизировали пользователей прямо на форму. Заодно разъясняли все спорные и сложные вопросы, чтобы уменьшить кол-во обращений в тех.поддержку.

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

Убрать «дельта функцию» при перезагрузке формы в 00:00 часов невозможно. Весь смысл этой процедуры в том, что услуга появляется в заданное время. Но можно постараться уменьшить кол-во запросов браузера на всех ожидаемых маршрутах пользователей и тем самым оставить нагрузку на систему только от необходимых — форма, динамические справочники и отправка заявления.

Сами настройки nginx достаточно стандартны. Здесь важнее подобрать ограничения, которые может выдержать система. Их и подобрать — т.е. начинать ставить в очередь запросы, когда сервер ожидаемо подойдет к границе своих возможностей.

Ну и самое важное — мы принудительно проставили кеширование (proxy_cache) и увеличили время жизни данных «expires» в nginx для всех путей статики и где возможно динамических страниц, в которых нет сессий. Это кстати частая ошибка при кешировании — записывать в кеш данные (иногда даже статику), в которых сохранена чужая сессия, выход обычно удалять эти куки из заголовков, если сервер не может разделять типы данных.

В браузере для пользователя это выглядит как обновление страниц из файлов загруженных с диска или памяти. Но даже когда у пользователя они получаются с сервера — они берутся из кеша nginx. Сами справочники конечно же в самой системе закешированы.

image

Это уменьшило кол-во потенциальных запросов с 89-запросов до 14 и объем с 2,1 Мб (для 1000 обновивших страницу пользователей это потенциальный пик 4-8Гбит/с) до 38Кб (мы все помним про webpack, но для entrprise платформ это не всегда легко сделать). По результатам прохождения – надо ещё было кешировать не только в системе, но и в nginx часть справочников с формы и динамических классификаторов не используемых в пиковый момент и проставлять для них принудительно время жизни. А при росте нагрузки вообще имеет смысл выставлять на главную полностью статичную страницу с маршрутизацией пользователей на нужную услугу или делать отдельный ресурс для услуги.

Для уменьшения нагрузки на отправку, были отключены черновики и автоматическое заполнение данных на ребёнка. Скорость ввода данных у всех пользователей разная, что исключает появление полностью готовой для отправки формы и позволяет избежать дельта функции на отправку заявлений — всю 1000 в одну минуту. При этом социальная справедливость сохраняется, хотя, конечно, появляются жалобы.

Оптимизацию самой системы описывать не буду — при нагрузочном тестировании выявлялись узкие места — в основном в запросах к СУБД и оптимизировались индексы и сами запросы.

Наверное самой главной оптимизацией является упрощение формы. Что сильнее всего влияет на скорость при реализации в форме?

  • загрузка файлов — при загруженном канале, это существенно увеличивает нагрузку на него и систему, особенно при загрузке сканов большого размера. Математика тут простая — типичная фотография на смартфоне сейчас занимает 5-10Мбайт (привет владельцам новых iPhone у которых низкое разрешение на камерах просто не поддерживается) и для 5-ти документов дает использование одним пользователем до 375 Мбит/с канала (1 байт считаем примерно равным в трафике 10 битам, хотя при кодирование файлов application/x-www-form-urlencoded – это 20 бит), а 100 пользователей в минуту это дает 625 Мбит/с. В регионах где ширина арендованных каналов к ЦОДам редко превышает 100Мбит/с это может стать неожиданным сюрпризом — так как начнется отказ в обслуживании по таймаутам. Пользователи будут нервничать, перегружать и это приведет к «эффекту собачьей стаи». Обычно первый вопрос — зачем нам нужны эти файлы? Если оригиналы все равно приносят, то часто их можно опустить. А если это копии, то какая юридическая значимость в них?
  • сложные справочники. Нагрузку повышает обычно использование адресного справочника ФИАС или КЛАДР. Проблемы здесь в силу размера — ФИАС занимает до 40Гбайт в БД и поиск по нему занимает время. Десятые доли секунды, но умноженные на 1000 одновременных запросов — нагружают любую систему. Без специальной подготовки, возможно в виде отдельного веб-сервиса и на отдельном ресурсе, сложно выдержать нагрузку — поэтому часто для адреса используют обычное текстовое поле.

Ну и перейдем собственно к тестам.

Нагрузочное тестирование при подготовке


Тестирование делали через puppeteer – путем эмуляции действий пользователя в браузере Crominium. Янедекс.танк и JMeter отбиваются защитой от атак, т. к. генерируют множество однотипных запросов. Кроме того эти тесты слабо совпадают с профилем реальных запросов при изменении поведения системы под нагрузкой. Кроме того сервера кешируют запросы, а часть процессов в них воспроизвести сложно (например авторизацию). Кстати, с одного из семинаров devDV у нас есть выступление с презентацией по вопросу использования puppeteer для тестирования, в т.ч. нагрузочного, ссылка на видео.

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

  1. массовая авторизация в ЕСИА
  2. единовременное обновление формы услуги,
  3. массовая подача

На каждый из этапов мы делали отдельный тест.

В прошлом году на этапе авторизации в ЕСИА были сложности, но протестировать его полномасштабно затруднительно. Система внешняя, срабатывает защита от атак и баны на авторизации. Тем не менее можно сформировать профиль тестов для проверки именно узких мест тестируемой системы — обычно это количество одновременно авторизованных сессий и плановые значения авторизаций в минуту, которые можно регулировать рекомендациями.
В тесте важна обертка для организации нескольких потоков, мы используем 'puppeteer-cluster'. Но обычно более сложным является обработка исключений и изменения поведения портала под нагрузкой — часто выявляются элементы верстки, которые всплывают по два раза. Или элементы не проявляются, если какие-то данные загрузились не так, как ожидалось. Это все те ошибки, которые увидят и пользователи и перезагрузят страницу — а значит создадут дополнительную нагрузку. Здесь два пути — реализовывать в тесте обработку исключений. Или дорабатывать портал.

Сам тест простой. Ниже фрагмент от клика на кнопку «Вход» на портале услуг до ввода данных в ЕСИА.

await page.waitForSelector(AUTH_AVAIL,{timeout:OPT_ELEM_WAIT_TIME});
const needAuth = await page.$(ELEM_AUTH_IN);
if (!needAuth) throw (new Error(`Нет элемента входа`));
        
await page.waitForSelector(AUTH_BUT, OPT_ELEMENT_VISIBLE);
await page.click(AUTH_BUT);
await waitNewUrl(page, 'https://esia.gosuslugi.ru/idp/rlogin?cc=bp', OPT_PAGE_WAIT_TIME);
await page.waitForSelector('#mobileOrEmail', OPT_ELEMENT_VISIBLE);
let text = await elemGetText(page, '#authnFrm > div.login-slils-box > div > div.detected > div.left > div.this-user');
if (text) 
   text = text.replace(/ -\(\)/g, '');        
if (text && text.indexOf(user) === -1) {
  await page.click('div.click-to-another > a');
  await page.waitForSelector('#authnFrm > div.login-slils-box > div >' +
                ' div.detected > div.left > div.this-user', OPT_ELEMENT_INVISIBLE);
}
await page.waitForSelector('#password', OPT_ELEMENT_VISIBLE);
await page.type('#mobileOrEmail', user);
await page.type('#password', pwd);
await page.click('#loginByPwdButton');

Проверка обновления формы заявления в ожидании пользователями «открытия записи». Тест на перезагрузку по сути одношаговый, но важно проверять типы возвращаемых ошибок – сетевая это проблема, ошибка nginx, ошибка сервера и соответствует ли форма критериям. А сложность в том, чтобы сгенерировать максимальный объем запросов за наименьшее кол-во времени и не попасть под ограничения защиты (впрочем на время тестов её можно изменить, с другой стороны, это тоже проверка настроек сетевой и серверной инфраструктуры и WAF).

Такие тесты на puppeteer требует достаточно много ресурсов для работы. Де факто получилось, что нужно не менее 2-х ядер против 1-го ядра фронтенд подсистемы и очень широкий канал. Но при аренде их в облаке — это вполне доступно. Мы пользовались Яндекс.облаком.

В тесте сначала реализуется авторизация в ЕСИА для каждого потока отдельно. После этого запускается на каждый поток отдельный браузер и в рамках одной инстанции проводится заданное кол-во обновлений. После этого инстанция перезапускается. Сама проверка может включать типовой путь, например главная страница, форма услуги. Но чаще достаточно только полного обновления услуги и проверки необходимого справочника, что услугу можно подать — все как в инструкции для пользователей.

image

Фрагмент теста по открытию главной и обновлению страницы.

try {
  await page.setViewport(PUP_OPT);
  await page.goto(BASE_URL);
  await page.setCookie(...cookies[worker.id]);
  await page.goto(`${BASE_URL}/nd/lk/form/dnv.htm`);
  rdyRefresh++;
} catch (err) {
  console.error(`# Ошибка в открытия портала или формы ${data}: ${err.message}`);
  getErr++;
  await page.screenshot({path: filename});
}
for (let i = 0; i < AMOUNT_REFRESH - 1; i++) {
  const filenameIter = path.join(BASE_DIR, PIC_DIR, `${data}-${i}.png`);
   try {
       await page.reload({waitUntil: ["networkidle0", "domcontentloaded"]});
        rdyRefresh++;
    } catch (err) {
        if (!err.message.includes('Navigation failed because browser')) {
           console.error(`# Ошибка в обновлении страницы ${data}-${i}: ${err.message}`);
           getErr++;
           await page.screenshot({path: filenameIter});
        }
   }
}

Для нагрузки отправкой заявлений реализовывался весь цикл проверки – с перезагрузкой формы и проверкой ввода всех данных.

Фрагмент.

for (let i = 0; i < AMOUNT_RESEND; i++) {
   const filename = path.join(BASE_DIR, PIC_DIR, `${data}-${i}.png`);
  try {
     await page.goto('https://uslugi27.ru/nd/lk/form/dnv.htm');
  } catch (err) {
      console.error(`# Ошибка в в открытиие страницы 1го класса ${data}-${i}: ${err.message}`);
      await page.screenshot({path: filename});
      getErr++;
      continue;
 }
 try {
     const FORM_PREF = '#createForm > div:nth-child(4) > ';
     await clickDelayed(page,`${FORM_PREF}fieldset.petgroup.ungroupped-attrs > div > div:nth-child(4) > div.col-md-9.attr-data`);
// <…>
     await page.type(`${FORM_PREF}fieldset:nth-child(2) > div > div:nth-child(1) > div.col-md-9.attr-data > input`, 'ТестФамилия');
// <…>
  } catch (err) {
      console.error(`# Ошибка в заполнении данных формы ${data}-${i}: ${err.message}`);
      await page.screenshot({path: filename});
     continue;
  }
  try {
      await page.click('#createForm > div.col_100.controls > button.btn.btn-primary.pull-right.next');
      await clickDelayed(page,`#createForm > div:nth-child(5) > fieldset > div > div:nth-child(1) > div > div`);
       await page.click('#createForm > div:nth-child(5) > fieldset > div > div:nth-child(2) > div > div');
       await page.click('#createForm > div.col_100.controls > button.btn.btn-success.pull-right.submit');
  } catch (err) {
    console.error(`# Ошибка в отправке формы ${data}-${i}: ${err.message}`);
    await page.screenshot({path: filename});
    sendErr++;
    continue;
  }

Кстати, прохождение теста можно ускорить, если вводить все данные не из из puppeteer конструкцией await page.type, а перенести эту логику в сам браузер. Но тогда возрастает сложность отлавливания ошибок. Например так

document.querySelector('#createForm > div:nth-child(4) > fieldset.petgroup.ungroupped-attrs > div > div:nth-child(4) > div.col-md-9.attr-data').click();
 document.querySelector('#createForm > div:nth-child(4) > fieldset:nth-child(2) > div > div:nth-child(1) > div.col-md-9.attr-data > input').value = 'ТестФамилия';

Во время тестов мы обеспечивали несколько тысяч авторизаций ЕСИА и около 16 тыс. отправленных заявлений. Как проводилось восстановление продуктивной информационной системы образования после такого кол-ва заявлений — даже не спрашивайте. Это совсем другая история.

Главный видимый результат этого процесса оказался в том, что местным СМИ в дни записи в первый класс теперь было скучно. Услуга ушла из медийной области.

Параллельно мы сделали сводную панель мониторинга, для проверки работоспособности формы на основе Grafana: количества заявлений, количества звонков, данные яндекс.метрики и т.д. Но эту тему оставим для следующего раза.

Ну и я хотел бы поздравить всех, кто связан с темой улучшения качества предоставления государственных и муниципальных услуг в электронном виде. Эта бесконечная работа по подготовкам не прошли зря — ведь за апрель и май количества поданных заявлений увеличились в разы.