CodeGym /Kurslar /JAVA 25 SELF /Böyük fayllar: chunking patternləri

Böyük fayllar: chunking patternləri

JAVA 25 SELF
Səviyyə, Dərs
Mövcuddur

1. Giriş

Müasir dünyada məlumatlar yağışdan sonra göbələklərdən də sürətli artır. Bəzən onlarla, hətta yüzlərlə giqabayt ölçülü fayllarla işləmək lazım gəlir — bunlar loglar, verilənlər bazası dump-ları və ya nəhəng arxivlər ola bilər. Belə faylı tamamilə yaddaşa yükləmək cəhdi adətən uğursuz olur: proqram ya bütün operativ yaddaşı “yeyir”, ya da dözülməz dərəcədə ləng işləməyə başlayır.

Səbəblər aydındır. Operativ yaddaş sonsuz deyil və fayl onun həcmini aşırsa, OutOfMemoryError ala bilərsiniz. Hətta yaddaş yetərli olsa belə, nəhəng faylın bir thread-də ardıcıl oxunması və emalı saatlarla çəkə bilər. Üstəlik, diskin öz məhdudiyyəti var: oxuma sürəti sabitdir, lakin bir neçə thread qoşduqda, xüsusilə SSD-də, prosesi nəzərəçarpacaq dərəcədə sürətləndirmək olar.

Bu səbəbdən əsas nəticə sadədir: böyük faylları hissələr üzrə, yəni chunk-lar şəklində emal etmək və imkan olduqca bunu paralel aparmaq lazımdır. Məhz bu yanaşma giqabaytlarla məlumatla əlavə əzablar olmadan işləməyə imkan verir.

2. Həll: Chunking patterni

Chunking — böyük faylı idarəolunan kiçik hissələrə (chunks) bölmək və onları bir-birindən asılı olmadan emal etmək patternidir.

Analogiya:
Bütöv bir qarpızı bir dəfəyə yemək əvəzinə onu dilimlərə kəsib hər dəfə birini yeyirsiniz. Bu, həm daha asan, həm də daha sürətlidir!

Necə işləyir?

  1. Faylın ölçüsünü müəyyənləşdirmək.
    • File.length() və ya Files.size(Path) vasitəsilə fayldakı bayt sayını öyrənirik.
  2. Hissənin ölçüsünü hesablamaq (chunk size).
    • Adətən 10–20 MB seçilir (daha çox/az — tapşırıqdan və avadanlıqdan asılıdır).
    • Hissə ölçüsünü chunkSize dəyişənində saxlamaq rahatdır və maksimal performans üçün disk blokunun ölçüsünə bölünən dəyər seçmək məsləhətdir.
  3. Tapşırıqlar siyahısının yaradılması.
    • Hər tapşırıq bir hissənin emalıdır: oxuma, parsing, şifrələmə, sıxma və s.
    • Tapşırıqları thread hovuzundan istifadə edərək paralel işlətmək olar.

Vizualizasiya:

+-------------------+
|       Fayl        |
+-------------------+
|  [chunk 1]        |
|  [chunk 2]        |
|  [chunk 3]        |
|  ...              |
|  [chunk N]        |
+-------------------+

3. Paralel emalın reallaşdırılması

ExecutorService və ya ForkJoinPool-dan istifadə

Hissələri paralel emal etmək üçün Java-nın standart multithreading vasitələrindən istifadə edin:

  • ExecutorService — sabit ölçülü thread hovuzu (Executors.newFixedThreadPool(n)).
  • ForkJoinPool — rekursiv tapşırıqlar və “böl və hökm sür” yanaşması üçün.

Nümunə:

ExecutorService pool = Executors.newFixedThreadPool(4); // 4 thread

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    pool.submit(() -> {
        processChunk(file, chunkIndex, chunkSize);
    });
}

pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);

Hər tapşırıq faylın öz hissəsini oxuyur və onu müstəqil şəkildə emal edir.

4. Açar mexanizmlər: RandomAccessFile və FileChannel

RandomAccessFile

RandomAccessFile faylda “gəzməyə” və lazım olan mövqedən oxumağa imkan verir.

try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
    raf.seek(chunkStart); // Hissənin başlanğıcına keçiririk
    byte[] buffer = new byte[chunkSize];
    int bytesRead = raf.read(buffer);
    // buffer-i emal edirik
}
  • seek(long pos) — “kursoru” lazımi mövqeyə keçir.
  • Yalnız lazım olan bayt diapazonunu oxumaq olar.

FileChannel

FileChannel — daha müasir və sürətli üsul (xüsusilə böyük fayllar üçün).

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
    channel.position(chunkStart);
    int bytesRead = channel.read(buffer);
    // buffer-i emal edirik
}
  • position(long newPosition) — oxuma mövqeyini təyin edir.
  • Faylın qalan hissəsinə toxunmadan yalnız tələb olunan diapazonu oxuya bilərsiniz.

5. Chunking ilə transferTo/transferFrom müqayisəsi

transferTo/transferFrom

FileChannel.transferTo() və transferFrom() metodları belə adlandırılan zero-copy yanaşmasına imkan verir. İdeya sadədir: məlumatları JVM buferlərini kənara qoyaraq fayllar və axınlar arasında birbaşa köçürmək mümkündür. Bu, əməliyyatları çox sürətli edir. Yeganə məhdudiyyət — məlumatları “yolda” dəyişmək olmur, yalnız köçürmək mümkündür; lakin bir çox tapşırıq üçün bu yanaşma böyük həcmdə məlumatla işi nəzərəçarpacaq dərəcədə sürətləndirir.

Nümunə:

try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dst = FileChannel.open(dstPath, WRITE)) {
    src.transferTo(0, src.size(), dst);
}

Chunking

Beləliklə, Chunking — böyük fayllarla hissələr üzrə işləmək üsuludur. O, yalnız məlumatların köçürülməsi üçün deyil, həm də emalı üçün lazımdır: parslamaq, şifrələmək, sıxmaq və ya məlumatı gedişatda axtarmaq olar. Hər bir fayl hissəsi müstəqil emal edilə bilər, hətta paralel də, bu da işi xeyli sürətləndirir.

İdeya belədir: əgər tapşırıq sadə köçürməyə qədər sadələşirsə, transferTo və ya transferFrom istifadə etmək daha yaxşıdır — məlumat birbaşa, sürətli və artıq nüsxələr olmadan hərəkət edir. Amma məzmunla nə isə etmək lazımdırsa — axtarmaq, dəyişmək, analiz etmək — chunking əvəzolunmaz alətə çevrilir.

6. Məhdudiyyətlər və incə məqamlar

Thread-lərin əlavə xərcləri

  • Həddən artıq çox thread yaratmaq performansı azalda bilər (kontekst dəyişmələri, resurslar uğrunda rəqabət).
  • Adətən thread sayı prosessor nüvələrinin sayına bərabər və ya bir qədər çox seçilir.

Disk məhdudiyyətləri

  • 100 thread olsa belə, disk yenə də öz maksimal oxu sürətindən daha tez oxuya bilməyəcək.
  • SSD-də paralel oxu artım verə bilər, HDD-də — demək olar ki, yox.

Sinxronizasiya zərurəti

  • Əgər hissələrin emalı müstəqildirsə — hər şey sadədir.
  • Ümumi nəticəni toplamaq lazımdırsa (məsələn, fayldakı bütün ədədlərin cəmini hesablamaq), ümumi dəyişənlərə girişi sinxronlaşdırmaq lazımdır (məsələn, AtomicLong istifadə etmək və ya nəticələri ayrıca siyahıda toplamaq).

Hissələrin sərhədləri

  • Fayl mətnlidirsə, diqqətli olun: sətiri və ya simvolu ortadan kəsməyin.
  • Binar fayllar (arxivlər, təsvirlər) üçün — adətən istənilən yerdən kəsmək olar.
  • Mətn faylları üçün tez-tez hissələr arasında “örtüşmə” edilir və ya ən yaxın sətirsonu axtarılır.

7. Nümunə: böyük faylda ədədlərin cəmini paralel hesablamaq

Tapşırıq:
Milyonlarla ədəd olan bir fayl var (hər sətirdə bir ədəd). Onların cəmini tez hesablamaq lazımdır.

Addım-addım plan:

  1. Faylın ölçüsünü müəyyən edirik.
  2. Hissənin ölçüsünü seçirik (məsələn, 10 MB).
  3. Hər hissə üçün:
    • Ən yaxın sətirsonunu tapırıq (ədədi bölməmək üçün).
    • Hissəni oxuyuruq, ədədləri parse edib cəmləyirik.
  4. Bütün hissələrin cəmlərini birləşdiririk.

Kod skeleti:

ExecutorService pool = Executors.newFixedThreadPool(4);
List<Future<Long>> results = new ArrayList<>();

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    results.add(pool.submit(() -> {
        // RandomAccessFile açırıq, hissənin sərhədlərini tapırıq
        // Oxuyuruq, ədədləri parse edirik, cəmi hesablayırıq
        long chunkSum = 0L;
        return chunkSum;
    }));
}

long total = 0;
for (Future<Long> f : results) {
    total += f.get();
}
pool.shutdown();
System.out.println("Cəm: " + total);

8. Nəticələr və best practices

  • Chunking — böyük faylların emalı üçün universal pattern: hissələrə böl, müstəqil emal et, nəticəni topla.
  • RandomAccessFile və ya FileChannel ilə lazım olan mövqedən oxuyun.
  • Paralel emal üçün — ExecutorService və ya ForkJoinPool.
  • Emalsız köçürmə üçün — transferTo/transferFrom (zero-copy) istifadə edin.
  • Hissə ölçüsü, thread sayı və disk məhdudiyyətlərinə nəzarət edin.
  • Mətn faylları üçün — sətir sərhədlərini diqqətlə tapın.
  • Binar fayllar üçün format spesifikası yoxdursa istənilən yerdən kəsilə bilər.

9. Chunking ilə işləyərkən tipik səhvlər

Səhv №1: Çox böyük fayl. Bütün faylı yaddaşa oxumağa çalışırsınız — OutOfMemoryError alırsınız.

Səhv №2: Həddindən artıq çox thread. Çox sayda thread yaradırsınız — sistem kontekst dəyişmələri səbəbilə “yavaşıyır”.

Səhv №3: Sətirlər parçalandı. Mətn fayllarında sətir sərhədlərini nəzərə almırsınız — “cırılmış” sətirlər və parsing səhvləri əldə edirsiniz.

Səhv №4: Metodlardan yanlış istifadə. Məlumatı emal etmək üçün transferTo/transferFrom istifadə etməyə çalışırsınız — bu alınmır, bu metodlar yalnız köçürmə üçündür.

Səhv №5: Sinxronizasiyanı unutdunuz. Nəticələrin toplanmasını sinxronlaşdırmırsınız — düzgün olmayan cəm və ya digər bug-lar əldə edirsiniz.

Səhv №6: Resurs sızması. Faylları/kanalları bağlamırsınız — resurs sızmaları baş verir.

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Arxivdə itmiş fraqmentlərin axtarışı 🔍
Arxivdə itmiş fraqmentlərin axtarışı 🔍
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Qalaktik sxemlərin modullara bölünməsi 🏗️
Qalaktik sxemlərin modullara bölünməsi 🏗️
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION