WebAssembly (WASM) is a great choice for user-defined functions, due to the fact that it was designed to be easily embeddable, with a focus on security and speed. Still, executing functions provided by users should not cause latency spikes - it's important for individual database clusters, and absolutely crucial for multi-tenancy. In order to keep latency low, one can utilize a WebAssembly runtime with async support. One such runtime is Wasmtime, a Rust project perfectly capable of running WebAssembly functions cooperatively and asynchronously. This talk briefly describes WebAssembly and Wasmtime, and shows how to integrate them into a C++ project in a latency-friendly manner, while implementing the core runtime for user-defined functions in async Rust.
Unleash Your Potential - Namagunga Girls Coding Club
Keeping Latency Low for User-Defined Functions with WebAssembly
1. Brought to you by
Keeping Latency Low
for User-Defined Functions
with WebAssembly
Piotr Sarna
Principal Software Engineer
2. 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
4. 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)
5. 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);
6. 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)
8. 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).
9. What compiles to WebAssembly
● AssemblyScript
● Rust
● C++
● and more!
10. 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)
12. 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
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
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
16. 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
17. 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();
}
20. 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!
21. Brought to you by
Piotr Sarna
https://github.com/psarna
https://www.linkedin.com/in/piotr-sarna-548a76a3