Skip to content

Commit 75d614a

Browse files
committed
docs: add work partitioning section
Signed-off-by: irozzo-1A <iacopo@sysdig.com>
1 parent b13d105 commit 75d614a

File tree

1 file changed

+104
-5
lines changed

1 file changed

+104
-5
lines changed

proposals/20251205-multi-thread-falco-design.md

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,113 @@ This document does not cover low-level implementation details that will be addre
2929

3030
* The kernel driver (modern eBPF probe) writes event into per-TGID ring buffers. Only the modern eBPF probe is supported, as it relies on [BPF_MAP_TYPE_RINGBUF](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_RINGBUF/) which does not have a per-CPU design as opposed of the `BPF_MAP_TYPE_PERF_EVENT_ARRAY` used by the legacy eBPF probe.
3131
* Each buffer is associated with an event loop worker thread, that processes events from its assigned ring buffer.
32-
* The `libsinsp` state, e.g. the thread state is maintained in a shared data structure, allowing all workers to access data pushed by other workers. This is crucial for handling events like clone() that rely on data written by other partitions.
33-
This requires designing lightweight synchronization mechanisms to ensure efficient access to shared state without introducing significant contention. A dedicated proposal document will address the design of the shared state and synchronization mechanisms, and data consistency.
34-
* Falco's rule evaluation is performed in parallel by multiple worker threads, each evaluating rules against the events they process.
35-
Current Falco plugins are not supposed to be thread-safe. A dedicated proposal document will address the design of a thread-safe plugin architecture.
32+
* The `libsinsp` state, e.g. the thread state is maintained in a shared data structure, allowing all workers to access data pushed by other workers. This is crucial for handling events like clone() that rely on data written by other partitions. This requires designing lightweight synchronization mechanisms to ensure efficient access to shared state without introducing significant contention. A dedicated proposal document will address the design of the shared state and synchronization mechanisms, and data consistency.
33+
* Falco's rule evaluation is performed in parallel by multiple worker threads, each evaluating rules against the events they process. Current Falco plugins are not supposed to be thread-safe. A dedicated proposal document will address the design of a thread-safe plugin architecture.
34+
35+
### Work Partitioning Strategies
36+
37+
A crucial and challenging design aspect is partitioning the work to achieve a good trade-off among the following properties:
38+
39+
1. **Even load balancing** between threads
40+
2. **Low contention** on shared data (or no shared data)
41+
3. **Avoiding temporal inconsistencies and causality violations** (e.g., processing a file-opening event before the related process-forking event)
42+
43+
The first two properties are primarily focused on performance, while the third is essential for the correctness of the solution. These aspects are intrinsically linked.
44+
45+
Based on the analysis below, **Static Partitioning by TGID** is the proposed approach for the initial implementation.
46+
47+
#### Static Partitioning by TGID (Thread Group ID / Process ID)
48+
49+
Events are routed based on the TGID at the kernel driver level to a ring-buffer dedicated to a specific partition. This partition is then consumed by a dedicated worker thread. The routing can be accomplished with a simple modulo operation, depending on the desired number of worker threads:
50+
51+
```
52+
hash(event->tgid) % num_workers
53+
```
54+
55+
**Pros:**
56+
57+
* Reduced need for thread synchronization. Only fork/clone and proc exit events require synchronization, as handling them necessitates accessing/writing thread information from/to threads, which might reside in a different partition.
58+
* Guarantee of sequential order processing of events related to the same thread group/process, as they are handled by the same worker thread. This limits the chance of temporal inconsistencies.
59+
60+
**Cons:**
61+
62+
* **Load Imbalance / "Hot" Process Vulnerability**: This static partitioning is susceptible to uneven worker load distribution, as a small number of high-activity ("hot") processes can overload the specific worker thread assigned to their TGID.
63+
* **Cross-Partition Temporal Inconsistency**: Events that require information from a parent thread (e.g., fork/clone events) can still lead to causality issues. If the parent's related event is handled by a different, lagging partition, the required context might be incomplete or arrive out of order. Note that load imbalance amplifies this issue. Missing thread information is easy to detect, but there are also cases where information is present but not up-to-date or ahead of the time the clone event happened.
64+
65+
**Mitigations:**
66+
67+
* **Last-Resort Fetching**: Fetching the thread information from a different channel to resolve the drift (e.g., proc scan, eBPF iterator). This solution is considered as a last resort because it risks slowing down the event processing loop, potentially negating the performance benefits of multi-threading.
68+
69+
* **Context Synchronization**: Wait for the required thread information to become available. This can be decomposed into two orthogonal concerns:
70+
71+
**How to handle the wait:**
72+
73+
* **Wait/Sleep (Blocking)**: The worker thread blocks (sleeping or spinning) until the required data becomes available. Simple to implement, but the worker is idle during the wait, reducing throughput.
74+
* **Deferring (Non-blocking)**: The event is copied/buffered for later processing; the worker continues with other events from its ring buffer. More complex (requires event copying, a pending queue, and a retry mechanism), but keeps the worker productive.
75+
76+
**How to detect data readiness:**
77+
78+
* **Polling**: Periodically check if the required data is available (spin-check for Wait/Sleep, or periodic retry for Deferring). Simple but wastes CPU cycles.
79+
* **Signaling**: Partitions proactively notify each other when data is ready. More efficient but requires coordination infrastructure (e.g., condition variables, eventfd, or message queues).
80+
81+
These combine into four possible approaches:
82+
83+
| | Polling | Signaling |
84+
|---|---------|-----------|
85+
| **Wait/Sleep** | Spin-check until ready | Sleep on condition variable, wake on signal |
86+
| **Deferring** | Periodically retry deferred events | Process deferred events when signaled |
87+
88+
**Synchronization point**: A natural synchronization point is the **clone exit parent event**. At this point, the parent process has completed setting up the child's initial state (inherited file descriptors, environment, etc.), making it safe to start processing events for the newly created thread group.
89+
90+
**Special case — `vfork()` / `CLONE_VFORK`**: When `vfork()` is used, the parent thread is blocked until the child calls `exec()` or exits, delaying the clone exit parent event. An alternative synchronization point may be needed (e.g., adding back clone enter parent).
91+
92+
### Other Considered Approaches
93+
94+
#### Static Partitioning by TID (Thread ID)
95+
96+
Similar to the previous approach, but events are routed by TID instead of TGID.
97+
98+
**Pros:**
99+
100+
* Guarantee of sequential order processing of events related to the same thread, as they are handled by the same worker thread. This limits the chance of temporal inconsistencies.
101+
* Good enough load balancing between partitions.
102+
103+
**Cons:**
104+
105+
* **Cross-Partition Temporal Inconsistency**: We can have temporal inconsistencies when accessing/writing information from/to other processes or from the Thread Group Leader (e.g., environment, file descriptor information is stored in the thread group leader).
106+
107+
#### Functional Partitioning (Pipelining)
108+
109+
Instead of partitioning the data, this approach partitions the work by splitting processing into phases:
110+
111+
1. **Extraction**: Runs in a single thread, the state is updated in this phase.
112+
2. **Processing**: Runs in a thread chosen from a worker thread pool, the state is accessed but not modified. The Rule Matching takes place in this phase.
113+
114+
**Pros:**
115+
116+
* The state handling remains single-threaded, avoiding any synchronization issue on the write side.
117+
* The load balancing of the Processing phase is good as it does not require any form of stickiness—every worker can take whatever event, and a simple round-robin policy can be applied.
118+
119+
**Cons:**
120+
121+
* The "Extraction" stage is likely to become the bottleneck; a single thread here limits total throughput regardless of how many cores you have.
122+
* As we are parallelizing extraction and processing phases, we need some sort of MVCC (multi-version concurrency control) technique to maintain multiple levels of state depending on the in-flight events to ensure data consistency.
123+
* Processing multiple events in parallel involves changes at the driver and libscap level. At the moment we are processing one event at a time from the driver memory without copying. To be able to process multiple events in parallel, we need to adapt the ring-buffer to make sure that `next()` does not consume the event. We would also need some flow control (e.g., backpressure) to avoid processing too many events in parallel. This last problem would arise only if the processing phase is slower than the extraction phase.
124+
125+
#### Comparison Summary
126+
127+
| Approach | Load Balancing | Contention | Temporal Consistency |
128+
|----------|----------------|------------|----------------------|
129+
| TGID | Moderate (hot process risk) | Low | Good (within process) |
130+
| TID | Good | Higher | Partial (thread-level only) |
131+
| Pipelining | Good (processing phase) | Low (writes) | Requires MVCC |
132+
133+
#### Rationale for TGID Partitioning
134+
135+
TGID partitioning was chosen because it offers the best balance between synchronization complexity and correctness guarantees. TID partitioning increases cross-partition access for thread group leader data (e.g. file descriptor table, working directory, environment variables), increasing the coordination cost. Functional partitioning, while elegant in its separation of concerns, introduces a single-threaded bottleneck in the extraction phase that limits scalability regardless of available cores, and requires complex MVCC mechanisms for data consistency and mechanism for handling multiple events in parallel.
36136

37137
### Risks and Mitigations
38138

39139
- **Increased Complexity**: Multi-threading introduces complexity in terms of synchronization and state management. Mitigation: Careful design of shared state and synchronization mechanisms, along with thorough testing.
40140
- **Synchronization Overhead vs Performance Gains**: The overhead of synchronization might negate the performance benefits of multi-threading. Mitigation: Use lightweight synchronization techniques and minimize shared state access.
41141
- **Synchronization Overhead vs Data Consistency**: In order to keep the synchronization overhead low with the shared state, we might need to relax some data consistency guarantees. Mitigation: Analyze the trade-offs and ensure that any relaxed guarantees do not compromise security.
42-
- **Uneven load balancing**: On large systems with a few syscall intensive processes, the load might not be evenly distributed across worker threads. Mitigation: Evaluate different load balancing strategies, such as per-TID. This would increase the contention on the shared state, so a careful analysis of the trade-offs is needed.

0 commit comments

Comments
 (0)