Наверх ▲

Как мы храним 75 млн пользователей (пишем неблокируемый сервер)

Денис Бирюков Денис Бирюков Ведущий инженер программист в компании "Каванга".

Денис Бирюков: Здравствуйте! Продолжаем тему больших нагрузок. Правда, сервер у нас более простой – не http. Как мы храним 75 миллионов пользователей? Мы пишем свой неблокируемый сервер.

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

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

Можно разбить эту группу таргетингов на уникальное ограничение, ретаргетинг и таргетинг по соцдему.

Уникальное ограничение. Показывать пользователю «не больше, чем 5 раз всего», «не чаще, чем 3 раза за 10 минут» или «до первого клика». 

Ретаргетинг. Если пользователь был на каком-то сайте – например, svyaznoy.ru, то мы показываем ему рекламу «Связного», а другим не показываем. 

Таргетинг по соцдему. Соцдем – это совокупность демографических характеристик, например, «мужчина от 25-ти до 30-ти лет». Вот этим мужчинам нужно показывать баннер, а остальным – нет. 

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

Такой сервер у нас был.

Что он собой представлял? Это был многопоточный сервер со всеми вытекающими из этого плюсами и минусами. Так как он многопоточный (и данные в единственном экземпляре хранятся), то были блокировки и фрагментация. Аллокаторы не использовались и не писались ни для контейнеров, ни для операторов new/delete. 

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

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

Удаление объектов производилось раз в час. Например, за час к нам могло прийти очень много пользователей – происходил внезапный всплеск.

Что делать, если мы исчерпали доступные нам лимиты по памяти? Мы либо уйдем в подкачку (англ. swap) и окончательно «ляжем», либо будем игнорировать пакеты, которые должны создавать какие-то объекты в системе. И то, и другое плохо. 

Однако у этого сервера были преимущества. При отсутствии записи на диск он потреблял в среднем около 5 % ресурсов ЦПУ. У него была довольно высокая скорость ответа. Он сохранял на диск без выделения дополнительной памяти. Для этого отдельный поток обращался к памяти, искал информацию о пользователях и выполнял запись на диск. Ничего дополнительно не тратилось. 

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

Нам нужно было его переписать. Надо было избавиться от минусов и постараться сохранить плюсы. Что можно было сделать? 

Задача такова: есть 75 миллионов пользователей за 3 месяца (это по данным статистики за какой-то период). В день меняется примерно 250 миллионов объектов, которые описывают этих пользователей. На это уходит приблизительно (оценить тяжело, помню цифру в 3,5 раза) 60 Гб памяти. В пике у нас до 3000 запросов в секунду на обновление и столько же на выборку данных. 

Время ответа достаточно критично. Тот сервис тратил порядка 300 микросекунд (это с учетом TCP-запроса и ответа). Здесь мы поставили себе планку в 200 микросекунд. 

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

Периодически нужно делать резервную копию базы из памяти на диск. Почему? Во-первых, по каким-либо причинам может произойти сбой самого сервера. Во-вторых, связь. Иногда в Питере выключают свет. Делая резервную копию, при сбое мы можем быстро подняться и восстановить хотя бы частично часовой снимок (англ. snapshot) памяти.

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

Сервис должен быть легко расширяемым по функционалу. Уже на момент переписывания было 3 UDP-клиента на обновление данных и два TCP-клиента на опрос данных. У каждого свой протокол. Мы пытались его несколько унифицировать. Посмотрим, что получилось. 

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

Насколько гибка логика удаления? Это решение может удалять старые объекты. Оно также может удалять объекты, если память достигает каких-то лимитов. Этот пункт присутствует.

Резервное копирование базы из памяти на диск. По-моему, Memcached DB (Database) это "умеет". Но я могу ошибаться. Кажется, такой пункт присутствует. Все ругают эту возможность, но она есть. 

Как обновляем по UDP? Тут я не нашел ничего внятного. Кажется, это тоже реализовано. Нам эта реализация точно не подойдет, потому что не все наши клиенты, выполняющие обновление по UDP, знают pkey value. Pkey user и ID им известны, а pkey value они не всегда "знают". Им предоставлена только часть данных о пользователе. Решение нам не подходит. 

Кто реализует дополнительную логику (ретаргетинг)? Memcached – это сервер общего назначения. У нас частный случай, это наши проблемы. В итоге пришлось кое-что переписать. 

В каком виде мы будем хранить пользователя? Любое событие в системе – это запись в лог. В логе пишется кто, когда, где и какой баннер видел, кликал на него либо еще что-то делал. Мы можем хранить список этих самых событий по каждому пользователю. В итоге, мы всегда знаем все. Это довольно гибко, но занимает много места в памяти.

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

Какой взять: по логам или по структурам? 

Первый дает гибкость. Например, мы можем задать несколько скоростей и показывать не больше, чем 5 раз за 10 минут, и не чаще, чем 8 раз за 20 минут, или выставить две скорости. 

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

Чем мы руководствовались, когда принимали решение? Нужна высокая производительность. Требования меняются крайне редко. Сервисы переписываются. Как бонус получаем sizeof (struct1) = const. Можем какие-то аллокаторы сами написать. 

Резюме требований. 200 микросекунд. По 67 миллионов на 2 машины – это мы думаем наперед. 67 миллионов на одной машине. Всего машин две. Но это в номинале. Иногда одна в плановом или срочном ремонте, поэтому вся нагрузка идет на одну машину. 

В день меняется примерно 250 миллионов объектов. Памяти около 67 Гб. В пике до 3000 запросов в секунду. Должны уметь делать резервную копию базы из памяти на диск и использовать ее, когда поднимаем сервер.

Архитектура. Пускай сервер будет однопоточным, тогда не будет блокировок. Чтобы однопоточный сервер выдержал такую нагрузку, должен использоваться неблокируемый ввод-вывод (ВВ). Poll, epoll, kqueue – в зависимости от определений (defines). 

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

Есть как UDP-клиенты, так и TCP-клиенты. 

Мы можем написать свои собственные аллокаторы. Зачем? Во-первых, чтобы избавиться от фрагментации. При написании своих аллокаторов, своих операторов new/delete мы можем дать собственное определение разным типам объектов. Это тоже классно. 

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

Вопрос из зала: Может, использовать потоки?

Денис Бирюков: Нельзя. Иначе блокировки придется вводить. 

Сервер-модуль состоит из двух частей. Это сетевой ВВ и логика. Сетевой ВВ полностью взят из предыдущего проекта. Его я почти не переписывал, только какие-то аллокаторы добавил. Все сокеты неблокируемые. Функции мультиплексирования, постоянный ВВ, постоянное соединение, UDP- и TCP-клиенты. 

Как мы общаемся? Мы всегда работаем с бинарными данными. В самом начале этих данных сервер всегда принимает и отправляет какой-то заголовок. Заголовок – это 8 байт. Это размер данных и функция, которая должна обрабатывать эти данные. 

По TCP-запросу сервер сначала читает 8-байтный заголовок. Затем используется функция "readv", чтобы уменьшить нагрузку на сервер, читает сами данные, производит их обработку. 

В случае с UDP мы не можем выполнить чтение два раза, поэтому мы пытаемся при помощи "readv" прочитать столько, сколько можем — 8 байтов заголовка и 30 килобайт данных. Запросы от тех, кто не смог уложиться в этот объем, игнорируются. Данные пришли не полностью, поэтому мы посылаем сообщение о том, что не успели их обработать. После обработки записываем заголовок и данные в результат, в поток вывода. 

Создадим иерархию объектов, с которыми будем работать. Есть пользователь со своим идентификатором, временем создания, битовой маской, которые описывают тот самый user_id. У него есть контейнеры, которые хранят "flights", баннеры. Они все одного объекта ParticularRestrictions. Указаны места, на которых он был, и аудитории, в которые он входит. Объекты типа "retarget". 

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

У вектора в большинстве реализаций "capacity" в полтора раза больше, чем "size". Это дополнительный расход памяти. Зависит от реализации, конечно. 

Можно использовать лист. Фрагментации нет. "Capacity" равно "size". Однако требуется дополнительная память под хранение данных под двунаправленный список. Нам это не надо. 

IndexHolder – это массив, однонаправленный список, в котором выделяются массивы по 4 указателя. Первые 3 используются под наши нужды. Последний указывает на следующий массив из четырех указателей. В общем, такая у нас структура объектов. 

Как мы теперь сообщаем, что объекты устарели? "User" – это массив памяти под объекты. Будем считать самым старым тот объект, кто дольше всех не проявлял активности в сети. 

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

IndexHolder – то же самое. Правда, потом получилось, что 2-х связный список для IndexHolder – это избыток. Первоначально планировалось обрабатывать исключение через bad_alloc. 

ParticularRestriction и Retarget – это обычные циклические массивы, большие куски памяти. Мы дошли до конца, идем сначала. В этом случае самым старым будет считаться объект самый старый по времени создания. 

Как мы храним самих пользователей? Класс "user". Мэп – конечно, не самый быстрый объект, но для него легко выделять память.

Мы добавили одного пользователя в мэп. Он просит: «Выдели мне дополнительно место под один объект». Для мэппинга легко писать аллокатор. Чтобы избавиться от балансинга и ребалансинга, создадим 64 мэп. Хотя у нас user ID – это довольно случайная величина. 

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

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

Как мы удаляем объект? Мы взяли самого старого пользователя по времени модификации, удалили все объекты ParticularRestriction и Retarget. Вернее, пометили память, которую они занимают, как свободную. В следующий раз, когда будем аллоцировать, все будет замечательно. На этом месте опять создадим нового пользователя. 

Что будет, если мы хотим создать объект ParticularRestriction? Возьмем очередную порцию в циклическом массиве, кусок памяти, и смотрим, свободен ли он. Если свободен – все замечательно. Если нет, то мы нотифицируем. Здесь содержится указатель на конкретный экземпляр класса "user". Мы уведомляем пользователя о том, что он должен почистить в своих контейнерах flight или баннер и "убить" этот указатель. 

ParticularRestriction содержит parent ID. Parent ID у flight равен минус 1, у баннера parent ID какой-то вменяемый. Четко, ясно, где искать: во flight или в баннерах. Аналогично с ретаргетингом. 

Сохранение на диск теперь довольно простая операция. Формируем UDP-пакет. Посылаем UDP-пакет на сервер, он делает fork. "Предок" продолжает работать, меняет страницы памяти. "Предок" записывает 4 куска на диск. 

Результаты. 1,5 Гб под пользователя, дополнительно 0,6 Гб под двухсвязный список. Аналогично для IndexHolder, ParticularRestriction, Retarget. Под std::map максимум 0,75 Гб.

Почему так? Если убрать отсюда все лишние данные и сравнить, что в памяти и что в очень сжатом виде, то получим КПД 61 %. Улучшение более, чем в 3,5 раза. Это уже хорошо. 

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

Давайте прикинем, какие константы нужно проставить для аллокаторов. Емкость просмотра у нас не очень высока – от 1 до 1,5 баннеров в месяц на пользователя. Время жизни всех объектов соствляет приблизительно месяц. 

Потеря активного flight/баннера для нас менее критична, чем потеря пользователя. Рекламная кампания закончилась. Flight/баннер не нужен, а пользователь еще может посмотреть новую рекламную кампанию. Очень не хочется терять профиль этого пользователя, доставшегося нам такой дорогой ценой. Flight/баннер у нас имеет циклический массив, а пользователь удаляется по времени модификации. 

Если потеряем старые auditory/place (они нужны для ретаргетинга), то охват мы, конечно, уменьшим, но качество повысим. Мы не будем показывать баннеры тем, кто давно интересовался какой-то темой, мы покажем их тем, кто недавно проявил заинтересованность. 

Это цифры для одной машины. Почему я говорю, что IndexHolder избыточен? Если IndexHolder у нас больше, чем количество объектов ParticularRestriction и Retarget, то никакого bad_alloc не возникнет только в случае, если у нас есть утечка памяти под IndexHolder. 

Теперь логика. Там был сетевой вывод, а тут — логика. Объект "Action" у нас имеет заголовок и данные. Переключение (англ. switch) по номеру функции и конкретному событию (англ. case). Пользователь сделал какой-то хит, мы его пытаемся найти. Если его нет, то создаем, добавляем или модифицируем в нем объекты "flight", "banner" или "place". 

Пример SET_USER_MASK – мы изменили профиль пользователя и пытаемся его найти. Если нашли, то поменяем его битовую маску. 

Пример SET_USER_ADT – добавляем пользователя в какую-то аудиторию. Аудитория у нас приходит из поиска. Какой-то онлайн-сервер постоянно ищет. Пользователь искал в Google или в "Яндекс" какое-то интересное слово. Если нам это слово или фраза интересны, то мы его добавляем. Затем обновляем ретаргетинги. 

Что такое ретаргетинги? Есть пользователи. Они хранят информацию о том, где и что они видели. Есть текущие настройки ретаргетинга. Например, мы захотели показывать "flight" № 10 тем, кто был на "place" № 8. Накладываем одно на другое и добавляем в список ретаргетингов.

Список ретаргетингов. Мы запрашиваем в GET_USER_EXP описание пользователя, чтобы рассчитать, какой баннер показать. Здесь мы показываем все flights, все баннеры. В конце находятся идентификаторы "flight", ретаргетингов, которые можно показывать этому пользователю. Туда добавляем эти ретаргетинговые flights. 

SET_CAMPAIGN – это новый функционал. У flight внутри есть список баннеров. Flights могут быть объединены в список кампаний. Не хотелось создавать отдельный объект, поэтому сделали как в ретаргетинге. Flight № 1 может показываться 2 раза. Flight № 3 может показываться 2 раза. Оба они объединены в кампанию, которая, в общем, может показываться 5 раз. Тогда мы можем сказать, что flight № 1 и flight № 3 показывать можно. Если нет, то не добавляем их в список, лишаем их показа.

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

Антиклик. Нельзя засчитывать клик, если пользователь кликает чаще одного раза в пять минут.

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

Mem_info показывает, сколько памяти выделил текущий экземпляр у сервера, сколько осталось. Память, в принципе, выделена, часть ее помечена как свободная. 

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

Графики. Как обычно, самописный мониторинг плюс запись. Тут видно, что сервер-потребитель опрашивал сервер EXP 35,5 тысяч раз в минуту. Нагрузка может увеличиваться вдвое, и если какой-то из серверов у сервера недоступен, и вся нагрузка идет на один. 

Время ответа. Среднее время ответа 160 микросекунд. Менее 200 – это уже классно. Зеленым обозначена средняя скорость ответа, красным — минимальная скорость ответа. 

Так сам сервер обрабатывал этот запрос. Среднее значение равно 21 микросекунде. Плюс сети, плюс данные, ожидающие обработки, соединения.

Теперь количество запросов в секунду от события (англ. event). Оно гораздо меньше. Примерно 2500. 

Время на 20 микросекунд меньше. Среднее - 140 микросекунд. 

Обработка внутри самого сервера занимает 7 микросекунд. 

Сколько UDP-пакетов мы отправили на обновление? Если сравним эту цифру (в среднем 38,3 тысячи), то предыдущие данные должны плюсоваться. 36,5 и два или три. Эти цифры должны быть равны. Значит, с сервером у нас все нормально. 38,3 – все нормально. 

Время обработки одного UDP-пакета около 5 микросекунд. 

Сколько пользователей добавляется в аудиторию? В среднем 4000 в минуту. Иногда бывают всплески. Мы анализируем статистику с помощью Sphinx. Вдруг мы захотим добавить новую аудиторию, и у нас уже будут по ней какие-то пользователи. 

Время добавления в аудиторию - около 4-х микросекунд. 

Резюме по "боевой" нагрузке. У нас в номинальном режиме 670 TCP-запросов в секунду. Плюс столько же UDP. 

Каков предел по нагрузке? Сделали специальную тестовую утилиту. Сначала она посылает UDP-пакет на создание пользователя, потом отправляет TCP-запрос и ожидает TCP-ответ.

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

Если мы запустим больше потоков (100 потоков), то они будут гораздо быстрее все это делать - за 10 секунд. Почему? Видимо, сервер справляется с нагрузкой, а клиенты имеют какое-то строго фиксированное время одного запроса. Чем больше одновременно запущенных клиентов, тем быстрее.

Как с этим справиться? На графике представлено количество запросов в секунду. Если у нас будет 50 клиентов, то количество запросов будет в районе 25-ти тысяч. Если 100 клиентов, то 55 тысяч. Самый пик – это 180 потоков – будет в районе 85-ти тысяч.

Тестовая машина AMD Opteron. Два ядра по 8 процессоров в каждом. Или два 8-ядерных процессора? Короче, 8 ядер. 

Все, спасибо.

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

Вопрос из зала: Вы сохраняете пользователей. По каким критериям вы определяете, кто есть кто? Сам идентификатор этого пользователя?

Денис Бирюков: Сначала мы даем ему cookies. Если он второй раз пришел с этими cookies – замечательно, то есть cookies ему проставились. Если нет, то мы заново генерируем ему cookies или считаем, что он плохой. 

В принципе, наш охват – 75 миллионов – это, конечно, нереальный охват. Реально 20 миллионов. Сколько cookies мы сгенерировали для 75-ти миллионов пользователей? Это большая проблема с синхронизацией cookies, в том числе.

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

Сейчас просто генерируем cookies на основании текущей временной метки (англ. timestamp), времени, IP-адреса и User Agent. Если одновременно к нам пришло 3 запроса от страницы, выполненных примерно в одно и то же время, то мы сгенерируем один cookie-файл. Если нет, то нет. Последний будет потерян.

Вопрос из зала: Этот сервер считает статистику?

Денис Бирюков: Этот сервер статистику не считает. Вообще статистика считается в Hadoop отдельно по логам. 

Вопрос из зала: Если у вас свой аллокатор, то зачем вы используете voit*, а не просто индекс?

Денис Бирюков: На самом деле там не voit*. Там int index в глобальном массиве. 

По поводу статистики. Этот сервер нужен для того, чтобы быстро отдать баннер. Время ответа 200 микросекунд. Иначе мы баннер быстро отдадим. 

Мы храним все в логах. Логов у нас несколько терабайт за несколько дней, точные цифры я вам не скажу. Короче, Hadoop как-то все разбирает. 

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

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

Вопрос из зала: Как вы боретесь с "накликиванием"?

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

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

Вопрос из зала: Борьба с кликами идет в реальном времени?

Денис Бирюков: Да, борьба идет в реальном времени. В Hadoop тоже можно какие-то алгоритмы вводить, но пока мы их еще не вводили.

Вопрос из зала: Алгоритмы по изменению в логах или баннерах не используются?

Денис Бирюков: Нет. Пользователю отгрузили баннер, он может по нему кликнуть. Там есть сложный отдельный сервис. Я про сетевой вывод говорил. Это отдельный сервер от накрутки. Мы будем пытаться прекращать отгрузку баннеров всяким читерам.

Допустим, нас проверяли каким-нибудь тестом производительности. Мы будем давать ответы, но при этом не грузить основной функционал сервера. 

Спасибо.

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Александр Горник

Александр Горник

Окончил факультет Кибернетики МИФИ, 7 лет опыта работы в индустрии. Несколько десятков успешно выполненных проектов как в роли разработчика, тим-лида и архитектора, так и как PM.

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

Иван Авсеянко

Иван Авсеянко

Программист в компании IponWeb.

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

Петр Зайцев

Петр Зайцев

Пётр Зайцев окончил МГУ им. М.В. Ломоносова, и ещё в студенческие годы являлся техническим директором проекта SpyLOG, сервиса статистики для веб-сайтов. В начале 2000-х Пётр стал сотрудником MySQL AB и возглавил группу оптимизации производительности (High Performance Group) внутри компании.

Петр Зайцев (Percona Inc) делится своим опытом оптимизации производительности с помощью архитектуры InnoDB.