4. Wannafun.ru
• Онлайн speed dating
• Знакомство только с теми, кто в сети
• Поток лиц aka «матрица»
• 3 минуты чата для принятия решения
• 48% / 52%
воскресенье, 16 декабря 12 г.
5. Проект в цифрах
• 500 000 пользователей
• 2000 онлайн
• 2-3 события в секунду на юзера
• Более 100 http-запросов в минуту на юзера
• Более 5 000 000 чатов
• Более 45 000 000 сообщений
воскресенье, 16 декабря 12 г.
6. На чем всё работает
Erlang Postgres Redis
EventMachine Rails Resque
воскресенье, 16 декабря 12 г.
8. Онлайн-взаимодействия
• Знакомства: входящие/исходящие, сообщения
• Контакт-лист: личные сообщения, онлайн/
оффлайн
• Уведомления
воскресенье, 16 декабря 12 г.
9. Взаимодействие с браузером
Pusher (http://pusher.com/) Faye (http://faye.jcoglan.com/)
Push-канал к клиенту Pub/sub
Нет серверной логики Нет серверной логики
Socket.io (http://socket.io/)
Абстракция над WebSocket, Flash и
Polling
Произвольная серверная логика
воскресенье, 16 декабря 12 г.
10. Серверная реализация socket.io
• NodeJS
• На тестах падала VM — epic fail
• EventMachine
• Не было актуальной версии
• Erlang
• Не было актуальной версии
воскресенье, 16 декабря 12 г.
11. Что хорошего в Erlang
• Нет коллбеков - простой последовательный код
• Нет разделяемого состояния, структуры данных
неизменяемы - concurrency проще
• Иерархия супервизоров - высокая устойчивость
• Прозрачная распределенность
• Бесшовный деплой
воскресенье, 16 декабря 12 г.
12. Архитектура чат-сервера
• Соединения обслуживаются Cowboy
• Каждая сессия - отдельный процесс
• Вспомогательные процессы для работы с БД, Redis
воскресенье, 16 декабря 12 г.
17. Очереди задач
• Resque
• Быстро работает, использует Redis
• Удобный Web UI
• Redis полезен и для других задач:
• Хранение счетчиков
• Синхронизация состояния
• Кэширование
воскресенье, 16 декабря 12 г.
18. Уникальные задачи
• Сохранение сообщений
• Прогрев кэша
• Расчет статистики
Можно использовать Redis для блокировки
воскресенье, 16 декабря 12 г.
19. Обычный код
def perform
return unless redis.setnx("lock", true)
# do task actions
ensure
redis.del "lock"
end
Реализация: resque-lock (<= 1.0.0)
воскресенье, 16 декабря 12 г.
21. Правильный код
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
Реализация: resque-lock (>= 1.1.0)
воскресенье, 16 декабря 12 г.
23. Хорошие индексы
Хорошие = Ускоряющие необходимые запросы
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 user
SELECT * FROM messages WHERE destination_id = ? AND source_id = ?
ORDER BY created_at DESC LIMIT 10
# Unread messages of user
SELECT * FROM messages WHERE destination_id = ? AND read_at IS NULL
ORDER BY created_at DESC LIMIT 10
воскресенье, 16 декабря 12 г.
24. Плохие индексы
# History of messages received from given user
add_index 'messages', ['source_id', 'destination_id']
# Unread messages of user
add_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 г.
25. Отличные индексы!
# History of messages received from given user
add_index 'messages', ['source_id', 'destination_id',
'created_at'], :order => { 'created_at' => 'desc' }
# Unread messages of user
add_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 г.
26. Массивы и hstore
• Как сериализация, только лучше
• Могут индексироваться
воскресенье, 16 декабря 12 г.
27. Размер таблиц
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.01
Usual table size: 249 MB
Hstore table size: 219 MB
Array table size: 257 MB
воскресенье, 16 декабря 12 г.
28. Индексирование
Поля:
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 г.
29. Кэширование
последовательностей
• Выбираем последовательность на несколько шагов
вперед
• Кэшируем идентификаторы в Redis
id = 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
end
end
воскресенье, 16 декабря 12 г.
31. Кэширование матрицы
• Проблема
• Различные фильтры: мин/макс возраст
(от 16 до 70) + пол
• 3080 возможных фильтров
• (1 + 2 + … + 55) * 2 = 55 * 56
• Решение: аппроксимация фильтров
воскресенье, 16 декабря 12 г.
32. Кэширование матрицы
• Проблема
• Различные фильтры: мин/макс возраст
(от 16 до 70) + пол
• 3080 возможных фильтров
• (1 + 2 + … + 55) * 2 = 55 * 56
• Решение: аппроксимация фильтров
воскресенье, 16 декабря 12 г.
33. Кэширование с аппроксимацией
• Возраст округляется до кратного X (= 4)
• Минимальный - вниз, максимальный - вверх
• 210 фильтров
• (1 + 2 + … + 14) * 2 = 14 * 15
• Кэшируется порция заведомо большего размера
• После извлечения из кэша выкидываются лишние
записи
воскресенье, 16 декабря 12 г.
34. Обработка фотографий
• 200–300 регистраций в минуту, половина грузит
JPG на 5 мегабайт
• Первым делом уменьшайте размер входящих
изображений
• CarrierWave лучше отделён от модели, чем
Paperclip, обратно совместим
воскресенье, 16 декабря 12 г.
35. Обработка фотографий
• RMagick MiniMagick не хранит в себе временного файла,
использует память отдельного процесса, не
поддерживает создание изображений
• GraphicsMagick — форк ImageMagick, ориентированный
на стабильность и производительность
• Прирост в скорости до 2-3 раз, но это не серебрянная
пуля: меньше фич, иногда производительность страдает
воскресенье, 16 декабря 12 г.
36. Отправка СМС
• SMPP – открытый протокол, поддерживаемый
большинством SMS-шлюзов
• Бинарный, за счет чего выше скорость передачи
и footprint воркеров
• github.com/raykrueger/ruby-smpp – реализация
для EventMachine
воскресенье, 16 декабря 12 г.
38. RSpec для Rails и Erlang
• Запуск Erlang при создании сессии
• Отдельный поток с EM, обслуживающий все соединения
• Socket.io поверх em-websocket-client
• Очередь входящих сообщений
s = open_session_with_chat
# Delegates to ActionDispatch::Integration::Session
s.post "/users/sign_in", email: '123@example.com', pass: '12345'
# Wait for a message (with timeout)
s.receive(:connect).should be
# Send message
s.send_event :contact_message, contact_id, text: "Hi!"
воскресенье, 16 декабря 12 г.
39. Боты-тестеры
• Нагрузочное тестирование чата
• Определение проблем с concurrency
• Помощь при ручном тестировании
• Настраиваемое поведение
воскресенье, 16 декабря 12 г.
40. Реализация ботов
• Акторы на основе EventMachine
• Socket.io поверх em-websocket-client
• Набор "шаблонов поведения"
class Caller < Wannafun::Actor
behave :get_matrix
behave :accept_calls
behave :call_to_users
behave :talk_in_calls
end
EventMachine.run do
Wannafun::ActorSet.new(Caller, options).start!
end
воскресенье, 16 декабря 12 г.
41. Как ловить JS ошибки на клиенте
• В сложных приложениях — сложные сценарии и
граничные случаи
• Обратная связь пользователь - разработчик.
Максимум информации собирается
автоматически
• Echoes.js (github.com/kossnocorp/echoes). На
клиенте собираем логи, фильтруем важные и
прикладываем к запросу
воскресенье, 16 декабря 12 г.
43. Мониторинг приложения
• Длины очередей в Resque
• Кол-во несохраненных сообщений
• Кол-во знакомств в разных состояниях
• Длина очереди модерации
воскресенье, 16 декабря 12 г.
44. Head-huntung
Wannafun: red.scorpix@gmail.com
Evil Martians: surrender@evl.ms
воскресенье, 16 декабря 12 г.
45. ВПРСВ НТ?
ЗБС!
Алексей Носков Алексей Найден
@alno @alexnayden
github.com/alno github.com/anayden
alexey.noskov@evl.ms alexey.nayden@evl.ms
Все изображения являются собственностью их авторов
воскресенье, 16 декабря 12 г.