Construire un noyau OLTP : Contrats API, taille de page et réglage adaptatif
Les développeurs d'une nouvelle base de données OLTP ont mis en place une architecture stricte avec des couches isolées utilisant des interfaces de traits en Rust. Chaque couche dispose de contrats API clairs, la taille de page est configurable par tablespace, et la configuration est gérée par un Resource Broker sans réglage manuel. Les diagnostics s'intègrent via USDT et eBPF.
Taille de page et paramètres des tablespaces
Les tailles de pages de données varient de 4-8 Ko pour un OLTP pur à 16-32 Ko pour du HTAP sur NVMe. Chaque tablespace est un fichier distinct avec une taille de page fixe après création. Par défaut, c'est 16 Ko. L'identifiant de page est composite : [tablespace_id:16][page_index:48]. Le BufferPool redirige les requêtes vers des sous-caches par taille.
Le superbloc au début du fichier stocke les métadonnées du tablespace indépendamment de la taille de page. Au montage, il vérifie la configuration ; les incohérences empêchent le démarrage sans dégradation.
Couches architecturales et contrats API
Le noyau est divisé en quatre couches :
- Couche Adaptateur (async tokio) : termine TLS, parse le protocole filaire.
#[async_trait]
pub trait NetworkAdapter {
async fn handle_connection(&self, stream: Box<dyn ConnectionStream>) -> Result<(), NetworkError>;
}
- Couche Compat : parse SQL en AST, émule pg_catalog, traduit en LogicalPlan, rejette les fonctionnalités non prises en charge (erreur 0A000).
pub trait CompatLayer {
fn translate_query(&self, ast: SqlAst) -> Result<LogicalPlan, CompatError>;
}
- Moteur Principal (sync) : optimiseur, exécuteur, gestionnaire de transactions. S'exécute dans un pool spawn_blocking.
pub trait ExecutionEngine {
fn execute_plan(&self, plan: LogicalPlan, session: &Session) -> Result<ResultSet, ExecutionError>;
}
- Gestionnaire de Stockage : façade sur PageProvider et TransactionLogSink. Le noyau demande des pages et des enregistrements UNDO.
pub trait StorageManager {
fn pin_page(&self, page_id: PageId, mode: LockMode) -> Result<PageGuard, StorageError>;
fn append_undo(&self, txn_id: TxnId, record: UndoRecord) -> Result<UndoPtr, StorageError>;
}
Les frontières entre couches empêchent les fuites de détails : les métriques de verrouillage excluent les attentes asynchrones.
Réglage adaptatif et Resource Broker
Environ 60 paramètres sont répartis en budgets, garde-fous et overrides. Les opérateurs définissent des limites de haut niveau :
[resources]
memory_budget = "16GB"
cpu_budget = "auto"
io_iops = 5000
Les conseillers (MemoryAdvisor, IoAdvisor, CpuAdvisor) redistribuent les ressources chaque seconde :
- CpuAdvisor : DOP et pool de threads vs throttling.
- MemoryAdvisor : BufferPool vs work_mem sous charge.
- IoAdvisor : priorité des transactions, détection de rafales dans le cloud.
Protections
- Hystérésis : fenêtre de 5 s, pas de 5 % pour éviter les oscillations.
- Planchers durs : min. 128 Mo pour BufferPool/UNDO.
- Transition gracieuse : nouvelles limites pour les nouvelles allocations.
- Overrides experts qui priment sur l'autoréglage.
Purge du journal UNDO en arrière-plan au lieu de VACUUM. Changements à l'exécution sans redémarrage.
Diagnostics : Sondes USDT et eBPF
Les mécanismes de diagnostic sont intégrés dans le binaire. Sondes USDT pour l'analyse à l'exécution, eBPF pour le traçage sans surcharge.
Points clés :
- Taille de page par tablespace avec vérifications au démarrage.
- Contrats de traits stricts entre couches pour éviter les fuites.
- Resource Broker : budgets → autoréglage avec garde-fous.
- Diagnostics via eBPF/USDT dans le noyau.
- Oubliez le réglage manuel au profit de mécanismes adaptatifs.
— Editorial Team
Aucun commentaire pour le moment.