Wednesday, February 5, 2025

Profiling Particular person Queries in a Concurrent System


A great CPU profiler is price its weight in gold. Measuring efficiency in-situ normally means utilizing a sampling profile. They supply lots of info whereas having very low overhead. In a concurrent system, nevertheless, it’s onerous to make use of the ensuing knowledge to extract high-level insights. Samples don’t embody context like question IDs and application-level statistics; they present you what code was run, however not why.

This weblog introduces trampoline histories, a method Rockset has developed to effectively connect application-level info (question IDs) to the samples of a CPU profile. This lets us use profiles to know the efficiency of particular person queries, even when a number of queries are executing concurrently throughout the identical set of employee threads.

Primer on Rockset

Rockset is a cloud-native search and analytics database. SQL queries from a buyer are executed in a distributed style throughout a set of servers within the cloud. We use inverted indexes, approximate vector indexes, and columnar layouts to effectively execute queries, whereas additionally processing streaming updates. Nearly all of Rockset’s performance-critical code is C++.

Most Rockset clients have their very own devoted compute assets known as digital situations. Inside that devoted set of compute assets, nevertheless, a number of queries can execute on the similar time. Queries are executed in a distributed style throughout all the nodes, so because of this a number of queries are lively on the similar time in the identical course of. This concurrent question execution poses a problem when attempting to measure efficiency.

Concurrent question processing improves utilization by permitting computation, I/O, and communication to be overlapped. This overlapping is particularly necessary for top QPS workloads and quick queries, which have extra coordination relative to their elementary work. Concurrent execution can also be necessary for lowering head-of-line blocking and latency outliers; it prevents an occasional heavy question from blocking completion of the queries that observe it.

We handle concurrency by breaking work into micro-tasks which are run by a hard and fast set of thread swimming pools. This considerably reduces the necessity for locks, as a result of we are able to handle synchronization through process dependencies, and it additionally minimizes context switching overheads. Sadly, this micro-task structure makes it tough to profile particular person queries. Callchain samples (stack backtraces) might need come from any lively question, so the ensuing profile exhibits solely the sum of the CPU work.

Profiles that mix all the lively queries are higher than nothing, however lots of handbook experience is required to interpret the noisy outcomes. Trampoline histories allow us to assign many of the CPU work in our execution engine to particular person question IDs, each for steady profiles and on-demand profiles. It is a very highly effective device when tuning queries or debugging anomalies.

DynamicLabel

The API we’ve constructed for including application-level metadata to the CPU samples known as DynamicLabel. Its public interface could be very easy:

class DynamicLabel {
  public:
    DynamicLabel(std::string key, std::string worth);
    ~DynamicLabel();

    template 
    std::invoke_result_t apply(Func&& func) const;
};

DynamicLabel::apply invokes func. Profile samples taken throughout that invocation could have the label connected.

Every question wants just one DynamicLabel. Each time a micro-task from the question is run it’s invoked through DynamicLabel::apply.

One of the vital necessary properties of sampling profilers is that their overhead is proportional to their sampling price; that is what lets their overhead be made arbitrarily small. In distinction, DynamicLabel::apply should do some work for each process whatever the sampling price. In some instances our micro-tasks might be fairly micro, so it will be important that apply has very low overhead.

apply‘s efficiency is the first design constraint. DynamicLabel‘s different operations (building, destruction, and label lookup throughout sampling) occur orders of magnitude much less steadily.

Let’s work via some methods we would attempt to implement the DynamicLabel performance. We’ll consider and refine them with the purpose of constructing apply as quick as potential. If you wish to skip the journey and bounce straight to the vacation spot, go to the “Trampoline Histories” part.

Implementation Concepts

Concept #1: Resolve dynamic labels at pattern assortment time

The obvious strategy to affiliate software metadata with a pattern is to place it there from the start. The profiler would search for dynamic labels on the similar time that it’s capturing the stack backtrace, bundling a replica of them with the callchain.

Rockset’s profiling makes use of Linux’s perf_event, the subsystem that powers the perf command line device. perf_event has many benefits over signal-based profilers (similar to gperftools). It has decrease bias, decrease skew, decrease overhead, entry to {hardware} efficiency counters, visibility into each userspace and kernel callchains, and the flexibility to measure interference from different processes. These benefits come from its structure, through which system-wide profile samples are taken by the kernel and asynchronously handed to userspace via a lock-free ring buffer.

Though perf_event has lots of benefits, we are able to’t use it for concept #1 as a result of it may’t learn arbitrary userspace knowledge at sampling time. eBPF profilers have an analogous limitation.

Concept #2: Document a perf pattern when the metadata modifications

If it’s not potential to tug dynamic labels from userspace to the kernel at sampling time, then what about push? We may add an occasion to the profile each time that the thread→label mapping modifications, then post-process the profiles to match up the labels.

A method to do that can be to make use of perf uprobes. Userspace probes can report perform invocations, together with perform arguments. Sadly, uprobes are too sluggish to make use of on this style for us. Thread pool overhead for us is about 110 nanoseconds per process. Even a single crossing from the userspace into the kernel (uprobe or syscall) would multiply this overhead.

Avoiding syscalls throughout DynamicLabel::apply additionally prevents an eBPF resolution, the place we replace an eBPF map in apply after which modify an eBPF profiler like BCC to fetch the labels when sampling.

edit: eBPF can be utilized to tug from userspace when accumulating a pattern, studying fsbase after which utilizing bpfprobelearnconsumer() to stroll a userspace knowledge construction that’s connected to a threadnative. If in case you have BPF permissions enabled in your manufacturing surroundings and are utilizing a BPF-based profiler then this different could be a good one. The engineering and deployment points are extra advanced however the consequence doesn’t require in-process profile processing. Because of Jason Rahman for pointing this out.

Concept #3: Merge profiles with a userspace label historical past

If it is too costly to report modifications to the thread→label mapping within the kernel, what if we do it within the userspace? We may report a historical past of calls to DynamicLabel::apply, then be a part of it to the profile samples throughout post-processing. perf_event samples can embody timestamps and Linux’s CLOCK_MONOTONIC clock has sufficient precision to look strictly monotonic (at the very least on the x86_64 or arm64 situations we would use), so the be a part of can be actual. A name to clock_gettime utilizing the VDSO mechanism is loads sooner than a kernel transition, so the overhead can be a lot decrease than that for concept #2.

The problem with this strategy is the information footprint. DynamicLabel histories can be a number of orders of magnitude bigger than the profiles themselves, even after making use of some easy compression. Profiling is enabled constantly on all of our servers at a low sampling price, so attempting to persist a historical past of each micro-task invocation would shortly overload our monitoring infrastructure.

Concept #4: In-memory historical past merging

The sooner we be a part of samples and label histories, the much less historical past we have to retailer. If we may be a part of the samples and the historical past in near-realtime (maybe each second) then we wouldn’t want to write down the histories to disk in any respect.

The commonest means to make use of Linux’s perf_event subsystem is through the perf command line device, however all the deep kernel magic is out there to any course of through the perf_event_open syscall. There are lots of configuration choices (perf_event_open(2) is the longest manpage of any system name), however when you get it arrange you’ll be able to learn profile samples from a lock-free ring buffer as quickly as they’re gathered by the kernel.

To keep away from competition, we may keep the historical past as a set of thread-local queues that report the timestamp of each DynamicLabel::apply entry and exit. For every pattern we might search the corresponding historical past utilizing the pattern’s timestamp.

This strategy has possible efficiency, however can we do higher?

Concept #5: Use the callchains to optimize the historical past of calls to `apply`

We will use the truth that apply exhibits up within the recorded callchains to scale back the historical past dimension. If we block inlining in order that we are able to discover DynamicLabel::apply within the name stacks, then we are able to use the backtrace to detect exit. Because of this apply solely wants to write down the entry data, which report the time that an affiliation was created. Halving the variety of data halves the CPU and knowledge footprint (of the a part of the work that’s not sampled).

This technique is the perfect one but, however we are able to do even higher! The historical past entry data a variety of time for which apply was sure to a specific label, so we solely have to make a report when the binding modifications, slightly than per-invocation. This optimization might be very efficient if now we have a number of variations of apply to search for within the name stack. This leads us to trampoline histories, the design that now we have applied and deployed.

Trampoline Histories

If the stack has sufficient info to search out the precise DynamicLabel , then the one factor that apply must do is depart a body on the stack. Since there are a number of lively labels, we’ll want a number of addresses.

A perform that instantly invokes one other perform is a trampoline. In C++ it would appear like this:

__attribute__((__noinline__))
void trampoline(std::move_only_function func) {
    func();
    asm unstable (""); // stop tailcall optimization
}

Be aware that we have to stop compiler optimizations that may trigger the perform to not be current within the stack, specifically inlining and tailcall elimination.

The trampoline compiles to solely 5 directions, 2 to arrange the body pointer, 1 to invoke func(), and a couple of to scrub up and return. Together with padding that is 32 bytes of code.

C++ templates allow us to simply generate a complete household of trampolines, every of which has a singular handle.

utilizing Trampoline = __attribute__((__noinline__)) void (*)(
        std::move_only_function);

constexpr size_t kNumTrampolines = ...;

template 
__attribute__((__noinline__))
void trampoline(std::move_only_function func) {
    func();
    asm unstable (""); // stop tailcall optimization
}

template 
constexpr std::array makeTrampolines(
        std::index_sequence) {
    return {&trampoline...};
}

Trampoline getTrampoline(unsigned idx) {
    static constexpr auto kTrampolines =
            makeTrampolines(std::make_index_sequence{});
    return kTrampolines.at(idx);
}

We’ve now bought all the low-level items we have to implement DynamicLabel:

  • DynamicLabel building → discover a trampoline that’s not at present in use, append the label and present timestamp to that trampoline’s historical past
  • DynamicLabel::apply → invoke the code utilizing the trampoline
  • DynamicLabel destruction → return the trampoline to a pool of unused trampolines
  • Stack body symbolization → if the trampoline’s handle is present in a callchain, lookup the label within the trampoline’s historical past

Efficiency Affect

Our purpose is to make DynamicLabel::apply quick, in order that we are able to use it to wrap even small items of labor. We measured it by extending our current dynamic thread pool microbenchmark, including a layer of indirection through apply.

{
    DynamicThreadPool executor({.maxThreads = 1});
    for (size_t i = 0; i < kNumTasks; ++i) {
        executor.add([&]() {
            label.apply([&] { ++depend; }); });
    }
    // ~DynamicThreadPool waits for all duties
}
EXPECT_EQ(kNumTasks, depend);

Maybe surprisingly, this benchmark exhibits zero efficiency impression from the additional stage of indirection, when measured utilizing both wall clock time or cycle counts. How can this be?

It seems we’re benefiting from a few years of analysis into department prediction for oblique jumps. The within of our trampoline appears like a digital methodology name to the CPU. That is extraordinarily frequent, so processor distributors have put lots of effort into optimizing it.

If we use perf to measure the variety of directions within the benchmark we observe that including label.apply causes about three dozen additional directions to be executed per loop. This may sluggish issues down if the CPU was front-end sure or if the vacation spot was unpredictable, however on this case we’re reminiscence sure. There are many execution assets for the additional directions, so that they don’t truly enhance this system’s latency. Rockset is mostly reminiscence sure when executing queries; the zero-latency consequence holds in our manufacturing surroundings as effectively.

A Few Implementation Particulars

There are some things we have performed to enhance the ergonomics of our profile ecosystem:

  • The perf.knowledge format emitted by perf is optimized for CPU-efficient writing, not for simplicity or ease of use. Regardless that Rockset’s perf_event_open-based profiler pulls knowledge from perf_event_open, now we have chosen to emit the identical protobuf-based pprof format utilized by gperftools. Importantly, the pprof format helps arbitrary labels on samples and the pprof visualizer already has the flexibility to filter on these tags, so it was simple so as to add and use the knowledge from DynamicLabel.
  • We subtract one from most callchain addresses earlier than symbolizing, as a result of the return handle is definitely the primary instruction that might be run after returning. That is particularly necessary when utilizing inline frames, since neighboring directions are sometimes not from the identical supply perform.
  • We rewrite trampoline to trampoline<0> in order that now we have the choice of ignoring the tags and rendering a daily flame graph.
  • When simplifying demangled constructor names, we use one thing like Foo::copy_construct and Foo::move_construct slightly than simplifying each to Foo::Foo. Differentiating constructor sorts makes it a lot simpler to seek for pointless copies. (If you happen to implement this be sure to can deal with demangled names with unbalanced < and >, similar to std::enable_if 4, void>::kind.)
  • We compile with -fno-omit-frame-pointer and use body tips to construct our callchains, however some necessary glibc capabilities like memcpy are written in meeting and don’t contact the stack in any respect. For these capabilities, the backtrace captured by perf_event_open‘s PERF_SAMPLE_CALLCHAIN mode omits the perform that calls the meeting perform. We discover it through the use of PERF_SAMPLE_STACK_USER to report the highest 8 bytes of the stack, splicing it into the callchain when the leaf is in a kind of capabilities. That is a lot much less overhead than attempting to seize all the backtrace with PERF_SAMPLE_STACK_USER.

Conclusion

Dynamic labels let Rockset tag CPU profile samples with the question whose work was lively at that second. This skill lets us use profiles to get insights about particular person queries, though Rockset makes use of concurrent question execution to enhance CPU utilization.

Trampoline histories are a means of encoding the lively work within the callchain, the place the present profiling infrastructure can simply seize it. By making the DynamicLabel ↔ trampoline binding comparatively long-lived (milliseconds, slightly than microseconds), the overhead of including the labels is stored extraordinarily low. The method applies to any system that desires to enhance sampled callchains with software state.

Rockset is hiring engineers in its Boston, San Mateo, London and Madrid places of work. Apply to open engineering positions at this time.



Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles

PHP Code Snippets Powered By : XYZScripts.com