-
Notifications
You must be signed in to change notification settings - Fork 0
feat: optimize L7 parsing performance and fix multi-packet response #149
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| #define HTTP2_CLIENT_INITIATED_STREAM(stream_id) (stream_id & 0x01000000) // big-endian (network byte order) odd number | ||
| #define HTTP2_SETTINGS_FRAME 0x4 | ||
|
|
||
| #define bpf_read(src, dst) bpf_probe_read(&dst, sizeof(dst), src) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Please remove this macro and use Example of correct usage: __u32 frame_length;
if (bpf_probe_read(&frame_length, sizeof(frame_length), buf)) {
return 0; // Or other error code
} |
||
|
|
||
| static __always_inline | ||
| int is_client_preface(char *buf, __u64 size, __u8 method) { | ||
| if (method != METHOD_HTTP2_CLIENT_FRAMES || size < 24) { | ||
|
|
@@ -38,4 +40,54 @@ int looks_like_http2_frame(char *buf, __u64 size, __u8 method) { | |
| return is_server_preface(frame_type, stream_id, method); | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| static __always_inline | ||
| int is_http2_response_partial(char *buf, __u64 size, __u8 partial) { | ||
| // If this is a continuation of a partial response | ||
| if (partial) { | ||
| return 1; // Continue collecting HTTP/2 frames | ||
| } | ||
|
|
||
| // Need at least 9 bytes for HTTP/2 frame header | ||
| if (size < 9) { | ||
| return 2; // Partial, need more data | ||
| } | ||
|
|
||
| // Check if this looks like HTTP/2 frames | ||
| if (!looks_like_http2_frame(buf, size, METHOD_HTTP2_SERVER_FRAMES)) { | ||
| return 0; // Not HTTP/2 | ||
| } | ||
|
|
||
| // Parse frame header to check completeness | ||
| __u32 frame_length; | ||
| bpf_read(buf, frame_length); | ||
| frame_length = bpf_htonl(frame_length) >> 8; // Get 24-bit length | ||
|
|
||
| __u8 frame_type; | ||
| bpf_read(buf + 3, frame_type); | ||
|
|
||
| __u8 flags; | ||
| bpf_read(buf + 4, flags); | ||
|
|
||
| // Check if we have the complete frame | ||
| if (size < frame_length + 9) { | ||
| return 2; // Incomplete frame, need more data | ||
| } | ||
|
|
||
| // For DATA frames (0x0), check if END_STREAM flag (0x1) is set | ||
| if (frame_type == 0x0) { // DATA frame | ||
| if (!(flags & 0x1)) { // END_STREAM flag not set | ||
| return 2; // More DATA frames expected | ||
| } | ||
| } | ||
|
|
||
| // For HEADERS frames (0x1), check if END_HEADERS flag (0x4) is set | ||
| if (frame_type == 0x1) { // HEADERS frame | ||
| if (!(flags & 0x4)) { // END_HEADERS flag not set | ||
| return 2; // More HEADERS/CONTINUATION frames expected | ||
| } | ||
| } | ||
|
|
||
| return 1; // Complete response or frame sequence | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,7 @@ | |
| }) | ||
| #define COPY_PAYLOAD(dst, size, src) ({ \ | ||
| TRUNCATE_PAYLOAD_SIZE(size); \ | ||
| if (bpf_probe_read(dst, size, src)) { \ | ||
| if (size > 0 && bpf_probe_read(dst, size, src)) { \ | ||
| return 0; \ | ||
| } \ | ||
| }) | ||
|
|
@@ -137,6 +137,24 @@ struct user_msghdr { | |
| __u32 msg_flags; | ||
| }; | ||
|
|
||
| static inline __attribute__((__always_inline__)) | ||
| void init_l7_event(struct l7_event *e) { | ||
| // Only initialize critical fields, payload/response arrays are overwritten anyway | ||
| e->fd = 0; | ||
| e->connection_timestamp = 0; | ||
| e->pid = 0; | ||
| e->status = STATUS_UNKNOWN; | ||
| e->duration = 0; | ||
| e->protocol = PROTOCOL_UNKNOWN; | ||
| e->method = METHOD_UNKNOWN; | ||
| e->padding = 0; | ||
| e->statement_id = 0; | ||
| e->payload_size = 0; | ||
| e->response_size = 0; | ||
| // Skip zeroing payload arrays - they're overwritten by COPY_PAYLOAD anyway | ||
| // This saves ~10,000 instructions per event | ||
| } | ||
|
|
||
| static inline __attribute__((__always_inline__)) | ||
| void send_event(void *ctx, struct l7_event *e, struct connection_id cid, struct connection *conn) { | ||
| e->connection_timestamp = conn->timestamp; | ||
|
|
@@ -222,14 +240,38 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size, | |
| k.is_tls = is_tls; | ||
| k.stream_id = -1; | ||
|
|
||
| if (is_http_request(payload)) { | ||
| // Fast path: Check most common protocols first | ||
| // Use first bytes to quickly eliminate protocols | ||
| char first_4[4]; | ||
| if (size >= 4 && !bpf_probe_read(first_4, 4, payload)) { | ||
| // HTTP: GET, POST, PUT, HEAD, DELETE, OPTIONS, PATCH | ||
| if ((first_4[0] == 'G' && first_4[1] == 'E' && first_4[2] == 'T') || | ||
| (first_4[0] == 'P' && first_4[1] == 'O' && first_4[2] == 'S') || | ||
| (first_4[0] == 'P' && first_4[1] == 'U' && first_4[2] == 'T') || | ||
| (first_4[0] == 'H' && first_4[1] == 'E' && first_4[2] == 'A') || | ||
| (first_4[0] == 'D' && first_4[1] == 'E' && first_4[2] == 'L') || | ||
| (first_4[0] == 'O' && first_4[1] == 'P' && first_4[2] == 'T') || | ||
| (first_4[0] == 'P' && first_4[1] == 'A' && first_4[2] == 'T') || | ||
| (first_4[0] == 'C' && first_4[1] == 'O' && first_4[2] == 'N')) { | ||
| req->protocol = PROTOCOL_HTTP; | ||
| } else if (first_4[0] == 'P' && first_4[1] == 'R' && first_4[2] == 'I' && first_4[3] == ' ') { | ||
| // HTTP/2: Check for connection preface or frame header | ||
| if (looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) { | ||
| req->protocol = PROTOCOL_HTTP2; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // If fast path didn't detect protocol, try full detection | ||
| if (req->protocol == PROTOCOL_UNKNOWN && is_http_request(payload)) { | ||
| req->protocol = PROTOCOL_HTTP; | ||
| } else if (is_postgres_query(payload, size, &req->request_type)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_postgres_query(payload, size, &req->request_type)) { | ||
| if (req->request_type == POSTGRES_FRAME_CLOSE) { | ||
| struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero); | ||
| if (!e) { | ||
| return 0; | ||
| } | ||
| init_l7_event(e); | ||
| e->protocol = PROTOCOL_POSTGRES; | ||
| e->method = METHOD_STATEMENT_CLOSE; | ||
| e->payload_size = size; | ||
|
|
@@ -238,16 +280,17 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size, | |
| return 0; | ||
| } | ||
| req->protocol = PROTOCOL_POSTGRES; | ||
| } else if (is_redis_query(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_redis_query(payload, size)) { | ||
| req->protocol = PROTOCOL_REDIS; | ||
| } else if (is_memcached_query(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_memcached_query(payload, size)) { | ||
| req->protocol = PROTOCOL_MEMCACHED; | ||
| } else if (is_mysql_query(payload, size, &req->request_type)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_mysql_query(payload, size, &req->request_type)) { | ||
| if (req->request_type == MYSQL_COM_STMT_CLOSE) { | ||
| struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero); | ||
| if (!e) { | ||
| return 0; | ||
| } | ||
| init_l7_event(e); | ||
| e->protocol = PROTOCOL_MYSQL; | ||
| e->method = METHOD_STATEMENT_CLOSE; | ||
| e->payload_size = size; | ||
|
|
@@ -256,53 +299,56 @@ int trace_enter_write(void *ctx, __u64 fd, __u16 is_tls, char *buf, __u64 size, | |
| return 0; | ||
| } | ||
| req->protocol = PROTOCOL_MYSQL; | ||
| } else if (is_mongo_query(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_mongo_query(payload, size)) { | ||
| req->protocol = PROTOCOL_MONGO; | ||
| } else if (is_rabbitmq_produce(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_rabbitmq_produce(payload, size)) { | ||
| struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero); | ||
| if (!e) { | ||
| return 0; | ||
| } | ||
| init_l7_event(e); | ||
| e->protocol = PROTOCOL_RABBITMQ; | ||
| e->method = METHOD_PRODUCE; | ||
| send_event(ctx, e, cid, conn); | ||
| return 0; | ||
| } else if (nats_method(payload, size) == METHOD_PRODUCE) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && nats_method(payload, size) == METHOD_PRODUCE) { | ||
| struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero); | ||
| if (!e) { | ||
| return 0; | ||
| } | ||
| init_l7_event(e); | ||
| e->protocol = PROTOCOL_NATS; | ||
| e->method = METHOD_PRODUCE; | ||
| send_event(ctx, e, cid, conn); | ||
| return 0; | ||
| } else if (is_cassandra_request(payload, size, &k.stream_id)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_cassandra_request(payload, size, &k.stream_id)) { | ||
| req->protocol = PROTOCOL_CASSANDRA; | ||
| } else if (looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && looks_like_http2_frame(payload, size, METHOD_HTTP2_CLIENT_FRAMES)) { | ||
| struct l7_event *e = bpf_map_lookup_elem(&l7_event_heap, &zero); | ||
| if (!e) { | ||
| return 0; | ||
| } | ||
| init_l7_event(e); | ||
| e->protocol = PROTOCOL_HTTP2; | ||
| e->method = METHOD_HTTP2_CLIENT_FRAMES; | ||
| e->duration = bpf_ktime_get_ns(); | ||
| e->payload_size = size; | ||
| COPY_PAYLOAD(e->payload, size, payload); | ||
| send_event(ctx, e, cid, conn); | ||
| return 0; | ||
| } else if (is_clickhouse_query(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_clickhouse_query(payload, size)) { | ||
| req->protocol = PROTOCOL_CLICKHOUSE; | ||
| } else if (is_zk_request(payload, total_size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_zk_request(payload, total_size)) { | ||
| req->protocol = PROTOCOL_ZOOKEEPER; | ||
| } else if (is_kafka_request(payload, size, &req->request_id)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_kafka_request(payload, size, &req->request_id)) { | ||
| req->protocol = PROTOCOL_KAFKA; | ||
| struct l7_request *prev_req = bpf_map_lookup_elem(&active_l7_requests, &k); | ||
| if (prev_req && prev_req->protocol == PROTOCOL_KAFKA) { | ||
| req->ns = prev_req->ns; | ||
| } | ||
| } else if (is_dubbo2_request(payload, size)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_dubbo2_request(payload, size)) { | ||
| req->protocol = PROTOCOL_DUBBO2; | ||
| } else if (is_dns_request(payload, size, &k.stream_id)) { | ||
| } else if (req->protocol == PROTOCOL_UNKNOWN && is_dns_request(payload, size, &k.stream_id)) { | ||
| req->protocol = PROTOCOL_DNS; | ||
| } | ||
|
|
||
|
|
@@ -393,11 +439,7 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret) | |
| if (!e) { | ||
| return 0; | ||
| } | ||
| e->protocol = PROTOCOL_UNKNOWN; | ||
| e->status = STATUS_UNKNOWN; | ||
| e->method = METHOD_UNKNOWN; | ||
| e->statement_id = 0; | ||
| e->payload_size = 0; | ||
| init_l7_event(e); | ||
| e->response_size = ret; | ||
| COPY_PAYLOAD(e->response, ret, payload); | ||
| if (is_rabbitmq_consume(payload, ret)) { | ||
|
|
@@ -435,13 +477,21 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret) | |
| } | ||
| response = 1; | ||
| } else if (looks_like_http2_frame(payload, ret, METHOD_HTTP2_SERVER_FRAMES)) { | ||
| e->protocol = PROTOCOL_HTTP2; | ||
| e->method = METHOD_HTTP2_SERVER_FRAMES; | ||
| e->duration = bpf_ktime_get_ns(); | ||
| e->payload_size = ret; | ||
| COPY_PAYLOAD(e->payload, ret, payload); | ||
| send_event(ctx, e, cid, conn); | ||
| return 0; | ||
| // Check if there's a matching HTTP/2 request for gRPC handling | ||
| req = bpf_map_lookup_elem(&active_l7_requests, &k); | ||
| if (req && req->protocol == PROTOCOL_HTTP2) { | ||
| // Handle as part of HTTP/2 response sequence | ||
| response = 1; | ||
| } else { | ||
| // Send standalone HTTP/2 frame (non-gRPC) | ||
| e->protocol = PROTOCOL_HTTP2; | ||
| e->method = METHOD_HTTP2_SERVER_FRAMES; | ||
| e->duration = bpf_ktime_get_ns(); | ||
| e->payload_size = ret; | ||
| COPY_PAYLOAD(e->payload, ret, payload); | ||
| send_event(ctx, e, cid, conn); | ||
| return 0; | ||
| } | ||
| } else { | ||
| return 0; | ||
| } | ||
|
|
@@ -451,7 +501,14 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret) | |
| e->payload_size = req->payload_size; | ||
| COPY_PAYLOAD(e->payload, req->payload_size, req->payload); | ||
| if (e->protocol == PROTOCOL_HTTP) { | ||
| response = is_http_response(payload, &e->status); | ||
| response = is_http_response_partial(payload, ret, req->partial); | ||
| if (response == 2) { // partial | ||
| req->partial = 1; | ||
| return 0; // keeping the query in the map | ||
| } | ||
| if (response == 1) { | ||
| is_http_response(payload, &e->status); // Get status code | ||
| } | ||
|
Comment on lines
503
to
+511
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of a complete HTTP response ( To fix this, you can modify Example modification in // in http.c
int is_http_response_partial(char *buf, __u64 size, __u8 partial, __s32 *status) {
// ...
if (!is_http_response(buf, status)) { // pass status pointer
return 0;
}
// ...
}Then you can update the call site here. if (e->protocol == PROTOCOL_HTTP) {
response = is_http_response_partial(payload, ret, req->partial, &e->status);
if (response == 2) { // partial
req->partial = 1;
return 0; // keeping the query in the map
}
} |
||
| } else if (e->protocol == PROTOCOL_POSTGRES) { | ||
| response = is_postgres_response(payload, ret, &e->status); | ||
| if (req->request_type == POSTGRES_FRAME_PARSE) { | ||
|
|
@@ -485,6 +542,15 @@ int trace_exit_read(void *ctx, __u64 id, __u32 pid, __u16 is_tls, long int ret) | |
| req->partial = 1; | ||
| return 0; // keeping the query in the map | ||
| } | ||
| } else if (e->protocol == PROTOCOL_HTTP2) { | ||
| response = is_http2_response_partial(payload, ret, req->partial); | ||
| if (response == 2) { // partial | ||
| req->partial = 1; | ||
| return 0; // keeping the query in the map | ||
| } | ||
| if (response == 1) { | ||
| e->method = METHOD_HTTP2_SERVER_FRAMES; | ||
| } | ||
| } else if (e->protocol == PROTOCOL_DUBBO2) { | ||
| response = is_dubbo2_response(payload, &e->status); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop to find the end of headers (
\r\n\r\n) callsbpf_probe_readfor 4 bytes on every iteration.bpf_probe_readis a relatively expensive call, and this PR is focused on performance. A more performant approach would be to read byte-by-byte and use a state machine to find the pattern. This would reduce the overhead ofbpf_probe_readcalls inside the loop.