fix: agent - Fix proc-events high CPU on proc-info eviction - #11895
fix: agent - Fix proc-events high CPU on proc-info eviction#11895yinjiping wants to merge 15 commits into
Conversation
971f25d to
99f1088
Compare
09ff4a4 to
5bada36
Compare
|
@claude review一下当前PR中的代码修改 |
|
Claude encountered an error after 3m 19s —— View job I'll analyze this and get back to you. |
必须修改的问题1. 修复 worker 获取错误
|
183303c to
43496df
Compare
总结:proc-events 高 CPU 修复及后续加固一、原始问题进程退出或 exec 后,proc-events 会从 Proc Cache hash 中删除 AO_DEC(&p->use);
while (AO_GET(&p->use) != 0)
CLIB_PAUSE();
free_proc_cache(p);如果 Java symbol task 或其他查询长期没有归还引用: proc-events 就会一直执行忙等: 结果是:
原始时序: 二、修复 Java symbol 引用长期不归还相关提交: 主要修改位于 jvm_symbol_collect.c。 1. Java refresh 等待增加 10 秒超时原来的无限等待: pthread_cond_wait();改成: pthread_cond_timedwait();最大等待: #define JAVA_SYMS_REFRESH_TIMEOUT_SECS 10超时后:
2. 修复 condition variable 丢失唤醒以下操作统一由 避免请求线程刚设置 refresh、还没进入等待时,collector 已经完成 signal,导致请求线程永久睡眠。 3. collector 退出时唤醒等待者collector 异常退出时设置: task->stopped = true;
task->need_refresh = false;
task->update_status = -1;
pthread_cond_broadcast(&task->cond);等待线程能够立即退出,不必永久等待已经消失的 collector。 4. Java task 增加引用计数为 ref_count
stoppedcreator、worker 和 refresh waiter 分别持有引用,最后一个引用释放时才销毁 task,避免: 5. 修复线程池失败路径补充处理:
三、修复
|
18564ae fix: make Java symbol refresh timeout reliable |
**Problem (high CPU)**
exec_proc_info_cache_update() processes process-exit events from
proc_event_ring and eventually calls free_symbolizer_cache_kvp() when a
process is evicted from the symbolizer proc-info cache. That function busy
waits for every in-flight reference to drain:
AO_DEC(&p->use);
/* Ensure that all tasks are completed before releasing. */
while (AO_GET(&p->use) != 0) /* never sleeps; burns one core */
CLIB_PAUSE();
free_proc_cache(p);
p->use fails to reach zero when a Java symbol-file refresh task keeps its
reference forever. The refresh path was update_java_symbol_file() waiting
unconditionally with pthread_cond_wait() for ipc_receiver_main (the Java
collector thread) to finish generating the symbol file. If that collector
thread exited abnormally or hung without signalling, the waiter blocked
indefinitely, java_syms_update_main() never reached its AO_DEC(&p->use),
and the next eviction of that process (exec_proc_info_cache_update ->
free_symbolizer_cache_kvp) spun forever in the while loop - sustained high
CPU on the proc-events thread.
While investigating we also found that the task could be freed while the
waiter still used it (no reference counting), and that several task/thread
pool failure paths left the task queued after create_symbol_collect_task()
had freed it, or double-closed the map/log sockets. Those were fixed as part
of making the refresh path reliably terminate so the busy wait can make
progress.
**Changes made**
All changes are in agent/src/ebpf/user/profile/java/jvm_symbol_collect.c
and its header.
1) Bounded, wakeable symbol-file refresh wait (fixes the stuck reference)
- update_java_symbol_file() now takes a task reference first and, instead
of pthread_cond_wait() forever, waits in a
while (need_refresh) + pthread_cond_timedwait() loop bounded by
JAVA_SYMS_REFRESH_TIMEOUT_SECS (10s). On timeout it clears need_refresh,
sets update_status = -1 and returns, so java_syms_update_main() can
finish and drop its p->use reference; the caller never holds a
symbolizer_proc_info (p->use) forever and proc-events never spins.
- The collector marks the task stopped (stopped = true, update_status =
-1, need_refresh = false) under task->mutex and broadcasts task->cond
when it exits, so refresh waiters wake up immediately instead of waiting
out the timeout on a collector that is gone.
2) Task lifetime reference counting (fixes use-after-free of the task)
- Added volatile int ref_count and volatile bool stopped to struct task.
- Split destroy_task() into put_task_ref(): decrement ref_count under the
pool lock and call destroy_task() only when it reaches zero. The worker
owns the base reference from enqueue until the task is removed from its
slot; update_java_symbol_file() takes an extra reference through
get_ref_task_by_pid() while holding the pool lock, so the task is never
freed while the refresh waiter is still touching it.
- Worker teardown clears the pool slot and calls put_task_ref() instead of
destroying the task directly; update_java_symbol_file() calls
put_task_ref() on every return path (pid gone / stale task / stopped /
refresh done / timeout).
3) Thread-pool slot growth and enqueue-failure rollback
- Grow the slot array with realloc() and reserve the new slot before
pthread_create(); publish thread_count only after the thread has been
created and detached. This removes the heap out-of-bounds read in loops
iterating pool->threads[0..thread_count) when realloc() failed after
++pool->thread_count had already run.
- On failure of realloc()/pthread_create()/pthread_detach(), unlink the
already enqueued task (list_head_del + task_count--/pending_tasks--) so
"failed" means "never enqueued"; create_symbol_collect_task() can then
free the task without leaving a dangling queue entry that a worker would
later dequeue after it was freed (use-after-free / corrupted queue).
- On pthread_detach() failure the worker already exists and keeps running,
so its slot and thread_count stay published (the pool has no stop/join
teardown, so no zombie accumulates at runtime and the kernel reclaims
threads at process exit); only the task is rolled back.
4) Double-close guard on the enqueue-failure path
- In create_symbol_collect_task(), on add_task failure call put_task_ref()
(which frees the task and the sockets moved into its args when the count
reaches zero) and set the local map_socket/log_socket to -1 so the
cleanup label does not close the same fds a second time (double close
could silently close an unrelated fd once the number was reused).
**Impact scope**
- Affects only the eBPF Java symbol collector
(agent/src/ebpf/user/profile/java/).
- The Java symbol-file refresh wait is now bounded (10s) and wakeable, so a
hung/exited collector thread no longer leaves p->use elevated forever and
no longer causes free_symbolizer_cache_kvp() to spin (high CPU) when the
process is evicted from the proc-info cache.
- Reference-counted task lifetime prevents the refresh waiter from touching a
task that the worker has already released.
- Failure paths of thread-pool enqueue no longer leave dangling queued tasks
or double-close the map/log sockets.
- The new fields change the layout of struct task at compile time only; there
is no external ABI dependency.
**Verification plan**
- Authoritative host (10.50.5.27, v6.6 branch): gcc -std=gnu99 -fsyntax-only
-Wall (with the full include set / macros / Java agent definitions) passes,
SYNTAX_OK, no new warnings.
- High-CPU scenario: make the Java collector thread exit/hang during a symbol
refresh and evict the process through exec_proc_info_cache_update();
confirm free_symbolizer_cache_kvp() no longer spins and update_java_symbol_file()
returns within ~10s.
- Failure injection: realloc/thread-create failure returns -1/-2 with the
queue counts rolled back and no double close in create_symbol_collect_task().
- Java agent end-to-end regression: multi-PID collection, process exit/recreate
and concurrent symbol-file refresh.
**Branches involved**
* v6.6 (fix branch on the authoritative host); merge into later releases as
planned.
**Checklist**
- [ ] Dependency update needed (no)
- [x] Common problem (bounded wait / reference counting reusable for other
tasks in the same pool)
- [x] Compiles (gcc -fsyntax-only SYNTAX_OK)
- [ ] Unit tests pass (to run on the authoritative host per the verification
plan)
A bihash lookup returned a proc-info pointer before p->use was incremented. A concurrent process-exit path could remove the entry, transfer its base reference to proc-events, and free it while the reader was descheduled in that window.
Use cache-line-separated reader slots selected by TLS thread_index around hash_search -> AO_INC. Serialize writer search-and-delete and full-table walks, then wait for all acquisition slots after removal before transferring or releasing the hash-owned reference.
Failure sequence:
reader sk-reader proc-events
hash_search -> p
descheduled
delete p
enqueue p
use 1 -> 0
free p
AO_INC p->use [UAF]
Fixed sequence:
reader slot sk-reader proc-events
slot active++
hash_search -> p
descheduled
lock writer
delete p
unlock writer
wait reader slots
AO_INC p->use
slot active--
enqueue p
drop base ref
wait p->use == 0
free p
free_symbolizer_cache_kvp() dropped the cache ownership reference and then busy-waited for every reader to release its reference. A long-lived or leaked reference could therefore make the proc-events thread spin indefinitely, consume CPU, and stop processing subsequent exec and exit events. Drop the ownership reference without waiting. Reclaim immediately when it was the last reference; otherwise link the object into a proc-events-owned retired list and retry reclamation during later cache-update iterations. The intrusive list requires no allocation, and objects are never freed until their reference count reaches zero.
A retired proc cache object may remain referenced after its ownership reference is dropped, while the use count alone does not identify which long-lived acquisition path last retained it. Add an atomic use_reason marker and helpers for recording and reading it. Mark hash-query and Java symbol-task acquisitions, keep temporary local references unchanged, and preserve the marker after reference release for later diagnostics.
Deferred process caches can remain referenced after hash removal, but there was no way to inspect how many were waiting, how long they had waited, or which long-lived reference last retained them. Record a monotonic timestamp on first retirement and protect the retired list with a dedicated mutex. Build diagnostic snapshots while the list is stable, then keep expensive cache destruction outside the lock. Add 'deepflow-ebpfctl proc-cache-reclaim show --older-than' to report active and retired counts, accounted memory, process start time, wait duration, symbol-cache state, and the last reference-increment reason. The total count excludes objects in proc_event_ring; the hash has an arena-size limit rather than a fixed entry-count limit.
Deferred proc caches can retain references after leaving the hash, while new processes continue allocating replacements. The bihash arena does not cover proc-info objects or symbol caches, so a leaked reference or process storm could grow memory without an object-count bound. Add a configurable total limit covering reserved, active, proc-event ring, and retired objects. Reserve slots with CAS before both allocation paths, return them on every initialization failure, and release them only after final cache destruction. Preserve ownership and reclaim locally when proc-event ring allocation or enqueue fails. Expose capacity, rejection, reclamation, and oldest-wait metrics; extend proc-cache-reclaim diagnostics with true total usage and bounded detail output; rate-limit capacity logs; and support validated startup and hot-update configuration without adding work to proc-cache lookup paths.
Workers read a shared pool thread index before acquiring the pool lock. A delayed worker could observe the index assigned to a later worker, fail the thread identity check, and exit while thread_count still included it. Long-lived Java symbol tasks could then remain pending without an available worker. Remove the shared startup index. Each worker now acquires the pool lock and locates its fully published slot with pthread_equal(). The creator holds the same lock through slot publication, so every worker binds to its own stable slot regardless of scheduling order.
The refresh waiter recalculated a CLOCK_REALTIME deadline after every wakeup, allowing spurious wakeups or wall-clock changes to extend or shorten the wait. A timeout racing with completion could also overwrite a successful refresh, while other condition-wait errors were ignored. Initialize the task condition variable with CLOCK_MONOTONIC, compute one deadline per refresh, recheck the predicate before handling the wait result, and fail all nonzero wait errors. Clean up the synchronization objects with the task and report pthread_detach failures using its returned error code instead of errno.
18564ae to
c53911a
Compare
symbols_cache_update could free and replace a resolver while symcache_resolve was still using it because the update and resolve paths used different locks. This allowed resolver fields to be accessed after bcc_free_symcache. Protect resolver replacement with the per-process mutex and validate the resolver while holding that mutex. Release the resolve mutex before taking the process state lock, then use a short p->lock-to-mutex recheck so stale misses cannot mark a replacement resolver without introducing a reverse lock order.
Retired proc-info objects could retain large BCC symbol caches indefinitely while outstanding references delayed final object reclamation. Detach resolvers under the per-process mutex, free them outside the retired-list lock in bounded batches, and keep the proc-info object until its reference count reaches zero. Prevent updates from rebuilding caches for exited objects and use atomic access for lifecycle fields shared across locks.
86ec0ee to
deb0a1d
Compare
The proc-events creator continued dereferencing a proc-info object after publishing it in the hash. A concurrent exec or exit could remove the entry and free it when event-ring enqueue failed, causing a use-after-free. The post-publication mount-cache registration also duplicated the registration performed during initialization and over-counted proc_count. Register mount information exactly once before publication, make the add helper return no proc-info pointer, and stop accessing the object after a successful hash insertion.

Problem
When a process exited or executed a new image,
proc-eventsremoved itssymbolizer_proc_infofrom the hash and waited for all outstanding references to be released:If a Java symbol task or another reader failed to release its reference,
p->usenever reached zero. Theproc-eventsthread then spun indefinitely, consuming an entire CPU core and preventing subsequent exec/exit events from being processed.1. Make Java Symbol Refresh Bounded and Wakeable
The Java symbol refresh path was updated so it can no longer retain a proc-info reference indefinitely.
Changes include:
pthread_cond_wait()withpthread_cond_timedwait().need_refresh, set the update status to failure, and return so the caller can releasep->use.need_refreshwith the sametask->mutexto prevent lost wakeups.2. Protect Java Symbol Task Lifetime
The Java task itself could previously be freed while another thread was still accessing it.
The task now contains:
The creator, worker, and refresh waiter hold their own references. The task is destroyed only after the final reference is released.
Additional thread-pool fixes include:
realloc()orpthread_create()fails.pthread_create()usingret != 0.Relevant commits:
3. Protect
hash_search → use++Reference AcquisitionA separate UAF window existed between finding a proc-info pointer and incrementing its reference count:
The lookup path now uses cache-line-separated per-thread reader slots:
Writers serialize hash search/delete operations and wait for all active reference-acquisition sections to finish after removing an entry:
This protects only the short
hash_search → use++window. The caller’s later access remains protected byp->use.Commit:
4. Remove Busy-Waiting from Proc-Info Eviction
The fundamental high-CPU fix was to stop waiting synchronously for
p->useto reach zero.The old behavior:
was replaced with deferred reclamation:
The
proc-eventsthread now continues processing other events immediately.A reaper periodically scans the retired list:
Expensive symbol-cache destruction is performed after releasing the retired-list lock.
Even if a future reference is retained indefinitely,
proc-eventswill no longer spin or block.Commit:
5. Record Why a Reference Was Acquired
A
use_reasonmarker was added tosymbolizer_proc_infoto help diagnose long-lived references.The currently recorded reasons are:
The marker records the most recent important reference-increment reason. It does not maintain a separate reference count for each reason.
Commit:
6. Add Retired Proc-Cache Diagnostics
The retired list now has:
The following command was added:
It reports summary fields such as:
Per-process details include:
At most 1,024 detail records are returned per request, while summary counters still cover the complete retired list.
Commit:
Remote execution support was added separately in
deepflow-core:7. Bound Proc-Cache Memory Growth
Deferred reclamation prevents high CPU, but a permanently retained reference could still cause retired objects to accumulate. A configurable total object limit was therefore added:
Configuration:
The limit covers every allocated but not yet freed proc-cache object:
A capacity slot is reserved before allocation using CAS. It is returned on every initialization failure or after final destruction in
free_proc_cache().When the limit is reached:
The limit applies on Agent startup and supports runtime hot updates.
Commits:
8. Preserve Ownership When Ring Enqueue Fails
After a proc-info entry leaves the hash, ownership is normally transferred to
proc_event_ring.Previously, if event allocation or ring enqueue failed, the object could be left without an owner:
The failure path now retains ownership locally:
The object is then either:
use == 0, orThis prevents leaks of the proc-info object, its symbol cache, and its capacity slot.
9. Add Capacity Metrics and Rate-Limited Logging
Runtime metrics include:
Capacity logging behavior is:
The timestamps use
CLOCK_MONOTONIC, so system clock adjustments do not affect rate limiting.Repeated warnings are event-driven: if no creation, release, or configuration event occurs, only the initial warning is guaranteed.
Performance Impact
The capacity work does not add a global capacity lock, capacity CAS, retired-list lock, or logging check to normal proc-info hash lookups.
New operations occur only during:
The lookup path retains only the short per-thread reader-slot protection needed to make
hash_search → use++safe.Final Protection Model
The final implementation provides three layers of protection:
The resulting behavior guarantees that:
proc-eventsno longer spins while waiting forp->use.hash_search → use++window cannot access freed memory.Branches involved
Checklist
This PR is for: