В этой статье рассматриваются различные виды тестирования в продакшене и условия, при которых каждый из них наиболее полезен, а также рассказывается о способах организации безопасного тестирования различных сервисов в продакшене.
Стоит заметить, что содержание этой статьи касается только тех сервисов, развёртывание которых контролируют разработчики. Кроме того, следует сразу предупредить, что применение любого из описанных здесь видов тестирования – нелёгкая задача, которая зачастую требует внесения серьёзных изменений в процессы проектирования, разработки и тестирования систем. И, несмотря на заголовок статьи, я не считаю, что какой-либо из видов тестирования в продакшене абсолютно надёжен. Есть лишь мнение, что такое тестирование позволяет значительно снизить уровень рисков в будущем, а вложенные затраты будут обоснованными.
(Прим. пер.: поскольку оригинальная статья — лонгрид, для удобства читателей она разделена на две части).
Зачем нужно тестирование в продакшене, если его можно выполнять на стейджинге?
Значение стейджинг-кластера (или стейджинговой среды) разными людьми воспринимается по-разному. Для многих компаний развёртывание и тестирование продукта на стейджинге — неотъемлемый этап, предшествующий его финальному выпуску.
Многие известные организации воспринимают стейджинг как миниатюрную копию рабочей среды. В таких случаях возникает необходимость обеспечивать их максимальную синхронизацию. При этом обычно необходимо обеспечивать работу отличающихся экземпляров stateful-систем, таких как базы данных, и регулярно синхронизировать со стейджингом данные из продакшен-среды. Исключением является только конфиденциальная информация, позволяющая установить личность пользователя (это необходимо для соблюдения требований GDPR, PCI, HIPAA и прочих регламентов).
Проблема этого подхода (по моему опыту) заключается в том, что отличие состоит не только в использовании отдельного экземпляра базы данных, содержащей актуальные данные продакшен-среды. Зачастую отличие распространяется на следующие аспекты:
- Размер стейджинг-кластера (если это можно назвать «кластером» — иногда это просто один сервер под видом кластера);
- Тот факт, что на стейджинге обычно используется кластер гораздо меньшего масштаба, также означает, что параметры конфигурации почти для каждого сервиса будут различаться. Это относится к конфигурациям балансировщиков нагрузки, баз данных и очередей, например, числу дескрипторов открытых файлов, числу открытых подключений к базе данных, размеру пула потоков и пр. Если конфигурация хранится в базе данных или хранилище данных типа «ключ-значение» (например, Zookeeper или Consul), эти вспомогательные системы тоже должны присутствовать в стейджинговой среде;
- Число оперативных подключений, обрабатываемых stateless-службой, или способ повторного использования TCP-соединений прокси-сервером (если эта процедура вообще выполняется);
- Недостаток мониторинга на стейджинге. Но даже если мониторинг осуществляется, некоторые сигналы могут оказаться совершенно неточными, так как отслеживается среда, отличная от рабочей. Например, даже если выполняется мониторинг задержки запроса MySQL или времени отклика, трудно определить, содержит ли новый код запрос, который может инициировать полное сканирование таблицы в MySQL, так как гораздо быстрее (а иногда даже предпочтительнее) выполнить полное сканирование маленькой таблицы, используемой в тестовой базе данных, нежели производственной базы данных, где запрос может иметь совершенно другой профиль производительности.
Хотя справедливо предположить, что все указанные выше отличия не являются серьёзными аргументами против использования стейджинга как такового, в отличие от антипаттернов, которых следует избегать. В то же время стремление всё сделать правильно зачастую требует огромных трудовых затрат инженеров в попытке обеспечить соответствие сред. Продакшен постоянно меняется и подвержен влиянию различных факторов, поэтому попытка достичь указанного соответствия похожа на путь в никуда.
Более того, даже если условия на стейджинге будут максимально похожи на рабочую среду, есть и другие виды тестирования, применять которые лучше на основе реальной производственной информации. Хорошим примером может быть soak-тестирование, при котором надёжность и устойчивость сервиса проверяется в течение продолжительного периода времени при реальных уровнях многозадачности и нагрузки. Оно используется для выявления утечек памяти, определения продолжительности пауз в работе GC, уровня загрузки процессора и прочих показателей за определённый период времени.
Ничто из написанного выше не предполагает, что стейджинг абсолютно бесполезен (это станет очевидно после прочтения раздела, посвящённого теневому дублированию данных при тестировании сервисов). Это лишь свидетельствует о том, что довольно часто на стейджинг полагаются в большей степени, чем это необходимо, и во многих организациях он остаётся единственным видом тестирования, выполняемого до полного выпуска продукта.
Искусство тестирования в продакшене
Так исторически сложилось, что с понятием «тестирование в продакшене» связаны определённые стереотипы и негативные коннотации («партизанское программирование», недостаток или отсутствие юнит- и интеграционного тестирования, небрежность или невнимание к восприятию продукта конечным пользователем).
Тестирование в продакшене, безусловно, будет заслуживать такой репутации, если оно выполняется небрежно и некачественно. Оно ни в коей мере не заменяет тестирования на этапе пре-продакшена и ни при каких условиях не является простой задачей. Более того, я утверждаю, что для успешного и безопасного тестирования в продакшене требуется значительный уровень автоматизации, хорошее понимание сложившихся практик, а также проектирование систем с изначальной ориентацией на этот вид тестирования.
Чтобы организовать комплексный и безопасный процесс эффективного тестирования сервисов в продакшене, важно не расценивать его как обобщающий термин, обозначающий набор разных инструментов и методик. Эта ошибка, к сожалению, была допущена и мной — в моей предыдущей заметке была представлена не совсем научная классификация методов тестирования, а в разделе «Тестирование в продакшене» оказались сгруппированы самые разные методологии и инструменты.
Из заметки Testing Microservices, the sane way («Разумный подход к тестированию микросервисов»)
С момента публикации заметки в конце декабря 2017 года я обсуждала её содержание и вообще тему тестирования в продакшене с несколькими людьми.
В ходе этих дискуссий, а также после ряда отдельных бесед мне стало ясно, что тему тестирования в продакшене нельзя свести к нескольким пунктам, перечисленным выше.
Понятие «тестирование в продакшене» включает в себя целый спектр методик, применяемых на трёх различных этапах. Каких именно — давайте разбираться.
Три стадии продакшена
Обычно дискуссии о продакшене ведутся только в контексте развёртывания кода в продакшен, мониторинга или авральных ситуаций, когда что-то пошло не так.
Я сама до сих пор использовала в качестве синонимов такие термины, как «развёртывание», «релиз», «доставка» и т. д., мало задумываясь об их значении. Ещё несколько месяцев назад все попытки разграничить эти термины были бы отвергнуты мной как что-то несущественное.
Поразмыслив над этим, я пришла к идее, что существует реальная необходимость разграничить различные этапы продакшена.
Этап 1. Развёртывание
Когда тестирование (даже в продакшене) является проверкой достижения наилучших из возможных показателей, точность тестирования (да и вообще любых проверок) обеспечивается лишь при условии, что способ выполнения тестов в максимальной степени схож со способом реального использования сервиса в продакшене.
Другими словами, тесты необходимо выполнять в среде, которая наилучшим образом имитирует рабочую среду.
И наилучшей имитацией рабочей среды является… сама рабочая среда. Для выполнения максимально возможного количества тестов в рабочей среде необходимо, чтобы неудачный результат любого из них не влиял на конечного пользователя.
Это, в свою очередь, возможно, только если при развёртывании сервиса в рабочей среде пользователи не получают непосредственный доступ к этому сервису.
В этой статье я решила использовать терминологию из статьи Deploy != Release («Развёртывание — не релиз»), написанной сотрудниками компании Turbine Labs. В ней термину «развёртывание» даётся следующее определение:
«Развёртывание — это установка рабочей группой новой версии программного кода сервиса в инфраструктуру продакшена. Когда мы говорим, что новая версия программного обеспечения развёрнута, то имеем в виду, что она выполняется где-то в рамках рабочей инфраструктуры. Это может быть новый экземпляр EC2 в AWS или контейнер Docker, выполняемый в поде в кластере Kubernetes. Сервис успешно запустился, прошёл проверку работоспособности и готов (вы надеетесь!) к обработке данных продакшен-среды, но может не получать данные на самом деле. Это важный момент, я ещё раз подчеркну его: для развёртывания не нужно, чтобы пользователи получали доступ к новой версии вашего сервиса. Если учесть это определение, развёртывание можно назвать процессом с почти нулевым риском».
Слова «процесс с нулевым риском» — просто бальзам на душу множества людей, которые настрадались от неудачных развёртываний. Возможность установки ПО в реальной среде без допуска пользователей к нему несёт ряд преимуществ, когда дело касается тестирования.
Во-первых, сводится к минимуму (а может и совсем исчезнуть) необходимость поддерживать отдельные среды для разработки, тестирования и стейджинга, которые неизбежно приходится синхронизировать с продакшеном.
Кроме того, на этапе проектирования сервисов возникает необходимость изолировать их друг от друга таким образом, чтобы неудачное тестирование конкретного экземпляра сервиса в продакшене не привело к каскадным или затрагивающим пользователей отказам других сервисов. Одним из решений, позволяющих это обеспечить, может быть такое проектирование модели данных и схемы базы данных, при котором неидемпотентные запросы (в основном операции записи) могут:
- Выполняться в отношении базы данных продакшен-среды при любом тестовом запуске сервиса в продакшене (я предпочитаю этот подход);
- Быть безопасно отклонены на уровне приложения до достижения ими уровня записи или сохранения;
- Быть выделены или изолированы на уровне записи или сохранения каким-нибудь способом (например, путём сохранения дополнительных метаданных).
Этап 2. Релиз
В заметке Deploy != Release термину «релиз» даётся следующее определение:
«Когда мы говорим, что состоялся релиз версии сервиса, то имеем в виду, что она обеспечивает обработку данных в продакшен-среде. Иными словами, релиз — это процесс, направляющий данные продакшен-среды в новую версию ПО. С учётом этого определения все риски, которые мы связываем с отправкой новых потоков данных (перебои в работе, недовольство клиентов, ядовитые заметки в The Register), относятся к релизу нового ПО, а не его развёртыванию (в некоторых компаниях этот этап также называют выпуском. В этой статье мы будем использовать термин релиз)».
В книге Google об SRE термин «выпуск» используется в главе, посвящённой организации релиза ПО, для его описания.
«Выпуск — это логический элемент работы, состоящий из одной или нескольких отдельных задач. Наша цель — согласовать процесс развёртывания с профилем риска данного сервиса.
В окружениях разработки или пре-продакшена мы можем выполнять сборку ежечасно и автоматически рассылать выпуски после прохождения всех тестов. Для крупных сервисов, ориентированных на пользователей, мы можем начать релиз с одного кластера, а затем увеличивать его масштабы, пока не обновим все кластеры. Для важных элементов инфраструктуры мы можем увеличить срок внедрения до нескольких дней и выполнять его по очереди в разных географических регионах».
В данной терминологии слова «релиз» и «выпуск» обозначают то, что в общей лексике понимается под «развёртыванием», а термины, часто используемые для описания различных стратегий развёртывания (например, blue-green развёртывание или канареечное развёртывание), относятся к релизу нового программного обеспечения.
Более того, неудачный релиз приложений может стать причиной частичных или существенных перебоев в работе. На этом этапе также выполняется откат или хотфикс, если выясняется, что выпущенная новая версия сервиса работает нестабильно.
Процесс выпуска лучше всего работает, когда он автоматизирован и выполняется инкрементно. Точно так же откат или хотфикс сервиса приносят больше пользы, когда частота появления ошибок и частота запросов автоматически соотносятся с базовыми показателями.
Этап 3. После релиза
Если релиз прошёл гладко и новая версия сервиса обрабатывает данные продакшен-среды без очевидных проблем, мы можем считать его успешным. За успешным релизом следует этап, который можно назвать «после релиза».
Любая достаточно сложная система всегда будет находиться в состоянии постепенной потери производительности. Это не означает, что обязательно требуется откат или хотфикс. Вместо этого необходимо отслеживать такие ухудшения (для различных операционных и рабочих целей) и при необходимости выполнять отладку. По этой причине тестирование после релиза больше похоже не на привычные процедуры, а на отладку или сбор аналитических данных.
Вообще, я считаю, что каждый компонент системы должен создаваться с учётом того факта, что ни одна крупная система не работает безупречно на все 100% и что сбои в работе должны быть осознаны и учитываться на этапах проектирования, разработки, тестирования, развёртывания и мониторинга программного обеспечения.
Теперь, когда мы определили три этапа продакшена, давайте рассмотрим различные механизмы тестирования, доступные на каждом из них. Не у всех есть возможность работать над новыми проектами или переписывать код с нуля. В этой статье я постаралась чётко выделить методики, которые лучше всего покажут себя при разработке новых проектов, а также рассказать о том, что ещё мы можем сделать, чтобы воспользоваться преимуществами предлагаемых методик, не внося существенных изменений в работающие проекты.
Тестирование в продакшене на этапе развёртывания
Мы отделили друг от друга этапы развёртывания и релиза, а теперь рассмотрим некоторые виды тестирования, которые можно применять после развёртывания кода в продакшен-среде.
Интеграционное тестирование
Обычно интеграционное тестирование выполняется сервером непрерывной интеграции в изолированной тестовой среде для каждой ветки Git. Копия всей топологии сервиса (включая базы данных, очереди, прокси-серверы и т. п.) разворачивается для тестовых наборов всех сервисов, которые будут работать совместно.
Я полагаю, что это не особенно эффективно по нескольким причинам. Прежде всего, тестовую среду, как и стейджинговую, невозможно развернуть так, чтобы она была идентична реальной продакшен-среде, даже если тесты запускаются в том же контейнере Docker, который будет использоваться в продакшене. Это особенно справедливо, когда единственное, что запускается в тестовой среде, — это сами тесты.
Вне зависимости от того, выполняется ли тест как контейнер Docker или процесс POSIX, он, вероятнее всего, осуществляет одно или несколько подключений к вышестоящему сервису, базе данных или кешу, что бывает редко, если сервис находится в продакшен-среде, где он может одновременно обрабатывать несколько параллельных соединений, зачастую повторно используя неактивные TCP-соединения (это называется повторным использованием HTTP-соединений).
Также проблемы вызывает тот факт, что большая часть тестов при каждом запуске создаёт новую таблицу базы данных или пространство ключей кеша на том же узле, где этот тест выполняется (таким образом тесты изолируются от сбоев сети). Этот вид тестирования в лучшем случае может показать, что система работает корректно при очень конкретном запросе. Он редко эффективен при имитации серьёзных, хорошо распространённых видов отказов, не говоря уже о различных видах частичных отказов. Существуют исчерпывающие исследования, подтверждающие, что распределённые системы часто демонстрируют непредсказуемое поведение, которое невозможно предвидеть с помощью анализа, выполняемого иначе, чем для всей системы целиком.
Но это не подразумевает, что интеграционное тестирование в принципе бесполезно. Можно лишь сказать, что выполнение интеграционных тестов в искусственной, полностью изолированной среде, как правило, не имеет смысла. Интеграционное тестирование по-прежнему следует выполнять, чтобы проверить, что новая версия сервиса:
- Не нарушает взаимодействие с вышестоящими или нижестоящими сервисами;
- Не оказывает негативного влияния на цели и задачи вышестоящих или нижестоящих сервисов.
Первое можно в какой-то мере обеспечить с помощью контрактного тестирования. Благодаря одному только обеспечению правильной работы интерфейсов между сервисами контрактное тестирование является эффективным методом разработки и тестирования отдельных сервисов на этапе пре-продакшена, который не требует развёртывания всей топологии сервиса.
Клиентоориентированные платформы тестирования контрактов, например Pact, в настоящее время поддерживают взаимодействие между сервисами только посредством RESTful JSON RPC, хотя, скорее всего, идёт работа и над поддержкой асинхронного взаимодействия через веб-сокеты, внесерверные приложения и очереди сообщений. В будущем, вероятно, добавится поддержка протоколов gRPC и GraphQL, но сейчас она пока отсутствует.
Однако перед релизом новой версии может возникнуть необходимость проверить не только правильную работу интерфейсов. А, например, убедиться, что продолжительность RPC-вызова между двумя сервисами укладывается в допустимый предел при изменении интерфейса между ними. Также необходимо бывает проверить, что коэффициент попадания в кеш остаётся постоянным, например, при добавлении дополнительного параметра во входящий запрос.
Как оказалось, интеграционное тестирование не является опциональным, его цель — гарантировать, что тестируемое изменение не приведёт к серьёзным, широко распространённым видам отказа системы (обычно к таким, для которых назначены алерты).
В связи с этим возникает вопрос: как безопасно провести интеграционное тестирование в продакшене?
Для этого рассмотрим следующий пример. На рисунке ниже – архитектура, с которой я работала пару лет назад: наши мобильные и веб-клиенты подключались к веб-серверу (сервис C) на базе MySQL (сервис D) с клиентской частью в виде кластера memcache (сервис B).
Несмотря на то, что это довольно традиционная архитектура (и её не назовёшь микросервисной), объединение stateful- и stateless-сервисов делает эту систему хорошим примером для моей статьи.
Отделение релиза от развёртывания означает, что мы можем безопасно развернуть новый экземпляр сервиса в рабочей среде.
Современные service discovery утилиты позволяют сервисам с одинаковыми именами получать теги (или метки), с помощью которых можно отличать выпущенную и развёрнутую версию сервиса с одним и тем же именем. Благодаря этой возможности клиенты могут подключаться только к выпущенной версии нужного сервиса.
Предположим, что мы развёртываем новую версию сервиса C в продакшене.
Чтобы проверить, что развёрнутая версия работает правильно, мы должны иметь возможность запустить её и убедиться, что ни один из контрактов не нарушается. Главное преимущество слабосвязанных сервисов в том, что они позволяют рабочим группам заниматься разработкой, развёртыванием и масштабированием независимо друг от друга. Кроме того, обеспечивается возможность независимо выполнять тестирование, что парадоксальным образом применимо и к интеграционному тестированию.
В блоге Google есть статья под названием Just Say No to More End-to-End Tests («Пришла пора отказаться от дополнительного сквозного тестирования»), где интеграционные тесты описываются следующим образом:
«В ходе интеграционного теста небольшой набор модулей (обычно два) тестируется на предмет согласованности их работы. Если два модуля не интегрируются должным образом, зачем писать сквозной тест? Можно написать гораздо меньший по объёму и более узконаправленный интеграционный тест, который может выявить те же ошибки. Хотя в целом следует мыслить шире, нет нужды гнаться за масштабом, когда речь идёт лишь о том, чтобы проверить совместную работу модулей».
Далее говорится о том, что интеграционное тестирование в продакшене должно следовать той же философии: оно должно быть достаточным и очевидно полезным только для комплексной проверки небольших групп модулей. При правильном проектировании все вышестоящие зависимости должны быть в достаточной степени изолированы от тестируемого сервиса, чтобы плохо сформированный запрос от сервиса A не приводил бы к каскадному сбою в архитектуре.
Для нашего примера это означает, что должно выполняться тестирование развёрнутой версии сервиса C и его взаимодействия с MySQL, как показано на рисунке ниже.
Тестирование операций чтения в большинстве случаев должно быть прямолинейным (если только поток данных, читаемых тестируемым сервисом, не приводит к заполнению кеша с его последующим «отравлением» данными, используемыми выпущенными сервисами). При этом тестирование взаимодействия развёрнутого кода с MySQL становится более сложным, если используются неидемпотентные запросы, которые могут привести к изменению данных.
Мой выбор — выполнять интеграционное тестирование с применением базы данных продакшен-среды. Раньше я вела белый список клиентов, которым разрешалось отправлять запросы тестируемому сервису. Некоторые рабочие группы поддерживают специальный набор пользовательских учётных записей для выполнения тестов в продакшен-системе, чтобы любое сопутствующее изменение данных было ограничено небольшой опытной серией.
Но если абсолютно необходимо, чтобы данные продакшен-среды ни при каких условиях не изменялись во время выполнения теста, то операции записи/изменения:
- Необходимо отклонять запросы на уровне приложения C или записывать в другую таблицу/коллекцию в базе данных;
- Необходимо регистрировать в базе данных в виде новой записи, помеченной как «созданная» при выполнении теста.
Если во втором случае необходимо выделить тестовые операции записи на уровне базы данных, то для поддержки этого вида тестирования схему базы данных следует спроектировать заранее (например, добавив дополнительное поле).
В первом случае отклонение операций записи на уровне приложения может происходить, если приложение способно определить, что запрос не следует отрабатывать. Это возможно либо путём проверки IP-адреса клиента, отправляющего тестовый запрос, либо по идентификатору пользователя, содержащемуся во входящем запросе, либо путём проверки запроса на наличие заголовка, который, как ожидается, должен быть задан клиентом, работающим в тестовом режиме.
То, что я предлагаю, похоже на мок или стаб, но на уровне сервиса, и это не слишком далеко от истины. Этому подходу сопутствует изрядное число проблем. В информационной брошюре для Facebook, посвящённой Kraken, говорится следующее:
«Альтернативное проектное решение — это использование теневого потока данных, когда входящий запрос регистрируется и воспроизводится в тестовой среде. В случае веб-сервера большая часть операций имеет побочные эффекты, которые распространяются вглубь системы. Теневые тесты не должны активировать эти побочные эффекты, так как это может повлечь изменения для пользователя. Использование стабов для побочных эффектов в теневом тестировании не только нецелесообразно из-за частого изменения логики работы сервера, но также снижает точность теста, так как не нагружаются зависимости, которые в ином случае подверглись бы воздействию».
Несмотря на то, что новые проекты могут разрабатываться так, чтобы побочные эффекты были сведены к минимуму, предотвращены или даже полностью устранены, применение стабов в готовой инфраструктуре может принести больше проблем, чем пользы.
Сетчатая архитектура сервиса в какой-то мере может помочь с этим. При использовании архитектуры service mesh сервисы ничего не знают о топологии сети и ждут подключений на локальном узле. Всё взаимодействие между сервисами осуществляется через дополнительный прокси-сервер. Если каждый переход между узлами осуществляется через прокси-сервер, то архитектура, описанная выше, будет выглядеть следующим образом:
Если мы тестируем сервис B, его исходящий прокси-сервер можно настроить на добавление специального заголовка
X-ServiceB-Test
в каждый тестовый запрос. При этом входящий прокси-сервер вышестоящего сервиса C сможет:- Обнаружить этот заголовок и отправить стандартный ответ сервису B;
- Сообщить сервису C, что запрос является тестовым.
Интеграционное тестирование взаимодействия развёрнутой версии сервиса B с выпущенной версией сервиса C, где операции записи никогда не достигают базы данных
Выполнение интеграционного тестирования таким способом также обеспечивает тестирование взаимодействия сервиса B с вышестоящими сервисами, когда они обрабатывают обычные данные продакшен-среды, — вероятно, это более близкая имитация того, как будет вести себя сервис B при релизе в продакшен.
Также было бы неплохо, если бы каждый сервис в этой архитектуре поддерживал реальные вызовы API в тестовом или макетном режиме, позволяя тестировать выполнение контрактов сервиса с нижестоящими сервисами без изменения реальных данных. Это было бы равноценно контрактному тестированию, но на уровне сети.
Теневое дублирование данных (тестирование тёмного потока данных или зеркалирование)
Теневое дублирование (в статье из блога Google оно называется тёмным запуском, а в Istio применяется термин зеркалирование) во многих случаях имеет больше преимуществ, чем интеграционное тестирование.
В «Принципах хаотичного проектирования» (Principles of Chaos Engineering) говорится следующее:
«Системы ведут себя по-разному в зависимости от среды и схемы передачи данных. Так как режим использования может измениться в любое время, выборка реальных данных является единственным надёжным способом зафиксировать путь запросов».
Теневое дублирование данных представляет собой метод, с помощью которого поток данных продакшен-среды, поступающий в данный сервис, захватывается и воспроизводится в новой развёрнутой версии сервиса. Этот процесс может выполняться либо в режиме реального времени, когда входящий поток данных разделяется и направляется как на выпущенную, так и на развёрнутую версии сервиса, либо асинхронно, когда копия ранее захваченных данных воспроизводится в развёрнутом сервисе.
Когда я работала в imgix (стартап со штатом из 7 инженеров, из которых только четверо были системными инженерами), тёмные потоки данных активно использовались для тестирования изменений в нашей инфраструктуре визуализации изображений. Мы регистрировали определённый процент всех входящих запросов и отправляли их в кластер Kafka — передавали журналы доступа HAProxy на конвейер heka, который в свою очередь передавал проанализированный поток запросов в кластер Kafka. Перед стадией релиза новая версия нашего приложения для обработки изображений тестировалась на захваченном тёмном потоке данных — это позволяло убедиться, что запросы обрабатываются правильно. Однако наша система визуализации изображений была по большому счету stateless-сервисом, который особенно хорошо подходил для этого вида тестирования.
Некоторые компании предпочитают захватывать не часть потока данных, а передавать новой версии приложения полную копию этого потока. Маршрутизатор McRouter компании Facebook (memcached прокси-сервер) поддерживает этот вид теневого дублирования потока memcache-данных.
«Во время тестирования новой инсталляции для кеша мы обнаружили, что очень удобно иметь возможность перенаправлять полную копию потока данных от клиентов. McRouter поддерживает гибкую настройку теневого дублирования. Можно выполнять теневое дублирование пула различных размеров (рехешировав пространство ключей), копировать только часть пространства ключей или динамически менять параметры в процессе работы».
Отрицательный аспект теневого дублирования всего потока данных для развёрнутого сервиса в продакшен-среде заключается в том, что если оно выполняется в момент максимальной интенсивности передачи данных, то для него может понадобиться в два раза большая мощность.
Такие прокси-серверы, как Envoy, поддерживают теневое дублирование потока данных на другой кластер в режиме fire-and-forget. В его документации написано:
«Маршрутизатор может выполнять теневое дублирование потока данных с одного кластера на другой. В настоящий момент реализован режим fire-and-forget, при котором прокси-сервер Envoy не ждёт ответа от теневого кластера перед возвратом ответа от основного кластера. Для теневого кластера собираются все обычные статистические данные, что полезно для целей тестирования. При теневом дублировании в заголовок host/authority добавляется параметр
-shadow
. Это полезно для ведения журналов. Например, cluster1
превращается в cluster1-shadow
».Однако зачастую нецелесообразно или невозможно создать синхронизированную с продакшеном реплику кластера для тестирования (по той же причине, по которой проблематично организовать синхронизированный стейджинг-кластер). Если теневое дублирование используется для тестирования нового развёрнутого сервиса, имеющего множество зависимостей, оно может инициировать непредусмотренные изменения состояния вышестоящих по отношению к тестируемому сервисов. Теневое дублирование суточного объёма пользовательских регистраций в развёрнутой версии сервиса с записью в производственную базу данных может привести к повышению частоты ошибок до 100% из-за того, что теневой поток данных будет восприниматься как повторные попытки регистрации и отклоняться.
Мои личный опыт говорит о том, что теневое дублирование лучше всего подходит для тестирования неидемпотентных запросов или stateless-сервисов со стабами серверной части. В этом случае теневое дублирование данных чаще применяется для тестирования нагрузки, устойчивости и конфигураций. При этом с помощью интеграционного тестирования или стейджинга можно протестировать, как сервис взаимодействует со stateful-сервером при работе с неидемпотентными запросами.
TAP-сравнение
Единственное упоминание этого термина есть в статье из блога Twitter, посвященной запуску сервисов с высоким уровнем качества предоставления услуг.
«Для проверки правильности новой реализации существующей системы мы использовали метод под названием tap-сравнение. Наш инструмент tap-сравнения воспроизводит образец данных продакшена в новой системе и сравнивает полученные ответы с результатами старой. Полученные результаты помогли нам найти и исправить ошибки в системе ещё до того, как с ними столкнулись конечные пользователи».
В другой статье из блога Twitter даётся такое определение tap-сравнения:
«Отправка запросов в экземпляры сервиса как в продакшен-, так и в стейджинг-средах с проверкой правильности результатов и оценкой характеристик производительности».
Различие между tap-сравнением и теневым дублированием заключается в том, что в первом случае ответ, возвращаемый выпущенной версией, сравнивается с ответом, возвращаемым развёрнутой версией, а во втором – запрос дублируется в развёрнутую версию в автономном режиме по типу fire-and-forget.
Ещё один инструмент для работы в этой области — библиотека scientist, доступная на GitHub. Этот инструмент был разработан для тестирования кода на языке Ruby, но затем был портирован на несколько других языков. Он полезен для некоторых видов тестирования, но имеет ряд нерешённых проблем. Вот что написал разработчик c GitHub в одном профессиональном Slack-сообществе:
«Этот инструмент просто выполняет две ветви кода и сравнивает результаты. Следует быть осторожнее с кодом этих ветвей. Нужно следить, чтобы не дублировались запросы к базе данных, если это приведёт к проблемам. Думаю, что это применимо не только к scientist, но и к любой ситуации, при которой вы что-то делаете дважды, а затем сравниваете результаты. Инструмент scientist создавался для проверки того, что новая система разрешений работает так же, как и старая, и в определённые моменты применялся для сравнения данных, характерных фактически для каждого запроса Rails. Думаю, что процесс будет занимать больше времени, так как обработка выполняется последовательно, но это проблема Ruby, в котором не используются потоки.
В большинстве известных мне случаев инструмент scientist применялся для работы с операциями чтения, а не записи, например, чтобы узнать, получают ли новые улучшенные запросы и схемы разрешений тот же ответ, что и старые. Оба варианта выполняются в продакшен-среде (на репликах). Если у тестируемых ресурсов есть побочные эффекты, я полагаю, тестирование придётся проводить на уровне приложения».
Diffy — это написанный на языке Scala инструмент с открытым исходным кодом, который компания Twitter представила в 2015 году. Статья из блога Twitter под названием Testing without Writing Tests («Тестирование без написания тестов») является, наверное, лучшим ресурсом, позволяющим понять, как tap-сравнение работает на практике.
«Diffy обнаруживает потенциальные ошибки в сервисе, параллельно запуская новый и старый вариант кода. Этот инструмент работает как прокси-сервер и отправляет все получаемые запросы каждому из запущенных экземпляров. Затем он сравнивает ответы экземпляров и сообщает обо всех отклонениях, обнаруженных в ходе сравнения. Diffy основывается на следующей идее: если две реализации сервиса возвращают одинаковые ответы при достаточно большом и разнообразном наборе запросов, то эти две реализации можно считать эквивалентными, а более новую из них — не имеющей ухудшений в работе. Новаторская методика подавления помех Diffy выделяет его на фоне других инструментов сравнительного регрессионного анализа».
Tap-сравнение прекрасно подходит, когда нужно проверить, дают ли две версии одинаковые результаты. По словам Марка Макбрайда (Mark McBride),
«инструмент Diffy часто использовался при перепроектировании систем. В нашем случае мы разделяли базу исходного кода Rails на несколько сервисов, созданных с помощью Scala, при этом большое количество клиентов API пользовались функциями не так, как мы ожидали. Функции типа форматирования даты были особенно опасны».
Tap-сравнение – не лучший вариант для тестирования активности пользователей или идентичности поведения двух версий сервиса при максимальной нагрузке. Как и при теневом дублировании, побочные эффекты остаются нерешённой проблемой, особенно в том случае, когда и развёрнутая версия, и версия в продакшене записывают данные в одну и ту же базу данных. Как и в случае с интеграционным тестированием, одним из способов обойти эту проблему является использование тестов tap-сравнения только с ограниченным набором учётных записей.
Нагрузочное тестирование
Для тех, кто не знаком с нагрузочным тестированием, эта статья может послужить хорошей отправной точкой. В инструментах и платформах нагрузочного тестирования с открытым исходным кодом нет недостатка. Самые популярные из них —Apache Bench, Gatling, wrk2, Tsung, написанный на языке Erlang, Siege, Iago от Twitter, написанный на Scala (который воспроизводит журналы HTTP-сервера, прокси-сервера или сетевого анализатора пакетов в тестовом экземпляре). Некоторые специалисты считают, что лучший инструмент для генерации нагрузки — mzbench, который поддерживает самые разные протоколы, включая MySQL, Postgres, Cassandra, MongoDB, TCP и т. д. NDBench от Netflix — ещё один инструмент с открытым исходным кодом для нагрузочного тестирования хранилищ данных, который поддерживает большую часть известных протоколов.
В официальном блоге Twitter, посвящённом Iago, более подробно описывается, какими характеристиками должен обладать хороший генератор нагрузки:
«Неблокирующие запросы генерируются с заданной частотой на основе внутреннего настраиваемого статистического распределения (по умолчанию моделируется процесс Пуассона). Частоту запросов можно изменять нужным образом, например, для подготовки кеша перед работой при полной нагрузке.
В целом основное внимание уделяется частоте поступления запросов в соответствии с законом Литтла, а не числу параллельных пользователей, которое может меняться в зависимости от величины задержки, присущей данному сервису. За счёт этого появляются новые возможности для сравнения результатов нескольких тестов и предотвращения ухудшений в работе сервиса, замедляющих работу генератора нагрузки.
Иными словами, инструмент Iago стремится смоделировать систему, в которой запросы поступают независимо от способности вашего сервиса обрабатывать их. Этим он отличается от генераторов нагрузки, моделирующих закрытые системы, в которых пользователи будут терпеливо работать с имеющейся задержкой. Это отличие позволяет нам довольно точно моделировать режимы отказа, с которыми можно столкнуться в продакшене».
Другой вид нагрузочного тестирования — стресс-тестирование путём перераспределения потока данных. Его суть в следующем: весь поток данных продакшен-среды направляется в более маленький кластер, чем тот, который подготовлен для сервиса; если при этом возникают проблемы, поток данных передаётся назад на кластер большего размера. Эта методика используется компанией Facebook, о чём говорится в одной из статей её официального блога:
«Мы специально перенаправляем больший поток данных на отдельные кластеры или узлы, измеряем потребление ресурсов на этих узлах и определяем границы устойчивости сервиса. Этот вид тестирования, в частности, полезен для определения ресурсов CPU, необходимых для поддержки максимального количества одновременных трансляций Facebook Live».
Вот что пишет бывший инженер LinkedIn в профессиональном Slack-сообществе:
«В LinkedIn также применялись redline-тесты в продакшене — из балансировщика нагрузки убирались серверы до тех пор, пока нагрузка не достигала пороговых значений или не начинали возникать ошибки».
Действительно, поиск в Google даёт ссылку на полный технический документ и статью в блоге LinkedIn на эту тему:
«В решении Redliner для измерений используется реальный поток данных продакшен-среды, что позволяет избежать ошибок, мешающих точно измерить производительность в лабораторных условиях.
Redliner перенаправляет часть потока данных в тестируемый сервис и в режиме реального времени анализирует его производительность. Это решение было внедрено в сотнях внутренних сервисов LinkedIn и ежедневно используется для различных видов анализа производительности.
Redliner поддерживает параллельное выполнение тестов для канареечных и рабочих экземпляров. Это позволяет инженерам передавать один и тот же объём данных в два разных экземпляра сервиса: 1) экземпляр сервиса, который содержит нововведения, например новые конфигурации, свойства или новый код; 2) экземпляр сервиса текущей рабочей версии.
Результаты нагрузочного тестирования учитываются при принятии решений и позволяют предотвратить развёртывание кода, который может привести к ухудшению производительности».
Facebook вывел нагрузочное тестирование с использованием реальных потоков данных на совершенно новый уровень благодаря системе Kraken, и с её описанием тоже стоит ознакомиться.
Тестирование реализуется посредством перераспределения потока данных при изменении весовых значений (считываемых из распределённого хранилища конфигураций) для пограничных устройств и кластеров в конфигурации Proxygen (балансировщик нагрузки Facebook). Эти значения определяют объёмы реальных данных, направляемых соответственно в каждый кластер и регион в данной точке присутствия.
Данные из технического документа Kraken
Система мониторинга (Gorilla) выводит показатели различных сервисов (как показано в таблице выше). На основании данных мониторинга и пороговых величин принимается решение о том, следует ли дальше направлять данные в соответствии с весовыми значениями, либо необходимо снизить или даже полностью прекратить передачу данных в конкретный кластер.
Конфигурационные тесты
Новая волна инфраструктурных инструментов с открытым исходным кодом сделала фиксирование всех изменений в инфраструктуре в виде кода не только возможным, но и сравнительно лёгким. Также в той или иной степени стало возможно тестировать эти изменения, хотя большинство тестов инфраструктуры-как-кода на этапе пре-продакшена могут подтвердить только правильность спецификаций и синтаксиса.
При этом отказ от тестирования новой конфигурации перед релизом кода становился причиной значительного числа перебоев в работе.
Для целостного тестирования конфигурационных изменений важно провести различия между разными видами конфигураций. Фред Хеберт (Fred Hebert) однажды предложил использовать следующий квадрант:
Этот вариант, конечно, не является универсальным, но такое разграничение позволяет решить, как лучше всего тестировать каждую из конфигураций и на каком этапе это делать. Конфигурация времени сборки имеет смысл, если можно обеспечить реальную повторяемость сборок. Не все конфигурации являются статическими, а на современных платформах динамическое изменение конфигурации неизбежно (даже если мы имеем дело с «постоянной инфраструктурой»).
Тестируя конфигурационные изменения с помощью таких методов, как интеграционное тестирование, теневое дублирование пикового потока данных и blue-green развёртывание, можно существенно снизить риски при развёртывании новых конфигураций. Джейми Уилкинсон (Jamie Wilkinson), инженер Google по надёжности, пишет:
«К обновлению конфигурации необходимо относиться с не меньшим вниманием, чем к коду, главным образом потому, что юнит-тестирование конфигураций трудно проводить без компилятора. Нужно проводить интеграционные тесты. Необходимо выполнять канареечные и стейджинг-развёртывания конфигурационных изменений, потому что единственное подходящее место для их тестирования — стадия продакшена, когда реальные пользователи инициируют выполнение ветвей кода, активированных вашей конфигурацией. Глобальное синхронизированное изменение конфигурации эквивалентно сбою в работе.
По этой причине функции следует развёртывать в отключенном виде и постепенно выпускать конфигурации, включающие их. Конфигурацию необходимо упаковать так же, как и бинарные файлы, — в компактную герметичную оболочку».
В статье на Facebook двухлетней давности можно найти полезную информацию об управлениях рисками при развёртывании изменений конфигурации:
«Системы конфигурации разработаны в расчёте на быструю репликацию изменений в глобальном масштабе. Быстрое изменение конфигурации — мощный инструмент, с помощью которого инженеры могут оперативно управлять запуском новых продуктов и регулировать их настройки. Однако быстрое изменение конфигурации также означает быстрый отказ в случае развёртывания с ошибками. Ряд практических методик позволяет предотвратить ошибки и сбои, которые могут быть вызваны изменением конфигурации.
- Использование общей системы конфигурации
При использовании общей системы конфигурации процедуры и инструменты применяются ко всем типам конфигурации. Сотрудники Facebook выяснили, что в отдельных случаях специалисты предпочитают обрабатывать различные конфигурации отдельно. Стремление избежать такого соблазна и осуществить унифицированное управление конфигурациями привело к созданию системы, которая помогла повысить надёжность сайта. - Статическая проверка изменений конфигурации
Многие системы конфигурации поддерживают конфигурации со слабой типизацией (например, структуры JSON). В этих случаях разработчик легко может сделать опечатку в имени поля, применить строковый тип данных вместо целочисленного или допустить другую простую ошибку. Для поиска ошибок такого типа лучше всего использовать статическую валидацию.
Структурированный формат (например, на Facebook используется Thrift) обеспечивает базовые возможности для её выполнения. Тем не менее, имеет смысл написать дополнительные программы для валидации более детально определённых требований. - Канареечное развёртывание
Рекомендуется сначала развернуть конфигурацию изолированно на незначительной части сервиса, чтобы предотвратить повсеместное распространение ошибок, которые могут возникать из-за изменений. Канареечное развёртывание может быть реализовано несколькими способами. Наиболее очевидный — A/B-тестирование, такое как запуск новой конфигурации только для 1 % пользователей. Можно одновременно проводить несколько A/B-тестов, а также использовать динамические данные для отслеживания показателей. Однако в плане повышения надёжности A/B-тесты не решают всех задач. Очевидно, что изменение, развёрнутое для небольшого числа пользователей и вызвавшее сбой на задействованных серверах или занявшее весь их объём памяти, оказывает влияние не только на тех пользователей, которые непосредственно участвуют в тесте. Кроме того, A/B-тесты отнимают слишком много времени. Инженеры зачастую хотят применять незначительные изменения, не прибегая к A/B-тестам. По этой причине инфраструктура Facebook автоматически тестирует новые конфигурации на небольшом наборе серверов.
Например, если требуется развернуть новый A/B-тест для 1% пользователей, то этот 1% пользователей будет выбран таким образом, чтобы для их обслуживания задействовалось как можно меньше серверов (этот приём называется «фиксированное канареечное развёртывание»). Мы отслеживаем работу данных серверов в течение короткого периода времени, чтобы убедиться в отсутствии сбоев или иных очевидных проблем. Такой механизм позволяет выполнить базовую проверку работоспособности в отношении всех изменений и убедиться в том, что они не вызовут глобальный сбой. - Сохранение корректных конфигураций
Система конфигурации Facebook ориентирована на сохранение корректных конфигураций в случае сбоя в процессе их обновления. Разработчики обычно стремятся создавать такие системы конфигурации, которые прекращают работу при получении недопустимых обновлённых конфигураций. Мы же предпочитаем системы, которые в подобной ситуации сохраняют предыдущую работоспособную версию и оповещают оператора системы о том, что конфигурацию не удалось обновить. Использование устаревшей конфигурации, как правило, более предпочтительно по сравнению с отображением сообщения об ошибке для пользователей. - Простая и удобная отмена изменений
В отдельных случаях, несмотря на все профилактические меры, выполняется развёртывание неработоспособной конфигурации. Быстрый поиск и откат изменений имеет критически важное значение для решения подобной проблемы. В нашей системе конфигурации доступны инструменты управления версиями, которые существенно упрощают отмену изменений».
Продолжение следует!
UPD: продолжение здесь.