devops, کلاسترینگ, مقالات

مقایسه Docker و Containerd

مقایسه Docker و Containerd

 

Docker vs. Containerd: A Quick Comparison (2023)

چندی پیش Kubernetes اعلام کرد که Docker را منسوخ می کند. “منسوخ” اساسا یک کلمه فنی برای “این به زودی منقضی می شود.” “دیگر از این استفاده نکنید، به زودی حذف خواهد شد.” پس چگونه Kubernetes کانتینرها را بدون داکر اجرا می کند؟ البته با containerd ! بله، داکر با چیز دیگری به نام containerd جایگزین شد. یا حداقل، این نوعی مسیر پیش‌فرض مهاجرت، از داکر بهcontainerd است. مدیران کلاستر Kubernetes در صورت تمایل می توانند چیز دیگری مانند CRI-O را انتخاب کنند. اما صبر کنید، چرا Docker را حذف کنید و آن را با این جایگزین کنید؟ ما قبلاً متوجه شدیم  که داکر می تواند هر کاری را که در آرزوی ماست، با کانتینرها انجام دهد. چرا ابزار دیگری لازم است؟ بهتر است؟ تفاوت بین داکر و containerd چیست؟ بیایید این را روشن کنیم! 

 

داکر چیست؟

 ابزاری مثل  Docker در محبوبیت بسیار سریع منفجر شد،مدتی بعد درست مانند Kubernetes . اما چرا؟ چه چیزی در مورد این ابزار جالب بود؟

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

بنابراین اساسا Docker یک ابزار “کامل” است. به ما راهی داد تا هر کاری را که می خواهیم با کانتینرها، با یک ابزار واحد، بدون نیاز به دانلود برنامه های اضافی انجام دهیم. این تجربه کاربری ما را ساده کرد. ناگفته نماند که دستوراتی که باید استفاده کنیم نیز بسیار شهودی هستند. دستوراتی مانند

 

در نگاه اول به راحتی قابل درک هستند.

بنابراین، اگر استفاده از Docker بسیار آسان است، مشکلcontainerd چیست؟ خوب، بیایید عمیق تر کاوش کنیم.

داکر یکپارچه قدیمی Old Monolithic Docker

Docker یک برنامه بسیار پیچیده بود که در طول عمر خود تغییرات بسیاری را پشت سر گذاشت. در ابتدا، این چیزی بود که به آن ابزار “monolithic” می گویند. «monolithic» در این مورد به معنای «یک چیز جدا نشدنی» است. به عبارت دیگر، این یک برنامه بزرگ بود که می توانست کارهای زیادی انجام دهد. اما این فقط یک برنامه بزرگ بود، نه دو تا، نه 10. اما، البته، حتی یک برنامه یکپارچه دارای بخش های زیادی است، به شکل بخش های برنامه، کتابخانه ها، یا به سادگی قطعات مختلف کد که با انواع مختلف فعالیت ها سروکار دارند. بخشی از کد آن مسئول کشیدن تصاویر کانتینر بود. بخشی دیگر وظیفه راه اندازی کانتینرها و … را بر عهده داشت.

برای درک بهتر این موضوع، می‌توانیم به بازی مانند GTA V فکر کنیم. این یک بازی بزرگ و یکپارچه است. اما بخشی از کد آن مسئول نحوه ظاهر شدن و حرکت خودروها در خیابان است. بخشی دیگر مسئول آب و هوا است. بخشی دیگر مسئول نحوه راه رفتن مردم در خیابان و انجام کارهای روزمره خود است. این یک بازی یکپارچه بزرگ است که همه این موارد را در خود دارد. ما نمی توانیم فقط بخشی از بازی را راه اندازی کنیم که مسئولیت حرکت ماشین ها در خیابان است. ما باید کل چیز را راه اندازی کنیم که شامل تمام آن قسمت ها باشد. اکنون این مشکلی برای یک بازی نیست، ما می‌خواهیم به کل چیز دسترسی داشته باشیم، با دنیای کاملی که شبیه‌سازی می‌کند. اما در مورد داکر، این درست نیست. به زودی آشکار شد که اگر بتوانیم به نحوی فقط به بخش هایی از آن دسترسی داشته باشیم، مفید خواهد بود. بیایید ببینیم چرا.

داکر ماژولار جدید

همه شروع به استفاده از کانتینرها کردند. بنابراین داکر بیشتر و پیچیده تر شد. وقتی سیستم پیچیده ای دارید، تقسیم آن به قطعات کوچکتر می تواند کارها را ساده کند. به عنوان مثال، بیایید در مورد دستوری مانند این فکر کنیم:

این تصویر “nginx” را  دانلود می کند  و بلافاصله کانتینری را راه اندازی می کند که این برنامه Nginx را اجرا می کند. این به نوبه خود به ما امکان دسترسی به وب سرور را می دهد. مردم اکنون می توانند در پورت 80 به آن متصل شوند و هر صفحه وب را که در آنجا داریم ببینند. حال بیایید به این فکر کنیم که Docker به عنوان یک برنامه باید در اینجا چه کاری انجام دهد. اول از همه، باید بخشی در کد خود داشته باشد که بتواند دستور ما را درک کند:

باید به نحوی این را در درون خود “ترجمه” کند و بداند که انسان در اینجا می خواهد به چه چیزی برسد. این کار Docker CLI، “واسط خط فرمان” است. بعد از اینکه متوجه شد چه می خواهیم، ​​قسمت دیگری از کد آن باید تصویر کانتینر “nginx” را وارد کند. بعد، بخش دیگری از کد باید آن کانتینر را راه‌اندازی کند و آن را در پورت 80 در دسترس قرار دهد. و اینجاست که به بیت جالب می‌رسیم، و در نهایت، می‌فهمیم که معامله با کانتینر چیست.

 Containerd چیست؟

زمانی که Docker یکپارچه بود، یک برنامه واحد دستور ما را ترجمه کرد، سپس تصویر کانتینر را کشید، آن را راه‌اندازی کرد و آن را در پورت 80 در دسترس قرار داد. امروزه، این دیگر درست نیست. در یک شکل بسیار ساده، این چیزی است که در حال حاضر اتفاق می افتد:

ابزار Docker CLI این فرمان را می پذیرد. سپس مشخص می کند که می خواهیم چه کار کنیم. بعد از اینکه هدف ما را فهمید، این قصد را به Docker Daemon منتقل می کند. این Daemon یک برنامه جداگانه (از Docker CLI) است که همیشه در پس زمینه اجرا می شود و منتظر دستورالعمل است. پس از اینکه Docker Daemon اکشن مورد نظر ما را دریافت کرد، به برنامه دیگری به نام Container Runtime می‌گوید تا تصویر کانتینر را دانلود کند. این Container Runtime  –زمان احرای کانتینر ، containerd نامیده می شود.

بنابراین ما در نهایت می توانیم بفهمیم که containerd چیست. از نظر فنی، این یک زمان اجرا کانتینری – container runtime است. این یک جور مدیر کانتینر است. از مواردی مانند:

  • دانلود تصاویر کانتینر.
  • بارگذاری تصاویر کانتینر
  • راه اندازی شبکه بین این کانتینرها، به طوری که آنها بتوانند با یکدیگر یا دنیای خارج ارتباط برقرار کنند.
  • مدیریت داده ها و فایل های ذخیره شده در این کانتینرها.
  • شروع، توقف، راه اندازی مجدد کانتینرها.

containerd را زمان اجرای کانتینر سطح بالا می نامند. برای برخی اقدامات، از زمان اجرا دیگری استفاده می کند که به آن زمان اجرای کانتینر سطح پایین می گویند. این زمان اجرا در سطح پایین runc نامیده می شود. برای مثال، زمانی که containerd باید یک کانتینر را راه اندازی کند، به runc می گوید که این کار را انجام دهد. در پایان روز، یک کانتینر یک برنامه کاربردی است که در بخش ایزوله از سیستم اجرا می شود. به عنوان مثال، اگر یک برنامه ماشین حساب معمولی را در ویندوز راه اندازی کنیم، این یک فرآیند معمولی را باز می کند، نه جدا از بقیه سیستم. می تواند به هر فایلی دسترسی داشته باشد و تقریباً هر کاری را که می خواهد انجام دهد. اما اگر این ماشین حساب در یک کانتینر اجرا شود، فقط فایل های داخل آن کانتینر را می بیند. و تنها قادر به برقراری ارتباط با سایر فرآیندهای داخل آن کانتینر خواهد بود. تمام دنیای آن درون آن کانتینر است. تا آنجا که به آن مربوط می شود، فکر می کند که “سیستم واقعی” است، بنابراین قادر به دیدن چیزی که خارج از آن فضا  نیست. runc مسئول شروع یک فرآیند در این حالت خاص و ایزوله است.

همه اینها، Docker CLI، Docker Daemon، containerd، runc، برنامه های کاملا مجزا هستند. بسیار جالب است که چگونه بسیاری از برنامه ها کارها را برای راه اندازی یک کانتینر به یکدیگر منتقل می کنند. و ما حتی برخی از مراحل کوچک را که طی می‌کند نادیده گرفتیم تا همه چیز ساده‌تر شود. اما چگونه از Docker یکپارچه به این مجموعه از برنامه‌های کاملاً مجزا که با یکدیگر صحبت می‌کنند، رسیدیم؟

خوب، پس از سال ها و سال ها کار، توسعه دهندگان Docker شروع به تقسیم بخش های مختلف کد Docker کردند. بنابراین یک قسمت از کد به کانتینر تبدیل شد. یه قسمت دیگه runc شد اما چرا؟ اول از همه، این کار را برای توسعه دهندگان ساده تر می کند. اکنون توسعه‌دهندگان به‌جای جستجو در میان فایل‌های مختلف، تلاش برای یافتن بخشی از کد مسئول راه‌اندازی کانتینرها، می‌توانند مستقیماً به runc بروند که یک صفحه GitHub جداگانه دارد. بنابراین، در گذشته، runc تنها بخشی از کد Docker بود که مسئول این کار بود. اما توسعه دهندگان به آرامی آن کد را استخراج کردند و آن را به یک ابزار کاملاً مجزا تبدیل کردند. اما این فقط برای تسهیل توسعه نبود.

ما می توانیم Docker قدیمی را به عنوان نوعی آیفون تصور کنیم. این یک تلفن جدانشدنی است که تمام قطعات آن به هم چسبیده است. مطمئناً داخل آن یک باتری جداگانه، یک دوربین، یک پردازنده و غیره وجود دارد. اما آنها به قدری محکم با یکدیگر مونتاژ و یکپارچه شده اند که ما نمی توانیم به راحتی آیفون خود را جدا کنیم و دوربین قدیمی خود را با یک دوربین جدید و بهتر جایگزین کنیم. اما اگر بتوانیم این کار را انجام دهیم مفید خواهد بود، اینطور نیست؟ تصور کنید ما از دوربین قدیمی و ۱۲ مگاپیکسلی خود ناراضی هستیم. و ما می‌توانیم آن را جدا کرده و با یک دوربین 48 مگاپیکسلی جدید جایگزین کنیم، همان‌طور که می‌توانیم باتری‌های ماوس خود را جایگزین کنیم. قطعا چیز خوبی خواهد بود خوب، ما نمی‌توانیم این کار را با آیفون انجام دهیم، اما می‌توانیم آن را با داکر ماژولار جدید انجام دهیم.

بنابراین اکنون می‌توانیم Docker جدید و مدرن را به عنوان یک خودروی بزرگ و کامل با تمام قطعاتش در نظر بگیریم: موتور، فرمان، پدال‌ها و غیره. و در صورت نیاز به موتور می توانیم به راحتی آن را استخراج کرده و به سیستم دیگری منتقل کنیم. دقیقا همان چیزی است که کوبرنتیز به چنین موتوری نیاز داشت. آنها اساسا گفتند: ” ما به کل ماشینی که داکر است نیاز نداریم، بیایید container runtime/enginecontainerd آن را بیرون بکشیم و آن را در Kubernetes نصب کنیم”. اگر می خواهید، می توانید در این پست وبلاگ در مورد اینکه چرا Kubernetes این کار را انجام داد بیشتر بخوانید. و این در واقع دلیل بزرگتری است که Docker به اجزای کوچکتر زیادی تقسیم شده است، به طوری که آنها می توانند آزادانه جابجا شوند و به سیستم های دیگر متصل شوند. این به مدیران سرور انعطاف‌پذیری زیادی می‌دهد تا زیرساخت‌های Kubernetes خود را هر طور که می‌خواهند بسازند، با قطعاتی که بهترین کار را برای آنها دارد، عملکرد یا امنیت بیشتری به آنها می‌دهد، یا هر چیزی که برایشان مهم‌تر است. و این به Kubernetes محدود نمی شود. قطعاتی مانند containerd را می توان در هر سیستمی که بخواهیم وارد کرد. در واقع، اگر بخواهیم، ​​containerd حتی می‌تواند مستقیماً در رایانه ما استفاده شود. اما 99٪ از کاربران نمی خواهند مستقیماً از containerd استفاده کنند، بدون اینکه ابتدا از داکر عبور کنند. چرا اینطور است؟

داکر در مقابل containerd : تفاوت چیست؟

داکر با در نظر گرفتن انسان نوشته شده است. می‌توانیم آن را به‌عنوان نوعی مترجم تصور کنیم که به کل کارخانه، پر از روبات‌ها، درباره آنچه انسان می‌خواهد بسازد یا انجام دهد، می‌گوید. Docker CLI مترجم واقعی است، برخی از قطعات دیگر در Docker ربات های کارخانه هستند. در سمت چپ، ما انسان را داریم که نیاز به انجام کاری با کانتینر دارد. در وسط، DockerCLI + تمام اجزای دیگر آن را داریم. و در سمت راست، اقداماتی را داریم که توسط داکر انجام شده است، مانند ساختن یک کانتینر، کشیدن یک تصویر، یا شروع یک کانتینر. بنابراین داکر یک واسطه است که دستورات انسان ها را می پذیرد و سپس نتیجه می دهد.

به عنوان مثال، برای دستوری مانند

به این صورت است که اکشن‌ها از یک مؤلفه Docker به مؤلفه دیگر جریان می‌یابند تا در نهایت، کانتینر شروع شود:

باز هم، برای سادگی، ما برخی از بخش ها را حذف کردیم، مانند Docker Daemon. اما به طور خلاصه، این چیزی است که پس از وارد کردن دستور توسط شخصی اتفاق می افتد:

  1. Docker CLI متوجه می شود که ما چه کاری می خواهیم انجام دهیم، و سپس دستورالعمل ها را به containerd می فرستد.
  2. Containerd جادوی خود را انجام می دهد، اگر تصویر nginx در دسترس نباشد، آن را دانلود می کند.
  3. در مرحله بعد، containerd به runc می گوید که این کانتینر را راه اندازی کند 
  4. و در نهایت نتیجه خود را دریافت می کنیم: nginx در یک جعبه کانتینری کوچک در حال اجرا است.

به راحتی می توان دریافت که Docker CLI لزوماً برای این عمل مورد نیاز نیست. این بدان معناست که ما واقعاً به Docker با تمام قطعات آن، مانند Docker CLI، Docker Daemon و برخی از قطعات و قطعات دیگر آن نیازی نداریم. با این حال، ما هنوز برای شروع یک کانتینر به containerd و runc نیاز داریم. پس چرا مستقیماً به containerd نیت خود را نگوییم؟ اگر حداقل از اجرای Docker CLI و Docker Daemon صرف نظر کنیم، از حافظه کمتری در سیستم خود استفاده خواهیم کرد، درست است؟ کارآمدتر خواهد بود، این درست است. در واقع، این یکی از دلایلی است که Kubernetes داکر را حذف کرده و مستقیماً از Containerd استفاده می کند. اما این یک معاوضه است که برای سرورهایی که صدها کانتینر دارند مفید است. برای رایانه شخصی ما، جایی که ما فقط چند کانتینر را اجرا می کنیم، چیزها را آزمایش می کنیم، این تفاوت محسوسی ایجاد نمی کند. اما اگر Kubernetes بتواند از واسطه‌ای که Docker است بگذرد و مستقیماً به Containerd درباره کاری که می‌خواهد انجام دهد بگوید، کانتینرها می‌توانند کمی سریع‌تر راه‌اندازی شوند. و نیم ثانیه در اینجا، نیم ثانیه در آنجا، با صدها کانتینر، می تواند اضافه شود و پیشرفت های قابل توجهی را نشان دهد.

اما به خاطر داشته باشید، Kubernetes یک برنامه است، containerd نیز یک برنامه است. و برنامه ها می توانند به سرعت با یکدیگر صحبت کنند، حتی اگر زبانی که صحبت می کنند پیچیده باشد. Containerd از ابتدا توسعه داده شده است تا برنامه های دیگر دستورات لازم را به آن بدهند. دستورالعمل ها را به زبان تخصصی دریافت می کند. این دستورالعمل ها فراخوانی های API نامیده می شوند و از طریق چیزی که API نامیده می شود، رابط برنامه نویسی برنامه ارسال می شوند. این رابط ها اساساً درهایی هستند که از طریق آنها می توان تماس های API را توسط یک برنامه ارسال کرد و توسط برنامه دیگری در انتهای دیگر دریافت کرد. البته، API همچنین تعیین می‌کند که این برنامه‌ها از چه زبانی باید استفاده کنند. پیام های ارسال شده در تماس های API باید از فرمت خاصی پیروی کنند تا برنامه دریافت کننده بتواند آنها را درک کند.

برای انسان‌ها خسته‌کننده خواهد بود که هر بار که می‌خواهند به Containerd بگویند که کاری انجام دهد، تماس‌های API ارسال کنند. اما زمانی که توسعه‌دهندگان برنامه‌هایی را می‌نویسند که باید با Containerd تعامل داشته باشند، روش‌هایی را برای ارسال تماس‌های API صحیح پیاده‌سازی می‌کنند. بنابراین برنامه‌ها می‌توانند از طریق این APIها به طور موثر با یکدیگر ارتباط برقرار کنند. در اینجا یک مثال از یک برنامه کوچک است که به containerd متصل می شود و سپس دستورالعملی برای دانلود یک تصویر کانتینر برای آن ارسال می کند:

کد منبع استخراج شده از این صفحه:

https://github.com/containerd/containerd/blob/main/docs/getting-started.md

آیا می خواهیم چنین چیزهایی را فقط برای کشیدن یک تصویر کانتیر بنویسیم؟ البته که نه. بنابراین Docker از طرف دیگر با Docker CLI خود برای دریافت دستورالعمل ها از انسان ساخته شده است. این بیشتر human-friendly  است، به ما اجازه می دهد تا بسیاری از کارها را انجام دهیم، با دستورات نسبتا کوتاهی که نوشتن آنها آسان و به خاطر سپردن آنها آسان است.

آزمایش با Containerd

اما اگر واقعاً بخواهیم با Containerd آزمایش کنیم، می‌توانیم این کار را انجام دهیم، بدون نیاز به برقراری تماس‌های پیچیده API. اگر داکر را روی سیستم خود نصب کرده باشیم، Containerd نیز قبلاً نصب شده است، زیرا داکر به آن نیاز دارد. و چند برنامه کاربردی وجود دارد که می توانیم از آنها برای صحبت مستقیم با Containerd استفاده کنیم. یک روش از طریق ابزار ctr است. به عنوان مثال، برای اینکه به containerd بگوییم تصویر nginx را دانلود کند، دستوری مانند این را وارد می کنیم:

برای اینکه ببینیم ctr از چه دستوراتی پشتیبانی می کند، این را وارد می کنیم:

برای دریافت راهنمایی در مورد یک فرمان فرعی خاص، ما فقط “ctr subcommand_name” را بدون هیچ پارامتر/دستورالعمل دیگری می نویسیم. به عنوان مثال، اگر بخواهیم ببینیم چگونه می توانیم از دستور فرعی “تصاویر” استفاده کنیم، می توانیم بنویسیم:

ctr images

اما این دستور ctr بیشتر یک “میانبر” است که برای تعاملات ساده با Containerd در نظر گرفته شده است، در صورتی که کسی نیاز به اشکال زدایی یا آزمایش برخی موارد داشته باشد. تصور کنید که ما توسعه‌دهنده‌ایم و فقط چیزهای جالب جدیدی را در کانتینر پیاده‌سازی کرده‌ایم. اکنون می‌خواهیم با بهینه‌سازی‌هایی که انجام دادیم، آزمایش کنیم که آیا تصویر کانتیر سریع‌تر دانلود می‌شود یا خیر. ارسال یک تماس API به containerd خسته کننده خواهد بود. اما با ctr، می‌توانیم نیاز به نوشتن و ارسال یک تماس API را دور بزنیم. ما کمتر به صورت دستورات کوتاه می نویسیم و سریعتر تست می کنیم. سپس ctr کارهای سنگین را انجام می دهد و تماس های API صحیح را ارسال می کند. بنابراین واقعاً قرار نیست از ctr استفاده شود زیرا ما از DockerCLI استفاده می کنیم. ممکن است شبیه به نظر برسد، اما هدفش این نیست. به علاوه، واقعاً از تمام کارهایی که می‌توانیم با Docker انجام دهیم، پشتیبانی نمی‌کند.

ابزاری به نام nerdctl نیز وجود دارد. این باید جداگانه دانلود و نصب شود. nerdctl سعی می کند از نحو DockerCLI تقلید کند. بنابراین این راهی برای نوشتن دستورات داکر مانند است، اما در واقع بدون صحبت با داکر. در عوض، مستقیماً بهContainerd درباره اقداماتی که می‌خواهیم انجام دهیم، می‌گوید.

به یاد داشته باشید که چگونه از این دستور برای راه اندازی یک ظرف Nginx استفاده می کنیم؟

البته این کار تمام آن مراحل را طی می‌کند و به Docker CLI در مورد کاری که می‌خواهیم انجام دهیم، می‌گوید، که سپس به داکر دیمون می‌رود و در نهایت در نقطه‌ای به کانتینر می‌رسد. با nerdctl، می توانیم به طور مستقیم به containerd بگوییم که کانتینر ما را راه اندازی کند، با دستوری مانند:

nerdctl run --name webserver -p 80:80 -d nginx

بنابراین ما از بررسی Docker CLI و Docker Daemon صرف نظر می کنیم.

nerdctl این مزیت را دارد که می تواند به ما دسترسی به جدیدترین ویژگی های پیاده سازی شده در Containerd را بدهد. به عنوان مثال، ما می توانیم با تصاویر کانتینر رمزگذاری شده کار کنیم، یک ویژگی نسبتاً جدید در سال 2022، که در نهایت در دستورات داکر معمولی نیز پیاده سازی خواهد شد. با این حال، این ویژگی ها هنوز آزمایشی هستند و لزوماً برای بارهای کاری در دنیای واقعی هنوز ایمن نیستند. بنابراین nerdctl برای کاربران نهایی طراحی نشده است. همچنین این ابزار برای توسعه دهندگان یا مدیران سیستمی است که می خواهند یک راه آسان برای آزمایش یا اشکال زدایی ویژگی های کانتینر داشته باشند. به این معنا که آن‌ها می‌توانند به‌سرعت چیزها را با دستورات nerdctl ساده، به جای دستورالعمل‌های پیچیده API، همانطور که قبلاً هنگام بحث در مورد ابزار ctr ذکر کردیم، آزمایش کنند.

بنابراین ما آن را داریم! امیدواریم که این معمای داکر چیست، چگونه ساخته شده است و کانتینر چیست را روشن کند.

منبع: https://kodekloud.com/blog/docker-vs-containerd/

دیدگاهتان را بنویسید