استخدام Reliable Collections

تقدم Service Fabric نموذج برمجة ذو حالة متاح لمطوري .NET عبر مجموعات موثوقة. على وجه التحديد، توفر خدمة Service Fabric قاموساً موثوقاً وفئات قائمة انتظار موثوقة. عند استخدام هذه الفئات، يتم تقسيم حالتك (لقابلية التوسع)، ونسخها نسخاً متماثلاً (للتوفر)، والتعامل معها داخل قسم (لدلالات ACID). دعونا نلقي نظرة على الاستخدام النموذجي لكائن قاموس موثوق ونرى ما يفعله بالفعل.

try
{
   // Create a new Transaction object for this partition
   using (ITransaction tx = base.StateManager.CreateTransaction())
   {
      // AddAsync takes key's write lock; if >4 secs, TimeoutException
      // Key & value put in temp dictionary (read your own writes),
      // serialized, redo/undo record is logged & sent to secondary replicas
      await m_dic.AddAsync(tx, key, value, cancellationToken);

      // CommitAsync sends Commit record to log & secondary replicas
      // After quorum responds, all locks released
      await tx.CommitAsync();
   }
   // If CommitAsync isn't called, Dispose sends Abort
   // record to log & all locks released
}
catch (TimeoutException)
{
   // choose how to handle the situation where you couldn't get a lock on the file because it was 
   // already in use. You might delay and retry the operation
   await Task.Delay(100);
}

تتطلب جميع العمليات على كائنات القاموس الموثوق (باستثناء ClearAsync، وهو أمر غير قابل للإلغاء)، كائن ITransaction. لقد ارتبط هذا الكائن به وأي وجميع التغييرات التي تحاول إجراؤها على أي قاموس موثوق وكائنات قائمة انتظار موثوقة أو أيٍ منهم داخل قسمٍ واحدٍ. يمكنك الحصول على كائن ITransaction عن طريق استدعاء أسلوب CreateTransaction الخاص بـ StateManager الخاص بالقسم.

في التعليمة البرمجية أعلاه، يتم تمرير كائن ITransaction إلى طريقة AddAsync الخاصة بقاموس موثوق. داخلياً، أساليب القاموس التي تقبل مفتاحاً تضع قفل القارئ/الكاتب المقترن بالمفتاح. إذا كان الأسلوب يعدِّل قيمة المفتاح، فإن الأسلوب يضع قفل الكتابة على المفتاح وإذا كان الأسلوب يقرأ فقط من قيمة المفتاح، فسيتم وضع قفل قراءة على المفتاح. نظراً لأن AddAsync يعدِّل قيمة المفتاح إلى القيمة الجديدة التي تم تمريرها، يتم وضع قفل الكتابة الخاص بالمفتاح. لذلك، إذا حاول مؤشرا ترابط (أو أكثر) إضافة قيم بنفس المفتاح في نفس الوقت، فسيضع مؤشر ترابط واحد على قفل الكتابة، وسيتم حظر مؤشرات الترابط الأخرى. بشكل افتراضي، يتم حظر الأساليب لمدة تصل إلى 4 ثوان للحصول على القفل؛ وبعد 4 ثوانٍ، تطرح الأساليب TimeoutException. توجد الأحمال الزائدة للأساليب مما يسمح لك بتمرير قيمة الوقت الصريحة إذا كنت تفضل ذلك.

عادةً ما تكتب تعليماتك البرمجية للتفاعل مع TimeoutException عن طريق التقاطها وإعادة محاولة العملية بأكملها (كما هو موضح في التعليمة البرمجية أعلاه). في هذه التعليمة البرمجية البسيطة، نحن نستدعي Task.Delay مروراً بـ 100 مللي ثانية في كل مرة فحسب. ولكن، في الواقع، قد يكون من الأفضل لك استخدام نوع من التأخير العكسي الأُسي بدلاً من ذلك.

بمجرد الحصول على القفل، يضيف AddAsync مراجع كائن المفتاح والقيمة إلى قاموس مؤقت داخلي مقترن بكائن ITransaction. يتم ذلك لتزويدك بدلالات "read-your-own-writes". أي، بعد استدعاء AddAsync، سيؤدي استدعاء لاحق إلى TryGetValueAsync باستخدام نفس كائن ITransaction إلى إرجاع القيمة حتى إذا لم تكن قد ثبتت المعاملة بعد.

إشعار

سيؤدي استدعاء TryGetValueAsync مع معاملة جديدة إلى إرجاع مرجع إلى آخر قيمة تم تثبيتها. لا تعدِّل ذلك المرجع مباشرةً، لأن ذلك يتجاوز آلية استمرار التغييرات وتكرارها. نوصي بجعل القيم للقراءة فقط بحيث تكون الطريقة الوحيدة لتغيير قيمة المفتاح من خلال واجهات برمجة تطبيقات القاموس الموثوق.

بعد ذلك، ينشئ AddAsync تسلسل لكائنات المفاتيح والقيم الخاصة بك إلى صفائف البايت وإلحاق صفائف البايت هذه بملف سجل على العُقدة المحلية. وأخيراً، يرسل AddAsync صفائف البايت إلى جميع النُسخ المتماثلة الثانوية بحيث يكون لديهم نفس معلومات المفتاح/القيمة. على الرغم من أن معلومات المفتاح/القيمة قد تمت كتابتها في ملف السجل، المعلومات لا تعتبر جزءاً من القاموس حتى تثبيت المعاملة المرتبطة بها.

في التعليمة البرمجية أعلاه، يثبت استدعاء CommitAsync جميع عمليات المعاملة. على وجه التحديد، فإنه يلحق معلومات التثبيت بملف السجل على العُقدة المحلية ويرسل أيضاً سجل التثبيت إلى جميع النُسخ المتماثلة الثانوية. بمجرد رد حصة (الأغلبية) النُسخ المتماثلة، تعتبر جميع تغييرات البيانات دائمة ويتم تحرير أي أقفال مرتبطة بالمفاتيح التي تم التلاعب بها عبر كائن ITransaction حتى تتمكن مؤشرات الترابط/المعاملات الأخرى من معالجة نفس المفاتيح وقيمها.

في حالة عدم استدعاء CommitAsync (عادةً بسبب طرح استثناء)، يتم التخلص من كائن ITransaction. عند التخلص من كائن ITransaction غير مثبت، تلحق Service Fabric معلومات الإجهاض بملف سجل العُقدة المحلية ولا يلزم إرسال أي شيء إلى أي من النُسخ المتماثلة الثانوية. وبعد ذلك، يتم إصدار أي أقفال مرتبطة بالمفاتيح التي تم التلاعب بها عبر المعاملة.

مجموعات موثوقة متغيرة

في بعض أحمال العمل، مثل ذاكرة التخزين المؤقت المنسوخة نسخاً متماثلاً على سبيل المثال، يمكن تحمل فقدان البيانات العرضي. يمكن أن يؤدي تجنب ثبات البيانات على القرص إلى توفير أزمنة انتقال ومعدلات نقل أفضل عند الكتابة إلى قواميس موثوقة. المفاضلة لعدم المثابرة هي أنه إذا حدث فقدان في الحصة، سيحدث فقدان كامل للبيانات. نظراً لأن فقدان الحصة أمر نادر الحدوث، فقد يكون الأداء المتزايد أمراً يستحق الاحتمال النادر لفقدان البيانات لأحمال العمل هذه.

حالياً، الدعم المتغير متاحٌ فقط للقواميس الموثوقة وقوائم الانتظار الموثوقة وليست ReliableConcurrentQueues. يُرجى الاطلاع على قائمة التحذيرات لإبلاغ قرارك بشأن ما إذا كنت ستستخدم مجموعات متغيرة.

لتمكين الدعم المتغير في خدمتك، بادر بتعيين العلامة HasPersistedState في إعلان نوع الخدمة إلى false، مثل:

<StatefulServiceType ServiceTypeName="MyServiceType" HasPersistedState="false" />

إشعار

لا يمكن جعل الخدمات المستمرة الحالية متغيرة، والعكس صحيح. إذا كنت ترغب في إجراء ذلك، فستحتاج إلى حذف الخدمة الحالية ثم توزيع الخدمة باستخدام العلامة المُحدَّثة. هذا يُعني أنه يجب أن تكون على استعداد لتحمل فقدان كامل للبيانات إذا كنت ترغب في تغيير العلامة HasPersistedState.

المخاطر الشائعة وكيفية تجنبها

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

using (ITransaction tx = StateManager.CreateTransaction())
{
   // AddAsync serializes the name/user, logs the bytes,
   // & sends the bytes to the secondary replicas.
   await m_dic.AddAsync(tx, name, user);

   // The line below updates the property's value in memory only; the
   // new value is NOT serialized, logged, & sent to secondary replicas.
   user.LastLogin = DateTime.UtcNow;  // Corruption!

   await tx.CommitAsync();
}

عند العمل مع قاموس .NET عادي، يمكنك إضافة مفتاح/قيمة إلى القاموس ثم تغيير قيمة خاصية (مثل LastLogin). ومع ذلك، لن تعمل هذه التعليمة البرمجية على نحوٍ صحيحٍ مع قاموسٍ موثوقٍ. تذكر من المناقشة السابقة، أن استدعاء AddAsync ينشئ تسلسل كائنات المفاتيح/القيمة إلى صفائف البايت، ثم يحفظ الصفائف في ملفٍ محلي ويرسلها أيضاً إلى النُسخ المتماثلة الثانوية. إذا غيَّرت لاحقاً خاصيةً ما، فإن هذا يغير قيمة الخاصية في الذاكرة فقط دون أن يؤثر ذلك على الملف المحلي أو البيانات المُرسلة إلى النسخ المتماثلة. إذا تعطلت العملية، يتم التخلص مما هو موجود في الذاكرة. عندما تبدأ عملية جديدة أو إذا أصبحت نسخة متماثلة أخرى أساسية، فإن قيمة الخاصية القديمة هي ما هو متاح.

لا أستطيع أن أؤكد بما فيه الكفاية على مدى سهولة ارتكاب هذا النوع من الأخطاء المُوضَّح أعلاه. وسوف تتعلم فقط عن الخطأ إذا/عندما تنخفض المعالجة. الطريقة الصحيحة لكتابة التعليمات البرمجية هي ببساطة عكس السطرين:

using (ITransaction tx = StateManager.CreateTransaction())
{
   user.LastLogin = DateTime.UtcNow;  // Do this BEFORE calling AddAsync
   await m_dic.AddAsync(tx, name, user);
   await tx.CommitAsync();
}

فيما يلي مثال آخر يوضح خطأ شائعاً:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> user = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (user.HasValue)
   {
      // The line below updates the property's value in memory only; the
      // new value is NOT serialized, logged, & sent to secondary replicas.
      user.Value.LastLogin = DateTime.UtcNow; // Corruption!
      await tx.CommitAsync();
   }
}

مرةً أخرى، مع قواميس .NET العادية، تعمل التعليمة البرمجية أعلاه على نحوٍ جيدٍ وهو نمط شائع: يستخدم المطور مفتاحاً للبحث عن قيمة. في حالة وجود القيمة، يغير المُطوِّر قيمة الخاصية. ومع ذلك، مع مجموعات موثوقة، تعرض هذه التعليمة البرمجية نفس المشكلة التي تمت مناقشتها بالفعل: يجب ألا تعدِّل كائن بمجرد إعطائه لمجموعة موثوقة.

الطريقة الصحيحة لتحديث قيمة في مجموعةٍ موثوقةٍ، هي الحصول على مرجع إلى القيمة الموجودة واعتبار الكائن المُشار إليه في هذا المرجع غير قابل للتغيير. إذن بادر بإنشاء كائن جديد يمثل نسخة طبق الأصل من الكائن الأصلي. الآن، يمكنك تعديل حالة هذا الكائن الجديد وكتابة الكائن الجديد في المجموعة بحيث يتم إنشاء تسلسل لصفائف البايت، وإلحاقه بالملف المحلي وإرساله إلى النُسخ المتماثلة. بعد تثبيت التغيير (التغييرات)، يكون للكائنات الموجودة في الذاكرة والملف المحلي وجميع النُسخ المتماثلة نفس الحالة الدقيقة. كل شيء جيد!

توضح التعليمة البرمجية أدناه الطريقة الصحيحة لتحديث قيمة في مجموعةٍ موثوقةٍ:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> currentUser = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (currentUser.HasValue)
   {
      // Create new user object with the same state as the current user object.
      // NOTE: This must be a deep copy; not a shallow copy. Specifically, only
      // immutable state can be shared by currentUser & updatedUser object graphs.
      User updatedUser = new User(currentUser);

      // In the new object, modify any properties you desire
      updatedUser.LastLogin = DateTime.UtcNow;

      // Update the key's value to the updateUser info
      await m_dic.SetValue(tx, name, updatedUser);
      await tx.CommitAsync();
   }
}

حدد أنواع البيانات غير القابلة للتغيير لمنع أخطاء المبرمج

من الناحية المثالية، نود أن يبلغ المحول البرمجي عن الأخطاء عندما تنتج عن طريق الخطأ تعليمة برمجية تغير حالة كائن من المفترض أن تعتبره غير قابل للتغيير. ولكن، لا يملك المحول البرمجي C# القدرة على القيام بذلك. لذلك، ولتجنب أخطاء المبرمج المحتملة، نوصي بشدة بتحديد الأنواع التي تستخدمها مع مجموعات موثوقة لتكون أنواعاً غير قابلة للتغيير. على وجه التحديد، هذا يُعني أنك تلتزم بأنواع القيم الأساسية (مثل الأرقام [Int32، UInt64، إلخ]، DateTime، Guid، TimeSpan، وما شابه ذلك). يمكنك أيضاً استخدام السلسلة. من الأفضل تجنب خصائص المجموعة لأن تسلسلها وإلغاء تسلسلها يمكن أن يضر بالأداء بصفةٍ متكررةٍ. ومع ذلك، إذا كنت ترغب في استخدام خصائص المجموعة، فإننا نوصي بشدة باستخدام مكتبة مجموعات .NET غير القابلة للتغيير (System.Collections.Immutable). هذه المكتبة متاحة للتنزيل من https://nuget.org. نوصي أيضاً بإغلاق فصولك الدراسية وجعل الحقول مصممةً للقراءة فقط كلما أمكن ذلك.

يوضح نوع UserInfo أدناه كيفية تعريف نوع غير قابل للتغيير مع الاستفادة من التوصيات المذكورة أعلاه.

[DataContract]
// If you don't seal, you must ensure that any derived classes are also immutable
public sealed class UserInfo
{
   private static readonly IEnumerable<ItemId> NoBids = ImmutableList<ItemId>.Empty;

   public UserInfo(String email, IEnumerable<ItemId> itemsBidding = null) 
   {
      Email = email;
      ItemsBidding = (itemsBidding == null) ? NoBids : itemsBidding.ToImmutableList();
   }

   [OnDeserialized]
   private void OnDeserialized(StreamingContext context)
   {
      // Convert the deserialized collection to an immutable collection
      ItemsBidding = ItemsBidding.ToImmutableList();
   }

   [DataMember]
   public readonly String Email;

   // Ideally, this would be a readonly field but it can't be because OnDeserialized
   // has to set it. So instead, the getter is public and the setter is private.
   [DataMember]
   public IEnumerable<ItemId> ItemsBidding { get; private set; }

   // Since each UserInfo object is immutable, we add a new ItemId to the ItemsBidding
   // collection by creating a new immutable UserInfo object with the added ItemId.
   public UserInfo AddItemBidding(ItemId itemId)
   {
      return new UserInfo(Email, ((ImmutableList<ItemId>)ItemsBidding).Add(itemId));
   }
}

نوع ItemId هو أيضاً نوع غير قابل للتغيير كما هو موضح هنا:

[DataContract]
public struct ItemId
{
   [DataMember] public readonly String Seller;
   [DataMember] public readonly String ItemName;
   public ItemId(String seller, String itemName)
   {
      Seller = seller;
      ItemName = itemName;
   }
}

تعيين إصدار المخطط (الترقيات)

داخلياً، تنشئ المجموعات الموثوقة تسلسل لكائناتك الخاصة باستخدام . DataContractSerializer من .NET. تستمر الكائنات المُدرجة بتسلسل في القرص المحلي للنسخة المتماثلة الأساسية ويتم نقلها أيضاً إلى النُسخ المتماثلة الثانوية. مع نضوج خدمتك، من المحتمل أنك ستحتاج إلى تغيير نوع البيانات (المخطط) التي تتطلبها خدمتك. تعامل مع تعيين إصدار لبياناتك بعنايةٍ فائقةٍ. أولاً وقبل كل شيء، يجب أن تكون دائماً قادراً على إلغاء تسلسل البيانات القديمة. على وجه التحديد، هذا يعني أن التعليمة البرمجية الخاصة بإلغاء التسلسل خاصتك يجب أن تكون متوافقةً مع الإصدارات السابقة بصفةٍ مطلقةٍ: يجب أن يكون الإصدار 333 من التعليمة البرمجية الخاصة بالخدمة خاصتك قادراً على العمل على البيانات الموضوعة في مجموعة موثوقة بواسطة الإصدار 1 من تعليمتك البرمجية الخاصة بالخدمة قبل 5 سنوات.

علاوة على ذلك، تتم ترقية التعليمة البرمجية الخاصة بالخدمة بمقدار نطاق ترقية واحد في كل مرة. لذلك، أثناء الترقية، يكون لديك إصداران مختلفان من التعليمة البرمجية الخاصة بالخدمة يعملان في وقتٍ واحدٍ. يجب تجنب استخدام الإصدار الجديد من التعليمات البرمجية الخاصة بالخدمة للمخطط الجديد لأن الإصدارات القديمة من التعليمات البرمجية الخاصة بالخدمة قد لا تتمكن من التعامل مع المخطط الجديد. عندما يكون ذلك ممكناً، ينبغي لك تصميم كل إصدار من خدمتك ليكون متوافقاً مع إصدار واحد سابق للإصدار الحالي. على وجه التحديد، هذا يُعني أن الإصدار V1 من التعليمة البرمجية الخاصة بالخدمة لديك يجب أن يكون قادراً على تجاهل أي عناصر لمخطط لا يتم التعامل معها صراحةً. ومع ذلك، يجب أن يكون قادراً على حفظ أي بيانات لا يعرفها صراحةً وإعادة كتابتها مرةً أخرى عند تحديث مفتاح أو قيمة في القاموس.

التحذير

بينما يمكنك تعديل مخطط المفتاح، يجب التأكد من أن خوارزميات المساواة والمقارنة الخاصة بالمفتاح مستقرة. سلوك المجموعات الموثوق بها بعد تغيير أي من هذه الخوارزميات غير معرف وقد يؤدي إلى تلف البيانات وفقدانها وتعطل الخدمة. يمكن استخدام سلاسل .NET كمفتاح ولكن استخدم السلسلة نفسها كمفتاح - لا تستخدم نتيجة String.GetHashCode كمفتاح.

بدلا من ذلك، يمكنك إجراء ترقية متعددة المراحل.

  1. ترقية الخدمة إلى إصدار جديد
    • يحتوي على كل من V1 الأصلي والإصدار V2 الجديد من عقود البيانات المضمنة في حزمة التعليمات البرمجية للخدمة؛
    • تسجيل مسلسلات حالة V2 المخصصة، إذا لزم الأمر؛
    • ينفذ جميع العمليات على مجموعة V1 الأصلية باستخدام عقود البيانات V1.
  2. ترقية الخدمة إلى إصدار جديد
    • إنشاء مجموعة V2 جديدة؛
    • ينفذ كل عملية إضافة وتحديث وحذف على أول V1 ثم مجموعات V2 في معاملة واحدة؛
    • ينفذ عمليات القراءة على مجموعة V1 فقط.
  3. انسخ جميع البيانات من مجموعة V1 إلى مجموعة V2.
    • يمكن القيام بذلك في عملية خلفية بواسطة إصدار الخدمة المنشور في الخطوة 2.
    • أعد معالجة جميع المفاتيح من مجموعة V1. يتم تنفيذ التعداد باستخدام IsolationLevel.Snapshot بشكل افتراضي لتجنب تأمين المجموعة طوال مدة العملية.
    • لكل مفتاح، استخدم معاملة منفصلة من أجل
      • جربGetValueAsync من مجموعة V1.
      • إذا تمت إزالة القيمة بالفعل من مجموعة V1 منذ بدء عملية النسخ، يجب تخطي المفتاح وعدم إعادة تأمينه في مجموعة V2.
      • TryAddAsync القيمة إلى مجموعة V2.
      • إذا تمت إضافة القيمة بالفعل إلى مجموعة V2 منذ بدء عملية النسخ، يجب تخطي المفتاح.
      • يجب أن يتم تنفيذ المعاملة فقط إذا كانت TryAddAsync ترجع true.
      • تستخدم واجهات برمجة تطبيقات الوصول إلى القيمة IsolationLevel.ReadRepeatable بشكل افتراضي وتعتمد على التأمين لضمان عدم تعديل القيم من قبل مستدعي آخر حتى يتم تنفيذ المعاملة أو إجهاضها.
  4. ترقية الخدمة إلى إصدار جديد
    • ينفذ عمليات القراءة على مجموعة V2 فقط؛
    • لا يزال ينفذ كل عملية إضافة وتحديث وحذف على أول V1 ثم مجموعات V2 للحفاظ على خيار العودة إلى V1.
  5. اختبر الخدمة بشكل شامل وتأكد من أنها تعمل كما هو متوقع.
    • إذا فاتتك أي عملية وصول إلى القيمة لم يتم تحديثها للعمل على كل من مجموعة V1 وV2، فقد تلاحظ فقدان البيانات.
    • إذا كانت هناك أي بيانات مفقودة للرجوع إلى الخطوة 1، فقم بإزالة مجموعة V2 وكرر العملية.
  6. ترقية الخدمة إلى إصدار جديد
    • تنفيذ كافة العمليات على مجموعة V2 فقط؛
    • لم يعد الرجوع إلى الإصدار 1 ممكنا مع عودة الخدمة إلى الحالة السابقة وسيتطلب العودة إلى الأمام مع الخطوات المعكوسة 2-4.
  7. ترقية الخدمة إصدار جديد
  8. انتظر اقتطاع السجل.
    • بشكل افتراضي، يحدث هذا كل 50 ميغابايت من عمليات الكتابة (إضافة وتحديثات وإزالات) إلى مجموعات موثوق بها.
  9. ترقية الخدمة إلى إصدار جديد
    • لم يعد لديه عقود البيانات V1 المضمنة في حزمة التعليمات البرمجية للخدمة.

الخطوات التالية

لمعرفة المزيد عن إنشاء عقود بيانات متوافقة، راجع عقود البيانات المتوافقة مع إعادة التوجيه

لمعرفة أفضل الممارسات المتعلقة بتعيين إصدارات عقود البيانات، راجع تعيين إصدار عقد البيانات

لمعرفة كيفية تنفيذ عقود البيانات المتسامحة مع الإصدارات، راجع عمليات رد الاتصال الخاصة بإنشاء تسلسل متسامح مع الإصدار

لمعرفة كيفية توفير بنية بيانات يمكن تشغيلها بينياً عبر إصدارات متعددة، راجع IExtensibleDataObject

لمعرفة كيفية تكوين مجموعات موثوق بها، راجع تكوين النسخ المتماثل