Ajax Layout

Доклад Олега Илларионова про архитектуру клиентской части vk.com (vkontakte).

Олег Илларионов: Разработчик ВКонтакте, ответственный за API, разделы Видеозаписей, Друзей, Приложений, интеграцию с внешними сервисами Twitter, Email и другие. Студент ЛЭТИ.

http://www.slideshare.net/profyclub_ru/ajax-layout

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

Начать хотелось бы с переходов.

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

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

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

Мы должны полностью формировать страницу с того момента, с которого она была. Мы не можем хранить события, мы не можем показывать что-то. Нам тяжело работать с Long Poll’ом. Веб очень простой, не Real Time’овый. Нужно это изменить. Начали экспериментировать. Многие сайты реализовали в разной степени прогресса эту технологию.

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

Эта схема работала недостаточно хорошо. Во-первых, адреса были некрасивыми. Были хеши, которые передавались на серверную сторону. Это была основная их причина. Если мы имеем страницу с хешом, то хеш не отправится к серверу, и нам нужно отловить его на JS и отправить самим. Это значит, что когда пользователь вставляет к себе в браузер ссылку а-ля “vkontakte.ru/hash что-то там”, мы должны сначала загрузить какую-то страницу, потом сделать Ajax-запрос на сервер, поймав то, что у нас в адресе. После этого мы можем ее как-то отображать и работать дальше. Это плохо.

После чего был следующий этап, как это все может работать. Это момент, когда мы делаем клик, очень хорошо оптимизируем скорость нагрузки за счет того, что мы заранее знаем, для каких страниц какие стили и скрипты нужны. Мы просто храним в config’е определенную схему сайта, что у нас есть такие-то страницы, которые требуют такие-то скрипты по «регуляркам». У нас есть «регулярки», которые описывают страницы. Мы находим нужную «регулярку» и решаем, какие скрипты нужно подключить. Тем самым мы начинаем загружать статику не в тот момент, когда мы уже получили контент, а одновременно. Тем самым, если статика не закеширована, мы ускоряем загрузку страницы. После чего мы отображаем контент и меняем location bar. Но есть проблемы с адресом, поэтому это плохо.

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

Но потом мы пришли к History API, и он позволил избавиться от хешей в тех браузерах, которые это поддерживают. Естественно, в браузерах, которые этого не поддерживают, мы не можем работать с History API. Когда была выкачана первая версия всего этого дела (это было, наверное, где-то в ноябре, даже чуть попозже, прошлого года), это работало только в Chrome. На тот момент был всего 1 браузер, который поддерживал History API. Все остальные работали с хешами.

History API стоит выделить, потому что он, на самом деле, дал такой шаг. Если бы его не было, у нас просто не было бы надежды, и мы не стали бы этого делать. Мы бы оставили старую схему и пытались бы искать какие-то другие способы – каждый раз перезагружать всякие notify, чаты и так далее.

Что у нас осталось. Мы отображаем контент. Используем нормальный адрес там, где можно. Где нет – мы используем решеточку (#) и используем разные хаки. Например, известно, что для того, чтобы старые версии IE отловили смену события, поняли, что у нас страница перешла с одной на другую, нам нужно использовать iframe. Нужно менять там страницу.

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

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

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

Поэтому пришлось осознать эту проблему. Понять, что вся проблема в контенте и вернуться к каким-то истокам.

Все начать заново осмыслять. Стало ясно, что Ajax не подходит для решения этой задачи.

Поэтому пришлось использовать iframe. У нас есть iframe, и когда мы хотим перейти на другую страницу, мы открываем скрытый iframe, в котором есть части. Части – это разные части страницы. Вместо того чтобы брать и загружать всю страницу Ajax’ом сразу, мы загружаем ее по частям.

Сначала header… В этом iframe’е есть функция, которая перекидывает наверх код в header. Потом еще что-то, еще что-то, еще что-то. Все это через iframe. Все это кажется нереальным. Сначала мы думали, что это не будет работать. Как это? Это не может быть быстрее.

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

Таким образом, все стало работать хорошо.

Но мы столкнулись со следующей проблемой. Естественно, все это затевалось ради Long Poll’а, ради Real Time’а, чтобы мы могли получать какие-то события. Но только представьте, какие нагрузки будут, если мы просто берем и начинаем держать Long Poll. Вот сайт, у него немаленькая посещаемость и вдруг ни с того ни с сего нужно держать по коннекту на каждого пользователя постоянно. Но это еще полпроблемы. Это проблема решаемая.

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

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

Мы просто берем и делаем поддомены.

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

Много поддоменов. Для каждой вкладки свой поддомен – все ok, работает.

Но нагрузка. Для каждой вкладки свой long poll – это неприемлемо.

Появилась такая мечта, что нужно общаться между вкладками. У нас много вкладок (браузер, вкладки), и хочется общаться между ними. Хочется иметь возможность из одной вкладки сказать, что «я буду держать long poll и передавать вам всем, что там происходит, а вы просто живите себе и ничего не делайте».

Сначала казалось, что это только flash.

Local Connection, Flash, и нет другого выхода. Это очень печально, потому что завязывать сайт на flash’е, от которого очень хочется отказаться, потому что он где-то вылетает… Все знают проблемы с разными версиями flash-player’а и с тем, что там происходит.

Но потом был открыт html5. Многие, наверное, в курсе, что есть такая вещь, как web-storage.

Web-storage – это стандартный html5, который позволяет нам хранить какую-то информацию прямо в браузере. Это никак не помогло бы нам, если бы не было события window on storage. Мы можем в момент положения каких-то данных в local storage отловить это событие, посмотреть, что туда положили и как на это отреагировали. То есть мы можем общаться между вкладками, причем без flash’а и в достаточно большом количестве браузеров. Можно сказать, что во всех современных браузерах, более или менее используемых, даже в мобильных. Единственное – Opera Mini, не знаю, насколько на это поддерживает. Но для нее у нас отключены Ajax-переходы, поэтому живем.

Таким образом, у нас появился long poll сервер, который держит один коннект, вкладки общаются, решают, кто будет держать. Если он очень долго не отвечает (то есть вкладка была закрыта), кто-то другой берет эту задачу и решает ее.

Хотелось бы рассказать, как устроен сам long poll сервер. Мне известно, что многие тоже решают эту проблему, многие используют Real Time в своих проектах и пытаются организовать что-то подобное.

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

Для других проектов это все можно делать совершенно по-другому. Но в нашем случае только так. Самое главное – это то, что ни в коем случае на сторону long poll’а не нужно навешивать всякой логики. Возможно, изначально может показаться, что long poll сервер должен быть таким большим сервером, которым узнает о каком-то событии, генерирует html, получает lang pack, проверяет приватность/неприватность и только потом что-то отдает пользователю.

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

Поэтому такой long poll сервер должен быть очень тонким. Он не должен ничего делать, кроме как передавать события и хранить, пока есть память, некая очередь. Мы называем это Q-Engine.

Нет шаблонизации. Нет бизнес-логики.

Нет ничего. Только передача событий.

Это работает быстро.

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

Первое – это подключение статики и контента, о чем я уже сказал.

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

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

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

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

Было найдено такое решение, которое сначала показалось каким-то странным, но оно хорошо работает. Мы берем и все, что относится к текущему контенту вкладки, изымаем из DOM’а. Мы получаем, например, по id этот элемент (какой-то контейнер), сохраняем в какую-то переменную, в некую очередь и выкидываем из DOM’а. Вместо этого вставляем какой-то новый элемент. Ссылка на старый у нас хранится. Она, естественно, не вычищается из памяти, потому что мы в любой момент можем ее использовать. Более того, это достаточно быстро работает. Мы можем достаточно быстро вставить его в DOM (то, что мы уже взяли), и оно вставится в том виде, в котором оно было.

Еще один момент – это переменные. Что делать с переменными. Большинство людей, когда пишут JavaScript, какие-то классы, функции и так далее, используют глобальные переменные либо закрывают их в какой-то scope. Но у них есть какие-то просто переменные, которые очень сложно выцепить. Мы решили, что все переменные, которые относятся к какой-то странице, мы будем называть “cur.что-то”. У нас такой объект cur, в котором мы храним все подэлементы (все переменные, с которыми мы что-то делаем на этой странице). Как только пользователь переходит на другую страницу, мы все эти переменные собираем, складываем куда-то в очередь и потом, когда он нажал кнопку «назад», мы их оттуда достаем, складывая те, где он был, чтобы он мог вернуться вперед.

Таким образом, мы сохраняем полностью переменные, сохраняем DOM. Единственный момент – это всякие события, которые хотелось бы сохранить и которые навешаны не на внутренние элементы, а которые снаружи. Очень часто на window нужно что-то повесить. Поэтому, естественно, нужны коллбеки, нужно что-то обрабатывать.

Еще один хороший момент – это поиск в разных разделах. Например, первым это было сделано в разделе «Друзья». Была задача сделать так, чтобы поиск по «моим друзьям» был очень быстрым. Стало понятно, что на серверной стороне мы не сможем этого сделать. Мы не можем сделать достаточно быстрый поиск на серверной стороне так, чтобы пользователь пользовался этим, как фильтром. Чтобы он зашел в раздел «Мои друзья», начал что-то набирать и тут же увидел список. Чтобы он не чувствовал какой-то задержки.

Нужно выгружать всех друзей на клиента. Но это достаточно проблематично, когда друзей много (например, когда их 10 тысяч). Как это делается. Мы рисуем какую-то страницу. Естественно, мы должны в JSON’е выгрузить этих друзей. Чтобы искать на клиентской стороне по ним, мы должны проиндексировать их на клиенте, составить дерево. Понятно, что для этого нам нужно получить их в JSON’е. Но если мы получаем всех друзей в JSON’е, и их, например, 10 тысяч, то мы сталкиваемся с двумя проблемами.

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

Поэтому было решено делать несколько запросов. Мы делаем один запрос и показываем только страницу с n-ным количеством друзей (15 друзей). После этого мы делаем еще один запрос, чем слегка напрягаем Apache, зато мы кешируем в memcached те данные, которые нам нужны посередине. То есть те данные, которые мы вытащили уже при первом запросе и используем при втором, мы кладем в memcached. Мы не достаем их второй раз, по крайней мере, из труднодоступных мест, только из memcached.

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

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

Это последний слайд. Наверное, надо рассказать что-то еще, так как времени много.

Есть ли какие-нибудь вопросы? Я бы оттолкнулся от какого-нибудь вопроса и продолжил рассказ.

Вопросы и Ответы

Вопрос из зала: По поводу long polling сервера. Что они вообще делают. Это прокси к чему-то или что? Например, откуда они берут данные?

Олег Илларионов: Да, я, кстати, не рассказал достаточно подробно об этом. Как это происходит.

Например, вы отправили кому-то сообщение. В момент отправки ваших сообщений мы формируем те данные, которые получит пользователь, которому вы отправили. В этот момент мы смотрим, какой у него lang pack, получаем его lang pack, берем шаблоны, составляем все, что нужно и отправляем в long poll сервер. Причем в независимости от того, есть ли этот пользователь или нет. Мы все равно отправляем в QE, чтобы потом это использовать.

Вопрос из зала: В смысле, это все клиент создает?

Олег Илларионов: Нет-нет-нет. Это, естественно, на сервере.

Вопрос из зала: То есть запрос сначала идет не на long polling, а отдельно.

Олег Илларионов: Нет-нет. Сами события в long poll сервер попадают из PHP. Мы в PHP получаем какое-то событие, формируем данные и отправляем их.

Вопрос из зала: Скорее всего, так, но просто на всякий случай, поговорить. На самом деле, речь идет о чем. Вы отправили данные на PHP, грубо говоря. Дальше в соответствии с какой-то логикой вы выплюнули все это в соответствующую очередь, которая потом ушла через long poll соединение клиенту. Правильно?

Олег Илларионов: Да.

Вопрос из зала: Есть ли у вас ситуации, когда нужно подсоединять не через PHP, а через какой-то отдельно стоящий сервис подключать. Не знаю, мультичат какой-нибудь. Или все идет только через PHP-frontend?

Олег Илларионов: Не совсем. На самом деле, есть два типа QE-серверов (серверов, которые управляют long poll’ом). Один из них является частью движка сообщений и отправляет события в изначально заданном формате сообщений, и это касается только сообщений.

Второй движок занимается этими же сообщениями для notify’ев. Но при этом он формирует и отправляет все остальные события, так как событий на сайте, на самом деле, очень много. Кто-то что-то залайкал, кто-то добавил что-то в newsfeed. Все это обновляется в реальном времени, поэтому требует событий, которые нужно обрабатывать на клиентской стороне.

Вопрос из зала: Получается, как-то симулируется ответ сервера в некоторые моменты. То же сообщение, например. При отправке ведь мы же должны показать список сообщений, а не ждать, пока это уйдет на сервер, придет в очередь, очередь рассосется long poll’ом и вернется обратно.

Олег Илларионов: Мы отдаем уже готовую информацию, но не какой-то раздел, а только то, что нужно поменять на странице. Для каждого случая это решается отдельно.

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

Олег Илларионов: В плане по порядку?

Вопрос из зала: Просто пока придет очередь, что происходит? Мы же должны что-то показать. Мы показываем список сообщений уже с новым отправленным сообщением. А, там входящие появляются…

Олег Илларионов: Нет, естественно, мы берем и…

Вопрос из зала: Тогда понятно.

Олег Илларионов: Мы просто на лету формируем верстку относительно того, что нам нужно.

Вопрос из зала: Можно поподробнее про использование iframe’ов для подгрузки вместо Ajax’а? Очень быстро было.

Олег Илларионов: Хорошо. Сейчас расскажу. Идея в том, что мы не можем в достаточно большом количестве браузеров получать информацию, которая приходит по Ajax’у в течение ее загрузки. Мы получаем этот контент только после того, как получили все, после того как сервер все отдал и прервал соединение. Это очень плохо.

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

Вопрос из зала: Это вы при первой загрузке страницы загружаете части, потом перемещаете в DOM? Или какая-то подгрузка идет. Сначала создается DOM-элемент, где iframe. Так?

Олег Илларионов: Когда просто страница открывается (не Ajax-переход, а просто она открывается), она открывается просто, как она должна быть. Когда пользователь сделал какой-то переход, перешел на какую-то ссылку, мы не перезагружаем страницу, мы остаемся на той странице, мы просто подгружаем элементы.

В этот момент мы, вместо того чтобы делать Ajax-запрос, делаем скрытый iframe, который имеет все параметры, аналогичные тому запросу, который бы мы сделали, если бы использовали Ajax для этого. Данные, которые приходят, выглядят примерно следующим образом: “top.какая-то функция callback” и набор данных, которые нужно использовать для отображения в каком-то блоке.

Вопрос из зала: Спасибо.

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

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

Вопрос из зала: Почему для этого надо использовать iframe, почему не использовать 3 разных Ajax-запроса?

Олег Илларионов: Потому что для этого придется очень мучить фронты и backend’ы.

Вопрос из зала: 3 iframe’а – точно так же будет 3 запроса.

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

Вопрос из зала: Сейчас производительность JavaScript’а постоянно улучшается. Когда вы планируете перейти на чисто клиентский рендеринг шаблонов и вообще всего?

Олег Илларионов: Мне кажется, это будет очень плавный переход. Точно так же, как с Ajax Layout’ом. Это был очень плавный момент. Не было такого, что раз, вчера сайт был обычный, а сегодня он весь на Ajax’е. Он переписывался по разделам. Специально для этого была придумана система, которая позволяла не конфликтовать обычному подходу и Ajax’овому. Переходы между разделами были Ajax’овыми, но как только пользователь выходил за этот круг, как только он выходил на какую-то старую страницу, она происходила обычным образом. Для этого мы просто руками на ссылки вешали callback’и. Так как мы взяли такое правило: все новые переписанные разделы мы использовали без .php, делали rewrite. Чуть позже мы заменили этот callback на простую проверку. Если у нас .php, мы делаем обычный запрос, если у нас без .php (какая-то внутренняя страница, но без расширения), значит, мы делаем Ajax-запрос.

Вопрос из зала: Здравствуйте. Спасибо за хороший доклад. У меня есть вопрос немного нетехнический. Вы при реализации используете немного нестандартные методы (iframe’ы и так далее, что специфично для разных браузеров). Каким образом вы все это тестируете, используете ли вы какие-то специализированные средства?

Олег Илларионов: Мы используем «виртуалки». Естественно, сложно поставить на одну машину все браузеры. Мне не удавалось.

Вопрос из зала: Это понятно. То есть у вас используется ручное тестирование?

Олег Илларионов: Да. Довольно проблематично, например, в 6-м EI автоматизировать тестирование верстки. Я не знаю таких примеров. Хотя, наверное, при должном желании это можно сделать.

Еще хотелось бы рассказать такую вещь. Это решение одной проблемы, с которой все сталкиваются, и мало кто знает, как ее решить. Когда у нас нет History API, как сделать так, чтобы не было окна перехода. Пользователь открывает ссылку без Ajax’а. Хеш мы можем получить только на клиентской стороне, поэтому большинство загружает некую страницу, в которой он обрабатывает этот хеш, и после этого загружает в нормальную страницу.

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

Как мы решили эту задачу. Оказалось, что есть очень простой способ, который работает везде. Если мы в хедере до боди делаем JavaScript код, который вызывает “location=что-нибудь”, то есть который меняет location. В этот момент браузер ничего не отрисовывает и по поведению ведет себя примерно так же, как серверный редирект. Это очень хороший хак, который спас всю затею.

Вопрос из зала: У меня целая серия вопросов. Для начала я хотел бы уточнить. Работа кнопки «назад» – это часть в History API? Когда я кликаю, происходит перехват метода, и браузер просто не переходит. Вы как-то через History API сделали?

Олег Илларионов: Да. Да. В старых браузерах это тоже работает за счет iframe’ов. Где нет History API, есть iframe’ы, которые меняют свои rule’ы. Страница не менялась, но менялись iframe’ы, меняется контент iframe’ов. Браузер запоминает это и при нажатии на кнопку «назад» не перезагружает страницу, а перезагружает iframe внутри страницы. Это можно отловить.

Вопрос из зала: Во время загрузки страниц таким хитрым способом у вас как-то производится загрузка счетчиков. Это загрузка страницы считается как загрузка страницы? Баннеры обновляются…

Олег Илларионов: Баннеры, естественно, обновляются, счетчики – тоже. Счетчики обновляются двумя способами. При любом обычном переходе получаются счетчики, но не все левое меню, а только цифры. Минимум трафика.

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

Вопрос из зала: Когда вы манипулируете с DOM’ом, переносите страницу в какое-то хранилище и загружаете на ее место новую. Все события, которые были навешаны на клики, на всплывающие окна, все это потом нормально возвращается назад в браузерах? С этим проблем нет?

Олег Илларионов: Если событие было на какой-то внутренний элемент, то оно возвращается. За исключением того, если это событие как-то баблится и так далее. С этим могут быть проблемы. Но нет таких случаев, когда это вело себя совершенно неадекватно.

Вопрос из зала: Проблем со снятием listener’а и навешиванием его обратно для очистки памяти не возникает?

Олег Илларионов: Возникает, конечно. Но это, в основном, верхние события. То есть события, которые навешиваются на window, на BodyNote. Еще такой интересный момент с BodyNote. Дело в том, что мы не используем document.body, потому что он не везде используется. Есть браузеры (например, EI), где когда вы заходите и смотрите на страницу, на самом деле, вы видите dif, у которого overflow out, у которого scrollbar. Вы видите не страницу, у которой scrollbar, а dif, у которого scrollbar.

Это сделано для того, чтобы спасти слои, которые открываются, когда вы, например, смотрите фотографию, и у которых своя верхняя прокрутка. Это можно очень элегантно сделать в Chrome. Можно достаточно элегантно, но не так, как в Chrome, сделать в Firefox’е, но без каких-то извращений. Например, в EI этого не сделать, а в Opera мы вообще не нашли способа сделать это достаточно элегантно, чтобы не было некоторых неприятных случаев с этим (например, чтобы не скролилась внутренняя страница при прокрутке слоя до конца).

Вопрос из зала: Большое спасибо.

Вопрос из зала: Я хотел бы задать вопрос по использованию веб-сокета. Будет ли он где-то использоваться или, может быть, уже где-то используется? Ваше отношение.

Олег Илларионов: Отношение хорошее. Очень хотелось бы иметь рабочие веб-сокеты в браузерах, но, к сожалению, сейчас с этим не все хорошо. Вы в курсе, что, например, Firefox отказался. Будем надеяться, что они передумают. Мы, например, считаем эту угрозу не такой страшной. С этим можно жить.

На мой взгляд, сокеты – это не обреченная технология. Она может жить. Когда-нибудь мы, возможно, будем это использовать. Но сейчас единственный способ сделать это достаточно эффективно – это long poll. Если заглядывать в будущее, скорее всего, это event driven. Всякие события, наверное, даже обгонят по развитию сокеты, потому что сокеты как-то застряли и непонятно, что с ними будет.

Вопрос из зала: Я хотел бы уточнить. Веб-сокеты обратно ввели в Firefox 6 со всеми починенными проблемами безопасности. Также в Chrome их уже ввели или введут в 16-й версии. И с Opera’ой что-то непонятное.

Ведущий: Небольшое объявление. У нас пропал следующий докладчик. Доклада про NodeJS не будет, но вместо этого можно продолжить общение с Олегом. Мне кажется, про архитектуру vkontakte будет очень много интересных вопросов. Можно будет хорошо побеседовать. Спасибо.

Олег Илларионов: Я могу рассказать про NodeJS. (Смех в зале, аплодисменты). На самом деле, в последнее время мы не очень активно его используем, но используем. В последнее время просто не было возможности его применить, за исключением отправки e-mail’ов, push-нотификации и всякие такие вещи, связанные с Twitter’ом и так далее.

Например, отправка e-mail’ов великолепно работает просто потому, что там нет ничего сложного пока. Пока мы не сделали e-mail @vk.com, пока у нас e-mail’ы только с хешиками, то есть вы можете отправить прямо из личных сообщений e-mail. Многие не замечали, потому что надо еще догадаться вместо имени человека начать набирать e-mail. Но такая возможность есть. Ответ от этого пользователя будет получен и придет вам в личное сообщение (правда, без attach’ей).

Мы, на самом деле, уже давно мечтаем сделать @vk.com. Но системные администраторы не могут сделать это быстро, потому что там есть некоторые вещи с портами и так далее. Не могу сказать, когда появится эта функция. Наверное, довольно скоро. Мы, по крайней мере, довольно давно движемся к тому, чтобы организовать на уровне дата-центров все необходимое, чтобы можно было это реализовать.

Мне кажется, самый лучший пример того, как можно использовать NodeJS в таких проектах – это push-нотификации. Все знают, что iPhone поддерживает push-нотификации. Мы можем отправлять события на iPhone. Я могу рассказать, как это работает, потому что не все, наверное, знают. У Apple есть свой бинарный протокол. Это не так, как в Android’е, не нужно дергать какие-то скрипты. Вы подключаетесь к TCP-потоку и отправляете туда события в бинарном виде внутри бинарника. Правда, вы вкладываете еще JSON. Такая веселая смесь, но это очень хорошо. Мне это очень нравится.

Базовые вещи вы отдаете в бинарном виде, все остальное – в JSON’е. Очень удобно, достаточно экономично и за счет того, что это всего лишь один коннект, это работает достаточно быстро. Очень долго этим занимался один сервер. Потом просто из соображений того, что он может вдруг отвалиться, добавили еще, но это совершенно не требует каких-то больших нагрузок, несмотря на то что пользователей iPhone’ов великое множество, и всем нужно отправлять нотификации.

У нас были проблемы с доходом этих нотификаций. Они не всегда приходили. Сейчас эти проблемы были решены. Насколько я знаю, сейчас у всех работают push-нотификации на iPhone.

Вопрос из зала: Не у всех.

Олег Илларионов: Не у всех? Они вообще не приходят или что? Вообще не приходят. Странно. Мне тоже не приходят, но у меня девелоперская сборка, у меня они не должны приходить. Сколько я ни тестировал, ни проверял… Вы можете оставить мне свой id’ишник, я посмотрю в базе. Если они вообще не приходят, скорее всего, проблема на уровне того, что у вас в базе прописано, что вам не нужно отправлять push-нотификации. У нас были некоторые такие моменты, когда не совсем то писалось в базу. Были пользователи, которые попали в такое несчастливое число. Но я был уверен, что я всех этих пользователей проапдейтил.

Вопрос из зала: Раньше приходило, теперь перестало.

Олег Илларионов: Это очень печально. Финальный момент. Пришлось все переписать и заставить это работать хорошо. Я немного сменил протокол, и они стали сбрасывать коннект. Собственно, из-за этого были большие проблемы с доходом push-нотификаций. Они стали сбрасывать коннект, если c одного IP’шника я коннекчусь с двумя инстансами, с двумя разными сертификатами. Я делал это, чтобы подсылать себе еще push-нотификации, потому что у меня девелоперская сборка, у которой другой сертификат. Они просто стали сбрасывать. Сервер все время реконнектился, отправлял несколько сообщений. Потом кому-нибудь из разработчиков отправлял эти сообщения, и все вырубалось. Это было очень печально, но сейчас такого не должно быть, я отключил всем разработчикам push-нотификации. Теперь все хорошо.

Вопрос из зала: Он отрубал вообще весь вконтакт, push’и от него полностью?

Олег Илларионов: Он дисконнектил. Apple очень весело себя ведет. Если что, он дисконнектит. Причем в feedback ничего не приходит. У них два TCP-канала – feedback и основной stream. Но, в принципе, достаточно легко догадаться, почему он отключает. По крайней мере, если все делать правильно, они этого делать не будут. Но так как все работало, и изменение было с их стороны, пришлось немного подебажить и поэкспериментировать, чтобы дойти до этого.

Вопрос из зала: При отправке сообщения на e-mail, куда оно уходит? Можно указать полный адрес с @ или что?

Олег Илларионов: Идея в том, что когда вы набираете e-mail и отправляете сообщение, ему приходит e-mail со специального адреса. В адресе есть хеш. Мы проверяем, чтобы отсекать спам и все подобное. Нет спама и нет нагрузки.

Вопрос из зала: Я слышал, что у вас используется своя СУБД. Это правда?

Олег Илларионов: Это правда.

Вопрос из зала: Вы можете рассказать, что лучше и какие сравнительные характеристики с чем-нибудь, хотя бы с MySQL?

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

Основная причина – поиск. Хотелось искать по сообщениям. В MySQL начать искать по сообщениям каждого пользователя оказалось нереальной задачей. Был написан специальный движок. Он называется text engine. Он написан специально для личных сообщений. В нем много чего не предусмотрено. Редактирование было не предусмотрено до последнего момента. Но так как text engine – это не только личные сообщения, но и стены, то недавно появилось редактирование на стенах. Это было очень серьезное изменение в движке.

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

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

Олег Илларионов: Отправка, естественно, не очень прямая. Мы не можем просто взять и отправить с того же сервера почту по понятным причинам. Есть сервера, которые выполняют разные задачи. Если один сервер будет выполнять все задачи, то сисадмины повесятся.

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

Вопрос из зала: Вы храните прямо исходник письма, который вы сгенерили, с html, заголовками – все вместе?

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

Вопрос из зала: Правильно ли я понял, что у вас есть некоторые машины, которые, видимо, сами генерят html и txt-версии писем с нужными заголовками. После этого кладут их в систему, которая база/небаза, в ней лежат полные исходники писем. Есть совершенно отдельные выделенные машины, которые из той очереди забирают эти сгенеренные и по факту делают только команду mail, отправляя в ваши NTA.

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

Та почта, которая отправляется на какой-то определенный e-mail из личных сообщений, на самом деле, работает совершенно по-другому. Ее отправка написана на JS. Это отдельный сервер в другом месте с другими API’шниками, который через наше API получает эти письма и отправляет их. Это совершенно внешняя система. Она сделана для того, чтобы в случае если все отправляемые письма будут характеризоваться как спам (чего мы очень боимся), это не стало критичным для отправки нотификаций. Чтобы были совсем разные API’шники и все было совсем по-разному.

Вопрос из зала: У вас куча всяких IP’шек из разных подсетей…

Олег Илларионов: Нет, их всего несколько. Есть такая маленькая подсеть, а есть все остальное.

Вопрос из зала: Сколько машин, если не секрет, занимается отправкой почты, на которых NTA крутится?

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

Вопрос из зала: Это десятки, сотни?

Олег Илларионов: Это десятки, но не сотни. Отправкой e-mail’ов занимается один сервер. Надо его продублировать, а то вдруг он упадет.

Вопрос из зала: Какие объемы писем, если не секрет, отправляете каждый день? Просто я читал доклад про 100 миллионов писем каждый день. Мне интересно, если мы с вами померяемся, кто окажется круче на цифрах. (Смех в зале).

Олег Илларионов: Невероятное количество. Я заикаюсь, когда такие цифры вижу. Очень много нотификаций за счет того, что очень долгое время регистрация на сайте была достаточно долгое время через e-mail, и всем этим людям надо отправлять нотификации. Это дикое количество.

Вопрос из зала: То есть у вас, извиняюсь, «длиннее», я правильно понял? (Смех в зале, аплодисменты).

Олег Илларионов: Я, если честно, не знаю этого числа, потому что я не запомнил количество нулей. Я его как-то видел.

Вопрос из зала: Просто чтобы добить вопрос про почту. Каким образом вы мониторите? Есть какой-нибудь отдельный человек или группа, которые занимаются вопросами, как эти письма правильно отправить, доставить, как их правильно генерить?

Олег Илларионов: Нет, конечно. У нас маленькая команда. Есть просто статистика по количеству отправляемых писем. Начальство периодически смотрит на статистику. Если там что-то не так, то все плохо.

Вопрос из зала: Что можно увидеть из статистики, которая представляет из себя прямую линию, на ней нотификации о новых сообщениях – 5 миллионов. Вот она прямая последние два месяца. Что там можно увидеть?

Олег Илларионов: Она не прямая. Сайт постоянно растет достаточно большими темпами, и это число увеличивается.

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

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

Вопрос из зала: Понятно. Но вы хотя бы меряете эту очередь? Грубо говоря, у вас стоит очередь заявок на генерацию писем про новые комменты, про еще что-то новое. Вы меряете, какой у нее объем? Вдруг там стоит 100 миллионов, вы их накопили за какое-то время.

Олег Илларионов: Да, я точно не знаю, стоит ли в этом случае очередь. Но  у нас есть такой специальный скрипт watch doc, который проверяет разные числа, смотрит, чтобы они были достаточно высокими, и шлет sms-ки, если что.

Вопрос из зала: Хорошо. Вы вроде очень подробно рассказали. Спасибо.

Вопрос из зала: Расскажите, пожалуйста, какие сервера очередей используются и в каких случаях?

Олег Илларионов: Вы имеете в виду – для внутренней передачи данных?

Вопрос из зала: Да.

Олег Илларионов: Мы не используем какой-то Open Source’ный формат, как делают многие. Мы решаем задачи такими, не самыми оптимальными способами, зато самыми быстрыми, потому что, как правило, это вещи, которые не касаются высоких нагрузок. За исключением e-mail’ов, где свой движок. Там понятно – там много писем, и это, действительно, высоконагруженная штука.

Во всех остальных случаях это всякие интеграции, парсинг RSS, еще что-то. Это, как правило, обычный MySQL. Даже импорт в Twitter – это обычный MySQL. Там специальная табличка, которая заточена на конкретные задачи. Она выступает в роли очереди.

Вопрос из зала: Давно хотел спросить. Зачем вы ломаете в History API логику поведения браузера? Если в ленте новостей открыть фотографию, она откроется в LightBox’е, потом ее закрываем и нажимаем «назад», мы попадаем не на предыдущую страницу, а в LightBox.

Олег Илларионов: Да. Действительно, ломаем. Мы считаем, что так лучше.

Вопрос из зала: То есть вы считаете, что ломать стандартную логику браузера лучше? Вы знаете лучше.

Олег Илларионов: Да, мы так считаем (смеется). Это такой спорный момент. Не все с этим согласны, естественно. Я считаю, что это момент, который нужно немного дорабатывать, нужно находить золотую середину. С одной стороны, не очень хорошо, когда пользователь просмотрел целый альбом, а потом, например, хочет вернуться на человека. Нажимает «назад», его двигает назад по всему альбому. С другой стороны, может быть, пользователь хотел как раз назад перейти.

Это разные психологии, разные вещи. Был очень забавный момент, мы периодически шутили. В “Facebook” был забавный баг: если из «ВКонтакте» перейти на “Facebook”, открыть там фотографию, что-то покликать и нажать «назад», то пользователь попадал на «ВКонтакте».

(Смех в зале).

Ведущий: У нас появился человек, который расскажет про NodeJS. Олег, огромное спасибо. Ты нас очень выручил. По-моему, всем было очень приятно пообщаться. Ждем снова.

(Аплодисменты).

Олег Илларионов: Всем большое спасибо. Рад был всех видеть.

(Аплодисменты).

Завершение презентации

Взято из http://profyclub.ru/docs/155

LEAVE A COMMENT