چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast

در این مقاله در مورد چگونگی و چرایی توسعه صحبت خواهیم کرد سیستم تعامل – مکانیزمی که اطلاعات را بین برنامه های کاربردی کلاینت و سرورهای 1C: Enterprise انتقال می دهد - از تعیین یک کار تا تفکر در معماری و جزئیات پیاده سازی.

سیستم تعامل (که از این پس SV نامیده می شود) یک سیستم پیام رسانی توزیع شده و مقاوم در برابر خطا با تحویل تضمین شده است. SV به عنوان یک سرویس با بار بالا با مقیاس پذیری بالا طراحی شده است که هم به عنوان یک سرویس آنلاین (ارائه شده توسط 1C) و هم به عنوان یک محصول تولید انبوه که می تواند در امکانات سرور شخصی شما مستقر شود در دسترس است.

SV از ذخیره سازی توزیع شده استفاده می کند فندقی و موتور جستجو ارزیابی جستجو. همچنین در مورد جاوا و نحوه مقیاس افقی PostgreSQL صحبت خواهیم کرد.
چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast

بیانیه مشکل

برای روشن شدن اینکه چرا سیستم تعامل را ایجاد کردیم، کمی در مورد نحوه عملکرد توسعه برنامه های کاربردی تجاری در 1C به شما خواهم گفت.

برای شروع، کمی در مورد ما برای کسانی که هنوز نمی دانند ما چه کار می کنیم :) ما در حال ساخت پلت فرم فناوری 1C: Enterprise هستیم. این پلتفرم شامل یک ابزار توسعه برنامه های کاربردی تجاری و همچنین یک زمان اجرا است که به برنامه های تجاری اجازه می دهد تا در یک محیط چند پلتفرمی اجرا شوند.

پارادایم توسعه کلاینت-سرور

برنامه های تجاری ایجاد شده در 1C: Enterprise در سه سطح کار می کنند مشتری-سرور معماری "DBMS - سرور برنامه - مشتری". کد برنامه نوشته شده در زبان داخلی 1Cرا می توان روی سرور برنامه یا کلاینت اجرا کرد. تمام کارها با اشیاء برنامه (دایرکتوری ها، اسناد و غیره) و همچنین خواندن و نوشتن پایگاه داده، فقط بر روی سرور انجام می شود. عملکرد فرم ها و رابط فرمان نیز بر روی سرور پیاده سازی شده است. مشتری دریافت، باز کردن و نمایش فرم ها، "ارتباط" با کاربر (هشدارها، سؤالات ...)، محاسبات کوچک در فرم هایی که نیاز به پاسخ سریع دارند (به عنوان مثال، ضرب قیمت در مقدار)، کار با فایل های محلی، کار با تجهیزات

در کد برنامه، سرصفحه‌های رویه‌ها و توابع باید به صراحت نشان دهند که کد کجا اجرا می‌شود - با استفاده از دستورالعمل‌های &AtClient / &AtServer (&AtClient / &AtServer در نسخه انگلیسی زبان). توسعه دهندگان 1C اکنون با گفتن اینکه دستورالعمل ها در واقع هستند، من را تصحیح می کنند بیش از، اما برای ما اکنون این مهم نیست.

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

چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast
کدی که یک کلیک دکمه را کنترل می کند: فراخوانی رویه سرور از سوی مشتری کارساز خواهد بود، فراخوانی رویه مشتری از سرور کارساز نخواهد بود.

این بدان معناست که اگر بخواهیم پیامی از سرور به برنامه مشتری ارسال کنیم، مثلاً تولید یک گزارش طولانی مدت به پایان رسیده است و می توان گزارش را مشاهده کرد، چنین روشی نداریم. شما باید از ترفندهایی استفاده کنید، به عنوان مثال، به طور دوره ای سرور را از روی کد مشتری نظرسنجی کنید. اما این رویکرد سیستم را با تماس‌های غیرضروری بارگذاری می‌کند و به طور کلی خیلی ظریف به نظر نمی‌رسد.

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

خود تولید

مکانیزم پیام رسانی ایجاد کنید. سریع، قابل اعتماد، با تحویل تضمینی، با قابلیت جستجوی انعطاف پذیر پیام ها. بر اساس مکانیسم، یک پیام رسان (پیام ها، تماس های ویدیویی) که در برنامه های 1C اجرا می شود، پیاده سازی کنید.

سیستم را طوری طراحی کنید که به صورت افقی مقیاس پذیر باشد. افزایش بار باید با افزایش تعداد گره ها پوشش داده شود.

اجرا

ما تصمیم گرفتیم که بخش سرور SV را مستقیماً در پلت فرم 1C: Enterprise ادغام نکنیم، بلکه آن را به عنوان یک محصول جداگانه پیاده سازی کنیم که API آن را می توان از کد راه حل های کاربردی 1C فراخوانی کرد. این کار به دلایل مختلفی انجام شد، یکی از اصلی‌ترین آنها این بود که من می‌خواستم امکان تبادل پیام بین برنامه‌های مختلف 1C را فراهم کنم (مثلاً بین مدیریت تجارت و حسابداری). برنامه های مختلف 1C می توانند بر روی نسخه های مختلف پلت فرم 1C: Enterprise اجرا شوند، در سرورهای مختلف قرار گیرند و غیره. در چنین شرایطی، اجرای SV به عنوان یک محصول جداگانه واقع در "کنار" تاسیسات 1C راه حل بهینه است.

بنابراین، تصمیم گرفتیم SV را به عنوان یک محصول جداگانه تولید کنیم. توصیه می‌کنیم که شرکت‌های کوچک از سرور CB که در فضای ابری خود (wss://1cdialog.com) نصب کرده‌ایم استفاده کنند تا از هزینه‌های سربار مربوط به نصب و پیکربندی محلی سرور جلوگیری کنند. مشتریان بزرگ ممکن است توصیه کنند که سرور CB خود را در تأسیسات خود نصب کنند. ما از رویکرد مشابهی در محصول ابری SaaS خود استفاده کردیم 1cFresh - به عنوان یک محصول تولید انبوه برای نصب در سایت های مشتریان تولید می شود و همچنین در فضای ابری ما مستقر می شود. https://1cfresh.com/.

درخواست

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

ارتباط بین مشتری و سرور از طریق وب سوکت است. این به خوبی برای سیستم های زمان واقعی مناسب است.

حافظه پنهان توزیع شده

ما بین Redis، Hazelcast و Ehcache را انتخاب کردیم. سال 2015 است. Redis به تازگی یک خوشه جدید منتشر کرده است (خیلی جدید، ترسناک)، Sentinel با محدودیت های زیادی وجود دارد. Ehcache نمی داند چگونه در یک خوشه جمع شود (این قابلیت بعدا ظاهر شد). ما تصمیم گرفتیم آن را با Hazelcast 3.4 امتحان کنیم.
Hazelcast به صورت خوشه ای خارج از جعبه مونتاژ می شود. در حالت تک گره، خیلی مفید نیست و فقط می تواند به عنوان کش استفاده شود - نمی داند چگونه داده ها را به دیسک تخلیه کند، اگر تنها گره را از دست بدهید، داده ها را از دست می دهید. ما چندین Hazelcast را مستقر می کنیم که بین آنها از داده های حیاتی نسخه پشتیبان تهیه می کنیم. ما از حافظه نهان نسخه پشتیبان تهیه نمی کنیم - برایمان مهم نیست.

برای ما، Hazelcast این است:

  • ذخیره سازی جلسات کاربر هر بار رفتن به پایگاه داده برای یک جلسه زمان زیادی طول می کشد، بنابراین ما تمام جلسات را در Hazelcast قرار می دهیم.
  • حافظه پنهان اگر به دنبال پروفایل کاربری هستید، کش را بررسی کنید. یک پیام جدید نوشت - آن را در حافظه پنهان قرار دهید.
  • موضوعاتی برای ارتباط بین نمونه های برنامه. گره یک رویداد تولید می کند و آن را در موضوع Hazelcast قرار می دهد. سایر گره های برنامه مشترک در این موضوع رویداد را دریافت و پردازش می کنند.
  • قفل های خوشه ای به عنوان مثال، ما یک بحث با استفاده از یک کلید منحصر به فرد ایجاد می کنیم (بحث تک نفره در پایگاه داده 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

ما بررسی کردیم که کانالی وجود ندارد. ما قفل را گرفتیم، دوباره آن را بررسی کردیم و آن را ایجاد کردیم. اگر بعد از گرفتن قفل قفل را بررسی نکنید، این احتمال وجود دارد که موضوع دیگری نیز در آن لحظه بررسی شود و اکنون سعی کند همان بحث را ایجاد کند - اما قبلاً وجود دارد. شما نمی توانید با استفاده از قفل جاوا همگام یا معمولی قفل کنید. از طریق پایگاه داده - کند است، و حیف است برای پایگاه داده؛ از طریق Hazelcast - این چیزی است که شما نیاز دارید.

انتخاب یک DBMS

ما تجربه گسترده و موفقی در کار با PostgreSQL و همکاری با توسعه دهندگان این DBMS داریم.

با یک خوشه PostgreSQL آسان نیست - وجود دارد XL, XC, سیتوس، اما به طور کلی اینها NoSQLهایی نیستند که خارج از جعبه مقیاس شوند. ما NoSQL را به عنوان ذخیره‌سازی اصلی در نظر نمی‌گرفتیم، همین که Hazelcast را که قبلاً با آن کار نکرده بودیم، کافی بود.

اگر شما نیاز به مقیاس بندی یک پایگاه داده رابطه ای دارید، به این معنی است خرد کردن. همانطور که می دانید با شاردینگ دیتابیس را به قسمت های جداگانه تقسیم می کنیم تا هر کدام از آنها در سرور جداگانه قرار گیرد.

اولین نسخه از اشتراک گذاری ما توانایی توزیع هر یک از جداول برنامه ما را در سرورهای مختلف به نسبت های مختلف در نظر گرفت. پیام های زیادی در سرور A وجود دارد - لطفاً، اجازه دهید بخشی از این جدول را به سرور B منتقل کنیم. این تصمیم به سادگی در مورد بهینه سازی زودرس فریاد می زد، بنابراین تصمیم گرفتیم خود را به رویکرد چند مستاجر محدود کنیم.

به عنوان مثال، می توانید در مورد چند مستاجر در وب سایت بخوانید داده های سیتوس.

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

ما یک پایگاه داده اصلی داریم که در آن یک جدول مسیریابی با اطلاعات مربوط به مکان همه پایگاه های داده مشترکین ذخیره می شود.

چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast

برای جلوگیری از گلوگاه بودن پایگاه داده اصلی، جدول مسیریابی (و سایر داده های اغلب مورد نیاز) را در حافظه پنهان نگه می داریم.

اگر پایگاه داده مشترک شروع به کند شدن کند، آن را به پارتیشن های داخل برش می دهیم. در پروژه های دیگر استفاده می کنیم pg_pathman.

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

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

Elastics جستجو برای جستجو

از آنجایی که، در میان چیزهای دیگر، SV نیز یک پیام رسان است، به جستجوی سریع، راحت و منعطف با در نظر گرفتن مورفولوژی و با استفاده از تطبیق های نادقیق نیاز دارد. ما تصمیم گرفتیم چرخ را دوباره اختراع نکنیم و از موتور جستجوی رایگان Elasticsearch که بر اساس کتابخانه ایجاد شده است استفاده کنیم لوسن. ما همچنین Elasticsearch را در یک خوشه (master - data - data) مستقر می کنیم تا مشکلات را در صورت خرابی گره های برنامه حذف کنیم.

در github پیدا کردیم پلاگین مورفولوژی روسی برای Elasticsearch و از آن استفاده کنید. در فهرست Elasticsearch ما ریشه های کلمات (که پلاگین تعیین می کند) و N-gram را ذخیره می کنیم. همانطور که کاربر متنی را برای جستجو وارد می کند، متن تایپ شده را در میان N-gram ها جستجو می کنیم. هنگامی که در فهرست ذخیره می شود، کلمه "متن" به N-گرم های زیر تقسیم می شود:

[آنها، تک، تک، متن، متن، ek، سابق، متن، متن، ks، kst، ksty، st، sty، شما]،

و ریشه کلمه "متن" نیز حفظ خواهد شد. این روش به شما امکان می دهد در ابتدا، وسط و انتهای کلمه جستجو کنید.

تصویر کلی

چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast
تصویر را از ابتدای مقاله تکرار کنید، اما با توضیحات:

  • بالانس در معرض اینترنت؛ ما nginx داریم، می تواند هر کدام باشد.
  • نمونه های برنامه جاوا از طریق Hazelcast با یکدیگر ارتباط برقرار می کنند.
  • برای کار با یک سوکت وب استفاده می کنیم Netty.
  • برنامه جاوا به زبان جاوا 8 نوشته شده است و از باندل ها تشکیل شده است OSGi. این برنامه ها شامل مهاجرت به جاوا 10 و انتقال به ماژول ها است.

توسعه و آزمایش

در فرآیند توسعه و آزمایش SV، با تعدادی از ویژگی های جالب محصولاتی که استفاده می کنیم مواجه شدیم.

تست بار و نشت حافظه

انتشار هر نسخه SV شامل تست بار است. زمانی موفقیت آمیز است که:

  • تست برای چند روز کار کرد و هیچ مشکلی در سرویس وجود نداشت
  • زمان پاسخ برای عملیات کلیدی از آستانه راحت فراتر نمی رود
  • افت عملکرد نسبت به نسخه قبلی بیش از 10٪ نیست

ما پایگاه داده آزمایشی را با داده ها پر می کنیم - برای انجام این کار، اطلاعاتی در مورد فعال ترین مشترک از سرور تولید دریافت می کنیم، اعداد آن را در 5 ضرب می کنیم (تعداد پیام ها، بحث ها، کاربران) و به این ترتیب آن را آزمایش می کنیم.

ما تست بار سیستم تعامل را در سه پیکربندی انجام می دهیم:

  1. سنجش استرس
  2. فقط اتصالات
  3. ثبت نام مشترک

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

به عنوان مثال، بخشی از تست استرس به این صورت است:

  • کاربر وارد می شود
    • بحث های خوانده نشده شما را درخواست می کند
    • احتمال خواندن پیام ها 50 درصد است
    • احتمال ارسال پیامک 50 درصد است
    • کاربر بعدی:
      • 20 درصد شانس ایجاد یک بحث جدید دارد
      • هر یک از بحث های خود را به صورت تصادفی انتخاب می کند
      • می رود داخل
      • درخواست پیام ها، پروفایل های کاربر
      • پنج پیام خطاب به کاربران تصادفی از این بحث ایجاد می کند
      • بحث را ترک می کند
      • 20 بار تکرار می شود
      • از سیستم خارج می شود، به ابتدای فیلمنامه برمی گردد

    • یک چت بات وارد سیستم می شود (پیام را از کد برنامه شبیه سازی می کند)
      • 50% شانس ایجاد یک کانال جدید برای تبادل داده (بحث ویژه) دارد
      • 50% احتمال دارد برای هر یک از کانال های موجود پیام بنویسد

سناریوی "فقط اتصالات" به دلیلی ظاهر شد. یک وضعیت وجود دارد: کاربران سیستم را متصل کرده اند، اما هنوز درگیر نشده اند. هر کاربر ساعت 09:00 صبح کامپیوتر را روشن می کند و با سرور ارتباط برقرار می کند و سکوت می کند. این افراد خطرناک هستند، تعداد زیادی از آنها وجود دارد - تنها بسته هایی که دارند PING/PONG است، اما اتصال به سرور را حفظ می کنند (نمی توانند آن را حفظ کنند - اگر یک پیام جدید وجود داشته باشد چه می شود). این تست وضعیتی را بازتولید می کند که در آن تعداد زیادی از این کاربران سعی می کنند در نیم ساعت وارد سیستم شوند. این شبیه یک تست استرس است، اما تمرکز آن دقیقا بر روی همین ورودی اول است - به طوری که هیچ شکستی وجود ندارد (فردی از سیستم استفاده نمی کند، و در حال حاضر سقوط می کند - فکر کردن به چیزی بدتر دشوار است).

اسکریپت ثبت نام مشترک از اولین راه اندازی شروع می شود. ما یک تست استرس انجام دادیم و مطمئن بودیم که سیستم در طول مکاتبات کند نمی شود. اما کاربران آمدند و به دلیل تایم اوت ثبت نام با شکست مواجه شد. هنگام ثبت نام استفاده کردیم / dev / تصادفی، که مربوط به آنتروپی سیستم است. سرور زمان کافی برای جمع آوری آنتروپی را نداشت و هنگامی که یک SecureRandom جدید درخواست شد، برای ده ها ثانیه متوقف شد. راه‌های زیادی برای خروج از این وضعیت وجود دارد، به عنوان مثال: به /dev/urandom کمتر امن بروید، یک برد ویژه که آنتروپی ایجاد می‌کند نصب کنید، اعداد تصادفی را از قبل تولید کنید و آنها را در یک استخر ذخیره کنید. ما به طور موقت مشکل استخر را بسته ایم، اما از آن به بعد تست جداگانه ای برای ثبت نام مشترکین جدید انجام داده ایم.

ما به عنوان یک مولد بار استفاده می کنیم JMeter. نمی داند چگونه با وب سوکت کار کند، به یک افزونه نیاز دارد. اولین نتایج جستجو برای پرس و جو "jmeter websocket" عبارتند از: مقالات از BlazeMeter، که توصیه می کنند پلاگین توسط Maciej Zaleski.

از همین جا تصمیم گرفتیم شروع کنیم.

تقریباً بلافاصله پس از شروع آزمایش جدی، متوجه شدیم که JMeter شروع به نشت حافظه کرد.

این افزونه یک داستان بزرگ جداگانه است؛ با 176 ستاره، 132 فورک در github دارد. خود نویسنده از سال 2015 به آن متعهد نشده است (ما آن را در سال 2015 گرفتیم، پس از آن سوء ظنی ایجاد نکرد)، چندین مشکل github در مورد نشت حافظه، 7 درخواست کشش بسته نشده.
اگر تصمیم به انجام تست بار با استفاده از این افزونه دارید، لطفاً به بحث های زیر توجه کنید:

  1. در یک محیط چند رشته ای، از یک LinkedList معمولی استفاده شد و نتیجه آن شد NPE در زمان اجرا این را می توان با تغییر به ConcurrentLinkedDeque یا با بلوک های هماهنگ حل کرد. ما اولین گزینه را برای خود انتخاب کردیم (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. نشت حافظه؛ هنگام قطع اتصال، اطلاعات اتصال حذف نمی شود (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. در حالت استریم (زمانی که سوکت وب در انتهای نمونه بسته نمی شود، اما بعداً در طرح استفاده می شود)، الگوهای پاسخ کار نمی کنند (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

این یکی از موارد موجود در github است. چه کار کردیم:

  1. گرفته اند چنگال الیران کوگان (@elyrank) - مشکلات 1 و 3 را برطرف می کند
  2. حل مشکل 2
  3. اسکله به روز شده از 9.2.14 به 9.3.12
  4. پیچیده SimpleDateFormat در ThreadLocal. SimpleDateFormat ایمن نیست، که منجر به NPE در زمان اجرا شد
  5. رفع نشت حافظه دیگر (اتصال در هنگام قطع شدن به اشتباه بسته شد)

و با این حال جریان دارد!

حافظه نه در یک روز، بلکه در دو روز شروع به از بین رفتن کرد. مطلقاً زمانی باقی نمانده بود، بنابراین تصمیم گرفتیم تا موضوعات کمتری را راه اندازی کنیم، اما در چهار عامل. این باید حداقل برای یک هفته کافی باشد.

دو روز گذشت...

اکنون حافظه Hazelcast در حال اتمام است. لاگ ها نشان دادند که بعد از چند روز آزمایش، Hazelcast شروع به شکایت از کمبود حافظه کرد و پس از مدتی خوشه از هم پاشید و گره ها یکی یکی به مردن ادامه دادند. ما JVisualVM را به hazelcast متصل کردیم و یک "اره در حال افزایش" را دیدیم - آن را به طور مرتب GC صدا می زد، اما نمی توانست حافظه را پاک کند.

چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast

معلوم شد که در hazelcast 3.4، هنگام حذف نقشه / multiMap (map.destroy())، حافظه به طور کامل آزاد نمی شود:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

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

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

ویزوف:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap برای هر اشتراک ایجاد شد و در صورت عدم نیاز حذف شد. تصمیم گرفتیم که Map را شروع کنیم ، کلید نام اشتراک خواهد بود و مقادیر، شناسه های جلسه (که در صورت لزوم می توانید شناسه های کاربر را از آنها دریافت کنید).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

نمودارها بهبود یافته اند.

چگونه و چرا یک سرویس مقیاس پذیر با بار بالا برای 1C نوشتیم: Enterprise: Java, PostgreSQL, Hazelcast

چه چیز دیگری در مورد تست بار یاد گرفته ایم؟

  1. JSR223 باید به صورت groovy نوشته شود و حافظه پنهان کامپایل را در خود جای دهد - بسیار سریعتر است. پیوند.
  2. نمودارهای Jmeter-Plugins نسبت به استانداردهای معمولی قابل درک هستند. پیوند.

درباره تجربه ما با Hazelcast

Hazelcast یک محصول جدید برای ما بود، ما از نسخه 3.4.1 کار با آن را شروع کردیم، اکنون سرور تولید ما نسخه 3.9.2 را اجرا می کند (در زمان نگارش آخرین نسخه Hazelcast 3.10 است).

تولید شناسه

ما با شناسه های عدد صحیح شروع کردیم. بیایید تصور کنیم که برای یک موجودیت جدید به Long دیگری نیاز داریم. توالی در پایگاه داده مناسب نیست، جداول درگیر اشتراک گذاری هستند - معلوم می شود که یک پیام ID=1 در DB1 و یک پیام ID=1 در DB2 وجود دارد، شما نمی توانید این شناسه را در Elasticsearch یا Hazelcast قرار دهید. ، اما بدترین چیز این است که می خواهید داده های دو پایگاه داده را در یک پایگاه داده ترکیب کنید (مثلاً تصمیم بگیرید که یک پایگاه داده برای این مشترکین کافی است). می توانید چندین AtomicLongs را به Hazelcast اضافه کنید و شمارنده را در آنجا نگه دارید، سپس عملکرد به دست آوردن شناسه جدید افزایش می یابد و زمان درخواست به Hazelcast را دریافت کنید. اما Hazelcast چیزی بهینه تر دارد - FlakeIdGenerator. هنگام تماس با هر مشتری، محدوده ID به آنها داده می شود، به عنوان مثال، اولی - از 1 تا 10، دومی - از 000 تا 10 و غیره. اکنون مشتری می تواند به تنهایی شناسه های جدیدی صادر کند تا زمانی که محدوده صادر شده برای او به پایان برسد. به سرعت کار می کند، اما وقتی برنامه (و کلاینت Hazelcast) را مجددا راه اندازی می کنید، یک دنباله جدید شروع می شود - از این رو پرش ها و غیره. علاوه بر این، توسعه دهندگان واقعاً نمی دانند که چرا شناسه ها عدد صحیح هستند، اما بسیار ناسازگار هستند. همه چیز را سنجیدیم و به UUID تغییر دادیم.

به هر حال، برای کسانی که می خواهند مانند توییتر باشند، چنین کتابخانه Snowcast وجود دارد - این اجرای Snowflake در بالای Hazelcast است. شما می توانید آن را در اینجا مشاهده کنید:

github.com/noctarius/snowcast
github.com/twitter/snowflake

اما ما دیگر به آن نرسیدیم.

TransactionalMap.replace

شگفتی دیگر: TransactionalMap.replace کار نمی کند. در اینجا یک آزمایش وجود دارد:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

من مجبور شدم جایگزین خودم را با استفاده از getForUpdate بنویسم:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

نه تنها ساختارهای داده معمولی، بلکه نسخه های تراکنشی آنها را نیز آزمایش کنید. این اتفاق می افتد که IMap کار می کند، اما TransactionalMap دیگر وجود ندارد.

یک JAR جدید بدون خرابی وارد کنید

ابتدا تصمیم گرفتیم اشیاء کلاس های خود را در Hazelcast ضبط کنیم. به عنوان مثال، ما یک کلاس Application داریم، می خواهیم آن را ذخیره کرده و بخوانیم. صرفه جویی:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

می خوانیم:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

همه چیز کار می کند. سپس تصمیم گرفتیم یک فهرست در Hazelcast بسازیم تا بر اساس:

map.addIndex("subscriberId", false);

و هنگام نوشتن یک موجودیت جدید، آنها شروع به دریافت ClassNotFoundException کردند. Hazelcast سعی کرد به ایندکس اضافه کند، اما چیزی در مورد کلاس ما نمی دانست و می خواست یک JAR با این کلاس به آن عرضه شود. ما دقیقاً این کار را انجام دادیم، همه چیز کار کرد، اما یک مشکل جدید ظاهر شد: چگونه می توان JAR را بدون توقف کامل کلاستر به روز کرد؟ Hazelcast JAR جدید را در طول به روز رسانی گره به گره دریافت نمی کند. در این مرحله تصمیم گرفتیم که بدون جستجوی فهرست زندگی کنیم. به هر حال، اگر از Hazelcast به عنوان یک فروشگاه با ارزش کلیدی استفاده کنید، همه چیز کار می کند؟ نه واقعا. در اینجا نیز رفتار IMap و TransactionalMap متفاوت است. جایی که IMap اهمیتی نمی‌دهد، TransactionalMap خطا می‌دهد.

IMap. ما 5000 شی می نویسیم، آنها را می خوانیم. همه چیز مورد انتظار است.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

اما در یک تراکنش کار نمی کند، یک ClassNotFoundException دریافت می کنیم:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

در 3.8، مکانیزم استقرار کلاس کاربر ظاهر شد. می توانید یک گره اصلی تعیین کنید و فایل JAR را روی آن به روز کنید.

اکنون رویکرد خود را کاملاً تغییر داده‌ایم: خودمان آن را به JSON سریال می‌کنیم و در Hazelcast ذخیره می‌کنیم. Hazelcast نیازی به دانستن ساختار کلاس های ما ندارد و می توانیم بدون توقف به روز رسانی کنیم. نسخه بندی اشیاء دامنه توسط برنامه کنترل می شود. نسخه‌های مختلف برنامه می‌توانند به طور همزمان اجرا شوند، و موقعیتی ممکن است که برنامه جدید اشیایی را با فیلدهای جدید بنویسد، اما برنامه قدیمی هنوز از این فیلدها اطلاعی ندارد. و در همان زمان، برنامه جدید اشیاء نوشته شده توسط برنامه قدیمی را که فیلدهای جدیدی ندارند، می خواند. ما چنین موقعیت‌هایی را در برنامه مدیریت می‌کنیم، اما برای سادگی، فیلدها را تغییر یا حذف نمی‌کنیم، فقط با افزودن فیلدهای جدید، کلاس‌ها را گسترش می‌دهیم.

چگونه عملکرد بالا را تضمین می کنیم

چهار سفر به Hazelcast - خوب، دو به پایگاه داده - بد

رفتن به حافظه پنهان برای داده ها همیشه بهتر از رفتن به پایگاه داده است، اما شما نمی خواهید رکوردهای استفاده نشده را نیز ذخیره کنید. ما تصمیم گیری در مورد اینکه چه چیزی را در حافظه پنهان کنیم را به آخرین مرحله توسعه واگذار می کنیم. هنگامی که عملکرد جدید کدگذاری می شود، ثبت همه پرس و جوها را در PostgreSQL روشن می کنیم (log_min_duration_statement به 0) و آزمایش بار را به مدت 20 دقیقه اجرا می کنیم. با استفاده از گزارش های جمع آوری شده، ابزارهایی مانند pgFouine و pgBadger می توانند گزارش های تحلیلی بسازند. در گزارش ها، ما در درجه اول به دنبال پرس و جوهای کند و مکرر هستیم. برای پرس و جوهای کند، ما یک برنامه اجرایی (EXPLAIN) می سازیم و ارزیابی می کنیم که آیا چنین پرس و جوی می تواند تسریع شود یا خیر. درخواست های مکرر برای داده های ورودی یکسان به خوبی در حافظه نهان قرار می گیرد. ما سعی می کنیم پرس و جوها را "مسطح" نگه داریم، یک جدول در هر پرس و جو.

عملیات

SV به عنوان یک سرویس آنلاین در بهار 2017 به بهره برداری رسید و به عنوان یک محصول جداگانه، SV در نوامبر 2017 (در آن زمان در وضعیت نسخه بتا) منتشر شد.

در بیش از یک سال فعالیت، هیچ مشکل جدی در عملکرد سرویس آنلاین CB وجود نداشته است. ما خدمات آنلاین را از طریق نظارت می کنیم Zabbix، جمع آوری و استقرار از خیزران.

توزیع سرور SV در قالب بسته های بومی عرضه می شود: RPM، DEB، MSI. به علاوه برای ویندوز، ما یک نصب کننده واحد به شکل یک EXE ارائه می دهیم که سرور، Hazelcast و Elasticsearch را روی یک دستگاه نصب می کند. ما در ابتدا به این نسخه از نصب به عنوان نسخه "دمو" اشاره کردیم، اما اکنون مشخص شده است که این محبوب ترین گزینه استقرار است.

منبع: www.habr.com

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