Repository navigation
[Bug] BE ls_normal thread_pool_queue_size grows monotonically and saturates at capacity — phantom queue count in the time-sharing scan pool (4.1.2) #68771
Description
Search before asking
- I had searched in the issues and found no similar issues.
Version
4.1.2
What's Wrong?
The Prometheus metric doris_be_thread_pool_queue_size{thread_pool_name="ls_normal", workload_group="normal"} grows monotonically on 2 of 5 BEs (172.31.20.48, 172.31.17.165), starting around 2026-09-17, until it saturates near 102400 (which equals doris_scanner_thread_pool_queue_size) around 2026-10-05, then drops vertically.
The vertical drop is not the queue draining — it coincides exactly with a BE restart:
| BE | LastStartTime (SHOW BACKENDS) |
Growing? |
|---|---|---|
| 172.31.17.66 | 2026-06-24 14:52:14 | No (flat) |
| 172.31.25.179 | 2026-06-24 14:50:20 | No (flat) |
| 172.31.19.197 | 2026-06-24 14:50:48 | No (flat) |
| 172.31.20.48 | 2026-10-05 14:17:36 | Yes |
| 172.31.17.165 | 2026-10-05 14:16:37 | Yes |
The two affected BEs also have ~1/33 of the cumulative task_execution_count_total of the other three, consistent with a recent restart.
Key paradox — the queue is a phantom. At the time of sampling, the affected pools report:
doris_be_thread_pool_active_threads{thread_pool_name="ls_normal"} = 0 (max_threads = 48)
doris_be_thread_pool_submit_failed{thread_pool_name="ls_normal"} = 0
doris_be_thread_pool_queue_size{thread_pool_name="ls_normal"} = 14905 (!!)
Idle workers (active = 0) together with a non-empty queue of ~15,000 entries is impossible for a correct implementation — the dispatcher would drain any real pending split immediately. This indicates the gauge is over-counted (phantom queue depth), while the real queue is empty.
It is still growing right now (two samples, 4 minutes apart):
| BE | queue_size |
task_execution_count_total |
active_threads |
|---|---|---|---|
| 172.31.20.48 | 14893 → 14905 (+12) | 102,235,465 → 102,275,040 (+39,575) | 1 → 0 |
| 172.31.17.165 | 14750 → 14764 (+14) | 106,108,764 → 106,152,122 (+43,358) | 0 → 0 |
| 172.31.17.66 (healthy) | 19030 → 19030 (+0) | 3,361,977,363 → 3,362,017,498 | 0 → 0 |
Rate ≈ 3–3.5 / minute ≈ 4,700 / day. From the 2026-10-05 restart to the 2026-10-08 sample (2.9 days) this accumulates to ~14,900 — matching the observed value exactly. Extrapolating, the pool will hit the 102400 capacity again around 2026-10-25, at which point submit_failed should become non-zero.
Note that a healthy BE processing the same volume of tasks (≈40,000 in 4 minutes) keeps queue_size perfectly flat — so this is a state-dependent accounting leak, not a throughput/backlog problem.
config:
sys_log_roll_num = 7
streaming_load_json_max_mb = 2048
string_type_length_soft_limit_bytes = 1073741824
max_cumu_compaction_threads = 24
max_base_compaction_threads = 8
compaction_task_num_per_disk = 32
compaction_task_num_per_fast_disk = 32
cumulative_compaction_min_deltas = 5
base_compaction_interval_seconds_since_last_operation = 43200
base_compaction_min_data_ratio = 0.2
enable_segcompaction = true
enable_vertical_compaction = true
enable_compaction_priority_scheduling = false
write_buffer_size = 1073741824
pipeline_executor_size = 12
blocking_pipeline_executor_size = 8
publish_version_worker_count = 16
upload_worker_count = 16
number_tablet_writer_threads = 32
push_worker_count_high_priority = 6
push_worker_count_normal_priority = 6
send_batch_thread_pool_thread_num = 96
flush_thread_num_per_store = 8
num_s3_file_upload_thread_pool_max_thread = 64
num_s3_file_upload_thread_pool_min_thread = 32
max_s3_file_system_thread_num = 128
min_s3_file_system_thread_num = 32
remote_storage_read_buffer_mb = 128
doris_remote_scanner_thread_pool_thread_num = 128
s3_write_buffer_size = 10485760
s3_file_system_local_upload_buffer_size = 10485760
disable_storage_row_cache = false
row_cache_mem_limit = 10%
storage_page_cache_limit = 15%
max_tablet_version_num = 4000
enable_packed_file = false
calc_delete_bitmap_worker_count = 16
calc_tablet_delete_bitmap_task_max_thread = 64
delete_bitmap_agg_cache_capacity = 1073741824
calc_delete_bitmap_max_thread = 64
calc_delete_bitmap_for_load_max_thread = 16
tablet_publish_txn_max_thread = 64
What You Expected?
doris_be_thread_pool_queue_sizeforls_normalshould fall back to ~0 when the pool is idle (active_threads = 0), and must never grow without bound.- The counter should stay consistent across token lifecycle events (token shutdown / task removal) — every entry removed from the queue must decrement
_total_queued_tasks. submit_failedshould not begin incrementing merely because of a leaked/phantom queue count.
How to Reproduce?
No response
Anything Else?
No response
Are you willing to submit PR?
- Yes I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
Activity
Doris-Breakwater commentedon Oct 8, 2026
Breakwater-GitHub-Analysis-Slot: slot_841d0d77defc
Initial assessment: This strongly matches a known
TimeSharingTaskExecutorqueue-accounting defect. The defect is present in the upstream 4.1.2 source; the matching fix was merged as PR #63568, with the 4.1 backport in PR #64855. I verified that backport commit1f197f6c931217f546463371865c4d756b1a6058is absent from the 4.1.2 ancestry and present in 4.1.3. Confidence is high for the source-level defect and its match to these symptoms; confirming the exact running BE builds is still necessary.Verified code mechanism: I inspected upstream tag
4.1.2(commitaec169d20256a788de448f46737b5fb53ee3b4e3) without changing the checkout.enable_task_executor_in_internal_table=true(the default),WorkloadGroupcreatesTaskExecutorSimplifiedScanSchedulerforls_<workload_group>, usingdoris_scanner_thread_pool_queue_sizeas its queue limit. See workload_group.cpp.get_queue_size(), which returns_total_queued_tasks, rather than the actual split-queue size._do_submit()increments that count when offering a split; a worker decrements it when taking the split. See executor metric/submission code and queue-size getter.ScannerContext::stop_scanners()and its destructor callremove_task(). That function closes the task handle and physically removes its queued splits withremove_all(), without decrementing_total_queued_tasks. Workers decide whether to sleep from the real split-queue size, so they can be idle while the reported count retains removed entries. See scanner cleanup, remove_task, and worker dispatch.A minimal sequence is therefore: enqueue a split (+1), remove its task before a worker takes it (real queue -1, accounting unchanged), repeat. This explains accumulated phantom entries and why continuing to execute unrelated scan tasks does not drain the leaked offset. It does not require token shutdown itself to omit a decrement.
This can affect query execution, not just the dashboard. The capacity check uses:
capacity_remaining = max_threads - active_threads + max_queue_size - _total_queued_tasksIt rejects submissions and increments
submit_failedwhen this is below 1. Thus the exact boundary depends on active workers; with 48 maximum workers and zero active workers, it is 102448, rather than exactly 102400. The projected exhaustion date remains an estimate.What remains unverified: The supplied samples support this mechanism but do not identify which queries removed queued splits. Equal execution rates do not imply equal cancellation/cleanup histories. The supposedly healthy BE's flat, nonzero count of 19030 could also be an older leaked offset. Also, active-thread and queue gauges are read under separate lock acquisitions, so one
active=0sample alone is not conclusive; sustained observations and source evidence are stronger.Please provide:
enable_task_executor_in_internal_table, scanner queue limit, and workload-group scan-thread settings./metricscaptures retaining all labels, including poolidand BE identity, plus the dashboard PromQL. This will exclude aggregation across different pool instances.TooManyTasks/Thread pool ... is at capacityerrors and workload/configuration changes near the reported onset. These events are candidates, not established triggers here.Recommended next steps: Use a tested maintenance build containing the 4.1 backport; upstream 4.1.3 contains this specific fix, but verify the packaged BE hash. The patch subtracts only entries actually removed from the executor queue, centralizes offer accounting, and handles the idle transition/notification. Subtracting the entire task's split list would be incorrect because it can include running or otherwise nonqueued splits.
For validation, use the existing
TimeSharingTaskExecutorTest.test_remove_task_clears_queued_task_countfrom the backport, then exercise scan cancellation/cleanup in staging and check that an idle pool returns to zero and accepts subsequent submissions. I inspected that test; I did not build or run the BE suite or reproduce this against your cluster. A BE restart recreates the counter and can temporarily restore headroom, but does not remove the defect from an unpatched binary. Increasing the queue limit only postpones rejection.If the deployed build already includes the fix and still exhibits growth, keep this issue open for further investigation: obtain a maintainer-assisted snapshot of the actual split-queue size and
_total_queued_tasksunder the same executor lock, plus worker thread stacks. That comparison distinguishes an accounting mismatch from a real queue that is failing to dispatch.15767714253 commentedon Oct 10, 2026
Key paradox — the queue is a phantom and it grows while the pool is idle.
Sampling / 采样(primary evidence)
Consecutive scrapes of
/metricson172.31.17.165(ls_normal,workload_group=normal) — 10 samples, 10 s apart, 2026-10-10 14:16:29 → 14:17:59 (GMT+8):queue_sizeactive_threadstask_execution_count_totalsubmit_failedDerived:
queue_size: +5 / 90 s ⇒ ≈ 3.3 / min ⇒ ≈ 4,800 / day.task_execution_count_total: +36,972 / 90 s ⇒ ≈ 411 splits/s actually consumed by the pool.max_threads = 48;active_threads ∈ {0, 1}for the whole window;submit_failed = 0throughout.Why this proves the counter is wrong (independent of the "two gauges are read under separate locks" caveat):
The pool reports it is consuming ≈411 splits/s. If a 25,610-entry real queue were feeding those ≤48 workers, even a single active worker would drain it in far under a minute; with ≥47 workers idle at every sample, a real queue of 25,610 would be emptied in well under 65 s. Yet across the entire 90 s window the value does not fall by a single unit — it only rises. A real backlog cannot simultaneously be huge, almost-idle-drained (411/s), and monotonically growing. ⇒ The reported depth cannot correspond to real queued splits; the gauge is over-counted (phantom queue depth).
This argument does not rely on the atomicity of reading
active_threadsandqueue_sizeseparately — it only relies on the fact that a real queue of that size cannot survive an almost-idle worker set that is draining the pool at ~411 splits/s.Earlier corroborating sample / 早期佐证采样(2026-10-08,间隔 4 分钟两次读数)
queue_sizetask_execution_count_totalactive_threadsRate ≈ 3–3.5 / min ≈ 4,700–5,100 / day. From the 2026-10-05 14:16 restart to the 2026-10-10 14:16 sample (5.00 days) this predicts ~25,600 — matching the observed 25,610 almost exactly. Extrapolating, the pool will hit the 102400 capacity again around 2026-10-25, at which point
submit_failedshould become non-zero.Note that a healthy BE processes the same volume of tasks (≈40,000 in 4 minutes) while its
queue_sizestays perfectly flat at the same ~19,000 floor — so this is a state-dependent accounting leak, not a throughput/backlog problem.yiguolei commentedon Oct 10, 2026
You mean the queue size is large, but active thread number is very low?
15767714253 commentedon Oct 15, 2026
YES