ذخیره سازی داده بادوام و APIهای فایل لینوکس

من که در مورد پایداری ذخیره سازی داده ها در سیستم های ابری تحقیق می کردم، تصمیم گرفتم خودم را آزمایش کنم تا مطمئن شوم که چیزهای اساسی را درک کرده ام. من با خواندن مشخصات NVMe شروع شد برای اینکه بفهمیم چه تضمین هایی در مورد ماندگاری داده ها (یعنی تضمین می کند که داده ها پس از خرابی سیستم در دسترس خواهند بود) دیسک های NMVe را به ما می دهد. من نتایج اصلی زیر را انجام دادم: باید داده ها را از لحظه ای که دستور نوشتن داده داده می شود و تا لحظه ای که در رسانه ذخیره سازی نوشته می شوند آسیب دیده در نظر بگیرید. با این حال، در اکثر برنامه ها، تماس های سیستمی کاملاً ایمن برای نوشتن داده ها استفاده می شود.

در این مقاله، مکانیسم‌های پایداری ارائه شده توسط APIهای فایل لینوکس را بررسی می‌کنم. به نظر می رسد که همه چیز در اینجا باید ساده باشد: برنامه دستور را فراخوانی می کند write()و پس از اتمام عملیات این دستور، داده ها به صورت امن بر روی دیسک ذخیره می شوند. ولی write() فقط داده های برنامه را در حافظه پنهان هسته واقع در RAM کپی می کند. برای اینکه سیستم مجبور شود داده ها را روی دیسک بنویسد، باید از مکانیسم های اضافی استفاده کرد.

ذخیره سازی داده بادوام و APIهای فایل لینوکس

به طور کلی، این مطالب مجموعه ای از یادداشت های مربوط به آموخته های من در مورد موضوع مورد علاقه من است. اگر به طور خلاصه در مورد مهمترین آنها صحبت کنیم، معلوم می شود که برای سازماندهی ذخیره سازی داده های پایدار، باید از دستور استفاده کنید fdatasync() یا فایل ها را با پرچم باز کنید O_DSYNC. اگر علاقه مندید اطلاعات بیشتری در مورد اتفاقاتی که برای داده ها در مسیر کد به دیسک می افتد بیاموزید، نگاهی به آن بیندازید این مقاله.

ویژگی های استفاده از تابع write().

تماس سیستمی write() در استاندارد تعریف شده است IEEE POSIX به عنوان تلاشی برای نوشتن داده ها در یک توصیفگر فایل. پس از اتمام موفقیت آمیز کار write() عملیات خواندن داده‌ها باید دقیقاً همان بایت‌هایی را که قبلاً نوشته شده بودند، برگردانند، حتی اگر داده‌ها از فرآیندها یا رشته‌های دیگر دسترسی داشته باشند.اینجا بخش مربوطه از استاندارد POSIX). اینجا، در بخش تعامل رشته ها با عملیات عادی فایل، نکته ای وجود دارد که می گوید اگر دو رشته هر کدام این توابع را فراخوانی کنند، هر فراخوانی باید یا تمام پیامدهای مشخص شده ای را که اجرای فراخوانی دیگر منجر می شود، ببیند یا هیچ عواقبی را نمی بینم این به این نتیجه می‌رسد که تمام عملیات ورودی/خروجی فایل باید روی منبعی که روی آن کار می‌شود قفل داشته باشد.

آیا این بدان معنی است که عملیات write() اتمی است؟ از نظر فنی بله. عملیات خواندن داده ها باید همه یا هیچ کدام از آنچه با آن نوشته شده است را برگرداند write(). اما عملیات write()، مطابق با استاندارد، لازم نیست تمام شود، زیرا همه چیزهایی را که از او خواسته شده بود یادداشت کند. نوشتن تنها بخشی از داده ها مجاز است. به عنوان مثال، ممکن است دو جریان داشته باشیم که هر کدام 1024 بایت به فایلی که توسط همان توصیف کننده فایل توصیف شده است اضافه می کنند. از نقطه نظر استاندارد، نتیجه زمانی قابل قبول خواهد بود که هر یک از عملیات نوشتن بتواند تنها یک بایت به فایل اضافه کند. این عملیات اتمی باقی می مانند، اما پس از تکمیل، داده هایی که در فایل می نویسند درهم می مانند. در اینجا این است بحث بسیار جالب در مورد این موضوع در Stack Overflow.

توابع fsync() و fdatasync().

ساده ترین راه برای شستشوی داده ها روی دیسک، فراخوانی تابع است fsync(). این تابع از سیستم عامل می خواهد تا تمام بلوک های اصلاح شده را از کش به دیسک منتقل کند. این شامل تمام ابرداده های فایل (زمان دسترسی، زمان اصلاح فایل و غیره) می شود. من معتقدم این ابرداده به ندرت مورد نیاز است، بنابراین اگر می دانید برای شما مهم نیست، می توانید از تابع استفاده کنید fdatasync()است. به کمک بر fdatasync() می گوید که در طول عملکرد این تابع، چنین مقداری از ابرداده در دیسک ذخیره می شود که "برای اجرای صحیح عملیات خواندن داده های زیر ضروری است." و این دقیقا همان چیزی است که اکثر برنامه ها به آن اهمیت می دهند.

یکی از مشکلاتی که می تواند در اینجا ایجاد شود این است که این مکانیسم ها تضمین نمی کنند که فایل پس از خرابی احتمالی پیدا شود. به طور خاص، هنگامی که یک فایل جدید ایجاد می شود، باید تماس بگیرید fsync() برای دایرکتوری که حاوی آن است. در غیر این صورت ممکن است پس از خرابی، معلوم شود که این فایل وجود ندارد. دلیل این امر این است که تحت یونیکس، به دلیل استفاده از لینک های سخت، یک فایل می تواند در چندین فهرست وجود داشته باشد. بنابراین، هنگام تماس fsync() هیچ راهی برای یک فایل وجود ندارد که بداند داده‌های دایرکتوری باید روی دیسک هم ریخته شوند (اینجا می توانید در این مورد بیشتر بخوانید). به نظر می رسد که سیستم فایل ext4 قادر به انجام این کار است به طور خودکار اعمال می شود fsync() به دایرکتوری های حاوی فایل های مربوطه، اما ممکن است در مورد سیستم های فایل دیگر اینطور نباشد.

این مکانیزم را می توان به طور متفاوت در سیستم های فایل مختلف پیاده سازی کرد. من استفاده کردم blktrace برای آشنایی با عملیات دیسک در سیستم های فایل ext4 و XFS. هر دو فرمان‌های معمول نوشتن را برای محتویات فایل‌ها و ژورنال سیستم فایل روی دیسک صادر می‌کنند، حافظه پنهان را پاک می‌کنند و با انجام یک نوشتن FUA (دسترسی واحد اجباری، نوشتن داده‌ها مستقیماً روی دیسک، دور زدن حافظه پنهان) در مجله خارج می‌شوند. آنها احتمالاً این کار را انجام می دهند تا واقعیت معامله را تأیید کنند. در درایوهایی که از FUA پشتیبانی نمی کنند، این باعث دو فلاش کش می شود. آزمایشات من این را نشان داده است fdatasync() کمی سریعتر fsync(). سودمند blktrace نشان میدهد که fdatasync() معمولاً داده های کمتری را روی دیسک می نویسد (در ext4 fsync() 20 کیلو بایت می نویسد و fdatasync() - 16 کیلو بایت). همچنین، متوجه شدم که XFS کمی سریعتر از ext4 است. و اینجا با کمک blktrace توانست آن را دریابد fdatasync() داده های کمتری را روی دیسک (4 کیلوبایت در XFS) روان می کند.

موقعیت های مبهم هنگام استفاده از fsync()

من می توانم به سه موقعیت مبهم در مورد فکر کنم fsync()که در عمل به آن برخورد کرده ام.

اولین حادثه در سال 2008 رخ داد. در آن زمان، اگر تعداد زیادی فایل روی دیسک نوشته می شد، رابط فایرفاکس 3 "یخ زده" می شد. مشکل این بود که پیاده سازی رابط از یک پایگاه داده SQLite برای ذخیره اطلاعات مربوط به وضعیت آن استفاده می کرد. پس از هر تغییری که در رابط ایجاد می شد، تابع فراخوانی می شد fsync()، که تضمین خوبی برای ذخیره سازی داده های پایدار می دهد. در سیستم فایل ext3 مورد استفاده، تابع fsync() تمام صفحات "کثیف" سیستم و نه فقط صفحات مربوط به فایل مربوطه را در دیسک فلاش کرد. این بدان معناست که کلیک کردن روی یک دکمه در فایرفاکس می‌تواند باعث شود که مگابایت داده بر روی یک دیسک مغناطیسی نوشته شود که ممکن است چندین ثانیه طول بکشد. راه حل مشکل، تا جایی که من فهمیدم آن مواد، انتقال کار با پایگاه داده به کارهای پس زمینه ناهمزمان بود. این بدان معناست که فایرفاکس از نیازهای سخت گیرانه تری برای ماندگاری ذخیره سازی استفاده می کرد و ویژگی های سیستم فایل ext3 فقط این مشکل را تشدید می کرد.

مشکل دوم در سال 2009 رخ داد. سپس، پس از خرابی سیستم، کاربران سیستم فایل ext4 جدید دریافتند که بسیاری از فایل‌های تازه ایجاد شده دارای طول صفر هستند، اما این اتفاق در سیستم فایل ext3 قدیمی‌تر رخ نداد. در پاراگراف قبلی، من در مورد اینکه چگونه ext3 داده های زیادی را روی دیسک ریخته صحبت کردم، که سرعت کار را بسیار کند کرد. fsync(). برای بهبود وضعیت، ext4 فقط صفحات "کثیف" را که مربوط به یک فایل خاص هستند، شستشو می دهد. و اطلاعات فایل های دیگر برای مدت زمان بسیار بیشتری نسبت به ext3 در حافظه باقی می ماند. این کار برای بهبود عملکرد انجام شد (به طور پیش فرض، داده ها به مدت 30 ثانیه در این حالت باقی می مانند، می توانید با استفاده از آن پیکربندی کنید dirty_expire_centisecs; اینجا می توانید اطلاعات بیشتری در این مورد بیابید). این بدان معنی است که مقدار زیادی از داده ها را می توان به طور جبران ناپذیری پس از یک خرابی از دست داد. راه حل این مشکل استفاده است fsync() در برنامه هایی که نیاز به ذخیره سازی پایدار داده ها دارند و تا حد امکان از آنها در برابر عواقب خرابی محافظت می کنند. تابع fsync() با ext4 بسیار کارآمدتر از ext3 کار می کند. عیب این روش این است که استفاده از آن مانند قبل باعث کندی برخی از عملیات ها مانند نصب برنامه ها می شود. جزئیات در این مورد را ببینید اینجا и اینجا.

مشکل سوم در مورد fsync()، در سال 2018 ایجاد شد. سپس در چارچوب پروژه PostgreSQL مشخص شد که اگر تابع fsync() با خطا مواجه می شود، صفحات "کثیف" را به عنوان "تمیز" علامت گذاری می کند. در نتیجه، تماس های زیر fsync() با چنین صفحاتی کاری انجام ندهید. به همین دلیل، صفحات تغییر یافته در حافظه ذخیره می شوند و هرگز روی دیسک نوشته نمی شوند. این یک فاجعه واقعی است، زیرا برنامه فکر می کند که برخی از داده ها روی دیسک نوشته شده است، اما در واقع اینطور نخواهد بود. چنین شکست هایی fsync() نادر هستند، برنامه در چنین شرایطی تقریباً هیچ کاری برای مقابله با مشکل انجام نمی دهد. این روزها، وقتی این اتفاق می افتد، PostgreSQL و سایر برنامه ها از کار می افتند. اینجا، در مقاله "آیا برنامه ها می توانند از خرابی های fsync بازیابی شوند؟" این مشکل به طور مفصل بررسی شده است. در حال حاضر بهترین راه حل برای این مشکل استفاده از Direct I/O با پرچم است O_SYNC یا با پرچم O_DSYNC. با این رویکرد، سیستم خطاهایی را که ممکن است هنگام انجام عملیات نوشتن داده خاص رخ دهد گزارش می‌کند، اما این رویکرد به برنامه نیاز دارد که خود بافرها را مدیریت کند. در مورد آن بیشتر بخوانید اینجا и اینجا.

باز کردن فایل ها با استفاده از پرچم های O_SYNC و O_DSYNC

بیایید به بحث مکانیسم های لینوکس که ذخیره سازی دائمی داده ها را فراهم می کند، بازگردیم. یعنی ما در مورد استفاده از پرچم صحبت می کنیم O_SYNC یا پرچم O_DSYNC هنگام باز کردن فایل ها با استفاده از تماس سیستمی باز کن(). با این رویکرد، هر عملیات نوشتن داده به گونه ای انجام می شود که گویی بعد از هر دستور انجام می شود write() به ترتیب دستوراتی به سیستم داده می شود fsync() и fdatasync()است. به مشخصات POSIX این "تکمیل یکپارچگی فایل I/O همگام" و "تکمیل یکپارچگی داده" نامیده می شود. مزیت اصلی این رویکرد این است که فقط یک فراخوانی سیستمی برای اطمینان از یکپارچگی داده ها باید اجرا شود و نه دو (به عنوان مثال - write() и fdatasync()). نقطه ضعف اصلی این روش این است که تمام عملیات نوشتن با استفاده از توصیفگر فایل مربوطه همگام می شود، که می تواند توانایی ساختار کد برنامه را محدود کند.

استفاده از Direct I/O با پرچم O_DIRECT

تماس سیستمی open() از پرچم پشتیبانی می کند O_DIRECT، که برای دور زدن حافظه پنهان سیستم عامل، انجام عملیات I / O، تعامل مستقیم با دیسک طراحی شده است. این در بسیاری از موارد به این معنی است که دستورات نوشتن صادر شده توسط برنامه مستقیماً به دستوراتی با هدف کار با دیسک ترجمه می شود. اما، به طور کلی، این مکانیسم جایگزینی برای توابع نیست fsync() یا fdatasync(). واقعیت این است که خود دیسک می تواند تاخیر یا حافظه پنهان دستورات مناسب برای نوشتن داده ها و حتی بدتر، در برخی موارد خاص، عملیات I/O هنگام استفاده از پرچم انجام می شود O_DIRECT, پخش به عملیات بافر سنتی. ساده ترین راه برای حل این مشکل استفاده از پرچم برای باز کردن فایل ها است O_DSYNC، به این معنی است که هر عملیات نوشتن با یک فراخوانی همراه خواهد بود fdatasync().

معلوم شد که سیستم فایل XFS اخیراً یک "مسیر سریع" برای آن اضافه کرده است O_DIRECT|O_DSYNC- رکوردهای داده اگر بلوک با استفاده از O_DIRECT|O_DSYNC، سپس XFS به جای شستشوی کش، دستور نوشتن FUA را در صورتی که دستگاه از آن پشتیبانی کند، اجرا می کند. من این را با استفاده از ابزار تأیید کردم blktrace در سیستم Linux 5.4/Ubuntu 20.04. این رویکرد باید کارآمدتر باشد، زیرا حداقل مقدار داده را روی دیسک می نویسد و از یک عملیات استفاده می کند، نه دو عمل (نوشتن و پاک کردن کش). من یک لینک پیدا کردم وصله هسته 2018 که این مکانیسم را پیاده سازی می کند. بحث هایی در مورد اعمال این بهینه سازی در فایل سیستم های دیگر وجود دارد، اما تا آنجا که من می دانم، XFS تنها فایل سیستمی است که تا کنون از آن پشتیبانی می کند.

تابع sync_file_range().

لینوکس یک تماس سیستمی دارد sync_file_range()، که به شما امکان می دهد فقط بخشی از فایل را روی دیسک فلاش کنید، نه کل فایل را. این تماس یک فلاش ناهمزمان را آغاز می کند و منتظر تکمیل آن نمی ماند. اما در اشاره به sync_file_range() گفته می شود این دستور "بسیار خطرناک" است. استفاده از آن توصیه نمی شود. ویژگی ها و خطرات sync_file_range() بسیار خوب در این مواد به طور خاص، به نظر می رسد که این فراخوانی از RocksDB برای کنترل زمانی که هسته داده های "کثیف" را روی دیسک می فرستد، استفاده می کند. اما در همان زمان، برای اطمینان از ذخیره سازی داده های پایدار، از آن نیز استفاده می شود fdatasync()است. به کد RocksDB نظرات جالبی در مورد این موضوع دارد. به عنوان مثال، به نظر می رسد تماس است sync_file_range() هنگام استفاده از ZFS داده ها را روی دیسک تخلیه نمی کند. تجربه به من می گوید که کدهایی که به ندرت استفاده می شوند ممکن است دارای اشکال باشند. بنابراین، من توصیه می کنم از استفاده از این تماس سیستمی مگر در موارد ضروری استفاده نکنید.

تماس های سیستمی برای کمک به اطمینان از پایداری داده ها

من به این نتیجه رسیده ام که سه رویکرد وجود دارد که می توان از آنها برای انجام عملیات ورودی/خروجی مداوم استفاده کرد. همه آنها به یک فراخوانی تابع نیاز دارند fsync() برای دایرکتوری که فایل در آن ایجاد شده است. اینها رویکردها هستند:

  1. فراخوانی تابع fdatasync() یا fsync() بعد از عملکرد write() (بهتر است استفاده شود fdatasync()).
  2. کار با یک توصیفگر فایل که با یک پرچم باز می شود O_DSYNC یا O_SYNC (بهتر - با پرچم O_DSYNC).
  3. استفاده از دستور pwritev2() با پرچم RWF_DSYNC یا RWF_SYNC (ترجیحا با پرچم RWF_DSYNC).

یادداشت های عملکردی

من عملکرد مکانیسم های مختلفی را که بررسی کردم به دقت اندازه گیری نکردم. تفاوت هایی که در سرعت کارشان متوجه شدم خیلی کم است. این به این معنی است که من ممکن است اشتباه کنم و در شرایط دیگر ممکن است همین موضوع نتایج متفاوتی را نشان دهد. اول، من در مورد آنچه که بر عملکرد بیشتر تأثیر می گذارد، و سپس در مورد آنچه که عملکرد کمتری را تحت تأثیر قرار می دهد صحبت خواهم کرد.

  1. بازنویسی داده های فایل سریعتر از الحاق داده ها به یک فایل است (افزایش عملکرد می تواند 2-100٪ باشد). پیوست کردن داده به یک فایل نیاز به تغییرات اضافی در فراداده فایل، حتی پس از تماس سیستمی دارد fallocate()، اما میزان این اثر ممکن است متفاوت باشد. توصیه می کنم برای بهترین عملکرد، تماس بگیرید fallocate() تا فضای مورد نیاز از قبل تخصیص داده شود. سپس این فاصله باید به صراحت با صفر پر شود و فراخوانی شود fsync(). این باعث می شود که بلوک های مربوطه در سیستم فایل به جای "تخصیص نشده" به عنوان "تخصیص" علامت گذاری شوند. این باعث بهبود عملکرد کوچک (حدود 2٪) می شود. همچنین، برخی از دیسک‌ها ممکن است عملیات دسترسی بلوک اول کندتر از سایرین داشته باشند. این بدان معنی است که پر کردن فضا با صفر می تواند منجر به بهبود عملکرد قابل توجه (حدود 100٪) شود. به ویژه، این می تواند با دیسک ها اتفاق بیفتد. AWS EBS (این اطلاعات غیر رسمی است، من نتوانستم آنها را تأیید کنم). در مورد ذخیره سازی هم همینطور. دیسک پایدار GCP (و این قبلاً اطلاعات رسمی است که توسط آزمایشات تأیید شده است). کارشناسان دیگر نیز همین کار را کرده اند مشاهدهمربوط به دیسک های مختلف
  2. هرچه تماس های سیستم کمتر باشد، عملکرد بالاتری دارد (بهره می تواند حدود 5٪ باشد). به نظر می رسد یک تماس است open() با پرچم O_DSYNC یا زنگ بزن pwritev2() با پرچم RWF_SYNC تماس سریعتر fdatasync(). من گمان می کنم که نکته اینجاست که با این رویکرد، این واقعیت که برای حل یک کار یکسان (یک تماس به جای دو) باید تعداد کمتری فراخوانی سیستم انجام شود، نقش دارد. اما تفاوت عملکرد بسیار کم است، بنابراین می توانید به راحتی آن را نادیده بگیرید و از چیزی در برنامه استفاده کنید که منجر به پیچیده شدن منطق آن نشود.

اگر به موضوع ذخیره سازی داده های پایدار علاقه مند هستید، در اینجا برخی از مطالب مفید وجود دارد:

  • روش های دسترسی I/O - مروری بر اصول اولیه مکانیسم های ورودی / خروجی.
  • اطمینان از رسیدن اطلاعات به دیسک - داستانی در مورد اتفاقاتی که برای داده ها در مسیر برنامه به دیسک می افتد.
  • چه زمانی باید دایرکتوری حاوی را همگام سازی کنید - پاسخ به این سوال که چه زمانی باید درخواست داد fsync() برای دایرکتوری ها به طور خلاصه، مشخص می شود که هنگام ایجاد یک فایل جدید باید این کار را انجام دهید و دلیل این توصیه این است که در لینوکس می تواند ارجاعات زیادی به همان فایل وجود داشته باشد.
  • SQL Server در لینوکس: FUA Internals - در اینجا توضیحی در مورد نحوه اجرای ذخیره سازی دائمی داده ها در SQL Server در پلت فرم لینوکس ارائه شده است. مقایسه های جالبی بین تماس های سیستمی ویندوز و لینوکس در اینجا وجود دارد. تقریباً مطمئن هستم که به لطف این مطالب بود که در مورد بهینه سازی FUA XFS یاد گرفتم.

آیا تا به حال داده هایی را که فکر می کردید به طور ایمن روی دیسک ذخیره شده اند از دست داده اید؟

ذخیره سازی داده بادوام و APIهای فایل لینوکس

ذخیره سازی داده بادوام و APIهای فایل لینوکس

منبع: www.habr.com