company_banner

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

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

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


    «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
    Badoo
    403.00
    Big Dating
    Share post

    Comments 42

      +1
      Возможно не в тему вопрос.
      Не могли бы вы порекомендовать книги (или какие-либо другие ресурсы) по highload-у для начинающих?
        +6
        Не уверен, что есть такие книги, ибо каких-то специфических знаний для работы с highload обычно не требуется. Намного более важную роль играет квалификация разработчика и опыт работы. Ну а опыт работы сложно чем-то заменить, ИМХО.
          +1
          Это да, но не помешал бы place to start — набор каких-то базовых принципов. Ну например проверка целостности данных при highload-е невозможна средствами СУБД, т.к. сами данные разнесены по нескольким серверам. Разбиение данных по спотам и т.п. Насколько я понимаю это базовые принципы построения горизонтально масштабированных систем, но в первый раз они могут показаться совсем неочевидными
          Я к примеру прочитал множество разрозненной информации в блоках, документации, просмотрел видео с конференций, но все равно осталось чувство, что я возможно что-то упустил
            +1
            см "три кита" общая вводная статья — много воды… но основы
              0
              Хороший сайт, спасибо за ссылку
              +3
              Тот же Badoo часто проводит на различных конференциях мастер-классы по хай-лоад. Проводит Алексей Рыбак. Я был в этом году на PHPConf, превосходный семинар (на весь день почти). Очень рекомендую посетить при возможности — не пожалеете нисколько. На самом деле крутая инфа, разжёванно и понятно.
            +2
            highload — это на 90% масштабирование, то есть возможность ускорить приложение, добавив больше серверов.
            Например есть у тебя сервер с тормозящей базой данных. Ты ставишь рядом вторую, переносишь часть данных (шардинг) или делаешь полную копию данных (репликация) на втором сервере — и твоя систем начинает работать быстрее.
            Или например есть у тебя тормозящий сервер обрабатывающий HTTP-запросы юзеров — ставишь рядом второй такой же и пускаешь туда половину юзеров — и твоя система ускоряется.
            В книге такое сложно описать потому что сложно придумать какую-то универсальную систему и дьявол скрывается в деталях
              –1
              Да, скорее всего поэтому мне и не удалось найти подобной книги.
              Не могли бы вы ответить на два вопроса:
              1) Каким образом у пользователей отображается количество непрочитанных сообщений? При посылке нового сообщения увеличивается соответствующий счетчик у пользователя, которому оно отправляется или кол-во каким-то образом высчитывается на лету?
              2) В статьях рассматриваются варианты хранения сессии пользователя. В основном предлагается выделить под нее отдельный сервер. Не будет ли этот сервер SPOF (single point of failure) и общим узким местом в системе (ведь данные из него потребуются всем остальным серверам)? Как у вас решается такой вопрос?
                0
                1. да, счетчик в базе
                2. ну конечно же серверов можно (и нужно) ставить несколько. Например завести 5-10 серверов с memcache или redis или mysql handler socket или что-то похожее и рапределять все сессии между этими серверами.
                  0
                  Если я правильно понял, то данные о сессиях хранятся не на всех серверах сразу, а распределены по ним. Как сервер, которому понадобилась информация о сессии, узнает к какому серверу сессии ему обращаться? Есть некоторая настроечная информация о связке {пользователь-сервер} и она хранится на всех серверах?
                  Что будет если один из этих серверов упадет? Данные о сессиях потеряются или они дублируются в базе?

                  Заранее спасибо за ответы

                    0
                    На твои вопросы нет однозначных ответов. Есть варианты из которых надо выбирать
                    Например некоторые стартапы настраивают свои лоад-балансеры так чтобы один и тот же юзер всегда попадал на один и тот же сервер и сессии хранят прямо на этом сервере в памяти приложения (особенно это любят java/scala разработчики)
                    > Как сервер, которому понадобилась информация о сессии, узнает к какому серверу сессии ему обращаться?
                    можно закодировать id сервера в session_id
                    >Что будет если один из этих серверов упадет? Данные о сессиях потеряются или они дублируются в базе?Зависит от того что выберете и как напишите. Если сессии в памяти хрянились (мемкеш) — то юзерам прийдется заново авторизоваться. Если redis / mysql — то ждать пока они перезапустятся. В mysql можно настроить репликацию
                    Вобщем вариантов много
                      0
                      >> один и тот же юзер всегда попадал на один и тот же сервер
                      И при падении этого сервера некоторый % пользователей 100% не смогут попасть на сайт пока балансер не выкинет упавший backend?
                      При равномерном распределении хотя бы" животворящий F5" сможет хоть как-то на это время разгрести затруднительную ситуацию.

                      А вот по сессиям, хранящимся в репликации, интересный вопрос появляется, когда сервера находятся в разных датацентрах в разных странах и появляется неприятное отставание.
                        0
                        И при падении этого сервера некоторый % пользователей 100% не смогут попасть на сайт пока балансер не выкинет упавший backend?

                        Это одна из причин для нас так не делать :).

                        А вот по сессиям, хранящимся в репликации, интересный вопрос появляется, когда сервера находятся в разных датацентрах в разных странах и появляется неприятное отставание.

                        Что касается Badoo, то датацентр, в который попадает пользователь, у нас однозначно определяется из его IP (по стране).

                        То есть, если (не дай бог) один из наших двух датацентров «ляжет», то для половины пользователей ничего не будет показываться. Но репликация данных в другой датацентр у нас есть, поэтому данные потеряны будут только те, которые ещё не успели отреплицироваться.
                          0
                          а как настроить географически распределенную репликацию?
                            +1
                            На мастер-классе Алексея Рыбака об этом рассказывается более подробно :). Я пересказывать всё не хочу и не могу, но смысл в том, что у нас своя репликация для БД. Она сделана очередях на простых SQL-запросах и PHP-скриптах, которые по крону перегоняют записи в другой ДЦ.
                              0
                              ладно, я сам спрошу у Лёши
                              коробочное решение не подходит?
                                0
                                Насколько я знаю, когда это писалось (году этак в 2006), не было никакого «коробочного решения» :).
                                  0
                                  спасибо за информацию.
                          0
                          И при падении этого сервера некоторый % пользователей 100% не смогут попасть на сайт пока балансер не выкинет упавший backend?
                          Я не пробовал этот метод на практике. Скорее всего да.
                          А вот по сессиям, хранящимся в репликации, интересный вопрос появляется, когда сервера находятся в разных датацентрах в разных странах и появляется неприятное отставание.

                          Если речь идет именно о сессии, то она нужно только в том датацентре, которым сейчас пользуется данный юзер. В других датацентрах она попросту не нужна. Бывают редкие случаи, когда юзера на средиректить в другой датацентр. В этом случае после редиректа можно запросить один раз сессию из старого датацентра и сохранить в новом. Запрос в другой датацентр — операция конечно медденная, но не смертельно. Тем более что ситуация такая возникает у малого кол-ва людей
                            0
                            >И при падении этого сервера некоторый % пользователей 100% не смогут попасть на сайт пока балансер не выкинет упавший backend?

                            У Badoo, может быть, теряются такие запросы, но вообще nginx позволяет при получении ошибки с апстрима обратиться к следующему.
                            nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
                              0
                              backend-ы падают по-разному
                              Если backend не принимает запросы, то тут все просто — выбираем следующий backend
                              Если же backend принял запрос, но ответил ошибкой, то не факт что запрос можно отправить на следующий backend. Например backend принял запрос на отсылку сообщения пользователю, сохранил сообщение в базе, но потом по какой-то причине упал например в core dump уже после коммита в базу. Если такой запрос переадресовать на следующий бекенд — в базу сохраняться 2 сообщения.
                                0
                                Вообще говоря, это вопрос выбора: либо мы допускаем потерю пользовательских запросов (что для всяких ajax'ов может быть болезненно), либо теоретическую возможность удвоения некритичных запросов. При этом первый трудно обнулить на уровне приложения, а последний вполне реально.

                                В вашем примере, скорее всего, отсылка сообщения идёт отложенно или через внешний для fpm-а сервис. В обоих случаях после всяческих проверок будет простая отсылка сообщения в 1 сервис, сразу после которой выдача ответа клиенту. Так что вероятность ошибки в промежутке между сохранением и отдачей ответа пренебрежимо мала, тогда как вероятность проблем при старте, работе с внешними сервисами итп вполне осязаема.
                                  0
                                  Я привет гипотетический пример который может случиться. Возможно что его и можно решить на уровне приложения, но в условиях дедлайна мало кто будет писать _php_-код из расчета на то что php-процесс может умереть в любой момент.
                                  PHP — не самый качественный язык. Иногда php-процессы падают в корку в самых разных местах (eacellerator, debug_backtrace, array_map, различные экстеншены не входящие в стандартный php)
                                  У нас даже небольшой скрипт написали который собирает корки со всех машин и в веб-интерфейсе показывает trace из gdb :)
                                  Но в целом согласен — это вопрос выбора.
                    0
                    «Release It!» — highly recommended
                    +5
                    По-моему много воды в статье, вроде начинали про код, закончили всем остальными узлами хайлоада, а каждый вопрос требует большой отдельной статьи если писать для новичков, а статья видимо на них направлена.

                    Какой-то интересной конкретики не увидел.
                      +1
                      Не волнуйтесь, отдельные статьи тоже будут, со временем :). Считайте это вводной статьей.
                        +1
                        ждем статей и новых решений от Badoo
                          +3
                          Считаем это «водной» статьёй ;)
                        +14
                        Код должен быть надежным, потому что должен быть хорошим. Хорошим он должен быть, чтобы быть надежным. Ошибки надо видеть и исправлять. Небо синее, вода мокрая, зима холодная.

                        :) А по существу что-нибудь?
                          +1
                          Я согласен, этот список рекомендаций абсолютно очевиден для любого человека с опытом работы в более-менее большом проекте :). Но такой опыт есть не у всех, поэтому, я надеюсь, статья кому-то была полезна.

                          В следующих статьях будут подробности, я гарантирую это :).
                            0
                            к сожалению, для тех, у кого нет опыта — это и не будет полезным, т.к. пока не пощупаешь эти проблемы руками, не поймешь зачем же это все делается.
                          0
                          Статья про «Fragile Code», который на хайлоад проекте может привести к отказу в обслуживании.
                          Надо бы добавить про требования, пренебрежение которыми и приводит к написанию хрупкого кода, который всегда ломается.
                            0
                            Это все конечно хорошо, но было бы отлично с примерами и ближе к продакшну так сказать. Какие то вещи, по типу последнего с rm -rf добавьте в следующей статье, если таковая будет. Тем более мы все знаем, что у вас хорошо получаются технические статьи по вашему сервису;)
                              0
                              Очередная вводная статья. Если реально много практического опыта — вот он будет интересен, вдруг что новое проскочит.
                                0
                                Статья очень содержательная: ловите и фиксите ошибки, следите за таймаутами внешних запросов. Пример про число строк надуман. Рекомендация про контрольные суммы при дедуплицировании не требует отдельного внимание программистов: либо хранилища имеют явную зависимость (например, БД и кеш) и поддержка идёт на уровне базового кода проекта, либо консистентность должно поддерживать используемое ПО и немногочисленные сервисные скрипты. О самой мякотке надёжного распределённо исполняемого кода хайлоада — локах, транзакциях итп — ни слова.

                                Кстати, у вас вроде при вашем надёжном коде на этих выходных не работала авторизация через Facebook. :)
                                  0
                                  О самой мякотке надёжного распределённо исполняемого кода хайлоада — локах, транзакциях итп — ни слова.

                                  Как уже говорили выше, об этом всём на многих конференциях рассказывает Алексей Рыбак в виде мастер-класса на целый день (около 8 часов). Как вы сами понимаете, в рамках одной статьи я об этом рассказать даже при всём желании бы не смог :).

                                    0
                                    Про авторизацию через Facebook, к сожалению, не могу ничего сказать — исходя из наших графиков, на выходных всё было в пределах нормы. Скорее всего это не из-за ненадежного кода, а каких-то особенностей, связанных с вашим конкретным профилем, которые у нас не учтены. Помимо этого, вполне возможно, что испытываемые вами проблемы с авторизацией были по вине Facebook ;).
                                    0
                                    А мне вот интересна оптимизация SQL запросов. Мало где об этом пишут. Можно легко составить вложенный запрос со множеством join условий и сортировок. Но как и что там можно оптимизировать?! Было бы интересно почитать об этом.
                                      0
                                      На этот вопрос ответ очень простой: в веб-части мы не используем сложные SQL-запросы. Вложенные запросы запрещены, обычно встречается не более, чем 2-3 джойна. Если и эти запросы тормозят, то ставятся простые индексы, если и это не помогает, то EXPLAIN и несколько других методик :). Об оптимизации одного из таких запросов у нас есть статья.
                                      0
                                      Я бы еще добавил две техники, сервисы должны быть по возможности независимы и gracefull degradation которые когда-то подсмотрел у NetFlix. Вы можете упустить при выкатке новой версии медленный скрипт, или проблемы с оборудованием возникнут, нужно иметь возможность отключить затронутые проблемой службы. Еще эта техника полезна для неожиданных пиковых нагрузок, когда можно отключить все менее приоритетные службы чтобы сайт не лег под нагрузкой.
                                        +2
                                        мы обычно включаем новые услуги сначало в какой-то одной/нескольких странах. Смотрим статистику, реакцию юзера на новую фичу, дорабатываем, оптимизируем и постепенно добавляем страны. Выкатывать новую фичу сразу везде — безумие

                                      Only users with full accounts can post comments. Log in, please.