Getting started with the smf rpc framework

Cross-posted from https://nwat.xyz/blog/2019/03/22/getting-started-with-the-smf-rpc-framework/.

In this post we are going to take a high-level look at the smf rpc framework, and walk through the process of creating a minimal client/server example. This post will show to setup a build tree, and bootstrap the client and server to exchange messages. We’ll then briefly expand the example to include server-side state management.

The goal of the smf project is provide a high-performance rpc implementation for the Seastar framework. Seastar is a C++ framework for building high-performance server applications. It uses a shared-nothing server design and makes extensive use of modern C++ to provide a futures-based programming model. And while becoming acquainted with Seastar is necessary to build real-world smf-based applications, the content in this post should be understandable by anyone familiar with modern C++.

Here are some helpful links:

Service definition

The smf framework makes extensive use of Google’s flatbuffers serialization library as a structured container for the network protocol as well as for defining rpc service interfaces. In this post we are going to be putting together a basic key/value store service, and we’ll start by examining the structure of the service in terms of the message types. The following is the contents of a file called kvstore.fbs that contains the PutRequest message type holding a single string-based key/value pair. Our service is very simple, so the response message for a PutRequest is empty—though as we’ll see smf provides a built-in mechanism for signaling basic return codes.

Finally at the bottom of the file is the rpc_service called MemoryNode containing a single rpc method Put that takes a PutRequest and responds with a PutResponse.

namespace kvstore.fbs;

table PutRequest {
  key: string;
  value: string;
}

table PutResponse {
}

rpc_service MemoryNode {
  Put(PutRequest):PutResponse;
}

Once message types and service interfaces are defined a tool in the smf framework is used to perform code generation that produces the C++ implementation of the message types and stub methods for the service interfaces. Next we’ll use the smf-provided cmake tools to invoke code generation within a cmake-based build.

Build scaffolding

Below is the complete CMakeLists.txt that we are going to use to build the sample project. The bottom three lines import smf as a sub-directory (e.g. via a git submodule) and configures smf to build and manage all of its dependencies.

cmake_minimum_required(VERSION 3.7.1)
project(smf-kvstore VERSION 0.0.1 LANGUAGES CXX)

set(SMF_MANAGE_DEPS ON CACHE "" INTERNAL FORCE)
set(SMF_BUILD_PROGRAMS OFF CACHE "" INTERNAL FORCE)
add_subdirectory(smf)

Next we use the smfc_gen macro provided by smf to compile the flatbuffers definition, and store the resulting source files in the kvstore_fbs variable.

smfc_gen(CPP TARGET_NAME kvstore_fbs
  OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
  SOURCES kvstore.fbs)

And finally we define client and server executables that link against the smf library and include the source produced by smfc_gen.

link_libraries(smf::smf)
include_directories(PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

add_executable(server server.cc ${kvstore_fbs})
add_executable(client client.cc ${kvstore_fbs})

In the next two sections we’ll work through the creation of the server and the client. Once the server.cc and client.cc files have been created, the project can be compiled and tested:

cmake .
make

./server
./client # in another terminal

Note that smf currently builds its dependencies at configure time which is not a standard technique and may come as a surprise when first starting with the project. For example, when initially running cmake . to configure the tree, smf will build Seastar and other dependencies from source. This can take quite a while to complete, but subsequent builds are fast.

The rpc server

When the flatbuffers file above is compiled it generates a couple C++ header files. These should be used in the server and client by including "kvstore.smf.fb.h" (which includes all the other generated files). The generated code contains the definition of the MemoryNode rpc service that we saw above.

class MemoryNode: public smf::rpc_service {
 private:
  std::vector<smf::rpc_service_method_handle> handles_;
 public:
  virtual ~MemoryNode() = default;
  MemoryNode() {

Recall that our service included a single Put method. The stub for this rpc method is auto-generated. Our implementation will override this default implementation.

  inline virtual
  seastar::future<smf::rpc_typed_envelope<kvstore::fbs::PutResponse>>
  Put(smf::rpc_recv_typed_context<kvstore::fbs::PutRequest> &&rec) {
    using out_type = kvstore::fbs::PutResponse;
    using env_t = smf::rpc_typed_envelope<out_type>;
    env_t data;
    // User should override this method.
    //
    // Helpful for clients to set the status.
    // Typically follows HTTP style. Not imposed by smf whatsoever.
    // i.e. 501 == Method not implemented
    data.envelope.set_status(501);
    return seastar::make_ready_future<env_t>(std::move(data));
  }

Now let’s look at our specific server implementation. It starts with a class that will specialize MemoryNode and implement the rpc methods of our key/value service.

#include <seastar/core/app-template.hh>
#include <seastar/core/sharded.hh>
#include <smf/rpc_server.h>
#include "kvstore.smf.fb.h"

class kvstore_service final : public kvstore::fbs::MemoryNode {
  virtual seastar::future<smf::rpc_typed_envelope<kvstore::fbs::PutResponse>> Put(
    smf::rpc_recv_typed_context<kvstore::fbs::PutRequest>&& req) final;
};

Next we provide the implementation of Put. At a high-level this method does two things. First, it performs any handling for the request, and second it creates and returns a response. Notice that the status value is set to 200. Every response in smf contains a helpful status code that can be used by implementations in any way they see fit.

Notice also that handling of the request is optional. This is because rpc handlers can be organized into a pipeline within the server, and handlers may decide to not forward the request to downstream handlers.

In any case, this handler doesn’t do much other than print a message with the key and value that are contained in the request. It also prints the identifier of the CPU where this request is being handled. In Seastar-based servers, work is generally sharded across CPUs. For networking applications like smf, Seastar assigns client connections to CPU cores in round-robin assignments to balance load.

seastar::future<smf::rpc_typed_envelope<kvstore::fbs::PutResponse>>
kvstore_service::Put(smf::rpc_recv_typed_context<kvstore::fbs::PutRequest>&& req)
{
  if (req) {
    LOG_INFO("recv core={} key={} value={}", seastar::engine().cpu_id(),
        req->key()->str(), req->value()->str());
  }

  smf::rpc_typed_envelope<kvstore::fbs::PutResponse> data;
  data.envelope.set_status(200);

  return seastar::make_ready_future<
    smf::rpc_typed_envelope<kvstore::fbs::PutResponse>>(std::move(data));
}

Now to finish up the server. The first important bit is seastar::sharded<smf::rpc_server> rpc. The sharded object is a special Seastar object that replicates instances of its template parameter onto each physical CPU. In this case, we are asking Seastar to replicate the rpc server across all the cores and to handle connections in parallel.

int main(int args, char **argv, char **env)
{
  seastar::app_template app;

  app.add_options()
    ("ip", po::value<std::string>()->default_value("127.0.0.1"), "bind to")
    ("port", po::value<uint16_t>()->default_value(20776), "listen on")
    ;

  seastar::sharded<smf::rpc_server> rpc;

Next the application itself is started by calling rpc.start(args) which will setup the service on all cores. Once this bootstrap phase completes the rpc service established on all cores (via invoke_on_all) will be configured to serve the kvstore_service that we built above. Finally each instance of the service is started.

  return app.run_deprecated(args, argv, [&app, &rpc] {
    seastar::engine().at_exit([&] { return rpc.stop(); });

    auto&& cfg = app.configuration();

    smf::rpc_server_args args;
    args.ip = cfg["ip"].as<std::string>().c_str();
    args.rpc_port = cfg["port"].as<uint16_t>();

    return rpc.start(args).then([&rpc] {
      return rpc.invoke_on_all(
          &smf::rpc_server::register_service<kvstore_service>);
    }).then([&rpc] {
      return rpc.invoke_on_all(&smf::rpc_server::start);
    });
  });
}

The server can be started using the default arguments by running ./server. You should see some output similar to the trace below. I’m running this example on a node with 8 cores and the log reflects this by printing that an rpc server is starting 8 times. Notice too the log prefix containing the CPU id [shard 0..7].

INFO  2019-03-16 13:26:50,603 [shard 0] smf - rpc_server.cc:73] Starging server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 0] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 1] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 2] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 3] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 4] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 5] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 7] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 6] smf - rpc_server.cc:73] Starting server:rpc_server{args.ip=127.0.0.1, args.flags=0, args.rpc_port=20776, args.http_port=33140, rpc_routes=rpc_handle_router{rpc_service{name=MemoryNode,handles=rpc_service_method_handle{name=Put}}}, limits=rpc_connection_limits{max_mem:2.000 GB, max_body_parsing_duration: 60000ms, res_avail:2.000 GB (2147483648)}, incoming_filters=0, outgoing_filters=0}
INFO  2019-03-16 13:26:50,603 [shard 2] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 3] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 1] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 5] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 4] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 6] smf - rpc_server.cc:94] Starting rpc server
INFO  2019-03-16 13:26:50,603 [shard 7] smf - rpc_server.cc:94] Starting rpc server

That’s it for the server. Later in this post we’ll expand the implementation so that the server is actually storing the key/value pairs, but right now let’s move on and put together a client that can generate a workload.

Client driver

The client that we’ll walk through below is very basic. It calls the Put rpc endpoint with a few different key/value pairs. The code resembles the server, but in this case we do not spin-up a connection per core but rather use a single connection to keep things simple. Like the server, there a couple arguments that can change the settings if you happen to run the server on a non-default address or port.

The most notable difference is that a client object is created. This is of type MemoryNodeClient and is created when the flatbuffers definition is compiled.

  return app.run(args, argv, [&app] {
    auto&& cfg = app.configuration();
    auto ip = cfg["ip"].as<std::string>().c_str();
    auto port = cfg["port"].as<uint16_t>();

    smf::rpc_client_opts opts{};
    opts.server_addr = seastar::ipv4_addr{ip, port};

    auto client = seastar::make_shared<
      kvstore::fbs::MemoryNodeClient>(std::move(opts));

    seastar::engine().at_exit([client] { return client->stop(); });

The client’s connect method establishes the connection and returns a future. When that future is complete we can begin invoking the rpc service methods. Here we build 10 key/value pairs, and in the next code block we call Put for each one.

    return client->connect().then([client] {
      boost::counting_iterator<int> from(0);
      boost::counting_iterator<int> to(10);
      return seastar::do_for_each(from, to, [client](int i) mutable {
        seastar::sstring key = fmt::format("key.{}", i);
        seastar::sstring val = fmt::format("val.{}", i);

The PutRequest object is created and populated with the key/value strings, then the Put method is invoked and the response is logged. After the workload generator completes, the client and Seastar application is shutdown.

        smf::rpc_typed_envelope<kvstore::fbs::PutRequest> req;
        req.data->key = std::move(key);
        req.data->value = std::move(val);

        return client->Put(std::move(req)).then([](auto res) {
          if (res) {
            LOG_INFO("recv core={} status={}",
                seastar::engine().cpu_id(),
                res.ctx->status());
          }
          return seastar::make_ready_future<>();
        });
      });
    }).then([client] {
      return client->stop();
    }).then([] {
      return seastar::make_ready_future<int>(0);
    });
  });
}

Now we can see the entire thing in action. First start the server, then run the client a few times. Each time you run the client you should see something like the following where we receive the response with a code of 200 (see above in the server with we set the 200 code).

INFO  2019-03-16 08:54:57,229 [shard 0] smf - client.cc:53] recv core=0 status=200
INFO  2019-03-16 08:54:57,229 [shard 0] smf - client.cc:53] recv core=0 status=200
...

Since the client is running as a single thread, you should generally only see it scheduled on core 0 (i.e. recv core=0 in the log). However, recall that the server shards its connections across cores. If you examine the log of the server, you should see that each new connection is handled by a different core. The first connection is handled on core 0.

INFO  2019-03-16 08:54:42,607 [shard 0] smf - server.cc:20] recv core=0 key=key.0 value=key.0.val
INFO  2019-03-16 08:54:42,607 [shard 0] smf - server.cc:20] recv core=0 key=key.1 value=key.1.val
...

And subsequent connections are assigned to additional cores until all cores are assigned connections and the assignment wraps back around to core 0.

INFO  2019-03-16 08:54:44,816 [shard 1] smf - server.cc:20] recv core=1 key=key.0 value=key.0.val
INFO  2019-03-16 08:54:44,816 [shard 1] smf - server.cc:20] recv core=1 key=key.1 value=key.1.val
INFO  2019-03-16 08:54:44,816 [shard 1] smf - server.cc:20] recv core=1 key=key.2 value=key.2.val
...
INFO  2019-03-16 08:54:56,616 [shard 7] smf - server.cc:20] recv core=7 key=key.0 value=key.0.val
INFO  2019-03-16 08:54:56,616 [shard 7] smf - server.cc:20] recv core=7 key=key.1 value=key.1.val
INFO  2019-03-16 08:54:56,616 [shard 7] smf - server.cc:20] recv core=7 key=key.2 value=key.2.val
...
INFO  2019-03-16 08:54:56,832 [shard 0] smf - server.cc:20] recv core=0 key=key.0 value=key.0.val
INFO  2019-03-16 08:54:56,832 [shard 0] smf - server.cc:20] recv core=0 key=key.1 value=key.1.val
INFO  2019-03-16 08:54:56,832 [shard 0] smf - server.cc:20] recv core=0 key=key.2 value=key.2.val
...

Managing server state

The server we’ve built so far doesn’t do much other than throw the key/value bits from the client on the floor. In this section we are going to modify the server so that the data from the client is stored. We’ll keep it simple and only store the values in memory. This should add just enough detail to our example project to demonstrate how the sharding works when managing server state.

The first thing is to define a new per-core object that will hold key/value pairs. This class contains a private std::unordered_map to store client key/value pairs, and a handler for inserting data into the store. This handler also prints out the core on which it is running, which will be helpful later when we examine the exact behavior of the server. Finally, when the server shuts down, we also print out the contents of the index to examine. We’ll see this in action after we walk through the rest of the code.

 #include <smf/rpc_server.h>
 #include "kvstore.smf.fb.h"
 
+class kvstore_db {
+ public:
+  seastar::future<> put(seastar::sstring key, seastar::sstring value) {
+    LOG_INFO("handling put core={} key={} value={}",
+        seastar::engine().cpu_id(), key, value);
+    store_.insert(std::make_pair(std::move(key), std::move(value)));
+    return seastar::make_ready_future<>();
+  }
+
+  seastar::future<> stop() {
+    for (auto kv : store_) {
+      LOG_INFO("{}/{}: {} {}", &store_,
+          seastar::engine().cpu_id(),
+          kv.first, kv.second);
+    }
+    return seastar::make_ready_future<>();
+  }
+
+ private:
+  std::unordered_map<seastar::sstring, seastar::sstring> store_;
+};

Next we modify the key/value service to track a reference to an instance of the store class we introduced above. Note that both of these will be sharded across the cores in the system. We’ll see shortly how the rpc service is given an instance of the store.

 class kvstore_service final : public kvstore::fbs::MemoryNode {
   virtual seastar::future<smf::rpc_typed_envelope<kvstore::fbs::PutResponse>> Put(
     smf::rpc_recv_typed_context<kvstore::fbs::PutRequest>&& req) final;
+
+ public:
+  kvstore_service(seastar::sharded<kvstore_db>& store) :
+    store_(store)
+  {}
+
+ private:
+  seastar::sharded<kvstore_db>& store_;
 };

The other change that is made to the rpc service is to forward the key/value pair request from the client to the store instance. In order to balance state across all cores in the system we adopt a simple scheme in which the key is hashed to a core number. The final piece of this puzzle is the magical invoke_on method that arranges for a particular method to be executed on a specific core. In this case, we execute the put operation on the core that is responsible for the particular key being stored.

@@ -14,6 +44,13 @@ kvstore_service::Put(smf::rpc_recv_typed
   if (req) {
     LOG_INFO("recv core={} key={} value={}", seastar::engine().cpu_id(),
         req->key()->str(), req->value()->str());
+
+    seastar::sstring key = req->key()->str();
+    auto hash = std::hash<seastar::sstring>{}(req->key()->str());
+    auto cpu_id = hash % seastar::smp::count;
+
+    store_.invoke_on(cpu_id, &kvstore_db::put,
+        std::move(key), req->value()->str());

The store is also treated as a sharded object by Seastar, creating an instance per physical core.

@@ -35,10 +72,12 @@ int main(int args, char **argv, char **e
       ;
   }
 
+  seastar::sharded<kvstore_db> store;
   seastar::sharded<smf::rpc_server> rpc;
 
-  return app.run_deprecated(args, argv, [&app, &rpc] {
+  return app.run_deprecated(args, argv, [&app, &store, &rpc] {
     seastar::engine().at_exit([&] { return rpc.stop(); });
+    seastar::engine().at_exit([&] { return store.stop(); });

And finally there are some changes to the setup. The piece to pay attention to is that when the rpc service is registered, an instance of the store is passed in which gives it access to the per-core storage service we created at the beginning of this section.

@@ -46,9 +85,12 @@ int main(int args, char **argv, char **e
     args.ip = cfg["ip"].as<std::string>().c_str();
     args.rpc_port = cfg["port"].as<uint16_t>();
 
-    return rpc.start(args).then([&rpc] {
-      return rpc.invoke_on_all(
-          &smf::rpc_server::register_service<kvstore_service>);
+    return rpc.start(args).then([&store] {
+      return store.start();
+    }).then([&rpc, &store] {
+      return rpc.invoke_on_all([&store](smf::rpc_server& server) {
+        server.register_service<kvstore_service>(store);
+      });
     }).then([&rpc] {
       return rpc.invoke_on_all(&smf::rpc_server::start);
     });

The client and server are ran in the same way as before, and now we can examine what is happening in the server as the client sends new key/value pairs. As before we log a message when we first receive a request from the client. In this case that connection is handled on core 0. The server then hashes the key and forwards the request to the core that owns that key. In this case the key hashes to core 5 which is where the key/value pair is stored.

INFO  2019-03-17 16:26:02,922 [shard 0] smf - server.cc:51] recv core=0 key=key.0 value=key.0.val
INFO  2019-03-17 16:26:02,922 [shard 5] smf - server.cc:12] handling put core=5 key=key.0 value=key.0.val

The same pattern holds for the rest of the requests which are all handled initially by core 0, and then forwarded to a different core, including core 0 which is also able to store key/value pairs.

INFO  2019-03-17 16:26:02,923 [shard 0] smf - server.cc:51] recv core=0 key=key.1 value=key.1.val
INFO  2019-03-17 16:26:02,923 [shard 6] smf - server.cc:12] handling put core=6 key=key.1 value=key.1.val
INFO  2019-03-17 16:26:02,923 [shard 0] smf - server.cc:51] recv core=0 key=key.2 value=key.2.val
INFO  2019-03-17 16:26:02,923 [shard 6] smf - server.cc:12] handling put core=6 key=key.2 value=key.2.val
INFO  2019-03-17 16:26:02,924 [shard 0] smf - server.cc:51] recv core=0 key=key.3 value=key.3.val
INFO  2019-03-17 16:26:02,924 [shard 1] smf - server.cc:12] handling put core=1 key=key.3 value=key.3.val
INFO  2019-03-17 16:26:02,924 [shard 0] smf - server.cc:51] recv core=0 key=key.4 value=key.4.val
INFO  2019-03-17 16:26:02,924 [shard 5] smf - server.cc:12] handling put core=5 key=key.4 value=key.4.val
INFO  2019-03-17 16:26:02,924 [shard 0] smf - server.cc:51] recv core=0 key=key.5 value=key.5.val
INFO  2019-03-17 16:26:02,924 [shard 3] smf - server.cc:12] handling put core=3 key=key.5 value=key.5.val
INFO  2019-03-17 16:26:02,925 [shard 0] smf - server.cc:51] recv core=0 key=key.6 value=key.6.val
INFO  2019-03-17 16:26:02,925 [shard 5] smf - server.cc:12] handling put core=5 key=key.6 value=key.6.val
INFO  2019-03-17 16:26:02,925 [shard 0] smf - server.cc:51] recv core=0 key=key.7 value=key.7.val
INFO  2019-03-17 16:26:02,925 [shard 0] smf - server.cc:12] handling put core=0 key=key.7 value=key.7.val
INFO  2019-03-17 16:26:02,925 [shard 0] smf - server.cc:51] recv core=0 key=key.8 value=key.8.val
INFO  2019-03-17 16:26:02,925 [shard 6] smf - server.cc:12] handling put core=6 key=key.8 value=key.8.val
INFO  2019-03-17 16:26:02,926 [shard 0] smf - server.cc:51] recv core=0 key=key.9 value=key.9.val
INFO  2019-03-17 16:26:02,926 [shard 7] smf - server.cc:12] handling put core=7 key=key.9 value=key.9.val

We only added 10 key/value pairs in this example, so it’s feasible to dump out the contents of the index on each core. Below is what that dump looks like. We can see that there are 8 different index instances on each of the eight cores. Note that none of the key/value pairs happened to hash to cores 0 or 2.

INFO  2019-03-19 20:44:33,824 [shard 1] smf - server.cc:19] 0x605000011560/1: key.3 val.3
INFO  2019-03-19 20:44:33,824 [shard 3] smf - server.cc:19] 0x606000011790/3: key.5 val.5
INFO  2019-03-19 20:44:33,825 [shard 5] smf - server.cc:19] 0x604000011600/5: key.6 val.6
INFO  2019-03-19 20:44:33,825 [shard 7] smf - server.cc:19] 0x6020000115b0/7: key.9 val.9
INFO  2019-03-19 20:44:33,825 [shard 5] smf - server.cc:19] 0x604000011600/5: key.0 val.0
INFO  2019-03-19 20:44:33,825 [shard 5] smf - server.cc:19] 0x604000011600/5: key.4 val.4
INFO  2019-03-19 20:44:33,825 [shard 6] smf - server.cc:19] 0x6010000115b0/6: key.8 val.8
INFO  2019-03-19 20:44:33,825 [shard 6] smf - server.cc:19] 0x6010000115b0/6: key.1 val.1
INFO  2019-03-19 20:44:33,825 [shard 6] smf - server.cc:19] 0x6010000115b0/6: key.2 val.2

If you found smf or Seastar intriguing I encourage you to check out these projects in more detail. Building high-performance network servers with these two technologies could not be easier or faster. I’m really exciting to keep exploring what they have to offer.

Thanks to Alex for providing feedback on this post!

reproducible deps

Dependencies:

smf comes with the ability to fetch and build transitive dependencies on it’s own at known revisions for reproducible builds.

To enable reproducible builds, simply invoke cmake like so:

mkdir build && cmake -GNinja -DSMF_MANAGE_DEPS=ON -DCMAKE_BUILD_TYPE=Release ..

By default automatic dep management is off (i.e.: -DSMF_MANAGE_DEPS=OFF) so you can use smf as part of a larger cmake project

Example app


➜  release git:(master) ✗ ldd bin/smf_demo_client 

        # system deps required

        linux-vdso.so.1 (0x00007ffdd8364000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fe397aec000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fe397749000)
        libatomic.so.1 => /lib64/libatomic.so.1 (0x00007fe39754b000)
        librt.so.1 => /lib64/librt.so.1 (0x00007fe3972e3000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fe3970fe000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fe396f7a000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fe396f5f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fe396d99000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe397b35000)
        
        # optionally manage deps
        
        libgnutls.so.30 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libgnutls.so.30 (0x00007fe397555000)
        libsctp.so.1 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libsctp.so.1 (0x00007fe397546000)
        libprotobuf.so.13 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libprotobuf.so.13 (0x00007fe3972ed000)
        libhwloc.so.5 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libhwloc.so.5 (0x00007fe3972a5000)
        libnuma.so.1 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libnuma.so.1 (0x00007fe397298000)
        libcares.so.2 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libcares.so.2 (0x00007fe397ad8000)
        libcryptopp.so.5.6 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib64/libcryptopp.so.5.6 (0x00007fe397766000)
        liblz4.so.1 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib64/liblz4.so.1 (0x00007fe39774f000)
        libz.so.1 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libz.so.1 (0x00007fe396d7c000)
        libnettle.so.6 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libnettle.so.6 (0x00007fe396d36000)
        libhogweed.so.4 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libhogweed.so.4 (0x00007fe396cf6000)
        libgmp.so.10 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libgmp.so.10 (0x00007fe396c80000)
        libpciaccess.so.0 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libpciaccess.so.0 (0x00007fe396c76000)
        libxml2.so.2 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/libxml2.so.2 (0x00007fe396b11000)
        liblzma.so.5 => /home/agallego/workspace/smf/build/release/smf-deps-install/lib/liblzma.so.5 (0x00007fe396ae9000)

Next steps / help wanted

I’d love to make most of the dependencies statically compiled into the final binary.

The only issue I ran into was w/ the TLS libraries.

protobuf, cares, lz4, z, lzma, xml2 are easy to change. They already produce the .a files for linking.

The only remaining problematic libs are hwloc, numa, gnutls, nettle, hogweed, gmp

Getting started projects

It’s now very easy to get started with smf in C++. Simply clone this project and ./build.sh to have a fully working project

C++

A fully working project with build system.

https://github.com/smfrpc/smf-getting-started-cpp


# 1 clone
git clone --recursive https://github.com/smfrpc/smf-getting-started-cpp

# 2 go to project
cd smf-getting-started-cpp

# 3 build
./build.sh

Introducing smf

smf is pronounced /smɝf/


Important Links
Mailing List
Documentation

smf RPC

smf is a new RPC system and code generation like gRPC, Cap n Proto, Apache Thrift, etc, but designed for microsecond tail latency.

Current benchmarks in microseconds

60 byte payload latency
p50 7us
p90 8us
p99 8us
p9999 15us
p100 26us

Highlights:

  • Code generation compiler for RPC
  • Load testing framework for RPC subsystem
  • Kernel-bypass RPC via DPDK
  • 0-copy Serialization framework based on Google’s Flatbuffers project
  • Arbitrary filter chaining on incoming and outgoing channels (like twitter’s Finagle)
  • Small 16byte overhead
  • Binary with pluggable compressors

We need your help!

We have a lot of issues marked as good first issue

Look at the contributing guideline for more details.

Take a look at the issue list or send an email to the smf-dev mailing List to get started.

Getting started

Please see our quick getting started!