mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-30 06:30:57 -05:00
* feat: `io_uring` setting to control which components use the io_uring backend A few components have both an mmap and an io_uring variant reading the very same files: the immutable dense vector storages, the single-file TurboQuant storage, and the mmap payload storage. Until now the choice was a side effect of `async_scorer` — a vector-search knob — plus, for the payload storage, a feature flag that was parked off because io_uring is ~2x slower than mmap when the data fits the page cache (#9310, #9409). Add `storage.performance.io_uring`, optional, with two modes: - unset (default): unchanged behaviour. The vector storages keep following `async_scorer`; the payload storage stays on mmap. - `disabled`: no component uses io_uring. - `auto`: a component uses io_uring when its memory placement is `cold` (data is left on disk, so reads hit the disk and there is something to gain), its feature flag allows it, and the kernel supports io_uring. Components meant to sit in RAM keep using mmap. The decision lives in one place, `segment::common::io_uring::use_io_uring`, so the openers no longer each reach for the async-scorer global. Kernel support is now probed up front through `is_io_uring_supported()` instead of opening a file and falling back on error. `async_payload_storage` now defaults to on: it no longer decides anything by itself, it only lifts the ban, and the payload storage no longer follows `async_scorer` at all — so turning it on cannot silently move an existing `async_scorer: true` deployment onto the slower path. Which backend a component ended up on depends on the config, the placement and the kernel at once, so report it in `SegmentInfo`: `vector_data[name].io_backend` and `payload_storage_io_backend`, both `"mmap" | "io_uring"`, absent for components that have no such choice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Trim comments, drop trivial tests Two tests were only restating their own implementation: `test_mode_round_trip` round-tripped the encode/decode pair next to it, and `test_io_uring_config` checked that serde deserializes a two-variant enum. The mode matrix test stays, it is the one that pins the semantics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Flatten `IoBackend` in OpenAPI, derive `JsonSchema` for `IoUringMode` Per-variant doc comments on a plain string enum make schemars emit a `oneOf` of anonymous single-value objects instead of a flat `enum`. Move the variant descriptions into the enum doc, as `Memory` and friends already do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Update lib/segment/src/vector_storage/turbo/turbo_vector_storage.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * upd openapi schema * Update lib/common/common/src/flags.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Require kernel io_uring support in the async-scorer fallback `use_io_uring` returned `get_async_scorer()` verbatim when the `io_uring` setting is unset, so an enabled async scorer on a kernel without io_uring opened the io_uring storage, failed, and fell back to mmap with an error log per segment. Gate that branch on `is_io_uring_supported()` too, like `Auto` already is, so the component just stays on mmap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * upd openapi schema --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com>
184 lines
6.6 KiB
Rust
184 lines
6.6 KiB
Rust
// Deprecated storage placement params (`on_disk`, `always_ram`, `on_disk_payload`) are still
|
|
// handled here for backward compatibility with the new `memory` parameter
|
|
#![allow(deprecated)]
|
|
|
|
use std::num::NonZeroUsize;
|
|
use std::sync::Arc;
|
|
|
|
use collection::operations::vector_params_builder::VectorParamsBuilder;
|
|
use collection::operations::verification::new_unchecked_verification_pass;
|
|
use collection::optimizers_builder::OptimizersConfig;
|
|
use collection::shards::channel_service::ChannelService;
|
|
use common::budget::ResourceBudget;
|
|
use common::load_concurrency::LoadConcurrencyConfig;
|
|
use common::mmap;
|
|
use segment::types::Distance;
|
|
use storage::content_manager::collection_meta_ops::{
|
|
ChangeAliasesOperation, CollectionMetaOperations, CreateAlias, CreateCollection,
|
|
CreateCollectionOperation, DeleteAlias, RenameAlias,
|
|
};
|
|
use storage::content_manager::consensus::operation_sender::OperationSender;
|
|
use storage::content_manager::toc::TableOfContent;
|
|
use storage::dispatcher::Dispatcher;
|
|
use storage::rbac::{Access, AccessRequirements, Auth};
|
|
use storage::types::{PerformanceConfig, StorageConfig};
|
|
use tempfile::Builder;
|
|
|
|
const FULL_ACCESS: Auth = Auth::new_internal(Access::full("For test"));
|
|
|
|
#[test]
|
|
fn test_alias_operation() {
|
|
let storage_dir = Builder::new().prefix("storage").tempdir().unwrap();
|
|
|
|
let config = StorageConfig {
|
|
storage_path: storage_dir.path().to_path_buf(),
|
|
snapshots_path: storage_dir.path().join("snapshots"),
|
|
snapshots_config: Default::default(),
|
|
temp_path: None,
|
|
on_disk_payload: false,
|
|
payload: None,
|
|
optimizers: OptimizersConfig {
|
|
deleted_threshold: 0.5,
|
|
vacuum_min_vector_number: 100,
|
|
default_segment_number: 2,
|
|
max_segment_size: None,
|
|
#[expect(deprecated)]
|
|
memmap_threshold: Some(100),
|
|
indexing_threshold: Some(100),
|
|
flush_interval_sec: 2,
|
|
max_optimization_threads: Some(2),
|
|
prevent_unoptimized: None,
|
|
},
|
|
optimizers_overwrite: None,
|
|
wal: Default::default(),
|
|
performance: PerformanceConfig {
|
|
max_search_threads: 1,
|
|
max_optimization_runtime_threads: 1,
|
|
optimizer_cpu_budget: 0,
|
|
optimizer_io_budget: 0,
|
|
update_rate_limit: None,
|
|
search_timeout_sec: None,
|
|
incoming_shard_transfers_limit: Some(1),
|
|
outgoing_shard_transfers_limit: Some(1),
|
|
async_scorer: None,
|
|
io_uring: None,
|
|
load_concurrency: LoadConcurrencyConfig::default(),
|
|
},
|
|
hnsw_index: Default::default(),
|
|
hnsw_global_config: Default::default(),
|
|
mmap_advice: mmap::Advice::Random,
|
|
low_memory_mode: Default::default(),
|
|
node_type: Default::default(),
|
|
update_queue_size: Default::default(),
|
|
handle_collection_load_errors: false,
|
|
recovery_mode: None,
|
|
update_concurrency: Some(NonZeroUsize::new(2).unwrap()),
|
|
// update_concurrency: None,
|
|
shard_transfer_method: None,
|
|
collection: None,
|
|
max_collections: None,
|
|
};
|
|
|
|
let (propose_sender, _propose_receiver) = std::sync::mpsc::channel();
|
|
let propose_operation_sender = OperationSender::new(propose_sender);
|
|
|
|
let toc = Arc::new(
|
|
TableOfContent::new(
|
|
&config,
|
|
ResourceBudget::default(),
|
|
ChannelService::new(6333, false, None, None),
|
|
0,
|
|
Some(propose_operation_sender),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
let handle = toc.general_runtime_handle().clone();
|
|
let dispatcher = Dispatcher::new(toc);
|
|
|
|
handle
|
|
.block_on(
|
|
dispatcher.submit_collection_meta_op(
|
|
CollectionMetaOperations::CreateCollection(
|
|
CreateCollectionOperation::new(
|
|
"test".to_string(),
|
|
CreateCollection {
|
|
vectors: VectorParamsBuilder::new(10, Distance::Cosine)
|
|
.build()
|
|
.into(),
|
|
sparse_vectors: None,
|
|
hnsw_config: None,
|
|
wal_config: None,
|
|
optimizers_config: None,
|
|
shard_number: Some(1),
|
|
on_disk_payload: None,
|
|
payload: None,
|
|
replication_factor: None,
|
|
write_consistency_factor: None,
|
|
quantization_config: None,
|
|
sharding_method: None,
|
|
strict_mode_config: None,
|
|
uuid: None,
|
|
metadata: None,
|
|
},
|
|
)
|
|
.unwrap(),
|
|
),
|
|
FULL_ACCESS,
|
|
None,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
handle
|
|
.block_on(dispatcher.submit_collection_meta_op(
|
|
CollectionMetaOperations::ChangeAliases(ChangeAliasesOperation {
|
|
actions: vec![CreateAlias {
|
|
collection_name: "test".to_string(),
|
|
alias_name: "test_alias".to_string(),
|
|
}
|
|
.into()],
|
|
}),
|
|
FULL_ACCESS,
|
|
None,
|
|
))
|
|
.unwrap();
|
|
|
|
handle
|
|
.block_on(dispatcher.submit_collection_meta_op(
|
|
CollectionMetaOperations::ChangeAliases(ChangeAliasesOperation {
|
|
actions: vec![
|
|
CreateAlias {
|
|
collection_name: "test".to_string(),
|
|
alias_name: "test_alias2".to_string(),
|
|
}
|
|
.into(),
|
|
DeleteAlias {
|
|
alias_name: "test_alias".to_string(),
|
|
}
|
|
.into(),
|
|
RenameAlias {
|
|
old_alias_name: "test_alias2".to_string(),
|
|
new_alias_name: "test_alias3".to_string(),
|
|
}
|
|
.into(),
|
|
],
|
|
}),
|
|
FULL_ACCESS,
|
|
None,
|
|
))
|
|
.unwrap();
|
|
|
|
// Nothing to verify here.
|
|
let pass = new_unchecked_verification_pass();
|
|
|
|
let _ = handle
|
|
.block_on(
|
|
dispatcher.toc(&FULL_ACCESS, &pass).get_collection(
|
|
&FULL_ACCESS
|
|
.check_collection_access("test_alias3", AccessRequirements::new(), "test")
|
|
.unwrap(),
|
|
),
|
|
)
|
|
.unwrap();
|
|
}
|