Brought to you by
Keeping Latency Low
for User-Defined Functions
with WebAssembly
Piotr Sarna
Principal Software Engineer
Piotr Sarna
Principal Software Engineer at ScyllaDB
■ Keen on open-source projects, C++ and Rust
■ Used to develop a distributed file system
■ Wrote a few patches for the Linux kernel
■ MSc in Computer Science from University of Warsaw
■ Maintainer of the ScyllaDB Rust Driver project
Motivation
User-defined functions
ScyllaDB allows users to define their own functions for data transformations:
scylla@cqlsh:ks> SELECT id, inv(id), mult(id, inv(id)) FROM t;
id | ks.inv(id) | ks.mult(id, ks.inv(id))
----+------------+-------------------------
7 | 0.142857 | 1
1 | 1 | 1
0 | Infinity | NaN
4 | 0.25 | 1
(4 rows)
User-defined aggregates
True power of user-defined functions:
being a building block for user-defined aggregates!
CREATE FUNCTION accumulate_len(acc tuple<bigint,bigint>, a text)
RETURNS NULL ON NULL INPUT RETURNS tuple<bigint,bigint>
LANGUAGE lua as 'return {acc[1] + 1, acc[2] + #a}';
CREATE OR REPLACE FUNCTION present(res tuple<bigint,bigint>)
RETURNS NULL ON NULL INPUT RETURNS text
LANGUAGE lua as
'return "The average string length is " .. res[2]/res[1] .. "!"';
CREATE OR REPLACE AGGREGATE avg_length(text)
SFUNC accumulate_len
STYPE tuple<bigint,bigint>
FINALFUNC present INITCOND (0,0);
User-defined aggregates
True power of user-defined functions:
being a building block for user-defined aggregates!
scylla@cqlsh:ks> SELECT * FROM words;
word
------------
monkey
rhinoceros
dog
(3 rows)
scylla@cqlsh:ks> SELECT avg_length(word) FROM words;
ks.avg_length(word)
-----------------------------------------------
The average string length is 6.3333333333333!
(1 rows)
WebAssembly
WebAssembly
Binary format for expressing executable code
● easily embeddable
● portable
● secure
● efficient
WebAssembly is a binary format, but it can also be expressed in
a human-readable way: WAT (WebAssembly Text Format).
What compiles to WebAssembly
● AssemblyScript
● Rust
● C++
● and more!
Wasmtime
"A fast and secure runtime for WebAssembly"
● implemented in Rust
● good async support
● customizable
● large community
● frequent releases
alternative to consider: Wasmer (http://wasmer.io)
Integration with Seastar
Requirements
WebAssembly integration should have the following characteristics:
● long computations cannot produce stalls
○ it should be capable of yielding the CPU periodically in order to let other tasks proceed
● memory allocations should be under control
○ Seastar uses its own sharded allocators
○ ideally, allocations performed by runtime should use Seastar's memory management
● overhead induced by the integration should be minimized
Async Rust model
● Based on futures
● Each future represents a computation
○ it can be thought of as a state machine
● Runtime is responsible for moving the computation forward
○ in other words, transitioning the machine to the next state
Async Rust + async C++
Asynchronous C++ code is often written in an async framework:
● Seastar
● boost::asio
● libuv
Asynchronous Rust code needs a runtime to be executed,
but doesn't need to depend on any particular runtime.
Computation-only state machine
An async Rust function can be classified as computation-only
if it does not rely on any blocking operations like:
● accessing disks
● communicating over the network
● delaying its execution (think tokio::time::sleep)
● etc.
Computation-only futures have a very nice characteristic:
polling a computation-only future always moves the state machine forward
A minimalistic runtime
An optimal runtime for a single computation-only function is very straightforward:
while the future is not ready yet:
poll the future to proceed with the computation
yield CPU to let other tasks proceed as well
Integration with C++
Rust is capable of exporting C symbols, which means that Rust functions
are callable directly from C++.
It means that the minimalistic runtime from the previous slide
can be implemented in C++ too!
while (!ready) {
ready = poll_rust_future(fut);
maybe_yield();
}
C++ bindings
Option 1:
extern "C"
Option 2:
cxx: https://crates.io/crates/cxx
Integration with Seastar
while (!stop) {
std::exception_ptr eptr;
try {
stop = udf_computation->resume();
} catch (const rust::Error& e) {
eptr = std::make_exception_ptr(wasm::instance_corrupting_exception());
}
if (eptr) {
co_await coroutine::return_exception_ptr(eptr);
}
co_await coroutine::maybe_yield();
}
Ref: https://github.com/scylladb/scylladb/pull/11351 (by @wmitros)
Architecture
1. Rust module responsible for running user-defined functions
implemented in WebAssembly
2. A few lines of glue code responsible for polling the Rust module
That's it!
Brought to you by
Piotr Sarna
https://github.com/psarna
https://www.linkedin.com/in/piotr-sarna-548a76a3

Keeping Latency Low for User-Defined Functions with WebAssembly

  • 1.
    Brought to youby Keeping Latency Low for User-Defined Functions with WebAssembly Piotr Sarna Principal Software Engineer
  • 2.
    Piotr Sarna Principal SoftwareEngineer at ScyllaDB ■ Keen on open-source projects, C++ and Rust ■ Used to develop a distributed file system ■ Wrote a few patches for the Linux kernel ■ MSc in Computer Science from University of Warsaw ■ Maintainer of the ScyllaDB Rust Driver project
  • 3.
  • 4.
    User-defined functions ScyllaDB allowsusers to define their own functions for data transformations: scylla@cqlsh:ks> SELECT id, inv(id), mult(id, inv(id)) FROM t; id | ks.inv(id) | ks.mult(id, ks.inv(id)) ----+------------+------------------------- 7 | 0.142857 | 1 1 | 1 | 1 0 | Infinity | NaN 4 | 0.25 | 1 (4 rows)
  • 5.
    User-defined aggregates True powerof user-defined functions: being a building block for user-defined aggregates! CREATE FUNCTION accumulate_len(acc tuple<bigint,bigint>, a text) RETURNS NULL ON NULL INPUT RETURNS tuple<bigint,bigint> LANGUAGE lua as 'return {acc[1] + 1, acc[2] + #a}'; CREATE OR REPLACE FUNCTION present(res tuple<bigint,bigint>) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE lua as 'return "The average string length is " .. res[2]/res[1] .. "!"'; CREATE OR REPLACE AGGREGATE avg_length(text) SFUNC accumulate_len STYPE tuple<bigint,bigint> FINALFUNC present INITCOND (0,0);
  • 6.
    User-defined aggregates True powerof user-defined functions: being a building block for user-defined aggregates! scylla@cqlsh:ks> SELECT * FROM words; word ------------ monkey rhinoceros dog (3 rows) scylla@cqlsh:ks> SELECT avg_length(word) FROM words; ks.avg_length(word) ----------------------------------------------- The average string length is 6.3333333333333! (1 rows)
  • 7.
  • 8.
    WebAssembly Binary format forexpressing executable code ● easily embeddable ● portable ● secure ● efficient WebAssembly is a binary format, but it can also be expressed in a human-readable way: WAT (WebAssembly Text Format).
  • 9.
    What compiles toWebAssembly ● AssemblyScript ● Rust ● C++ ● and more!
  • 10.
    Wasmtime "A fast andsecure runtime for WebAssembly" ● implemented in Rust ● good async support ● customizable ● large community ● frequent releases alternative to consider: Wasmer (http://wasmer.io)
  • 11.
  • 12.
    Requirements WebAssembly integration shouldhave the following characteristics: ● long computations cannot produce stalls ○ it should be capable of yielding the CPU periodically in order to let other tasks proceed ● memory allocations should be under control ○ Seastar uses its own sharded allocators ○ ideally, allocations performed by runtime should use Seastar's memory management ● overhead induced by the integration should be minimized
  • 13.
    Async Rust model ●Based on futures ● Each future represents a computation ○ it can be thought of as a state machine ● Runtime is responsible for moving the computation forward ○ in other words, transitioning the machine to the next state
  • 14.
    Async Rust +async C++ Asynchronous C++ code is often written in an async framework: ● Seastar ● boost::asio ● libuv Asynchronous Rust code needs a runtime to be executed, but doesn't need to depend on any particular runtime.
  • 15.
    Computation-only state machine Anasync Rust function can be classified as computation-only if it does not rely on any blocking operations like: ● accessing disks ● communicating over the network ● delaying its execution (think tokio::time::sleep) ● etc. Computation-only futures have a very nice characteristic: polling a computation-only future always moves the state machine forward
  • 16.
    A minimalistic runtime Anoptimal runtime for a single computation-only function is very straightforward: while the future is not ready yet: poll the future to proceed with the computation yield CPU to let other tasks proceed as well
  • 17.
    Integration with C++ Rustis capable of exporting C symbols, which means that Rust functions are callable directly from C++. It means that the minimalistic runtime from the previous slide can be implemented in C++ too! while (!ready) { ready = poll_rust_future(fut); maybe_yield(); }
  • 18.
    C++ bindings Option 1: extern"C" Option 2: cxx: https://crates.io/crates/cxx
  • 19.
    Integration with Seastar while(!stop) { std::exception_ptr eptr; try { stop = udf_computation->resume(); } catch (const rust::Error& e) { eptr = std::make_exception_ptr(wasm::instance_corrupting_exception()); } if (eptr) { co_await coroutine::return_exception_ptr(eptr); } co_await coroutine::maybe_yield(); } Ref: https://github.com/scylladb/scylladb/pull/11351 (by @wmitros)
  • 20.
    Architecture 1. Rust moduleresponsible for running user-defined functions implemented in WebAssembly 2. A few lines of glue code responsible for polling the Rust module That's it!
  • 21.
    Brought to youby Piotr Sarna https://github.com/psarna https://www.linkedin.com/in/piotr-sarna-548a76a3