Seastar Framework Documentation

repository·master·Indexed 27 days ago

https://github.com/scylladb/seastar

An event-driven framework for writing high-performance, non-blocking, asynchronous code using a future-based model. Includes guides on building from source, configuring build modes (debug, release, dev, sanitize), integrating with DPDK, and using KVM or Docker environments. Provides detailed coding style conventions for C++20/C++23, including naming patterns, header inclusion, and API version management via Seastar_API_LEVEL.

Tokens
40.4K
Snippets
107
Records
194
Agent score
93%

What's inside Seastar

  1. Overview of Seastar Asynchronous Framework

    master

    Seastar is a C++14 library designed for building highly efficient, complex server applications on modern multi-core machines. It addresses the limitations of traditional synchronous (process/thread-per-connection) and simple asynchronous (callback-heavy) models by providing a framework that handles both network and disk I/O with optimal performance.

    Key architectural pillars include:

    • Cooperative micro-task scheduler: Each core runs a lightweight scheduler that processes tasks (e.g., handling I/O results) without the overhead of OS context switching.
    • Share-nothing SMP architecture: Each core (often called a shard) operates independently. Memory and data structures are not shared; instead, cores communicate via explicit message passing to avoid cache line bouncing and lock contention.
    • Future-based APIs: Uses futures and continuations to represent and chain asynchronous events (network, disk, or inter-core communication) in a manageable way.
    • High-performance stacks: Includes a share-nothing TCP/IP stack with zero-copy capabilities and DMA-based storage APIs for zero-copy disk I/O.
  2. Understand the Seastar IO Scheduler mechanism

    master

    The Seastar IO scheduler uses a rate-limiter to throttle the amount of data dispatched to the disk. It prevents disk saturation by modeling disk behavior using a combination of bandwidth and IOPS (Input/Output Operations Per Second) for both reads and writes.

    Core Concepts

    • Tickets: Every IO request is assigned a 2D value called a "ticket".
      • Reads: Assigned a value of (1, bytes).
      • Writes: Assigned a value of (m_o, m_b * bytes), where m_o and m_b are normalization constants derived from the maximum possible IOPS and bandwidth for reads and writes.
    • Normalization: The scheduler converts these tickets into a single cost using the formula: N(ticket) = (ticket_0 / iops_r_max) + (ticket_1 / bandwidth_r_max)
    • Token Bucket: The time-derivative limitation is implemented via a token bucket algorithm. The bucket has a refill rate of 1.0. Each request consumes a fractional token value equal to its normalized cost N(ticket).
    • Latency Goal: The maximum capacity of the token bucket (limit) is calculated based on the io_latency_goal_ parameter, representing the amount of tokens that can accumulate during that target duration.
  3. Understand SeaStar performance tuning (Polling vs Interrupts)

    master

    SeaStar is tuned for high performance (100,000+ IOPS) by favoring polling over interrupt-driven I/O.

    Implication: Because of polling, each core will consume 100% CPU even when no work is being performed. This is a deliberate design choice to minimize latency and maximize throughput for high-load applications.

  4. Understand Seastar compatibility guarantees

    master

    Seastar provides the following compatibility guarantees:

    • Source Compatibility: Application code should continue to build with newer versions of Seastar.
    • Protocol Compatibility: Binary protocols exposed by Seastar (e.g., RPC) are maintained.
    • Link Compatibility (NOT maintained): You cannot link an application built with one version of Seastar with a different version of Seastar.

    Language and Compiler Support:

    • C++ Standards: Seastar supports the last two standards approved by the ISO C++ committee (e.g., if C++20 is out, it supports C++17 and C++20).
    • Compilers: Supports GCC and Clang (specifically the last two major releases).
  5. Use the asymmetric_io_uring reactor backend

    master

    The asymmetric_io_uring reactor backend optimizes compute-intensive workloads by offloading networking and disk I/O processing from application shards onto dedicated CPU cores called worker cores.

    Unlike traditional backends, this backend removes the speculative 'fast track' for networking I/O; all I/O requests are consistently submitted via io_uring and processed off-shard. This prevents application cores from splitting time between computation and synchronous system calls.

    Note: This backend is unlikely to improve I/O-bound workloads because multiple shards share the same worker cores, which can create a bottleneck.

    ./your_seastar_app --reactor-backend=asymmetric_io_uring --async-workers-cpuset=14-15
  6. Wait for background operations in a loop using a gate

    master

    If you are using an external semaphore to limit total parallelism across multiple different loops, you cannot use that semaphore to wait for a specific loop's completion (as limit.wait(N) would wait for all loops to finish).

    Instead, use a seastar::gate to track the lifecycle of operations within a specific loop. Use gate.enter() when starting an operation and gate.leave() in a .finally() block when the operation completes. Finally, call gate.close() to wait for all operations associated with that gate to finish.

    thread_local seastar::semaphore limit(100);
    seastar::future<> f() {
        return seastar::do_with(seastar::gate(), [] (auto& gate) {
            return seastar::do_for_each(boost::counting_iterator<int>(0),
                    boost::counting_iterator<int>(456), [&gate] (int i) {
                return seastar::get_units(limit, 1).then([&gate] (auto units) {
                    gate.enter();
                    seastar::futurize_invoke(slow).finally([&gate, units = std::move(units)] {
                        gate.leave();
                    });
                });
            }).finally([&gate] {
                return gate.close();
            });
        });
    }
  7. Consume Seastar from the build directory

    master

    You can use Seastar without installing it by pointing your build system to the build directory. Assume the Seastar repository is at $seastar_dir.

    # Via pkg-config
    $ g++ my_app.cc $(pkg-config --libs --cflags --static $seastar_dir/build/release/seastar.pc) -o my_app
    
    # Via CMake
    # 1. Create a CMakeLists.txt with:
    set (CMAKE_CXX_STANDARD 23)
    find_package (Seastar REQUIRED)
    add_executable (my_app my_app.cc)
    target_link_libraries (my_app Seastar::seastar)
    
    # 2. Run cmake with specific paths:
    $ mkdir $my_app_dir/build
    $ cd $my_app_dir/build
    $ cmake -DCMAKE_PREFIX_PATH="$seastar_dir/build/release;$seastar_dir/build/release/_cooking/installed" -DCMAKE_MODULE_PATH=$seastar_dir/cmake $my_app_dir
  8. Fetch dependencies locally using --cook

    master

    If a dependency is missing, you can instruct the configuration process to fetch a specific dependency locally for development using the --cook flag.

    $ ./configure.py --mode=dev --cook fmt
  9. Write asynchronous code using Seastar coroutines

    master

    The preferred way to write efficient asynchronous code in Seastar is using C++20 coroutines. A coroutine is a function that returns a seastar::future<T> and utilizes the co_await or co_return keywords. Coroutines integrate seamlessly with traditional Seastar code; they can be called from non-coroutine functions and can call non-coroutine functions.

    Requirements:

    • C++20 support.
    • A compatible compiler (Clang 10+ is known to work).

    When using co_await on a seastar::future, if the future is not ready, the coroutine suspends, allowing Seastar to perform other work. Once the future is ready, the coroutine resumes, and the value is extracted.

    #include <seastar/core/coroutine.hh>
    
    seastar::future<int> read();
    seastar::future<> write(int n);
    
    seastar::future<int> slow_fetch_and_increment() {
        auto n = co_await read();     // #1
        co_await seastar::sleep(1s);  // #2
        auto new_n = n + 1;           // #3
        co_await write(new_n);        // #4
        co_return n;                  // #5
    }
  10. Define user-defined command-line options using `app_template`

    master

    Seastar applications should use seastar::app_template to manage command-line arguments. This allows you to integrate your custom options with Seastar's standard options (like -c for threads and -m for memory) using boost::program_options.

    To add options:

    1. Instantiate seastar::app_template.
    2. Use app.add_options() to define flags and valued options.
    3. Use app.add_positional_options() for arguments that don't start with a dash.
    4. Call app.run(argc, argv, callback) to start the application. Inside the callback, retrieve settings via app.configuration().
    #include <seastar/core/app-template.hh>
    #include <seastar/core/reactor.hh>
    
    int main(int argc, char** argv) {
        seastar::app_template app;
        namespace bpo = boost::program_options;
        app.add_options()
            ("flag", "some optional flag")
            ("size,s", bpo::value<int>()->default_value(100), "size")
            ;
        app.add_positional_options({
           { "filename", bpo::value<std::vector<seastar::sstring>>()->default_value({}),
             "sstable files to verify", -1}
        });
        app.run(argc, argv, [&app] {
            auto& args = app.configuration();
            if (args.count("flag")) {
                std::cout << "Flag is on\n";
            }
            std::cout << "Size is " << args["size"].as<int>() << "\n";
            return seastar::make_ready_future<>();
        });
        return 0;
    }
  11. Optimize Coroutine Generator performance

    master

    Seastar provides two generator variants. Choosing the correct one is critical for performance based on your data type and latency requirements.

    Unbuffered Generator

    Best for:

    • Large objects where moves are expensive.
    • Latency-critical applications (need the first element immediately).
    • Memory-constrained environments.
    • Elements that are naturally references to existing data.

    Characteristics:

    • Zero-copy: Stores a pointer to the value in the producer's stack frame.
    • One suspension per element.

    Buffered Generator

    Best for:

    • Throughput-oriented applications.
    • Small, cheap-to-move elements (integers, small PODs).
    • Scenarios where the producer can generate elements in batches.

    CRITICAL PERFORMANCE NOTE: When using a buffered generator, you must use a fixed-capacity container like circular_buffer_fixed_capacity to avoid heap allocations. Using std::vector can make the generator up to 2.6x slower and increase allocations due to dynamic memory overhead.

    Characteristics:

    • Amortized suspensions (multiple elements per suspension).
    • Moves elements into a buffer for independent lifetime.