COLLABORATIVE
APPLICATIONS
AT SCALE WITH
RSOCKET
Sergey Tselovalnikov
@SerCeMan
Canva
SCALE
Why should you care?
Canva
Alice
Bob
Canva
Alice
Bob
Add "!"
Canva
Alice
Bob
Add "!"
Alice
added "!"
Alice
Canva
Bob Dillon
Caroll
s
o
g
o
o
d
!
DESIGN CHOICES
Application
Architecture Infrastructure
Architecture
Bob
WebSocket
Bob
Bob
Bob
Multiplexing
O(1) vs O(N)
Editing
Presenting
WebSocket
Gateway
Editing
Presenting
WebSocket
Gateway
WebSocket
Gateway
RSocket WebSocket connection
WebSocket
Gateway
Presenting Channel
RSocket WebSocket connection
WebSocket
Gateway
Editing Channel
Presenting Channel
RSocket WebSocket connection
Editing
Presenting
WebSocket
Gateway
No Backpressure
Editing
Presenting
WebSocket
Gateway
No Backpressure
Editing
Presenting
WebSocket
Gateway
No Backpressure
Editing
Presenting
WebSocket
Gateway
Editing
Presenting
WebSocket
Gateway
Application
RSocket-Java
Transport
- how to send & receive
Acceptor
- how to handle
public interface DuplexConnection {
Mono<Void> send(Publisher<ByteBuf> frames);
Flux<ByteBuf> receive();
}
public interface DuplexConnection {
Mono<Void> send(Publisher<ByteBuf> frames);
Flux<ByteBuf> receive();
} RSocket
Frame
RSocket
Frame
class RsInterceptor implements DuplexConnectionInterceptor {
@Override
public DuplexConnection apply(Type type, DuplexConnection connection) {
return new InstrumentedRsConnection(type, connection);
}
static class InstrumentedRsConnection implements DuplexConnection {
@Override
public Mono<Void> send(Publisher<ByteBuf> frames) {
return connection.send(Flux.from(frames) //
.doOnNext(frame -> recordFrame("out", frame, connectionType)));
}
...
}
wraps the
connection
record each
frame
Transport metrics
REQ
OK
ERROR
rate(ERROR)
rate(OK + ERROR)
Transport metrics
NEXT NEXT NEXT
COMPLETE
ERROR
Transport metrics
NEXT NEXT NEXT NEXT
COMPLETE
ERROR
+ CANCEL frame
Transport metrics
NEXT NEXT NEXT NEXT
COMPLETE
ERROR
+ cancellation
+ in/out
rate(ERROR)
rate(ALL() - KEEPALIVE)
Error rate
rate(CANCEL)
rate(ALL() - KEEPALIVE)
Cancel rate
Acceptor
public interface RSocket {
Flux<Payload> requestChannel(Flux<Payload> incoming) {
...
}
}
incoming
messages
outgoing
messages
Intercepting Acceptor
class ForwardingRSocket implements RSocket {
private final RSocket delegate;
public ForwardingRSocket(RSocket delegate) {
this.delegate = delegate;
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return delegate.requestChannel(payloads);
}
...
What's the latency for establishing a channel?
Intercepting Acceptor
What's the latency for establishing a channel?
For how long do the channels stay alive?
Flux<Payload> recordSignal(Supplier<Flux<Payload>> signalProvider) {
var time = Timer.start("RSOCKET_STREAM_TIMER");
return signalProvider.get().doFinally(signal -> {
time.stop();
});
}
Intercepting Acceptor
Logging
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
var incoming = Flux.from(payloads)
.doOnSubscribe(s -> logger.info("subscribed to incoming"))
.doOnCancel(() -> logger.info("unsubscribed from incoming"))
.doOnComplete(() -> logger.info("complete incoming"))
.doOnError(e -> logger.info("error incoming"));
var outgoing = delegate().requestChannel(incoming);
return outgoing //
.doOnSubscribe(s -> logger.info("subscribed to outgoing"))
.doOnCancel(() -> logger.info("unsubscribed from outgoing"))
.doOnComplete(() -> logger.info("complete outgoing"))
.doOnError(e -> logger.info("error outgoing"));
}
}
Logging Caveats
Sep 3, 2021 @ 18:42:42.042 socket connected
Sep 3, 2021 @ 18:42:42.042 socket connected
Sep 3, 2021 @ 18:42:42.042 subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 unsubscribed from outgoing
Sep 3, 2021 @ 18:42:42.042 unsubscribed from incoming
Sep 3, 2021 @ 18:42:42.042 socket closed
Sep 3, 2021 @ 18:42:42.042 subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 subscribed to incoming
Sep 3, 2021 @ 18:42:42.042 subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 socket connected
Sep 3, 2021 @ 18:42:42.042 unsubscribed from incoming
Sep 3, 2021 @ 18:42:42.042 socket closed
Sep 3, 2021 @ 18:42:42.042 socket connected
Sep 3, 2021 @ 18:42:42.042 subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 unsubscribed from outgoing
Sep 3, 2021 @ 18:42:42.042 socket connected
Sep 3, 2021 @ 18:42:42.042 subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 socket closed
Sep 3, 2021 @ 18:42:42.042 socket closed
Sep 3, 2021 @ 18:42:42.042 unsubscribed from incoming
Logging Caveats
Sep 3, 2021 @ 18:42:42.042 685424232abc socket connected
Sep 3, 2021 @ 18:42:42.042 685424232def socket connected
Sep 3, 2021 @ 18:42:42.042 685424232abc subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 685424232def unsubscribed from outgoing
Sep 3, 2021 @ 18:42:42.042 685424232abc unsubscribed from incoming
Sep 3, 2021 @ 18:42:42.042 685424232ghi socket closed
Sep 3, 2021 @ 18:42:42.042 685424232abc subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 685424232def subscribed to incoming
Sep 3, 2021 @ 18:42:42.042 685424232abc subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 685424232ghi socket connected
Sep 3, 2021 @ 18:42:42.042 685424232abc unsubscribed from incoming
Sep 3, 2021 @ 18:42:42.042 685424232def socket closed
Sep 3, 2021 @ 18:42:42.042 685424232ghi socket connected
Sep 3, 2021 @ 18:42:42.042 685424232abc subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 685424232ghi unsubscribed from outgoing
Sep 3, 2021 @ 18:42:42.042 685424232def socket connected
Sep 3, 2021 @ 18:42:42.042 685424232ghi subscribed to outgoing
Sep 3, 2021 @ 18:42:42.042 685424232abc socket closed
Sep 3, 2021 @ 18:42:42.042 685424232ghi socket closed
Sep 3, 2021 @ 18:42:42.042 685424232abc unsubscribed from incoming
Logging Caveats
if (Random.nextInt(0, 1_000_000) < flags.get(SAMPLING_RATE)) {
setAttribute("sampled", true)
}
Service Definition
service FooService {
rpc connect (stream IncomingMsg) returns (stream OutgoingMsg) {
option (connectMessage) = "ConnectMsg"
}
}
Flux<Payload> => Flux<IncomingMsg>
Flowable
Observable
backpressure
no
backpressure
Backpressure
strategy
Flowable
Observable
backpessure
no
backpessure
Reconnection
/**
* Returns a Flowable that immediately publishes the current connection
* status and thereafter updates as it changes. Once a connection is in
* the CLOSED or ERROR state, it may not be connected again.
* Implementations must publish values per the comments on ConnectionStatus.
*/
connectionStatus(): Flowable<ConnectionStatus>;
Exponential backoff
function backoff(): () => number {
let i = 0;
return () => Math.pow(2, i++);
}
...
const next = backoff();
...
await Math.round(next() * 1000);
Jitter
function backoff(): () => number {
let i = 0;
return () =>
Math.pow(1.5, Math.random() + i++);
}
...
const next = backoff();
...
await Math.round(next() * 1000);
WebSocket Inspector
RSocket Frame Inspector
https://github.com/rsocket/rsocket-chrome-devtools
Load Testing
50
40
30
20
10
0
Load shedding
75
50
25
0
Instance configuration
Number of open files
Low latency GC (ShenandoahGC for memory)
Infrastructure
Load Balancing
Backend
Backend
Backend
WebSocket
Gateway
Backend
Backend
Backend
WebSocket
Gateway
1
2
Backend
Backend
Backend
WebSocket
Gateway
1
2
Backend
Backend
WebSocket
Gateway
Backend
Backend
WebSocket
Gateway
Backend
Backend
Backend
WebSocket
Gateway
Backend
Backend
Backend
WebSocket
Gateway
2
2
Backend
Backend
WebSocket
Gateway
Backend
Backend
WebSocket
Gateway
Service
Registry
Backend
Backend
WebSocket
Gateway
Service
Registry
Backend
Backend
Backend
Service
Registry
Backend
WebSocket
Gateway
Backend
Backend
Service
Registry
Backend
WebSocket
Gateway
50000
50000
1
Backend
Backend
Service
Registry
Backend
WebSocket
Gateway
50001
50001
2
Connecting
Healthy Disposed
Unhealthy
Healthy
Healthy
Healthy
50000
50000
1
var socket = healthy.stream()
.filter(s -> s.availability() > 0)
.min(s -> s.openChannels());
Availability
/**
* @return a positive number representing the availability of the entity.
* Higher is better, 0.0 means not available
*/
double availability();
0 1
warm up
Healthy
Healthy
Healthy
50000
50000
1
var socket = healthy.stream()
.filter(s -> s.availability() > 0)
.min(s -> {
var active = s.activeChannels();
var availability = s.availability();
return active / availability;
});
Autoscaling
Autoscaling target
Primary: the number of connections
Autoscaling target
Primary: the number of connections
Autoscaling target
Primary: the number of connections
Secondary: CPU
Redeployments
Redeployments
WebSocket
Gateway
WebSocket
Gateway
old
new
Redeployments
deregistration delay
deregistration
started
Conclusion
Thank you
Sergey Tselovalnikov
@SerCeMan
Canva

Collaborative Applications at Scale with RSocket