بغض النظر عما إذا كنت تتابع أم لا ، فإن تطبيقات الويب Java تستخدم تجمعات مؤشرات الترابط لمعالجة الطلبات إلى حد أكبر أو أقل. قد يتم تجاهل تفاصيل تنفيذ تجمعات الخيوط ، ولكن من الضروري فهم عاجلاً أم آجلاً على استخدام وضبط تجمعات الخيوط. تقدم هذه المقالة بشكل أساسي استخدام تجمع مؤشرات الترابط Java وكيفية تكوين مجموعة مؤشرات الترابط بشكل صحيح.
خيوط واحدة
لنبدأ بالأساسيات. بغض النظر عن خادم التطبيق أو إطار العمل (مثل Tomcat ، Jetty ، وما إلى ذلك) ، لديهم تطبيقات أساسية مماثلة. أساس خدمة الويب هو مقبس ، مسؤول عن الاستماع إلى المنفذ ، في انتظار اتصال TCP ، وقبول اتصال TCP. بمجرد قبول اتصال TCP ، يمكن قراءة البيانات وإرسالها من اتصال TCP الذي تم إنشاؤه حديثًا.
من أجل فهم العملية أعلاه ، لا نستخدم أي خادم تطبيق مباشرة ، ولكن بناء خدمة ويب بسيطة من نقطة الصفر. هذه الخدمة عبارة عن صورة مصغرة لمعظم خوادم التطبيقات. يبدو أن خدمة ويب واحدة بسيطة واحدة مثل هذا:
ServersOcket Bearcher = New ServersOcket (8080) ؛ جرب {بينما (صحيح) {socket socket = leader.accept () ؛ حاول {HandleRequest (Socket) ؛ } catch (ioException e) {E.PrintStackTrace () ؛ }}} أخيرًا {beader.close () ؛}يقوم الرمز أعلاه بإنشاء مقبس خادم (ServersOction) ، ويستمع إلى المنفذ 8080 ، ثم حلقات للتحقق من المقبس لمعرفة ما إذا كان هناك اتصال جديد. بمجرد قبول اتصال جديد ، سيتم تمرير المقبس إلى طريقة HandleRequest. تقوم هذه الطريقة بتجديد دفق البيانات في طلب HTTP ، ويستجيب ، ويكتب بيانات الاستجابة. في هذا المثال البسيط ، تقوم طريقة HandleRequest ببساطة بتنفيذ القراءة في دفق البيانات وإرجاع بيانات استجابة بسيطة. بشكل عام ، ستكون هذه الطريقة أكثر تعقيدًا ، مثل قراءة البيانات من قاعدة بيانات ، إلخ.
استجابة السلسلة الثابتة النهائية = "http/1.0 200 ok/r/n" + "نوع المحتوى: text/plain/r/n" + "/r/n" + "Hello World/r/n" ؛ يرمي HandleRequest public static static (مقبس المقبس) ioException {// اقرأ دفق الإدخال ، وإرجاع "200 OK" try {bufferedReader in = new BufferedReader (New InputStreamReader (socket.getInputStream ())) ؛ log.info (in.readline ()) ؛ outputStream Out = socket.getOutputStream () ؛ out.write (response.getBytes (StandardCharsets.UTF_8)) ؛ } أخيرًا {socket.close () ؛ }}نظرًا لوجود موضوع واحد فقط لمعالجة الطلب ، يجب أن ينتظر كل طلب معالجة الطلب السابق قبل الرد عليه. بافتراض أن وقت استجابة الطلب هو 100 ميلي ثانية ، وعدد الردود في الثانية (TPS) من هذا الخادم هو 10 فقط.
متعدد الخيوط
على الرغم من أن طريقة HandleRequest قد تمنع على IO ، إلا أنه لا يزال بإمكان وحدة المعالجة المركزية التعامل مع المزيد من الطلبات. ولكن في حالة واحدة الخيوط ، لا يمكن القيام بذلك. لذلك ، يمكن تحسين إمكانية المعالجة المتوازية للخادم عن طريق إنشاء طرق متعددة الخيوط.
الطبقة الثابتة العامة HandleRequestrunnable الأدوات Runnable {Final Socket Socket ؛ HandleRequestrunnable (مقبس المقبس) {this.socket = socket ؛ } public void run () {try {HandleRequest (socket) ؛ } catch (ioException e) {E.PrintStackTrace () ؛ }}} serverSocket Bearner = new ServersOcket (8080) ؛ حاول {بينما (صحيح) {socket socket = leader.accept () ؛ موضوع جديد (HandleRequestrunnable (Socket)). start () ؛ }} أخيرًا {leader.close () ؛}هنا ، لا تزال طريقة قبول () في مؤشر الترابط الرئيسي ، ولكن بمجرد إنشاء اتصال TCP ، سيتم إنشاء مؤشر ترابط جديد للتعامل مع الطلب الجديد ، وهو تنفيذ طريقة HandleRequest في النص السابق في مؤشر الترابط الجديد.
من خلال إنشاء مؤشر ترابط جديد ، يمكن أن يستمر مؤشر الترابط الرئيسي في قبول اتصالات TCP جديدة ، ويمكن معالجة هذه الطلبات بالتوازي. تسمى هذه الطريقة "موضوع واحد لكل طلب". بالطبع ، هناك طرق أخرى لتحسين أداء المعالجة ، مثل النموذج غير المتزامن الذي يحركه الأحداث المستخدمة من قبل Nginx و Node.js ، لكنها لا تستخدم تجمعات الخيوط وبالتالي لا يتم تغطيتها في هذه المقالة.
في كل طلب تنفيذ مؤشر ترابط واحد ، يعد إنشاء مؤشر ترابط (والتدمير اللاحق) مكلفًا للغاية لأن كل من JVM ونظام التشغيل يحتاجون إلى تخصيص الموارد. بالإضافة إلى ذلك ، فإن التنفيذ أعلاه لديه مشكلة أيضًا ، أي أن عدد مؤشرات الترابط التي تم إنشاؤها لا يمكن السيطرة عليها ، مما قد يتسبب في استنفاد موارد النظام بسرعة.
الموارد المنهكة
يتطلب كل مؤشر ترابط قدرًا معينًا من مساحة ذاكرة المكدس. في أحدث 64 بت JVM ، يبلغ حجم المكدس الافتراضي 1024 كيلو بايت. إذا تلقى الخادم عددًا كبيرًا من الطلبات ، أو يتم تنفيذ طريقة HandleRequest ببطء ، فقد يتعطل الخادم بسبب إنشاء عدد كبير من مؤشرات الترابط. على سبيل المثال ، هناك 1000 طلب متوازي ، ويحتاج مؤشرات الترابط 1000 التي تم إنشاؤها إلى استخدام 1 جيجابايت من ذاكرة JVM كمساحة مكدس مؤشر ترابط. بالإضافة إلى ذلك ، يمكن أيضًا إنشاء الكائنات التي تم إنشاؤها أثناء تنفيذ رمز كل مؤشر ترابط على الكومة. إذا تفاقم هذا الموقف ، فسوف يتجاوز ذاكرة كومة JVM ويولد كمية كبيرة من عمليات جمع القمامة ، مما سيؤدي في النهاية إلى تجاوز الذاكرة (OutofMemoryerrors).
لا تستهلك هذه المواضيع الذاكرة فحسب ، بل تستخدم أيضًا موارد محدودة أخرى ، مثل مقابض الملفات ، واتصالات قاعدة البيانات ، وما إلى ذلك. لذلك ، تتمثل إحدى الطرق المهمة لتجنب استنفاد الموارد في تجنب هياكل البيانات التي لا يمكن السيطرة عليها.
بالمناسبة ، نظرًا لمشاكل الذاكرة الناتجة عن حجم مكدس الخيط ، يمكن ضبط حجم المكدس من خلال مفتاح -XSS. بعد تقليل حجم مكدس الخيط ، يمكن تقليل النفقات العامة لكل مؤشر ترابط ، ولكن قد يتم رفع سعة مكدس (stackoverflowerrors). بالنسبة للتطبيقات العامة ، يكون الافتراضي 1024 كيلو بايت غنيًا جدًا ، وقد يكون من الأفضل تقليله إلى 256 كيلو بايت أو 512 كيلو بايت. القيمة الدنيا المسموح بها في Java هي 160 كيلو بايت.
بركة الموضوع
لتجنب إنشاء مؤشرات ترابط جديدة ، يمكنك الحد من الحد الأعلى لمجموعة مؤشرات الترابط باستخدام تجمع مؤشر ترابط بسيط. تجمع الخيوط يدير جميع المواضيع. إذا لم يصل عدد مؤشرات الترابط إلى الحد الأعلى ، فإن تجمع مؤشرات الترابط ينشئ مؤشرات ترابط إلى الحد الأعلى ويعيد استخدام المواضيع الحرة قدر الإمكان.
ServersOcket Bearner = New ServersOcket (8080) ؛ evectorService evelysor = evectors.NewFixedThreadPool (4) ؛ حاول {بينما (صحيح) {socket socket = leader.accept () ؛ Executor.Submit (New HandleRequestrunnable (Socket)) ؛ }} أخيرًا {leader.close () ؛}في هذا المثال ، بدلاً من إنشاء مؤشر الترابط مباشرة ، يتم استخدام خدمة ExecutorService. يقدم المهام التي تحتاج إلى تنفيذ (تحتاج إلى تنفيذ واجهة RunNables) إلى تجمع مؤشرات الترابط وتنفيذ الكود باستخدام مؤشرات الترابط في تجمع مؤشرات الترابط. في المثال ، يتم استخدام تجمع مؤشرات ترابط ثابت الحجم مع عدد من المواضيع من 4 لمعالجة جميع الطلبات. هذا يحد من عدد المواضيع التي تتعامل مع الطلبات وأيضًا يحد من استخدام الموارد.
بالإضافة إلى إنشاء مجموعة مؤشرات ترابط ذات حجم ثابت من خلال طريقة NewFixedThreadPool ، توفر فئة Executors أيضًا طريقة NewCacheDthreadPool. قد لا يزال إعادة استخدام تجمع مؤشرات الترابط يؤدي إلى عدد لا يمكن السيطرة عليه من مؤشرات الترابط ، ولكنه سيستخدم مؤشرات الترابط الخاملة التي تم إنشاؤها قبل قدر الإمكان. عادةً ما يكون هذا النوع من تجمع الخيوط مناسبًا للمهام القصيرة التي لا يتم حظرها بواسطة الموارد الخارجية.
قائمة انتظار العمل
بعد استخدام تجمع مؤشرات ترابط ثابت ، إذا كانت جميع مؤشرات الترابط مشغولة ، فما الذي سيحدث إذا جاء طلب آخر؟ يستخدم ThreadPoolExecutor قائمة انتظار للاحتفاظ بطلبات معلقة ، وتستخدم تجمعات مؤشرات الترابط ذات الحجم الثابت قوائم مرتبطة غير محدودة بشكل افتراضي. لاحظ أن هذا قد يتسبب بدوره في مشاكل استنفاد الموارد ، ولكن لن يحدث ذلك طالما كانت سرعة معالجة الخيط أكبر من معدل نمو قائمة الانتظار. ثم في المثال السابق ، سيعقد كل طلب في قائمة الانتظار مقبسًا ، وسيستهلك في بعض أنظمة التشغيل مقبض الملفات. نظرًا لأن نظام التشغيل يحد من عدد مقابض الملفات التي تم فتحها بواسطة العملية ، فمن الأفضل الحد من حجم قائمة انتظار العمل.
static static evecororservice newboundedfixedThreadPool (int nthreads ، int int) {return new threadpoolexecutor (nThReads ، nThReads ، 0L ، timeUnit.milliseconds ، new LinkedBlocking <Runnable> (القدرة) ، threadpoolexecutor.discardpolicy ()) ؛ يلقي ioException {serversocket beasher = new ServersOcket (8080) ؛ ExecutorService Executor = newBoundEdFixedThreadPool (4 ، 16) ؛ جرب {بينما (صحيح) {socket socket = leader.accept () ؛ Executor.Submit (New HandleRequestrunnable (Socket)) ؛ }} أخيرًا {leader.close () ؛ }}هنا ، بدلاً من استخدام Executors.NewFixedThreadPool مباشرة لإنشاء تجمع مؤشرات ترابط ، قمنا ببناء كائن Threadpoolexecutor بأنفسنا وقمنا بتقييد طول قائمة انتظار العمل إلى 16 عنصرًا.
إذا كانت جميع المواضيع مشغولة ، فسيتم ملء المهمة الجديدة في قائمة الانتظار. نظرًا لأن قائمة الانتظار تحد من الحجم إلى 16 عنصرًا ، إذا تم تجاوز هذا الحد ، فيجب التعامل معه بواسطة المعلمة الأخيرة عند إنشاء كائن ThreadPoolexecutor. في المثال ، يتم استخدام discardpolicy ، أي عندما تصل قائمة الانتظار إلى الحد الأعلى ، سيتم التخلص من المهمة الجديدة. بالإضافة إلى المرة الأولى ، هناك أيضًا سياسة الإحباط (AbortPolicy) وسياسة تنفيذ المتصل (CallerRunspolicy). السابق سوف يلقي استثناء ، في حين أن الأخير سوف ينفذ المهمة في مؤشر ترابط المتصل.
بالنسبة لتطبيقات الويب ، يجب أن تكون السياسة الافتراضية المثلى للتخلي عن السياسة أو إحباطها وإرجاع خطأ إلى العميل (مثل خطأ HTTP 503). بالطبع ، من الممكن أيضًا تجنب التخلي عن طلبات العميل عن طريق زيادة طول قائمة انتظار العمل ، لكن طلبات المستخدم غير راغبة بشكل عام في الانتظار لفترة طويلة ، وسيستهلك ذلك المزيد من موارد الخادم. الغرض من قائمة انتظار العمل هو عدم الاستجابة لطلبات العميل دون الحد ، ولكن تهدئة الطلبات. عادة ، يجب أن تكون قائمة انتظار العمل فارغة.
ترابط عدد الضبط
يوضح المثال السابق كيفية إنشاء واستخدام تجمع مؤشرات الترابط ، ولكن المشكلة الأساسية مع استخدام تجمع مؤشرات الترابط هي عدد المواضيع التي يجب استخدامها. أولاً ، نحتاج إلى التأكد من أنه عند الوصول إلى حد الخيط ، لن يتم استنفاد المورد. تشمل الموارد هنا الذاكرة (كومة ومكدس) ، وعدد مقابض الملفات المفتوحة ، وعدد اتصالات TCP ، وعدد اتصالات قاعدة البيانات عن بُعد ، وغيرها من الموارد المحدودة. على وجه الخصوص ، إذا كانت المهام الخيطية مكثفة من الناحية الحسابية ، فإن عدد نوى وحدة المعالجة المركزية هو أيضًا أحد قيود الموارد. بشكل عام ، يجب ألا يتجاوز عدد المواضيع عدد نوى وحدة المعالجة المركزية.
نظرًا لأن اختيار عدد مؤشرات الترابط يعتمد على نوع التطبيق ، فقد يتطلب الأمر الكثير من اختبار الأداء قبل الحصول على النتائج المثلى. بالطبع ، يمكنك أيضًا تحسين أداء التطبيق الخاص بك عن طريق زيادة عدد الموارد. على سبيل المثال ، قم بتعديل حجم ذاكرة كومة JVM ، أو قم بتعديل الحد الأعلى لمقبض الملفات لنظام التشغيل ، وما إلى ذلك ، ثم ستصل هذه التعديلات في النهاية إلى الحد الأعلى النظري.
قانون ليتل
يصف قانون ليتل العلاقة بين ثلاثة متغيرات في نظام مستقر.
عندما يمثل L متوسط عدد الطلبات ، يمثل λ تواتر الطلبات ، ويمثل W متوسط الوقت للرد على الطلب. على سبيل المثال ، إذا كان عدد الطلبات في الثانية هو 10 وكل طلب معالجة وقت واحد ، في أي لحظة يتم معالجة 10 طلبات. بالعودة إلى موضوعنا ، يتطلب 10 مؤشرات ترابط للمعالجة. إذا كان وقت المعالجة لطلب واحد يتضاعف ، فإن عدد المواضيع التي تمت معالجتها ستضاعف أيضًا ، ليصبح 20.
بعد فهم تأثير وقت المعالجة على كفاءة معالجة الطلب ، سنجد أن الحد الأعلى النظري قد لا يكون القيمة المثلى لحجم تجمع مؤشرات الترابط. يتطلب الحد الأعلى لتجمع الخيوط أيضًا وقت معالجة المهام المرجعية.
على افتراض أن JVM يمكنه معالجة 1000 مهمة بالتوازي ، إذا لم يتجاوز كل وقت معالجة الطلب 30 ثانية ، ثم في أسوأ الحالات ، يمكن معالجة 33.3 طلبًا في الثانية على الأقل. ومع ذلك ، إذا كان كل طلب يستغرق 500 مللي ثانية فقط ، يمكن للتطبيق معالجة 2000 طلب في الثانية.
سبليت بركة الخيط
في الخدمات المجهرية أو البنية الموجهة نحو الخدمة (SOA) ، عادة ما يكون الوصول إلى خدمات الواجهة الخلفية المتعددة مطلوبًا. إذا كانت إحدى الخدمات تنفيذها متدهورة ، فقد يتسبب ذلك في نفاد مجموعة مؤشرات الترابط من مؤشرات الترابط ، مما يؤثر على طلبات الخدمات الأخرى.
تتمثل إحدى الطرق الفعالة في التعامل مع فشل خدمة الواجهة الخلفية في عزل تجمع الخيوط المستخدمة من قبل كل خدمة. في هذا الوضع ، لا يزال هناك تجمع مؤشرات ترابط تم إرساله يرسل المهام إلى تجمعات مؤشرات ترابط طلبات الخلفية المختلفة. قد لا يكون لهذا تجمع الخيوط أي تحميل بسبب وجود خلفية بطيئة ، ونقل العبء إلى تجمع مؤشرات الترابط الذي يطلب الواجهة الخلفية البطيئة.
بالإضافة إلى ذلك ، يحتاج وضع التجميع متعدد الخيوط أيضًا إلى تجنب مشاكل الجمود. إذا كان كل مؤشر ترابط يحظر أثناء انتظار نتيجة طلب غير معالج ، يحدث Deadlock. لذلك ، في وضع البلياردو متعدد مؤشرات الترابط ، من الضروري فهم المهام التي ينفذها كل تجمع مؤشر ترابط وتبعيات بينهما ، وذلك لتجنب مشاكل الجمود قدر الإمكان.
لخص
حتى إذا لم يتم استخدام تجمعات الخيوط مباشرة في التطبيق ، فمن المحتمل أن يتم استخدامها بشكل غير مباشر بواسطة خادم التطبيق أو إطار العمل في التطبيق. توفر الأطر مثل Tomcat و JBoss و UnderTow و Dropwizard ، إلخ. جميعها توفر خيارات لضبط تجمعات مؤشرات الترابط (تجمعات مؤشرات الترابط المستخدمة في تنفيذ Servlet).
آمل أن تتمكن هذه المقالة من تحسين فهمك لمجموعة الخيوط وتساعدك على التعلم.