Avito. Backend United.

Александр Емелин, 04.2018

https://slides.com/fz/c

Real-time сообщения

Сообщения о событиях, доставленные клиенту в кратчайшие сроки после возникновения

Решения

Centrifugo

Мотивация

>>> import hmac
>>> from hashlib import sha256

>>> h = hmac.new(b"secret", digestmod=sha256)
>>> h.update("42".encode())
>>> h.hexdigest()
'93c121e7aa437a1e01e3c512c6f0ce3c821a839025dca4408f85616de4aaee70'

HMAC example

Передаем с клиента "42" и получившуюся подпись, затем на стороне сервера проверяем подлинность полученных данных, повторив то же самое и сверив подписи

Генерируем на бекенде:

Особенности

  • Language-agnostic
  • Простая интеграция, не нужно переписывать бек
  • Маcштабируется с помощью Redis
  • Performance по сравнению с динамическими языками
  • Восстановление пропущенных сообщений
  • Presence информация, join/leave события
  • Кроссплатформенность
  • Бесплатно, MIT лицензия

Ограничения

  • Только односторонний поток сообщений
  • Нет нативной аутентификации и авторизации
  • Нет тесной интеграции с бекендом и хуков (решаемо)
  • Не подходит для динамичных мультиплеер-игр
  • Большинство пользователей не знают Go

Библиотека Centrifuge

Зачем библиотека?

Ограничения

  • Только односторонний поток сообщений
  • Нет нативной аутентификации и авторизации
  • Нет тесной интеграции с бекендом и хуков (решаемо)
  • Не подходит для динамичных мультиплеер-игр
  • Большинство пользователей не знают Go

Форматы протокола

JSON-streaming

{"id": 1, "method": "subscribe", "params": {"channel": "ch1"}}
{"id": 2, "method": "subscribe", "params": {"channel": "ch2"}}

Protobuf

0a 0a 32 08 08 07 10 00 18 1a 20 00 0a 08 22 06 08 02 10 19 18 03 
0a 09 22 07 08 02 10 a2 03 18 20 0a 09 22 07 08 02 10 8d 02 ...

Protobuf vs JSON

BenchmarkJsonMarshal-8            500000    2980 ns/op    1223 B/op   9 allocs/op
BenchmarkJsonUnmarshal-8          500000    3120 ns/op    463 B/op    7 allocs/op


BenchmarkProtobufMarshal-8        2000000    901 ns/op    200 B/op    7 allocs/op
BenchmarkProtobufUnmarshal-8      2000000    692 ns/op    192 B/op   10 allocs/op
JSON: {"type":"ping","time":123456789} = 32 bytes

Protobuf: 0A 05 08 95 9A EF 3A = 7 bytes

Protobuf

Транспорты

Websocket

Но уже не из-за поддержки браузерами

SockJS

  • xhr(xdr)-streaming
  • eventsource
  • iframe-eventsource
  • iframe-htmlfile
  • xhr(xdr)-polling
  • iframe-xhr-polling
  • jsonp-polling

SockJS fallbacks

GRPC

GRPC bidirectional streaming

service Centrifuge {
    rpc Communicate(stream Command) returns (stream Reply) {}
}

message Command {
    uint32 id = 1;
    uint32 method = 2;
    bytes params = 3;
}

message Reply {
    uint32 id = 1;
    Error error = 2;
    bytes result = 3;
}
server {
    listen 443 ssl http2;

    ssl_certificate     cert.pem;
    ssl_certificate_key key.pem;

    proxy_buffering off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Scheme $scheme;
    proxy_set_header Host $http_host;

    location /proto.Centrifuge {
        grpc_pass grpc://localhost:8001;
    }

    location /connection/websocket {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

Nginx: Websocket + GRPC

QUIC ?

Возможно когда-нибудь...

Prometheus

(+ Graphite в Centrifugo)

Метрики

Аутентификация

func authMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
        credentials := &centrifuge.Credentials{
	    UserID: "42",
	    Exp:    time.Now().Unix() + 600,
	    Info:   []byte(`{"name": "Alexander"}`),
	}
	ctx = context.WithValue(
            ctx, 
            centrifuge.CredentialsContextKey,
            credentials,
        )
	r = r.WithContext(ctx)
	h.ServeHTTP(w, r)
    })
}

Аутентификация

authInterceptor := func(
    srv interface{}, 
    ss grpc.ServerStream, 
    info *grpc.StreamServerInfo, 
    handler grpc.StreamHandler
) error {
	ctx := ss.Context()
	newCtx := context.WithValue(
            ctx, 
            centrifuge.CredentialsContextKey, 
            &centrifuge.Credentials{
		UserID: "42",
		Exp:    time.Now().Unix() + 10,
		Info:   []byte(`{"name": "Alexander"}`),
	    },
        )
	wrapped := grpc_middleware.WrapServerStream(ss)
	wrapped.WrappedContext = newCtx
	return handler(srv, wrapped)
}

Возможности протокола

PUB/SUB

var centrifuge = new Centrifuge("ws://example.com/websocket");

centrifuge.subscribe("channel", function(message){
    console.log(message);
});

centrifuge.connect();

Асинхронное двунаправленное общение

var centrifuge = new Centrifuge("ws://example.com/websocket");

centrifuge.on("connect", function(){
    centrifuge.send({"text": "hello"});
});

centrifuge.on("message", function(data) {
    console.log(data);
});

centrifuge.connect();

RPC

var centrifuge = new Centrifuge("ws://example.com/websocket");

centrifuge.rpc({"method": "say_hi"}).then(function(data){
    console.log(data);
});

centrifuge.connect();

PRESENCE

JOIN and LEAVE

RECOVER

PING-PONG

REFRESH

Оптимизации

  • Производительный язык (Go)
  • Слияние сообщений для экономии на syscall'ах
  • Операции в Redis с помощью LUA для экономии RTT
  • Buffer pool для JSON, Gogoprotobuf для Protobuf
  • Неблокирующая отправка сообщения в канал
  • Батчинг сообщений со стороны клиента
> XADD mystream * payload "hello"
1506871964177.0


> XRANGE mystream 1506871964175.0 +
1) 1) 1506871964177.0
   2) 1) "payload"
      2) "hello"



> XREAD BLOCK 5000 STREAMS mystream $

Redis STREAMs?

Почему все не так радужно? 

Клиенты

Бенчмарки

Спасибо за внимание!

Присоединяйтесь!

Centrifugo and Centrifuge

By Emelin Alexander

Centrifugo and Centrifuge

  • 2,849