РИТ++ 2017, Backend Conf
Зал Кейптаун, 6 июня, 17:00
Тезисы:
http://backendconf.ru/2017/abstracts/2748.html
Большое количество живых соединений с сервером требует решения интересных и порой неоднозначных задач. Речь будет идти о том, как в условиях доставки тысяч уведомлений в секунду миллионам пользователей иметь возможность управлять потребляемыми ресурсами, производить безболезненные рестарты и при этом иметь "план Б" на случай непредвиденных проблем.
Доклад расскажет историю запуска системы уведомлений между сервисами mail.ru и их пользователями.
...
5. Состояние в почте
Какая информация является состоянием в любом почтовом
сервисе?
• письма в ящике
• пометки о прочтении
• срок жизни сессии
• …
6. События в почте
Как пользователь узнает об изменении ящика?
HTTP polling каждые 2 минуты:
7. События в почте
Как пользователь узнает об изменении ящика?
HTTP polling каждые 2 минуты:
• 3 миллиона запросов в минуту
8. События в почте
Как пользователь узнает об изменении ящика?
HTTP polling каждые 2 минуты:
• 3 миллиона запросов в минуту
• 50 тысяч запросов в секунду
9. Как пользователь узнает об изменении ящика?
HTTP polling каждые 2 минуты:
• 3 миллиона запросов в минуту
• 50 тысяч запросов в секунду
• 60% ответов - 304 Not Modified
События в почте
13. Publisher/Subscriber
Способ реагирования на изменение состояния при помощи
шины событий.
• Издатель отправляет множество событий в шину.
• Подписчик интересуется подмножеством событий из шины.
14. Как было
+-----------+ +-----------+ +-----------+
| | | | | |
| Storage | | API | | Browser |
| | | | | |
+-----------+ +-----------+ +-----------+
48. Маршрутизация событий
Filtering можно сделать по-разному
• с помощью таблиц и деревьев в памяти
• можно не париться и взять подходящую базу данных
• издатели публикуют полный набор параметров
• подписчики подписываются на любой набор параметров
49. Маршрутизация событий
Как применяем фильтры?
• издатели публикуют полный набор параметров
• подписчики подписываются на любой набор параметров
68. WebSocket
Бинарный двухсторонний протокол общения между браузером
и сервером.
• Стандартизован в 2011г. как RFC6455.
• Поддерживается всеми современными браузерами.
69. WebSocket
Использует HTTP для Upgrade.
После оперирует «фреймами» внутри соединения:
• Контрольные: ping, pong, close
• Пользовательские: text, binary
76. WebSocket
Браузеры по-разному реализуют RFC6455.
• Chrome не дожидается ответного Close фрейма и сразу
закрывает соединение
• Firefox периодически посылает Ping фреймы, ожидая в
ответ Pong
77. WebSocket
Браузеры по-разному реализуют RFC6455:
• Chrome не дожидается ответного Close фрейма и сразу
закрывает соединение
• Firefox периодически посылает Ping фреймы, ожидая в
ответ Pong
• IE периодически посылает.. Pong фреймы (но это не
противоречит спецификации)
78. Трудности
• 3 миллиона «живых» соединений
• время жизни соединения от нескольких секунд до
нескольких дней
• при разрыве соединений клиенты (браузер)
переподключаются
80. Go
• Компилируемый, со строгой статической типизацией
• Только структуры и интерфейсы
• Управление памятью с помощью сборщика мусора
• Каналы и «горутины» для конкурентного программирования
113. Как работает go runtime?
func (c *Channel) reader() {
for {
conn.Read() // wait incoming bytes
}
}
114. Как работает go runtime?
func read(c *conn, p []byte) (int, error) {
}
115. Как работает go runtime?
func read(c *conn, p []byte) (int, error) {
n, err := syscall.Read(c.fd, p)
}
116. Как работает go runtime?
func read(c *conn, p []byte) (int, error) {
n, err := syscall.Read(c.fd, p)
if err.IsTemp() {
}
}
117. Как работает go runtime?
func read(c *conn, p []byte) (int, error) {
n, err := syscall.Read(c.fd, p)
if err.IsTemp() {
// EAGAIN || EWOULDBLOCK
}
}
118. Как работает go runtime?
func read(c *conn, p []byte) (int, error) {
n, err := syscall.Read(c.fd, p)
if err.IsTemp() {
pollWait(c.descriptor, 'r')
}
}
119. Как работает go runtime?
• сокеты в Go неблокирующие
• runtime_pollWait на Linux реализован с помощью Epoll
• почему бы не использовать похожий подход?
128. Запись
С отправкой пакетов дело обстоит проще – мы всегда знаем,
когда хотим что-то записать в соединение.
Единственный сложный момент – синхронизация записи из
разных горутин.
135. Контроль ресурсов
Что, если вдруг все соединения решат отправить нам
сообщение?
Тогда реализация с epoll ничем не будет отличаться от
изначальной idiomatic реализации.
136. Goroutine pool
p := pool.New(128)
pool.Schedule(func() {
// Do some work.
})
pool.ScheduleTimeout(time.Second, func() {
// Do some work or return error.
})
146. Время запилить свою либу
В Go существуют две реализации WebSocket.
И обе они не позволяют полностью контролировать работу с
аллокацией памяти.
Поэтому пришлось реализовать RFC6455 с более
низкоуровневым API.
147. github.com/gobwas/ws
• работает с io.Reader, io.Writer
• умеет zero-copy upgrade
• позволяет кешировать фреймы
• проходит autobahn test suite
148. Zero-copy upgrade
BenchmarkUpgradeHTTP 5156 ns/op 8576 B/op 9 allocs/op
BenchmarkUpgradeTCP 973 ns/op 0 B/op 0 allocs/op
В случае простого Upgrade без сохранения значений заголовков.
Код бенчмарков: http://bit.ly/2svISpr.
150. Все вместе
При использовании goroutine pool можно вообще не
принимать соединения, если ресурсов больше нет.
Это решает многие проблемы DDOS в случае нештатных
ситуаций.
157. Все вместе
• При большом количестве бездействующих соединений
лучше использовать epoll и отказаться от читающей
горутины
• При большом количестве соединений отказаться от
пишущей горутины
• Goroutine pool позволяет зафиксировать количество
максимально потребляемой памяти
• Goroutine pool позволяет ограничить количество
обрабатываемых соединений
159. Итого
Событийная модель позволяет:
• реализовать действительно интерактивный сервис
• предлагать пользователям множество дополнительных
функций
• снизить сопряженность сервисов и упростить
взаимодействие
160. Цифры
• 3 миллиона “живых” соединений
• 30 тысяч уведомлений в секунду от storage
• 9 тысяч из которых доходят до пользователей ежесекундно
• 75 тысяч событий EPOLLIN (доступность для чтения) в
секунду
• 1 тысяча Upgrade запросов в секунду
• в ходе оптимизаций количество памяти на одно соединение
сократилось с 60KB до 10KB (и это не все!)