Содержание статьи
Понятное дело, что всем разработчикам, как людям техническим, хочется интересных и сложных задач и архитектур, но на старте важен баланс технологий и клиентских фич продукта. С одной стороны, надо как можно быстрее выкатывать новую функциональность, с другой — не оказаться в точке, когда ничего не работает из‑за сложных нагрузок и сделать ничего нельзя. Как бы авантюрно это ни звучало — за пять лет работы мы почти не видели, чтобы компания действительно оказывалась в такой ситуации, что сделать было уже ничего нельзя (за редкими исключениями). Зато видели множество случаев, когда компании тратили свое время на то, чтобы готовиться к жуткой посещаемости, до которой в итоге банально не доживали.
Как говаривал дедушка Кнут, преждевременная оптимизация — корень всех зол. И в данной статье мы хотим пройтись по трем видам преждевременной оптимизации, которые видим чаще всего в веб‑разработке. Это:
- преждевременная оптимизация работы с базой данных;
- преждевременная оптимизация всего приложения в целом;
- чрезмерное увлечение хипстерскими технологиями в надежде найти серебряную пулю.
Преждевременная оптимизация работы с базой данных
Мы часто видим, как стартапы ожидают, что с первого же дня основная нагрузка ляжет на базу данных, которая не сможет справиться с растущим объемом запросов и положит проект. В одной дружественной нам компании, которой, к сожалению, теперь уже нет, разработчики исходили из того, что гигантское количество данных будет уже на старте. И разработку проекта начали с планирования архитектуры, которая дала бы возможность легко и практично балансировать данные между любым количеством серверов. При этом схема учитывала возможность добавления новой шарды базы данных на лету.
Разработчики возвращались к этой подсистеме несколько раз на протяжении двух лет — она не была полностью готова, а разработчики, связанные с продуктовой частью, регулярно меняли схемы БД. В итоге проект был закрыт, а силы, которые могли бы быть брошены на разработку продуктовой части, были потрачены на создание подсистемы, которую никогда не пришлось использовать.
На деле — шардинг необходим как решение двух проблем:
- Операции с БД в условиях огромного объема данных.
- Масштабирование нагрузки на дисковую запись.
Отдельно хотелось бы отметить: в 99 из 100 случаев (и в условиях широкой доступности SSD в наши дни) задачи масштабирования записи возникают далеко не сразу. Чтобы контент создавался, пользователя надо заинтересовать. При этом большие объемы данных в вашей системе также не окажутся внезапно и сразу — почти наверняка у вас будет время на модификацию архитектуры в процессе работы системы так, чтобы она могла масштабироваться по записи.
Что может этому помешать и что действительно стоит сделать в начале? Мы скажем крамольную вещь: следует быть крайне осторожным с использованием любых абстракций доступа к базе данных. Да, ORM — это клево и удобно, но, когда речь зайдет о том, что запрос надо раскидать по двум разным местам, — вам придется докопаться до самых глубин используемого ORM, чтобы понять, как это реализовать. Мы часто видим, как, казалось бы, простая задача модификации в условиях генерирования SQL-запросов превращается в настоящий ад.
Вторая оптимизация с точки зрения программиста, которую можно сделать, — это сразу предусмотреть, что серверов может быть несколько, и в момент выбора данных допустить то, что SQL-запрос может быть выполнен на одном из нескольких серверов. То есть: у тебя есть объект доступа к БД, и у тебя есть SQL-запрос, который ты хочешь выполнить. По‑хорошему, в том месте, где ты выполняешь этот запрос к серверу, ты должен иметь возможность непосредственно выбрать сервер, к которому обращаешься.
И еще немного про ORM: в условиях высоких нагрузок не может получиться нормального разделения сфер влияния — программист программирует, а администратор БД администрирует БД. По сути, работа программиста должна уже исходить из специфики работы с БД, поскольку даже небольшие модификации (форсирование использования индексов, изменение запроса под специфику оптимизатора базы данных) могут дать огромный прирост производительности. Простота и доступность ORM изолируют всю эту специфику от разработчика и дают шанс сгенерировать безумный запрос, который для программиста будет выглядеть вполне нормальным.
На одном из проектов — сайте с вопросами и ответами, написанном на джанге, — мы видели, как список вопросов на протяжении какого‑то количества кода перед его получением был ограничен рядом критериев. С точки зрения программиста все выглядело более‑менее ОК — время от времени к объекту списка просто дополнялся новый критерий. В итоге же ORM джанги генерировал запрос на 25 group by, которые экспоненциально создавали нагрузку на базу по мере роста объема обрабатываемых данных. И если бы запрос был в виде простого SQL’я — еще оставался шанс подумать, как оптимизировать процедуру, но в этом случае все было сильно усложнено.
Напоследок про часть с оптимизацией БД. Как мы уже сказали, мы чаще видим нагрузку на чтение, чем на запись. Организовать большую нагрузку на запись — это отдельный успех :). А нагрузку на чтение можно балансировать вообще без особых изменений со стороны кода. Если мы готовы к тому, что во время выполнения запроса мы можем выбрать сервер, то, если нам надо балансировать чтение, мы можем просто создать n-ное количество slave-серверов. А при выполнении селекта рандомно выбирать один из серверов, на котором уже и выполнить этот селект. А запись производить только в один отдельный master.
Преждевременная оптимизация всего приложения в целом
Эта история начинается так же: одни наши друзья ожидали сто миллионов пользователей. Послушали доклад про то, как все устроено в Яндексе, и захотели сделать у себя так же. Решили, что nginx будет собирать веб‑страницы по шаблонам в XSLT/XML, описывающим общую структуру компонент на странице. При запросе nginx парсит файл, видит, какие используются компоненты, и по каждому компоненту обращается на бэкенд за его отрендеренной версией, передавая идентификатор сессии, чтобы сохранить состояние. А соответственно, бэкенд всей платформы понимает, как получать такие запросы и генерировать вывод компонента.
В итоге создание этого решения заняло больше года, привлеченные C-разработчики запросили предоплату, но так и не сделали модуль, который бы реализовывал данный функционал в nginx. Впрочем, он и не потребовался, так как после двух лет разработки проект пришлось свернуть.
В наш XXI век облачных технологий, где CPU-ресурсы масштабируются довольно дешево по вертикали, а балансировщики нагрузки включаются за очень короткое время, создавать заранее какой‑то специфический код для балансировки приложения, на наш взгляд, кажется лишней работой. Что вы действительно можете сделать, так это предусмотреть, чтобы сессии пользователей не были привязаны к конкретной веб‑машине, и после этого просто клонировать то количество веб‑инстансов, которое вам нужно в ожидании оптимизации кода.
Другая крайность, которую мы встречали в своей практике, — это усложнение работы со статическим содержимым, загружаемым пользователями. В расчете на высокую нагрузку разработчики создают поддержку кластера nginx-серверов с WebDAV-модулем. Каждый файл, загружаемый пользователем, сначала ложился на промежуточный сервер, а следом отправлялся на WebDAV-сервер, откуда позже отдавался nginx’ом. Информация о месте хранения этого файла сохранялась в базе. Реальная нагрузка на этот проект так и не пришла.
Действительно серьезной проблемой, на наш взгляд, для большинства проектов является лишь полное отсутствие какого‑либо принципа хранения файлов — когда все складывается в одну директорию. И когда в один прекрасный день ты понимаешь, что файлов стало под 70 тысяч и ext4 не дает тебе записывать новые, ты либо экстренно придумываешь более разумную схему распределения файлов, либо переезжаешь, к примеру на XFS. Во втором случае тебя уже ничто не спасет :).
Как мы предлагаем решать этот вопрос всем клиентам:
- Если есть разделение «горячих» и «холодных» данных по времени (свежие фотографии ВКонтакте пролистывают чаще, чем старые), а поток заливаемых данных примерно одинаков — при загрузке файлов храни файлы в директории, созданной из даты/времени момента загрузки. В зависимости от количества это может быть просто разбиение по дням, а может быть разбиение по часам. Опять же так будет проще удалять старые файлы при необходимости.
- Если разделения данных на свежие и не очень нет, то можно высчитывать два различных цифровых хеша от имени файла, а затем использовать их в некой структуре директорий, внутри которой уже складывать сам файл.
Но что делать, если мы боимся, что сервер не вывезет нагрузки на чтение? На первом этапе нас спасет lsyncd, который поможет распределить статику по нескольким отдельным серверам и тем самым распределить нагрузку на ее чтение. А так как статика везде одинаковая, то читать ее можно с любого сервера. При необходимости можно будет легко добавлять в схему дополнительные мощности, пока программисты наконец не придумают более тонкую схему. Но будь осторожен — помни, что нет ничего более постоянного, чем какой‑то временный костыль. От них обязательно нужно будет потом избавляться.
Про хипстеров и серебряную пулю
Не стоит сломя голову бросать проверенные временем и миллиардами RPS’ов решения ради новых модных фишечек, в надежде найти спасение от всех бед сразу. Зачастую оказывается, что даже если какие‑то сверхновые методики и технологии решают какие‑то из твоих текущих проблем, то при этом они добавляют изрядную долю новых, даже о потенциальной возможности которых ты мог никогда не задумываться. Взять, к примеру, те же NoSQL базы данных. В увлечении NoSQL как панацеей от проблем с БД на самом деле есть очень много подводных камней. Например, на большинстве NoSQL-решений синхронизация данных на диск — это глобальный лок, во время которого работа с базой данных крайне затруднительна. При использовании редиса, если находящиеся в нем данные тебе нужны, у тебя есть два выхода:
- Иметь на отдельной машине слейв, который будет периодически дампить данные.
- Отключить дамп на мастере, не делать слейв, постоянно молиться, что ничего не упадет.
По сути, две главных проблемы всех новых технологий следуют одна из другой:
- Сырость решения.
- Отсутствие базы знаний по существующим проблемам.
Если у тебя есть проблема с MySQL и Postgres, ты можешь написать про нее в гугле и почти наверняка найдешь миллион людей, которые встречались с подобной до тебя. Если же проблема связана с какой‑то свежей технологией, велик риск, что тебе придется связываться с разработчиком и вместе с ним разбираться, что к чему и почему она возникла. Мы сами однажды слишком увлеклись подобными вещами (и это произошло даже уже на относительно известном и долго разрабатываемом Openstack’е) — вместо привычных методов виртуализации хотелось иметь возможность управлять виртуальными машинами «как в амазоне». Для этого мы выбрали Openstack, с которым мучились в течение следующего месяца. Основная причина страданий — сырость и необходимость поддержки. Для того чтобы запустить виртуальную машину в Openstack, работают пять Python-демонов, взаимодействие между которыми идет через RabbitMQ, а код постоянно меняется. Понятное дело, что сломаться там может все что угодно, и оно ломается.
Не перегибай
С другой стороны, быть совсем уж ретроградом тоже опасно для жизни. Давайте вспомним Perl-разработчиков (мы совсем не хотим обидеть Perl-разработчиков), которые говорили, что Perl лучше чем PHP, Perl лучше Python, Perl лучше Ruby и так далее. И сейчас мы видим огромное количество Perl-разработчиков, рынок для которых постепенно закрывается. Как же быть? Главное — найди кого‑нибудь, кто прошел тернистый путь выбранной тобой технологии до тебя :). У очень многих людей есть много свободного времени, а стартапы, которые выделяют по два года на всякие технологические решения, всегда будут существовать. Выбрал хитрую технологию, про которую все пишут? Найди ребят, которые уже пробовали и помогут.