Технические аспекты знакоства с девушкой в Интернете
DESCRIPTION
Обзор опыта и проблем, решенных при разработке speed-dating сайта Wannafun.ruTRANSCRIPT
Технические аспекты знакомства с девушкой в
интернетеАлексей Найден, Алексей Носков
Evil Martians
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Wannafun.ru• Онлайн speed dating
• Знакомство только с теми, кто в сети• Поток лиц aka «матрица»
• 3 минуты чата для принятия решения
• 48% / 52%
воскресенье, 16 декабря 12 г.
Проект в цифрах
• 500 000 пользователей
• 2000 онлайн
• 2-3 события в секунду на юзера
• Более 100 http-запросов в минуту на юзера
• Более 5 000 000 чатов
• Более 45 000 000 сообщений
воскресенье, 16 декабря 12 г.
На чем всё работает
ErlangEventMachine
PostgresRails
RedisResque
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Онлайн-взаимодействия
• Знакомства: входящие/исходящие, сообщения• Контакт-лист: личные сообщения, онлайн/оффлайн
• Уведомления
воскресенье, 16 декабря 12 г.
Взаимодействие с браузеромPusher (http://pusher.com/)
Push-канал к клиенту
Нет серверной логики
Faye (http://faye.jcoglan.com/)
Pub/sub
Нет серверной логики
Socket.io (http://socket.io/)
Абстракция над WebSocket, Flash и Polling
Произвольная серверная логика
воскресенье, 16 декабря 12 г.
Серверная реализация socket.io• NodeJS
• На тестах падала VM — epic fail
• EventMachine
• Не было актуальной версии• Erlang
• Не было актуальной версии
воскресенье, 16 декабря 12 г.
Что хорошего в Erlang
• Нет коллбеков - простой последовательный код• Нет разделяемого состояния, структуры данных неизменяемы - concurrency проще
• Иерархия супервизоров - высокая устойчивость• Прозрачная распределенность• Бесшовный деплой
воскресенье, 16 декабря 12 г.
Архитектура чат-сервера
• Соединения обслуживаются Cowboy
• Каждая сессия - отдельный процесс• Вспомогательные процессы для работы с БД, Redis
воскресенье, 16 декабря 12 г.
Начало знакомства
воскресенье, 16 декабря 12 г.
Одновременный ответ
воскресенье, 16 декабря 12 г.
Синхронная реализация
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Очереди задач• Resque
• Быстро работает, использует Redis
• Удобный Web UI
• Redis полезен и для других задач:
• Хранение счетчиков• Синхронизация состояния• Кэширование
воскресенье, 16 декабря 12 г.
Уникальные задачи
• Сохранение сообщений• Прогрев кэша• Расчет статистики
Можно использовать Redis для блокировки
воскресенье, 16 декабря 12 г.
Обычный код
Реализация: resque-lock (<= 1.0.0)
def perform return unless redis.setnx("lock", true)
# do task actionsensure redis.del "lock"end
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Правильный код
Реализация: resque-lock (>= 1.1.0)
http://redis.io/commands/setnx
def perform now = Time.now.to_i timeout = now + 60
unless redis.setnx("lock", timeout) # Lock is active return if now <= redis.get("lock").to_i # Lock is not expired return if now <= redis.getset("lock", timeout).to_i end
# do task actions 11
redis.del "lock"end
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Хорошие индексыХорошие = Ускоряющие необходимые запросы
create_table 'messages' do |t| t.references 'source' t.references 'destination' t.string 'body' t.timestamp 'created_at' t.timestamp 'read_at'end
# History of messages received from given userSELECT * FROM messages WHERE destination_id = ? AND source_id = ?ORDER BY created_at DESC LIMIT 10
# Unread messages of userSELECT * FROM messages WHERE destination_id = ? AND read_at IS NULLORDER BY created_at DESC LIMIT 10
воскресенье, 16 декабря 12 г.
Плохие индексы# History of messages received from given useradd_index 'messages', ['source_id', 'destination_id']
# Unread messages of useradd_index 'messages', ['destination_id']
Limit -> Sort Sort Key: created_at Sort Method: top-N heapsort Memory: 25kB -> Index Scan using messages_between_users on messages Index Cond: ((source_id = ?) AND (destination_id = ?))Total runtime: 6.451 ms
Limit -> Sort Sort Key: created_at Sort Method: quicksort Memory: 26kB -> Bitmap Heap Scan on messages Recheck Cond: (destination_id = ?) Filter: (read_at IS NULL) -> Bitmap Index Scan on messages_unread Index Cond: (destination_id = ?)Total runtime: 123.983 ms
воскресенье, 16 декабря 12 г.
Отличные индексы!# History of messages received from given useradd_index 'messages', ['source_id', 'destination_id', 'created_at'], :order => { 'created_at' => 'desc' }
# Unread messages of useradd_index 'messages', ['destination_id', 'created_at'], :order => { 'created_at' => 'desc' },:where => 'read_at IS NULL'
Limit -> Index Scan using messages_between_users on messages Index Cond: ((source_id = ?) AND (destination_id = ?))Total runtime: 0.209 ms
Limit -> Index Scan using messages_unread on messages Index Cond: (destination_id = ?)Total runtime: 0.183 ms
воскресенье, 16 декабря 12 г.
Массивы и hstore
• Как сериализация, только лучше• Могут индексироваться
воскресенье, 16 декабря 12 г.
Размер таблицcreate_table 'users_usual' do |t| t.boolean 'flag1' ... t.boolean 'flag20'end
create_table 'users_hstore' do |t| t.hstore 'flags' # gem 'activerecord-postgres-hstore'end
create_table 'users_array' do |t| t.integer_array 'flags' # gem 'activerecord-postgres-array'end
5 000 000 записей, флаги независимы, P[flag=yes] = 0.01Usual table size: 249 MBHstore table size: 219 MBArray table size: 257 MB
воскресенье, 16 декабря 12 г.
ИндексированиеПоля:
Seq Scan on users_usual Filter: (flag2 AND flag7 AND flag13)Total runtime: 799.959 ms
Hstore:Bitmap Heap Scan on users_hstore Recheck Cond: (flags @> '2=>y, 7=>y, 13=>y'::hstore) -> Bitmap Index Scan on users_hstore_flags Index Cond: (flags @> '2=>y, 7=>y, 13=>y'::hstore)Total runtime: 350.778 ms
Массив:Bitmap Heap Scan on users_array Recheck Cond: (flags @> '{2,7,13}'::integer[]) -> Bitmap Index Scan on users_array_flags Index Cond: (flags @> '{2,7,13}'::integer[])Total runtime: 48.118 ms
воскресенье, 16 декабря 12 г.
Кэширование последовательностей
• Выбираем последовательность на несколько шагов вперед
• Кэшируем идентификаторы в Redisid = redis.lpop(cache_key) # Get next value from cache
unless id # No cached value ids = connection.select_values some_heavy_scope.select('id').to_sql id = ids.shift
redis.multi do |r| ids.each{ |id| r.rpush cache_key, id } r.expire cache_key, 7200 # Expire cache after 2 hours endend
воскресенье, 16 декабря 12 г.
воскресенье, 16 декабря 12 г.
Кэширование матрицы• Проблема• Различные фильтры: мин/макс возраст
(от 16 до 70) + пол
• 3080 возможных фильтров
• (1 + 2 + … + 55) * 2 = 55 * 56
• Решение: аппроксимация фильтров
воскресенье, 16 декабря 12 г.
Кэширование матрицы• Проблема• Различные фильтры: мин/макс возраст
(от 16 до 70) + пол
• 3080 возможных фильтров
• (1 + 2 + … + 55) * 2 = 55 * 56
• Решение: аппроксимация фильтров
воскресенье, 16 декабря 12 г.
Кэширование с аппроксимацией• Возраст округляется до кратного X (= 4)
• Минимальный - вниз, максимальный - вверх• 210 фильтров
• (1 + 2 + … + 14) * 2 = 14 * 15
• Кэшируется порция заведомо большего размера• После извлечения из кэша выкидываются лишние записи
воскресенье, 16 декабря 12 г.
Обработка фотографий
• 200–300 регистраций в минуту, половина грузит JPG на 5 мегабайт
• Первым делом уменьшайте размер входящих изображений
• CarrierWave лучше отделён от модели, чем Paperclip, обратно совместим
воскресенье, 16 декабря 12 г.
Обработка фотографий
• RMagick MiniMagick не хранит в себе временного файла, использует память отдельного процесса, не поддерживает создание изображений
• GraphicsMagick — форк ImageMagick, ориентированный на стабильность и производительность
• Прирост в скорости до 2-3 раз, но это не серебрянная пуля: меньше фич, иногда производительность страдает
воскресенье, 16 декабря 12 г.
Отправка СМС
• SMPP – открытый протокол, поддерживаемый большинством SMS-шлюзов
• Бинарный, за счет чего выше скорость передачи и footprint воркеров
• github.com/raykrueger/ruby-smpp – реализация для EventMachine
воскресенье, 16 декабря 12 г.
Тестирование
• Модульное• Ruby — RSpec
• Erlang — EUnit
• Интеграционное?• Нагрузочное?
воскресенье, 16 декабря 12 г.
RSpec для Rails и Erlang• Запуск Erlang при создании сессии
• Отдельный поток с EM, обслуживающий все соединения
• Socket.io поверх em-websocket-client
• Очередь входящих сообщенийs = open_session_with_chat
# Delegates to ActionDispatch::Integration::Sessions.post "/users/sign_in", email: '[email protected]', pass: '12345'
# Wait for a message (with timeout)s.receive(:connect).should be
# Send messages.send_event :contact_message, contact_id, text: "Hi!"
воскресенье, 16 декабря 12 г.
Боты-тестеры
• Нагрузочное тестирование чата• Определение проблем с concurrency
• Помощь при ручном тестировании
• Настраиваемое поведение
воскресенье, 16 декабря 12 г.
Реализация ботов
• Акторы на основе EventMachine
• Socket.io поверх em-websocket-client
• Набор "шаблонов поведения"class Caller < Wannafun::Actor behave :get_matrix behave :accept_calls behave :call_to_users behave :talk_in_callsend
EventMachine.run do Wannafun::ActorSet.new(Caller, options).start!end
воскресенье, 16 декабря 12 г.
Как ловить JS ошибки на клиенте
• В сложных приложениях — сложные сценарии и граничные случаи
• Обратная связь пользователь - разработчик. Максимум информации собирается автоматически
• Echoes.js (github.com/kossnocorp/echoes). На клиенте собираем логи, фильтруем важные и прикладываем к запросу
воскресенье, 16 декабря 12 г.
Echoes.jsecho.log('Test', 'logging', namespace: 'app.lol_module.45')echo.log(['trololo'])
[ { "timestamp": 1341468018606, "body": ["Test", "logging"], "namespace": "app.lol_module.45" }, { "timestamp": 1341468018606, "body": [["trololo"]], "namespace": "" } ]
echo.logs.grep 'some'#=> [{ body: ['Something'] }, { body: ['I want some LSD.']}]
воскресенье, 16 декабря 12 г.
Мониторинг приложения
• Длины очередей в Resque
• Кол-во несохраненных сообщений• Кол-во знакомств в разных состояниях• Длина очереди модерации
воскресенье, 16 декабря 12 г.
Head-huntung
Wannafun: [email protected]
Evil Martians: [email protected]
воскресенье, 16 декабря 12 г.
Алексей Носков@alno
github.com/[email protected]
Алексей Найден@alexnayden
github.com/[email protected]
ВПРСВ НТ?
ЗБС!
Все изображения являются собственностью их автороввоскресенье, 16 декабря 12 г.