راهنمای جامع Ceph
Englishشروع کنید
در این صفحه
لایه‌های پایینی · فصل ۲۳

لایه‌های پایینی (۱): دستگاه بلوکی، ‏bufferlist‏، ‏mempool‏ و ‏crc32c‏

۳۶ دقیقه مطالعهویرایش این صفحه
حوصله ندارم این فصل در چند خط
  • ‏KernelDevice‏ دستگاه بلوکی را با ‏O_DIRECT‏ باز می‌کند و ‏I/O‏ را با ‏libaio‏ یا ‏io_uring‏ می‌فرستد؛ ‏offset‏، طول و بافر باید با اندازه‌ی بلوک هم‌تراز باشند.
  • ‏flush‏ به ‏fdatasync‏ ختم می‌شود و ‏discard‏ می‌تواند همگام یا ناهمگام باشد؛ ‏NVMEDevice‏ در ‏SPDK‏ هسته را کاملاً دور می‌زند.
  • ‏bufferlist‏ فهرستی از اشاره‌گرهای بافر با شمارش ارجاع است؛ ‏c_str()‏ روی فهرست چندتکه بی‌صدا همه را در یک بافر کپی می‌کند.
  • ‏mempool‏ها حافظه را به تفکیک زیرسیستم حساب می‌کنند و ‏BlueStore‏ به‌طور پیش‌فرض بلوک‌ها را با ‏crc32c‏ بررسی می‌کند.

۲۳-۱ جایگاه این بخش#

بخش «لایه‌های پایینی» از جایی شروع می‌شود که فصل‌های قبل متوقف شدند: پس از اینکه ‏BlueStore‏ تصمیم گرفت «این بایت‌ها را در این ‏offset‏ بنویس»، چه اتفاقی می‌افتد؟ این فصل مسیر را از ‏bufferlist‏ در حافظه‌ی ‏ceph-osd‏ تا فرمان ‏DMA‏ روی دستگاه دنبال می‌کند. فصل‌های بعدی همین کار را برای قالب روی دیسک ‏BlueStore‏، ‏RocksDB‏، ‏msgr2‏، ریاضیات ‏erasure code‏، ‏CRUSH‏ در سطح بیت و کلاینت‌های هسته انجام می‌دهند. همه‌ی نام‌ها با شاخه‌ی ‏main‏ مخزن ‏ceph/ceph‏ تطبیق داده شده‌اند.

در این فصل
  • ‏KernelDevice‏: باز کردن دستگاه با ‏O_DIRECT‏، قفل، ‏write hint‏، ‏discard‏ و ‏flush‏
  • ‏libaio‏ در برابر ‏io_uring‏ و مسیر ‏aio_submit‏ تا ‏callback‏ تکمیل
  • هم‌ترازی و ‏bdev_block_size‏
  • ساختار درونی ‏bufferlist‏، ‏ptr‏ و ‏raw‏، کپی صفر و ‏mempool‏
  • ‏crc32c‏: انتخاب پیاده‌سازی سخت‌افزاری و یک مثال عددی

۲۳-۲ ‏KernelDevice‏: پنجره‌ی ‏BlueStore‏ به دیسک#

‏src/blk/BlockDevice.h‏ رابط انتزاعی ‏BlockDevice‏ را تعریف می‌کند و ‏BlockDevice.cc‏ نوع آن را انتخاب می‌کند. در شاخه‌ی ‏main‏ فقط دو نوع وجود دارد: ‏aio‏ (کلاس ‏KernelDevice‏ در ‏src/blk/kernel/‏) و ‏spdk‏ (کلاس ‏NVMEDevice‏ در ‏src/blk/spdk/‏). پیاده‌سازی ‏PMEM‏ که در نسخه‌های قدیمی‌تر وجود داشت، در ‏main‏ دیگر در ‏block_device_t‏ نیست.

شکل ۲۳-۱: مسیر یک نوشتن از BlueStore تا دیسک. در فضای کاربر، BlueStore یک bufferlist هم‌تراز با بلوک می‌سازد و از طریق IOContext فهرستی از aio_t (offset، iovec و write hint ) جمع می‌کند. KernelDevice::aio_submit آن‌ها را دسته‌ای به صف aio می‌دهد. در مرز syscall ، libaio از io_submit و io_uring از io_uring_prep_writev و io_uring_submit استفاده می‌کند، روی fd ی که با O_RDWR و O_DIRECT باز شده است (برای هر write hint یک fd جدا). در هسته، bio به request در blk-mq تبدیل و درایور NVMe یا SCSI داده را با DMA مستقیم از صفحه‌های حافظه‌ی کاربر منتقل می‌کند. تکمیل از طریق io_getevents یا CQE به _ aio_thread می‌رسد و او aio_callback را صدا می‌زند
شکل ۲۳-۱: مسیر یک نوشتن از ‏BlueStore‏ تا دیسک. در فضای کاربر، ‏BlueStore‏ یک ‏bufferlist‏ هم‌تراز با بلوک می‌سازد و از طریق ‏IOContext‏ فهرستی از ‏aio_t‏ (offset، ‏iovec‏ و ‏write hint‏) جمع می‌کند. ‏KernelDevice::aio_submit‏ آن‌ها را دسته‌ای به صف ‏aio‏ می‌دهد. در مرز ‏syscall‏، ‏libaio‏ از ‏io_submit‏ و ‏io_uring‏ از ‏io_uring_prep_writev‏ و ‏io_uring_submit‏ استفاده می‌کند، روی ‏fd‏ی که با ‏O_RDWR‏ و ‏O_DIRECT‏ باز شده است (برای هر ‏write hint‏ یک ‏fd‏ جدا). در هسته، ‏bio‏ به ‏request‏ در ‏blk-mq‏ تبدیل و درایور ‏NVMe‏ یا ‏SCSI‏ داده را با ‏DMA‏ مستقیم از صفحه‌های حافظه‌ی کاربر منتقل می‌کند. تکمیل از طریق ‏io_getevents‏ یا ‏CQE‏ به _‏aio_thread‏ می‌رسد و او ‏aio_callback‏ را صدا می‌زند

۲۳-۲-۱ باز کردن دستگاه#

‏KernelDevice::open()‏ برای هر مقدار ‏write hint‏ از ‏WRITE_LIFE_NOT_SET‏ تا ‏WRITE_LIFE_MAX‏ دو ‏fd‏ باز می‌کند: یکی در ‏fd_directs‏ با ‏O_RDWR | O_DIRECT‏ و دیگری در ‏fd_buffereds‏ بدون ‏O_DIRECT‏ برای مسیرهای ‏buffered‏ (مثلاً خواندن‌های بزرگ ‏BlueFS‏). اگر هسته از ‏F_SET_FILE_RW_HINT‏ پشتیبانی کند، روی هر ‏fd hint‏ مربوطه با ‏fcntl‏ تنظیم می‌شود تا ‏SSD‏های دارای ‏streams‏ بتوانند داده‌های با طول عمر متفاوت را جدا نگه دارند. روی ‏fd buffered‏ هم ‏posix_fadvise(..., POSIX_FADV_RANDOM)‏ تنظیم می‌شود.

قفل انحصاری: ‏KernelDevice::_lock()‏ ابتدا قفل رکورد (‏struct flock‏ با ‏F_WRLCK‏) و در صورت نیاز ‏flock(fd, LOCK_EX | LOCK_NB)‏ را امتحان می‌کند، با حداکثر ‏bdev_flock_retry=3‏ بار تلاش دوباره. این قفل مانع می‌شود که دو فرایند (مثلاً ‏OSD‏ در حال اجرا و یک ‏ceph-bluestore-tool‏ اشتباهی) هم‌زمان روی یک دستگاه بنویسند.

text
pseudocode: KernelDevice::open (خلاصه‌ی این کتاب)
  for hint in WRITE_LIFE_NOT_SET .. WRITE_LIFE_MAX-1:
      fd_directs[hint]   = open(path, O_RDWR | O_DIRECT | O_CLOEXEC)
      fd_buffereds[hint] = open(path, O_RDWR | O_CLOEXEC)
      if F_SET_FILE_RW_HINT: fcntl(fd, F_SET_FILE_RW_HINT, &hint)
  _lock()                                   # flock/F_WRLCK، تا 3 بار retry
  fstat → size، block_size = bdev_block_size (4K)؛ اگر st_blksize فرق کند فقط هشدار
  detect rotational، discard support، NUMA node، device model/serial
  if bdev_aio: io_queue = io_uring (اگر bdev_ioring=true و پشتیبانی شود) یا libaio
  start _aio_thread ؛ start discard threads (bdev_async_discard_threads)

۲۳-۲-۲ هم‌ترازی#

با ‏O_DIRECT‏، هسته داده را مستقیم از صفحه‌های کاربر ‏DMA‏ می‌کند. بنابراین ‏offset‏، طول و آدرس بافر باید با اندازه‌ی بلوک منطقی دستگاه هم‌تراز باشند. ‏Ceph‏ این را با ‏bdev_block_size=4_K‏ و ‏assert‏ در مسیرهای نوشتن تضمین می‌کند. بافرهای خواندن با ‏bdev_read_buffer_alignment=4_K‏ تخصیص می‌یابند. نتیجه‌ی عملی: ‏BlueStore‏ هرگز کمتر از ۴‏K‏ روی دستگاه نمی‌نویسد. نوشتن ۱۰۰ بایتی کلاینت یا به یک ‏read-modify-write‏ روی یک بلوک ۴‏K‏ تبدیل می‌شود یا (روی ‏HDD‏) به نوشتن ‏deferred‏ در ‏WAL‏ (فصل ۲۴).

⚠ هشدار

دیسک‌هایی که «۴‏K native‏» نیستند ولی بلوک فیزیکی ۴‏K‏ را در قالب ۵۱۲‏e‏ گزارش می‌کنند، با ‏bdev_block_size=4K‏ درست کار می‌کنند. اما دستگاهی با بلوک منطقی بزرگ‌تر از ۴‏K‏ (برخی آرایه‌های سخت‌افزاری) با پیش‌فرض‌ها سازگار نیست و ‏OSD‏ هنگام ‏mkfs‏ یا ‏open‏ خطا می‌دهد. پیش از خرید، مقدار ‏cat /sys/block/<dev>/queue/logical_block_size‏ را بررسی کنید.

۲۳-۲-۳ ‏libaio‏ یا ‏io_uring‏#

گزینهپیش‌فرض (‏main‏)نقش
‏bdev_aio‏‏true‏استفاده از ‏I/O‏ ناهمگام
‏bdev_ioring‏‏false‏استفاده از ‏io_uring‏ به‌جای ‏libaio‏ (اگر هسته و ‏liburing‏ پشتیبانی کنند)
‏bdev_ioring_hipri‏ / ‏bdev_ioring_sqthread_poll‏‏false‏ / ‏false‏‏poll‏ سخت‌افزاری و ‏thread‏ ارسال هسته
‏bdev_aio_max_queue_depth‏۱۰۲۴عمق صف ‏aio‏
‏bdev_aio_reap_max‏۱۶حداکثر تکمیل‌های برداشته‌شده در هر بار
‏bdev_aio_poll_ms‏۲۵۰مهلت انتظار برای تکمیل
‏bdev_block_size‏۴_‏K‏اندازه‌ی بلوک فرضی
‏bdev_enable_discard‏‏false‏ارسال ‏discard/TRIM‏ پس از آزادسازی فضا
‏bdev_async_discard_threads‏۰‏discard‏ ناهمگام (جایگزین گزینه‌ی قدیمی ‏bdev_async_discard‏)
‏bdev_flock_retry‏۳تلاش دوباره برای قفل

در ‏libaio‏ (‏src/blk/aio/aio.cc‏)، ‏aio_queue_t::submit_batch‏ آرایه‌ای از ‏iocb‏ها را با ‏io_submit‏ ارسال می‌کند. فیلد ‏iocb‏ باید نخستین عضو ‏aio_t‏ باشد تا اشاره‌گر برگشتی از هسته مستقیم به ‏aio_t‏ ‏cast‏ شود (توضیح خود کد در ‏aio.h‏). در صورت ‏EAGAIN‏، ارسال با تأخیر کوتاه تکرار می‌شود. ‏get_next_completed‏ با ‏io_getevents‏ منتظر تکمیل می‌ماند. در ‏io_uring‏ (‏src/blk/kernel/io_uring.cc‏) همان رابط با ‏io_uring_prep_writev‏، ‏io_uring_prep_readv‏ و ‏io_uring_submit‏ پیاده شده است.

شکل ۲۳-۲: توالی یک دسته I/O. thread تراکنش فقط aio_write را در IOContext ثبت می‌کند و سپس aio_submit همه را دسته‌ای (تا عمق ۱۰۲۴) ارسال می‌کند. thread مستقل _ aio_thread در حلقه‌ای با مهلت ۲۵۰ میلی‌ثانیه حداکثر ۱۶ تکمیل را برمی‌دارد و شمارنده‌ی num_running هر IOContext را کم می‌کند. وقتی این شمارنده به صفر برسد، aio_callback با priv همان تراکنش فراخوانی می‌شود و BlueStore آن را از AIO_WAIT به IO_DONE و سپس KV_QUEUED می‌برد
شکل ۲۳-۲: توالی یک دسته ‏I/O. thread‏ تراکنش فقط ‏aio_write‏ را در ‏IOContext‏ ثبت می‌کند و سپس ‏aio_submit‏ همه را دسته‌ای (تا عمق ۱۰۲۴) ارسال می‌کند. ‏thread‏ مستقل _‏aio_thread‏ در حلقه‌ای با مهلت ۲۵۰ میلی‌ثانیه حداکثر ۱۶ تکمیل را برمی‌دارد و شمارنده‌ی ‏num_running‏ هر ‏IOContext‏ را کم می‌کند. وقتی این شمارنده به صفر برسد، ‏aio_callback‏ با ‏priv‏ همان تراکنش فراخوانی می‌شود و ‏BlueStore‏ آن را از ‏AIO_WAIT‏ به ‏IO_DONE‏ و سپس ‏KV_QUEUED‏ می‌برد
cpp
// from ceph/ceph, LGPL — src/blk/kernel/KernelDevice.cc, _aio_thread() (excerpt)
	if (--ioc->num_running == 0) {
	    aio_callback(aio_callback_priv, ioc->priv);

۲۳-۲-۴ ‏flush‏ و ‏discard‏#

‏KernelDevice::flush()‏ در انتها ‏fdatasync(fd_directs[WRITE_LIFE_NOT_SET])‏ را اجرا می‌کند تا کش نوشتن دستگاه تخلیه شود. در ‏BlueStore::_kv_sync_thread‏ منطقی به نام ‏force_flush‏ وجود دارد: اگر داده و ‏DB‏ روی یک دستگاه باشند، ‏commit‏ لاگ ‏BlueFS‏ خودش دستگاه را ‏flush‏ می‌کند و ‏flush‏ جداگانه لازم نیست. در غیر این صورت (یا وقتی چیزی برای ‏commit‏ در ‏RocksDB‏ نیست)، ‏flush‏ دستگاه داده اجباری می‌شود تا داده‌ی ‏aio‏ پیش از ثبت فراداده پایدار باشد. ‏discard‏ فقط وقتی ارسال می‌شود که دستگاه آن را پشتیبانی کند و ‏bdev_enable_discard=true‏ باشد. با ‏bdev_async_discard_threads>0‏، ‏discard‏ در ‏thread‏های جدا و با سقف صف ‏bdev_async_discard_max_pending‏ انجام می‌شود تا تأخیر نوشتن را بالا نبرد.

◆ عمیق‌تر

چرا ‏discard‏ پیش‌فرض خاموش است: روی بسیاری از ‏SSD‏های ‏SATA‏، فرمان ‏TRIM‏ هم‌گام و کند است و می‌تواند صف دستگاه را چند میلی‌ثانیه مسدود کند. ‏SSD‏های امروزی ‏NVMe‏ معمولاً بدون ‏discard‏ هم عملکرد خوبی دارند، چون ‏over-provisioning‏ داخلی دارند. اگر ‏discard‏ را فعال می‌کنید، آن را ناهمگام کنید و تأخیر ‏p99‏ را قبل و بعد بسنجید.

۲۳-۲-۵ ‏SPDK‏#

‏NVMEDevice‏ در ‏src/blk/spdk/‏ دستگاه ‏NVMe‏ را کاملاً از هسته جدا می‌کند: درایور فضای کاربر ‏SPDK‏ صف‌های ‏NVMe‏ را مستقیم با ‏polling‏ مدیریت می‌کند و هیچ ‏syscall‏ و وقفه‌ای در مسیر داده نیست. مسیر دستگاه با پیشوند ‏spdk:‏ در ‏bluestore_block_path‏ مشخص می‌شود. هزینه‌ی آن، اختصاص هسته‌ی ‏CPU‏ به ‏polling‏، نیاز به ‏hugepage‏ و ‏vfio‏، و از دست رفتن ابزارهای معمول هسته (‏iostat‏، ‏blktrace‏) است. در عمل بیشتر استقرارها همان ‏KernelDevice‏ را ترجیح می‌دهند و ‏crimson‏ با ‏SeaStore‏ مسیر دیگری برای حذف سربار هسته دنبال می‌کند.

✚ عیب‌یابی

دیدن مسیر واقعی ‏I/O‏:

bashReef+
ceph osd metadata 4 | jq '{bluestore_bdev_type, bluestore_bdev_rotational, bluestore_bdev_dev_node}'
cat /sys/block/nvme0n1/queue/{logical_block_size,physical_block_size,write_cache}
ceph config get osd.4 bdev_ioring
strace -f -e trace=io_submit,io_getevents,io_uring_enter -p $(pgrep -f 'ceph-osd.*--id 4') 2>&1 | head
blktrace -d /dev/nvme0n1 -o - | blkparse -i - | head -30   # Q/G/D/C برای هر request

۲۳-۳ ‏bufferlist‏: ساختار داده‌ی اصلی ‏Ceph‏#

تقریباً هر بایتی که در ‏Ceph‏ جابه‌جا می‌شود، چه پیام شبکه، چه داده‌ی شیء، چه رکورد ‏RocksDB‏، داخل یک ‏ceph::buffer::list‏ (معمولاً با نام کوتاه ‏bufferlist‏) است. طراحی آن سه هدف دارد: کپی صفر (اتصال تکه‌ها بدون جابه‌جایی بایت‌ها)، اشتراک (چند لیست به یک حافظه اشاره کنند) و حسابداری حافظه (هر تکه به یک ‏mempool‏ تعلق دارد).

شکل ۲۳-۳: سه لایه‌ی bufferlist . در بالا، list یک فهرست پیوندی از ptr_node ها در _ buffers نگه می‌دارد، به‌همراه _ carriage (اشاره به آخرین ptr که append سریع روی آن انجام می‌شود)، _ len (کل بایت‌ها) و _ num (تعداد ptr ها). در میانه، هر ptr فقط یک پنجره است: اشاره‌گر به یک raw همراه _ off و _ len . در پایین، raw مالک حافظه‌ی واقعی است و data ، len ، شمارنده‌ی ارجاع اتمیک nref ، شناسه‌ی mempool و کش آخرین crc را نگه می‌دارد. چند ptr در چند list می‌توانند به یک raw اشاره کنند؛ کپی یک bufferlist فقط nref را افزایش می‌دهد
شکل ۲۳-۳: سه لایه‌ی ‏bufferlist‏. در بالا، ‏list‏ یک فهرست پیوندی از ‏ptr_node‏ها در _‏buffers‏ نگه می‌دارد، به‌همراه _‏carriage‏ (اشاره به آخرین ‏ptr‏ که ‏append‏ سریع روی آن انجام می‌شود)، _‏len‏ (کل بایت‌ها) و _‏num‏ (تعداد ‏ptr‏ها). در میانه، هر ‏ptr‏ فقط یک پنجره است: اشاره‌گر به یک ‏raw‏ همراه _‏off‏ و _‏len‏. در پایین، ‏raw‏ مالک حافظه‌ی واقعی است و ‏data‏، ‏len‏، شمارنده‌ی ارجاع اتمیک ‏nref‏، شناسه‌ی ‏mempool‏ و کش آخرین ‏crc‏ را نگه می‌دارد. چند ‏ptr‏ در چند ‏list‏ می‌توانند به یک ‏raw‏ اشاره کنند؛ کپی یک ‏bufferlist‏ فقط ‏nref‏ را افزایش می‌دهد
نوعفایلنکته
‏buffer::ptr‏‏include/buffer.h‏فیلدهای ‏_raw‏، ‏_off‏، ‏_len‏؛ متدهای ‏c_str()‏، ‏length()‏، ‏is_aligned()‏، ‏is_page_aligned()‏
‏buffer::list‏‏include/buffer.h‏‏_buffers‏، ‏_carriage‏، ‏_len‏، ‏_num‏؛ ‏append‏، ‏claim_append‏، ‏splice‏، ‏rebuild‏، ‏c_str()‏
‏buffer::raw‏‏include/buffer_raw.h‏‏data‏، ‏len‏، ‏nref‏، ‏mempool‏، ‏last_crc_offset‏/‏last_crc_val‏ با ‏crc_spinlock‏
پیاده‌سازی‌های ‏raw‏‏common/buffer.cc‏‏create_aligned‏، ‏create_small_page_aligned‏، ‏claim_char‏، ‏claim_malloc‏، ‏claim_buffer‏ (با ‏deleter‏ دلخواه)، ‏raw_hack_aligned‏

۲۳-۳-۱ هزینه‌ی واقعی ‏c_str‏()#

‏list::c_str()‏ اشاره‌گری به یک ناحیه‌ی پیوسته برمی‌گرداند. اگر لیست از چند ‏ptr‏ تشکیل شده باشد، ابتدا ‏rebuild()‏ اجرا می‌شود که یک ‏raw‏ تازه می‌سازد و همه‌ی تکه‌ها را در آن کپی می‌کند. بنابراین یک فراخوانی ظاهراً بی‌خطر می‌تواند یک کپی چند مگابایتی ایجاد کند. کد مسیر داده برای همین از ‏iterator‏ها (‏begin()‏، ‏copy()‏) یا از ‏rebuild_aligned_size_and_memory‏ (برای ‏O_DIRECT‏) استفاده می‌کند.

text
pseudocode: مسیر بدون کپی یک نوشتن (خلاصه‌ی این کتاب)
  msgr2 دریافت: segment داده در یک raw تازه (با هم‌ترازی اعلام‌شده در preamble) خوانده می‌شود
  MOSDOp::decode: ptr روی همان raw (بدون کپی) → OSDOp.indata
  PrimaryLogPG: t.write(oid, off, len, indata)                          # nref++
  BlueStore::_do_write: اگر هم‌تراز → همان ptrها به iovec تبدیل می‌شوند
                         اگر نه → pad با صفر/داده‌ی قبلی + rebuild_aligned (یک کپی)
  KernelDevice::aio_write: iovec[] → io_prep_pwritev (DMA از همان صفحه‌ها)
◆ عمیق‌تر

‏crc‏ کش‌شده روی ‏raw‏: ‏buffer::list::crc32c()‏ برای هر ‏ptr‏ با ‏raw::get_crc‏ بررسی می‌کند آیا ‏raw‏ برای همان بازه قبلاً ‏crc‏ را محاسبه و با ‏set_crc‏ جفت (‏seed‏ پایه، ‏crc‏) را ذخیره کرده است (‏last_crc_offset‏ و ‏last_crc_val‏). اگر ‏seed‏ فعلی با ‏seed‏ ذخیره‌شده فرق کند، به‌جای خواندن دوباره‌ی داده، اختلاف ‏seed‏ با کمک خطی بودن ‏crc‏ و تابع ‏ceph_crc32c_zeros‏ (‏crc‏ یک ناحیه‌ی صفر به طول بازه) اصلاح می‌شود. هر تغییری در ‏raw‏ (مثلاً ‏copy_in‏ یا ‏zero‏) این کش را با ‏invalidate_crc()‏ باطل می‌کند. این کار برای پیام‌هایی که چند بار با ‏crc‏ ارسال یا بررسی می‌شوند صرفه‌جویی بزرگی است.

۲۳-۴ ‏mempool‏: حسابداری حافظه#

‏src/include/mempool.h‏ با ماکرو ‏DEFINE_MEMORY_POOLS_HELPER‏ فهرست ثابتی از ‏pool‏ها تعریف می‌کند. هر ‏allocator‏ از نوع ‏mempool::<pool>::vector‏، ‏map‏ و... تعداد بایت‌ها و آیتم‌ها را به‌صورت شاردشده (برای کاهش رقابت روی ‏cache line‏) در همان ‏pool‏ ثبت می‌کند. ‏osd_memory_target‏ و ‏autotune‏ کش ‏BlueStore‏ بر اساس همین اعداد تصمیم می‌گیرند.

‏pool‏چه چیزی
‏bluestore_cache_onode‏ / _‏meta‏ / _‏data‏ / _‏buffer‏ / _‏other‏اجزای کش ‏BlueStore‏
‏bluestore_alloc‏ساختارهای ‏allocator‏
‏bluestore_extent‏ / _‏blob‏ / _‏shared_blob‏ / _‏inline_bl‏ساختارهای فراداده‌ی ‏onode‏ در حافظه
‏bluestore_txc‏ / _‏writing‏ / _‏writing_deferred‏تراکنش‌ها و داده‌های در حال نوشتن
‏bluefs‏ / ‏bluefs_file_reader‏ / ‏bluefs_file_writer‏‏BlueFS‏
‏buffer_anon‏ / ‏buffer_meta‏‏buffer‏های بدون ‏pool‏ مشخص و فراداده‌ی آن‌ها
‏osd‏ / ‏osd_mapbl‏ / ‏osd_pglog‏ / ‏osdmap‏ / ‏osdmap_mapping‏ / ‏pgmap‏ساختارهای ‏OSD‏ و نقشه‌ها
bashReef+
ceph tell osd.4 dump_mempools | jq '.mempool.by_pool | to_entries | sort_by(-.value.bytes) | .[:6]'
ceph tell osd.4 dump_mempools | jq '.mempool.total'
ceph config get osd.4 osd_memory_target          # پیش‌فرض 4_G
⚠ هشدار

رشد بی‌رویه‌ی ‏buffer_anon‏ معمولاً به این معنی است که داده‌ی شبکه یا پیام‌ها در جایی نگه داشته شده‌اند (مثلاً صف‌های بزرگ ‏op‏ در یک ‏OSD‏ کند، یا کلاینت‌هایی که پاسخ نمی‌خوانند). رشد ‏osd_pglog‏ به طول بیش از حد ‏pg log‏ اشاره دارد (گزینه‌های ‏osd_min_pg_log_entries‏ و ‏osd_max_pg_log_entries‏).

۲۳-۵ ‏crc32c‏: از دستور ‏CPU‏ تا ‏checksum‏ بلوک#

‏Ceph‏ از ‏crc32c‏ (چندجمله‌ای ‏Castagnoli‏، به شکل معکوس ۰‏x82F63B78‏) هم برای ‏checksum‏ داده در ‏BlueStore‏ و هم برای سلامت پیام در ‏msgr2‏ استفاده می‌کند. ‏ceph_choose_crc32()‏ در ‏common/crc32c.cc‏ یک بار هنگام بارگذاری بهترین پیاده‌سازی را برای ‏CPU‏ انتخاب می‌کند.

شکل ۲۳-۴: زنجیره‌ی انتخاب پیاده‌سازی crc32c و محل‌های استفاده. ترتیب بررسی چنین است: روی x86 ابتدا AVX512 همراه VPCLMULQDQ ، سپس SSE4.2 (با یا بدون PCLMUL برای پردازش موازی سه جریان)، روی aarch64 دستورهای CRC32 و روی ppc ، s390x و riscv نسخه‌های مخصوص. اگر هیچ‌کدام در دسترس نباشد، پیاده‌سازی جدولی نرم‌افزاری sctp استفاده می‌شود. این تابع در BlueStore برای هر chunk داده، در msgr2 حالت crc برای هر segment و preamble ، و در bufferlist با کش crc روی raw به کار می‌رود
شکل ۲۳-۴: زنجیره‌ی انتخاب پیاده‌سازی ‏crc32c‏ و محل‌های استفاده. ترتیب بررسی چنین است: روی ‏x86‏ ابتدا ‏AVX512‏ همراه ‏VPCLMULQDQ‏، سپس ‏SSE4.2‏ (با یا بدون ‏PCLMUL‏ برای پردازش موازی سه جریان)، روی ‏aarch64‏ دستورهای ‏CRC32‏ و روی ‏ppc‏، ‏s390x‏ و ‏riscv‏ نسخه‌های مخصوص. اگر هیچ‌کدام در دسترس نباشد، پیاده‌سازی جدولی نرم‌افزاری ‏sctp‏ استفاده می‌شود. این تابع در ‏BlueStore‏ برای هر ‏chunk‏ داده، در ‏msgr2‏ حالت ‏crc‏ برای هر ‏segment‏ و ‏preamble‏، و در ‏bufferlist‏ با کش ‏crc‏ روی ‏raw‏ به کار می‌رود

۲۳-۵-۱ یک مثال عددی#

‏ceph_crc32c(seed, data, len)‏ بدون معکوس کردن نهایی کار می‌کند و ‏BlueStore‏ با ‏seed‏ برابر ‏-1‏ (۰‏xFFFFFFFF‏) شروع می‌کند. برای رشته‌ی مرجع ‏"123456789"‏:

ورودی‏seed‏خروجی ‏ceph_crc32c‏پس از ‏XOR‏ نهایی با ۰‏xFFFFFFFF‏
‏"123456789"‏ (۹ بایت)۰‏xFFFFFFFF‏۰‏x1CF96D7C‏۰‏xE3069283‏ (مقدار مرجع استاندارد ‏CRC-32C‏)
۴۰۹۶ بایت صفر۰‏xFFFFFFFF‏۰‏x6706BE76‏—
‏"ceph"‏۰۰‏x575990D6‏—
python
# مثال خود این راهنما: crc32c بیت‌به‌بیت (کند، فقط برای فهم)
def crc32c(crc, data):
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ (0x82F63B78 if crc & 1 else 0)
    return crc
assert crc32c(0xFFFFFFFF, b"123456789") == 0x1CF96D7C

نسخه‌ی سخت‌افزاری همین کار را با دستور ‏crc32‏ در ‏x86‏ (۸ بایت در هر دستور) و با سه جریان موازی ترکیب‌شده با ‏PCLMULQDQ‏ انجام می‌دهد. روی پردازنده‌های امروزی سرعت آن چند ده گیگابایت در ثانیه در هر هسته است، بنابراین ‏checksum‏ در عمل تقریباً رایگان است.

۲۳-۵-۲ ‏checksum‏ در ‏BlueStore‏#

‏Checksummer.h‏ انواع ‏CSUM_XXHASH32=2‏، ‏CSUM_XXHASH64=3‏، ‏CSUM_CRC32C=4‏، ‏CSUM_CRC32C_16=5‏ (۱۶ بیت پایین) و ‏CSUM_CRC32C_8=6‏ را تعریف می‌کند و پیش‌فرض ‏bluestore_csum_type=crc32c‏ است. هر ‏blob‏ (فصل ۲۴) فیلدهای ‏csum_type‏، ‏csum_chunk_order‏ (اندازه‌ی ‏chunk‏ برابر ‏1 << order‏ بایت) و ‏csum_data‏ (آرایه‌ی ‏checksum‏ها) دارد.

حساب سربار: با ‏chunk‏ ۴‏K‏ و ‏crc32c‏، هر ۴۰۹۶ بایت داده ۴ بایت ‏checksum‏ دارد، یعنی حدود ۰.۱٪. برای یک ‏OSD‏ با ۱۰ ‏TiB‏ داده، این حدود ۱۰ ‏GiB checksum‏ در ‏onode‏ها است که بخشی از آن در کش ‏onode‏ قرار می‌گیرد. اگر ‏chunk‏ را بزرگ‌تر کنید (‏blob‏های بزرگ و خواندن‌های ترتیبی، مثل ‏RGW‏)، این سربار کم می‌شود ولی هر خواندن کوچک مجبور است کل ‏chunk‏ را بخواند تا آن را بررسی کند.

۲۳-۶ ‏libaio‏ و ‏io_uring‏ در سطح ساختار و قفل#

بخش ‏KernelDevice‏ مسیر کلی را نشان داد. اینجا همان مسیر در سطح بایت‌ها، قفل‌ها و ‏thread‏ها دنبال می‌شود. ‏KernelDevice‏ در ‏KernelDevice::KernelDevice‏ بر اساس ‏bdev_ioring‏ (پیش‌فرض ‏false‏) یکی از دو صف را می‌سازد: ‏aio_queue_t‏ (‏libaio‏) یا ‏ioring_queue_t‏ (‏io_uring‏). عمق صف در هر دو حالت ‏bdev_aio_max_queue_depth‏ (پیش‌فرض ۱۰۲۴) است.

شکل ۲۳-۵: ساختار iocb شصت و چهار بایتی که aio_t در Ceph برای هر I/O پر می‌کند. نمونه یک pwritev با سه iovec و در مجموع ۱۲ KiB در آفست ۰ x5A000 است. aio_data اشاره‌گر خود aio_t است و در io_event برمی‌گردد. با این اشاره‌گر، thread برداشت بدون جست‌وجو می‌فهمد کدام IOContext را باید کامل کند. در فرمان‌های برداری، aio_nbytes تعداد iovec هاست، نه طول بایتی
شکل ۲۳-۵: ساختار ‏iocb‏ شصت و چهار بایتی که ‏aio_t‏ در ‏Ceph‏ برای هر ‏I/O‏ پر می‌کند. نمونه یک ‏pwritev‏ با سه ‏iovec‏ و در مجموع ۱۲ ‏KiB‏ در آفست ۰‏x5A000‏ است. ‏aio_data‏ اشاره‌گر خود ‏aio_t‏ است و در ‏io_event‏ برمی‌گردد. با این اشاره‌گر، ‏thread‏ برداشت بدون جست‌وجو می‌فهمد کدام ‏IOContext‏ را باید کامل کند. در فرمان‌های برداری، ‏aio_nbytes‏ تعداد ‏iovec‏هاست، نه طول بایتی
∑ الگوریتم: ارسال دسته‌ای ‏libaio‏ با عقب‌نشینی نمایی

منبع در کد: src/blk/aio/aio.cc :: aio_queue_t::submit_batch

پیچیدگی: ‏O‏(N/max_iodepth) فراخوانی ‏io_submit‏ در حالت عادی؛ در بدترین حالت ۱۶ تلاش و حدود ۸.۲ ثانیه انتظار تجمعی

text
pseudocode
attempts = bdev_aio_submit_retry_max (16) ; delay = 125 µs
while کار باقی است:
    piocb[] را تا max_iodepth از فهرست پر کن
    r = io_submit(ctx, pulled − pushed, piocb + pushed)
    if 0 ≤ r < toSubmit: pushed += r ; r = −EAGAIN      # پذیرش جزئی
    if r == −EAGAIN and attempts−− > 0:
        usleep(delay) ; delay ×= 2 ; retries++ ; continue
    if r < 0: return r
    pushed = pulled = 0 ; attempts و delay را بازنشانی کن

مثال عددی. با ۱۰۲۴ درخواست معلق در صف هسته و یک دسته‌ی ۶۴ تایی، ‏io_submit‏ ممکن است فقط ۱۰ درخواست را بپذیرد. در این حالت ‏pushed‏ = ۱۰، ‏r‏ به −‏EAGAIN‏ تبدیل می‌شود و پس از ۱۲۵ µ‏s‏ تلاش دوباره انجام می‌شود. اگر صف همچنان پر بماند، انتظارها ۱۲۵، ۲۵۰، ۵۰۰ µ‏s‏ و همین‌طور دوبرابر می‌شوند تا ۴.۱ ثانیه برای تلاش شانزدهم. جمع کل ‏125×(216−1) μs≈8.19125 \times (2^{16} - 1)\,\mu\mathrm{s} \approx 8.19‏ ثانیه است. شمارنده‌ی ‏retries‏ در لاگ «‏aio_submit retries‏» دیده می‌شود. اگر این عدد در لاگ ‏OSD‏ زیاد دیده شود، عمق صف یا تأخیر دستگاه مشکل دارد، نه ‏CPU‏.

شکل ۲۳-۶: انتظار پیش از هر تلاش دوباره (میله‌ها، مقیاس لگاریتمی) و انتظار تجمعی (خط قرمز) وقتی io_submit پیوسته EAGAIN برمی‌گرداند. پس از ۸ تلاش فقط ۳۲ میلی‌ثانیه گذشته است، اما ۸ تلاش آخر بیش از ۸ ثانیه طول می‌کشند. پس یک اشباع کوتاه تقریباً نامرئی است و فقط اشباع طولانی به slow op تبدیل می‌شود
شکل ۲۳-۶: انتظار پیش از هر تلاش دوباره (میله‌ها، مقیاس لگاریتمی) و انتظار تجمعی (خط قرمز) وقتی ‏io_submit‏ پیوسته ‏EAGAIN‏ برمی‌گرداند. پس از ۸ تلاش فقط ۳۲ میلی‌ثانیه گذشته است، اما ۸ تلاش آخر بیش از ۸ ثانیه طول می‌کشند. پس یک اشباع کوتاه تقریباً نامرئی است و فقط اشباع طولانی به ‏slow op‏ تبدیل می‌شود
شکل ۲۳-۷: ورودی صف ارسال io_uring که init_sqe در Ceph برای همان pwritev می‌سازد. opcode برابر IORING_OP_WRITEV (۲) است. پرچم IOSQE_FIXED_FILE یعنی fd اندیسی در جدول فایل‌های ثبت‌شده است و هسته هزینه‌ی fget/fput را در هر I/O نمی‌پردازد. user_data همان اشاره‌گر aio_t است و در CQE شانزده بایتی برمی‌گردد
شکل ۲۳-۷: ورودی صف ارسال ‏io_uring‏ که ‏init_sqe‏ در ‏Ceph‏ برای همان ‏pwritev‏ می‌سازد. ‏opcode‏ برابر ‏IORING_OP_WRITEV‏ (۲) است. پرچم ‏IOSQE_FIXED_FILE‏ یعنی ‏fd‏ اندیسی در جدول فایل‌های ثبت‌شده است و هسته هزینه‌ی ‏fget/fput‏ را در هر ‏I/O‏ نمی‌پردازد. ‏user_data‏ همان اشاره‌گر ‏aio_t‏ است و در ‏CQE‏ شانزده بایتی برمی‌گردد
∑ الگوریتم: ارسال و برداشت ‏io_uring‏ با دو قفل و ‏epoll‏

منبع در کد: src/blk/kernel/io_uring.cc :: ioring_queue_t::submit_batch، ioring_queue، ioring_get_cqe، get_next_completed

پیچیدگی: ارسال ‏O‏(N) نوشتن ‏SQE‏ و یک ‏io_uring_submit‏؛ برداشت ‏O(min‏(ready, reap_max)) بدون فراخوانی سیستمی وقتی ‏CQE‏ آماده است

text
pseudocode
submit_batch(beg, end):
    lock(sq_mutex)
    for io in [beg, end): sqe = io_uring_get_sqe() ; if null: break   # حلقه پر است
                          init_sqe(sqe, io)   # prep_writev/readv، FIXED_FILE، user_data=io
    rc = io_uring_submit()        # io_uring_enter، یا فقط به‌روزرسانی tail در SQPOLL
    unlock(sq_mutex) ; return rc
get_next_completed(timeout, paio[], max):
    lock(cq_mutex) ; for each cqe تا max: io.rval = cqe.res ; paio[n++] = io
    io_uring_cq_advance(n) ; unlock(cq_mutex)
    if n == 0: epoll_wait(ring_fd, timeout = bdev_aio_poll_ms) ؛ اگر آماده بود، دوباره برو

مثال عددی و تفاوت با ‏libaio‏. با حلقه‌ی ۱۰۲۴ خانه‌ای، ‏SQ‏ برابر ۱۰۲۴ × ۶۴ = ۶۴ ‏KiB‏ و ‏CQ‏ (که هسته به‌طور پیش‌فرض دو برابر ‏SQ‏ می‌سازد) ۲۰۴۸ × ۱۶ = ۳۲ ‏KiB‏ است. هر دو با ‏mmap‏ بین هسته و ‏Ceph‏ مشترک‌اند. در ‏libaio‏ هر دسته دست‌کم یک ‏io_submit‏ و هر برداشت یک ‏io_getevents‏ دارد. در ‏io_uring‏، وقتی ‏CQE‏ آماده است، برداشت فقط خواندن حافظه‌ی مشترک است. با ‏bdev_ioring_sqthread_poll=true‏ (پرچم ‏IORING_SETUP_SQPOLL‏) ارسال هم فراخوانی سیستمی ندارد، اما یک ‏thread‏ هسته روی یک هسته‌ی ‏CPU‏ می‌چرخد. ‏bdev_ioring_hipri‏ پرچم ‏IORING_SETUP_IOPOLL‏ را می‌گذارد: کامل شدن با ‏polling‏ روی صف ‏NVMe‏ انجام می‌شود، و این فقط برای دستگاه‌هایی که ‏poll queue‏ دارند و با ‏O_DIRECT‏ کار می‌کند.

شکل ۲۳-۸: مسیر کامل io_uring در KernelDevice . ارسال زیر sq_mutex انجام می‌شود، چون چند thread (تراکنش، deferred ، BlueFS ) هم‌زمان I/O می‌فرستند. برداشت در thread aio_thread زیر cq_mutex است. وقتی CQE آماده نیست، به جای مسدود شدن در io_uring_enter ، روی ring_fd با epoll منتظر می‌ماند، و این همان الگوی زمان‌بندی bdev_aio_poll_ms در libaio را حفظ می‌کند
شکل ۲۳-۸: مسیر کامل ‏io_uring‏ در ‏KernelDevice‏. ارسال زیر ‏sq_mutex‏ انجام می‌شود، چون چند ‏thread‏ (تراکنش، ‏deferred‏، ‏BlueFS‏) هم‌زمان ‏I/O‏ می‌فرستند. برداشت در ‏thread aio_thread‏ زیر ‏cq_mutex‏ است. وقتی ‏CQE‏ آماده نیست، به جای مسدود شدن در ‏io_uring_enter‏، روی ‏ring_fd‏ با ‏epoll‏ منتظر می‌ماند، و این همان الگوی زمان‌بندی ‏bdev_aio_poll_ms‏ در ‏libaio‏ را حفظ می‌کند
⚠ هشدار

‏io_uring‏ در ‏Ceph‏ به‌طور پیش‌فرض خاموش است و روی بعضی هسته‌ها و سیاست‌های امنیتی ممکن است غیرفعال یا محدود باشد (مثلاً ‏sysctl‏ ‏kernel.io_uring_disabled‏ در هسته‌های جدیدتر). ‏ioring_queue_t::supported()‏ با ساختن یک حلقه‌ی ۱۶ تایی آزمایشی پشتیبانی را می‌سنجد. اگر شکست بخورد، ‏OSD‏ با هشدار به ‏libaio‏ برمی‌گردد.

۲۳-۷ رشد بافر در ‏bufferlist::append‏#

∑ الگوریتم: رشد نمایی فضای الحاق

منبع در کد: src/common/buffer.cc :: buffer::list::append(const char*, unsigned)، refill_append_space

پیچیدگی: ‏O‏(۱) سرشکن برای هر ‏append‏ کوچک؛ ‏O(log‏(L/۴K)) تخصیص تا سقف ۲۵۶‏K‏ و سپس ‏L/256K‏ تخصیص

text
pseudocode
append(data, len):
    first = min(len, فضای خالی انتهای _carriage)
    memcpy درون همان raw (برای طول ≤ 32 با کپی inline) ; _carriage.len += first
    if len − first > 0: refill_append_space(len − first).append(بقیه)
refill_append_space(len):
    alen = round_up(round_up(len, 8) + S, 4096)          # S = sizeof(raw_combined)
    if _carriage آخرین ptr است:
        alen = max(alen, min(2 × round_up(_carriage.raw_length, 4096), 256 KiB))
    raw_combined::create(alen − S) ← داده و سرآیند در یک تخصیص

مثال عددی. اگر ‏encode‏ یک ساختار ۱ ‏MiB‏ داده را در قطعه‌های کوچک (مثلاً ‏u64‏ و رشته‌های کوتاه) ‏append‏ کند، تخصیص‌ها ۴‏K‏، ۸‏K‏، ۱۶‏K‏، ۳۲‏K‏، ۶۴‏K‏، ۱۲۸‏K‏ و سپس ۲۵۶‏K‏ هستند. پس از شش بافر اول ۲۵۲‏K‏ پر شده است و ۷۷۲‏K‏ باقی‌مانده به چهار بافر ۲۵۶‏K‏ دیگر می‌رود. نتیجه ۱۰ ‏ptr_node‏ است، با جمع ۱۲۷۶ ‏KiB‏ تخصیص برای ۱۰۲۴ ‏KiB‏ داده، به جای ۲۵۶ بافر ۴‏K‏. سقف ۲۵۶‏K‏ عمداً زیر آستانه‌ی «شیء کوچک» ‏tcmalloc‏ است. هر ‏raw_combined‏ سرآیند کنترلی خود را در انتهای همان تخصیص دارد، پس هر بافر دقیقاً مضربی از ۴‏K‏ حافظه از ‏allocator‏ می‌گیرد.

شکل ۲۳-۹: اندازه‌ی ده بافر raw_combined که refill_append_space هنگام append کردن یک مگابایت داده در قطعه‌های کوچک می‌سازد. اندازه‌ها از ۴ K دوبرابر می‌شوند تا به سقف ۲۵۶ K برسند. S اندازه‌ی سرآیند raw_combined است که از هر تخصیص کم می‌شود. فضای بلااستفاده‌ی انتهای بافر آخر دلیل اختلاف ۱۲۷۶ KiB تخصیص و ۱۰۲۴ KiB داده است
شکل ۲۳-۹: اندازه‌ی ده بافر ‏raw_combined‏ که ‏refill_append_space‏ هنگام ‏append‏ کردن یک مگابایت داده در قطعه‌های کوچک می‌سازد. اندازه‌ها از ۴‏K‏ دوبرابر می‌شوند تا به سقف ۲۵۶‏K‏ برسند. ‏S‏ اندازه‌ی سرآیند ‏raw_combined‏ است که از هر تخصیص کم می‌شود. فضای بلااستفاده‌ی انتهای بافر آخر دلیل اختلاف ۱۲۷۶ ‏KiB‏ تخصیص و ۱۰۲۴ ‏KiB‏ داده است
💡 نکته

اگر اندازه‌ی نهایی از پیش معلوم است، ‏bl.reserve(n)‏ یا ‏bl.push_back(buffer::create(n))‏ همه‌ی این تخصیص‌های میانی را حذف می‌کند. در مسیرهای داغ ‏Ceph‏، مثلاً ‏encode‏ پیام‌ها با ‏encode(..., features)‏ و پیش‌برآورد ‏bound_encode‏ در ‏denc‏، از همین روش استفاده می‌شود.

۲۳-۸ ‏crc32c‏ برای صفرها و تطبیق ‏seed‏#

‏ceph_crc32c(crc, NULL, len)‏ در ‏include/crc32c.h‏ قرارداد ویژه‌ای دارد: اگر ‏data‏ تهی و ‏len‏ بزرگ‌تر از ۱۶ باشد، ‏crc‏ یک ناحیه‌ی تماماً صفر حساب می‌شود، آن هم بدون خواندن حافظه. ‏BlueStore‏ و ‏bufferlist‏ از این برای ناحیه‌های صفر (‏buffer::list::append_zero‏، بافرهای ‏raw_zeros‏) و برای تطبیق ‏crc‏ ذخیره‌شده استفاده می‌کنند.

∑ الگوریتم: ‏crc32c‏ صفرها با ماتریس‌های جهش

منبع در کد: src/common/crc32c.cc :: ceph_crc32c_zeros، create_turbo_table (جدول crc_turbo_table[32][32])

پیچیدگی: ‏O‏(log len) ضرب ماتریس ۳۲×۳۲ روی ‏GF‏(۲)، هر ضرب حداکثر ۳۲ ‏XOR‏؛ مستقل از طول (به جای ‏O‏(len))

text
pseudocode
# M[0][bit] = crc(1≪bit، یک بایت صفر) ؛ M[r] = M[r−1]², یعنی جهش 2^r بایت
zeros(crc, len):
    rem = len & 15 ; n = len ≫ 4 ; r = 4                 # از جهش 16 بایتی شروع کن
    while n: if n & 1: crc = Σ_{bit j of crc = 1} M[r][j]   (XOR)
             n ≫= 1 ; r += 1
    if rem: crc = crc_معمولی(crc، rem بایت صفر)

مثال عددی. برای ۴۰۹۶ بایت صفر با ‏seed‏ = ۰‏xFFFFFFFF: rem‏ = ۰ و ‏n=256=28n = 256 = 2^8‏، پس فقط یک ضرب با ‏M[12]‏ لازم است و نتیجه ۰‏x6706BE76‏ است. محاسبه‌ی مستقیم بایت‌به‌بایت (پیاده‌سازی جداگانه‌ی این راهنما) همین عدد را می‌دهد. برای ۶۹۶۳۷ = ۶۵۵۳۶ + ۴۰۹۶ + ۵ بایت: دو ضرب (‏M[16]‏ و ‏M[12]‏) و سپس ۵ بایت معمولی، با نتیجه‌ی ۰‏xAC433F9D‏ که باز با محاسبه‌ی مستقیم یکی است. چهار مقدار اول ردیف ‏M[0]‏ (۰xF۲۶B۸۳۰۳، ۰‏xE13B70F7‏، ۰‏xC79A971F‏، ۰‏x8AD958CF‏) همان مقادیر جدول ‏crc_turbo_table‏ در کد ‏Ceph‏ هستند.

∑ الگوریتم: تطبیق ‏crc‏ کش‌شده با ‏seed‏ تازه

منبع در کد: src/common/buffer.cc :: buffer::list::crc32c؛ include/buffer_raw.h :: raw::get_crc، set_crc

پیچیدگی: ‏O‏(log len) به جای ‏O‏(len) برای هر ‏raw‏ که ‏crc‏ آن کش شده است

text
pseudocode
for node in _buffers:
    if raw.get_crc((off, off+len)) → (v, c):
        if v == crc: crc = c                                  # cache hit
        else: crc = c ⊕ ceph_crc32c(v ⊕ crc, NULL, len)       # adjust
    else: base = crc ; crc = ceph_crc32c(crc, data, len) ; raw.set_crc(..., (base, crc))

مثال عددی. برای یک بافر ۱۰۰۰ بایتی، ‏crc‏ با ‏seed 0‏ برابر ۰‏xACBE2A2C‏ است و در ‏raw‏ کش می‌شود. اکنون همان بافر با ‏seed 0xFFFFFFFF‏ خواسته می‌شود. تطبیق ۰‏xACBE2A2C‏ ⊕ ‏zeros‏(۰ ⊕ ۰‏xFFFFFFFF, 1000‏) = ۰‏x8B0C0F84‏ می‌دهد، که دقیقاً ‏crc‏ مستقیم با ‏seed‏ جدید است. این همان جایی است که پیام‌رسان (‏msgr‏) و ‏BlueStore‏ یک بافر را دو بار با ‏seed‏های متفاوت ‏checksum‏ می‌کنند و بار دوم فقط چند ده ‏XOR‏ هزینه دارد. آمار آن با ‏buffer_track_crc‏ در شمارنده‌های ‏buffer_cached_crc‏ و ‏buffer_cached_crc_adjusted‏ دیده می‌شود.

شکل ۲۳-۱۰: سه کاربرد ماتریس‌های جهش crc32c در Ceph . ردیف بالا الگوریتم ceph_crc32c_zeros است که طول را به توان‌های دو می‌شکند. ردیف میانی مثال ۴۰۹۶ بایت صفر است که با یک ضرب ماتریس حل می‌شود. ردیف پایین تطبیق crc کش‌شده در bufferlist است: به دلیل خطی بودن CRC ، تفاوت دو seed معادل crc یک ناحیه‌ی صفر با seed برابر XOR آن دو است
شکل ۲۳-۱۰: سه کاربرد ماتریس‌های جهش ‏crc32c‏ در ‏Ceph‏. ردیف بالا الگوریتم ‏ceph_crc32c_zeros‏ است که طول را به توان‌های دو می‌شکند. ردیف میانی مثال ۴۰۹۶ بایت صفر است که با یک ضرب ماتریس حل می‌شود. ردیف پایین تطبیق ‏crc‏ کش‌شده در ‏bufferlist‏ است: به دلیل خطی بودن ‏CRC‏، تفاوت دو ‏seed‏ معادل ‏crc‏ یک ناحیه‌ی صفر با ‏seed‏ برابر ‏XOR‏ آن دو است

۲۳-۹ تکمیلی و به‌روز#

  • در ‏main‏، ‏block_device_t‏ فقط ‏aio‏ و ‏spdk‏ را دارد. پشتیبانی ‏PMEM‏ (دستگاه‌های ‏DAX‏) در ‏main‏ حذف شده است، ولی ‏Tentacle‏ هنوز ‏block_device_t::pmem‏ را دارد. پیش از ارتقا، اگر از ‏PMEM‏ استفاده می‌کنید، یادداشت‌های انتشار را بررسی کنید.
  • گزینه‌ی ‏bdev_async_discard‏ با ‏bdev_async_discard_threads‏ جایگزین شده است و کد برای سازگاری، مقدار قدیمی را به یک ‏thread‏ نگاشت می‌کند.
  • ‏io_uring‏ همچنان به‌طور پیش‌فرض خاموش است (‏bdev_ioring=false‏).

۲۳-۱۰ تمرین عملی#

  1. ۱. روی یک ‏OSD‏ آزمایشی با ‏strace -c -f -p <pid>‏ در حین ‏rados bench‏ نسبت ‏io_submit‏ به ‏io_getevents‏ را بسنجید. سپس ‏bdev_ioring=true‏ را تنظیم کنید، ‏OSD‏ را راه‌اندازی دوباره کنید و تفاوت را با ‏ceph osd perf‏ مقایسه کنید.
  2. ۲. تابع پایتون ‏crc32c‏ این فصل را اجرا کنید و نتیجه را برای یک بلوک ۴‏K‏ تصادفی با ‏crcmod‏ یا ‏crc32c‏ پایتون مقایسه کنید (یادتان باشد ‏XOR‏ نهایی را اعمال کنید).
  3. ۳. خروجی ‏dump_mempools‏ را قبل و بعد از یک ‏rados bench‏ بزرگ ذخیره و ۵ ‏pool‏ با بیشترین رشد را شناسایی کنید.
  4. ۴. ‏cat /sys/block/<dev>/queue/write_cache‏ و ‏rotational‏ را برای همه‌ی دستگاه‌های ‏OSD‏ جمع کنید و با ‏ceph osd metadata‏ مقایسه کنید.

۲۳-۱۱ پرسش‌های مرور#

  1. ۱. چرا ‏KernelDevice‏ برای هر ‏write hint‏ یک ‏fd‏ جدا باز می‌کند؟
  2. ۲. هم‌ترازی ‏O_DIRECT‏ چه محدودیتی بر کوچک‌ترین نوشتن ‏BlueStore‏ می‌گذارد؟
  3. ۳. نقش ‏bdev_aio_max_queue_depth‏ و ‏bdev_aio_reap_max‏ چیست؟
  4. ۴. چه زمانی ‏BlueStore flush‏ جداگانه‌ی دستگاه داده را اجباری می‌کند؟
  5. ۵. چرا ‏discard‏ به‌طور پیش‌فرض خاموش است؟
  6. ۶. تفاوت ‏buffer::ptr‏ و ‏buffer::raw‏ چیست؟
  7. ۷. چرا ‏bufferlist::c_str()‏ می‌تواند گران باشد؟
  8. ۸. ‏mempool‏ چگونه به ‏autotune‏ حافظه کمک می‌کند؟
  9. ۹. چرا ‏crc32c‏ مقدار ‏"123456789"‏ در ‏Ceph‏ با مقدار مرجع استاندارد فرق دارد؟
  10. ۱۰. سربار ‏checksum‏ با ‏chunk‏ ۴‏K‏ چقدر است و بزرگ کردن ‏chunk‏ چه بده‌بستانی دارد؟

پاسخ این پرسش‌ها در پیوست ه: پاسخ پرسش‌ها آمده است. پاسخ پرسش‌های این فصل →

۲۳-۱۲ منابع این فصل#

راهنمای جامع Ceph · ۱٫۶ · آبان ۱۴۰۵
© 2026 Ramin Najarbashi · CC BY-NC-SA 4.0

حریم خصوصی: این سایت کوکی نمی‌گذارد. برای شمارش بازدیدها از Cloudflare Web Analytics استفاده می‌شود که کوکی ندارد و افراد را دنبال نمی‌کند.