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

سلام وقت بخیر.
من دارم روی یک سایت کار می‌کنم که قراره یه جورایی شبیه به paste bin باشه، با این تفاوت که یک کلیپ برد منیجر هم هست!

سناریو اینطوریه:

  1. سایت همه تکست هایی که شما اد می‌کنید رو توی index نمایش میده.
  2. هر تکستی رو هم می‌تونید براش یک لینک انحصاری درست کنید و تکست رو برای بقیه اشتراک بذارید.
  3. هم توی سایت و هم توی برنامه های کلاینت دسکتاپ و اندروید هر متنی توی کلیپ برد کپی کنید، خودکار توی سایت هم وارد میشه.
    (یه نکته اینکه تکست هرکسی برای خود کاربر پرایوته تا زمانی که منتشرش کنه)

شما از داخل سایت لاگین می‌کنید، بعد مثلا یک تکست اد می‌کنید به دیتابیستون.
حالا برنامه سمت کلاینت چطوری باید تکست جدید رو از سایت بخونه ؟

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

  1. اینکه سایت هر ثانیه برای کلی یوزر باید کوئری بزنه و خب منابع زیادی می‌خاد.
  2. اینکه کلاینت همش داره کلی ترافیک مصرف می‌کنه و داده هایی که حتی داره رو هم باز از سایت می‌خونه.

سایت رو دارم برای یادگیری جنگو درست می‌کنم برای تمرین.
ادرس فعلی سایت اینه. ممنون می‌شم راهنمایی کنید که چطور باید دیتا ها رو سینک کنم که کمترین هزینه برای کاربر و سرور رو داشته باشه.
ممنون.

ادرس موقت سایت:
https://cmng-loading.fandogh.cloud/

۱. در مورد کوعری زدن نمیشه کاریش کرد شاید از ی دیتابیس ساده تر مثل key-value استفاده کنی واسه این کار بهتر باشه - حالا من نمیدونم بقیه هم بیان نظر بدن

۲. میتونی ی کد بهش بدی به عنوان آیدی آخرین رکورد توی دیتابیس - و هردفعه با دادن اون آیدی چک کنی رکوردی بعد از اون آیدی ذخیره شده یا نه

1 Likes

کاریش نمیشه کرد بجز اینکه تایم بین کوئریها رو بیشتر کنید. مثلا دقیقه‌ای یک بار چک بشه (یا حتی هر ۵دقیقه یکبار!) و اگه کلاینت باز شد، همون لحظه یه چک دیکه هم بکنه. چون این زمانیه که مطمئنیم کاربر نیاز به دیتای آپدیت داره.
و یه rate limit هم به صورت کلی روی همه‌ی اکانتها باشه که مثلا در هر ساعت بیشتر از ۲۰۰تا ریکوئست نزنن.

کلاینت میتونه timestamp آخرین آپدیت موفق رو نگه‌داره و هربار که ریکوئست میزنه، اون تایم رو به سرور بفرسته و سرور با استفاده از اون، کوئری بزنه و فقط دیتای جدیدتر رو از دیتابیس بگیره (و به همراه timestamp جدید به کلاینت بفرسته)

تکته: اینکه گفتم timestamp، میتونه یه استرینگ datetime مثل 2021-02-23 14:53:11.649 هم باشه. فقط نکته اینه که تداخل بین timezone کاربر و سرور وجود نداشته باشه و درست هندل بشه.

1 Likes

سلام
ببین کلا که باید cache کنی بجای اینکه هر دفعه همه رو بگیری. تازه سمت سرور هم میتونی روی ردیس آخرین تغییرات رو cache‌کنی (برنامه بزرگتر شه باید بکنی). و یک تکنیک caching کلاسیک توی حوزه سیستم و دیتابیس هست (اسمش خاطرم نیست) که مثل یک checksum (یا مثلا counter) برای آخرین تغییر نگه میداری و کلاینت‌ها اون checksum خودشون رو همراه با درخواست می‌فرستن و اگر تغییری نکرده بود سرور سریع بهش میگه (یعنی دیتا تکراری نمیفرسته). باز برای نگه داشتن این checksum (و همون آخرین تغییرات) برای هر کاربر، یک چیزی مثل ردیس نیاز داری که جلوی دیتابیس اصلیت باشه. ولی برای شروع هم میشه توی همون دیتابیست باشه.

حالا یکم پیچیده‌تر اش، اگر خواستی ۳ روش کلاسیک برای نوشتن و بروزرسانی cache‌هست اینو یک نگاه کن:

2 Likes

سلام
پیشنهاد میکنم در مورد Django Channels بررسی کنید. به کمک webSocket یک ارتباط غیرهمگام(asynchrony) بین سرور و کلاینت برقرار میکنه. هروقت سرور به روز شد به طور اتوماتیک به کلاینت اطلاع میده. تا کنون از این امکان استفاده نکرده ام اما طبق سناریوی شما و مستندات جنگو به نظر میرسه که میتونه برای شما مفید باشه.
ریپوی رسمی: https://github.com/django/channels
برای صرفه جویی در تراکنشهای بین سرور و کلاینت حتما استفاده از GraphQL رو بررسی کنید.

2 Likes

کلا با این پاسخ موافقم ولی مشکلش اینه که redis رو نمیشه به این راحتی توی سرویس رایگان فندوق گذاشت و استفاده کرد.
و اینطور که از سوال به نظر میرسه، هدف یادگیری منطق پروژست.


وب‌سوکت نیاز به منابع خیلی زیادی داره نسبت به restFul. از طرف دیگه، هیچوقت اون پایداری مورد نظر رو نداره و مثلا باید هر یک دقیقه یه ping بفرستیم که بروزر فکر نکنه دیگه به این کانکشن نیاز نداره و بخواد ببندتش (بروزرها هیچ منطقی برای این قضیه ندارن.) از اون عجیبتر اینه که یه‌وقتایی بروزر رو ریفرش میکنیم، ولی بروزر خودش سعی میکنه کانکشن رو نگه داره!
بعد باید حواسمون باشه اگه به خاطر مشکل اینترنت، قطع شد، دوباره وصلش کنیم یا سویچ کنیم به restful (مخصوصا توی موبایل، این مشکل وجود داره که وقتی از یه دکل مخابراتی به یه دکل دیگه وصل میشیم، کانکشن قطع میشه. درحال عادی هم خیلی ناپایدارتر از اینترنت کابلیه)

توی چندتا پروژه درگیر websocket شدم، آخرش به این نتیجه رسیدم که توی پروژه‌ی بعدیم اگه سرعت خیلی برام مهم بود، به جای TCP از UDP استفاده کنم (کاری که اکثر بازیها انجام میدن) ولی سراغ وب‌سوکت نرم!

2 Likes

عرض کردم. میشه توی همون دیتابیس اصلی هم باشه. ولی cache‌باید کنه

1 Likes

سلام. آخرین عدد خیلی مفید و جالبه، api ربات های تلگرام دقیقا چنین کاری می‌کنند.
نیاز من دقیقا این بود که اگر مثلا یک تکست رو هم حذف کرد، در برنامه کلاینت هم اون تکست حذف بشه،
برای همین اخرین id فقط نیاز تکست های جدید رو برطرف می‌کنه. اما از ایده شما و ایده @MalekMFS ایده بهتری گرفتم. و با لطف بقیه دوستانِ تویِ این تاپیک، تکمیل تر هم میشه.

ایده اینشکلی شد که کلاینت هر چند ثانیه یک کوئری بزنه و فقط id اون text هایی که داره رو بگیره.
با این کار id های uniq هر text خودش رو داره. و می‌تونه با یک کوئری دیگه این دیتا رو بگیره.
و کلاینت id ها رو رو توی یک لیست روی لوکال ذخیره کنه، و بعد از چند ثانیه مجدد کوئری بزنه و باز id های
text هاش رو بگیره. اگر آیدی جدید وارد شده بود اون رو با کوئری از سرور بخونه، و اگر id توی لوکال بود اما توی id های جدید نبود، اون رو حذف کنه.

  1. اینطوری هنوز پایبند به api هستم.
  2. کمترین دیتا رو کاربر مصرف می‌کنه.
  3. api رو به روشی درست کردم که وقتی یک یوزر مثلا 5 تا text جدید داشت، کلاینت 5 بار کوئری نزنه و همه رو با یک کوئری دریافت کنه.
  4. با کمک دوستان متوجه شدم که باید از یادگیری redis نترسم و یک redis در جلوی سرور قرار بدم تا بار این کوئری های متعدد رو از سرور کمتر کنه.

به نظر من، این برنامه نباید Data Driven، طراح بشه. باید Event Driven باشه.
یعنی توی دیتابیس، اتفاقاتی که میفته رو ثبت کنید و بعدا از روی اون اتفاقات، خروجی نهایی رو تولید کنید.

اینطوری بعد از یه مدت، حجم هر کوئری به شدت بالا میره.
وقتی کاربر ۱۰۰تا متن توی clipboard داشته باشه، کوئری که باید بزنه میشه یه json که ۱۰۰ تا key-value توش هست. فردا همین تبدیل میشه به ۲۰۰تا key-value

2 Likes

سلام. ممنون بابت این پیشنهاد، حتما باید چنین کاری رو انجام بدم.
همینطور که ذکر کردم سایت من قراره به عنوان یک paste bin هم باشه،‌ پس برای پرفورمنس بیشتر یک dynamic content cache با nginx جلوی سرور قرار می‌دم، توی همین کانفیگ هم یک rate limit هم به پیشنهاد شما می‌نویسم.

مشکل اینجاست که فقط text های جدید برای کاربر ارسال میشه و text های حذف شده همچنان توی کلاینت باقی می‌مونند.

برای اینکه چنین مشکلی پیش نیاد بنظرم باید از unix timestamp استفاده کرد.

حواسم به این قضیه نبود. توی پاسخ بالایی، توی event-driven این مشکل حل میشه.

نه. همون date-time هم اوکیه و به صورت پیشفرض، روی UTC هست. فقط خواستم بگم این یه چیزیه که باید حواستون باشه دستکاری نکنید با فرض اینکه «خب من سرورم الآن فلان کشور هست پس یادم میمونه که دارم timezone رو فلان‌جور تغییر میدم»

1 Likes

راستش وقتی کاربر تعداد کلیپ برد هاش زیاد شد فقط چنتا عدد می‌گیره و این زیاد نیست. فرض کنید تعداد تکست هاش باشه 1110 تا، کار بر فقط 1110 تا خط دیتا دریافت می‌کنه.
درواقع میشه عددی از 0 تا 999 حجم نهایی هم فقط 4320 byte میشه.

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

برای درک بهتر لینک های api با یوزر test رو ببینید:

@pouya-abbassi

این لیک برای چک کردن آی دی های جدید

و این هم برای دریافت فقط یک id

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

اگه event-driven باشه، کاربر فقط همچین چیزی رو دریافت میکنه:

event: delete
text-id: 456273489569

یا:

event: add
text-id: 034985703432
text: "There is an art, or, rather, a knack to flying. The knack lies in learning how to throw yourself at the ground and miss."
2 Likes

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

اینطور که من دارم بهش نگاه میکنم، همچین مشکلی وجود نداره.
هر کلاینتی میاد id آخرین eventی که چک کرده رو ثبت میکنه. بعد اون id رو میفرسته به سرور.
سرور هم میره نگاه میکنه ببینه event جدیدی بعد از اون ثبت شده یا نه. اگه شده بود، پاس میده به کاربر. حالا ممکنه کاربر، یک مرحله عقبتر از سرور باشه یا ۱۰۰ مرحله!
سرور اصلا state نگه نمیداره که بخواد بدونه کلاینت دسکتاپ من تا کجا سینک شده و کلاینت اندروید من تا کجا سینک شده!

2 Likes

از این ایده به روشی دیگه استفاده کردم.

  1. مشکل counter اینه که من مثلا تعداد text کاربر رو می‌شمارم و کلاینت با مقایسه عدد با عددی که قبلا ثبت کرده دیتای جدید رو می‌خونه، اما فرض کنید کاربر یک دیتا اضافه و در همان لحظه یک دیتا حذف کرده باشه. اینطوری مقایسه ها درست هستند اما دیتای جدید به کاربر داده نمی‌شه،

  2. میشه از hash هم استفاده کرد، مثلا از blake2b که خیلی سریع هم هست، اما هش ها complexity دارند و خب این فرایند بار پردازش رو زیاد می‌کرد، و همچنین برای دیتای زیاد بنظرم کند هستند،
    فرض کنید یک کوئری باید اتفاق بیوفته و بعد دیتا hash بشه. هم کند هست و هم اینکه پردازش رو زیاد تر مصرف می‌کنه.

خیلی عالیه. پس من منظور شما رو درست درک نکرده بودم.
یک event جدید رو من متوجه نمی‌شم چیه.در واقع یعنی این باید یک table جدیدی موجود باشه که مشخص کنه که چه چیزی برای چه یوزری اتفاق افتاده ؟

و این که در این تیبل فیلدی برای timestamp باید باشه ؟

سلام، ممنونم بابت مشارک و لطف بزرگتون.

  1. من دانشم از همه کمتره پس با توجه به نظر دوستان استفاده از وب سوکت راه دشواری هست، api ها برای دیگر برنامه نویس هایی که نیاز داشته باشند تا از سایت استفاده کنند راه آسان تری می‌شه.

  2. انگار مشکلاتی که ایجاد می‌کنند زیاده.

  3. و همچنین باز زیادی روی سرور قرار می‌ده انگار.

اصولا توی سیستمهای event driven همه‌چیز توی همون جدول eventها اتفاق میفته. ولی توی این مورد خاص، بله. باید دوتا جدول داشته باشیم. یکی eventها رو ثبت کنه مثلا میتونه ستونهای event-id و state (که یکی از سه وضعیت add یا remove یا update داره) و timestamp داشته باشه و یه text-id که مربوط به اون‌یکی جدوله.
و یه جدول هم فقط text-id و text داشته باشه.

وضعیت add که مشخصه. توی جدول متن‌ها، یه ردیف اضافه میکنیم.
درمورد remove و update دوتا راه دارید:

  • واقعا اون ردیف از جدول متن‌ها رو حذف یا ویرایش کنید.
  • زمان حذف شدن، دیتا رو از جدول پاک نکنید و همیشه باقی بمونه. زمان ویرایش هم نسخه‌ی جدید رو به عنوان یه ستون جدید، به جدول اضافه کنید. (موقع کوئری گرفتن، آخرین ورژن رو به کاربر میفرستیم) (در این حالت، جدول دیتاها، نباید idهای unique داشته باشه چون از هر دیتایی ممکنه چند نسخه داشته باشیم و بهتره که همشون یه id داشته باشن که راحت پیدا بشن)

حالت اول، مدل معمولی و mutable هست که دیتاها توش امکان تغییر دارن.
حالت دوم، state های قدیمی رو نگه‌میداره و قابل بازگشت هست. یه جورایی time machine داره :sunglasses:

شاید حالت دوم برای این پروژه، لازم و حیاتی نباشه ولی مثلا مزیتش اینه که میتونیم بفهمیم این دیتا درگذر زمان، چه تغییراتی کرده و درچه تاریخی چه محتوایی داشته!
این یکی از ویژگیهای خیلی مهم و خیلی به‌دردبخور توی سیستمهای event driven هست.

مثلا اگه پروژه‌ی شما مربوط به حسابهای بانکی باشه، اگه event-driven کار کنید و هیچوقت دیتایی حذف نشه، میتونید یه نمودار نشون بدید از موجودی حساب کاربر، توی بازه‌ی زمانی «سه ماه اول سال جاری»!
اگه پروژه مربوط به آنالیز مصرف سوخت اتومبیل باشه و event-driven پیاده شده باشه، میتونید بررسی کنید اون زمان که فشار باد لاستیک فلان‌قدر بوده و دمای هوا فلان قدر بوده، مصرف سوخت در لحظه چقدر بوده.
اگه پروژه‌ی حضور/غیاب باشه، میتونید ببینید چه کسی چه تایمی اومده و چه تایمی رفته، علاوه بر اینکه میتونید ببینید الآن یا در هر تایمی چندنفر توی محیط حضور داشتن (اگه event-driven نباشه و دیتاها همش override بشن با state فعلی، فقط میتونید ببینید در حال حاضر چند نفر داخل ساختمون هستن. هیچ خروجی دیگه‌ای نمیتونید بگیرید)

3 Likes