[Bug] BE SIGSEGV @0x8 in destructors across three unrelated subsystems during MoW bulk load (4.0.8)
- Dominant language
- Java
- Stars
- 15.9k
- Forks
- 3.9k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 520
Description
### Search before asking
- [x] I had searched in the [issues](https://github.com/apache/doris/issues?q=is%3Aissue) and found no similar issues.
### Version
```
doris-4.0.8-rc02
Git commit: bc8ea1bac6d
```
The `4.0.8` and `4.0.8-rc02` tags both dereference to commit `bc8ea1bac6d`, so this is the
official 4.0.8 GA build.
- JDK: OpenJDK 17.0.17
- OS: Linux x86_64, RHEL 9 family, kernel 5.14
- CPU: AMD EPYC (family 19h model 10h)
- RAM: 251 GiB physical; BE `mem_limit` = 90% = 225.99 GB
- Storage: 4 local SSD data dirs (`storage_root_path`, `medium:SSD`), ~14 TB free
- Deployment: 1 FE + 1 BE, single node, local (non-cloud) mode
- `be.conf`: stock defaults apart from ports / `priority_networks` / `storage_root_path`
### What's Wrong?
During a bulk `INSERT INTO ... SELECT` backfill into **Unique Key + Merge-on-Write** tables,
the BE crashed **three times in ~2 hours** with `SIGSEGV` at fault address `0x8`.
Every crash happens **while destroying elements of a container during cleanup**, but in
**three unrelated subsystems**:
| # | Time | Faulting frame | Subsystem | Process RSS at crash |
|---|---|---|---|---|
| A | T+0h | `ColumnReaderCache::~ColumnReaderCache` → `_List_base::_M_destroy_node` → `shared_ptr` dispose | Segment cache | 54.3 GB / 226 GB |
| B | T+1h | `LRUCache::insert` (`lru_cache.cpp:530`, inlined `delete (LRUCacheValueBase*)value`) | Storage page cache | 43.0 GB / 226 GB |
| C | T+2h | `DorisRAMFSDirectory::~DorisRAMFSDirectory` → `__CLMap::clear()` → `RAMFile::~RAMFile` | Inverted index (CLucene) | **27.4 GB** / 226 GB |
All three faulted at `@0x8` and all three are on the **tablet write / close path**
(`TabletsChannel::close`, `calc_delete_bitmap*`, `BetaRowsetWriter::build`).
Crashes A and B were mitigated by disabling the two caches involved (see *Anything Else*).
The load then progressed further and crashed at C instead — a **different subsystem** that
does not use `LRUCache` at all. This "whack-a-mole" behaviour is why I suspect a shared
underlying lifetime / heap issue on the MoW write path rather than three independent bugs.
#### Crash A — SegmentCache
```
*** SIGSEGV address not mapped to object (@0x8) received by PID (TID ) ***
0# doris::signal::(anonymous namespace)::FailureSignalHandler(int, siginfo_t*, void*) at be/src/common/signal_handler.h:420
1# PosixSignals::chained_handler(int, siginfo_t*, void*) [clone .part.0] in libjvm.so
2# JVM_handle_linux_signal in libjvm.so
3# 0x00007F... in /lib64/libc.so.6
4# std::_Sp_counted_ptr::_M_dispose() at bits/shared_ptr_base.h:427
5# std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release_last_use_cold() at bits/shared_ptr_base.h:198
6# std::__cxx11::_List_base::_M_destroy_node(...) at bits/stl_list.h:845
7# doris::segment_v2::ColumnReaderCache::~ColumnReaderCache() at be/src/olap/rowset/segment_v2/column_reader_cache.cpp:47
9# doris::segment_v2::Segment::~Segment() at be/src/olap/rowset/segment_v2/segment.cpp:237
11# std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release_last_use_cold() at bits/shared_ptr_base.h:198
12# doris::SegmentCache::CacheValue::~CacheValue() at be/src/olap/segment_loader.h:78
13# doris::LRUCache::insert(...) at be/src/olap/lru_cache.cpp:530
14# doris::SegmentCache::insert(...) at be/src/olap/segment_loader.cpp:46
15# doris::SegmentLoader::load_segment(...) at be/src/olap/segment_loader.cpp:83
16# doris::SegmentLoader::load_segments(...) at be/src/olap/segment_loader.cpp:94
17# doris::BaseTablet::lookup_row_key(...) at be/src/olap/base_tablet.cpp:507
18# doris::BaseTablet::calc_segment_delete_bitmap(...) at be/src/olap/base_tablet.cpp:712
19# doris::BaseTablet::calc_delete_bitmap(...) at be/src/olap/base_tablet.cpp:569
20# doris::CalcDeleteBitmapToken::submit_func(...) at bits/std_function.h:292
21# doris::ThreadPool::dispatch_thread() at be/src/util/threadpool.cpp:623
22# doris::Thread::supervise_thread(void*) at be/src/util/thread.cpp:461
```
#### Crash B — StoragePageCache (primary key index page)
```
*** SIGSEGV address not mapped to object (@0x8) received by PID (TID ) ***
4# doris::LRUCache::insert(...) at be/src/olap/lru_cache.cpp:530
5# doris::StoragePageCache::insert(..., doris::MemoryTrackedPageWithPageEntity*, ...) at be/src/olap/page_cache.cpp:122
6# doris::segment_v2::PageIO::read_and_decompress_page_(...) at be/src/olap/rowset/segment_v2/page_io.cpp:264
7# doris::segment_v2::PageIO::do_read_and_decompress_page(...) at be/src/olap/rowset/segment_v2/page_io.h:162
8# doris::segment_v2::PageIO::read_and_decompress_page(...) at be/src/olap/rowset/segment_v2/page_io.cpp:276
9# doris::segment_v2::IndexedColumnReader::read_page(...) const at be/src/olap/rowset/segment_v2/indexed_column_reader.cpp:144
10# doris::segment_v2::IndexedColumnIterator::_read_data_page(...) at be/src/olap/rowset/segment_v2/indexed_column_reader.cpp:165
11# doris::segment_v2::IndexedColumnIterator::next_batch(...) at be/src/olap/rowset/segment_v2/indexed_column_reader.cpp:304
12# doris::MergeIndexDeleteBitmapCalculatorContext::_next_batch(unsigned int) at be/src/olap/delete_bitmap_calculator.cpp:81
13# doris::MergeIndexDeleteBitmapCalculatorContext::seek_at_or_after(doris::Slice const&) at be/src/olap/delete_bitmap_calculator.cpp:71
14# doris::MergeIndexDeleteBitmapCalculator::calculate_one(doris::RowLocation&) at be/src/olap/delete_bitmap_calculator.cpp:208
15# doris::MergeIndexDeleteBitmapCalculator::calculate_all(std::shared_ptr) at be/src/olap/delete_bitmap_calculator.cpp:218
16# doris::BaseTablet::calc_delete_bitmap_between_segments(...) at be/src/olap/base_tablet.cpp:384
17# doris::BaseRowsetBuilder::submit_calc_delete_bitmap_task() in doris_be
18# doris::BaseDeltaWriter::submit_calc_delete_bitmap_task() at be/src/olap/delta_writer.cpp:196
19# doris::TabletsChannel::close(...) at be/src/runtime/tablets_channel.cpp:350
20# doris::LoadChannel::_handle_eos(...) at be/src/runtime/load_channel.cpp:221
22# doris::LoadChannelMgr::add_batch(...) at be/src/runtime/load_channel_mgr.cpp:179
24# doris::WorkThreadPool::work_thread(int) at be/src/util/work_thread_pool.hpp:159
```
#### Crash C — inverted index RAM directory (with both caches already disabled)
```
*** SIGSEGV address not mapped to object (@0x8) received by PID (TID ) ***
4# lucene::store::RAMFile::~RAMFile() at contrib/clucene/src/core/CLucene/store/RAMDirectory.cpp:50
5# lucene::util::__CLMap::clear() at contrib/clucene/src/core/CLucene/util/VoidMap.h:127
6# doris::segment_v2::DorisRAMFSDirectory::~DorisRAMFSDirectory() at be/src/olap/rowset/segment_v2/inverted_index_fs_directory.cpp:740
8# std::_Rb_tree, ..., std::shared_ptr>::_M_erase(...) at bits/stl_tree.h:2590
10# doris::segment_v2::IndexFileWriter::finish_close() at be/src/olap/rowset/segment_v2/index_file_writer.cpp:253
11# doris::InvertedIndexFileCollection::finish_close() at be/src/olap/rowset/beta_rowset_writer.cpp:282
12# doris::SegmentFlusher::close() at be/src/olap/rowset/segment_creator.cpp:120
13# doris::SegmentCreator::close() at be/src/olap/rowset/segment_creator.cpp:472
14# doris::BaseBetaRowsetWriter::_close_file_writers() at be/src/olap/rowset/beta_rowset_writer.cpp:923
16# doris::BetaRowsetWriter::build(std::shared_ptr&) at be/src/olap/rowset/beta_rowset_writer.cpp:972
17# doris::BaseRowsetBuilder::build_rowset() at be/src/olap/rowset_builder.cpp:248
18# doris::BaseDeltaWriter::build_rowset() at be/src/olap/delta_writer.cpp:186
20# doris::TabletsChannel::close(...) at be/src/runtime/tablets_channel.cpp:344
21# doris::LoadChannel::_handle_eos(...) at be/src/runtime/load_channel.cpp:221
23# doris::LoadChannelMgr::add_batch(...) at be/src/runtime/load_channel_mgr.cpp:179
25# doris::WorkThreadPool::work_thread(int) at be/src/util/work_thread_pool.hpp:159
```
`index_file_writer.cpp:253` is `_indices_dirs.clear();` — destroying the
`std::map<..., std::shared_ptr>` of per-index RAM directories.
#### Notes on crash B
`LRUCache::insert()` unlinks evicted entries under `_mutex`, then frees them outside it:
```cpp
// be/src/olap/lru_cache.cpp (4.0.8, lines 526-532)
// we free the entries here outside of mutex for
// performance reasons
while (to_remove_head != nullptr) {
LRUHandle* next = to_remove_head->next;
to_remove_head->free(); // <-- faulting line
to_remove_head = next;
}
```
`LRUHandle::free()` is inline in `lru_cache.h` and explicitly null-checks `value`
(`if (value != nullptr) delete (LRUCacheValueBase*)value;`), so a plain null `value` does
not explain the fault. In crash A the fault lands two frames deeper, in a `shared_ptr`
control-block release — a `shared_ptr`'s control-block pointer sits at offset `0x8`, which
matches the fault address.
This `insert()` free-outside-mutex block is byte-identical in `branch-3.0` (line 453),
`branch-3.1` (line 518) and `branch-4.0` (line 530).
#### Ruled out
| Hypothesis | Evidence against |
|---|---|
| OOM / memory limit | RSS at the three crashes: 54.3 / 43.0 / **27.4** GB against a 226 GB `mem_limit`; 170 GB system memory free. Highest query `PeakMemoryBytes` was 12.7 GB. |
| Linux OOM killer | No kernel records; the process caught SIGSEGV in its own handler and unwound over several seconds. |
| ECC / memory hardware | `amd64_edac` (F19h_M10h) loaded and monitoring; **zero** MCE/EDAC events in `dmesg` and `journalctl -k` across the whole uptime. |
| Disk / I/O error | Zero `corrupt` / `checksum` / `Input/output error` in BE logs; zero SCSI/HBA errors in the kernel log. |
| Query concurrency | Statements ran strictly **serially** — verified in `fe.audit.log`, each starts only after the previous finishes. |
| Cache capacity auto-adjust thread | Process memory stayed at ~12-24% of `mem_limit`, far below the default `cache_capacity_reduce_mem_limit_frac` (0.6). |
| Version accumulation / compaction backlog | Zero `too many versions` / `-235`; delete-bitmap batches ~416 K rows with `conflict rows: 0`. |
| Non-GA build | `4.0.8` and `4.0.8-rc02` both point to `bc8ea1bac6d`. |
### What You Expected?
The BE should not `SIGSEGV`. Under heavy load it should degrade gracefully — cancel the
query, return `MEM_LIMIT_EXCEEDED`, reject the write — not take the whole process down,
especially while using only ~12% of its configured memory limit.
### How to Reproduce?
Reproduced three times in about two hours on the same node.
**Target schema** (52 tables of this shape; names/columns redacted):
```sql
CREATE TABLE db1.tbl_mow (
id VARCHAR(...), -- key
... 7 to 42 value columns ...,
seq_id BIGINT,
INDEX idx_a (`some_col`) USING INVERTED,
INDEX idx_b (`text_col_1`) USING INVERTED
PROPERTIES("lower_case"="true","parser"="chinese","parser_mode"="fine_grained","support_phrase"="true"),
INDEX idx_c (`text_col_2`) USING INVERTED
PROPERTIES("lower_case"="true","parser"="chinese","parser_mode"="fine_grained","support_phrase"="true")
)
ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 48
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"enable_unique_key_merge_on_write" = "true",
"function_column.sequence_col" = "seq_id",
"compression" = "LZ4",
"storage_format" = "V2",
"inverted_index_storage_format" = "V1"
);
```
Source tables are append-style tables with the same column set.
**Workload:**
```sql
-- one statement at a time, strictly serial, no concurrency
INSERT INTO db1.tbl_mow (col1, ..., colN, seq_id)
SELECT col1, ..., colN, seq_id
FROM db1.tbl_source;
```
- 52 statements, ~3.27 billion source rows (~414 GB) in total.
- Targets start **empty**; each statement is one transaction.
- Single BE, so all tablet writes, delete-bitmap computation, inverted-index building and
compaction land on one process.
- Largest single statements: 256 M and 260 M rows, each with two fine-grained Chinese
inverted indexes over long text columns.
The common condition is a sustained, high-throughput MoW load that keeps the segment
cache, the page cache and the inverted-index writers all busy for hours.
### Anything Else?
**Possibly the same root cause as #64826** (`[Bug] BE SIGSEGV crash in
bthread::TaskGroup::sched_to and Page Cache on 4.0.5-rc01`, still open). That report also
shows `SIGSEGV @0x8` while an `LRUCache` frees evicted entries, reached from
`Daemon::cache_adjust_capacity_thread` → `CacheManager::for_each_cache_refresh_capacity`
→ `LRUCache::set_capacity`. `set_capacity()` frees evicted handles the same way `insert()`
does.
**Core dumps unusable.** `systemd-coredump` truncated them (BE RSS 27-54 GB vs default
`ProcessSizeMax=2G`), so only the BE's own symbolized handler output is available — that
is what is quoted above. Happy to reconfigure and capture a full core if that would help.
**Mitigations found, each blocking one manifestation** (source-level; each `if` below is
the gate that stops the crashing call from being reached):
```
disable_segment_cache = true # segment_loader.cpp:74 -> blocks crash A
disable_pk_storage_page_cache = true # primary_key_index.cpp:103 -> page_io.cpp:260 -> blocks crash B
disable_storage_page_cache = true # base_tablet.cpp:104, segment.cpp:992
inverted_index_ram_dir_enable = false # inverted_index_fs_directory.cpp:927 -> blocks crash C
```
```cpp
// be/src/olap/segment_loader.cpp:74
if (use_cache && !config::disable_segment_cache) { _segment_cache->insert(...); } // crash A
else { cache_handle->push_segment(std::move(segment)); }
// be/src/olap/rowset/segment_v2/page_io.cpp:260
if (opts.use_page_cache && cache) { cache->insert(...); } // crash B
else { *handle = PageHandle(page.get()); }
// be/src/olap/rowset/segment_v2/inverted_index_fs_directory.cpp:927
if (config::inverted_index_ram_dir_enable && can_use_ram_dir) { /* DorisRAMFSDirectory */ } // crash C
```
Note `disable_pk_storage_page_cache` is `DEFINE_Bool` (needs a BE restart) while the other
three are `DEFINE_mBool`.
After disabling the two caches, the load got substantially further (43 tables in 13
minutes, versus 2 tables in 105 minutes before), then hit crash C. **Each config removes
one entry point; it does not appear to fix the underlying problem.**
I still have the environment available and can gather more detail (full unredacted stacks,
`SHOW DATA` output, audit-log timings, a full core dump) on request.
### Are you willing to submit PR?
- [ ] Yes I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct)
Contributor guide
Research direction
Start with the three reported paths: be/src/olap/lru_cache.cpp, be/src/olap/rowset/segment_v2/column_reader_cache.cpp, and be/src/olap/rowset/segment_v2/index_file_writer.cpp, then trace the TabletsChannel::close and BetaRowsetWriter::build callers. Reproduce the serial MoW bulk load with inverted indexes while collecting the listed crash frames. Done means the shared lifetime failure is isolated and the BE no longer crashes under the reduced reproducer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, sql
- Domain
- backend, databases, search
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100