Scaricare un file non significa soltanto riceverlo
Un download tradizionale tratta il file come un unico flusso.
Finché la connessione rimane stabile, è un modello semplice. Quando però la rete cade, il processo viene interrotto o il server inizia a limitare le richieste, il problema cambia.
Un trasferimento già quasi completato può diventare inutilizzabile e costringere a ricominciare da zero.
Volevo quindi esplorare un approccio diverso: dividere il file in intervalli indipendenti, scaricarli in parallelo e fare in modo che il sistema sappia esattamente quali parti sono già state completate.
Da qui nasce Chunk Downloader.
Costruire un downloader che sappia recuperare dagli errori
L'obiettivo non era soltanto aumentare il throughput.
Volevo costruire un sistema capace di gestire in modo affidabile un trasferimento reale, anche quando qualcosa va storto.
Il downloader doveva quindi essere in grado di:
- scaricare diverse parti dello stesso file contemporaneamente
- riprendere un trasferimento interrotto senza riscaricare i dati già completati
- reagire automaticamente a errori temporanei di rete
- adattare il numero di worker alle condizioni del trasferimento
- verificare che il file finale sia realmente quello atteso
- evitare di lasciare un file apparentemente completo quando il contenuto è corrotto
Il risultato doveva essere un downloader che non considerasse il fallimento un'eccezione rara, ma una condizione da gestire direttamente nell'architettura.
Come funziona
Chunk Downloader utilizza le HTTP Range Requests per suddividere il file in intervalli di byte indipendenti.
Il sistema analizza prima le capacità del server e pianifica gli intervalli necessari. I chunk vengono poi assegnati a un pool configurabile di worker Go che possono eseguire più richieste contemporaneamente.
Ogni worker scarica il proprio intervallo e scrive direttamente nella posizione corretta del file.
Quando un chunk viene completato, il suo stato viene registrato in un manifest di checkpoint. In caso di interruzione, il downloader può leggere questo stato e riprendere dal punto corretto invece di ricominciare l'intero trasferimento.
Download concorrente
Il file viene suddiviso in più range HTTP e distribuito tra goroutine worker.
Le richieste possono completarsi in ordine diverso senza compromettere il file finale.
Scrittura diretta
Ogni chunk viene scritto direttamente al proprio offset utilizzando os.File.WriteAt.
Non è quindi necessario scaricare tutto in memoria o eseguire successivamente un passaggio separato per ricostruire il file.
Ripresa dopo un'interruzione
Il progresso viene salvato in un file .chk.json.
Quando il processo viene interrotto, il manifest conserva lo stato dei chunk già completati e permette di riprendere il trasferimento successivamente.
L'aggiornamento del manifest avviene in modo atomico per evitare di trasformare un'interruzione in uno stato di recupero inconsistente.
Retry e recupero dagli errori
Gli errori temporanei vengono gestiti attraverso retry con exponential backoff e jitter.
In caso di errori persistenti, il sistema interrompe il chunk interessato invece di continuare indefinitamente.
Verifica dell'integrità
Quando tutti i chunk sono stati scaricati, il file viene verificato tramite SHA-256.
Solo dopo una verifica riuscita il file temporaneo viene rinominato nella destinazione finale.
Se il checksum non corrisponde, il risultato viene rifiutato.
Un sistema costruito attorno allo stato del trasferimento
La struttura del progetto separa chiaramente le responsabilità.
Il comando CLI gestisce input, flag e segnali del processo. Il motore di download coordina il probing dei range, i worker, la scrittura diretta e la verifica finale. Un modulo dedicato gestisce i retry, mentre il manifest mantiene lo stato persistente del download. Il controller adaptive misura il throughput e decide come modificare il livello di concorrenza. Infine, il modulo di metriche espone opzionalmente informazioni compatibili con Prometheus.
Il flusso complessivo è quindi:
HTTP Server → Range Planning → Worker Pool → Direct Writes → Checkpoint → SHA-256 → Atomic Rename
Questa separazione permette di trattare il download come una pipeline composta da più fasi indipendenti invece di un singolo processo monolitico.
Le parti più difficili
1. Gestire chunk che finiscono in ordine diverso
Con più worker attivi, non c'è alcuna garanzia che il chunk numero 4 finisca dopo il numero 3.
La soluzione è evitare di associare l'ordine di completamento all'ordine fisico del file.
Ogni worker conosce l'offset del proprio intervallo e scrive direttamente nella posizione corretta. In questo modo i chunk possono arrivare in qualsiasi ordine senza richiedere un secondo processo di merge.
2. Riprendere senza corrompere il file
La resumability introduce un problema importante: non basta sapere quanti byte sono stati scaricati.
Il sistema deve sapere quali intervalli sono completi e avere un modo affidabile per persistere questo stato.
Il manifest .chk.json diventa quindi parte integrante del trasferimento e non un semplice file di log.
3. Trovare il livello corretto di concorrenza
Più worker non significa automaticamente più velocità.
A un certo punto il collo di bottiglia può diventare la rete, il server, il sistema di storage oppure il costo della concorrenza stessa.
Per questo il progetto include un controller adattivo che misura il throughput su finestre temporali e modifica dinamicamente il numero di worker quando la concorrenza aggiuntiva smette di produrre benefici.
4. Gestire server che non supportano Range Requests
Il modello a chunk funziona bene quando il server risponde correttamente alle richieste parziali.
Non tutti i server lo fanno.
Chunk Downloader gestisce quindi anche il caso in cui il server non supporti il byte range e ricade su un download a flusso singolo.
Perché ho scelto questo approccio
1. HTTP Range Requests
Permettono di trasformare un singolo file in più trasferimenti indipendenti.
Questo rende possibili sia la concorrenza sia la ripresa selettiva dei chunk già completati.
2. os.File.WriteAt
I chunk possono terminare in qualsiasi ordine.
Scrivere direttamente all'offset corretto elimina la necessità di mantenere un cursore di scrittura condiviso e rende inutile una successiva fase di merge.
3. Checkpoint persistenti
Lo stato del download viene trattato come parte dei dati del sistema.
In questo modo un'interruzione del processo non obbliga a ricostruire il progresso dal file parziale.
4. Concorrenza adattiva
Il numero di worker non viene considerato un valore universale.
Il controller osserva il throughput e prova ad adattare la concorrenza alle condizioni reali del trasferimento. Il repository mostra anche perché questa scelta è necessaria: nei benchmark inclusi nel progetto l'aumento dei worker non produce una crescita monotona delle prestazioni.
5. Verifica finale prima della consegna
Un file completo non è necessariamente un file corretto.
Per questo il risultato viene verificato tramite SHA-256 prima di essere considerato valido e rinominato nella destinazione finale.
Un downloader progettato per fallire bene
Chunk Downloader è partito dalla domanda più semplice possibile: cosa succede quando un download lungo viene interrotto proprio prima della fine?
La risposta non è stata aggiungere un semplice pulsante "riprendi".
Ho costruito un sistema in cui concorrenza, checkpoint, retry, adattamento del numero di worker e verifica dell'integrità fanno parte dello stesso modello di trasferimento.
Il progetto include anche benchmark controllati. Nel test documentato nel repository viene utilizzato un payload da 50 MB su una rete loopback locale. I risultati mostrano 416 MB/s con un worker e 826 MB/s con 32 worker in quell'ambiente specifico. Sono dati utili per analizzare il comportamento della concorrenza, non una promessa di velocità su una normale connessione Internet.
Oltre il download
Chunk Downloader mi ha portato a lavorare su un problema tipico dei sistemi concorrenti: come aumentare le prestazioni senza perdere affidabilità.
Il progetto mi ha costretto a ragionare su concorrenza in Go, network I/O, gestione dello stato persistente, recovery dagli errori, sincronizzazione, filesystem e verifica dell'integrità.
La parte più interessante non è stata dividere un file in chunk.
È stata costruire un sistema in cui un trasferimento può essere interrotto, ripreso, adattato alle condizioni della rete e verificato senza perdere la certezza sul risultato finale.