improve postgresql performance

This commit is contained in:
DioCrafts
2025-04-09 00:21:20 +02:00
parent 7473ee438f
commit 8f1d213526
36 changed files with 2191 additions and 233 deletions
+145
View File
@@ -0,0 +1,145 @@
# Sistema de Migraciones de Base de Datos
Este documento describe el sistema de migraciones de base de datos implementado en OxiCloud para gestionar cambios de esquema de forma controlada y segura.
## Descripción General
OxiCloud utiliza un sistema de migraciones basado en archivos SQL versionados para garantizar que los cambios en la estructura de la base de datos sean:
- Versionados y rastreables
- Aplicados de forma consistente en todos los entornos
- Reproducibles y comprobables
- Independientes del código de la aplicación
## Estructura de Directorios
```
OxiCloud/
├── migrations/ # Directorio principal de migraciones
│ ├── 20250408000000_initial_schema.sql # Migración 1: Esquema inicial
│ ├── 20250408000001_default_users.sql # Migración 2: Usuarios por defecto
│ └── ... # Futuras migraciones
├── src/
├── bin/
│ └── migrate.rs # Herramienta CLI para ejecutar migraciones
```
## Convenciones de Nomenclatura
Las migraciones siguen el formato: `YYYYMMDDHHMMSS_descripción_breve.sql`, donde:
- `YYYYMMDDHHMMSS`: Timestamp que garantiza el orden correcto (año, mes, día, hora, minuto, segundo)
- `descripción_breve`: Descripción concisa del propósito de la migración
- `.sql`: Extensión de archivo SQL
## Ejecución de Migraciones
Las migraciones se ejecutan mediante una herramienta CLI dedicada:
```bash
cargo run --bin migrate --features migrations
```
Este comando:
1. Conecta con la base de datos configurada en el entorno
2. Busca migraciones en el directorio `/migrations/`
3. Compara las migraciones aplicadas con las disponibles
4. Ejecuta secuencialmente las migraciones pendientes
5. Registra las migraciones aplicadas en una tabla de control
## Creación de Nuevas Migraciones
Para crear una nueva migración:
1. Crea un nuevo archivo en el directorio `migrations/` siguiendo la convención de nomenclatura
2. Define los cambios SQL en el archivo
3. Asegúrate de que los cambios sean compatibles con la versión actual del esquema
4. Ejecuta las migraciones con el comando correspondiente
Ejemplo de estructura para una nueva migración:
```sql
-- Migración: Añadir tabla de etiquetas
-- Descripción: Crea la tabla para almacenar etiquetas de archivos y sus relaciones
-- Crear tabla de etiquetas
CREATE TABLE IF NOT EXISTS auth.tags (
id SERIAL PRIMARY KEY,
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#3498db',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, name)
);
-- Crear índices
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON auth.tags(user_id);
-- Tabla de relación entre archivos y etiquetas
CREATE TABLE IF NOT EXISTS auth.file_tags (
id SERIAL PRIMARY KEY,
tag_id INTEGER NOT NULL REFERENCES auth.tags(id) ON DELETE CASCADE,
file_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tag_id, file_id)
);
-- Comentarios de documentación
COMMENT ON TABLE auth.tags IS 'Almacena etiquetas definidas por usuarios';
COMMENT ON TABLE auth.file_tags IS 'Relación muchos-a-muchos entre archivos y etiquetas';
```
## Guía de Buenas Prácticas
1. **Migraciones Incrementales**: Cada migración debe representar un cambio atómico y coherente.
2. **Migraciones Idempotentes**: Cuando sea posible, usa comandos que pueden ejecutarse múltiples veces sin errores (ej. `CREATE TABLE IF NOT EXISTS`).
3. **Migraciones Forward-Only**: Diseña las migraciones para avanzar, no para revertir. Si necesitas deshacer un cambio, crea una nueva migración.
4. **Compatibilidad Hacia Adelante**: Las migraciones deben ser compatibles con el código existente y el que se va a desplegar.
5. **Prueba Antes de Desplegar**: Prueba las migraciones en un entorno similar al de producción antes de aplicarlas.
6. **Documentación**: Documenta el propósito y los cambios clave de cada migración con comentarios dentro del archivo SQL.
## Solución de Problemas
### Verificación del Estado de las Migraciones
Para verificar qué migraciones se han aplicado, OxiCloud incluye detección en tiempo de inicio:
```rust
// Desde src/common/db.rs
let migration_check = sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users')")
.fetch_one(&pool)
.await;
match migration_check {
Ok(row) => {
let tables_exist: bool = row.get(0);
if !tables_exist {
tracing::warn!("Las tablas de la base de datos no existen. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
},
Err(_) => {
tracing::warn!("No se pudo verificar el estado de las migraciones. Por favor, ejecuta las migraciones con: cargo run --bin migrate --features migrations");
}
}
```
### Problemas Comunes
1. **Error de conexión a la base de datos**: Verifica la URL de conexión en la variable de entorno `DATABASE_URL`.
2. **Conflictos de migración**: Si una migración falla, revisa los mensajes de error para identificar conflictos con el esquema existente.
3. **Permisos insuficientes**: Asegúrate de que el usuario de la base de datos tenga permisos suficientes para crear esquemas, tablas e índices.
## Beneficios del Enfoque Basado en Migraciones
- **Separación de Responsabilidades**: Las migraciones están separadas del código de la aplicación.
- **Automatización**: Facilita la automatización de despliegues y CI/CD.
- **Historial de Cambios**: Proporciona un historial claro de cómo ha evolucionado el esquema.
- **Colaboración**: Permite que múltiples desarrolladores contribuyan cambios al esquema de forma ordenada.
- **Entornos Múltiples**: Garantiza que todos los entornos (desarrollo, pruebas, producción) tengan estructuras de base de datos idénticas.
+171
View File
@@ -0,0 +1,171 @@
# Base de Datos y Transacciones en OxiCloud
## Introducción a Transacciones Explícitas en la Base de Datos
Este documento describe la implementación de transacciones explícitas en OxiCloud para garantizar la integridad de los datos en operaciones de base de datos PostgreSQL.
## ¿Qué son las Transacciones?
Una transacción es una secuencia de operaciones de base de datos tratadas como una única unidad lógica. Las transacciones siguen las propiedades ACID:
- **Atomicidad**: Una transacción es "todo o nada". Si cualquier parte falla, toda la transacción falla.
- **Consistencia**: La base de datos pasa de un estado válido a otro estado válido.
- **Aislamiento**: Las transacciones simultáneas se comportan como si fueran secuenciales.
- **Durabilidad**: Una vez confirmada, la transacción permanece confirmada incluso en caso de fallo del sistema.
## Implementación en OxiCloud
OxiCloud ahora utiliza un enfoque consistente para las transacciones de base de datos mediante la función `with_transaction`, que:
1. Comienza una transacción
2. Ejecuta operaciones
3. Confirma automáticamente si todo fue exitoso
4. Revierte (rollback) automáticamente en caso de error
### Utilidad de Transacciones
En `src/infrastructure/repositories/pg/transaction_utils.rs` hemos implementado:
```rust
/// Helper function to execute database operations in a transaction
pub async fn with_transaction<F, T, E>(
pool: &Arc<PgPool>,
operation_name: &str,
operation: F,
) -> Result<T, E>
where
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
E: From<SqlxError> + std::fmt::Display
{ ... }
```
Esta función:
- Recibe un pool de conexiones y un closure con operaciones
- Maneja begin/commit/rollback automáticamente
- Proporciona logging detallado del ciclo de vida de la transacción
### Ejemplo de Uso en Repositorios
```rust
// Creación de un usuario con transacción explícita
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
with_transaction(
&self.pool,
"create_user",
|tx| {
Box::pin(async move {
// Operación principal - insertar usuario
sqlx::query("INSERT INTO auth.users ...")
.bind(...)
.execute(&mut **tx)
.await?;
// Operaciones adicionales dentro de la misma transacción
// ...
Ok(user_clone)
})
}
).await
}
```
## Casos de Uso Implementados
### En UserPgRepository
1. **Creación de Usuario**
- Garantiza que todas las operaciones de inserción son atómicas
- Permite agregar operaciones relacionadas (como configuración de permisos)
2. **Actualización de Usuario**
- Asegura que las modificaciones se apliquen completamente o no se apliquen en absoluto
- Soporta operaciones combinadas como actualización de información de perfil y preferencias
### En SessionPgRepository
1. **Creación de Sesión**
- Inserta la sesión y actualiza el timestamp de último acceso del usuario en una única transacción
- Garantiza consistencia entre sesiones y datos de usuario
2. **Revocación de Sesiones**
- Asegura que la revocación de una sesión o de todas las sesiones de un usuario sea atómica
- Permite registrar eventos de seguridad dentro de la misma transacción
## Niveles de Aislamiento
OxiCloud admite diferentes niveles de aislamiento de transacciones mediante `with_transaction_isolation`:
```rust
// Ejemplo de uso con nivel de aislamiento específico
with_transaction_isolation(
&pool,
"operacion_critica",
sqlx::postgres::PgIsolationLevel::Serializable,
|tx| { ... }
).await
```
Los niveles de aislamiento disponibles son:
1. **Read Committed** (predeterminado)
- Garantiza que los datos leídos están confirmados
- No previene lecturas no repetibles o fantasma
2. **Repeatable Read**
- Garantiza que las lecturas sean consistentes durante toda la transacción
- Previene lecturas no repetibles pero no lecturas fantasma
3. **Serializable**
- Nivel más alto de aislamiento
- Garantiza que las transacciones se comporten como si se ejecutaran en serie
- Puede causar errores de serialización que requieren reintento
## Mejores Prácticas
1. **Duración de Transacciones**
- Mantén las transacciones lo más cortas posible
- Evita operaciones de larga duración dentro de transacciones
2. **Manejo de Errores**
- Los errores dentro de una transacción provocan rollback automático
- Utiliza logging adecuado para diagnosticar fallos
3. **Límites de Transacción**
- Define claramente dónde comienzan y terminan las transacciones
- Agrupa operaciones relacionadas en una sola transacción
4. **Aislamiento Apropiado**
- Usa el nivel de aislamiento más bajo adecuado para tu caso de uso
- Considera serializable para operaciones críticas con posibilidad de conflicto
## Ventajas de Transacciones Explícitas
1. **Integridad de Datos Mejorada**
- Garantía ACID para operaciones complejas
- Prevención de estados inconsistentes
2. **Mejor Manejo de Errores**
- Rollback automático ante fallos
- Comportamiento predecible en caso de error
3. **Concurrencia Segura**
- Manejo adecuado de operaciones simultáneas
- Prevención de condiciones de carrera
4. **Rendimiento**
- Reducción de trips a la base de datos
- Operaciones en lote para mejor eficiencia
## Consideraciones de Rendimiento
- Las transacciones añaden cierta sobrecarga
- El rendimiento puede verse afectado por:
- Duración de la transacción
- Nivel de aislamiento
- Número de registros afectados
- Contención por bloqueos
## Conclusión
La implementación de transacciones explícitas en OxiCloud mejora significativamente la robustez del sistema y garantiza la integridad de los datos en escenarios complejos. El enfoque modular y la API de transacciones simplificada permiten extender fácilmente estos beneficios a nuevas funcionalidades.
+168
View File
@@ -0,0 +1,168 @@
# File System Safety in OxiCloud
This document describes the implementation of file system safety mechanisms in OxiCloud to ensure data integrity and durability during file operations.
## Introduction
Data integrity is critical in a file storage system like OxiCloud. When files are written to disk, it's important to ensure that:
1. Writes are atomic - they either complete fully or not at all
2. Data is properly synchronized to persistent storage
3. Directory entries are properly updated and persisted
4. The system can recover from unexpected crashes or power failures
OxiCloud implements several mechanisms to achieve these goals.
## The Problem: Buffered I/O and Data Loss
Standard file system operations in many programming languages and operating systems use buffered I/O by default:
```rust
// This operation may not immediately persist to disk
fs::write(path, content)
```
When an application writes data, the operating system typically:
1. Accepts the write into memory buffers
2. Acknowledges completion to the application
3. Schedules the actual disk write for later
This creates a window where a system crash or power failure can result in data loss, as the data may exist only in memory buffers that haven't been flushed to disk.
## OxiCloud's Solution
OxiCloud implements a comprehensive approach to file system safety through the `FileSystemUtils` service, which provides:
### 1. Atomic Write Pattern
Files are written using a safe atomic pattern:
```rust
/// Writes data to a file with fsync to ensure durability
/// Uses a safe atomic write pattern: write to temp file, fsync, rename
pub async fn atomic_write<P: AsRef<Path>>(path: P, contents: &[u8]) -> Result<(), IoError>
```
This implements a write-then-rename pattern:
1. Write to a temporary file in the same directory
2. Call `fsync` to ensure data is on disk
3. Atomically rename the temp file to the target file
4. Sync the parent directory to ensure the rename is persisted
### 2. Directory Synchronization
Directory operations are also synchronized:
```rust
/// Creates directories with fsync
pub async fn create_dir_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError>
```
This ensures that:
1. Directories are properly created
2. Directory entries are persisted to disk
3. Parent directories are also synchronized
### 3. Rename and Delete Operations
Renames and delete operations follow the same pattern:
```rust
/// Renames a file or directory with proper syncing
pub async fn rename_with_sync<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), IoError>
/// Removes a file with directory syncing
pub async fn remove_file_with_sync<P: AsRef<Path>>(path: P) -> Result<(), IoError>
```
These operations ensure that:
1. The operation itself is completed
2. The parent directory entry is updated and synchronized
## Implementation Details
### Implementing fsync on Files
```rust
// Write file content
file.write_all(contents).await?;
// Ensure data is synced to disk
file.flush().await?;
file.sync_all().await?;
```
The `sync_all()` call is critical as it instructs the operating system to flush data and metadata to the physical storage device.
### Implementing fsync on Directories
```rust
// Sync a directory to ensure its contents (entries) are durable
async fn sync_directory<P: AsRef<Path>>(path: P) -> Result<(), IoError> {
let dir_file = OpenOptions::new().read(true).open(path).await?;
dir_file.sync_all().await
}
```
This is essential after operations that modify directory entries, such as creating, renaming, or deleting files.
## Usage in the Codebase
The `FileSystemUtils` service is integrated throughout OxiCloud's file operations:
### In File Write Repository
```rust
// Write the file to disk using atomic write with fsync
tokio::time::timeout(
self.config.timeouts.file_write_timeout(),
FileSystemUtils::atomic_write(&abs_path, &content)
).await
```
### In File Move Operations
```rust
// Move the file physically with fsync
time::timeout(
self.config.timeouts.file_timeout(),
FileSystemUtils::rename_with_sync(&old_abs_path, &new_abs_path)
).await
```
### For Directory Creation
```rust
// Ensure the parent directory exists with proper syncing
self.ensure_parent_directory(&abs_path).await?;
// Implementation uses FileSystemUtils
async fn ensure_parent_directory(&self, abs_path: &PathBuf) -> FileRepositoryResult<()> {
if let Some(parent) = abs_path.parent() {
time::timeout(
self.config.timeouts.dir_timeout(),
FileSystemUtils::create_dir_with_sync(parent)
).await
}
}
```
## Benefits
By implementing these safety measures, OxiCloud provides:
1. **Data Durability**: Critical data is properly synchronized to persistent storage
2. **Crash Resilience**: The system can recover from unexpected failures without data loss
3. **Consistency**: File operations maintain a consistent file system state
4. **Atomic Operations**: File writes appear as all-or-nothing operations
## Performance Considerations
These safety measures do have some performance impact, as synchronizing to disk is more expensive than buffered writes. However, OxiCloud:
1. Applies these measures only to critical operations
2. Uses timeouts to prevent operations from blocking indefinitely
3. Implements parallel processing for large files
The safety-performance tradeoff favors safety for critical data while still maintaining good performance for most operations.
+250
View File
@@ -0,0 +1,250 @@
# Mejores Prácticas para PostgreSQL en OxiCloud
Este documento describe las mejores prácticas para el uso de PostgreSQL en OxiCloud, siguiendo recomendaciones oficiales y la guía ["Don't Do This"](https://wiki.postgresql.org/wiki/Don%27t_Do_This) de PostgreSQL.
## Diseño de Esquema
### Tipos de Datos
#### Uso de TEXT en lugar de VARCHAR(n)
OxiCloud utiliza el tipo `TEXT` en lugar de `VARCHAR(n)` con límites arbitrarios para campos de texto:
```sql
-- Recomendado ✅
username TEXT NOT NULL UNIQUE
-- Evitar ❌
username VARCHAR(32) NOT NULL UNIQUE
```
**Razones:**
- `TEXT` y `VARCHAR` tienen el mismo rendimiento y ocupan el mismo espacio.
- `VARCHAR(n)` impone un límite arbitrario que puede causar errores inesperados.
- PostgreSQL optimiza internamente ambos tipos de manera idéntica.
#### Uso de TIMESTAMPTZ para Fechas y Horas
OxiCloud utiliza `TIMESTAMP WITH TIME ZONE` (o `TIMESTAMPTZ`) para todos los campos de fecha/hora:
```sql
-- Recomendado ✅
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
-- Evitar ❌
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
```
**Razones:**
- `TIMESTAMPTZ` almacena un punto en el tiempo unívoco.
- Gestiona correctamente las zonas horarias y cambios de horario de verano.
- Evita problemas de ambigüedad al trabajar con diferentes husos horarios.
#### Evitar CHAR(n)
OxiCloud no utiliza el tipo `CHAR(n)` en ningún caso:
```sql
-- Recomendado ✅
country_code TEXT NOT NULL CHECK (length(country_code) = 2)
-- Evitar ❌
country_code CHAR(2) NOT NULL
```
**Razones:**
- `CHAR(n)` rellena con espacios hasta la longitud declarada.
- Este comportamiento puede causar problemas sutiles en comparaciones.
- Para valores de longitud fija, es mejor usar `TEXT` con una restricción CHECK.
#### Usar SERIAL con Precaución
OxiCloud utiliza `SERIAL` solo en casos específicos, prefiriendo `IDENTITY` cuando es posible:
```sql
-- Recomendado para PostgreSQL 10+ ✅
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
-- Alternativa aceptable para compatibilidad ✅
id SERIAL PRIMARY KEY
```
**Razones:**
- `SERIAL` tiene comportamientos extraños con gestión de dependencias y permisos.
- Las columnas `IDENTITY` (PostgreSQL 10+) ofrecen mejor integración con el sistema.
### Índices y Restricciones
#### Nombrado Consistente de Índices
OxiCloud sigue una convención de nomenclatura para índices:
```sql
-- Índice en una columna
CREATE INDEX IF NOT EXISTS idx_table_column ON schema.table(column);
-- Índice en múltiples columnas
CREATE INDEX IF NOT EXISTS idx_table_col1_col2 ON schema.table(col1, col2);
```
#### Uso de Restricciones Explícitas
OxiCloud define restricciones explícitas en lugar de depender de convenciones implícitas:
```sql
-- Restricción de unicidad
UNIQUE(user_id, item_id, item_type)
-- Restricción de comprobación
CHECK (storage_quota_bytes >= 0)
```
## Consultas SQL
### Evitar NOT IN con Subconsultas
OxiCloud evita el uso de `NOT IN` con subconsultas:
```sql
-- Recomendado ✅
SELECT * FROM files
WHERE NOT EXISTS (SELECT 1 FROM deleted_files WHERE deleted_files.id = files.id);
-- Evitar ❌
SELECT * FROM files
WHERE id NOT IN (SELECT id FROM deleted_files);
```
**Razones:**
- `NOT IN` se comporta de manera inesperada con valores NULL.
- `NOT EXISTS` es más eficiente y predecible.
### Usar BETWEEN con Precaución
OxiCloud evita `BETWEEN` para rangos de fechas, prefiriendo comparaciones explícitas:
```sql
-- Recomendado ✅
WHERE timestamp_col >= '2025-01-01' AND timestamp_col < '2025-01-02'
-- Evitar ❌
WHERE timestamp_col BETWEEN '2025-01-01' AND '2025-01-02'
```
**Razones:**
- `BETWEEN` incluye ambos extremos, lo que puede ser problemático para rangos de tiempo.
- Usar `>=` y `<` es más claro para expresar rangos de tiempo.
## Transacciones
### Uso Explícito de Transacciones
OxiCloud implementa transacciones explícitas para operaciones que deben ser atómicas:
```rust
// Ejemplo de transacción explícita
let mut tx = pool.begin().await?;
// Operaciones dentro de la transacción
sqlx::query("INSERT INTO users (id, username) VALUES ($1, $2)")
.bind(id)
.bind(username)
.execute(&mut *tx)
.await?;
sqlx::query("INSERT INTO profiles (user_id, display_name) VALUES ($1, $2)")
.bind(id)
.bind(display_name)
.execute(&mut *tx)
.await?;
// Confirmar la transacción
tx.commit().await?;
```
### Manejo de Errores en Transacciones
Las transacciones incluyen manejo adecuado de errores con rollback automático:
```rust
let result = sqlx::Transaction::begin(&pool).await.and_then(|mut tx| async move {
// Operaciones dentro de la transacción
let result1 = operation1(&mut tx).await?;
let result2 = operation2(&mut tx).await?;
// Confirmar la transacción si todo fue exitoso
tx.commit().await?;
Ok((result1, result2))
}).await;
// Si ocurre un error, la transacción se revierte automáticamente
if let Err(e) = &result {
log::error!("Error en la transacción: {}", e);
}
```
## Migraciones y Gestión de Esquema
### Separación del Esquema del Código
OxiCloud separa la definición del esquema del código de la aplicación:
```
OxiCloud/
├── migrations/ # Archivos SQL de migración
├── src/
├── bin/migrate.rs # Herramienta de migración
├── common/db.rs # Solo conecta a la BD, no crea esquema
```
### Uso de Migraciones Versionadas
Las migraciones siguen un formato versionado y se aplican secuencialmente:
```
20250408000000_initial_schema.sql
20250408000001_default_users.sql
```
## Seguridad
### Uso de Consultas Parametrizadas
OxiCloud utiliza consultas parametrizadas para todas las operaciones SQL:
```rust
// Recomendado ✅
sqlx::query("SELECT * FROM users WHERE username = $1")
.bind(username)
.fetch_one(&pool)
.await?;
// Evitar ❌
sqlx::query(&format!("SELECT * FROM users WHERE username = '{}'", username))
.fetch_one(&pool)
.await?;
```
**Razones:**
- Previene ataques de inyección SQL.
- Permite la reutilización de planes de consulta.
- Mejora el rendimiento general.
### Configuración de Autenticación Segura
OxiCloud evita el uso de autenticación `trust` para conexiones TCP/IP:
```
# pg_hba.conf recomendado ✅
hostssl all all 0.0.0.0/0 scram-sha-256
# Evitar ❌
host all all 0.0.0.0/0 trust
```
## Recursos Adicionales
- [Wiki PostgreSQL - Don't Do This](https://wiki.postgresql.org/wiki/Don%27t_Do_This)
- [Documentación oficial de PostgreSQL](https://www.postgresql.org/docs/)
- [Guía de migraciones de OxiCloud](DATABASE-MIGRATIONS.md)