🏠 خانه برنامه‌نویسی آموزش پیشرفته Zarr: ذخیره‌سازی آرایه‌های...

آموزش پیشرفته Zarr: ذخیره‌سازی آرایه‌های چندبعدی با Chunking، فشرده‌سازی و Backendهای متنوع

راهنمایی جامع و عملی برای درک عمیق Zarr، طراحی طرح‌واره داده (schema)، انتخاب backend مناسب، بهینه‌سازی chunking و فشرده‌سازی، کار با ابر (S3/GCS) و ادغام با Dask/Xarray. مناسب برای داده‌های علمی بزرگ‌مقیاس.

⌘ برنامه‌نویسی پیشرفته ⏱ 120 دقیقه 👁 24 بازدید 📅 2026/08/23
📄 محتوای آموزش

Zarr قالب و کتابخانه‌ای برای ذخیره‌سازی آرایه‌های چندبعدی (N-dimensional arrays) است که با تقسیم داده به قطعه‌های مستقل (chunk)، فشرده‌سازی شفاف، متادیتای خود-توصیف‌گر و پشتیبانی از backendهای گوناگون (فایل‌سیستم، شیء-ذخیره مانند S3/GCS، zip و حافظه) امکان مقیاس‌پذیری و دسترسی تصادفی کارآمد را فراهم می‌کند.

Zarr چیست و کجا به کار می‌آید؟

Zarr یک فرمت ذخیره‌سازی «ساده اما قدرتمند» برای آرایه‌های بزرگ است که به‌ویژه در داده‌های علمی، سنجش‌ازدور، مدل‌های اقلیمی، تصویربرداری، ژنومیک و یادگیری ماشین کاربرد دارد. ایدهٔ مرکزی Zarr این است که هر آرایه به شبکه‌ای از قطعه‌ها (chunks) تقسیم می‌شود؛ هر chunk مستقل فشرده و به‌صورت کلید/مقدار ذخیره می‌گردد. این طراحی چند مزیت کلیدی دارد:

  • دسترسی تصادفی سریع: فقط chunkهای موردنیاز خوانده/نوشته می‌شوند.
  • مقیاس‌پذیری در ابر: هر chunk یک شیء مستقل است؛ مناسب برای S3/GCS.
  • انعطاف در فشرده‌سازی/فیلترها: انتخاب codecها متناسب با داده و الگوی دسترسی.
  • سادگی استقرار: روی دیسک محلی، zip یا object storage به‌خوبی کار می‌کند.

Zarr در نسخهٔ 2 (v2) به‌صورت گسترده در اکوسیستم Python استفاده می‌شود. مشخصات نسخهٔ 3 (v3) ویژگی‌های جدیدی معرفی کرده و به‌صورت تدریجی در ابزارها پشتیبانی می‌شود. برای سازگاری گسترده، تولید دادهٔ v2 همچنان گزینهٔ ایمن‌تری است.

مفاهیم پایه و مدل داده‌ای Zarr

Array و Group

  • Array: یک آرایهٔ چندبعدی با ویژگی‌هایی مانند shape، chunks، dtype، compressor و filters. متادیتای آن در مسیر آرایه نگه‌داری می‌شود.
  • Group: ظرفی سلسله‌مراتبی برای آرایه‌ها و گروه‌های دیگر؛ مشابه پوشه‌ها. برای سازمان‌دهی مجموعهٔ داده‌ها (مثلاً multiscale، نسخه‌ها یا متغیرهای مختلف) استفاده می‌شود.

Chunk

  • هر chunk شامل بلوکی مستطیلی از داده است (مثلاً 256×256). اندازهٔ chunk باید با الگوی دسترسی معمول و منابع سامانه متناسب طراحی شود.
  • هر chunk یک کلید مستقل در Store است؛ این استقلال اساس موازی‌سازی و هم‌زمانی کارآمد است.

Compressor و Filters

  • فشرده‌سازها (مثلاً Blosc با zstd، lz4، یا gzip) حجم و هزینهٔ انتقال/ذخیره را کاهش می‌دهند.
  • فیلترها (مثلاً Delta، BitShuffle، Shuffle) می‌توانند قبل از فشرده‌سازی الگوی داده را بهبود دهند.

Attributes

  • هر Array/Group می‌تواند attrs داشته باشد (متادیتای سبک JSON-compatible) برای واحدها، توصیف، نسخهٔ schema و غیره.

در Zarr v2 انتخاب dimension_separator بین "/" و "." اهمیت دارد. استفاده از "/" معمولاً برای سازگاری با ابزارهای جدید توصیه می‌شود.

انتخاب Store و Backend مناسب

Store توضیح کوتاه مناسب برای نکته‌ها
DirectoryStore پوشهٔ محلی؛ هر chunk یک فایل. توسعهٔ محلی، SSD/NAS ساده و سریع روی دیسک سریع؛ مراقب تعداد فایل‌ها باشید.
FSStore (fsspec) نگاشت فایل‌سیستم‌های گوناگون (S3/GCS/HTTP/…) ابر و فایل‌سیستم‌های مجازی برای S3 از s3fs استفاده کنید؛ key_separator را تنظیم کنید.
ZipStore آرشیو فشردهٔ ZIP با دسترسی تصادفی توزیع یک‌فایله نوشتن ممکن است کندتر باشد؛ مناسب برای خواندن.
MemoryStore کلید/مقدار در حافظه تست، محاسبات موقت دائمی نیست؛ با Cache ترکیب می‌شود.
DBMStore پشتوانهٔ dbm (پایگاه‌داده کلید/مقدار) تعداد فایل کمتر کارایی بسته به پیاده‌سازی dbm متفاوت است.

در object storage (مانند S3/GCS) تعداد بسیار زیاد objectهای کوچک (chunkهای ریز) باعث هزینهٔ list/get بالا و کارایی ضعیف می‌شود. اندازهٔ chunk و الگوی دسترسی را با دقت طراحی کنید.

فشرده‌سازی و Filters در عمل

برای داده‌های عمومی، Blosc با zstd و BITSHUFFLE ترکیبی متداول است. برای داده‌های صحیح با تفاوت‌های کوچک، فیلتر Delta می‌تواند موثر باشد.

import numpy as np
import zarr
from numcodecs import Blosc, Delta, BitShuffle

# Store محلی
store = zarr.DirectoryStore('example.zarr')
root = zarr.open_group(store=store, mode='w')

# فشرده‌ساز پیشنهادی عمومی
comp = Blosc(cname='zstd', clevel=5, shuffle=Blosc.BITSHUFFLE)

# آرایهٔ اعشاری با فشرده‌سازی
arr_f = root.create_dataset(
    'float_data', shape=(10000, 10000), chunks=(512, 512), dtype='f4',
    compressor=comp, dimension_separator='/'
)
arr_f.attrs['description'] = 'نمونهٔ دادهٔ اعشاری'
arr_f[0:512, 0:512] = np.random.random((512, 512)).astype('f4')

# آرایهٔ صحیح با فیلتر Delta و BitShuffle
filters = [Delta(dtype='i4'), BitShuffle()]
arr_i = root.create_dataset(
    'int_data', shape=(1000000,), chunks=(100000,), dtype='i4',
    compressor=Blosc(cname='lz4', clevel=5, shuffle=Blosc.NOSHUFFLE),
    filters=filters, dimension_separator='/'
)
arr_i[:] = np.arange(1_000_000, dtype='i4')

فیلتر Delta باید با dtype سازگار باشد. برای داده‌های اعشاری معمولاً از فیلترهای ترتیبی مثل BitShuffle/Shuffle همراه با فشرده‌ساز استفاده کنید.

طراحی Chunking بهینه

راهنمای عملی

  • حجم هر chunk (پس از فشرده‌سازی) را در محدودهٔ چند صد کیلوبایت تا چند مگابایت هدف‌گذاری کنید؛ برای object storage معمولاً 1–10MB فشرده مناسب است.
  • ابعاد chunk را با الگوی دسترسی منطبق کنید: اگر اغلب برش‌های زمانی می‌خوانید، chunks=(time_small, y_block, x_block).
  • تعداد chunkها را در هر بعد متعادل نگه دارید تا از میلیون‌ها شیء کوچک جلوگیری شود.
  • برای داده‌های تصویری، 256×256 یا 512×512 معمول است؛ برای 3بعدی، بلوک‌های آجرگونه (brick) مانند (1, 256, 256) یا (4, 128, 128).

برآورد اندازهٔ chunk

import numpy as np

def estimate_chunk_bytes(chunks, dtype):
    return int(np.prod(chunks)) * np.dtype(dtype).itemsize

print(estimate_chunk_bytes((1, 256, 256), 'f4'))  # ~ 256 KiB خام

chunk خیلی بزرگ => تأخیر زیاد و انتقال بی‌مورد؛ chunk خیلی کوچک => فراخوانی‌های زیاد و سربار فراوان. با داده و زیرساخت خود اندازهٔ بهینه را اندازه‌گیری کنید.

خواندن/نوشتن: از لوکال تا Cloud

نصب و آماده‌سازی

pip install zarr numcodecs fsspec s3fs dask xarray

ایجاد Dataset محلی

import numpy as np
import zarr
from numcodecs import Blosc

store = zarr.DirectoryStore('local_data.zarr')
root = zarr.open_group(store=store, mode='w')
comp = Blosc(cname='zstd', clevel=5, shuffle=Blosc.BITSHUFFLE)

A = root.create_dataset(
    'A', shape=(365, 1800, 3600), chunks=(1, 256, 256), dtype='f4',
    compressor=comp, dimension_separator='/'
)
A.attrs.update({'units': 'K', 'long_name': 'daily surface temperature'})

# نوشتن منطقه‌ای (region writes)
for t in range(0, 365):
    tile = np.random.random((1, 256, 256)).astype('f4')
    # نمونه: یک کاشی در گوشهٔ بالا-چپ
    A[t:t+1, 0:256, 0:256] = tile

کار با S3 از طریق FSStore

import s3fs
from zarr.storage import FSStore
from zarr.convenience import consolidate_metadata

fs = s3fs.S3FileSystem(anon=False)
# مسیر به‌صورت bucket/prefix؛ کلیدها با '/' جدا شوند
s3_store = FSStore('my-bucket/my-dataset.zarr', fs=fs, key_separator='/')
root_s3 = zarr.open_group(store=s3_store, mode='w')

# ایجاد آرایه مستقیماً روی S3
A_s3 = root_s3.create_dataset(
    'A', shape=(365, 1800, 3600), chunks=(1, 256, 256), dtype='f4',
    compressor=comp, dimension_separator='/'
)
A_s3.attrs['note'] = 'written directly to S3'

# پس از اتمام، متادیتا را یکپارچه کنید تا بازشدن سریع‌تر شود (برای برخی خوانندگان)
consolidate_metadata(s3_store)

برای بهبود کارایی خواندن از ابر می‌توانید از Cache استفاده کنید:

from zarr.storage import LRUStoreCache
cached_s3 = LRUStoreCache(s3_store, max_size=2**29)  # ~512MB
root_cached = zarr.open_group(cached_s3, mode='r')
  

هم‌زمانی، قفل‌ها و پایداری نوشتن

هر chunk مستقل نوشته می‌شود؛ بنابراین نوشتن موازی chunkهای متفاوت امن است. برای جلوگیری از شرایط رقابتی روی همان متادیتا یا chunk، از Synchronizer استفاده کنید.

from zarr.sync import ThreadSynchronizer
import zarr

sync = ThreadSynchronizer()
root = zarr.open_group('local_data.zarr', mode='a', synchronizer=sync)
A = root['A']

# اکنون چند نخ می‌توانند روی بخش‌های متفاوت بنویسند
  • برای نخ‌ها: ThreadSynchronizer. برای فرایندها: ProcessSynchronizer (هماهنگی بین‌فرایندی).
  • نوشتن متادیتا معمولاً کوچک اما حساس است؛ از نوشتن هم‌زمان متادیتای یک Array/Group خودداری کنید.

در object storage عملیات list و ایجاد کلیدها گران است. نوشتن موازی با chunkهای بسیار کوچک می‌تواند باعث کندی شدید شود. اندازهٔ chunk را افزایش دهید یا تعداد کارگران را محدود کنید.

متادیتا، Attributes و مدیریت Schema

  • برای هر Array/Group، attrs را برای واحدها، مقیاس‌ها، نسخهٔ schema و تاریخ تولید پر کنید.
  • dimension_separator: برای سازگاری بهتر، از '/' استفاده کنید؛ این امر مسیر chunkها را سلسله‌مراتبی می‌کند.
  • یکپارچه‌سازی متادیتا (consolidated metadata) تعداد درخواست‌های فهرست‌سازی را کاهش می‌دهد. پس از نوشتن مجموعهٔ بزرگ:
from zarr.convenience import consolidate_metadata
consolidate_metadata(s3_store)
  • برای خواندن با Xarray/Dask (در صورت نیاز): xr.open_zarr(..., consolidated=True).
  • Endian و dtype: بسیاری از مصرف‌کنندگان little-endian انتظار دارند. در صورت داشتن دادهٔ big-endian:
# اطمینان از little-endian
import numpy as np
arr = arr.byteswap().newbyteorder('<') if arr.dtype.byteorder == '>' else arr

مثال واقعی: دادهٔ آب‌وهوا (زمان × عرض × طول)

هدف: ذخیرهٔ دمای روزانهٔ سطح زمین با ابعاد (time=365, lat=1800, lon=3600)، chunk بهینه برای دسترسی زمانی و برش مکانی.

import numpy as np
import zarr
from numcodecs import Blosc

comp = Blosc(cname='zstd', clevel=5, shuffle=Blosc.BITSHUFFLE)
root = zarr.open_group('climate.zarr', mode='w')
T = root.create_dataset(
    'tas', shape=(365, 1800, 3600), chunks=(1, 256, 256), dtype='f4',
    compressor=comp, dimension_separator='/'
)
T.attrs.update({'units': 'K', 'long_name': 'near-surface air temperature'})

# فرض: داده را قطعه‌قطعه تولید/خوانده و می‌نویسیم
for t in range(365):
    # اینجا دادهٔ واقعی را بارگذاری/محاسبه کنید
    block = np.random.random((1, 256, 256)).astype('f4')
    # کاشی‌های مکانی را به‌صورت موزاییکی بنویسید
    for y0 in range(0, 1800, 256):
        for x0 in range(0, 3600, 256):
            T[t:t+1, y0:y0+256, x0:x0+256] = block  # نمونهٔ ساده

ادغام با Dask و Xarray

  • Dask می‌تواند Zarr را به‌صورت تنبل (lazy) بخواند و عملیات را موازی کند:
import dask.array as da
x = da.from_zarr('climate.zarr/tas')  # یا از شیٔ zarr.Array
print(x.mean(axis=0).compute())
  • Xarray خواندن/نوشتن Datasetهای چندمتغیره را ساده می‌کند:
import xarray as xr
# خواندن از S3 با متادیتای یکپارچه
# ds = xr.open_zarr(s3_store, consolidated=True)
# نوشتن از Xarray به Zarr (مثال):
# ds.to_zarr('out.zarr', mode='w', consolidated=True)

اگر بعداً به تغییر chunking نیاز داشتید، ابزارهایی مانند rechunker می‌توانند آرایه‌ها را به طرح chunk جدید تبدیل کنند بدون بارگذاری کامل در حافظه.

عیب‌یابی و خطاهای رایج

  • کارایی ضعیف روی S3: chunk خیلی کوچک، فقدان consolidated metadata، عدم استفاده از Cache. راه‌حل: chunk بزرگ‌تر (۱–۱۰MB فشرده)، اجرای consolidate_metadata، LRUStoreCache.
  • KeyError یا فقدان .zarray: مسیر نادرست یا بازکردن group به‌جای array. راه‌حل: از open_group استفاده کنید و مسیر array را بررسی کنید.
  • ناسازگاری endianness: خطا یا نتایج نادرست در برخی خوانندگان. راه‌حل: تبدیل به little-endian پیش از نوشتن.
  • Race condition در نوشتن: نوشتن هم‌زمان روی یک chunk/متادیتا. راه‌حل: تقسیم کار روی chunkهای متفاوت، استفاده از Synchronizer.
  • تعداد فایل/شیء بسیار زیاد: انتخاب chunk نامناسب. راه‌حل: افزایش اندازهٔ chunk، ادغام داده‌های کوچک، استفاده از backend مناسب.
  • 403/AccessDenied روی S3: مجوز ناکافی. راه‌حل: پیکربندی اعتبارنامه‌ها و سیاست‌های bucket.

تغییر dtype یا chunks یک آرایهٔ موجود به‌صورت درجا پشتیبانی نمی‌شود. برای تغییر schema، آرایهٔ جدید بسازید و داده را منتقل کنید.

جمع‌بندی

Zarr با معماری chunkمحور و فشرده‌سازی شفاف، پلی میان داده‌های عظیم و محاسبات کارآمد می‌سازد. با انتخاب درست backend، طراحی سنجیدهٔ chunking و مدیریت دقیق متادیتا، می‌توانید جریان‌کار قابل‌اعتماد و مقیاس‌پذیری برای علم داده و HPC ایجاد کنید.

  • الگوی دسترسی را مستند و بر اساس آن chunks را طراحی کنید.
  • فشرده‌ساز/فیلترها را با سنجش کارایی روی نمونهٔ واقعی انتخاب کنید.
  • در cloud: از FSStore، dimension_separator='/' و consolidate_metadata استفاده کنید.
  • Attributes را برای خود-توصیف‌گری کامل پر کنید.
  • برای نوشتن موازی امن، از Synchronizer و تقسیم کار chunk-aware بهره ببرید.
  • برای خواندن تحلیلی، ادغام با Dask/Xarray را مدنظر قرار دهید.