Как воспроизвести условия ошибки и посмотреть, что происходит во время выполнения приложения?
Как вы визуализируете взаимодействия между различными параллельными частями приложения?
Исходя из моего опыта, ответ на эти два аспекта заключается в следующем:
Распределенная трассировка
Распределенная трассировка - это технология, которая собирает данные о времени для каждого параллельного компонента вашей системы и представляет их вам в графическом формате. Представления о параллельных выполнениях всегда чередуются, что позволяет вам увидеть, что работает параллельно, а что нет.
Распределенная трассировка обязана своим происхождением (конечно) распределенным системам, которые по определению являются асинхронными и в высокой степени параллельными. Распределенная система с распределенной трассировкой позволяет людям:
а) выявить важные узкие места, б) получить визуальное представление об идеальных «прогонах» вашего приложения, и в) предоставить представление о том, какое параллельное поведение выполняется, г) получить данные о времени, которые можно использовать для оценки различий между изменениями в вашем приложении. система (чрезвычайно важно, если у вас есть сильные соглашения об уровне обслуживания).
Последствия распределенного отслеживания, однако, следующие:
Это добавляет накладные расходы ко всем вашим параллельным процессам, поскольку переводит в дополнительный код для выполнения и отправки потенциально по сети. В некоторых случаях эти издержки очень значительны - даже Google использует свою систему отслеживания Dapper только для небольшого подмножества всех запросов, чтобы не испортить взаимодействие с пользователем.
Существует много различных инструментов, не все из которых могут взаимодействовать друг с другом. Это несколько улучшается такими стандартами, как OpenTracing, но не полностью решено.
Он ничего не говорит об общих ресурсах и их текущем состоянии. Вы можете догадаться, исходя из кода приложения и того, что показывает график, но это не очень полезный инструмент в этом отношении.
Текущие инструменты предполагают, что у вас есть память и хранилище. Размещение сервера временных рядов может быть недешевым, в зависимости от ваших ограничений.
Программное обеспечение для отслеживания ошибок
Я ссылаюсь на Sentry выше, прежде всего потому, что это наиболее широко используемый инструмент, и по понятной причине - программное обеспечение для отслеживания ошибок, такое как Sentry Hijack Runtime Execution, для одновременной пересылки трассировки стека ошибок, обнаруженных на центральном сервере.
Чистая выгода такого специализированного программного обеспечения в параллельном коде:
- Дублирующиеся ошибки не дублируются . Другими словами, если одна или несколько одновременно работающих систем сталкиваются с одним и тем же исключением, Sentry увеличивает отчет об инциденте, но не отправляет две копии инцидента.
Это означает, что вы можете выяснить, какая параллельная система испытывает какую ошибку, без необходимости проходить бесчисленные одновременные отчеты об ошибках. Если вы когда-либо сталкивались со спамом в электронной почте из распределенной системы, вы знаете, что чувствует ад
Вы даже можете «пометить» различные аспекты вашей параллельной системы (хотя это предполагает, что у вас нет чередования работы над одним потоком, что технически не является параллельным в любом случае, так как поток просто переходит между задачами эффективно, но все равно должен обрабатывать обработчики событий до завершения) и увидеть разбивку ошибок по тегам.
- Вы можете изменить это программное обеспечение для обработки ошибок, чтобы предоставить дополнительную информацию с вашими исключениями времени выполнения. Какие открытые ресурсы были у процесса? Есть ли общий ресурс, который этот процесс держал? У какого пользователя возникла эта проблема?
Это, в дополнение к тщательным трассировкам стека (и исходным картам, если вам нужно предоставить минимизированную версию ваших файлов), позволяет легко определить, что происходит в большинстве случаев.
- (Для Sentry). У вас может быть отдельная панель мониторинга отчетов Sentry для тестовых запусков системы, что позволяет вам обнаруживать ошибки при тестировании.
К недостаткам такого программного обеспечения относятся:
Как и все, они добавляют объем. Например, вам может не понадобиться такая система на встроенном оборудовании. Я настоятельно рекомендую выполнить пробный запуск такого программного обеспечения, сравнив простое выполнение с несколькими выборками из бездействующего компьютера и без него.
Не все языки поддерживаются одинаково, так как многие из этих систем полагаются на неявный перехват исключения, и не все языки имеют надежные исключения. При этом есть клиенты для множества систем.
Они могут рассматриваться как угроза безопасности, так как многие из этих систем по сути являются закрытыми. В таких случаях проявите должную осмотрительность, исследуя их, или, если хотите, сверните свои собственные.
Они не всегда могут дать вам необходимую информацию. Это риск со всеми попытками добавить видимость.
Большинство этих сервисов были разработаны для веб-приложений с высокой степенью параллелизма, поэтому не каждый инструмент может быть идеальным для вашего случая использования.
В итоге : наличие видимости является наиболее важной частью любой параллельной системы. Два метода, которые я описал выше, в сочетании со специальными информационными панелями об оборудовании и данных, чтобы получить целостное представление о системе в любой момент времени, широко используются в отрасли именно для решения этого аспекта.
Некоторые дополнительные предложения
Я потратил больше времени, чем забочусь на исправление кода людьми, которые пытались решить параллельные проблемы ужасными способами. Каждый раз я обнаруживал случаи, когда следующие вещи могли бы значительно улучшить опыт разработчика (что так же важно, как и опыт пользователя):
Положитесь на типы . Типизирование существует для проверки вашего кода и может использоваться во время выполнения в качестве дополнительной защиты. Там, где типизация не существует, полагайтесь на утверждения и подходящий обработчик ошибок для выявления ошибок. Параллельный код требует защитного кода, а типы служат наилучшим доступным видом проверки.
- Тестируйте ссылки между компонентами кода , а не только самим компонентом. Не путайте это с полнофункциональным интеграционным тестом, который проверяет каждую ссылку между каждым компонентом, и даже тогда он ищет только глобальную проверку конечного состояния. Это ужасный способ отловить ошибки.
Хороший тест ссылки проверяет, если один компонент говорит с другим компонентом изолированно , полученное и отправленное сообщение совпадают, как вы ожидаете. Если у вас есть два или более компонента, которые полагаются на общую службу для связи, раскрутите их все, попросите их обмениваться сообщениями через центральную службу и посмотреть, все ли они получают то, что вы ожидаете, в конце концов.
Разбиение тестов, включающих множество компонентов, на тестирование самих компонентов и тестирование взаимодействия каждого из компонентов дает вам повышенную уверенность в достоверности вашего кода. Наличие такого строгого набора тестов позволяет принудительно выполнять контракты между службами, а также обнаруживать неожиданные ошибки, возникающие при их одновременном запуске.
- Используйте правильные алгоритмы для проверки состояния вашего приложения. Я говорю о простых вещах, таких как, когда у вас есть мастер-процесс, ожидающий, пока все его работники завершат задачу, и вы хотите перейти к следующему шагу, только если все работники полностью выполнены - это пример обнаружения глобального завершение, для которого существуют известные методологии, такие как алгоритм Safra.
Некоторые из этих инструментов поставляются в комплекте с языками - Rust, например, гарантирует, что ваш код не будет иметь условий гонки во время компиляции, в то время как Go имеет встроенный детектор взаимоблокировок, который также работает во время компиляции. Если вы можете поймать проблемы до того, как они попадут в производство, это всегда победа.
Общее практическое правило: проектирование на отказ в параллельных системах . Ожидайте, что общие службы будут аварийно завершаться или ломаться. Это относится даже к коду, который не распределяется по компьютерам - одновременный код на одном компьютере может зависеть от внешних зависимостей (таких как общий файл журнала, сервер Redis, проклятый сервер MySQL), которые могут исчезнуть или быть удалены в любое время ,
Лучший способ сделать это - периодически проверять состояние приложения - проверять работоспособность каждого сервиса и следить за тем, чтобы потребители этого сервиса были уведомлены о плохом состоянии. Современные контейнерные инструменты, такие как Docker, делают это довольно хорошо, и их следует использовать для песочницы.
Как вы понимаете, что можно сделать параллельным, а что можно сделать последовательным?
Один из самых больших уроков, которые я усвоил, работая над высококонкурентной системой, заключается в следующем: у вас никогда не будет достаточно метрик . Метрики должны определять абсолютно все в вашем приложении - вы не инженер, если не все измеряете.
Без метрик вы не сможете сделать несколько очень важных вещей:
Оцените разницу, внесенную изменениями в системе. Если вы не знаете, что при настройке ручки A метрика B повышается, а метрика C снижается, вы не знаете, как исправить вашу систему, когда люди вставляют неожиданно злонамеренный код в вашу систему (и они передают код в вашу систему) ,
Поймите, что вам нужно делать дальше, чтобы улучшить ситуацию. Пока вы не узнаете, что приложениям не хватает памяти, вы не сможете понять, следует ли вам получить больше памяти или купить больше дисков для своих серверов.
Метрики настолько важны и важны, что я сознательно попытался спланировать то, что я хочу измерить, прежде чем даже думать о том, что потребуется системе. На самом деле, метрики настолько важны, что я считаю, что они являются правильным ответом на этот вопрос: вы знаете только то, что может быть сделано последовательным или параллельным, когда вы измеряете, что делают биты в вашей программе. Правильный дизайн использует числа, а не догадки.
При этом, безусловно, есть несколько практических правил:
Последовательность подразумевает зависимость. Два процесса должны быть последовательными, если один каким-то образом зависит от другого. Процессы без зависимостей должны быть параллельными. Тем не менее, спланируйте способ обработки сбоя в восходящем потоке, который не мешает процессам, находящимся в нисходящем направлении, ожидать бесконечно.
Никогда не смешивайте задачу, связанную с вводом / выводом, с задачей, связанной с ЦП, в том же ядре. Не (например) не пишите веб-сканер, который запускает десять одновременных запросов в одном потоке, очищает их, как только они поступают, и не рассчитывает на масштабирование до пятисот - запросы ввода-вывода идут в очередь параллельно, но процессор все равно будет проходить через них последовательно. (Эта однопотоковая модель, управляемая событиями, является популярной, но она ограничена из-за этого аспекта - вместо того, чтобы понять это, люди просто складывают руки и говорят, что Node не масштабируется, чтобы дать вам пример).
Один поток может выполнять много операций ввода-вывода. Но чтобы полностью использовать параллелизм вашего оборудования, используйте пулы потоков, которые вместе занимают все ядра. В приведенном выше примере запуск пяти процессов Python (каждый из которых может использовать ядро на шестиядерной машине) только для работы с процессором, а шестой поток Python только для работы ввода-вывода будет масштабироваться гораздо быстрее, чем вы думаете.
Единственный способ использовать преимущества параллелизма ЦП - выделенный пул потоков. Один поток часто достаточно хорош для большого количества операций ввода-вывода. Вот почему управляемые событиями веб-серверы, такие как Nginx, масштабируются лучше (они выполняют работу, связанную исключительно с вводом / выводом), чем Apache (который связывает работу, связанную с вводом / выводом, с чем-то, требующим ЦП, и запускает процесс по запросу), но зачем использовать Node для выполнения десятки тысяч вычислений на GPU, полученных параллельно, - ужасная идея.