43
Семь тысяч rps, один Go. Сергей Камардин, @mail.ru

Семь тысяч Rps, один go

Embed Size (px)

Citation preview

Page 1: Семь тысяч Rps, один go

Семь тысяч rps, один Go.Сергей Камардин, @mail.ru

Page 2: Семь тысяч Rps, один go

Вступление

Page 3: Семь тысяч Rps, один go

Что нужно сделать?Сервер между браузером и сторонним сервисом. Нужно уметь:

● Держать много соединений по WebSocket;● Делать много HTTPS запросов;● Разбирать ответы от сервиса;● Инлайнить ресурсы;● Много работать с JSON.

Page 4: Семь тысяч Rps, один go

Какую технологию выбрать?Выбирали по соотношению эффективности разработки и производительности результата:

● Node.js;● Rust;● Go;● Scala.

bit.do/mailgo

Page 5: Семь тысяч Rps, один go

WebSocket

WebSocket — протокол полнодуплексной связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени.

● Бинарный;● Использует HTTP для Upgrade соединения;● Поддерживается всеми современными браузерами.

Page 6: Семь тысяч Rps, один go

WebSocket

Имплементации:

● golang.org/x/net/websocket● github.com/gorilla/websocket

Аргументы за gorilla/websocket:

● Активно поддерживается;● Умеет control messages;

Page 7: Семь тысяч Rps, один go

JSON-RPCПротокол вызова удаленных процедур.

● Двунаправленный;● Асинхронный.

--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}

<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

--> {"jsonrpc": "2.0", "method": "divide", "params": [42, 0], "id": 2}

<-- {"jsonrpc": "2.0", "error": "division by zero", "id": 2}

Page 8: Семь тысяч Rps, один go

Реальность

Page 9: Семь тысяч Rps, один go

Проблемы● Как сократить количество соединений к серверу?● Что будет, если сторонний сервис заснет?● Как сократить затраты CPU на установку HTTPS соединений?

Page 10: Семь тысяч Rps, один go

Переиспользование соединенийПри большой нагрузке, исходящие соединения практически не переиспользуются.

Page 11: Семь тысяч Rps, один go

Переиспользование соединенийСтандартная библиотека позволяет влиять на это:

provider.SetClient(&http.Client{

Transport: &http.Transport{

...

MaxIdleConnsPerHost: 128,

...

},

})

Page 12: Семь тысяч Rps, один go

Переиспользование соединенийСтандартная библиотека позволяет влиять на это.

Page 13: Семь тысяч Rps, один go

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

Page 14: Семь тысяч Rps, один go

Если сервис зависнет

Page 15: Семь тысяч Rps, один go

TLS и рукопожатиеУстановка HTTPS соединения забирает много CPU:

Page 16: Семь тысяч Rps, один go

TLS и рукопожатиеУстановка HTTPS соединения забирает много CPU:

Page 17: Семь тысяч Rps, один go

Переиспользование TLS сессий

provider.SetClient(&http.Client{

Transport: &http.Transport{

...

TLSClientConfig: &tls.Config{

ClientSessionCache: tls.NewLRUClientSessionCache(128),

},

...

},

})

Стандартная библиотека позволяет влиять и на это:

Page 18: Семь тысяч Rps, один go

Переиспользование TLS сессийСтандартная библиотека позволяет влиять и на это:

Page 19: Семь тысяч Rps, один go

Переиспользование TLS сессий

Page 20: Семь тысяч Rps, один go

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

Page 21: Семь тысяч Rps, один go

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

Page 22: Семь тысяч Rps, один go

JSON

Page 23: Семь тысяч Rps, один go

JSON

type MyCoolStruct struct {

Key string

Value int32

}

Можно значительно сэкономить время, если генерировать код сериализации/десериализации структур:

Page 24: Семь тысяч Rps, один go

{"key": "question", "value": 42}

JSON

func decodeMyStruct(in *lexer.Lexer, out *MyCoolStruct) {

for !in.HasNext() {

switch in.UnsafeString() {

case "key":

out.Key = in.String()

case "value":

out.Value = in.Int32()

}

}

}

Можно значительно сэкономить время, если генерировать код сериализации/десериализации структур:

Page 25: Семь тысяч Rps, один go

JSON

BenchmarkEncodingJson-8 1000000 1236 ns/op 288 B/op 4 allocs/op

BenchmarkEasyJson-8 10000000 179 ns/op 0 B/op 0 allocs/op

Можно значительно сэкономить время, если генерировать код сериализации/десериализации структур:

Page 26: Семь тысяч Rps, один go

JSON

Библиотеки реализующие данный подход:

● github.com/pquerna/ffjson● github.com/mailru/easyjson

Аргументы за mailru/easyjson:

● На данный момент самая быстрая;● Гибкая конфигурация (auto to_snake_case и тд);

Page 27: Семь тысяч Rps, один go

Микрооптимизации

Page 28: Семь тысяч Rps, один go

Микрооптимизации● sync.Pool;● избегать копирований;● избегать аллокаций;● инлайнить функции;● работать с байтами;

Page 29: Семь тысяч Rps, один go

Inline функцийfunc CheckChar(c byte) int {

if isGoodChar(c) {

return 42

} else {

return 0

}

}

func isGoodChar(c byte) bool {

switch c {

case 'x', 'y', 'z':

return false;

default:

return true;

}

}

Page 30: Семь тысяч Rps, один go

Inline функцийfunc CheckChar(c byte) int {

if isGoodChar(c) {

return 42

} else {

return 0

}

}

func isGoodChar(c byte) bool {

return c != 'x' && c != 'y' && c != 'z'

}

Page 31: Семь тысяч Rps, один go

Inline функцийfunc CheckChar(c byte) int {

if c != 'x' && c != 'y' && c != 'z' {

return 42

} else {

return 0

}

}

func isGoodChar(c byte) bool {

return c != 'x' && c != 'y' && c != 'z'

}

Page 32: Семь тысяч Rps, один go

Inline функций

$ go build -gcflags=-m inline.go

inline/inline.go:20: can inline isGoodChar

inline/inline.go:3: can inline CheckChar

inline/inline.go:4: inlining call to isGoodChar

Можно проверить с помощью gcflags -m:

BenchmarkInline-8 2000000000 1.28 ns/op

BenchmarkNoInline-8 500000000 3.84 ns/op

Или косвенно по бенчмаркам:

Page 33: Семь тысяч Rps, один go

Как обнаружить проблемы

Page 34: Семь тысяч Rps, один go

Утечка памятиКлассический пример утечки с использованием горутин:

func LoadImages(done chan struct{}, urls []string) (ret [][]byte) {

results := make(chan []byte)

for _, url := range urls {

go loadImage(results, url)

}

for count := 0; count < len(urls); count++ {

select {

case <-done: // client has canceled loading

return

case r := <-results: // yet another image

ret = append(ret, r)

}

}

return

}

Page 35: Семь тысяч Rps, один go

Эмуляция нагрузкиДля HTTP:

● apache jmeter;● wrk;● ab;

Для WebSocket:

● gws;● tcpkali;● apache jmeter;

Page 36: Семь тысяч Rps, один go

Apache JMeter● Очень гибкий;● Множество отчетов;● Умеет следить за состоянием машин;● Умеет remote testing;● Можно поставить WebSocket sampler;

Page 37: Семь тысяч Rps, один go

WRKhttps://github.com/wg/wrk/

● Весьма прост;● Можно писать lua-скрипты;● Очень быстрый, может завалить nginx;

Page 38: Семь тысяч Rps, один go

GWShttps://github.com/gobwas/gws

● Можно писать lua-скрипты;● Может быть как клиентом так и сервером;

Page 39: Семь тысяч Rps, один go

Концовка

Page 40: Семь тысяч Rps, один go

SummaryПереиспользуем ресурсы:

● Коннекты;● TLS сессии;● Слайсы и структуры;

Оптимизируем код:

● Парсим синтаксис без reflection;● Профилируем;● Пишем бенчмарки;● Нагружаем и мониторим;

Page 41: Семь тысяч Rps, один go

Статистика● 170 000 живых соединений;● 1500 в секунду новых WebSocket соединений;● 7000 json-rpc сообщений в секунду;● Около 20 000 загрузок небольших изображений (~10Kb) в секунду

(~200mb/s) для последующего инлайна;

Page 42: Семь тысяч Rps, один go

Спасибо!