به افتخار اول آوریل، Reddit صفحه ای را برای کاربران باز کرد تا نظرات خود را بیان کنند، اما این پروژه که به عنوان یک شوخی ایجاد شد، به نوعی دیوار برای گرافیتی جمعی از شرکت کنندگان از سراسر جهان تبدیل شد و نشان می داد که آنها می خواهند چه ردپایی در تاریخ بگذارند. .
در ۱ آوریل، Reddit پروژه Place را راهاندازی کرد، صفحهای با بوم خالی که هر کاربر انجمن میتوانست هر تصویری را روی آن بکشد. هنرمندان محدودیتهایی داشتند: آنها فقط میتوانستند هر پنج دقیقه یک پیکسل از یکی از ۱۶ رنگ را بکشند و اندازههای بوم نیز محدود بود. پیکسل های دیگر را می توان روی پیکسل های ترسیم شده ترسیم کرد (و سپس نویسنده اول می تواند دوباره پیکسل خود را روی پیکسل حریف بکشد)، به همین دلیل است که نویسندگان تصاویر به طور پیشینی درگیر می شوند. همانطور که در توضیحات بوم گفته شد، پروژه به این صورت طراحی شده است خلاقیت مشترک- "هر یک از شما می توانید چیزی منحصر به فرد ایجاد کنید. با هم می توانید چیزی بزرگتر خلق کنید."
در لحظه شروع پروژه، همه به معنای واقعی کلمه به بوم حمله کردند - هر پیکسل روی آن پر شد. در ابتدا، کاربران به سادگی رنگها را در پیکسلهای آزاد قرار میدادند، اما سپس تیمها شروع به شکلگیری کردند و شروع به ترسیم یکی از سادهترین ایدههای ممکن کردند که افراد همفکر را پیدا کرد. پرچم های ملی. هر چه کاربران بیشتر روی بوم کار کنند، جالب تر و جذاب تر است ایده های پیچیدهبه ذهنشان رسید. و "مکان" از یک پروژه اول آوریل به مکانی تبدیل شده است که در آن کاربران در سراسر جهان در جوامع واقعی متحد می شوند تا چیزی را به جهان نشان دهند و از خلق خود از گروه هایی از مهاجمان حمایت کنند که واقعاً فقط می خواهند نوعی از خود را ترسیم کنند. طراحی
در اولین ساعات کار قرار دهید
"مکان" به یک تابلوی برچسب آنلاین تبدیل شده است، اما هر کدام با آن ساخته می شوند با سختی زیاد. به عنوان مثال، برای ترسیم و محافظت از یک لوگوی لینوکس با ابعاد 48 در 68 پیکسل، 3264 نفر باید به طور همزمان این کار را انجام دهند.
کاربران متحد می شوند تا ایده های کوچکتر و ساده تر را اجرا کنند، اما هنوز هم ایده های بسیار تیمی، مانند این ردیف قلب ها با پرچم های کشورهای مختلف (و نه تنها): همه مسئول قلب "خود" هستند، اما همه افراد با هم به طور غیرارادی یک تیم را تشکیل می دهند.
و دیگران تمام بوم های متن را می نویسند، به عنوان مثال، مانند این گروه از طرفداران " جنگ ستارگان"، که نوشتند و حمایت کردند مونولوگ معروفصدراعظم پالپاتین در مورد سیث دارث پلاگیس از قسمت سوم حماسه فضایی.
با این حال، برخی از کاربران شکایت داشتند که شرکت کنندگان پروژه خود را با ربات هایی جایگزین می کنند که به طور خودکار پیکسل های اشغال شده توسط نویسنده را هر پنج دقیقه به روز می کنند. علیرغم این واقعیت که هنگام ترسیم هر پیکسل، کاربر باید مجموعه ای از اقدامات را تکرار کند (به عنوان مثال، انتخاب یک رنگ خاص)، برخی از آنها توانستند مکانیسم امنیتی را دور بزنند و ربات هایی ایجاد کنند که برای آنها تصاویر ترسیم می کنند.
با این حال، کسانی هستند که هنوز تاپیک های خاصی ایجاد می کنند و سعی می کنند افراد همفکری را تشویق کنند که به ترسیم کردن کمک می کنند و برای نسل های آینده چیزی مانند... استفراغ ریک سانچز از کارتون "ریک و مورتی" را تشویق می کنند. جدی؟ مطمئنی که اینها ربات نیستند؟
پس از 72 ساعت کار، پروژه تعطیل شد. اداره منابع از همه برای مشارکت و این واقعیت که مردم متحد شدند "برای ایجاد چیزی بیشتر" تشکر کرد.
Reddit اغلب به مکانی برای فعالیت های اجتماعی مختلف تبدیل می شود. به عنوان مثال، اخیراً کاربران غمگین این منبع تصمیم گرفتند بپرسند که بیدار شدن هر روز با لبخند چگونه است؟ و قبل از آن، خوانندگان Reddit داستان هایی درباره دختران با یکدیگر به اشتراک گذاشتند. این فقط افراد ناشناس نیستند که از این منبع محبوب استفاده می کنند: این بازیگر اخیراً چندین ساعت به سوالات کاربران در مورد فیلم "Trainspotting" پاسخ داده است.
برای شروع، تعیین الزامات پروژه اول آوریل بسیار مهم بود، زیرا باید بدون "اورکلاک" راه اندازی می شد تا همه کاربران Reddit بلافاصله به آن دسترسی داشته باشند. اگر از همان ابتدا بی نقص کار نمی کرد، به سختی توجه بسیاری از افراد را به خود جلب می کرد.
- در صورت بروز گلوگاه یا شکست غیرمنتظره باید پیکربندی انعطاف پذیر ارائه شود. یعنی باید بتوانید اندازه برد و فرکانس مجاز ترسیم را در صورت افزایش حجم داده یا فرکانس به روز رسانی خیلی زیاد تنظیم کنید.
اندازه "تخته" باید 1000x1000 کاشی باشد تا بسیار بزرگ به نظر برسد.
همه کلاینت ها باید همگام شوند و وضعیت برد یکسانی را نمایش دهند. پس از همه، اگر کاربران مختلف داشته باشند نسخه های مختلف، برای آنها تعامل دشوار خواهد بود.
شما باید حداقل 100000 کاربر همزمان را پشتیبانی کنید.
کاربران می توانند هر پنج دقیقه یک کاشی قرار دهند. بنابراین، لازم است میانگین نرخ به روز رسانی 100000 کاشی در پنج دقیقه (333 به روز رسانی در ثانیه) حفظ شود.
پروژه نباید بر عملکرد سایر قسمت ها و عملکردهای سایت تأثیر منفی بگذارد (حتی اگر ترافیک زیادی در r/Place وجود داشته باشد).
Backend
راهکارهای اجرایی
مشکل اصلی در ایجاد backend همگام سازی نمایش وضعیت برد برای همه مشتریان بود. تصمیم گرفته شد که مشتریان در زمان واقعی به رویدادهای قرار دادن کاشی گوش دهند و فوراً وضعیت کل تابلو را جویا شوند. اگر قبل از ایجاد حالت کامل در بهروزرسانیها مشترک شده باشید، داشتن یک حالت کامل کمی قدیمی قابل قبول است. هنگامی که مشتری وضعیت کامل را دریافت می کند، تمام کاشی هایی را که در حین انتظار دریافت کرده است نمایش می دهد. تمام کاشی های بعدی باید به محض دریافت روی تابلو ظاهر شوند.
برای اینکه این طرح کار کند، درخواست وضعیت کامل هیئت مدیره باید در اسرع وقت تکمیل شود. در ابتدا، میخواستیم کل تابلو را در یک ردیف در Cassandra ذخیره کنیم و هر درخواست به سادگی آن ردیف را بخواند. فرمت هر ستون در این خط به صورت زیر بود:
(x، y): ("مهر زمان": دوره، "نویسنده": نام_کاربر، "رنگ": رنگ)
اما از آنجایی که تابلو شامل یک میلیون کاشی است، ما نیاز به خواندن یک میلیون ستون داشتیم. در خوشه تولید ما، این تا 30 ثانیه طول کشید، که غیرقابل قبول بود و میتواند منجر به بارگذاری بیش از حد بر روی کاساندرا شود.
سپس تصمیم گرفتیم کل برد را در Redis ذخیره کنیم. ما یک میدان بیتی از یک میلیون عدد چهار بیتی گرفتیم، که هر کدام می توانست یک رنگ چهار بیتی را رمزگذاری کند، و مختصات x و y با آفست (offset = x + 1000y) در فیلد بیت تعیین شد. برای به دست آوردن وضعیت کامل برد، کل فیلد بیت باید خوانده می شد.
امکان بهروزرسانی کاشیها با بهروزرسانی مقادیر در افستهای خاص (بدون نیاز به مسدود کردن یا انجام کل فرآیند خواندن/بهروزرسانی/نوشتن) وجود داشت. اما تمام جزئیات هنوز باید در Cassandra ذخیره شود تا کاربران بتوانند بفهمند چه کسی و چه زمانی هر یک از کاشی ها را قرار داده است. همچنین قصد داشتیم از کاساندرا برای بازیابی برد هنگام سقوط ردیس استفاده کنیم. خواندن کل برد از روی آن کمتر از 100 میلی ثانیه طول کشید که بسیار سریع بود.
در اینجا نحوه ذخیره رنگ ها در Redis با استفاده از یک نمونه برد 2x2 آمده است:
ما نگران بودیم که ممکن است در Redis با توان خواندن مواجه شویم. اگر بسیاری از مشتریان به طور همزمان متصل یا به روز شوند، همه آنها به طور همزمان درخواست هایی را برای وضعیت کامل برد ارسال می کنند. از آنجایی که هیئت مدیره یک دولت جهانی مشترک را نشان می داد، راه حل واضح استفاده از حافظه پنهان بود. ما تصمیم گرفتیم در سطح CDN (سریع) حافظه پنهان کنیم زیرا پیاده سازی آن آسان تر بود و کش نزدیک ترین به کلاینت ها به دست آمد که زمان دریافت پاسخ را کاهش داد.
درخواستها برای حالت فول برد توسط Fastly با تایم اوت یک ثانیه ذخیره شدند. برای جلوگیری از تعداد زیادیدرخواستهایی که زمانی که مهلت زمانی منقضی شد، از سرصفحه stale-while-revalidate استفاده کردیم. Fastly از حدود 33 POP که به طور مستقل یکدیگر را ذخیره می کنند، پشتیبانی می کند، بنابراین انتظار داشتیم تا 33 درخواست حالت فول برد در هر ثانیه دریافت کنیم.
برای انتشار بهروزرسانیها برای همه مشتریان، از سرویس وب سوکت خود استفاده کردیم. ما قبلاً با موفقیت از آن برای تقویت Reddit.Live با بیش از 100000 کاربر همزمان برای اعلانهای پیام خصوصی زنده و سایر ویژگیها استفاده کردیم. سرویس همچنین سنگ بنای پروژههای گذشته اول آوریل ما، The Button و Robin بود. در مورد r/Place، مشتریان از اتصالات وب سوکت برای دریافت بهروزرسانیهای بیدرنگ در محلهای کاشی پشتیبانی میکردند.
API
گرفتن حالت فول برد
در ابتدا درخواست ها به Fastly می رفت. اگر یک نسخه معتبر از برد داشت، بلافاصله بدون تماس با سرورهای برنامه Reddit آن را برمی گرداند. اگر نه، یا کپی خیلی قدیمی بود، برنامه Reddit برد کامل را از Redis می خواند و آن را به Fastly برمی گرداند تا کش شود و به مشتری بازگردانده شود.
توجه داشته باشید که نرخ درخواست هرگز به 33 در ثانیه نرسید، به این معنی که کش با Fastly در محافظت از برنامه Reddit در برابر اکثر درخواستها بسیار موثر بود.
و هنگامی که درخواست ها به برنامه رسید، Redis خیلی سریع پاسخ داد.
کشیدن کاشی
مراحل کشیدن کاشی:
- مهر زمانی آخرین قرار دادن کاشی توسط کاربر از Cassandra خوانده می شود. اگر کمتر از پنج دقیقه پیش بود، هیچ کاری انجام نمیدهیم و یک خطا به کاربر برمیگردد.
- جزئیات کاشی به ردیس و کاساندرا نوشته شده است.
- زمان فعلی در کاساندرا به عنوان آخرین باری که کاربر کاشی قرار داده بود ثبت می شود.
- سرویس websocket پیامی در مورد کاشی جدید به همه مشتریان متصل ارسال می کند.
برای حفظ ثبات دقیق، تمام نوشتن و خواندن در Cassandra با استفاده از لایه قوام QUORUM انجام شد.
در واقع، ما در اینجا مسابقه ای داشتیم که در آن کاربران می توانستند چندین کاشی را در یک زمان قرار دهند. در مراحل 1 تا 3 هیچ مسدودی وجود نداشت، بنابراین تلاشهای همزمان برای کشیدن کاشیها میتوانست در مرحله اول بررسی را پشت سر بگذارد و در مرحله دوم کشیده شود. به نظر می رسد که برخی از کاربران این باگ را کشف کردند (یا از ربات هایی استفاده کردند که محدودیت تعداد درخواست ها را نادیده گرفتند) - و در نتیجه، حدود 15000 کاشی با استفاده از آن قرار گرفتند (~0.09٪ از کل).
نرخ درخواست و زمان پاسخ اندازه گیری شده توسط برنامه Reddit:
اوج میزان قرارگیری کاشی تقریباً 200 در ثانیه بود. این کمتر از حد تخمینی ما یعنی 333 کاشی در ثانیه است (میانگین با فرض اینکه 100000 کاربر هر پنج دقیقه کاشی را قرار می دهند).
دریافت جزئیات برای یک کاشی خاص
هنگام درخواست کاشی های خاص، داده ها مستقیماً از Cassandra خوانده می شد.
نرخ درخواست و زمان پاسخ اندازه گیری شده توسط برنامه Reddit:
این درخواست بسیار محبوب شد. علاوه بر درخواستهای مشتری معمولی، افراد اسکریپتهایی را برای بازیابی کل تخته یک کاشی در یک زمان نوشتهاند. از آنجایی که این درخواست در CDN ذخیره نمی شد، همه درخواست ها توسط برنامه Reddit ارائه می شدند.
زمان پاسخگویی به این درخواست ها بسیار کوتاه بود و در طول عمر پروژه در همان سطح باقی ماند.
سوکت های وب
ما معیارهای جداگانه ای نداریم که نشان دهد r/Place چگونه بر سرویس وب سوکت تأثیر گذاشته است. اما می توان با مقایسه داده ها قبل از شروع پروژه و پس از اتمام آن، مقادیر را تخمین زد.
تعداد کل اتصالات به سرویس وب سوکت:
بار پایه قبل از راه اندازی r/Place حدود 20000 اتصال بود، اوج آن 100000 اتصال بود. بنابراین در اوج ما احتمالاً حدود 80000 کاربر همزمان به r/Place متصل بودند.
توان عملیاتی سرویس Websocket:
در اوج بار روی r/Place، سرویس وب سوکت بیش از 4 گیگابیت در ثانیه (150 مگابیت در ثانیه در هر نمونه، در مجموع 24 نمونه) انتقال داد.
Frontend: مشتریان وب و موبایل
در فرآیند ایجاد فرانتاند برای Place، ما مجبور بودیم بسیاری از مشکلات پیچیده مربوط به توسعه بین پلتفرمی را حل کنیم. ما میخواستیم این پروژه روی همه پلتفرمهای اصلی از جمله دسکتاپ و دستگاه های تلفن همراهدر iOS و اندروید.
رابط کاربری باید سه عملکرد مهم را انجام می داد:
- نمایش وضعیت تابلو در زمان واقعی
- به کاربران اجازه تعامل با برد را بدهید.
- روی همه پلتفرم ها از جمله برنامه های موبایل کار کنید.
هدف اصلی رابط بوم بود و Canvas API برای آن ایده آل بود. ما از عنصر استفاده کردیم
کشیدن بوم
بوم باید وضعیت تخته را در زمان واقعی منعکس می کرد. لازم بود وقتی صفحه بارگذاری شد، کل تابلو ترسیم شود و بهروزرسانیهای ترسیمی که از طریق وبسوکتها میآمدند، تمام شود. یک عنصر بوم که از رابط CanvasRenderingContext2D استفاده می کند می تواند به سه روش به روز شود:
- با استفاده از drawImage() یک تصویر موجود را روی بوم بکشید.
- رسم اشکال با استفاده از روش های مختلففرم های نقاشی به عنوان مثال، fillRect() یک مستطیل را با مقداری رنگ پر می کند.
- یک شی ImageData بسازید و با استفاده از ()putImageData آن را روی بوم بکشید.
گزینه اول برای ما مناسب نبود زیرا تابلویی به شکل یک تصویر تمام شده نداشتیم. که گزینههای 2 و 3 باقی مانده است. سادهترین راه این بود که کاشیها را با استفاده از fillRect() بهروزرسانی کنید: وقتی یک بهروزرسانی از طریق websocket میآید، ما به سادگی یک مستطیل 1x1 را در موقعیت (x, y) ترسیم میکنیم. به طور کلی، روش کار می کرد، اما برای ترسیم حالت اولیه تخته خیلی راحت نبود. روش putImageData() بسیار بهتر بود: میتوانستیم رنگ هر پیکسل را در یک شیء ImageData مشخص کنیم و کل بوم را یکجا ترسیم کنیم.
ترسیم حالت اولیه تابلو
استفاده از putImageData() مستلزم تعریف وضعیت برد به صورت Uint8ClampedArray است که در آن هر مقدار یک عدد هشت بیتی بدون علامت در محدوده 0 تا 255 است. هر مقدار نشان دهنده یک کانال رنگی (قرمز، سبز، آبی، آلفا) و هر یک پیکسل به چهار عنصر در آرایه نیاز دارد. یک بوم 2x2 به یک آرایه 16 بایتی نیاز دارد که در آن چهار بایت اول نشان دهنده پیکسل بالای سمت چپ بوم و چهار بایت آخر نشان دهنده پیکسل پایین سمت راست است.
در اینجا نحوه ارتباط پیکسل های بوم با نمایش های Uint8ClampedArray آنها آمده است:
برای بوم پروژه ما به آرایه ای از چهار میلیون بایت نیاز داشتیم - 4 مگابایت.
در باطن، وضعیت برد به صورت یک فیلد چهار بیتی ذخیره می شود. هر رنگ با عددی از 0 تا 15 نشان داده می شود که به ما امکان می دهد دو پیکسل را در هر بایت قرار دهیم. برای استفاده از آن در دستگاه مشتری، باید سه کار را انجام دهید:
- انتقال داده های باینری از API ما به مشتری.
- داده ها را باز کنید.
- تبدیل رنگ های چهار بیتی به 32 بیتی.
برای انتقال دادههای باینری، ما از Fetch API در مرورگرهایی که از آن پشتیبانی میکنند استفاده کردیم. و در مواردی که پشتیبانی نمی کنند، استفاده کردیم XMLHttpRequestبا answerType بر روی "arraybuffer" تنظیم شده است.
داده های باینری دریافتی از API شامل دو پیکسل در هر بایت است. کوچکترین سازنده TypedArray که داشتیم به شما امکان می دهد با داده های باینری در قالب واحدهای تک بایتی کار کنید. اما استفاده از آنها در دستگاه های مشتری دشوار است، بنابراین ما داده ها را باز کردیم تا کار با آنها آسان تر شود. فرآیند ساده است: ما از طریق دادههای بستهبندی شده، بیتهای مرتبه بالا و پایین را بیرون آوردیم و سپس آنها را در بایتهای جداگانه در آرایه دیگری کپی کردیم.
در نهایت، رنگ های چهار بیتی باید به 32 بیت تبدیل می شدند.
ساختار ImageData که برای استفاده از putImageData() نیاز داشتیم، مستلزم آن است که نتیجه نهایی یک Uint8ClampedArray با بایتهایی باشد که کانالهای رنگی را به ترتیب RGBA کدگذاری میکنند. این به این معنی است که ما نیاز داشتیم یک فشرده سازی دیگر انجام دهیم، هر رنگ را به بایت های کانال کامپوننت بشکنیم و آنها را در فهرست درست قرار دهیم. انجام چهار نوشتن در هر پیکسل خیلی راحت نیست. اما خوشبختانه گزینه دیگری هم وجود داشت.
اشیاء TypedArray اساساً نمایش آرایه ای از ArrayBuffer هستند. در اینجا یک اخطار وجود دارد: چندین نمونه TypedArray می توانند در یک نمونه ArrayBuffer بخوانند و بنویسند. به جای نوشتن چهار مقدار در یک آرایه هشت بیتی، می توانیم یک مقدار را در یک آرایه 32 بیتی بنویسیم! با استفاده از Uint32Array برای نوشتن، ما توانستیم به راحتی رنگ های کاشی را با به روز رسانی یک شاخص آرایه به روز کنیم. با این حال، ما مجبور بودیم پالت رنگ خود را به ترتیب بایت بزرگ (ABGR) ذخیره کنیم تا بایت ها به طور خودکار در مکان های صحیح در هنگام خواندن با استفاده از Uint8ClampedArray قرار گیرند.
پردازش به روز رسانی های دریافت شده از طریق وب سوکت
متد drawRect() برای ترسیم بهروزرسانیها برای پیکسلهای جداگانه به هنگام دریافت آنها خوب بود، اما یک چیز وجود داشت. نقطه ضعف: دسته بزرگی از بهروزرسانیهایی که به طور همزمان میرسند میتوانند منجر به کندی سرعت مرورگرها شوند. و ما فهمیدیم که بهروزرسانیهای وضعیت هیئت مدیره ممکن است اغلب انجام شود، بنابراین مشکل باید به نحوی حل میشد.
بهجای اینکه هر بار که یک بهروزرسانی از طریق وبسوکت دریافت میشود، بلافاصله بوم را دوباره ترسیم کنیم، تصمیم گرفتیم آن را به گونهای انجام دهیم که بهروزرسانیهای وبسوکت که در همان زمان میرسند را بتوان دستهبندی و رندر کرد. برای رسیدن به این هدف، دو تغییر ایجاد شد:
- استفاده از drawRect() را متوقف کنید - ما پیدا کردیم راه راحتبا استفاده از ()putImageData، تعداد زیادی پیکسل را در یک زمان به روز کنید.
- انتقال رندر بوم به حلقه requestAnimationFrame.
با انتقال رندر به حلقه انیمیشن، میتوانیم بلافاصله بهروزرسانیهای وب سوکت را در یک ArrayBuffer بنویسیم و در عین حال رندر واقعی را به تعویق بیندازیم. همه بهروزرسانیهای وب سوکت که بین فریمها (حدود 16 میلیثانیه) میرسند، بهطور همزمان دستهبندی و ارائه شدند. به لطف استفاده از requestAnimationFrame، اگر رندر بیش از حد طول بکشد (بیش از 16 میلی ثانیه)، تنها بر نرخ تازه سازی بوم تأثیر می گذارد (به جای کاهش عملکرد کل مرورگر).
تعامل با بوم
مهم است که توجه داشته باشید که بوم برای راحت تر کردن تعامل کاربران با سیستم مورد نیاز بود. سناریوی تعامل اصلی، قرار دادن کاشی ها بر روی بوم است.
اما انجام یک رندر دقیق از هر پیکسل در مقیاس 1:1 بسیار دشوار خواهد بود و ما از اشتباه جلوگیری نمی کنیم. بنابراین ما به یک زوم (بزرگ!) نیاز داشتیم. علاوه بر این، کاربران باید بتوانند به راحتی روی بوم حرکت کنند، زیرا برای اکثر صفحهها بسیار بزرگ بود (مخصوصاً هنگام استفاده از زوم).
بزرگنمایی ضربه بزنید؛
از آنجایی که کاربران میتوانند هر پنج دقیقه یک بار کاشیها را قرار دهند، خطاهای قرار دادن بهویژه برای آنها ناامیدکننده خواهد بود. لازم بود بزرگنمایی به اندازه ای اجرا شود که کاشی به اندازه کافی بزرگ باشد و بتوان به راحتی در مکان مناسب قرار گیرد. این به ویژه در دستگاه های صفحه لمسی مهم بود.
ما زوم 40 برابری اجرا کردیم، یعنی هر کاشی اندازه 40x40 داشت. ما عنصر را پیچیدیم