Files
dashlibstorage/vfs.md
2026-09-22 07:25:16 +02:00

190 lines
7.4 KiB
Markdown

Here is a complete design and implementation document for a block-based Virtual File System (VFS) in SQL Server. This incorporates the architectural fixes, concurrency safety, and performance optimizations discussed earlier.
SQL Server Virtual File System (VFS) Implementation Guide
1. Architecture Overview
This VFS architecture mimics a traditional inode-based disk filesystem inside a relational database. Instead of storing large files in a single VARBINARY(MAX) column—which causes severe memory pressure and transaction log bloat—files are divided into fixed-size chunks (blocks) at the application layer and streamed into the database.
Key Benefits:
Low Memory Footprint: Applications can stream gigabytes of data using small, fixed-size memory buffers (e.g., 1MB at a time).
Resumable Uploads: If a network failure occurs, the client only needs to resume from the last successfully written block.
Azure SQL Compatibility: Provides FILESTREAM-like chunking benefits in PaaS environments where native FILESTREAM is unsupported.
2. Database Schema
The schema separates metadata (Nodes) from the binary payload (Blocks). It includes a soft-delete flag to prevent massive transaction log spikes when deleting huge files.
SQL
-- Represents files, directories, and metadata
CREATE TABLE dbo.Filesystem_Nodes (
NodeID BIGINT IDENTITY(1,1) PRIMARY KEY,
ParentNodeID BIGINT NULL FOREIGN KEY REFERENCES dbo.Filesystem_Nodes(NodeID),
Name NVARCHAR(255) NOT NULL,
IsDirectory BIT NOT NULL DEFAULT 0,
ByteSize BIGINT NOT NULL DEFAULT 0,
IsDeleted BIT NOT NULL DEFAULT 0, -- Soft-delete flag for garbage collection
CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
ModifiedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_Parent_Name_NotDeleted UNIQUE (ParentNodeID, Name, IsDeleted)
);
-- Index to quickly find files pending deletion
CREATE NONCLUSTERED INDEX IX_Filesystem_Nodes_Deleted
ON dbo.Filesystem_Nodes(IsDeleted)
INCLUDE (NodeID)
WHERE IsDeleted = 1;
-- Represents the raw binary chunks of each file
CREATE TABLE dbo.Filesystem_Blocks (
NodeID BIGINT NOT NULL,
BlockIndex BIGINT NOT NULL,
Data VARBINARY(MAX) NOT NULL,
-- Crucial: Clustered PK ensures contiguous disk storage for sequential reads
CONSTRAINT PK_Filesystem_Blocks PRIMARY KEY CLUSTERED (NodeID, BlockIndex)
);
-- Foreign key is NO ACTION because we handle cascading deletes via background jobs
ALTER TABLE dbo.Filesystem_Blocks
ADD CONSTRAINT FK_Blocks_Nodes FOREIGN KEY (NodeID)
REFERENCES dbo.Filesystem_Nodes(NodeID);
3. Core Stored Procedures
A. Create a Node (File or Directory)
Initializes the metadata record.
SQL
CREATE OR ALTER PROCEDURE dbo.sp_FS_CreateNode
@ParentNodeID BIGINT = NULL,
@Name NVARCHAR(255),
@IsDirectory BIT = 0,
@NodeID BIGINT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.Filesystem_Nodes (ParentNodeID, Name, IsDirectory, ByteSize)
VALUES (@ParentNodeID, @Name, @IsDirectory, 0);
SET @NodeID = SCOPE_IDENTITY();
END;
B. Write a Block (Concurrency-Safe)
Handles writing or overwriting a specific chunk. Uses an UPDATE / INSERT pattern to avoid MERGE deadlocks and correctly calculates total file size based on a standard block size.
SQL
CREATE OR ALTER PROCEDURE dbo.sp_FS_WriteBlock
@NodeID BIGINT,
@BlockIndex BIGINT,
@Data VARBINARY(MAX),
@StandardBlockSize BIGINT = 1048576 -- Default 1MB blocks
AS
BEGIN
SET NOCOUNT ON;
-- 1. Upsert the block safely
BEGIN TRAN;
UPDATE dbo.Filesystem_Blocks
SET Data = @Data
WHERE NodeID = @NodeID AND BlockIndex = @BlockIndex;
IF @@ROWCOUNT = 0
BEGIN
INSERT INTO dbo.Filesystem_Blocks (NodeID, BlockIndex, Data)
VALUES (@NodeID, @BlockIndex, @Data);
END
COMMIT TRAN;
-- 2. Calculate the file's current max boundary
-- (Previous blocks * Standard Size) + Size of current block
DECLARE @CalculatedSize BIGINT = (@BlockIndex * @StandardBlockSize) + DATALENGTH(@Data);
-- 3. Expand file size if we wrote past the current known end
UPDATE dbo.Filesystem_Nodes
SET ByteSize = CASE WHEN @CalculatedSize > ByteSize THEN @CalculatedSize ELSE ByteSize END,
ModifiedAt = SYSUTCDATETIME()
WHERE NodeID = @NodeID;
END;
C. Read a Block
Retrieves a specific chunk for streaming back to the client.
SQL
CREATE OR ALTER PROCEDURE dbo.sp_FS_ReadBlock
@NodeID BIGINT,
@BlockIndex BIGINT
AS
BEGIN
SET NOCOUNT ON;
SELECT
BlockIndex,
Data,
DATALENGTH(Data) AS BlockByteSize
FROM dbo.Filesystem_Blocks
WHERE NodeID = @NodeID
AND BlockIndex = @BlockIndex;
END;
D. Delete a File (Soft Delete)
Marks the file as deleted to immediately hide it from the UI/Application, preventing long synchronous locks.
SQL
CREATE OR ALTER PROCEDURE dbo.sp_FS_DeleteNode
@NodeID BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- Soft delete the node. A background job will clean up the physical blocks.
UPDATE dbo.Filesystem_Nodes
SET IsDeleted = 1,
ModifiedAt = SYSUTCDATETIME(),
-- Scramble the name slightly to free up the unique constraint
-- so a new file with the same name can be created immediately
Name = Name + '_deleted_' + CAST(NEWID() AS NVARCHAR(36))
WHERE NodeID = @NodeID;
END;
4. Background Maintenance (Garbage Collection)
Because large files can contain tens of thousands of blocks, a single DELETE statement would lock the table and bloat the transaction log. This script should be scheduled via SQL Server Agent to run every few minutes to physically delete blocks in small, manageable batches.
SQL
CREATE OR ALTER PROCEDURE dbo.sp_FS_GarbageCollection
@BatchSize INT = 2000
AS
BEGIN
SET NOCOUNT ON;
DECLARE @DeletedNodes TABLE (NodeID BIGINT);
DECLARE @RowsAffected INT = 1;
-- Get a list of nodes pending deletion
INSERT INTO @DeletedNodes (NodeID)
SELECT NodeID FROM dbo.Filesystem_Nodes WHERE IsDeleted = 1;
-- Delete blocks in small batches to protect the Transaction Log
WHILE @RowsAffected > 0
BEGIN
DELETE TOP (@BatchSize) B
FROM dbo.Filesystem_Blocks B
INNER JOIN @DeletedNodes D ON B.NodeID = D.NodeID;
SET @RowsAffected = @@ROWCOUNT;
-- Brief pause to yield to production workloads
IF @RowsAffected > 0 WAITFOR DELAY '00:00:00.100';
END
-- Once all blocks are gone, delete the metadata nodes
DELETE N
FROM dbo.Filesystem_Nodes N
INNER JOIN @DeletedNodes D ON N.NodeID = D.NodeID
WHERE NOT EXISTS (
SELECT 1 FROM dbo.Filesystem_Blocks B WHERE B.NodeID = N.NodeID
);
END;
5. Application Layer Implementation Notes
To use this database architecture successfully, the application layer (e.g., C#, Python, Node.js) must follow these rules:
Enforce Block Sizes: The application MUST slice incoming files into exact standard sizes (e.g., 1MB) for every block except the final block, which can be smaller.
Concurrency: Multiple threads can upload different blocks of the same file simultaneously (e.g., Thread A writes Block 0, Thread B writes Block 1) without locking each other out.
Read Streaming: To download a file, the application should query sp_FS_ReadBlock in a loop (incrementing BlockIndex from 0 upwards) and pipe the binary result directly to the HTTP Response stream until an empty result is returned.