مقدمه

Cloudflare بیش از یک میلیارد رویداد در ثانیه پردازش می‌کند و شبکه‌اش در بیش از 330 شهر در 120 کشور گسترده است. پشت هر HTTP request، هر Worker invocation و هر R2 read، حجم بزرگی از داده وجود دارد — و دسترسی آسان به این داده‌ها همیشه ساده نبوده است. 📈

چالش اصلی

وقتی یک شرکت دوران hyper-growth را تجربه می‌کند، داده‌ها پراکنده و پنهان می‌شوند. در حالت ما چند مشکل ملموس بود: وجود سیستم‌های جداافتاده متعدد (Postgres، ClickHouse، BigQuery، R2، Kafka و …)، نمونه‌سازی داده‌ها (sampling) که برای dashboards مناسب است اما برای حوزه‌هایی مثل billing یا بررسی‌های امنیتی جوابگو نیست، وابستگی به سرویس‌های خارجی برای بعضی گزارش‌های داخلی، و در نهایت این‌که هیچکس دقیقاً نمی‌دانست داده‌ی مورد نیاز کجا قرار دارد — دانشِ قبیله‌ای بیش از حد وجود داشت. 😕

برای مثال، پاسخ دادن به سوال ساده‌ای مثل «چند دامنه که امروز ثبت نام کرده‌اند در Top 100 بر اساس ترافیک هستند؟» نیازمند دانستن اینکه باید کدام سیستم را پرس‌وجو کنیم، چه credentials، چه زبان پرس‌وجویی، و آیا داده‌ها نمونه‌گیری‌شده یا تازه هستند یا هفت روز قدیمی — این پیچیدگی مانع کسب insight سریع و قابل اتکا می‌شد.

هدف ما

هدف‌مان ایجاد یک نقطه واحد بود که هرکس در شرکت با مجوز مناسب بتواند پاسخ سوالات عملیاتی و تحلیلی را بگیرد: مانند «Top 100 مشتریان بر اساس revenue در квартال گذشته چه کسانی‌اند؟»، یا «تمام Bot Management ML scoring events با score > 0.9 از یک ASN خاص در 48 ساعت گذشته». ما می‌خواستیم داده‌های تازه و بدون نمونه‌سازی برای مواردی که نیاز است (مثل billing یا تحقیقات امنیتی) در دسترس باشند و برای کارهای اکتشافی یا داشبوردها داده‌های downsampled و سریع‌تر ارائه شود.

همچنین خواستیم حاکمیت (governance) و امنیت از ابتدا در سیستم تعبیه شود: تشخیص خودکار PII، قفل شدن جداول حساس به‌صورت پیش‌فرض، ثبت (audit) همه دسترسی‌ها و مجوزهای زمانی‌محدود تا کاربر فقط در زمان انجام وظیفه به داده دسترسی داشته باشد. و همه‌ی این‌ها روی پلتفرم خود Cloudflare ساخته شود: R2، Workers، Cloudflare Access، Workflows — یعنی استفاده از همان محصولات که به مشتریان می‌فروشیم. هدف نهایی هم یک رابط بود که نیازی به دانستن SQL نداشته باشد — همین خواسته شد Skipper. 🚀

Town Lake — پلتفرم

هستهٔ پلتفرم ما یک data lakehouse است: موتور کوئری‌ای که از object storage می‌خواند و با لایهٔ متادیتا storage را مثل یک پایگاه داده رفتار می‌دهد. اسم سیستم را Town Lake گذاشتیم.

اجزای کلیدی آن به‌صورت خلاصه:

Query engine: از Apache Trino استفاده کردیم. با یک SQL واحد می‌توان جدول‌های Postgres، ClickHouse و Iceberg روی R2 را join کرد بدون نیاز به materialize میانی. برنامهٔ کوئری فیلترها را به ClickHouse می‌فرستد، joinها را با dimensionهای Postgres انجام می‌دهد و محاسبات rollup را روی داده‌های در R2 می‌زند — همه در یک پلان اجرا می‌شود.

R2 Data Catalog (managed Apache Iceberg): محل نگهداری cold و warm data است. Iceberg به ما schema evolution، time travel، partition evolution و قابلیت compact کردن داده‌ها با افزایش سن را می‌دهد. به‌عنوان مثال، بازهٔ per-minute هفتهٔ گذشته به hourly تبدیل می‌شود و hourly از سه‌ماههٔ گذشته به daily؛ هزینهٔ ذخیره کاهش می‌یابد اما داده‌ها هنوز قابل کوئری‌اند. Parquet در R2 هزینهٔ ذخیرهٔ بسیار کمتری نسبت به نگه‌داشتن همان داده در یک OLAP دارد.

DataHub: کاتالوگ متادیتا — هر جدول، ستون، owner، lineage و glossary term در آن ثبت شده است. وقتی کاربر می‌پرسد “what’s in townlake.dim.accounts”، DataHub توضیحات جدول، ستون‌ها، تیم مالک و upstream/downstream مربوطه را نشان می‌دهد.

Lifeguard: سرویس کنترل دسترسی ماست: قوانین دسترسی را در D1 ذخیره می‌کند، membershipها را از سیستم مدیریت دسترسی داخلی می‌کشد و یک JSON policy تولید می‌کند که Trino آن را از طریق HTTP می‌خواند. Lifeguard همچنین اطلاعات پایهٔ دسترسی را به Skipper و Gateway می‌دهد تا کاربرها در درگاه ورودی مسدود شوند نه در زمان اجرای کوئری. 🔐

Skimmer: اسکنر تشخیص PII که مداوم اجرا می‌شود و نمونه‌هایی از هر ستون را بررسی می‌کند. ابتدا یک classifier سریع per-column اجرا می‌شود، و اگر چیزی flagged شد، یک پاس دوم agentic با context کامل جدول که مستقیماً از Trino کوئری می‌زند برای تأیید انجام می‌دهد. نتایج وارد DataHub و allowlistِ Lifeguard می‌شود تا مرور انسانی نیز ممکن باشد.

Transformer: موتور ELT مبتنی بر Workflows. کاربران یک DAG از تبدیل‌های SQL با YAML frontmatter تعریف می‌کنند (target table، materialization mode، dependencies، schedule). Transformer DAG را کامپایل و روی Trino اجرا می‌کند، state را با Durable Objects مدیریت می‌کند، تعاریف را در R2 نگه می‌دارد و تاریخچهٔ اجراها در D1 ثبت می‌شود.

Ingestion: پلی بین سیستم‌های عملیاتی و دریاچهٔ داده. یک orchestrator در Kubernetes به‌صورت طولانی‌مدت اجرا می‌شود، configهای pipeline را می‌خواند و jobهای کوتاه‌مدت worker برای استخراج از Postgres یا ClickHouse، تبدیل به Parquet و بارگذاری به R2 به‌عنوان جداول Iceberg می‌سازد. هر pipeline یا full-replace است یا incremental-append.

حکمرانی پیش‌فرض: بسته (Default-closed)

یک پلتفرم یکپارچهٔ داده احتمالاً سطح بزرگی از دادهٔ حساس ایجاد می‌کند. رویکرد معمول «باز به‌صورت پیش‌فرض، محدود به‌صورت استثنا» است؛ اما Town Lake وارونه عمل می‌کند: جداول تا زمان بازبینی قابل کوئری نیستند. وقتی دیتابیس یا جدول جدیدی به Trino وصل می‌شود، Skimmer آن را اسکن، ستون‌ها را classify و جدول را به‌صورت pending در allowlist ثبت می‌کند. تا زمانی که بازبین جدول را و ستون‌های مشخص را تأیید نکند، کاربران اجازهٔ کوئری ندارند.

این رویکرد دو مزیت کلیدی دارد: اولاً خودکار است — classifierهای Skimmer اکثر PII مشخص و حتی برخی موارد پنهان را شناسایی می‌کنند و بازبینی‌ها معمولاً در چند ثانیه انجام می‌شود. ثانیاً workflow خودسرویس است: اگر جدولِ دسترسی‌نداشته‌ای را کوئری کنید، خطا «permission denied» نیست؛ پیام می‌گوید جدول نیاز به بازبینی دارد و لینک درخواست را می‌دهد، حتی Skipper پیشنهاد گروه RBAC مناسب را می‌دهد و مستقیم لینک می‌کند. 🧭

ما discoveryِ schema را از دسترسیِ داده جدا کردیم: کاربران می‌توانند ببینند چه جداولی وجود دارد اما ستون‌های review-نشده از DESCRIBE و SHOW COLUMNS و SELECT * مخفی می‌مانند. این تفاوت ظریف اهمیت دارد چون اضافه شدن ستون جدید بررسی‌نشده dashboards موجود را خراب نمی‌کند.

PII به‌صورت per-session opt-in است: به‌صورت پیش‌فرض Trino ستون‌های حساس را قبل از اینکه به صفحهٔ شما برسند redacted می‌کند. اگر نیاز مشروعی مثل تحقیق در مورد fraud وجود داشته باشد، کاربر می‌تواند فلگِ session را بزند، مجوزها بررسی می‌شود و redaction برداشته می‌شود — و هم فلگ و هم هر کوئری لاگ می‌شوند.

Skipper — عامل دادهٔ مبتنی بر AI

مجرّد داشتن یک query engine کافی نیست؛ SQL مانعی است و دانستن اینکه باید کدام یک از ده‌ها هزار جدول را پرس‌وجو کرد هم مانع دیگری است. Skipper نقش یک agent را بازی می‌کند تا هرکس بتواند با زبان ساده بپرسد و پاسخ‌های صحیح، قابل استناد و سریع بگیرد. این واسط مخصوصاً برای تیم‌های غیرتحلیلی یا اپراتینگی مفید است که نمی‌خواهند برای هر سوال کوچک به یک analyst مراجعه کنند. 🤖

اساساً Town Lake پاسخ به مشکل دادهٔ پراکنده و Skipper پاسخ به مشکل دسترسی و توانمندسازی کاربران است. این دو با هم هدفمان را که دسترسی امن، دقیق و سریع به داده‌ها برای تیم‌های SRE/DevOps و بقیهٔ تیم‌ها بود، محقق کردند.

این خلاصهٔ داستان ساخت Town Lake و Skipper است — راه‌حل‌هایی که هم برای عملیات روزمره و هم برای بررسی‌های حساس طراحی شده‌اند تا در مقیاس ابری امن و قابل اتکا باقی بمانند. 🧰