Centrifugo – building language-agnostic real-time messaging server in Go. And one more Centrifuge
Centrifugo
Real-time messages
Real-time solutions
Primus
Firebase
Meteor
Phoenix
pusher.com
pubnub.com
crossbar.io
Atmosphere
socket.io
Faye
Horizon
deepstreamhub
emitter.io
feathers.js
SignalR
Pushpin
SocketCluster
Nchan
Initial motivation
Simplified Centrifugo scheme
Features
- Language-agnostic
- Decoupled from application backend
- JWT authentication with expiration support
- Simple integration with existing application
- Scale horizontally with Redis
- Solid performance
- Message recovery after short disconnects
- Information about active subscribers in channel
- Cross platform (Linux, MacOS, Windows)
- MIT license
- Works everywhere (browser, mobile)
- Works over same ports as HTTP
- Low-overhead
- Bidirectional
Websocket advantages
Websocket
Actually not that bad anymore
- xhr-streaming 👍
- eventsource 👍
- iframe-eventsource
- iframe-htmlfile
- xhr-polling
- iframe-xhr-polling
- jsonp-polling
SockJS fallbacks
js
Websocket and HTTP/2
Running the WebSocket Protocol (RFC 6455) over a single stream of an HTTP/2 connection:
Internals
Client-to-server transports
Websocket – https://github.com/gorilla/websocket
SockJS – https://github.com/igm/sockjs-go
GRPC bidirectional streaming
Websocket vs GRPC streaming:
Websocket | GRPC streaming | |
---|---|---|
Server memory usage with 10k clients | ~ 500 mb | ~ 2000 mb |
Cpu usage under the same load | ~ 30% | ~ 85% |
Traffic over interface in same scenario | ~ 17 mb | ~ 19 mb |
Protocol
- Custom
- Defined in protocol buffers schema
- Looks similar to JSON RPC 2.0 on high level
- Similar to MQTT on business level (but no QOS)
- Two serialization formats: JSON and Protobuf
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
Engine
- Responsible for PUB/SUB mechanics
- Manages history cache
- Manages presence information
Built-in engines
- In memory
- Redis Engine – thanks again Gary Burd
Message delivery model
At most once
+ with recovery feature
Optimizations
- Gogoprotobuf library for protocol buffers
Gogoprotobuf
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
BenchmarkGogoprotobufMarshal-8 10000000 152 ns/op 64 B/op 1 allocs/op
BenchmarkGogoprotobufUnmarshal-8 10000000 221 ns/op 96 B/op 3 allocs/op
Optimizations
- Gogoprotobuf library for protocol buffers
- Reduce syscalls by merging messages
BenchmarkMerge-4 20000 75249 ns/op 69170 B/op 63 allocs/op
BenchmarkNoMerge-4 10000 193548 ns/op 63568 B/op 213 allocs/op
Reducing syscalls
{"id": 1, "method": "subscribe", "params": {"channel": "ch1"}}
{"id": 2, "method": "subscribe", "params": {"channel": "ch2"}}
JSON streaming
Varint-prefixed Protobuf
Optimizations
- Gogoprotobuf library for protocol buffers
- Reduce syscalls by merging messages
- sync.Pool to reuse buffers
- Redis pipelining with smart batching technique
Smart batching
subCh := make(chan string, 128)
for i := 0; i < 128; i++ {
subCh <- "channel"
}
maxBatchSize := 50
for {
select {
case channel := <-subCh:
batch := []string{channel}
loop:
for len(batch) < maxBatchSize {
select {
case channel := <-subCh:
batch = append(batch, channel)
default:
break loop
}
}
println(len(batch))
}
}
> go run main.go
50
50
28
Optimizations
- Gogoprotobuf library for protocol buffers
- Reduce syscalls by merging messages
- sync.Pool to reuse buffers
- Redis pipelining with smart batching technique
- Save RTT amount with Redis using Lua scripts
- Batch messages on client side
Prometheus
(+ export to Graphite)
Metrics
Dependency management
Goreleaser
Release management
Centrifuge library
Beyond Centrifugo
- Native authentication with middleware
- Tight integration with business logic
- Bidirectional messaging
- Built-in RPC calls
- More freedom in channel permission management
package main
import (
"context"
"log"
"net/http"
cent "github.com/centrifugal/centrifuge"
)
func main() {
cfg := centrifuge.DefaultConfig
cfg.ClientInsecure = true
cfg.Publish = true
node, _ := centrifuge.New(cfg)
node.On().Connect(func(ctx context.Context, cl *cent.Client, e cent.ConnectEvent) cent.ConnectReply {
cl.On().Subscribe(func(e cent.SubscribeEvent) cent.SubscribeReply {
log.Printf("client subscribes on channel %s", e.Channel)
return cent.SubscribeReply{}
})
cl.On().Publish(func(e cent.PublishEvent) cent.PublishReply {
log.Printf("client publishes into channel %s: %s", e.Channel, string(e.Data))
return cent.PublishReply{}
})
cl.On().Disconnect(func(e cent.DisconnectEvent) cent.DisconnectReply {
log.Printf("client disconnected")
return cent.DisconnectReply{}
})
log.Println("client connected")
return cent.ConnectReply{}
})
if err := node.Run(); err != nil {
panic(err)
}
http.Handle("/connection/websocket", cent.NewWebsocketHandler(node, cent.WebsocketConfig{}))
go func() {
if err := http.ListenAndServe(":8000", nil); err != nil {
panic(err)
}
}()
}
How it feels
node.On().Connect(func(
ctx context.Context,
client *Client,
e ConnectEvent
) ConnectReply {
client.On().Subscribe(func(e SubscribeEvent) SubscribeReply {
log.Printf("client subscribes on channel %s", e.Channel)
return SubscribeReply{}
})
client.On().Publish(func(e PublishEvent) PublishReply {
log.Printf("client publishes into channel %s: %s", e.Channel, e.Data)
return PublishReply{}
})
client.On().Disconnect(func(e DisconnectEvent) DisconnectReply {
log.Printf("client disconnected")
return DisconnectReply{}
})
log.Println("client connected")
return ConnectReply{}
})
How it feels with zoom
Clients
- centrifuge-js
- centrifuge-go
- centrifuge-mobile (based on gomobile)
- centrifuge-dart (WIP)
Some examples
PUB/SUB
var centrifuge = new Centrifuge("ws://example.com/websocket");
centrifuge.subscribe("channel", function(message){
console.log(message);
});
centrifuge.connect();
Bidirectional messaging
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();
Links
Questions?
Centrifugo – building language-agnostic real-time messaging server. And one more Centrifuge
By Emelin Alexander
Centrifugo – building language-agnostic real-time messaging server. And one more Centrifuge
- 2,704