Skip to content

فصل ۱۰ — مطالعهٔ موردی: طرحواره‌های پایگاه دادهٔ تکراری

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

این پروژه عمدتاً به این دلیل موفق بود که بر هدف نهایی‌مان — یکپارچه‌سازی جداول پایگاه دادهٔ تکراری — به‌شدت متمرکز ماندیم. یک برنامهٔ اجرایی ساده اما مؤثر نوشتیم (فصل ۴)، با درنگ در ریسک و سرعت اجرا تا بتوانیم راه‌حل را به‌موقع تحویل دهیم. برای جمع‌آوری معیارها رویکردی سبک‌وزن انتخاب کردیم (فصل ۳) و فقط روی چند نقطهٔ دادهٔ کلیدی تمرکز کردیم. هر بار که یک نقطهٔ عطف جدید را تمام می‌کردیم، تغییراتمان را در سراسر تیم مهندسی به‌طور فعال اطلاع می‌دادیم (فصل ۷). ابزار ساختیم تا تغییراتمان پایدار بماند (فصل ۹). در نهایت، با عرضهٔ بی‌درز یک قابلیت جدید بر پایهٔ طرحوارهٔ تازه یکپارچه‌شده، تنها چند هفته پس از اتمام، ارزش بازآرایی را نشان دادیم. این به ما کمک کرد برای شروع بازآرایی‌های بعدی پذیرش بیشتری بگیریم (فصل ۵).

اگرچه بازآرایی بهبودهای عملکردی که می‌خواستیم را به‌دست داد، در مسیر چند اشتباه هم مرتکب شدیم. به‌خاطر فشار شدید از مهم‌ترین مشتریانمان، عجله کردیم و پیش از آنکه بررسی کنیم چرا طرحواره‌ها به هم نزدیک شده‌اند، یا برنامه‌مان را به‌صورت مکتوب برای تیم‌های دیگر در دسترس قرار دهیم (فصل ۴)، کار را شروع کردیم. حمایت گسترده‌تر بین‌وظیفه‌ای نخواستیم (فصل ۵) و بیشتر کار را به تیم کوچک خودمان سپردیم. حتی در آن شرایط هم حفظ شتاب سخت بود و بازآرایی در هفته‌های پایانی کش آمد (فصل ۸).

پیش از ورود به خود بازآرایی، درک اینکه Slack چه می‌کند و اصول کار آن چیست ضروری است. اگر با محصول آشنا نیستید، شدیداً توصیه می‌کنم این بخش را با دقت بخوانید. اگر کاربر عادی Slack هستید، می‌توانید مستقیماً به «معماری Slack ۱۰۱» بروید.

Slack ۱۰۱

Slack در درجهٔ اول ابزاری برای همکاری شرکت‌ها در هر اندازه و صنعتی است. معمولاً یک کسب‌وکار یک workspace در Slack راه می‌اندازد و برای هر کارمند حساب کاربری می‌سازد. به‌عنوان کارمند، می‌توانید برنامه را (روی رایانهٔ رومیزی، موبایل، یا هر دو) دانلود کنید و فوراً با هم‌تیمی‌ها ارتباط بگیرید.

Slack موضوعات و گفتگوها را در کانال‌ها سازمان‌دهی می‌کند. فرض کنید روی قابلیتی کار می‌کنید که به کاربران اجازه می‌دهد فایل‌ها را سریع‌تر در برنامه آپلود کنند. پروژه را «Faster Uploads» می‌نامیم. می‌توانید کانال جدیدی به نام #feature-faster-uploads بسازید و توسعه را با مهندسان، مدیر و مدیر محصول هماهنگ کنید. هر کسی در شرکت که بخواهد بداند توسعهٔ «Faster Uploads» چطور پیش می‌رود، می‌تواند به #feature-faster-uploads برود، تاریخچهٔ اخیر را بخواند یا به گفتگو بپیوندد و مستقیماً از تیم بپرسد.

نمونهٔ ساده‌ای از ظاهر رابط Slack در نیمهٔ اول ۲۰۱۷، حوالی زمان این مطالعهٔ موردی، در شکل ۱۰-۱ دیده می‌شود.

شکل ۱۰-۱. رابط Slack حدود ژانویه ۲۰۱۷

کاربر نمونهٔ ما Matt Kump است، کارمند Acme Sites. نام workspaceای که می‌بینیم بالا سمت چپ است و نام Matt بلافاصله زیر آن.

نوار کناری سمت چپ همهٔ کانال‌های Matt را دارد. فعلاً بخش starred را نادیده می‌گیریم و اول روی بخش Channels تمرکز می‌کنیم. از این فهرست می‌بینیم Matt در گفتگوهایی دربارهٔ هزینه‌های حسابداری (#accounting-costs)، طوفان فکری (#brainstorming)، عملیات کسب‌وکار (#business-ops) و چند مورد دیگر حضور دارد. هر کدام از این کانال‌ها عمومی است؛ یعنی هر کسی با حساب در Acme Sites می‌تواند کانال را پیدا کند، محتوا را ببیند و بپیوندد.

شاید دیده باشید کانال #design-chat قفل کوچکی دارد جایی که بقیه نماد # دارند. این یعنی کانال خصوصی است. فقط اعضای کانال خصوصی می‌توانند آن را پیدا کنند و محتوا را ببینند. برای پیوستن باید توسط عضوی که قبلاً داخل است دعوت شوید.

پایین‌تر در نوار کناری، فهرست پیام‌های مستقیم Matt است. می‌بینیم در چند گفتگوی یک‌به‌یک با هم‌تیمی‌هایی مثل Brandon، Corey و Fayaz است. همچنین در گفتگوی گروهی با Lane و Pavel است؛ مثل پیام مستقیم کار می‌کند، فقط با چند هم‌تیمی به‌جای یک نفر.

تفاوت کانال‌های عمومی و خصوصی وقتی دربارهٔ مشکلات کلیدی این بازآرایی صحبت می‌کنیم اهمیت پیدا می‌کند.

شاید دیده باشید بعضی کانال‌ها در نوار کناری با سفید پررنگ bold شده‌اند. یعنی پیام خوانده‌نشده دارند. اگر Matt #brainstorming را انتخاب کند، محتوای جدید می‌بیند و کانال در نوار کناری مثل بقیه کم‌رنگ می‌شود.

Slack خیلی بیشتر از این‌ها دارد، اما این‌ها مبانی لازم برای درک زمینهٔ تاریخی پیش از این مطالعهٔ موردی است.

معماری Slack ۱۰۱

حالا چند جزء پایه از معماری Slack را که در قلب مطالعهٔ ما هستند بررسی می‌کنیم. مهم است بدانید بعضی از این جزءها پس از بازآرایی این فصل به‌شکل قابل‌توجهی تغییر کرده‌اند؛ جزئیات اینجا دقیقاً معماری امروز Slack را منعکس نمی‌کند.

یک درخواست ساده برای گرفتن تاریخچهٔ پیام یک کانال را در نظر بگیرید. Slack را باز می‌کنم و یکی از کانال‌های موردعلاقه‌ام #core-infra-sourdough (شکل ۱۰-۲) را باز می‌کنم؛ جایی که چند مهندس زیرساخت دربارهٔ نان خمیرترش صحبت می‌کنند.

شکل ۱۰-۲. خواندن آخرین توصیه‌های نان‌پزی در #core-infra-sourdough

اگر ترافیک شبکه را مانیتور می‌کردم، درخواست GET به Slack API برای channels.history با شناسهٔ کانال #core-infra-sourdough می‌دیدم. درخواست اول به load balancer می‌رسد تا به وب‌سرور در دسترس برسد. سپس سرور چند مورد را دربارهٔ درخواست تأیید می‌کند؛ از جمله معتبر بودن token و دسترسی من به کانالی که می‌خواهم بخوانم. اگر دسترسی داشتم، سرور تازه‌ترین پیام‌ها را از پایگاه دادهٔ مناسب می‌گیرد، قالب‌بندی می‌کند و به کلاینت برمی‌گرداند. در چند میلی‌ثانیه محتوای تازهٔ کانال انتخاب‌شده را داشتم.

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

هر workspace به یک database shard واحد اختصاص داشت و همهٔ اطلاعات مرتبطش آنجا ذخیره می‌شد. اگر کاربر عضو workspace بود و می‌خواست فهرست کانال‌های عمومی را ببیند، سرورها ابتدا shard حاوی دادهٔ workspace را پیدا می‌کردند و سپس همان shard را برای کانال‌ها می‌پرسیدند.

اگر مشتری بزرگی رشد می‌کرد و فضای بیشتری در shard مشترک با شرکت‌های دیگر اشغال می‌کرد، شرکت‌های دیگر را به shardهای مختلف منتقل می‌کردیم تا به مشتری در حال رشد فضای بیشتری بدهیم. اگر مشتری تنها ساکن shard خودش بود و همچنان رشد می‌کرد، سخت‌افزار shard را ارتقا می‌دادیم. در مجموع ساختار پایگاه داده‌مان شبیه شکل ۱۰-۳ بود.

شکل ۱۰-۳. چند workspace توزیع‌شده روی shardها

حالا نگاهی به نحوهٔ ذخیرهٔ چند قطعهٔ کلیدی اطلاعات در هر workspace shard می‌اندازیم؛ به‌ویژه کانال‌ها و عضویت کانال. آغاز ۲۰۱۷، Slack چند جدول برای اطلاعات کانال داشت: teams_channels برای کانال‌های عمومی و groups برای کانال‌های خصوصی و پیام مستقیم گروهی (بیش از یک کاربر). هر جدول اطلاعات پایه مثل نام کانال، زمان ایجاد و سازنده داشت. شکل ۱۰-۴ چند ردیف نمونه از دو جدول را نشان می‌دهد.

شکل ۱۰-۴. طرحوارهٔ ساده‌شده برای teams_channels و groups

اطلاعات اعضای آن کانال‌ها را به‌ترتیب در teams_channels_members و groups_members ذخیره می‌کردیم. برای هر عضو، ردیفی با ترکیب یکتا از workspace ID، channel ID و user ID داشتیم. همچنین اطلاعات کلیدی عضویت مثل تاریخ پیوستن و زمان آخرین خواندن محتوا در آن کانال (به‌صورت Unix epoch timestamp) را نگه می‌داشتیم. شکل ۱۰-۵ نشان می‌دهد این دو جدول تقریباً یکسان بودند.

شکل ۱۰-۵. طرحوارهٔ ساده‌شده برای teams_channels_members و groups_members

برای پیام مستقیم، جدول واحدی به نام teams_ims (شکل ۱۰-۶) هم اطلاعات کانال و هم عضویت را نگه می‌داشت.

شکل ۱۰-۶. طرحوارهٔ ساده‌شده برای teams_ims

در مجموع سه جدول مجزا برای اطلاعات کانال و سه جدول مجزا برای عضویت کانال داشتیم. شکل ۱۰-۷ نقش هر جدول را بسته به نوع کانال (عمومی، خصوصی، DM گروهی یا DM) نشان می‌دهد.

شکل ۱۰-۷. نمودار جداول مسئول ذخیرهٔ اطلاعات کانال و عضویت متناظر، بسته به نوع کانال

مشکلات مقیاس‌پذیری

حالا که درک بهتری از معماری پایه و نمایش کانال‌ها و عضویت داریم، می‌توانیم مشکلاتی را که در پی آن پیش آمد بررسی کنیم. سه مورد جدی‌تر را توصیف می‌کنیم؛ همان‌طور که بزرگ‌ترین مشتری آن زمان — Very Large Business یا VLB — تجربه کرد.

VLB مشتاق بود ۳۵۰٬۰۰۰ کارمندش از Slack استفاده کنند. ابتدا آهسته شروع کرده بود اما در ماه‌های اول ۲۰۱۷ استفاده را تهاجمی افزایش داد. تا آوریل بیش از ۵۰٬۰۰۰ کاربر روی پلتفرم داشت، تقریباً دو برابر دومین مشتری بزرگ ما. VLB به محدودیت تقریباً هر بخش محصول برخورد. آن زمان در تیم مسئول عملکرد Slack برای بزرگ‌ترین مشتریان بودم. چند هفته تیم ما rotation داشت که دو نفر باید ساعت ۶:۳۰ صبح در دفتر San Francisco حاضر می‌شدند تا برای مشکلات فوری در ساعت اوج ورود VLB در East Coast آماده باشند. وقتی برای وصلهٔ مشکلات عجله می‌کردیم، دیدیم همه را تشدید می‌کند که جداول تکراری برای عضویت کانال داریم.

راه‌اندازی کلاینت Slack

هر روز کاری از ۹ صبح به وقت شرقی، کارمندان VLB وارد Slack می‌شدند. با شروع روز کاری، بار بیشتری روی shard پایگاه دادهٔ VLB جمع می‌شد. ابزارسازی موجود نشان می‌داد مقصر احتمالاً یکی از مهم‌ترین APIهای startup، rtm.start است.

این API همهٔ اطلاعات لازم برای پر کردن نوار کناری کاربر را برمی‌گرداند؛ کانال‌های عمومی و خصوصی که عضو است، پیام‌های گروهی و مستقیم باز، و اینکه کدام کانال پیام خوانده‌نشده دارد. کلاینت نتیجه را parse می‌کرد و رابط را با فهرست مرتب گفتگوهای bold و عادی پر می‌کرد.

از دید سرور، این فرایند بسیار پرهزینه بود. برای تعیین عضویت‌ها باید سه جدول را می‌پرسیدیم: teams_channels_members، groups_members و teams_ims. از هر مجموعه channel_id را می‌گرفتیم و ردیف متناظر teams_channels یا groups را برای نام کانال. همچنین جدول messages را برای timestamp آخرین پیام می‌پرسیدیم و با last_read کاربر مقایسه می‌کردیم تا unread بودن را بفهمیم. اکثر این پرس‌وجوها را جداگانه اجرا می‌کردیم و هر بار هزینهٔ roundtrip شبکه می‌پرداختیم.

دیدپذیری فایل

در طول روز، گاهی اوج در پرس‌وجوهای پرهزینه دیدیم. داشبوردها چند محل احتمالی نشان داد، از جمله تابع محاسبهٔ دیدپذیری فایل در هستهٔ بیشتر APIهای فایل. باز کردن تابع هدف دوباره مجموعه‌ای از پرس‌وجوهای پیچیده بود.

وقتی کاربر فایلی آپلود می‌کند، سرورها ردیف جدیدی در جدول files با نام، محل روی سرور فایل و اطلاعات دیگر می‌نویسند. وقتی فایل در کانالی share می‌شود، ورودی جدید در files_share با file ID و channel ID می‌نویسیم. اگر share به کانال عمومی باشد، برای همهٔ کاربران workspace قابل‌کشف است و با is_public = true در ردیف files علامت‌گذاری می‌شود. در ساده‌ترین حالت فایل عمومی است و سریع می‌فهمیم.

وقتی عمومی نیست، منطق پیچیده‌تر می‌شود. باید همهٔ کانال‌هایی که کاربر عضو است را با کانال‌هایی که فایل در آن‌ها share شده تطبیق دهیم. مثل rtm.start، برای مجموعهٔ کامل عضویت باید سه جدول جدا پرسیده می‌شد. سپس با نتایج files_shares برای فایل هدف ترکیب می‌کردیم. اگر تطابق بود فایل را نشان می‌دادیم؛ وگرنه خطا به کلاینت.

Mentionها

پرس‌وجویی که در طول روز کاری بار پایدار روی shard VLB ایجاد می‌کرد، مسئول تعیین mention خوانده‌نشده برای کاربر (یا موضوعات subscribe‌شده) بود. mention در Slack می‌تواند نام کاربری یا username با @، یا کلمهٔ highlight در تنظیمات باشد. کلاینت با آن داده badge تعداد mentionهای unread کنار نام کانال در نوار کناری را پر می‌کرد. یکی از پرس‌وجوهای پیچیده mention در مثال ۱۰-۱ دیده می‌شود.

این پرس‌وجو دوباره عضویت کاربر را از سه جدول عضویت می‌گرفت. بخش دشوار حذف عضویت کانال‌های حذف یا بایگانی‌شده بود که نیاز به join نتایج عضویت با ردیف کانال در groups یا teams_channels داشت.

sql
-- Example 10-1. Query to determine whether to notify a user of a mention; % symbolizes
-- substitution syntax

SELECT
     tcm.channel_id as channel_id,
     'C' as type,
     tcm.last_read
from
     teams_channels tc
     INNER JOIN teams_channels_members tcm ON (
         tc.team_id = tcm.team_id
         AND tc.id = tcm.channel_id
     )
WHERE
     tc.team_id = %TEAM_ID
     AND tc.date_delete = 0
     AND tc.date_archived = 0
     AND tcm.user_id = %USER_ID
UNION ALL
SELECT
     gm.group_id as channel_id,
     'G' as type,
     gm.last_read
from
     groups g
     INNER JOIN groups_members gm ON (
         g.team_id = gm.team_id
         AND g.id = gm.group_id
     )
WHERE
     g.team_id = %TEAM_ID
     AND g.date_delete = 0
     AND g.date_archived = 0
     AND gm.user_id = %USER_ID
UNION ALL
SELECT
     channel_id as channel_id,
     'D' as type,
     last_read
FROM
    teams_ims
WHERE
    team_id = %TEAM_ID
    AND user_id = %USER_ID

یکپارچه‌سازی جداول

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

بدون برنامهٔ مکتوب و تقریباً فوراً شروع کردیم. اولویت بالاتر رساندن یکپارچه‌سازی به نقطه‌ای بود که بتوانیم پرس‌وجوی mentions — پرتکرارترین ضربه به shardها — را migrate کنیم. می‌دانستیم پرس‌وجوهای زیادی سود می‌برند، اما migration آن‌ها ثانویه بود. در فصل ۱ گفتم مگر مطمئن باشید می‌توانید تمام کنید، وارد بازآرایی بزرگ نشوید. اینجا قصد اتمام داشتیم؛ فقط نمی‌دانستیم مسائل فوری‌تر اولویت بگیرند یا نه. با فوریت مسئله ریسک را پذیرفتیم.

اول جدول جدید channels_members ساختیم. طرحوارهٔ جداول عضویت را با همان indexها ترکیب کردیم و ستون جدیدی برای منشأ ردیف (teams_channels_members یا groups_members) اضافه کردیم تا migration و وابستگی‌های business logic آسان‌تر شود. شکل ۱۰-۸ حالت هدف را در مقابل شکل ۱۰-۷ نشان می‌دهد.

شکل ۱۰-۸. حالت هدف ما

جمع‌آوری پرس‌وجوهای پراکنده

بازنویسی پرس‌وجوها برای یک جدول جدید آسان نبود. پایگاه کد Slack به سبک imperative نوشته شده بود؛ توابع کوتاه و بلند در صدها فایل با namespaceهای شل. نویسندگان اولیه به دلیل نگرانی عملکرد PHP از الگوهای شیءگرا دوری می‌کردند و پرس‌وجوها را inline می‌نوشتند تا ORM و bloating زودهنگام رخ ندهد.

پرس‌وجوهای یک‌بار مصرف به teams_channels_members یا groups_members در ۱۲۶ فایل پخش بود. بسیاری از قبل از عرضهٔ محصول دست نخورده بودند. بخش زیادی از کد پوشش تست ضعیف داشت. برای حس واقعی، کد قدیمی در مثال ۱۰-۲ آمده است.

php
// Example 10-2. An inlined SQL query to teams_channels_members

function chat_channels_members_get_display_counts(
    $team,
    $user,
    $channel
){
    // Some business logic

    $sql = "SELECT
        COUNT(*) as display_counts,
        SUM(CASE
                 WHEN (is_restricted != 0 OR is_ultra_restricted != 0)
                     THEN 1
                 ELSE 0
             END) as guest_counts
    FROM
         teams_channels_members AS tcm
         INNER JOIN users AS u ON u.id = tcm.user_id
    WHERE
         tcm.team_id = % team_id
         AND tcm.channel_id = % channel_id
         AND u.deleted = 0";

      $ret = db_fetch_team($team, $sql, array(
              'team_id' => $team['id'],
              'channel_id' => $channel['id']));

      // A bit more business logic

      return $counts;
}

کد business logic اطراف این پرس‌وجوها مستقیماً به ستون‌های نتیجه index می‌کرد و coupling شدید بین طرحواره و کد ایجاد می‌کرد. ستون جدید یعنی به‌روزرسانی کد متناظر. مثلاً ستون is_public در files؛ اگر منطق اضافی برای «عمومی بودن» لازم شد، هر if ($file['is_public']) باید به‌روز می‌شد.

برای یکپارچه‌سازی باید همهٔ پرس‌وجوها را پیدا می‌کردیم. با grep فهرست فایل‌ها و شماره خط‌ها را گرفتیم و در Google Sheets مشترک (شکل ۱۰-۹) گذاشتیم.

شکل ۱۰-۹. فایل Google Sheets برای ردیابی پرس‌وجوها به teams_channels_members و groups_members

تصمیم گرفتیم یک فایل واحد برای همهٔ پرس‌وجوهای عضویت کانال بسازیم. همزمان گفتگو دربارهٔ متمرکز کردن پرس‌وجوها شروع شده بود. به‌روزرسانی پرس‌وجو در گوشه‌های پایگاه کد هر بار تغییر جدول خسته‌کننده بود. پیشنهادها شامل نگه‌داشتن همهٔ پرس‌وجوهای یک جدول در یک فایل بود. بعضی تولید پرس‌وجو از پارامترها می‌خواستند (لایهٔ دسترسی پیچیده‌تر)؛ بعضی inline بودن را ترجیح می‌دادند. در این پروژه تولید minimal پرس‌وجو را prototype کردیم تا تعداد توابع کم بماند. این الگو را unidata یا ud نامیدیم؛ فایل هدف ud_channel_membership.php شد.

تدوین استراتژی migration

با جدول و مجموعهٔ پرس‌وجوها، شروع کردیم. هر پرس‌وجوی insert/update/delete از grep اولیه را در کتابخانه unidata تابع متناظر گرفت. هر تابع پارامتری داشت که مشخص کند روی teams_channels_members یا groups_members اجرا شود، به‌همراه منطق اجرای همان پرس‌وجو روی channels_members. ایدهٔ کلی در مثال ۱۰-۳ است.

php
// Example 10-3.

function ud_channel_membership_delete(
    $team,
    $channel_id,
    $user_id,
    $channel_type
){

    if ($channel_type == 'groups'){
        $sql = 'DELETE FROM groups_members WHERE team_id=%team_id AND
                group_id=%channel_id AND user_id=%user_id';
    }else{
        $sql = 'DELETE FROM teams_channels_members WHERE team_id=%team_id AND
                channel_id=%channel_id AND user_id=%user_id';
    }

    $bind = array(
        'team_id'    => $team['id'],
        'channel_id' => $channel_id,
        'user_id'    => $user_id,
    );

      $ret = db_write_team($team, $sql, $bind);

      if (feature_enabled('channel_members_table')){
          $sql = 'DELETE FROM channels_members WHERE team_id=%team_id AND
                  channel_id=%channel_id AND user_id=%user_id';
          $double_write_ret = db_write_team($team, $sql, $bind);

          if (not_ok($double_write_ret)){
              log_error("UD_DOUBLE_WRITE_ERR: Failed to delete row for
                  channels_members for {$team['id']}-{$channel_id}-{$user_id}");
          }
      }

      return $ret;
}

پس از انتقال همهٔ writeها، اسکریپت backfill برای کپی دادهٔ موجود از هر دو جدول عضویت به جدول جدید نوشتیم. writeها قبل از backfill migrate شدند تا دادهٔ جدول جدید دقیق بماند. ابتدا workspace خودمان، سپس VLB در ساعات off-hours backfill شد. سه‌بار چک کردیم write پراکنده‌ای باقی نمانده باشد؛ با سرعت سازمان مهندسی، احتمال از دست رفتن یکی دو پرس‌وجو غیرصفر بود. هنوز مکانیزمی برای جلوگیری از افزودن پرس‌وجوی جدید بدون اطلاع ما نبود؛ تیم را از فرایند (شکل ۱۰-۱۰) آگاه کردیم و اسکریپتی برای یافتن ناسازگاری و patch اختیاری نوشتیم.

در بعضی اسکرین‌شات‌های این فصل ممکن است TS ببینید. TS مخفف Tiny Speck، نام قبلی شرکت قبل از عرضهٔ عمومی Slack در ۲۰۱۴ است. «enabled to TS» یعنی تغییر را برای workspace خودمان فعال کرده‌ایم.

شکل ۱۰-۱۰. اعلام شروع double-write به جدول جدید

پس از فعال‌سازی double-write برای VLB، سلامت پایگاه داده را با دقت watched کردیم؛ ردیف‌های teams_channels_members و groups_members بسیار پرتکرار به‌روز می‌شدند. هر بار خواندن پیام، کلاینت last_read را به‌روز می‌کرد. با channels_members تعداد write دو برابر شد. یک روز ترافیک را مانیتور کردیم تا مطمئن شویم workspace پهنای باند کافی دارد.

با sync جداول و double-write، به مهم‌ترین نقطهٔ عطف رسیدیم: migration پرس‌وجوی mentions. برای آزمایش در production ابتدا به تیم خودمان rollout می‌کردیم — استراتژی معمول برای feature، زیرساخت یا بهبود عملکرد. معمولاً بعد workspaceهای رایگان و tierهای پرداخت تا مشتریان بزرگ آخر می‌آمدند؛ اینجا برعکس کردیم تا بار tier بالا زودتر کم شود.

mentions بهینه را برای تیم خودمان فعال کردیم. تست خودکار کم و framework واحد نتوانست پرس‌وجو را درست تست کند؛ به افراد داخلی برای regression قبل از مشتریان دیگر تکیه کردیم. کانال‌های گزارش باگ را مانیتور کردیم. بعد برای VLB فعال شد.

کمی‌سازی پیشرفت

می‌دانستیم پایگاه‌های داده overload هستند. سلامت را با درصد CPU بیکار می‌سنجیدیم؛ معمولاً حدود ۲۵٪ و گاه به ۱۰٪ و پایین‌تر می‌رسید. هرچه بیشتر زیر ۲۵٪ بماند، احتمال تحمل spike بار کمتر است. VLB محصول را سخت آزمایش می‌کرد و نمی‌دانستیم کدام بخش بعدی بار را بالا می‌برد.

وقتی یکپارچه‌سازی شروع شد، پروژه‌های موازی دیگری هم برای بار در جریان بود. با double-write، نوسانات و featureهای جدید، دادهٔ usage پایگاه داده برای اثبات اثربخشی کافی نبود. دادهٔ monitoring حدود یک هفته نگه می‌داشت؛ مگر در روز آرام screenshot و نقطه داده ثبت می‌کردیم، baseline خوبی در پایان نداشتیم.

عمدتاً به timing پرس‌وجوها تکیه کردیم. هر پرس‌وجو را instrument کردیم. EXPLAIN مفید است اما metric واقعی زمان اجرا از دید سرور بهتر است. به‌جای فعال‌سازی فوری برای همهٔ کاربران VLB، درخواست‌های ورودی را تصادفی ۵۰-۵۰ بین دو پرس‌وجو تقسیم کردیم — پس از تأیید feature flag برای workspace. چند ساعت صبر کردیم؛ پرس‌وجوی جدید باید هم در بار متوسط و هم در اوج سریع‌تر می‌بود. داده امیدوارکننده بود: حدود ۲۰٪ speed-up! شکل ۱۰-۱۱ دادهٔ اصلی را نشان می‌دهد. پرس‌وجوی اول join روی teams_channels_members و groups_members میانگین ~۴٫۴ ثانیه؛ دوم فقط channels_members ~۳٫۵ ثانیه. تقریباً یک ثانیه کم کردیم. (هر دو پرس‌وجو برای نمودار timing طولانی بودند.)

شکل ۱۰-۱۱. دادهٔ timing یکی از پرس‌وجوهای mentions برای VLB

با تأیید اثربخشی برای use case مهم، ادامهٔ یکپارچه‌سازی توجیه شد. به Google Sheet برگشتیم و بقیهٔ read queryها را بین مهندسان تقسیم کردیم.

تلاش برای حفظ انگیزهٔ تیم

متأسفانه گرفتن کمک برای اتمام migration سخت بود. با آتش‌های زیاد، همه روی remediationهای موازی بودند. استخراج چند پرس‌وجو چند ساعت وقت می‌خواست. بیشتر کد اطراف پرس‌وجوهای باقی‌مانده تست‌نشده بود و تغییر ساده را خطرناک می‌کرد. یک بعدازظهر migration جذاب نبود.

فکر کردم به تیم‌های Enterprise یا مهندسان performance-minded دیگر برسم، اما در نهایت عمدتاً تنها با کمک گاه‌به‌گاه هم‌تیمی‌ها ادامه دادم. کار پرریسک و کم‌تحریک فکری بود؛ متقاعد کردن دایرهٔ wider سخت به نظر می‌رسید. با hindsight می‌توانستم جذاب‌ترش کنم، کار را پخش کنم و چند هفته کم کنم.

وقتی پیشرفت خزید، با کوکی رشوه دادم (شکل ۱۰-۱۲). گزینه‌های سنتی انگیزه هم هست (فصل ۸)؛ گاه غذا بهترین incentive است.

شکل ۱۰-۱۲. تلاش برای رشوه

اطلاع‌رسانی پیشرفت

با وجود پروژه‌های موازی، به حمایت یکدیگر نیاز داشتیم: code review، debug باگ‌های سخت، gut check. برای مؤثر ماندن در این نقش‌ها و تمرکز روی کار خودمان، مرتب مشکلات عملکرد را در کانال‌های عمومی (اغلب کانال تیم) debug می‌کردیم و جلسهٔ هفتگی حضوری برای پیشرفت و blocker داشتیم. برای من یعنی گزارش درصد پرس‌وجوهای باقی‌مانده و باگ یا ناسازگاری داده.

هر نقطهٔ عطف معنادار — double-write برای workspace خودمان، mentions جدید برای VLB — در کانال تیم و چند کانال engineering-wide اعلام می‌شد. هرچه مهندسان بیشتر بدانند، کمتر احتمال افزودن پرس‌وجوی جدید به جداول در حال deprecate بدون کتابخانهٔ جدید. در triage باگ مشتری هم هر مهندس می‌توانست مؤثرتر مشکل مرتبط را isolate کند.

برخورد با باگ اجتناب‌ناپذیر

هیچ بازآرایی بزرگی بدون یکی دو باگ pesky تمام نمی‌شود. حدود یک ماه پس از انتقال read queryها به کتابخانهٔ جدید، تعداد کمی اما معنادار ردیف عضویت در جدول جدید کمی قدیمی بود. فهمیدیم گاه write به teams_channels_members یا groups_members موفق بود اما double-write به channels_members شکست می‌خورد. چون تابع فقط موفقیت write اول را برمی‌گرداند، caller فکر می‌کرد همه‌چیز درست است.

تغییر دادیم که شکست هر write خطا برگرداند، اما مطمئن نبودیم باگ دیگری نباشد. برای تأیید، channels_members workspace خودمان را از صفر دوباره populate کردیم: prune همهٔ محتوا، سپس کپی از دو جدول قدیمی.

باید درست می‌بود، اما فراموش کرده بودم تیم ما فقط از جدول جدید می‌خواند. چند ثانیه پس از شروع اسکریپت همهٔ کانال‌هایم ناپدید شد. کل شرکت از همهٔ کانال‌ها boot شد. چند دقیقه طول کشید feature flag را برگردانم تا دوباره از جداول اصلی بخوانیم. خوشبختانه بیشتر همکاران رفته بودند؛ اما برای بعضی ترس جدی بود.

جمع‌بندی

وقتی tracker خالی شد، به‌تدریج بقیهٔ تیم‌ها (غیر از خودمان و VLB) را برای read از جدول جدید فعال کردیم. دو هفته صبر کردیم و سپس double-write به جداول قدیمی را متوقف کردیم. می‌خواستیم tier پایگاه داده، صحت داده و نبود باگ جدید را تأیید کنیم. اگر double-write از نظر بار و هزینه گران نبود، شاید بیشتر می‌گذاشتیم bake شود؛ اما مشتاق حذف overhead بودیم.

double-write را متوقف کردیم: اول تیم خودمان، بعد VLB، بعد بقیهٔ مشتریان. هر گام مهم را broad اعلام کردیم (شکل ۱۰-۱۳). سپس کتابخانهٔ جدید را از ارجاع به جداول قدیمی پاک کردیم. linter rule نوشتیم که پرس‌وجوی جدید به جداول deprecated ممنوع و همهٔ پرس‌وجوهای channels_members در کتابخانهٔ متمرکز باشد. همه announcementهای cross-functional را نمی‌خوانند؛ نباید فقط به اعلان تکیه کرد.

شکل ۱۰-۱۳. اعلام توقف double-write برای workspace خودمان

نمای نزدیک نمودار در پیام Slack شکل ۱۰-۱۳:

(نمودار در نسخهٔ چاپی)

البته جشن را فراموش نکردیم! سنت تیم مهندسی San Francisco کیک سفارش بود (شکل ۱۰-۱۴) با نام جدول جدید.

شکل ۱۰-۱۴. کیک Funfetti برای جشن بازآرایی!

مسیر کامل پروژه در شکل ۱۰-۱۵ است: تعداد پرس‌وجوهای روزانه به هر جدول از مه تا سپتامبر ۲۰۱۷.

شکل ۱۰-۱۵. حجم پرس‌وجو برای teams_channels_members، groups_members و channels_members در طول بازآرایی

درس‌های آموخته‌شده

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

تدوین برنامهٔ مشخص و ابلاغ‌شده

پروژه خیلی سریع شروع شد و برنامهٔ مکتوب نداشتیم. با migration جدول به جدول آشنا بودیم؛ mentions اولویت بود و فقط به اندازهٔ لازم migrate می‌کردیم. تنها مکتوب، update در کانال تیم بود نه کانال اختصاصی پروژه — و فقط بخشی از کل برنامه.

نداشتن گام‌های deliberate از شروع تا پایان یعنی احتمال فراموشی چیز حیاتی. نگران‌کننده‌تر: برنامه را با تیم‌های دیگر share نکردیم تا تأثیر و نگرانی‌ها را بشنوند. فرض کردیم عملکرد مهم‌ترین کار برای رابطه با بزرگ‌ترین مشتری (و شرکت) است و تغییر را طوری implement می‌کنیم که کمترین اختلال برای تیم‌های دیگر باشد.

این فرض در چند front غلط بود. وقتی باگ‌های اجتناب‌ناپذیر آمد و تغییر socialized نشده بود، مهندسان پاسخ‌دهنده غافلگیر شدند. یک تیم را کلاً نادیده گرفتیم که تأثیر حاد می‌برد: data engineering. حدود یک ماه قبل از اتمام، هم‌تیمی یادآور شد باید آن‌ها را warn کنیم. با انتقال عضویت به جدول جدید و نزدیک شدن به قطع write به جداول قدیمی، بیشتر pipelineها — از جمله metrics مصرف — مختل می‌شد. خوشبختانه data engineering سریع pipelineها را به‌روز کرد و بحران جدی رخ نداد.

این نشان می‌دهد برنامهٔ اجرایی thorough و vet‌شده چقدر مهم است. خوش‌شانس بودیم سریع recover کردیم؛ چرا به شانس واگذار کنیم آنچه در planning عمدی حل می‌شد؟ فصل‌های ۴ و ۷ برنامهٔ concrete را برای کشف شکاف و کم کردن communication gap تأکید کردند.

درک تاریخچهٔ کد

توصیه می‌کنم قبل از اجرا، باستان‌شناسی کد را شروع کنید؛ context شکل و جهت پروژه را عوض می‌کند. به‌خاطر فوریت، فرایند deliberate درک و همدلی با کد موجود را رد کردیم و مستقیم به اجرا رفتیم. مدتی پس از migration پرس‌وجوها فهمیدم چرا اصلاً teams_channels_members و groups_members جدا بودند.

هفته‌ها گذشت و ده‌ها پرس‌وجو باقی بود؛ ناامیدی از جداول تکراری و SQL پراکنده. هرچه ناامیدتر، پروژه طولانی‌تر به نظر می‌رسید (و وسوسهٔ cut corner بیشتر).

پس از اتمام، از مهندسان اولیه پرسیدم. فهمیدم جدا نگه داشتن کانال عمومی و خصوصی precaution امنیتی بود. تاریخچهٔ محصول هم نقش داشت؛ در روزهای اول مفاهیم خیلی متفاوت به نظر می‌رسیدند. با همگرایی مفاهیم، schemeها هم همگرا شدند.

این دید برای بازآرایی‌های بعد — یکپارچه‌سازی teams_channels و groups — مفید بود. قدردانی تازه از تصمیم‌های اولیه و نگرش مثبت‌تر: بازآرایی بهبود چیزی که مدتی خوب خدمت کرده، نه فقط «کد بد». دقیقاً دلیل توصیهٔ فصل ۲ برای درک منشأ کد و چگونگی degradation. همدلی بیشتر یعنی ذهن بازتر و صبورتر در بازآرایی.

اطمینان از پوشش تست کافی

فصل ۱ گفت پوشش تست کافی قبل از بازآرایی برای حفظ رفتار در هر گام مهم است. بیشتر کدی که تغییر دادیم زودهنگام Slack نوشته شده بود و با فشار time-to-market تست کافی نداشت. بازآرایی عضویت کانال هم تحت فشار زمان بود؛ فقط برای critical pathهای بدون تست، تست نوشتیم.

این منجر به چند باگ شد که با تست مناسب قابل پیشگیری بود. شاید بیشتر از نوشتن تست، وقت recovery از regression گذاشتیم. پوشش کافی برای refactor روان، بدون باگ برای مشتری و بدون وقت تیم برای fix.

حفظ انگیزهٔ تیم

به‌جای ادامهٔ تنها، باید از ابتدا و وقتی پیشرفت کند شد، مهندسان بیشتری جدی‌تر involve می‌کردم. ۱۰٪ آخر پرس‌وجوها تقریباً به اندازهٔ ۵۰٪ اول وقت برد. پس از بهبود mentions برای VLB، حس فوریت اول کم شد. هر باگ یا ناسازگاری کمی steam از دست می‌داد. نزدیک پایان، مثل غلتاندن سنگ بالای کوه.

به کمک مهندسان خارج از تیم فکر نکردیم. می‌توانستیم از تیم‌های product بخواهیم پرس‌وجوهای feature خودشان را migrate کنند و با نشان دادن boost عملکرد بفروشیم. پخش کار زمان را نصف می‌کرد.

اگر شتاب کم شد، زود راه boost پیدا کنید. refactor کند اولویت را از دست می‌دهد و کد زیادی بین دو state می‌ماند — مشکل خودش (فصل ۱). فصل ۸ راه‌های انگیزه را پوشش داد؛ درخواست حمایت بیشتر را dریغ نکنید.

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

دادهٔ اولیه EXPLAIN برای فرضیهٔ بهبود عملکرد با ترکیب دو جدول عضویت داشتیم. تأیید بیشتر در مراحل اول لازم بود تا در صورت ناکافی بودن consolidation pivot کنیم. با تمرکز فقط روی تغییرات لازم برای migration mentions برای VLB، ظرف چند هفته تأیید گرفتیم و بار shard VLB را کم کردیم — زمان بیشتر برای بقیهٔ refactor.

اثبات زودهفتم effectiveness یعنی وقت تلف نکردن روی پروژهٔ طولانی بدون نتیجه. نقاط عطف راهبردی زودتر به ذینفعان سود می‌دهد و حمایت را در حین کار تقویت می‌کند. جزئیات در فصل ۴.

شناسایی و اتکا به معیارهای معنادار

مجموعهٔ معیار مشخص برای موفقیت در نقاط میانی و پس از rollout کامل داشتیم. EXPLAIN قبل و بعد برای مستندسازی migration پرس‌وجوهای پیچیده. instrument کردن mentions با timing برای impact بلادرنگ.

چشم روی معیارها یعنی اثبات tilt در جهت درست در طول توسعه. اگر معیارها بهبود نیافتند (یا بدتر regression)، فوراً dig in کنید — نه در پایان پروژه. فصل ۳ برای اندازه‌گیری refactor.

جمع‌بندی

مهم‌ترین نکات از بازآرایی یکپارچه‌سازی جداول عضویت کانال Slack:

  • برنامهٔ مکتوب thorough بنویسید و broad share کنید.
  • وقت بگذارید تاریخچهٔ کد را بفهمید؛ شاید نگاه مثبت‌تری بگیرید.
  • پوشش تست کافی برای کدی که بهبود می‌دهید؛ وگرنه تست‌های missing را commit کنید.
  • تیم را motivated نگه دارید؛ اگر momentum از دست رفت، creatively boost کنید.
  • روی نقاط عطف راهبردی تمرکز کنید تا impact زود و مکرر ثابت شود.
  • معیارهای معنادار را شناسایی و به آن‌ها تکیه کنید.