Надежный код при высоких нагрузках

в 11:44, , рубрики: badoo, fault tolerance, highload, php, Блог компании Badoo, Веб-разработка, метки: , , ,

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

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

«High load» — что это?

Разные люди вкладывают в термин «высокие нагрузки» (англ. high load) различный смысл. Обычно все-таки подразумевают большое количество запросов в секунду. Это очень относительный критерий, ведь для многих сайтов даже скромные 100 запросов в секунду — это уже высокая нагрузка. Её может создавать не только количество запросов, но и их качество. Некоторые запросы могут быть очень «тяжелыми» с вычислительной точки зрения.

На сайт Badoo приходит более 40 000 запросов в секунду на PHP-FPM, поэтому для нас вопросы, связанные с высокими нагрузками, являются более чем актуальными.

Высокие нагрузки = высокая надежность?

Представим себе, что у нас есть «обычный сайт» с 10 000 хитов в сутки. Если в коде такого сайта есть ошибка, которая затрагивает 0,05% запросов, то она будет проявляться 5 раз в день. Скорее всего, 5 записей в error.log за сутки будут просто пропущены и не замечены.

Представим себе тот же код, работающий в условиях высоких нагрузок в Badoo. Это в 100 000 раз больше хитов, чем в предыдущем примере. Мы будем получать 5-10 сообщений об ошибках в секунду, что очень сложно проигнорировать. Если же ошибка будет касаться 1% пользователей, то сообщений будет уже в 20 раз больше — мы будем получать сотни сообщений в секунду. К тому же мы получим тысячи недовольных посетителей сайта, у которых что-то не работает, и большое количество обращений в нашу службу поддержки.

Пишем «пуленепробиваемый» код

Важно сообщать об ошибках

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

Сам по себе характер проблемы не принципиален: даже если у вас возникает раз в 10 000 обращений тайм-аут при соединении с базой данных, это тоже может быть симптомом какой-нибудь серьёзной сетевой ошибки, которая — пока — редко проявляется.

Проверяйте результат выполнения любых операций

Существуют языки вроде Java, в которых любая обработка ошибок сделана в виде исключений, так что случайно пропустить ошибку чрезвычайно сложно. В языке PHP «by design» исключений не было, поэтому стандартные PHP-функции просто возвращают false в случае ошибки. Как бы то ни было, в любом коде, в котором ошибки критичны, нужно проверять результат выполнения всех команд. Ведь даже fclose или SQL-запрос COMMIT могут вернуть ошибку.

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

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

Проверяйте контрольные суммы данных

В крупных проектах данные обычно сильно денормализованы для увеличения производительности, поэтому могут возникать расхождения в данных. Для выявления этих проблем нужно делать проверки целостности данных в фоновом режиме.

Например, если у вас есть какие-либо счётчики, которые хранятся отдельно от самих данных, можно время от времени проверять, что количество строк в базе данных сходится с этим счётчиком. Если цифры не совпадают, то такие значения стоит автоматически исправлять.

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

Надежность записи данных в модели «eventual consistency»

Если существует репликация БД, то часто не требуется так называемой «strong consistency», когда на всех серверах находятся строго одинаковые данные. Если вас устраивает, что на различных серверах могут быть данные различной «степени свежести», то в таких случаях написание надежного кода сильно упрощается. Вам всего лишь нужно гарантировать, что вы записали данные на N серверов, после чего данные на остальных серверах будут обновлены, как только до них дойдет очередь.

В простейшем случае надежное сохранение данных в модели «eventual consistency» выглядит как первичное надежное (например, в транзакции) сохранение данных в очередь репликации. После этого другие скрипты могут сделать неограниченное число попыток считать эти данные и положить на другие сервера, пока это не увенчается успехом.

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

«Помни о смерти»

Этот раздел частично перекликается с разделом про проверку всех кодов ответа.
Когда у вас тысячи серверов, вероятность получить ошибки даже в очень надежных сервисах сильно возрастает. Например, Kernel panic после 7 месяцев аптайма (англ. uptime) в ядре Linux, с которым нам «посчастливилось» столкнуться, являлся причиной многих «падений» в наших внутренних сервисах.

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

Одним словом, помните о смерти, когда вы пишете код, который будет работать под высокой нагрузкой.

Почему эти рекомендации работают?

Мы рассказали о неприятных последствиях, которые могут возникнуть, если не соблюдать описанные выше советы. Каким образом наши рекомендации помогают легкой отладке и написанию надежного кода в целом?

Давайте рассмотрим по пунктам.

Сообщения об ошибках

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

Проверка результатов выполнения операций

Как вы вообще узнаете о том, что в процессе выполнения программы были ошибки? Правильно — проверяя результат выполнения всех операций. Если весь код написан в таком стиле (а для любой уважающей себя программы это так), то даже если случайно пропустить несколько проверок, ошибка всё равно будет выявлена. Если при выполнении последующих операций что-нибудь не сойдется, вы это успешно отловите (поскольку всё проверяете), и пропущенная проверка не приведет к фатальным последствиям.

Проверка контрольных сумм

В веб-окружении контрольные суммы обычно не проверяются «на лету» (для уменьшения времени отклика), но такие проверки обязательно должны проходить в фоновом режиме. Для CLI-скриптов, как правило, нет жёстких требований ко времени работы, поэтому в CLI-скриптах или демонах вполне можно позволить себе проводить подсчёт контрольных сумм во время исполнения программы.

Наличие проверок целостности данных позволяет выявить ошибки синхронизации между сервисами (например, сходится ли количество строк в базе данных со значением соответствующего счётчика). Такие проверки нужны при совершении любых операций, которые в теории могут завершиться неудачей. Проверять целостность данных нужно и в других случаях, когда цена ошибки слишком высока.

Проверки данных могут не ограничиваться подсчётом числа строк. Например, в наших скриптах, которые осуществляют развёртывание кода на конечные машины, перед вызовом

system("rm -rf " . escapeshellarg($dir))

делается проверка на длину $dir. Если путь слишком короткий, значит, закралась ошибка, и выполнять удаление ни в коем случае нельзя.

Итого

Приведенные выше рекомендации очевидны, но на практике редко используются при написании программ. Надеемся, наша статья убедила вас, что «high load» — это одна из тех сфер, где качественный код и хорошая архитектура имеют исключительное значение.

Помните о наших рекомендациях, следуйте им при написании высоконагруженных систем, и ваши волосы будут мягкими и шелковистыми :).

Источник картинки: vbgcity.ru/sites/default/files/krepost-oreshek.jpg

Юрий Насретдинов, разработчик Badoo

Автор: youROCK

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js