Ten projekt demonstruje problem backpressure w Node.js oraz pokazuje różne sposoby jego rozwiązania. Backpressure występuje, gdy strumień produkujący dane (np. odczyt z pliku) działa szybciej niż strumień konsumujący dane (np. zapis do pliku), co może prowadzić do przepełnienia buforów i problemów z pamięcią.
- Zrozumienie problemu backpressure - jak szybki odczyt może przepełnić bufor wolnego zapisu
- Poznanie rozwiązań - użycie
pipeline(),pipe()z automatyczną obsługą backpressure - Monitorowanie pamięci - obserwacja wpływu backpressure na zużycie RAM
- Dobór właściwego rozwiązania - kiedy użyć którego podejścia
- Node.js 14.0.0 lub nowszy
- Dostęp do dysku dla utworzenia pliku testowego (~100MB)
Najpierw wygeneruj duży plik testowy (100MB):
npm run generate-fileLub bezpośrednio:
node generate-file.jsnpm run start:problem
# lub
node app.js --solution=problemnpm run start:pipeline
# lub
node app.js --solution=pipelinenpm run start:pipe
# lub
node app.js --solution=pipenpm run start:all
# lub
node app.js --solution=allDodaj flagę --slow, aby symulować wolny zapis:
npm run start:slow
# lub
node app.js --slow --solution=problemCo się dzieje?
Gdy szybko czytamy dane z pliku i wolno je zapisujemy, Node.js może gromadzić dane w pamięci. Bez odpowiedniej obsługi backpressure, wszystkie dane mogą zostać załadowane do pamięci, co prowadzi do:
- Nadmiernego zużycia RAM
- Spowolnienia aplikacji
- Potencjalnych awarii z powodu braku pamięci
Demonstracja:
W części 1 używamy ręcznej obsługi zdarzeń drain, aby pokazać, jak backpressure wpływa na przepływ danych. Kod monitoruje liczbę wystąpień backpressure i loguje je w konsoli.
readStream.on('data', (chunk) => {
if (!writeStream.write(chunk)) {
// Backpressure! Zatrzymujemy odczyt
readStream.pause();
}
});
writeStream.on('drain', () => {
// Bufor pusty, wznawiamy odczyt
readStream.resume();
});Dlaczego pipeline()?
pipeline() jest zalecanym podejściem w Node.js, ponieważ:
- ✅ Automatycznie obsługuje backpressure
- ✅ Poprawnie czyści strumienie po zakończeniu lub błędzie
- ✅ Obsługuje błędy we wszystkich strumieniach
- ✅ Zwraca Promise, co ułatwia obsługę asynchroniczną
Przykład:
await pipeline(readStream, writeStream);Pipeline automatycznie:
- Zatrzymuje odczyt, gdy bufor zapisu jest pełny
- Wznawia odczyt, gdy bufor jest gotowy
- Czyści wszystkie strumienie po zakończeniu
Kiedy użyć pipe()?
pipe() również automatycznie obsługuje backpressure, ale wymaga ręcznej obsługi błędów:
readStream.pipe(writeStream);
readStream.on('error', (err) => {
readStream.destroy();
writeStream.destroy();
});
writeStream.on('error', (err) => {
readStream.destroy();
writeStream.destroy();
});Uwaga: pipe() nie czyści automatycznie strumieni przy błędach, więc należy to robić ręcznie.
Projekt używa process.memoryUsage() do monitorowania zużycia pamięci:
- RSS (Resident Set Size) - całkowita pamięć przydzielona procesowi
- Heap Used - użyta pamięć sterty JavaScript
- Heap Total - całkowita przydzielona pamięć sterty
- External - pamięć używana przez obiekty C++ (np. buforów strumieni)
Obserwacje:
- Bez obsługi backpressure: pamięć może gwałtownie wzrosnąć
- Z automatyczną obsługą: pamięć pozostaje na stałym, niskim poziomie
Parametr highWaterMark kontroluje rozmiar bufora wewnętrznego strumienia:
const readStream = fs.createReadStream(file, {
highWaterMark: 64 * 1024 // 64KB
});Wpływ na backpressure:
- Większy
highWaterMark→ więcej danych w pamięci, ale mniej operacji I/O - Mniejszy
highWaterMark→ mniej danych w pamięci, ale więcej operacji I/O - Dla dużych plików: zalecany jest mniejszy
highWaterMark(16-64KB)
| Opcja | Opis |
|---|---|
--solution=problem |
Uruchom tylko demonstrację problemu (domyślnie) |
--solution=pipeline |
Uruchom rozwiązanie z pipeline() |
--solution=pipe |
Uruchom rozwiązanie z pipe() |
--solution=all |
Uruchom wszystkie scenariusze |
--slow |
Symuluj wolny konsument (dodaje opóźnienie 10ms) |
🔴 CZĘŚĆ 1: Problem backpressure
=====================================
Plik wejściowy: large.txt
Plik wyjściowy: output1.txt
Tryb wolny: NIE
📊 Pamięć przed rozpoczęciem:
RSS: 45.23 MB
Heap Used: 8.45 MB
Heap Total: 12.34 MB
External: 0.12 MB
⚠️ Backpressure wykryty! Bufor zapisu pełny (event #1)
Przerwano odczyt, czekamy na opróżnienie bufora...
✅ Bufor zapisu opróżniony. Wznowienie odczytu...
✅ Odczyt zakończony: 100.00 MB
✅ Zapis zakończony: 100.00 MB
⏱️ Czas wykonania: 2.34s
⚠️ Liczba wystąpień backpressure: 3
📊 Pamięć po zakończeniu:
RSS: 48.56 MB
Heap Used: 9.12 MB
Heap Total: 13.45 MB
External: 0.15 MB
- Zawsze używaj
pipeline()lubpipe()zamiast ręcznej obsługidata/write - Monitoruj pamięć podczas pracy z dużymi plikami
- Dostosuj
highWaterMarkw zależności od rozmiaru danych i dostępnej pamięci - Obsługuj błędy we wszystkich strumieniach
- Testuj z realistycznymi danymi (duże pliki, wolne sieci, itp.)
backpressure-demo/
├── app.js # Główny plik z demonstracjami
├── generate-file.js # Skrypt generujący plik testowy
├── package.json # Konfiguracja projektu
├── README.md # Ta dokumentacja
├── large.txt # Wygenerowany plik testowy (100MB)
├── output1.txt # Wynik demonstracji problemu
└── output2.txt # Wynik demonstracji rozwiązania
npm run generate-file- Sprawdź, czy używasz
pipeline()lubpipe() - Zmniejsz
highWaterMark - Sprawdź, czy wszystkie strumienie są prawidłowo zamykane
- To normalne przy flagę
--slow - Bez
--slow, działanie powinno być szybkie - Sprawdź, czy dysk nie jest przeciążony
ISC
Autor: Backpressure Demo
Wersja: 1.0.0
Data: 2024