مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما

مشتریان به طور فزاینده ای درخواست های زیر را دریافت می کنند: "ما آن را مانند Amazon RDS می خواهیم، ​​اما ارزان تر". ما آن را مانند RDS می‌خواهیم، ​​اما در همه جا، در هر زیرساختی.» برای پیاده سازی چنین راه حل مدیریت شده ای در Kubernetes، ما به وضعیت فعلی محبوب ترین اپراتورهای PostgreSQL (Stolon، اپراتورهای Crunchy Data و Zalando) نگاه کردیم و انتخاب خود را انجام دادیم.

این مقاله تجربه ای است که ما هم از نظر تئوریک (بررسی راه حل ها) و هم از جنبه عملی (آنچه انتخاب شد و چه چیزی از آن حاصل شد) به دست آورده ایم. اما ابتدا، بیایید مشخص کنیم که الزامات کلی برای جایگزینی بالقوه برای RDS چیست؟

RDS چیست؟

وقتی مردم در مورد RDS صحبت می کنند، طبق تجربه ما، منظور آنها یک سرویس مدیریت شده DBMS است که:

  1. آسان برای پیکربندی؛
  2. قابلیت کار با اسنپ شات و بازیابی از آنها را دارد (ترجیحاً با پشتیبانی PITR);
  3. به شما امکان می دهد توپولوژی های master-slave ایجاد کنید.
  4. دارای فهرست غنی از پسوندها؛
  5. حسابرسی و مدیریت دسترسی/کاربر را فراهم می کند.

به طور کلی، رویکردهای اجرای کار در دست می تواند بسیار متفاوت باشد، اما مسیر با Ansible شرطی به ما نزدیک نیست. (در نتیجه همکاران 2GIS به نتیجه مشابهی رسیدند تلاش او ایجاد "ابزاری برای استقرار سریع یک خوشه Failover مبتنی بر Postgres.")

اپراتورها یک رویکرد رایج برای حل مشکلات مشابه در اکوسیستم Kubernetes هستند. مدیر فنی "Flanta" قبلاً با جزئیات بیشتری در مورد آنها در رابطه با پایگاه های داده راه اندازی شده در Kubernetes صحبت کرده است. منقطعبه یکی از گزارش های او.

NB: برای ایجاد سریع اپراتورهای ساده، توصیه می کنیم به ابزار منبع باز ما توجه کنید اپراتور پوسته. با استفاده از آن، می توانید این کار را بدون اطلاع از Go انجام دهید، اما به روش هایی که برای مدیران سیستم آشناتر است: در Bash، Python و غیره.

چندین اپراتور K8s برای PostgreSQL وجود دارد:

  • استولون;
  • اپراتور PostgreSQL داده های ترد.
  • اپراتور Zalando Postgres.

بیایید دقیق تر به آنها نگاه کنیم.

انتخاب اپراتور

علاوه بر ویژگی های مهمی که قبلاً در بالا ذکر شد، ما - به عنوان مهندسان عملیات زیرساخت Kubernetes - از اپراتورها نیز انتظار داریم:

  • استقرار از Git و با منابع سفارشی;
  • پشتیبانی ضد قرابت غلاف؛
  • نصب گره افینیتی یا انتخابگر گره.
  • نصب تلرانس ها؛
  • در دسترس بودن قابلیت های تنظیم؛
  • فناوری ها و حتی دستورات قابل درک.

بدون پرداختن به جزئیات در مورد هر یک از نکات (اگر پس از خواندن کل مقاله هنوز سؤالی در مورد آنها دارید، در نظرات بپرسید)، به طور کلی خاطرنشان می کنم که این پارامترها برای توصیف دقیق تر تخصصی گره های خوشه ای مورد نیاز هستند تا آنها را برای برنامه های خاص سفارش دهید. به این ترتیب می توانیم به تعادل مطلوب از نظر عملکرد و هزینه دست یابیم.

حالا بیایید به سراغ خود اپراتورهای PostgreSQL برویم.

1. استولون

استولون از شرکت ایتالیایی Sorint.lab در گزارش قبلا ذکر شد به عنوان نوعی استاندارد در بین اپراتورها برای DBMS در نظر گرفته شد. این یک پروژه نسبتاً قدیمی است: اولین انتشار عمومی آن در نوامبر 2015 (!)، و مخزن GitHub تقریباً 3000 ستاره و بیش از 40 مشارکت کننده دارد.

در واقع، استولون یک نمونه عالی از معماری متفکرانه است:

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما
دستگاه این اپراتور را می توانید به طور کامل در گزارش یا مستندات پروژه. به طور کلی، کافی است بگوییم که می تواند همه چیزهایی را که توضیح داده شده انجام دهد: failover، پروکسی برای دسترسی شفاف به کلاینت، پشتیبان گیری... علاوه بر این، پروکسی ها دسترسی را از طریق یک سرویس نقطه پایانی فراهم می کنند - بر خلاف دو راه حل دیگر که در زیر توضیح داده شد (هر کدام دارای دو سرویس برای دسترسی به پایگاه).

با این حال، استولون بدون منابع سفارشی، به همین دلیل است که نمی توان آن را به گونه ای مستقر کرد که ایجاد نمونه های DBMS در Kubernetes آسان و سریع باشد - "مانند کیک های داغ". مدیریت از طریق ابزار انجام می شود stolonctl، استقرار از طریق نمودار Helm انجام می شود و موارد سفارشی در ConfigMap تعریف و مشخص می شوند.

از یک طرف، معلوم می شود که اپراتور واقعا یک اپراتور نیست (در نهایت، از CRD استفاده نمی کند). اما از سوی دیگر، این یک سیستم منعطف است که به شما امکان می دهد منابع را در K8s به دلخواه خود پیکربندی کنید.

به طور خلاصه، برای ما شخصاً ایجاد نمودار جداگانه برای هر پایگاه داده بهینه به نظر نمی رسد. بنابراین، ما شروع به جستجوی جایگزین کردیم.

2. Crunchy Data PostgreSQL Operator

اپراتور از Crunchy Data، یک استارتاپ جوان آمریکایی، جایگزین منطقی به نظر می رسید. تاریخچه عمومی آن با اولین انتشار در مارس 2017 آغاز می شود، از آن زمان مخزن GitHub کمتر از 1300 ستاره و 50+ مشارکت کننده دریافت کرده است. آخرین نسخه از سپتامبر برای کار با Kubernetes 1.15-1.18، OpenShift 3.11+ و 4.4+، GKE و VMware Enterprise PKS 1.3+ آزمایش شد.

معماری اپراتور Crunchy Data PostgreSQL نیز شرایط ذکر شده را برآورده می کند:

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما

مدیریت از طریق ابزار مفید انجام می شود pgoبا این حال، به نوبه خود منابع سفارشی را برای Kubernetes ایجاد می کند. بنابراین، اپراتور ما را به عنوان کاربران بالقوه خوشحال کرد:

  • کنترل از طریق CRD وجود دارد.
  • مدیریت راحت کاربر (همچنین از طریق CRD)؛
  • ادغام با سایر اجزا Crunchy Data Container Suite - مجموعه ای تخصصی از تصاویر ظرف برای PostgreSQL و ابزارهای کاربردی برای کار با آن (از جمله pgBackRest، pgAudit، برنامه های افزودنی از contrib و غیره).

با این حال، تلاش برای شروع استفاده از اپراتور از Crunchy Data چندین مشکل را نشان داد:

  • امکان تحمل وجود نداشت - فقط nodeSelector ارائه شده است.
  • پادهای ایجاد شده بخشی از Deployment بودند، علیرغم این واقعیت که ما یک برنامه stateful مستقر کردیم. برخلاف StatefulSets، Deployments نمی توانند دیسک ایجاد کنند.

آخرین اشکال منجر به لحظات خنده‌داری می‌شود: در محیط آزمایشی موفق شدیم 3 نسخه را با یک دیسک اجرا کنیم. ذخیره سازی محلی، باعث می شود که اپراتور گزارش دهد که 3 ماکت کار می کنند (حتی اگر نبودند).

یکی دیگر از ویژگی های این اپراتور ادغام آماده آن با سیستم های مختلف پشتیبانی است. به عنوان مثال، نصب pgAdmin و pgBounce و در آن آسان است مستندات Grafana و Prometheus از پیش تنظیم شده در نظر گرفته شده اند. اخیرا انتشار 4.5.0-beta1 ادغام بهبود یافته با پروژه به طور جداگانه ذکر شده است pgMonitor، به لطف آن اپراتور تجسم واضحی از معیارهای PgSQL خارج از جعبه ارائه می دهد.

با این حال، انتخاب عجیب منابع تولید شده توسط Kubernetes ما را به نیاز به یافتن راه حل متفاوتی سوق داد.

3. اپراتور Zalando Postgres

ما محصولات Zalando را برای مدت طولانی می شناسیم: ما تجربه استفاده از Zalenium را داریم و البته سعی کردیم حامی راه حل محبوب HA آنها برای PostgreSQL است. درباره رویکرد شرکت به ایجاد اپراتور Postgres یکی از نویسندگان آن، الکسی کلیوکین، روی آنتن گفت Postgres-سه شنبه شماره 5، و ما آن را دوست داشتیم.

این جوانترین راه حل مورد بحث در مقاله است: اولین نسخه در اوت 2018 انجام شد. با این حال، حتی با وجود تعداد کمی از نسخه های رسمی، این پروژه راه طولانی را پیموده است، و در حال حاضر از نظر محبوبیت از راه حل Crunchy Data با بیش از 1300 ستاره در GitHub و حداکثر تعداد مشارکت کنندگان (70+) فراتر رفته است.

"زیر هود" این اپراتور از راه حل های آزمایش شده با زمان استفاده می کند:

  • حامی و اسپیلو برای رانندگی،
  • WAL-E - برای پشتیبان گیری،
  • PgBouncer - به عنوان یک استخر اتصال.

به این ترتیب معماری اپراتور از Zalando ارائه شده است:

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما

اپراتور به طور کامل از طریق منابع سفارشی مدیریت می شود، به طور خودکار یک StatefulSet از کانتینرها ایجاد می کند، که سپس می تواند با افزودن کارگاه های جانبی مختلف به غلاف سفارشی شود. همه اینها در مقایسه با اپراتور Crunchy Data یک مزیت قابل توجه است.

از آنجایی که ما راه حل را از بین 3 گزینه در نظر گرفته شده از Zalando انتخاب کردیم، توضیحات بیشتر در مورد قابلیت های آن در زیر بلافاصله همراه با تمرین کاربرد ارائه خواهد شد.

با اپراتور Postgres از Zalando تمرین کنید

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

پس از نصب، باید نگران راه اندازی باشید ذخیره سازی برای لاگ ها و نسخه های پشتیبان. این کار از طریق ConfigMap انجام می شود postgres-operator در فضای نامی که اپراتور را در آن نصب کرده اید. هنگامی که مخازن پیکربندی شدند، می توانید اولین خوشه PostgreSQL خود را مستقر کنید.

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

apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
 name: staging-db
spec:
 numberOfInstances: 3
 patroni:
   synchronous_mode: true
 postgresql:
   version: "12"
 resources:
   limits:
     cpu: 100m
     memory: 1Gi
   requests:
     cpu: 100m
     memory: 1Gi
 sidecars:
 - env:
   - name: DATA_SOURCE_URI
     value: 127.0.0.1:5432
   - name: DATA_SOURCE_PASS
     valueFrom:
       secretKeyRef:
         key: password
         name: postgres.staging-db.credentials
   - name: DATA_SOURCE_USER
     value: postgres
   image: wrouesnel/postgres_exporter
   name: prometheus-exporter
   resources:
     limits:
       cpu: 500m
       memory: 100Mi
     requests:
       cpu: 100m
       memory: 100Mi
 teamId: staging
 volume:
   size: 2Gi

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

ارزش توجه دارد پنل مدیریت وب - postgres-operator-ui. به همراه اپراتور ارائه می شود و به شما امکان ایجاد و حذف خوشه ها و همچنین کار با نسخه های پشتیبان تهیه شده توسط اپراتور را می دهد.

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما
لیست خوشه های PostgreSQL

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما
مدیریت پشتیبان گیری

یکی دیگر از ویژگی های جالب پشتیبانی است Teams API. این مکانیسم به طور خودکار ایجاد می کند نقش ها در PostgreSQL، بر اساس لیست حاصل از نام های کاربری. سپس API به شما امکان می دهد لیستی از کاربرانی را که نقش ها به طور خودکار برای آنها ایجاد می شود، برگردانید.

مشکلات و راه حل آنها

با این حال، استفاده از اپراتور به زودی چندین کاستی قابل توجه را نشان داد:

  1. عدم پشتیبانی nodeSelector.
  2. عدم توانایی در غیرفعال کردن پشتیبان گیری؛
  3. هنگام استفاده از تابع ایجاد پایگاه داده، امتیازات پیش فرض ظاهر نمی شوند.
  4. گاهی اوقات اسناد گم شده یا قدیمی هستند.

خوشبختانه بسیاری از آنها قابل حل هستند. بیایید از پایان شروع کنیم - مشکلات با مستندات.

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

  1. نیاز به یک راز؛
  2. آن را به عنوان پارامتر به اپراتور ارسال کنید pod_environment_secret_name در CRD با تنظیمات اپراتور یا در ConfigMap (بسته به نحوه نصب اپراتور).

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

اگر پارامترهای پشتیبان گیری را به اپراتور ارسال کنید، یعنی - wal_s3_bucket و به کلیدهای AWS S3 دسترسی پیدا کنید، سپس آن از همه چیز نسخه پشتیبان تهیه خواهد کرد: نه تنها پایه در تولید، بلکه صحنه سازی. این به ما نمی خورد

در توضیح پارامترهای Spilo، که در هنگام استفاده از اپراتور، بسته بندی پایه Docker برای PgSQL است، مشخص شد: می توانید یک پارامتر را ارسال کنید. WAL_S3_BUCKET خالی، در نتیجه پشتیبان گیری غیرفعال می شود. علاوه بر این، در کمال خوشحالی، متوجه شدم روابط عمومی آماده، که ما بلافاصله آن را در چنگال خود پذیرفتیم. حالا فقط باید اضافه کنید enableWALArchiving: false به یک منبع خوشه PostgreSQL.

بله، با اجرای 2 اپراتور فرصتی برای انجام متفاوت وجود داشت: یکی برای مرحله بندی (بدون پشتیبان) و دومی برای تولید. اما ما توانستیم به یکی بسنده کنیم.

خوب، ما یاد گرفتیم که چگونه دسترسی به پایگاه‌های داده را برای S3 انتقال دهیم و نسخه‌های پشتیبان وارد فضای ذخیره‌سازی شدند. چگونه صفحات پشتیبان را در Operator UI کار کنیم؟

مروری کوتاه بر بیانیه های PostgreSQL برای Kubernetes، انتخاب ها و تجربه ما

شما باید 3 متغیر را به رابط کاربری اپراتور اضافه کنید:

  • SPILO_S3_BACKUP_BUCKET
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

پس از این، مدیریت پشتیبان‌گیری در دسترس خواهد بود، که در مورد ما کار با مرحله‌بندی را ساده می‌کند و به ما امکان می‌دهد تا برش‌هایی از تولید را بدون اسکریپت‌های اضافی در آنجا تحویل دهیم.

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

چرا اینطور است؟ علیرغم اینکه در کد وجود دارد لازم GRANT، همیشه مورد استفاده قرار نمی گیرند. 2 روش وجود دارد: syncPreparedDatabases и syncDatabasesاست. به syncPreparedDatabases - علیرغم اینکه در بخش preparedDatabases وجود دارد یک شرط وجود دارد defaultRoles и defaultUsers برای ایجاد نقش ها، حقوق پیش فرض اعمال نمی شود. ما در حال آماده سازی یک پچ هستیم تا این حقوق به طور خودکار اعمال شود.

و آخرین نکته در بهبودهایی که به ما مربوط است - وصله، که Node Affinity را به StatefulSet ایجاد شده اضافه می کند. مشتریان ما اغلب ترجیح می دهند با استفاده از نمونه های نقطه ای هزینه ها را کاهش دهند، و به وضوح ارزش میزبانی خدمات پایگاه داده را ندارند. این مشکل از طریق تحمل ها قابل حل است، اما وجود Node Affinity اعتماد به نفس بیشتری می دهد.

چه اتفاقی افتاد؟

بر اساس نتایج حل مشکلات فوق، ما اپراتور Postgres را از Zalando وارد کردیم مخزن شما، جایی که با چنین پچ های مفیدی جمع آوری می شود. و برای راحتی بیشتر، ما نیز جمع آوری کردیم تصویر داکر.

لیست روابط عمومی پذیرفته شده در فورک:

بسیار عالی خواهد بود اگر جامعه از این PR ها پشتیبانی کند تا با نسخه بعدی اپراتور (1.6) در بالادستی قرار گیرند.

جایزه! داستان موفقیت مهاجرت تولید

اگر از Patroni استفاده می کنید، می توان تولید زنده را با حداقل زمان خرابی به اپراتور منتقل کرد.

Spilo به شما این امکان را می دهد که از طریق S3 SXNUMX کلاسترهای آماده به کار ایجاد کنید وال ای، زمانی که log باینری PgSQL ابتدا در S3 ذخیره می شود و سپس توسط ماکت پمپ می شود. اما اگر دارید چه باید بکنید هیچ توسط Wal-E در زیرساخت های قدیمی استفاده می شود؟ راه حل این مشکل در حال حاضر است پیشنهاد شد در هاب

تکرار منطقی PostgreSQL به کمک می آید. با این حال، در مورد نحوه ایجاد نشریات و اشتراک‌ها به جزئیات نمی‌پردازیم، زیرا... طرح ما یک شکست بود.

واقعیت این است که پایگاه داده چندین جدول بارگذاری شده با میلیون ها ردیف داشت که علاوه بر این، دائماً پر و حذف می شدند. اشتراک ساده с copy_data، هنگامی که ماکت جدید تمام محتویات را از Master کپی می کند، به سادگی نمی تواند با master هماهنگ شود. کپی کردن محتوا برای یک هفته جواب داد، اما هرگز به استاد نرسید. در نهایت به من کمک کرد تا مشکل را حل کنم مقاله همکاران از Avito: شما می توانید داده ها را با استفاده از pg_dump. من نسخه (کمی اصلاح شده) خود از این الگوریتم را شرح خواهم داد.

ایده این است که می‌توانید یک اشتراک غیرفعال را به یک شکاف تکراری خاص متصل کنید و سپس شماره تراکنش را تصحیح کنید. ماکت هایی برای کارهای تولیدی موجود بود. این مهم است زیرا ماکت به ایجاد یک Dump ثابت و ادامه دریافت تغییرات از Master کمک می کند.

دستورات بعدی که فرآیند مهاجرت را توصیف می کنند از نمادهای میزبان زیر استفاده می کنند:

  1. استاد - سرور منبع؛
  2. کپی 1 - پخش ماکت در تولید قدیمی؛
  3. کپی 2 - ماکت منطقی جدید

طرح مهاجرت

1. یک اشتراک در Master برای همه جداول موجود در طرحواره ایجاد کنید public پایه dbname:

psql -h master -d dbname -c "CREATE PUBLICATION dbname FOR ALL TABLES;"

2. یک اسلات Replication در Master ایجاد کنید:

psql -h master -c "select pg_create_logical_replication_slot('repl', 'pgoutput');"

3. تکرار را روی ماکت قدیمی متوقف کنید:

psql -h replica1 -c "select pg_wal_replay_pause();"

4. شماره تراکنش را از استاد دریافت کنید:

psql -h master -c "select replay_lsn from pg_stat_replication where client_addr = 'replica1';"

5. زباله را از ماکت قدیمی بردارید. ما این کار را در چندین رشته انجام خواهیم داد که به سرعت بخشیدن به روند کمک می کند:

pg_dump -h replica1 --no-publications --no-subscriptions -O -C -F d -j 8 -f dump/ dbname

6. Dump را در سرور جدید آپلود کنید:

pg_restore -h replica2 -F d -j 8 -d dbname dump/

7. پس از دانلود dump، می توانید همانند سازی را روی ماکت جریان شروع کنید:

psql -h replica1 -c "select pg_wal_replay_resume();"

7. بیایید یک اشتراک در یک ماکت منطقی جدید ایجاد کنیم:

psql -h replica2 -c "create subscription oldprod connection 'host=replica1 port=5432 user=postgres password=secret dbname=dbname' publication dbname with (enabled = false, create_slot = false, copy_data = false, slot_name='repl');"

8. بیایید بگیریم oid اشتراک ها:

psql -h replica2 -d dbname -c "select oid, * from pg_subscription;"

9. فرض کنید دریافت شد oid=1000. بیایید شماره تراکنش را برای اشتراک اعمال کنیم:

psql -h replica2 -d dbname -c "select pg_replication_origin_advance('pg_1000', 'AA/AAAAAAAA');"

10. بیایید همانندسازی را شروع کنیم:

psql -h replica2 -d dbname -c "alter subscription oldprod enable;"

11. وضعیت اشتراک را بررسی کنید، تکرار باید کار کند:

psql -h replica2 -d dbname -c "select * from pg_replication_origin_status;"
psql -h master -d dbname -c "select slot_name, restart_lsn, confirmed_flush_lsn from pg_replication_slots;"

12. پس از شروع تکثیر و همگام سازی پایگاه های داده، می توانید جابجا شوید.

13. پس از غیرفعال کردن replication، باید دنباله ها را تصحیح کنید. این به خوبی توضیح داده شده است در مقاله wiki.postgresql.org.

به لطف این طرح، جابجایی با کمترین تاخیر صورت گرفت.

نتیجه

اپراتورهای Kubernetes به شما اجازه می دهند تا اقدامات مختلف را با کاهش آنها به ایجاد منابع K8s ساده کنید. با این حال، با دستیابی به اتوماسیون قابل توجه با کمک آنها، لازم به یادآوری است که می تواند تعدادی از تفاوت های ظریف غیر منتظره را نیز به همراه داشته باشد، بنابراین اپراتورهای خود را عاقلانه انتخاب کنید.

با در نظر گرفتن سه اپراتور محبوب Kubernetes برای PostgreSQL، پروژه را از Zalando انتخاب کردیم. و ما مجبور بودیم بر مشکلات خاصی با آن غلبه کنیم، اما نتیجه واقعاً خوشایند بود، بنابراین قصد داریم این تجربه را به برخی دیگر از نصب های PgSQL گسترش دهیم. اگر تجربه استفاده از راه حل های مشابه را دارید، خوشحال خواهیم شد که جزئیات را در نظرات مشاهده کنید!

PS

در وبلاگ ما نیز بخوانید:

منبع: www.habr.com

اضافه کردن نظر