mrproliu opened a new pull request, #211:
URL: https://github.com/apache/skywalking-rover/pull/211

   ## Background
   
   When running the access log module on a node with multi-threaded workloads 
(JVM, Go,
   etc.), `rover` keeps logging the **same process being "detected" and 
"recognized as
   dead" over and over**, every few seconds, for the whole lifetime of the 
process.
   
   The affected processes are never actually restarted — they are healthy and 
long-lived.
   The churn produces a large amount of misleading `info` logs, makes the 
process entities
   flap on the backend (their `id` never stabilizes), and pollutes the eBPF
   `process_monitor_control` map.
   
   ## Symptom (logs)
   
   The same PID is added by the eBPF path and immediately deleted by the 
periodic `/proc`
   scan, in a tight loop:
   
   ```
   time="2026-06-27T01:54:40Z" level=info msg="detected new process by add 
process: pid: 814309, entity: 
{\"Layer\":\"K8S_SERVICE\",\"ServiceName\":\"skywalking-showcase::demo-oap.skywalking-showcase\",\"InstanceName\":\"demo-oap-84c6bc7b44-8v6cn\",\"ProcessName\":\"java\",\"Labels\":[\"k8s-service\"]}"
 module=process.finder
   time="2026-06-27T01:54:45Z" level=info msg="the process has been recognized 
as dead, so deleted. pid: 814309, entity: {...\"ProcessName\":\"java\"...}, id: 
" module=process.finder
   time="2026-06-27T01:54:49Z" level=info msg="detected new process by add 
process: pid: 814309, entity: {...\"ProcessName\":\"java\"...}" 
module=process.finder
   time="2026-06-27T01:54:50Z" level=info msg="the process has been recognized 
as dead, so deleted. pid: 814309, ... id: " module=process.finder
   ...repeats every ~5s...
   ```
   
   Aggregated over a single rover instance, the "java" entries appear **0 
times** via the
   periodic scan (`by sync all`) but are re-added **continuously** by the eBPF 
path
   (`by add process`), while being marked dead on every scan tick:
   
   ```
   detected new process by add process : 1031
   detected new process by sync all    : 0     (for java)
   recognized as dead                  : 152   (for java)
   ```
   
   Note the dead entries carry an **empty `id`** — the entity dies (from 
rover's view)
   before it can complete backend registration.
   
   ## Root cause
   
   The two process-discovery paths disagree about what a "process" is:
   
   1. **Periodic scan** (`pkg/process/finders/kubernetes/finder.go`, every 5s) 
enumerates
      processes via gopsutil `process.Processes()`, which does a 
`readdir(/proc)` and
      therefore only returns **TGIDs** (thread-group leaders / real processes). 
Anything
      not in this set is marked dead by `SyncAllProcessInFinder`.
   
   2. **eBPF path** feeds PIDs from the `sched_process_fork` tracepoint
      (`bpf/accesslog/process/process.c`) into `ShouldMonitor()` → 
`process.NewProcess()`,
      which validates a PID with `os.Stat(/proc/<pid>)` — and `/proc/<tid>` is 
accessible
      for **threads** too, so a thread is happily accepted as a "process".
   
   The bug is in the tracepoint:
   
   ```c
   SEC("tracepoint/sched/sched_process_fork")
   int tracepoint_sched_process_fork(struct trace_event_raw_sched_process_fork* 
ctx) {
       __u32 tgid = ctx->parent_pid;   // <-- parent_pid is a TID, not a TGID
       ...
       event.pid = tgid;               // reported to user space as if it were 
a process id
   }
   ```
   
   The `sched_process_fork` tracepoint format only exposes thread ids:
   
   ```
   field:pid_t parent_pid;   // = parent->pid  (kernel task->pid == TID)
   field:pid_t child_pid;    // = child->pid   (TID)
   ```
   
   In the kernel, `task->pid` is the **thread id (TID)** and `task->tgid` is 
the **process
   id**. A multi-threaded app constantly creates threads, and any worker thread 
that calls
   `clone()`/`fork()` makes the tracepoint fire with `parent_pid` = that worker 
thread's
   TID. rover then registers the **thread** as a process. The 5s scan 
(TGID-only) never
   sees that TID, so it deletes it; the next thread activity re-adds it → 
infinite churn.
   
   Verified on a live node — the "processes" being churned are JVM threads:
   
   ```
   tid     comm              Tgid (real process)   in `ls /proc` (readdir)   
state
   817781  prometheus-http   815492                NO  (it is a thread)      
ALIVE 4.7h
   818295  prometheus-http   815492                NO                        
ALIVE
   822290  grpc-nio-worker   821882                NO                        
ALIVE
   910081  java              887536                NO                        
ALIVE
   815492  (main java)       815492 (itself)       YES (a real process)      
never churned
   ```
   
   The stable main JVMs (TGIDs) are tracked correctly and never marked dead; 
only the
   threads (TIDs) flip.
   
   ## Fix
   
   Report the real process **TGID** instead of the tracepoint's `parent_pid` (a 
TID):
   
   ```c
   SEC("tracepoint/sched/sched_process_fork")
   int tracepoint_sched_process_fork(struct trace_event_raw_sched_process_fork* 
ctx) {
   -    __u32 tgid = ctx->parent_pid;
   +    // ctx->parent_pid is the forking task's TID (thread id), not the 
process id.
   +    // For multi-threaded apps (e.g. JVM) a worker thread forking would 
report its
   +    // thread TID, which the periodic /proc scan never lists, causing the 
process to
   +    // flip between detected and dead. Use the real TGID instead.
   +    __u32 tgid = bpf_get_current_pid_tgid() >> 32;
       __u32 v = 1;
       bpf_map_update_elem(&process_monitor_control, &tgid, &v, 0);
   
       struct process_execute_event event = {};
       event.pid = tgid;
       bpf_perf_event_output(ctx, &process_execute_queue, BPF_F_CURRENT_CPU, 
&event, sizeof(event));
       return 0;
   }
   ```
   
   This also fixes a latent bug: the consumer `tgid_should_trace()` looks up
   `process_monitor_control` **by TGID**, but the producer was inserting 
**TIDs**, so those
   entries could never match and just wasted map space.
   
   `sched_process_fork` is invoked as `trace_sched_process_fork(current, 
child)` from
   `copy_process()`, so `current` is the forking **parent** task. 
`bpf_get_current_pid_tgid()
   >> 32` therefore yields the parent's TGID — semantically equivalent to the 
original
   `parent_pid`'s process, just corrected from TID to TGID.
   
   ## Is `bpf_get_current_pid_tgid()` valid inside a tracepoint program?
   
   Yes.
   
   - It runs in process context here (the fork syscall path), so `current` is a 
valid user
     task and the helper returns its `pid`/`tgid` (lower/upper 32 bits 
respectively).
   - `BPF_PROG_TYPE_TRACEPOINT` is among the supported program types for this 
helper — see
     the official helper reference: 
<https://docs.ebpf.io/linux/helper-function/bpf_get_current_pid_tgid/>
     and the kernel `bpf-helpers(7)` man page: 
<https://man7.org/linux/man-pages/man7/bpf-helpers.7.html>
   - It is already used by many existing tracepoint programs in this repository
     (`bpf/accesslog/syscalls/transfer.c`, `connect.c`, `close.c`,
     `bpf/accesslog/l24/write_l4.c`, ...), confirming it loads and works for 
tracepoint
     programs.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to