نشر خدمات Rust بدون توقف: كيفية إعادة تحميل التطبيق دون فقدان العملاء
تخيل الموقف: لقد قمت بنشر إصدار جديد من خدمة Rust الخاصة بك، ولكن في لحظة إعادة التشغيل، حصل 50 مستخدمًا على خطأ 502. شخص ما فقد بياناته، وآخر فقد أمواله، وآخر فقد ثقته في منتجك. ألم مألوف؟ اليوم سنشرح كيفية إعداد نشر خدمات Rust بدون توقف، بحيث تتم التحديثات دون أن يلاحظها العملاء.
النشر بدون توقف (أو النشر دون انقطاع) ليس سحرًا، بل مجموعة من التقنيات: الإغلاق السلس، الإدارة الصحيحة للاتصالات، وموازنة حركة المرور. يمنحنا Rust مع نظام الأنواع الخاص به وبيئة التشغيل غير المتزامنة (Tokio) الأدوات اللازمة لحل هذه المشكلة بأناقة. هيا بنا!
المشكلة: لماذا يقتل إعادة التشغيل العادي بيئة الإنتاج؟
عندما تقتل العملية ببساطة (kill -9) أو تستدعي std::process::exit()، يقوم نظام التشغيل بإغلاق جميع المقابس (sockets) فورًا. العملاء الذين كانوا يرسلون طلبات في تلك اللحظة يتلقون Connection reset أو Timeout. بالنسبة لخدمات HTTP، هذه كارثة: المتصفحات تظهر أخطاء، تطبيقات الجوال تتعطل، والمهام الخلفية تفقد حالتها.
في Rust يمكننا إصلاح هذا. نحتاج إلى:
- التوقف عن قبول طلبات جديدة (ولكن إنهاء الطلبات القديمة).
- انتظار اكتمال جميع المعالجات النشطة.
- عندها فقط إنهاء العملية.
يسمى هذا الإغلاق السلس (graceful shutdown) - أساس نشر خدمات Rust بدون توقف.
الخطوة 1. الإغلاق السلس في Rust مع Tokio
يوفر Tokio آليات مدمجة للإغلاق السلس. لنلقِ نظرة على مثال لخادم HTTP باستخدام axum (إطار ويب شائع في Rust).
خادم أساسي بدون إغلاق سلس
use axum::{Router, routing::get};use std::net::SocketAddr;
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap();}إذا ضغطت على Ctrl+C، سينهار الخادم ببساطة - ستنقطع جميع الطلبات النشطة. هذا غير مقبول.
إضافة الإغلاق السلس
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // انتظار SIGTERM أو SIGINT (Ctrl+C) let ctrl_c = async { signal::ctrl_c() .await .expect("فشل تثبيت معالج Ctrl+C"); };
#[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("فشل تثبيت معالج الإشارة") .recv() .await; };
#[cfg(not(unix))] let terminate = std::future::pending::<()>();
tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, }
println!("تم استلام إشارة الإنهاء، بدء الإغلاق السلس...");}
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await .unwrap();
println!("الخادم أنهى عمله.");}الآن عند استلام SIGTERM أو Ctrl+C، الخادم:
- يتوقف عن قبول اتصالات جديدة.
- ينتظر اكتمال جميع الطلبات الحالية (مع مهلة افتراضية).
- ينهي العملية.
هذه هي الخطوة الأولى نحو نشر خدمات Rust بدون توقف. ولكن إذا كان لديك خادم واحد، فخلال فترة انتظار اكتمال الطلبات، لن يتمكن العملاء الجدد من الاتصال. نحتاج