[Перевод] История моего стартапа: 500000 пользователей за 5 дней на стодолларовом сервере
Джонатан Зарра, разработчик GoChat, уж точно не совершит подобной ошибки снова. Он набрал миллион пользователей за 5 дней, создав приложение-чат для фанатов Pokémon GO. Из упомянутого выше материала можно узнать о том, что он общался с инвесторами на предмет монетизации и расширения приложения. Сразу после этого GoChat рухнул. Было потеряно множество пользователей и потрачены серьёзные средства. Идея-то ведь гениальная, а в результате — настоящее безобразие. Да, кстати, я, через несколько дней после написания, слегка отредактировал первоначальный вариант этой статьи, так как GoChat всё ещё жив в Google Play, и имеет уже более двух миллионов пользователей. Ожидается, что и его iOS версия скоро снова восстановится.
Зарре пришлось нелегко: ему нужно было оплачивать сервера, необходимые для поддержки миллиона активных пользователей. Он и не думал, что ему удастся привлечь такую аудиторию. Приложение он создал в лучших традициях MVP, отложив заботу о масштабировании на потом. В общем-то, можно сказать, что он изначально обрёк свой проект на провал. Когда проблемы архитектуры приложения проявились во всей красе, Зарра нанял программиста на Upwork для исправления множества проблем с производительностью. Тот заявил, что расходы на сервера находятся в районе $4000. На дворе — 2016-й, поэтому можно с уверенность предположить, что речь идёт не о покупке «железных» серверов. Эти $4000 — стоимость годовой или месячной аренды виртуальных серверов и плата за трафик.
Практически всю свою профессиональную жизнь я проектировал и создавал веб-платформы для сотен миллионов активных пользователей. И я могу сказать, что $4000 на сервера — это слишком много для миллиона чатеров. Даже для MVP. Это говорит о том, что серверная часть приложения плохо спроектирована. На самом деле, не так-то просто создать экономичную, и при этом масштабируемую систему для миллионов ежемесячных активных пользователей. Но это и не за пределами человеческих возможностей — использовать такую комбинацию ПО, которая позволит поддерживать серьёзную аудиторию на недорогих облачных серверах. Это стоит принимать во внимание, подбирая подходящие компоненты при создании MVP.
GoSnaps: 500000 пользователей за 5 дней на сервере за $100 в месяц
GoChat позволил игрокам в Pokémon GO общаться в чате, в официальном приложении они этого делать не могут. Я создал проект, GoSnaps, так же нацеленный на фанатов Pokémon GO. Это — мобильное приложение, которое позволяет пользователям делиться скриншотами и картинками с привязкой к карте. Это что-то вроде Instagram или Shapchat для Pokémon GO.
В первый день GoSnaps набрал 60000 пользователей. Во второй — их было уже 160 тысяч, на пятый день (момент написания этого материала) их оказалось уже полмиллиона. К этому моменту пользователи загрузили в систему порядка 200 тысяч картинок. В любой момент приложением одновременно пользуется порядка 1000 человек. Я создал подсистему распознавания изображений для автоматической проверки того, имеет ли загружаемое изображение какое-то отношение к Pokémon GO, а также — средства для изменения размера загружаемых картинок. Всё это работает на одном вполне обычном сервере из Google Cloud, аренда которого стоит $100 в месяц. Сюда добавляется недорогое хранилище Google Cloud Storage, в котором размещаются изображения. Речь идёт о сотне долларов в месяц, а не о тысячах. При этом всё отлично работает.
Сравнение GoChat и GoSnaps
Сравним GoChat и GoSnaps. Оба приложения, вероятно, выполняют множество запросов в секунду для того, чтобы показывать чаты или изображения в определённой области карты. Это — геопространственный поиск в базе данных (или в поисковой системе), выполняемый либо в пределах некоего многоугольника на карте, либо — проводимый для некоей точки, задаваемой широтой и долготой. Мы используем многоугольник, запросы выполняются каждый раз, когда пользователь перемещает карту. Подобные запросы создают серьёзную нагрузку на БД, особенно в комбинации с сортировкой или фильтрацией данных. GoSnaps приходится обрабатывать подобные поисковые запросы сотни раз в секунду. Вероятно, то же самое происходит и в недрах GoChat.
Особенность GoChat заключается в том, что приложению ежесекундно приходится вытаскивать из базы и рассылать пользователям множество сообщений чата. В материале про GoChat говорится о примерно 600 запросах в секунду для приложения в целом. Эти 600 запросов представляют собой комбинацию запросов, касающихся карты и сообщений чата. Сообщения имеют небольшой размер, работу с ними можно (или даже нужно) организовать через простые сокеты. Но сообщения появляются часто и их нужно распределять между множеством пользователей, находящихся в чате. С такой ситуацией вполне можно справиться, если программная часть решения организована правильно. Если же мы имеем дело с плохо спроектированным MVP-приложением, поддержка чата может оказаться непосильной задачей.
С другой стороны, в GoSnaps имеется множество изображений, которые каждую секунду загружают из хранилища и лайкают. Изображения сохраняются на сервере, так как даже старые картинки не теряют актуальности. В то же время, устаревшие чаты из GoChat уже никому не нужны. Так как файлы изображений хранятся в Google Cloud Storage, количество запрошенных файлов изображений меня, как разработчика, не беспокоит. Всем этим занимается Google Cloud, а в возможностях Google я уверен. Но запрошенные изображения с привязкой к карте — это уже то, что меня очень даже беспокоит.
В GoSnaps есть подсистема распознавания изображений, которая ищет шаблоны на загруженных пользователями картинках для того, чтобы проверить, имеют ли эти картинки отношение к Pokémon GO или нет. Кроме того, эта подсистема занимается изменением размера изображений и отправкой их в Cloud Storage. Всё это — ресурсоёмкие, в плане нагрузки на CPU и потребляемого трафика, операции. Эти действия гораздо тяжелее, чем распределение некоего количества небольших сообщений чата, но выполняются они реже.
Вышесказанное позволяет мне сделать вывод о том, что оба приложения очень похожи в плане сложности масштабирования. GoChat обрабатывает больше маленьких сообщений, в то время как GoSnaps работает с более крупными изображениями и выполняет более ресурсоёмкие серверные операции. Проектирование и архитектура этих двух приложений требуют, при почти одинаковой сложности, немного разного подхода.
Как создать масштабируемый MVP за 24 часа
GoSnaps создан как MVP, а не как профессиональный бизнес-продукт. Он был полностью готов за 24 часа. Я взял Node.js-шаблон для хакатонов и использовал базу данных MongoDB без каких-либо форм кэширования. Больше ничего в проекте не применяется: ни Redis, ни Varnish, ни замысловатые схемы Nginx. Приложение для iOS было написано на Objective-C, кое-какой код для работы с картами Apple Maps был взят из нашего основного приложения Unboxd. Как же мне удалось сделать GoSnaps масштабируемым? На самом деле — только потому, что я не ленился, следуя вредным канонам MVP.
Скажем, я рассматривал бы создание MVP исключительно как гонку на время, цель которой — как можно более быстрый выпуск работающего приложения, и при этом не обращал бы внимание на качество серверной части. Где, в таком случае, хранить изображения? Конечно, в базе данных, в MongoDB. Это не потребует дополнительных настроек, да и кода надо — всего ничего. Очень просто. В духе MVP. Как запрашивать из базы данных изображения из некоторой области на карте, у которых больше всего лайков? Достаточно выполнить обычный запрос к MongoDB, охватывающий весь объем хранящихся там картинок. Один запрос к одному набору данных в базе. Снова — MVP. Всё это уничтожило бы моё приложение и сделало бы невозможным пользоваться его функциями.
Взглянем на запрос, который мне пришлось бы выполнить для того, чтобы вытащить из базы вышеупомянутые изображения. Выглядел бы он примерно так: «найти все картинки, относящиеся к области на карте [A, B, C, D], исключая те, которые помечены как недопустимые, и те, которые всё ещё обрабатываются, отсортированные по количеству лайков, по тому, имеют ли они отношение к Pokémon GO, и отсортированные по новизне». На маленьком наборе данных такой запрос сработает отлично. Но если речь идёт о серьёзной нагрузке, подобные обращения к базе данных повалят всю систему. Это случится даже в том случае, если упростить вышеописанный запрос так, чтобы он включал в себя всего три условия и операции сортировки. Почему? Потому что такой подход — это не то, на что рассчитана база данных. Обращения к базе должны выполняться с использованием только одного индекса за раз, что невозможно в случае с геопространственными запросами. Если у приложения мало пользователей, подобная конструкция окажется вполне работоспособной. Но если приложение вдруг станет успешным, это его убьёт. Как, собственно, случилось с GoChat.
Что я сделал вместо этого? После выполнения ресурсоёмких операций по анализу изображений и изменению их размера, обработанные изображения загружаются в Google Cloud Storage. Благодаря этому мой сервер и база данных не испытывают нагрузки, связанной с выдачей пользователям изображений. Базе данных следует заботиться о данных, а не о картинках. Это, само по себе, позволяет серьёзно сэкономить на серверах.
С точки зрения организации базы данных, я разделил изображения на несколько наборов. Это — все изображения, наиболее лайкаемые, самые новые, самые новые подходящие под тематику Pokémon GO, и так далее. Когда пользователи добавляют новые изображения, лайкают их, помечают как недопустимые, код проверяет принадлежность изображений к одной из групп и действует соответствующим образом.
При таком подходе запросы выполняются к подготовленным наборам данных, вместо того, чтобы выполнять сложные обращения к одной огромной куче неструктурированных записей. Это — результат логического разбиения данных на несколько простых блоков. Ничего сложного. Но это позволило мне выполнять запросы только по геопространственным координатам с одной операцией сортировки, вместо сложного запроса, описанного выше. Проще говоря, такой подход максимально упрощает операции выборки данных.
Сколько времени я на все эти улучшения потратил? Часа 2–3, не больше. Почему я это сделал в первую очередь? Потому что я привык работать подобным образом. Я исхожу из того, что моё приложение ждёт успех. Я спать бы не смог, если моя разработка обрела бы популярность, а затем свалилась бы под нагрузкой только потому, что она плохо спроектирована. Я встроил в приложение минимально жизнеспособные принципы масштабирования. Это — разница между счастьем от успеха и безнадёгой. Это то, что, по моему мнению, следовало бы сделать частью идеологии MVP-приложений.
Выбор правильных средств для реализации MVP
Если бы я создал GoSnaps с использованием более медленной среды исполнения кода, или на основе неповоротливого фреймворка, мне понадобилось бы больше серверов. Если бы я использовал что-то вроде PHP с Symfony, или Python с Django, или Ruby on Rails, я либо целыми днями занимался бы ускорением медленных компонентов, либо только и знал бы, что добавлять сервера. Можете поверить: мне уже пришлось через это пройти. Эти языки и фреймворки отлично подходят для множества сценариев, но не для MVP с маленьким серверным бюджетом. Это так преимущественно из-за множества уровней кода, которые обычно используются для работы с данными из баз в программах и неоправданно раздутых функций фреймворков. Всё это слишком сильно нагружает сервера. Позвольте привести пример, показывающий, насколько много это, на самом деле, значит.
Как я уже сказал, серверная часть GoSnaps основана на Node.js. Эта платформа, в целом, быстра и эффективна. Я использовал Mongoose как ORM для того, чтобы упростить программную работу с MongoDB. Я никак не отношу себя к экспертам Mongoose, и я знаю, что эта библиотека обладает огромной кодовой базой. Таким образом над Mongoose я мысленно поставил большой вопросительный знак. Но да, мы же говорим об MVP. Однажды, совсем недавно, 4 процесса Node.js на нашем сервере потребляли примерно по 90% ресурсов CPU каждый, что для меня, с примерно тысячей одновременно работающих пользователей, неприемлемо. Я понял, что дело, скорее всего в том, что Mongoose что-то делает с полученными данными. Очевидно, мне просто нужно было включить функцию Mongoose lean () для того, чтобы вместо хитроумных объектов Mongoose получать обычные JSON-объекты. После этого изменения процессы Node.js стали нагружать сервер примерно на 5 — 10%. Простые решения, основанные на знании того, что, на самом деле делает код, очень важны. В моём случае это уменьшило нагрузку на 90%. А теперь представьте, что у нас — по-настоящему тяжёлая библиотека, вроде Symphony с Doctrine. Такой махине понадобилась бы пара многопроцессорных серверов только для того, чтобы работал её собственный код, даже учитывая то, что предполагается, что узким местом системы является база данных, а не программные механизмы.
Выбор экономичной и быстрой среды исполнения кода важен для масштабируемости, если только оплата серверов — для вашего проекта не проблема. Выбор языка программирования со множеством полезных доступных библиотек ещё более важен, так как MVP обычно нужно создать как можно быстрее. Node.js, Scala и Go — это среды и языки, которые удовлетворяют обоим условиям. У них есть и высокая производительность, и множество библиотек. Сами по себе языки вроде PHP или Java не обязательно медленны, но они обычно используются вместе с большими фреймворками и библиотеками, которые утяжеляют приложения. Эти языки хороши для чистой объектно-ориентированной разработки и отлично протестированного кода, но не для создания приложений, которые можно быстро и дёшево масштабировать. Не хочу затевать тут священную войну, поэтому просто позвольте мне сказать, что всё это — моё субъективное мнение, не подкреплённое глубоким и полным анализом всех деталей. Я, например, люблю Erlang, но никогда бы не использовал его для MVP-приложения, поэтому любые споры по данному вопросу считаю бессмысленными.
Мой предыдущий стартап Cloud Games
Несколько лет назад я стал одним из основателей проекта Cloud Games — платформы для издания HTML5-игр. Когда всё началось, мы были игровым B2C-сайтом, нацеленным на регион MENA. Мы приложили большие усилия по привлечению аудитории, и достигли, через несколько месяцев, миллиона ежемесячных активных пользователей (MAU, Monthly Active Users). В то время я использовал PHP, Symphony2, Doctrine и MongoDB в очень простой и экономичной конфигурации. Я работал в Spil Games с 200 миллионами MAU, там в то время использовали PHP и затем перешли на Erlang. После того, как Cloud Games достигла примерно 100000 MAU, мы столкнулись с перегрузкой серверов. Причина заключалась в Doctrine и MongoDB. Я правильно настроил MongoDB, индексы и запросы, но серверы едва справлялись с обработкой кода. Я использовал APC-кэш PHP и так далее, но без особых успехов.
Так как cloudgames.com был достаточно статичным сайтом, я смог перевести этот MVP-проект на Node.js с Redis буквально за несколько дней. Получилась похожая функциональность, но в другой среде. Это привело к мгновенному падению нагрузки на сервера примерно на 95%. Признаю, дело тут в том, что удалось избавиться от тяжёлых PHP-библиотек, а не в выборе среды исполнения приложений или языка. Но минимальная рабочая конфигурация Node.js гораздо более функциональна, нежели минимальная PHP-конфигурация. Особенно с учётом того, что и MongoDB, и интерфейсная часть проекта — это на 100% JavaScript, как и Node.js. PHP же, без фреймворков и библиотек, это просто один из множества языков программирования.
Нам нужна была такая вот легковесная конфигурация, так как мы были самостоятельно финансируемым стартапом на ранней стадии развития. Сегодня Cloud Games показывает хорошие результаты. Проект всё ещё основан на Node.js. Мы могли бы и не преуспеть, если бы использовали технологии, требующие больших вложений, учитывая тот факт, что в жизни Cloud Games, как стартапа, были нелёгкие времена. Проектирование экономичной масштабируемой архитектуры стало одним из основных условий успеха.
Итоги: MVP и масштабируемость могут сосуществовать
Если у вашего приложения есть шанс экспоненциального роста из-за взрывного интереса к нему или возможного освещения в СМИ, не забудьте рассмотреть масштабируемость как часть MVP-стратегии. Принципы минимального жизнеспособного продукта и масштабируемости могут сосуществовать. Нет ничего печальнее, чем создать успешное приложение и присутствовать при его провале, вызванном техническими проблемами. И у самого Pokémon GO было немало проблем, но проект это настолько уникальный и распиаренный, что это особого значения не имело. Маленькие стартапы не могут позволить себе такой роскоши. Выбор времени — это всё. Миллион пользователей GoChat и полмиллиона пользователей GoSnaps, вероятно, согласятся со мной.