-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement new variant type and builder API #368
Conversation
95b535f
to
036fc9a
Compare
FreeBSD and Windows are unrelated failures. We'll have to sort that out separately. @ckreibich, @awelzel, @timwoj: anyone up for reviewing this? 🙂 I know it looks like a lot, but it's actually mostly boilerplate code that doesn't require that much attention in a review. It's there to make the API easy and safe to use. Classes like |
Very general high-level question and possibly not suitable for the PR, but anyhow:
I've dabbled a bit writing up a schema with Cap 'n Proto. To me it would sound much less intimidating if we had a broker.capnp and used that. Reading a bit of the description the ideas are similar around in-memory and on-wire format being identical. This could also open the possibility to use that format over websocket as an alternative to JSON and have other languages generate the parsing code based on the schema file. Disclaimer: I have no experience with Cap'n Proto otherwise, I had worked with Protobuf previously. |
Sorry for not mentioning this. 🙂 Currently, Broker just uses CAF classes to serialize and deserialize its data. To parse binary data into views, we need to implement the format in Broker. This is 100% the exact same binary representation as before, just implemented in Broker directly. You can serialize a
I don't think there are prior thoughts to doing this. But it would require us to break both API and network protocol at the same time. What I wanted to do with this approach was to have a way to keep everything on the wire and (including handshake) stable and then migrate Broker internally to the new
The in-memory format for Interoperability with I don't have too much of an opinion which would ultimately be better, but if we decide to go with a schema-based approach, we'll have a different discussion and different set of decisions to make.
To do that, I think we wouldn't need to use Protobuf/msgpack/etc. internally. As long as there is a 1:1 mapping between two formats, we can translate binary representation on-the-fly, just as we convert to and from JSON on the fly. It does invoke overhead, of course, since we cannot just write out the in-memory representation. Just pointing out that this is not black & white. 🙂 |
A few comments on this:
For now, it looks to me like the next step is to understand how to complete this changeset so we can explore it from Zeek. Could you sketch what remains to be done for this, Dominik, or if it's already there, how to toggle use of the variant vs current code? |
Not to downplay the patch, but the actual bulk of this PR is new tests, benchmarks and wrapper types that exist to make the API safe and easy to use. Most of this can be skimmed in a review since it has low complexity and is often quite repetitive. The bulk of code that does add complexity is mostly under
As mentioned earlier, this only adds the new abstractions to Broker. My idea was to have one PR only adding the new pieces to the code base before we start integrating them. So there's no switch to toggle old vs. new yet. But of course you're right. If there's no sufficient gain for Zeek, it would be pointless to integrate this. I'll prepare a separate branch with a quick & dirty integration of the new variant to measure the performance impact that we can get out of this. I'll get back if I have new numbers, ideally from a Zeek cluster. 🙂 |
I have prepared a "quick & dirty" integration branch that switches from the old Here is the current
The current version can do about ~450k events/s with one server and one client process. Now the same setup with the new message envelopes and
So for this benchmark, we get an increase of ~40% events/s (or additional ~200k in total numbers). That's actually much more than I had anticipated for full end-to-end performance. Of course that's just a Broker benchmark. I'll port Zeek to the new API next and then see how the full system behaves. We can't really do a maximum throughput measurement with Zeek, because Zeek doesn't use the backpressure. So I'll probably need some help putting together a meaningful benchmark with Zeek. Maybe we can throw a fixed rate at Broker and just compare CPU/Memory load? Fun facts:
|
036fc9a
to
ab98502
Compare
I'll re-integrate this after the other PRs are merged. Closing for now since it'll need rebasing and a Zeek-side PR once it's ready. |
This is a huge "code dump", so let me first explain the big picture here. 🙂
The performance in Broker is held back by
broker::data
. Performance problems related to this type keep coming up. In #318, I've built a barebones alternative tobroker::data
(nameddata_view
at the time) that achieved a ~3x speedup overbroker::data
when deserializing a simple (Zeek) event-like structure.It's not really a "view", though. After a bit of thinking, I've settled on
broker::variant
for "the new thing". The reasoning is simply thatbroker::variant
is a tagged union type just likestd::variant
, only that it hard-codes what types are allowed. How can it be so much faster thanbroker::data
? Custom allocation strategy.broker::data
uses "regular" STL types that allocate stuff on the heap. As a result, a Zeek event will allocate multiple memory regions and scatter an object all over the heap.A
broker::variant
always uses a monotonic buffer resource (albeit a Broker replacement type that we use since the STL type isn't available everywhere yet) to build the nested data structure. The work flow is now always that we start from a serialized version of the value, iterate it, and build the nested structure as we go. Once avariant
is assembled, it is immutable and can be (thread-safe) passed around freely.The interaction between types is as follows:
variant_data
simply wraps thestd::variant
that will hold the actual data. This is very similar tobroker::data
except that we will only store pointers to the container types. This is because we allocate everything in the same allocator, so we can just safely pass around pointers. Further, we don't need to call any destructors. We allocate everything into the same memory region. If that region gets discarded, all of the objects that lived in there just conveniently disappear.envelope
holds a shared pointer to the buffer resource alongside thevariant_data
that lives in the buffer.variant
is a high-level wrapper type that gives us a nice and clean interface without having to care about thestd::variant
and the "trivial" types. It holds a (raw) pointer to the actualvariant_data
plus a (shared) pointer to theenvelope
that owns the memory of the data.variant_list
is a high-level wrapper to replacebroker::vector
.variant_set
is a high-level wrapper to replacebroker::set
.variant_table
is a high-level wrapper to replacebroker::table
.Because we need to build an envelope now, before we can construct the actual
broker::variant
, we also add a couple of builder types. The builders generate the serialized version of the object. Side note: thevariant
will simply point back to the buffer it was deserialized from for string data. This means ultimately, we can have this workflow in Broker:variant
from the buffer "in-place".Because the envelope holds on to the serialized version of the object, we can trivially serialize it when sending it over to the network: just copy the bytes from the buffer to the network.
This first PR just adds all of the new tools to the code base. Ultimately, we want to get rid of the old types. The
data_message
will be replaced byenvelope
anddata
will be replaced byvariant
. Of course we'll do this in small increments. We will also keep thedata
type around for a bit to not break APIs immediately and for Python bindings.By the way, the
envelope
also holds the topic. But it's accessible asstring_view
to give us more freedom on how and where to store it. Ourbroker::topic
will also get the axe eventually because it forces us to commit to astd::string
.The critical part of the equation of all of this is of course performance. I've re-used the
serialization
micro benchmark. Here are the results:The
event_1
is a simple, nested representation that models a simple Zeek event:(1, 1, ("event_1", (42, "test")))
. This is basically how abroker::zeek::Event
looks like.The
table_1
is a simple table with two key/value pairs:{"first-name": "John", "last-name": "Doe"}
.Serializing a
broker::data
of this type is ~330ns. Now, serializing abroker::variant
wouldn't be a good comparison here. In the new design, the builders have the job of producing the binary format. And they do this much faster with a speedup of around ~2x.The real deal is deserializing, though. For real code, we would be interested in how long it takes to deserialize an
envelope
vs. adata
. The values for deserializing a "naked"variant
are just available for the sake of completeness. So here, we look at 560ns vs. 200ns forevent_1
(almost 3x speedup) and just a small gain (around 20%) fortable_1
. Of course this would become a much bigger speedup real quick if the table had more keys or more complex key/value entries.In practice, Broker will mostly operate on Zeek events and Zeek log entries. They come from the nesting implemented in
broker/zeek.hh
, so this will give us a nice boost. A ~3x speedup for the deserialization path just from switching to a different data structure. We will combine this with the additional gains from putting radix trees to work for the filtering.