
في غضون 20 عامًا فقط ، انتقل تطوير البرامج من متراصة معمارية ذات قاعدة بيانات واحدة وحالة مركزية إلى خدمات مصغرة ، حيث يتم توزيع كل شيء عبر العديد من الحاويات والخوادم ومراكز البيانات وحتى القارات. يجعل التوزيع التوسع أسهل ، ولكنه يقدم أيضًا تحديات جديدة تمامًا ، تم حل العديد منها مسبقًا باستخدام وحدات متجانسة.
لنقم بجولة سريعة في تاريخ التطبيقات المتصلة بالشبكة لمعرفة كيف وصلنا إلى هنا اليوم. ثم لنتحدث عن نموذج التنفيذ المصحوب بالحالة المستخدم في Temporal.وكيف تحل مشكلة البنى الموجهة للخدمات (SOA). قد أكون متحيزًا لأنني أدير قسم البقالة في Temporal ، لكنني أعتقد أن هذا النهج هو المستقبل.
درس قصير في التاريخ
منذ عشرين عامًا ، أنشأ المطورون دائمًا تطبيقات متجانسة. إنه نموذج بسيط ومتسق ، مشابه لكيفية البرمجة في بيئتك المحلية. بحكم طبيعتها ، تعتمد monoliths على قاعدة بيانات واحدة ، أي أن جميع الولايات مركزية. في صفقة واحدة ، يمكن للوحدة المتراصة تغيير أي من حالاتها ، أي أنها تعطي نتيجة ثنائية: سواء نجحت أم لا. لا يوجد مجال للتضارب. وهذا يعني أن الشيء الجميل في monolith هو أنه لن تكون هناك حالة غير متسقة بسبب الصفقة الفاشلة. وهذا يعني أن المطورين لا يحتاجون إلى كتابة تعليمات برمجية ، فطوال الوقت يخمنون حالة العناصر المختلفة.
لفترة طويلة ، كانت الأحجار المتراصة منطقية. لم يكن هناك العديد من المستخدمين المتصلين حتى الآن ، لذلك كانت متطلبات تحجيم البرنامج ضئيلة. حتى أكبر عمالقة البرمجيات كانوا يشغلون أنظمة تافهة بالمعايير الحديثة. استخدم عدد قليل فقط من الشركات مثل Amazon و Google حلولاً واسعة النطاق ، لكن هذه كانت الاستثناءات من القاعدة.
الناس كبرامج

على مدار العشرين عامًا الماضية ، كانت متطلبات البرامج تنمو باستمرار. اليوم ، يجب أن تعمل التطبيقات في السوق العالمية من اليوم الأول. جعلت شركات مثل Twitter و Facebook 24/7 عبر الإنترنت شرطًا أساسيًا. لم تعد التطبيقات تقدم أي شيء ، لقد أصبحت تجربة المستخدم نفسها. يجب أن يكون لدى كل شركة اليوم منتجات برمجية. لم تعد "الموثوقية" و "التوفر" خصائص ، بل متطلبات.
لسوء الحظ ، بدأت الوحدات المتراصة في الانهيار عندما تمت إضافة "قابلية التوسع" و "التوافر" إلى المتطلبات. يحتاج المطورون والشركات على حد سواء إلى إيجاد طرق لمواكبة النمو العالمي المتفجر وتوقعات المستخدمين المتزايدة. كان عليّ البحث عن بنى بديلة تقلل من المشكلات الناشئة المرتبطة بالتدرج.
كانت الخدمات المصغرة (حسنًا ، البنى الموجهة نحو الخدمة) هي الحل. في البداية ، بدت وكأنها حل رائع لأنها سمحت لك بتقسيم التطبيقات إلى وحدات قائمة بذاتها نسبيًا يمكن تحجيمها بشكل مستقل. ونظرًا لأن كل خدمة صغيرة حافظت على حالتها الخاصة ، لم تعد التطبيقات مقتصرة على سعة جهاز واحد! تمكن المطورون أخيرًا من إنشاء برامج يمكن أن تتوسع مع العدد المتزايد من الاتصالات. كما منحت الخدمات المصغرة للفرق والشركات المرونة في عملهم بسبب الشفافية في المسؤولية وفصل البنى.

لا يوجد جبن مجاني
بينما نجحت الخدمات المصغرة في حل مشكلات قابلية التوسع والتوافر التي أعاقت نمو البرامج ، لم تكن الأمور خالية من السحاب. بدأ المطورون يدركون أن الخدمات المصغرة بها عيوب خطيرة.
تحتوي الوحدات المتجانسة عادةً على قاعدة بيانات واحدة وخادم تطبيق واحد. وبما أنه لا يمكن تقسيم الكتلة المتراصة ، فهناك طريقتان فقط للقياس:
- عمودي : ترقية الأجهزة لزيادة الإنتاجية أو السعة. يمكن أن يكون هذا القياس فعالاً ، لكنه مكلف. وبالتأكيد لن تحل المشكلة إلى الأبد إذا احتاج تطبيقك إلى الاستمرار في النمو. وإذا قمت بالتوسع بدرجة كافية ، فلن ينتهي بك الأمر بمعدات كافية للترقية.
- : , . , .
تختلف الخدمات المصغرة ، وتكمن قيمتها في القدرة على امتلاك العديد من "أنواع" قواعد البيانات وقوائم الانتظار والخدمات الأخرى التي يتم قياسها وإدارتها بشكل مستقل عن بعضها البعض. ومع ذلك ، فإن المشكلة الأولى التي بدأت بالملاحظة عند التبديل إلى الخدمات المصغرة كانت بالتحديد حقيقة أنه يتعين عليك الآن الاهتمام بمجموعة من جميع أنواع الخوادم وقواعد البيانات.
لفترة طويلة ، ترك كل شيء للصدفة ، وخرج المطورون والمشغلون بمفردهم. من الصعب معالجة مشاكل إدارة البنية التحتية التي تطرحها الخدمات المصغرة ، وفي أحسن الأحوال تضعف موثوقية التطبيق.
ومع ذلك ، ينشأ العرض استجابة للطلب. كلما زاد انتشار الخدمات المصغرة ، زاد تحفيز المطورين لحل مشكلات البنية التحتية. ببطء ولكن بثبات ، بدأت الأدوات في الظهور ، وسدّت تقنيات مثل Docker و Kubernetes و AWS Lambda الفجوة. لقد جعلوا بنية الخدمات المصغرة سهلة التشغيل للغاية. بدلاً من كتابة التعليمات البرمجية الخاصة بهم للتنسيق مع الحاويات والموارد ، يمكن للمطورين الاعتماد على الأدوات المعدة مسبقًا. في عام 2020 ، وصلنا أخيرًا إلى مرحلة مهمة حيث لم يعد توافر بنيتنا التحتية يتعارض مع موثوقية تطبيقاتنا. تماما!

بالطبع ، نحن لا نعيش بعد في المدينة الفاضلة للبرامج المستقرة تمامًا. لم تعد البنية التحتية مصدر عدم أمان التطبيق ؛ فقد حل رمز التطبيق مكانه.
مشكلة أخرى مع الخدمات المصغرة
في الأحجار المتجانسة ، يكتب المطورون كودًا يغير الحالات بطريقة ثنائية: إما أن يحدث شيء ما أو لا يحدث. ومع الخدمات المصغرة ، تنتشر الحالة عبر خوادم مختلفة. لتغيير حالة التطبيق ، يجب تحديث قواعد بيانات متعددة في نفس الوقت. من المحتمل أن يتم تحديث قاعدة بيانات واحدة بنجاح بينما تتعطل قواعد البيانات الأخرى ، مما يتركك في حالة وسيطة غير متسقة. ولكن نظرًا لأن الخدمات كانت الحل الوحيد لمشكلة القياس الأفقي ، لم يكن لدى المطورين أي خيار آخر.

تتمثل إحدى المشكلات الأساسية المتعلقة بالحالة الموزعة عبر الخدمات في أن كل مكالمة لخدمة خارجية سيكون لها نتيجة عشوائية من حيث التوافر. بالطبع ، يمكن للمطورين تجاهل المشكلة في التعليمات البرمجية الخاصة بهم والنظر في كل استدعاء لاعتماد خارجي دائمًا ما يكون ناجحًا. ولكن بعد ذلك ، يمكن لبعض التبعية إيقاف التطبيق دون سابق إنذار. لذلك ، كان على المطورين تكييف الكود الخاص بهم من عصر الأحجار المتراصة لإضافة شيكات لفشل العمليات في منتصف المعاملات. يعرض ما يلي باستمرار استرداد آخر حالة مسجلة من متجر myDB مخصص لتجنب ظروف السباق. لسوء الحظ ، حتى هذا التنفيذ لا يساعد. إذا تغيرت حالة الحساب دون تحديث myDB ، فقد يحدث تضارب.
public void transferWithoutTemporal(
String fromId,
String toId,
String referenceId,
double amount,
) {
boolean withdrawDonePreviously = myDB.getWithdrawState(referenceId);
if (!withdrawDonePreviously) {
account.withdraw(fromAccountId, referenceId, amount);
myDB.setWithdrawn(referenceId);
}
boolean depositDonePreviously = myDB.getDepositState(referenceId);
if (!depositDonePreviously) {
account.deposit(toAccountId, referenceId, amount);
myDB.setDeposited(referenceId);
}
}
للأسف ، من المستحيل كتابة التعليمات البرمجية بدون أخطاء. وكلما زادت تعقيد الكود ، زادت احتمالية ظهور الأخطاء. كما قد تتوقع ، فإن الكود الذي يعمل مع "البرامج الوسيطة" ليس معقدًا فحسب ، بل معقدًا أيضًا. بعض الموثوقية على الأقل أفضل من عدم الموثوقية ، لذلك كان على المطورين كتابة رمز عربات التي تجرها الدواب في البداية للحفاظ على تجربة المستخدم. يكلفنا الوقت والجهد ، ويكلّفنا أرباب العمل الكثير من المال. بينما يتم توسيع نطاق الخدمات المصغرة بشكل جميل ، إلا أنها تأتي بسعر متعة المطور وإنتاجيته وموثوقية التطبيق.
يقضي الملايين من المطورين وقتًا كل يوم في إعادة ابتكار واحدة من أكثر العجلات المعاد اختراعها - موثوقية القوالب. الأساليب الحديثة للعمل مع الخدمات المصغرة ببساطة لا تعكس متطلبات الموثوقية وقابلية التوسع في التطبيقات الحديثة.

زمني
الآن وصلنا إلى حلنا. لم يتم اعتماده من قبل Stack Overflow ، ولا ندعي أنه مثالي. نريد فقط مشاركة أفكارنا والاستماع إلى رأيك. ما هو أفضل مكان من Stack للحصول على تعليقات حول تحسين التعليمات البرمجية الخاصة بك؟
حتى اليوم ، لا يوجد حل يسمح لك باستخدام الخدمات المصغرة دون حل المشكلات الموضحة أعلاه. يمكنك اختبار ومحاكاة حالات التعطل ، وكتابة التعليمات البرمجية مع أخذ الأعطال في الاعتبار ، ولكن لا تزال هذه المشاكل تظهر. نعتقد أن المؤقت يحلها. إنها بيئة مفتوحة المصدر (MIT لا معنى لها) لتنسيق الخدمات الدقيقة.
يحتوي Temporal على مكونين رئيسيين: الخلفية ذات الحالة التي يتم تشغيلها على قاعدة البيانات التي تختارها ، وإطار عمل للعميل بإحدى اللغات المدعومة. يتم إنشاء التطبيقات باستخدام إطار عمل عميل ورمز قديم منتظم يحفظ تلقائيًا تغييرات الحالة في الواجهة الخلفية أثناء تشغيلها. يمكنك استخدام نفس التبعيات والمكتبات وبناء السلاسل كما تفعل عند إنشاء أي تطبيق آخر. بصراحة ، الواجهة الخلفية موزعة بشكل كبير ، لذا فهي ليست مثل J2EE 2.0. في الواقع ، فإن توزيع الواجهة الخلفية هو الذي يسمح بقياس أفقي غير محدود تقريبًا. يجلب Temporal الاتساق والبساطة والموثوقية إلى طبقة التطبيق ، كما فعلت البنية التحتية Docker و Kubernetes والبنية بدون خادم.
يوفر المؤقت عددًا من الآليات الموثوقة للغاية لتنسيق الخدمات المصغرة. لكن أهم شيء هو الحفاظ على الدولة. تستخدم هذه الوظيفة إصدار حدث لحفظ أي تغييرات ذات حالة على تطبيق قيد التشغيل تلقائيًا. بمعنى أنه في حالة تعطل الكمبيوتر الذي يعمل عليه Temporal ، سينتقل الرمز تلقائيًا إلى كمبيوتر آخر ، كما لو لم يحدث شيء. ينطبق هذا أيضًا على المتغيرات المحلية وخيوط التنفيذ والحالات الأخرى الخاصة بالتطبيق.
اسمحوا لي أن أقدم لكم تشبيه. بصفتك مطورًا ، ربما تعتمد اليوم على إصدار SVN (وهذا هو OG Git) لتتبع التغييرات التي تجريها على التعليمات البرمجية الخاصة بك. يقوم SVN فقط بحفظ الملفات الجديدة ثم الارتباط بالملفات الموجودة لتجنب الازدواجية. المؤقت هو شيء مثل SVN (تشبيه تقريبي) للتاريخ الحافل للتطبيقات قيد التشغيل. عندما يغير الرمز الخاص بك حالة التطبيق ، يقوم Temporal تلقائيًا بحفظ هذا التغيير (وليس النتيجة) دون أخطاء. أي أن Temporal لا يستعيد التطبيق المعطل فحسب ، بل يقوم أيضًا بإعادته إلى الوراء والتشعب والقيام بأكثر من ذلك بكثير. لذلك لم يعد المطورون بحاجة إلى إنشاء تطبيقات مع توقع احتمال تعطل الخادم.
يشبه التبديل من حفظ المستندات يدويًا (Ctrl + S) بعد كل حرف تم إدخاله إلى الحفظ السحابي التلقائي لـ Google Docs. ليس بمعنى أنك لم تعد تحفظ أي شيء يدويًا بعد الآن ، بل يعني فقط أنه لم يعد هناك جهاز واحد مرتبط بهذا المستند. تعني الحالة أنه يمكن للمطورين كتابة كود معياري أقل مملاً والذي يجب كتابته بسبب الخدمات المصغرة. بالإضافة إلى ذلك ، لم تعد بحاجة إلى بنية أساسية خاصة - قوائم انتظار وذاكرة تخزين مؤقت وقواعد بيانات منفصلة. هذا يجعل من السهل صيانة وإضافة ميزات جديدة. كما أنه يجعل من السهل جدًا تحديث المبتدئين ، لأنهم لا يحتاجون إلى فهم رمز إدارة الحالة المربك والمحدّد.
كما يتم تنفيذ الاحتفاظ بالحالة في شكل "مؤقتات مستمرة". هذه آلية آمنة من الفشل يمكن استخدامها مع الأمر
Workflow.sleep. إنه يعمل تمامًا مثل sleep. ومع ذلك ، Workflow.sleepيمكن الموت الرحيم بأمان لأي فترة زمنية. ينام العديد من المستخدمين المؤقتين لأسابيع أو حتى سنوات. يتم تحقيق ذلك من خلال تخزين أجهزة ضبط الوقت طويلة المدى في متجر Temporal وتتبع الرمز للاستيقاظ. مرة أخرى ، حتى إذا تعطل الخادم (أو قمت بإيقاف تشغيله للتو) ، فسوف ينتقل الرمز إلى الجهاز المتاح عند انتهاء صلاحية المؤقت. لا تستهلك عمليات النوم الموارد ، يمكن أن يكون لديك الملايين منها بنفقات لا تذكر. قد يبدو الأمر مجرّدًا جدًا ، لذا إليك مثال على رمز زمني عامل:
public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
private final SubscriptionActivities activities =
Workflow.newActivityStub(SubscriptionActivities.class);
public void execute(String customerId) {
activities.onboardToFreeTrial(customerId);
try {
Workflow.sleep(Duration.ofDays(180));
activities.upgradeFromTrialToPaid(customerId);
while (true) {
Workflow.sleep(Duration.ofDays(30));
activities.chargeMonthlyFee(customerId);
}
} catch (CancellationException e) {
activities.processSubscriptionCancellation(customerId);
}
}
}
بالإضافة إلى الحالة المستمرة ، تقدم Temporal مجموعة من الآليات لبناء تطبيقات قوية. يتم استدعاء وظائف النشاط من مهام سير العمل ، لكن الكود الذي يعمل داخل النشاط ليس له حالة. على الرغم من أنهم لا يحفظون الحالة الخاصة بهم ، إلا أن الأنشطة تحتوي على عمليات إعادة المحاولة التلقائية ، والمهلة ، ونبضات القلب. الأنشطة مفيدة جدًا لتغليف التعليمات البرمجية التي قد تفشل. لنفترض أن تطبيقك يستخدم واجهة برمجة تطبيقات مصرفية غير متوفرة غالبًا. بالنسبة للبرامج القديمة ، تحتاج إلى التفاف جميع التعليمات البرمجية التي تستدعي واجهة برمجة التطبيقات هذه بعبارات try / catch ، ومنطق إعادة المحاولة ، والمهلة. ولكن إذا اتصلت بواجهة برمجة التطبيقات المصرفية من نشاط ما ، فسيتم توفير كل هذه الوظائف خارج الصندوق: إذا فشلت المكالمة ، فستتم إعادة محاولة النشاط تلقائيًا. كل شيء رائعلكن في بعض الأحيان تمتلك خدمة غير موثوق بها وتريد حمايتها من DDoS. لذلك ، تدعم مكالمات النشاط أيضًا المهلات ، مدعومة بمؤقتات طويلة. أي أن فترات التوقف المؤقت بين تكرار الأنشطة يمكن أن تصل إلى ساعات أو أيام أو أسابيع. هذا مفيد بشكل خاص للكود الذي يجب أن يعمل بنجاح ، لكنك لست متأكدًا من السرعة التي يجب أن يحدث بها.
يشرح هذا الفيديو نموذج البرمجة في Temporal في دقيقتين:
قوة أخرى من Temporal هي إمكانية ملاحظة التطبيق قيد التشغيل. توفر واجهة برمجة تطبيقات Observation واجهة تشبه SQL للاستعلام عن البيانات الوصفية من أي سير عمل (قابل للتنفيذ أم لا). يمكنك أيضًا تحديد قيم البيانات الوصفية الخاصة بك وتحديثها داخل العملية. تعد واجهة برمجة تطبيقات Observation مفيدة جدًا للمشغلين والمطورين المؤقتين ، خاصة عند تصحيح الأخطاء أثناء التطوير. حتى أن المراقبة تدعم الإجراءات المجمعة على نتائج الاستعلام. على سبيل المثال ، يمكنك إرسال إشارة قتل إلى جميع عمليات العاملين التي تطابق طلبًا مع وقت إنشاء> أمس. يدعم المؤقت ميزة الجلب المتزامن التي تتيح لك سحب قيم المتغيرات المحلية من المثيلات قيد التشغيل. إنه يشبه عمل مصحح أخطاء من IDE الخاص بك مع تطبيقات الإنتاج. على سبيل المثال ، هذه هي الطريقة التي يمكنك من خلالها الحصول على القيمة
greeting في حالة تشغيل:
public static class GreetingWorkflowImpl implements GreetingWorkflow {
private String greeting;
@Override
public void createGreeting(String name) {
greeting = "Hello " + name + "!";
Workflow.sleep(Duration.ofSeconds(2));
greeting = "Bye " + name + "!";
}
@Override
public String queryGreeting() {
return greeting;
}
}
خاتمة
الخدمات المصغرة رائعة ، فهي تأتي على حساب الإنتاجية والموثوقية التي يدفعها المطورون والشركات. تم تصميم Temporal لحل هذه المشكلة من خلال توفير بيئة تدفع خدمات مصغرة للمطورين. تعد الحالة غير المألوفة ، والفشل التلقائي ، والجهات الرقابية مجرد بعض الميزات التي يوفرها Temporal والتي تجعل تطوير الخدمات المصغرة ذكيًا.