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

12.03.2019

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

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

То, что произошло в течение следующих 72 часов, повергло организаторов в шок. На пустом полотне возникло это:

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

Как все происходило

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

Создатели

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

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

Довольно быстро они поняли, что работая в одиночку и размещая только один пиксель каждые 5-10 минут, создать что-нибудь значительное невозможно. Кто-нибудь обязательно испортит их работу. Чтобы создать что-то большее, они должны работать вместе.

И тогда кто-то предложил рисовать на сетке, на которой будет ясно видно, где нужно закрасить следующий пиксель, чтобы получилось связное изображение. Так в левой нижней части полотна появился Dickbutt – известный интернет-мем, плод подросткового чувства юмора. Он стал первым совместным произведением.

Но создатели не остановились на достигнутом. Они стали добавлять к Dickbutt различные элементы, раскрашивать его в разные цвета и даже попытались трансформировать его в Dickbutterfly . За этой глупой затеей скрывался намек на надвигающееся творческое цунами.

Однако это произошло не сразу. Создателей опьянила их власть. Рядом с Dickbutt появился покемон Charmander, у которого вместо лапы начал расти член, а затем еще два.

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

Стало понятно, что слишком большая свобода ведет к хаосу. Творчество нуждается в ограничениях в той же мере, в которой оно нуждается в свободе. Когда кто-то может поставить любой пиксель в любом месте, как это может не привести к беспределу?

Хранители

Данную проблему очень быстро решил другой тип пользователей – хранители. Они пришли с одной целью – завоевать весь мир.

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

Blue Corner не был одинок в своих стремлениях. На другой стороне полотна появилась другая группа — Red Corner (красный угол). Ее участники заявили, что они – приверженцы левых политических взглядов. Еще одна группа – Green Lattice (зеленая решетка) – занялась повсеместным вкраплением зеленых и белых пикселей. Она продемонстрировала высокую эффективность, так как ей требовалось закрашивать вдвое меньше пикселей, чем другим фракциям.

Хранители пошли на создателей в лобовую атаку. Charmander стал первым местом сражения. Обнаружив, что Blue Corner начал забивать покемона синими пикселями, создатели осознали угрозу и прекратили междоусобные войны.

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

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

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

Это стало поворотным моментом. Бессмысленные фракции превратились в защитников.

Но это был еще не конец

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

Создатели объединялись в группы, работающие над общим проектом. Они делились между собой стратегиями и шаблонами. Одной из наиболее успешных была группа, которая создала панель Windows 95 с кнопкой Start в углу.

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

Еще одна группа воссоздала картину Ван Гога «Звездная ночь».

Однако не все протекало гладко. Защитники, которые когда-то приветствовали создание произведений искусства, стали тиранами, диктующими моду. Они начали указывать, что можно создавать, а что – нет. Началось это незадолго до того, как создатели начали творить по своим правилам.

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

Бои между защитниками разгорались нешуточные. Twitch live-streamer подговаривали своих последователей атаковать Blue Corner и Purple. Строились планы сражений. Взывали к эмоциям.

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

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

С самого начала на полотне появлялись флаги различных стран. Они росли и натыкались друг на друга. Настоящая эпическая битва разразилась между флагами Германии и Франции. Стало ясно, что для освоения новых пространств необходим посредник.

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

Нужен был проныра, с которым каждый мог бы договориться.

Разрушители

На интернетовской площадке 4chan обратили внимание на то, что происходило на Reddit. И не смогли пройти мимо. Их пользователи выбрали самый близкий их сердцу цвет — черный. Они стали Пустотой.

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

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

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

И это было именно то, что требовалось. Оказавшись на грани исчезновения, все участники проекта объединились, чтобы бороться с Пустотой за сохранение своего искусство.

Но Пустоту было не так легко победить, потому что она была нужна. Необходимо было все уничтожить, чтобы из пепла возродилось новое искусство — лучшее. И без Пустоты это было невозможно.

Так Пустота стал катализатором создания крупнейшего произведения искусства.

За центральную часть полотна с самого начала шла упорная борьба. Создатели претендовали на эту территорию для своих произведений. Сначала они пытались это делать с помощью иконок. Затем скоординированной попыткой создать призму как на обложке альбома Pink Floyd «Обратная сторона Луны».

Но Пустота съела все. Одно за другим создаваемые произведения лишь разогревало ее хищный аппетит к хаосу.

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

И этой идей стал американский флаг.

В последний день проекта все собрались вместе, чтобы прогнать пустоту раз и навсегда. Из людей, которые в иной ситуации разорвали бы друг друга на части, – из сторонников и противников Трампа, из демократов и республиканцев, из американцев и европейцев была создана коалиция.

Они объединились, чтобы создать что-то вместе, в этом маленьком уголке интернета, доказав, что в эпоху, когда такое сотрудничество кажется невозможным, они все еще могут это сделать.

Древние были правы

Вскоре после этого эксперимент на Reddit закончился. Сегодня его сопровождает множество историй, рассказываемых на десятках чатов. Каждое произведение искусства, созданное в проекте, покрыли сотни новых, из которых лишь немногие остались на окончательном полотне.

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

Многие тысячелетия назад, когда человечество (реальное, а не только то, что на Reddit) еще находилось в зачаточном состоянии, индуистские философы предположили, что небеса состоят из трех конкурирующих, но необходимых, божеств: Брахмы-Создателя, Вишны-Хранителя и Шивы-Разрушителя.

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

Несколько дней проекта показали, что такой подход оказался пророческим. Самым невероятным образом Reddit доказал, что созидание требует наличия всех трех компонентов.

Финальное полотно

Facebook

Twitter

Pocket

Linkedin

Fb messenger

Нельзя сказать, чтобы 100% корпоративных шуток на День юмора были удачными и вовлекающими. В этом году администрация Reddit запустила Место – интерактивное графическое полотно размером 1000 на 1000 пикселей, и посвященный ему раздел. Предполагалось, что участники сообщества совместно разрисуют это полотно, как им понравится. Но в результате это переросло в битву за Место, местами переходящую в философское противостояние. Обычное упражнение в рисовании превратилось в захватывающий социальный эксперимент. Историю от начала и до конца задокументировал блог Sudoscript .

Правила работы Места были простыми. Каждый участник мог выбрать один пиксель из 16 цветов и поместить его в любое место полотна. Можно было размещать сколько угодно пикселей, но между каждым размещением нужно было подождать 5 минут. Через 72 часа эти очень простые правила привели к созданию удивительного коллективного полотна :

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

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

Создатели

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

Создатели сразу разглядели, какую силу и потенциал скрывают пиксели. Но работая поодиночке, они могли размещать один пиксель каждые 5 или 10 минут. Создание значимого рисунка заняло бы вечность. Чтобы нарисовать что-то, они должны были работать сообща.

Тогда кому-то пришла блестящая идея использовать для рисования сетку, которая накладывалась бы на рисунок и показывала, где должны располагаться следующие пиксели. Первым через этот эксперимент прошел известный мем англоязычного интернета Dickbutt. И жители Места принялись за работу: Dickbutt материализовался буквально за минуты в нижнем левом углу полотна. На Месте появилось первое создание коллективного творчества.

Затем, когда создатели слегка опьянели от возможностей, появился покемон Чармандер, у которого вместо ноги довольно скоро нарисовался член. И начался первый конфликт: некоторые создатели старательно пытались прибрать обидные рисунки, но другие настойчиво дорисовывали скабрезности.

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

Защитники

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

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

Прошло совсем немного времени, прежде чем фракции столкнулись с создателями. Чармандер стал одним из первых объектов сражения. Синий угол начал зарисовывать покемона синими пикселями, и Создатели переключились от “фаллических войн” (кто больше членов нарисует) к более серьезной угрозе. Они приняли бой, зарисовывая каждый синий пиксель своим. Но количественный перевес был не в их пользу.

Так что Создатели сдались на милость победителя и каким-то образом это задело чувства Синих. Среди них появились сомневающиеся в своей роли в мире Места. “Наша волна неизбежно накроет весь мир, от края до края, должны ли мы проявлять милость к другому искусству, с которым сталкиваемся”, – спросил один из группы.

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

Это был поворотный пункт. Бессмысленные цветные фракции стали полезными Защитниками.

Но это еще не счастливый конец

Наконец-то ненасытные цветные волны были остановлены и Создатели могли вернуться к творчеству. Рисунки становились все сложнее. Появились тексты, написанные пикселями.

Создатели объединялись в небольшие группы, создавая подразделы на Reddit, где можно было обсуждать черновики рисунков и стратегию. Одна из самых успешных групп нарисовала панель задач в стиле Windows 95. Другая зарисовывала Место сердечками.

Затем появился и Ван Гог.

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

Битвы между Защитниками становились все жестче. Один стример из Twitch призвал своих фоловеров атаковать Синих. Разрабатывались стратегии битв. Были даже провокации: поклонники одного цвета сами рисовали пиксели цвета противника у себя на территории, чтобы иметь оправдание для ответной атаки. Пока фракции воевали между собой, Создатели обнаружили, что места для новых рисунков не осталось.

Начали возникать флаги разных стран – при росте они неизбежно натыкались друг на друга. Например, на “ничейной” территории столкнулись флаги Германии и Франции.

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

Место нуждалось в злодее, против которого могли бы объединиться все остальные.

Разрушители

Пришла пустота.

Началось это с 4chan – самого известного имиджборда в мире. Населяющие его пранкеры заметили, что происходит на Reddit и не смогли пройти мимо. Они стали Пустотой.

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

Это был именно тот пинок под зад, которого не хватало Месту. Перед общей угрозой Создатели и Защитники снова объединились, чтобы спасти искусство. Но смысл Пустоты был не просто в разрушении, каким-то образом она дала начало новому, лучшему искусству.

К примеру, место в центре было одним из самых оспариваемых среди создателей. И когда оно почернело, Защитники поняли, что придется придумать идею получше, которая вовлекла бы достаточно последователей, чтобы сразиться с черным монстром. Одной из таких идей стал флаг США.

В последний день существования Места в нем сформировалась самая невероятная коалиция, призванная сражаться с Пустотой – там были поклонники Трампа и противники Трампа, республиканцы и демократы, американцы и европейцы.

Вскоре эксперимент Reddit завершился. На финальном полотне не было ни единого раситского рисунка, ни одного символа ненависти.

Twitter

Pocket

Linkedin

Fb messenger

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

Первого апреля Reddit запустил проект Place («Место») - страницу с пустым холстом, на котором каждый пользователь форума мог нарисовать любое изображение. У художников были ограничения: рисовать можно только один пиксель одного из 16 цветов раз в пять минут, размеры холста тоже ограничены. Поверх нарисованных пикселей можно рисовать другие (а затем первый автор может снова нарисовать свой пиксель поверх пикселя соперника), из-за чего авторы изображений априори конфликтуют. Как указывается в описании холста, проект рассчитан на совместное творчество - «Каждый из вас может создать нечто индивидуальное. Вместе вы можете создать нечто большее».

В момент старта проекта все желающие буквально накинулись на холст - каждый пиксель на нём был заполнен. Поначалу пользователи просто тыкали цветом в свободные пиксели, но потом начали образовываться команды, которые стали рисовать одну из простейших возможных идей, находящую единомышленников, - государственные флаги. Чем больше над холстом работали пользователи, тем более интересные и сложные идеи приходили им в голову. И «Место» превратилось из первоапрельского проекта в место, где пользователи всего мира объединяются в настоящие сообщества, чтобы показать что-то миру и поддерживать своё творение от групп рейдеров, которые на самом деле просто также хотят нарисовать какой-то собственный рисунок.

Place в первые часы работы

«Место» стало онлайн-доской для стикеров, но каждый из них создаётся с огромным трудом. К примеру, чтобы нарисовать и защитить от сквоттеров логотип Linux размером 48 х 68 пикселей, им должны одновременно заниматься 3 264 человека.

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

А другие пишут целые полотна текста, например, как эта группа фанатов «Звёздных войн», написавшая и поддерживающая известный монолог верховного канцлера Палпатина о ситхе Дарте Плэгасе из третьего эпизода космической саги.

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

Однако есть и те, кто до сих пор создаёт специальные треды, пытаясь призвать единомышленников, которые помогут нарисовать и оставить для будущих поколений что-то вроде… блюющего Рика Санчеза из мультфильма «Рик и Морти». Серьёзно? Это точно не боты?

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

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

Для начала было крайне важно определить требования к первоапрельскому проекту, потому что запустить его нужно было без «разгона», чтобы все пользователи Reddit сразу получили к нему доступ. Если бы он с самого начала не работал идеально, то вряд ли привлёк бы внимание большого количества людей.

    «Доска» должна быть размером 1000х1000 тайлов, чтобы выглядеть очень большой.

    Все клиенты должны быть синхронизированы и отображать единое состояние доски. Ведь если у разных пользователей будут разные версии, им будет трудно взаимодействовать.

    Нужно поддерживать как минимум 100 000 пользователей одновременно.

    Пользователи могут размещать по одному тайлу в пять минут. Поэтому необходимо поддерживать среднюю частоту обновления 100 000 тайлов в пять минут (333 обновления в секунду).

    Проект не должен негативно влиять на работу остальных частей и функций сайта (даже при условии высокого трафика на r/Place).

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

Бэкенд

Решения по реализации

Главной трудностью при создании бэкенда было синхронизировать отображение состояния доски для всех клиентов. Было решено сделать так, чтобы клиенты в реальном времени прослушивали события размещения тайлов и немедленно запрашивали состояние всей доски. Иметь немного устаревшее полное состояние допустимо в случае подписки на обновления до того, как это полное состояние было сгенерировано. Когда клиент получает полное состояние, он отображает все тайлы, которые получил во время ожидания; все последующие тайлы должны отображаться на доске сразу же по мере получения.


Чтобы эта схема работала, запрос полного состояния доски должен выполняться как можно быстрее. Сначала мы хотели хранить всю доску в одной строке в Cassandra , и чтобы каждый запрос просто считывал эту строку. Формат каждой колонки в этой строке был таким:


(x, y): {‘timestamp’: epochms, ‘author’: user_name, ‘color’: color}

Но поскольку доска содержит миллион тайлов, нам нужно было считывать миллион колонок. На нашем рабочем кластере это занимало до 30 секунд, что было неприемлемо и могло привести к чрезмерной нагрузке на Cassandra.


Тогда мы решили хранить всю доску в Redis. Взяли битовое поле на миллион четырёхбитовых чисел, каждое из которых могло кодировать четырёхбитный цвет, а координаты х и y определялись смещением (offset = x + 1000y) в битовом поле. Для получения полного состояния доски нужно было считать всё битовое поле.


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


Здесь показано, как мы хранили цвета в Redis на примере доски 2х2:



Мы переживали, что можем упереться в пропускную способность чтения в Redis. Если много клиентов одновременно подключались или обновлялись, то все они одновременно отправляли запросы на получение полного состояния доски. Поскольку доска представляла собой общее глобальное состояние, то очевидным решением было воспользоваться кешированием. Решили кешировать на уровне CDN (Fastly), потому что это было проще в реализации, да и кеш получался ближе всего к клиентам, что уменьшало время получения ответа.


Запросы полного состояния доски кешировались Fastly с тайм-аутом в секунду. Чтобы предотвратить большое количество запросов при истечении тайм-аута, мы воспользовались заголовком stale-while-revalidate . Fastly поддерживает около 33 POP, которые независимо друг от друга осуществляют кеширование, поэтому мы ожидали получать до 33 запросов полного состояния доски в секунду.


Для публикации обновлений для всех клиентов мы воспользовались своим вебсокет-сервисом . До этого мы успешно использовали его для обеспечения работы Reddit.Live с более чем 100 000 одновременных пользователей для уведомлений о личных сообщениях в Live и прочих фич. Сервис также был краеугольным камнем наших прошлых первоапрельских проектов - The Button и Robin. В случае с r/Place клиенты поддерживали вебсокет-подключения для получения обновлений о размещениях тайлов в реальном времени.

API

Получение полного состояния доски


Сначала запросы попадали в Fastly. Если в нём была действующая копия доски, то он немедленно её возвращал без обращения к серверам приложений Reddit. Если же нет или копия была слишком старой, то приложение Reddit считывало полную доску из Redis и возвращало её в Fastly, чтобы тот закешировал и вернул клиенту.




Обратите внимание, что частота запросов никогда не достигала 33 в секунду, то есть кеширование с помощью Fastly было очень эффективным средством защиты приложения Reddit от большинства запросов.



А когда запросы всё же доходили до приложения, то Redis отвечал очень быстро.

Отрисовка тайла


Этапы отрисовки тайла:

  1. Из Cassandra считывается временная метка последнего размещения пользователем тайла. Если это было менее пяти минут назад, то мы ничего не делаем, а пользователю возвращается ошибка.
  2. Подробности о тайле записываются в Redis и Cassandra.
  3. Текущее время записывается в Cassandra в качестве последнего размещения тайла пользователем.
  4. Вебсокет-сервис отправляет всем подключённым клиентам сообщение о новом тайле.

Чтобы соблюсти строгую консистентность, все записи и чтение в Cassandra выполнялись с помощью QUORUM консистентного уровня .


На самом деле, здесь у нас возникла гонка, из-за чего пользователи могли размещать за раз несколько тайлов. На этапах 1–3 не было блокировки, поэтому одновременные попытки отрисовки тайлов могли пройти проверку на первом этапе и быть отрисованы – на втором. Похоже, некоторые пользователи обнаружили этот баг (либо они использовали ботов, которые пренебрегали ограничением на частоту отправки запросов) – и в результате с его помощью было размещено около 15 000 тайлов (~0,09% от общего количества).


Частота запросов и время ответов, измеренные приложением Reddit:



Пиковая частота размещения тайлов составила почти 200 в секунду. Это ниже нашего расчётного предела в 333 тайла/с (среднее значение при условии, что 100 000 пользователей размещают свои тайлы раз в пять минут).


Получение подробностей по конкретному тайлу


При запросе конкретных тайлов данные считывались напрямую из Cassandra.


Частота запросов и время ответов, измеренные приложением Reddit:



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



Время ответа на эти запросы было довольно небольшим и держалось на одном уровне в течение всего существования проекта.

Вебсокеты

У нас нет отдельных метрик, показывающих, как r/Place повлиял на работу вебсокет-сервиса. Но мы можем прикинуть значения, сравнив данные до запуска проекта и после его завершения.


Общее количество подключений к вебсокет-сервису:



Базовая нагрузка до запуска r/Place была около 20 000 подключений, пик - 100 000 подключений. Так что на пике мы, вероятно, имели около 80 000 одновременно подключённых к r/Place пользователей.


Пропускная способность вебсокет-сервиса:



На пике нагрузки на r/Place вебсокет-сервис передавал более 4 Гбит/с (150 Мбит/с на каждый инстанс, всего 24 инстанса).

Фронтенд: веб- и мобильные клиенты

В процессе создания фронтенда для Place нам пришлось решать много сложных задач, связанных с кроссплатформенной разработкой. Мы хотели, чтобы проект работал одинаково на всех основных платформах, включая настольные ПК и мобильные устройства на iOS и Android.


Пользовательский интерфейс должен был выполнять три важные функции:

  1. Отображать состояние доски в реальном времени.
  2. Позволять пользователям взаимодействовать с доской.
  3. Работать на всех платформах, включая мобильные приложения.

Главным объектом интерфейса был канвас, и для него идеально подошёл Canvas API . Мы использовали элемент размером 1000х1000, а каждый тайл отрисовывали как одиночный пиксель.

Отрисовка канваса

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

  1. Рисовать существующее изображение в канвасе с помощью drawImage() .
  2. Рисовать формы с помощью разных методов отрисовки форм. Например, fillRect() заполняет прямоугольник каким-нибудь цветом.
  3. Конструировать объект ImageData и рисовать его в канвасе с помощью putImageData() .

Первый вариант нам не подошёл, потому что у нас не было доски в форме готового изображения. Оставались варианты 2 и 3. Проще всего было обновлять отдельные тайлы с помощью fillRect() : когда приходит обновление через вебсокет, просто рисуем прямоугольник размером 1х1 на позиции (x, y). В целом способ работал, но был не слишком удобен для отрисовки начального состояния доски. Метод putImageData() подходил гораздо лучше: мы могли определять цвет каждого пикселя в одном-единственном объекте ImageData и рисовать весь канвас за раз.

Отрисовка начального состояния доски

Использование putImageData() требует определения состояния доски в виде Uint8ClampedArray , где каждое значение - восьмибитное беззнаковое число в диапазоне от 0 до 255. Каждое значение представляет какой-то цветовой канал (красный, зелёный, синий, альфа), и для каждого пикселя нужно четыре элемента в массиве. Для канваса 2х2 необходим 16-байтный массив, в котором первые четыре байта представляют верхний левый пиксель канваса, а последние четыре - правый нижний.


Здесь показано, как пиксели канваса связаны со своими Uint8ClampedArray-представлениями:



Для канваса нашего проекта понадобился массив на четыре миллиона байтов - 4 Мб.


В бэкенде состояние доски хранится в виде четырёхбитного битового поля. Каждый цвет представлен числом от 0 до 15, что позволило нам упаковать два пикселя в каждый байт. Чтобы использовать это на клиентском устройстве, нужно сделать три вещи:

  1. Передать клиенту бинарные данные из нашего API.
  2. Распаковать данные.
  3. Преобразовать четырёхбитные цвета в 32-битные.

Для передачи бинарных данных мы использовали Fetch API в тех браузерах, которые его поддерживают. А в тех, которые не поддерживают, использовали XMLHttpRequest с responseType , имеющим значение “arraybuffer” .


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


Наконец, четырёхбитные цвета нужно было преобразовать в 32-битные.



Структура ImageData , которая нам понадобилась для использования putImageData() , требует, чтобы конечный результат был в виде Uint8ClampedArray с байтами, кодирующими цветовые каналы в очерёдности RGBA. Это означает, что нам нужно было осуществить ещё одну распаковку, разбивая каждый цвет на компонентные канальные байты и помещяя их в правильный индекс. Не слишком-то удобно выполнять четыре записи на каждый пиксель. Но к счастью, был ещё один вариант.


Объекты TypedArray по сути являются представлениями ArrayBuffer в виде массивов. Тут есть один нюанс: многочисленные инстансы TypedArray могут читать и писать в один и тот же инстанс ArrayBuffer . Вместо записи четырёх значений в восьмибитный массив мы можем записать одно значение в 32-битный! Используя Uint32Array для записи, мы смогли легко обновлять цвета тайлов, просто обновляя один индекс массива. Правда, пришлось сохранять нашу палитру цветов в обратном байтовом порядке (ABGR), чтобы байты автоматически попадали на правильные места при считывании с помощью Uint8ClampedArray .


Обработка обновлений, полученных через вебсокет

Метод drawRect() хорошо подходил для отрисовки обновлений по отдельным пикселям по мере их получения, но было одно слабое место: большие порции обновлений, приходящие одновременно, могли привести к торможению в браузерах. А мы понимали, что обновления состояния доски могут приходить очень часто, так что проблему нужно было как-то решать.


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

  1. Прекращение использования drawRect() – мы нашли удобный способ обновлять много пикселей за раз с помощью putImageData() .
  2. Перенос отрисовки канваса в цикл requestAnimationFrame.

Благодаря переносу отрисовки в анимационный цикл мы смогли немедленно записывать вебсокет-обновления в ArrayBuffer , при этом откладывая фактическую отрисовку. Все вебсокет-обновления, приходящие между фреймами (около 16 мс), объединялись в пакеты и отрисовывались одновременно. Благодаря использованию requestAnimationFrame , если бы отрисовка заняла слишком много времени (дольше 16 мс), то это повлияло бы только на частоту обновления канваса (а не ухудшило бы производительность всего браузера).

Взаимодействие с канвасом

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


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

Зум

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


Мы реализовали 40-кратный зум, то есть каждый тайл имел размер 40х40. Мы обернули элемент в

, к которому применили CSS transform: scale(40, 40) . Это было отличным решением для размещения тайлов, но затрудняло просмотр доски (особенно на маленьких экранах), поэтому мы сделали двухступенчатый зум: 40х - для рисования тайлов, 4х - для просмотра доски.


Использование CSS для масштабирования канваса позволило легко отделить код, отвечающий за отрисовку доски, от кода, отвечающего за масштабирование. Но у этого подхода оказалось несколько недостатков. При масштабировании картинки (канваса) браузеры по умолчанию применяют алгоритмы сглаживания изображений. В каких-то случаях это не доставляет неудобств, но пиксельную графику просто уничтожает, превращая её в мыльную кашу. Хорошая новость - есть CSS-свойство image-rendering , с помощью которого мы смогли «попросить» браузеры не применять сглаживание. Плохая новость - не все браузеры имеют полноценную поддержку этого свойства.


Размытие при зуме:



Для таких браузеров нужно было найти другой способ масштабирования. Выше я упоминал, что есть три способа рисования в канвасе. Первый, drawImage() , поддерживает отрисовку имеющегося изображения или другого канваса. Также он поддерживает масштабирование изображения при отрисовке (с увеличением или уменьшением). И хотя увеличение имеет те же проблемы с размытием, что и вышеупомянутый CSS, их можно решить более универсальным с точки зрения поддержки браузеров способом - сняв флаг CanvasRenderingContext2D.imageSmoothingEnabled .


Итак, мы решили проблему с размытием канваса, добавив ещё один этап в процесс рендеринга. Для этого мы сделали ещё один элемент , который по размеру и позиции совпадает с элементом-контейнером (то есть с видимой зоной доски). После перерисовки канваса с помощью drawImage() в новом канвасе рисуется видимая его часть в нужном масштабе. Поскольку этот дополнительный этап немного увеличивает стоимость рендеринга, мы использовали его только в браузерах, которые не поддерживают CSS-свойство image-rendering .

Перемещение по канвасу

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

, к которому применили CSS transform: translate(x, y) . Благодаря отдельному div’у мы смогли легко управлять порядком применения преобразований к канвасу, что было необходимо для предотвращения перемещения «камеры» при изменении зума.


В результате мы обеспечили поддержку разных способов настройки позиции «камеры»:

  • «Нажать и перетащить» (click-and-drag, или touch-to-drag);
  • «Нажать для перемещения» (click-to-move);
  • навигация с клавиатуры.

Каждый из этих методов реализован по-разному.

«Нажать и перетащить»

Это первичный способ навигации. Мы сохраняли координаты x и y события mousedown . Для каждого из таких событий мы находили смещение позиции курсора мыши относительно начальной позиции, а затем добавляли это смещение к имеющемуся смещению канваса. Сразу же обновлялась позиция камеры, так что навигация была очень отзывычивой.

«Нажать для перемещения»

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

Навигация с клавиатуры

Можно было перемещаться по канвасу с помощью клавиатурных стрелок или WASD. Эти клавиши управляли внутренним вектором движения. Если ни одна из клавиш не была нажата, то вектор по умолчанию имел координаты (0, 0). Нажатие любой из клавиш навигации добавляло 1 к x или y. Например, если нажать «вправо» и «вверх», то координаты вектора будут (1, -1). Затем этот вектор использовался внутри анимационного цикла для перемещения «камеры».


В процессе анимации скорость движения вычислялась в зависимости от уровня приближения по следующей формуле:


movementSpeed = maxZoom / currentZoom * speedMultiplier

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


Затем вектор движения нормализовывался, умножался на скорость движения и применялся к текущей позиции «камеры». Нормализация использовалась, чтобы скорость диагональных и ортогональных перемещений совпадала. Наконец, мы применили функцию плавности к изменениям самого вектора движения. Это сгладило изменения направления перемещения и скорости, так что «камера» двигалась гораздо плавнее .

Поддержка мобильных приложений

При встраивании канваса в iOS- и Android-приложения мы столкнулись с некоторыми сложностями. Во-первых, нам нужно было аутентифицировать пользователя, чтобы он мог размещать тайлы. В отличие от веб-версии, где аутентификация основана на сессии, в мобильных приложениях мы использовали OAuth: в этом случае приложения должны предоставлять залогиненному пользователю WebView с токеном доступа. Наиболее безопасно реализовать это можно с помощью внедрения авторизационных заголовков OAuth посредством JS-вызова из приложения к WebView. Это позволило бы нам при необходимости настроить другие заголовки. Затем нужно было просто парсить авторизационные заголовки при каждом вызове API:


r.place.injectHeaders({‘Authorization’: ‘Bearer ’});

В версии для iOS мы дополнительно реализовали поддержку уведомлений, когда тайл пользователя был готов к помещению в канвас. Поскольку размещение выполнялось полностью в WebView, нам пришлось реализовать колбэк нативного приложения. К счастью, в iOS 8 и выше это делается с помощью простого JS-вызова:


webkit.messageHandlers.tilePlacedHandler.postMessage(this.cooldown / 1000);

Затем метод делегата в приложении диспетчеризировал уведомления на основании переданного ему таймера перезарядки.


Что мы узнали

Всегда что-то упускаешь из виду

Мы всё идеально спланировали. Мы знали, когда будет запуск. Всё должно было пройти как по маслу. У нас были протестированные под нагрузкой фронтенд и бэкенд. Мы, люди, просто не могли совершить ещё какие-то ошибки. Верно?


Запуск действительно прошёл гладко. В течение утра по мере роста популярности r/Place увеличивалось количество подключений и возрастал трафик на инстансы вебсокетов:




Мы это предвидели. И готовились к тому, что в результате сеть станет узким местом в нашей системе. Но оказалось, что у нас есть большой запас. Однако, посмотрев на загрузку ЦПУ, мы увидели совсем другую картину:



Это восьмиядерные машины, так что было очевидно, что они достигли своего предела. Почему эти «коробки» повели себя так неожиданно? Мы решили, что генерируемая Place нагрузка по своему характеру сильно отличается от того, что было раньше. Кроме того, использовалось большое количество очень маленьких сообщений, в то время как обычно мы отправляем сообщения большего размера вроде обновления Live-тредов и уведомлений. Также, как правило, у нас нет такого количества пользователей, получающих одно и то же сообщение. Так что условия работы сильно отличались от привычных.


Мы решили, что ничего страшного не происходит: масштабируемся – и дело с концом. Ответственный сотрудник просто удвоил количество инстансов и отправился к врачу без грамма волнения.


А потом случилось это:



На первый взгляд, ничего особенного. Если бы не тот факт, что это был наш production-инстанс RabbitMQ, обрабатывающий не только вебсокет-сообщения, но и вообще всё, от чего зависит функционирование reddit.com. И это было нехорошо. Совсем нехорошо.


После многочисленных расследований, заламываний рук и апгрейдов инстансов мы сузили область поиска источника проблемы до интерфейса управления. Он всегда казался каким-то медленным, и мы решили, что его регулярно запрашивает наш Rabbit Diamond collector . Мы подумали, что дополнительный обмен данными, связанный с запуском новых вебсокет-инстансов, в сочетании с массой сообщений, получаемых в связи с этим обменом, привели к перегрузке Rabbit, пытавшегося вести учёт выполнения запросов к админке. Поэтому мы просто выключили её – и ситуация улучшилась.


Но мы не любим пребывать в неведении, поэтому на скорую руку сварганили кустарный мониторинговый скрипт:


$ cat s****y_diamond.sh #!/bin/bash /usr/sbin/rabbitmqctl list_queues | /usr/bin/awk "$2~//{print "servers.foo.bar.rabbit.rabbitmq.queues." $1 ".messages " $2 " " systime()}" | /bin/grep -v "amq.gen" | /bin/nc 10.1.2.3 2013

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


К сожалению, несмотря на такие сообщения:



Упомянутые здесь изменения времени перезарядки имели чисто технические причины. Хотя после них занятно было наблюдать за веткой r/place/new:



Возможно, это было частью мотивации пользователей.

Боты останутся ботами

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


Когда мы отключили Place, то конечные точки, к которым обращалось множество ботов, начали возвращать «не двухсотые» ошибки. Этот код был не слишком удачен. К счастью, все эти повторные обращения удалось легко блокировать на уровне Fastly.

Создание чего-то большего

r/Place не был бы так успешен, если бы не слаженная командная работа. Мы хотели бы поблагодарить u/gooeyblob, u/egonkasper, u/eggplanticarus, u/spladug, u/thephilthe, u/d3fect и всех остальных, кто помогал нам претворить в жизнь этот первоапрельский эксперимент.



Похожие статьи
 
Категории