
مقدمة
مفاعل I / O ( حلقة حدث مفردة الترابط) هو نمط لكتابة البرامج المحملة بدرجة عالية ، ويستخدم في العديد من الحلول الشائعة:
في هذه المقالة ، سننظر في خصوصيات وعموميات مفاعل الإدخال / الإخراج ومبدأ تشغيله ، وكتابة تطبيق لأقل من 200 سطر من التعليمات البرمجية وفرض خادم HTTP بسيط لمعالجة أكثر من 40 مليون طلب / دقيقة.
مقدمة
- تمت كتابة المقال بهدف المساعدة في فهم عمل مفاعل الإدخال / الإخراج ، وبالتالي إدراك المخاطر عند استخدامه.
- لإتقان المقال ، يلزم معرفة أساسيات اللغة C وتجربة صغيرة في تطوير تطبيقات الشبكة.
- تتم كتابة جميع الشفرات في لغة C بدقة وفقًا لـ ( بعناية: PDF طويل ) معيار C11 لنظام Linux وهو متاح على GitHub .
لماذا هذا مطلوب؟
مع ازدياد شعبية الإنترنت ، تحتاج خوادم الويب إلى معالجة عدد كبير من الاتصالات في نفس الوقت ، وبالتالي تمت تجربة طريقتين: حظر الإدخال / الإخراج على عدد كبير من سلاسل عمليات OS وإدخال / إخراج غير محظور مع نظام إخطار الأحداث ، والذي يسمى أيضًا "system" محدد "( epoll / kqueue / IOCP / etc).
تتضمن الطريقة الأولى إنشاء مؤشر ترابط نظام تشغيل جديد لكل اتصال وارد. العيب هو ضعف قابلية التوسع: سيتعين على نظام التشغيل إجراء العديد من انتقالات السياق ومكالمات النظام . إنها عمليات باهظة الثمن ويمكن أن تؤدي إلى نقص ذاكرة الوصول العشوائي المجانية مع وجود عدد كبير من الاتصالات.
يخصص الإصدار المعدل عددًا ثابتًا من مؤشرات الترابط (تجمع مؤشرات الترابط) ، وبالتالي يمنع النظام من إيقاف التنفيذ بشكل غير طبيعي ، ولكن في نفس الوقت يقدم مشكلة جديدة: إذا كان تجمع مؤشرات الترابط محظورًا في الوقت المحدد من خلال عمليات القراءة الطويلة ، فإن مآخذ التوصيل الأخرى التي يمكنها بالفعل تلقي البيانات لن تكون قادرة على القيام بذلك.
يستخدم الأسلوب الثاني نظام إعلام الأحداث (محدد النظام) الذي يوفره نظام التشغيل. تتناول هذه المقالة النوع الأكثر شيوعًا من محدد النظام استنادًا إلى التنبيهات (الأحداث والإشعارات) حول الاستعداد لعمليات الإدخال / الإخراج ، بدلاً من التنبيهات حول اكتمالها . يمكن تمثيل مثال مبسط لاستخدامه بواسطة المخطط الانسيابي التالي:

الفرق بين هذه الأساليب كالتالي:
- يؤدي حظر عمليات الإدخال / الإخراج إلى تعليق دفق المستخدم حتى يقوم نظام التشغيل بإلغاء تجزئة حزم IP الواردة بشكل صحيح في دفق البايت ( TCP ، استقبال البيانات) أو تحرير مساحة كافية في مخازن الكتابة الداخلية المؤقتة للإرسال اللاحق عبر NIC (إرسال البيانات).
- بعد فترة من الوقت ، يقوم محدد النظام بإخطار البرنامج بأن نظام التشغيل قام بالفعل بإلغاء تجزئة حزم IP (TCP ، استقبال البيانات) أو أن مساحة كافية في مخازن التخزين الداخلية للتسجيل متاحة بالفعل (إرسال البيانات).
للتلخيص ، يعد حجز مؤشر ترابط نظام التشغيل لكل إدخال / إخراج مضيعة لقوة الحوسبة ، لأنه في الواقع ، لا تشغل مؤشرات الترابط عملًا مفيدًا (مصطلح "مقاطعة البرنامج" له جذوره فيه ). يعمل محدد النظام على حل هذه المشكلة عن طريق السماح لبرنامج المستخدم باستهلاك موارد وحدة المعالجة المركزية بطريقة أكثر اقتصادا.
مفاعل I / O نموذج
يعمل مفاعل الإدخال / الإخراج كطبقة بين محدد النظام ورمز المستخدم. يتم وصف مبدأ عملها من خلال المخطط الانسيابي التالي:

- اسمحوا لي أن أذكرك بأن هذا الحدث هو إخطار بأن مأخذ توصيل معين قادر على إجراء عملية إدخال / إخراج غير محظورة.
- معالج الأحداث هو وظيفة يطلق عليها مفاعل الإدخال / الإخراج عند تلقي حدث ، والذي يقوم بعد ذلك بإجراء عملية إدخال / إخراج غير محظورة.
من المهم أن نلاحظ أن مفاعل الإدخال / الإخراج هو بحكم التعريف مترابط واحد ، ولكن لا شيء يمنع استخدام المفهوم في بيئة متعددة الخيوط فيما يتعلق بتيار واحد: 1 مفاعل ، وبالتالي يستخدم جميع النوى وحدة المعالجة المركزية.
تطبيق
نضع الواجهة العامة في ملف reactor.h
، والتطبيق في reactor.c
. سوف تتكون reactor.h
من التصريحات التالية:
عرض الإعلانات في reactor.h typedef struct reactor Reactor; typedef void (*Callback)(void *arg, int fd, uint32_t events); Reactor *reactor_new(void); int reactor_destroy(Reactor *reactor); int reactor_register(const Reactor *reactor, int fd, uint32_t interest, Callback callback, void *callback_arg); int reactor_deregister(const Reactor *reactor, int fd); int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest, Callback callback, void *callback_arg); int reactor_run(const Reactor *reactor, time_t timeout);
تتكون بنية I / O الخاصة بالمفاعل من واصف ملف محدد epoll GHashTable
تجزئة GHashTable
، يقوم كل مقبس GHashTable
إلى GHashTable
(بنية من معالج حدث وسيطة مستخدم له).
إظهار مفاعل و CallbackData struct reactor { int epoll_fd; GHashTable *table;
يرجى ملاحظة أننا استخدمنا القدرة على التعامل مع نوع غير مكتمل عن طريق المؤشر. في reactor.h
نعلن عن بنية reactor
، و في reactor.c
بتعريفه ، وبالتالي نمنع المستخدم من تغيير حقوله بشكل صريح. هذا هو واحد من أنماط إخفاء البيانات التي تناسب عضويا في دلالات C.
تقوم reactor_register
reactor_deregister
و reactor_deregister
و reactor_reregister
بتحديث قائمة مآخذ الاهتمام ومعالجات الأحداث المناظرة في محدد النظام وفي جدول التجزئة.
إظهار ميزات التسجيل #define REACTOR_CTL(reactor, op, fd, interest) \ if (epoll_ctl(reactor->epoll_fd, op, fd, \ &(struct epoll_event){.events = interest, \ .data = {.fd = fd}}) == -1) { \ perror("epoll_ctl"); \ return -1; \ } int reactor_register(const Reactor *reactor, int fd, uint32_t interest, Callback callback, void *callback_arg) { REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest) g_hash_table_insert(reactor->table, int_in_heap(fd), callback_data_new(callback, callback_arg)); return 0; } int reactor_deregister(const Reactor *reactor, int fd) { REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0) g_hash_table_remove(reactor->table, &fd); return 0; } int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest, Callback callback, void *callback_arg) { REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest) g_hash_table_insert(reactor->table, int_in_heap(fd), callback_data_new(callback, callback_arg)); return 0; }
بعد اعتراض مفاعل الإدخال / الإخراج الحدث مع واصف fd
، فإنه يستدعي معالج الحدث المقابل ، والذي يمرر فيه fd
، وقناع بت الأحداث التي تم إنشاؤها ، ومؤشر المستخدم ليتم void
.
إظهار وظيفة reactor_run () int reactor_run(const Reactor *reactor, time_t timeout) { int result; struct epoll_event *events; if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL) abort(); time_t start = time(NULL); while (true) { time_t passed = time(NULL) - start; int nfds = epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed); switch (nfds) {
لتلخيص ، ستتخذ سلسلة استدعاءات الوظائف في كود المستخدم الشكل التالي:

خادم واحد مترابطة
من أجل اختبار مفاعل الإدخال / الإخراج تحت تحميل عالي ، سنكتب خادم ويب HTTP بسيط للرد على أي طلب مع صورة.
مرجع بروتوكول HTTP السريعHTTP هو بروتوكول على مستوى التطبيق يستخدم بشكل أساسي لتفاعل الخادم مع المستعرض.
يمكن بسهولة استخدام HTTP أعلى بروتوكول نقل TCP ، وإرسال واستقبال رسائل بالتنسيق المحدد بواسطة المواصفات .
<> <URI> < HTTP>CRLF < 1>CRLF < 2>CRLF < N>CRLF CRLF <>
CRLF
عبارة عن سلسلة من حرفين: \r
و \n
، تفصل السطر الأول من الاستعلام والرؤوس والبيانات.<>
هي واحدة من CONNECT
و DELETE
و GET
و HEAD
و OPTIONS
و PATCH
و POST
و PUT
و TRACE
. سيرسل المتصفح أمر GET
إلى خادمنا ، بمعنى "أرسل لي محتويات الملف".<URI>
هو معرف المورد الموحد . على سبيل المثال ، إذا كان URI = /index.html
، /index.html
العميل الصفحة الرئيسية للموقع.< HTTP>
- إصدار بروتوكول HTTP/XY
بتنسيق HTTP/XY
. الإصدار الأكثر استخدامًا حتى الآن هو HTTP/1.1
.< N>
هو زوج ذو قيمة <>: <>
بالتنسيق <>: <>
، تم إرساله إلى الخادم لمزيد من التحليل.<>
- البيانات المطلوبة من قبل الخادم لإكمال العملية. غالبًا ما يكون مجرد JSON أو أي تنسيق آخر.
< HTTP> < > < >CRLF < 1>CRLF < 2>CRLF < N>CRLF CRLF <>
< >
هو رقم يمثل نتيجة العملية. سيرجع خادمنا دائمًا إلى الحالة 200 (التشغيل الناجح).< >
- تمثيل سلسلة لرمز الحالة. لرمز الحالة 200 ، هذا OK
.< N>
- رأس بنفس التنسيق كما في الطلب. سنقوم بإرجاع رؤوس Content-Length
(حجم الملف) ونوع Content-Type: text/html
(بيانات نوع الإرجاع).<>
- البيانات المطلوبة من قبل المستخدم. في حالتنا ، هذا هو المسار إلى الصورة في HTML .
يتضمن http_server.c
(خادم ذو http_server.c
فردية) ملف common.h
، والذي يحتوي على نماذج أولية للوظيفة التالية:
إظهار النماذج الأولية للوظيفة في common.h static void on_accept(void *arg, int fd, uint32_t events); static void on_send(void *arg, int fd, uint32_t events); static void on_recv(void *arg, int fd, uint32_t events); static void set_nonblocking(int fd); static noreturn void fail(const char *format, ...); static int new_server(bool reuse_port);
SAFE_CALL()
الماكرو الدالة SAFE_CALL()
أيضًا وتعرف الدالة fail()
. يقارن الماكرو قيمة التعبير مع الخطأ ، وإذا تم استيفاء الشرط ، فإنه يستدعي الدالة fail()
:
#define SAFE_CALL(call, error) \ do { \ if ((call) == error) { \ fail("%s", #call); \ } \ } while (false)
تقوم الدالة fail()
بطباعة الوسائط التي تم تمريرها إلى الجهاز الطرفي (مثل printf()
) وتنهي البرنامج برمز EXIT_FAILURE
:
static noreturn void fail(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); fprintf(stderr, ": %s\n", strerror(errno)); exit(EXIT_FAILURE); }
ترجع الدالة new_server()
واصف ملف مأخذ توصيل "الخادم" الذي تم إنشاؤه بواسطة استدعاء النظام socket()
و bind()
و listen()
وقادرة على قبول الاتصالات الواردة في وضع عدم الحظر.
إظهار الوظيفة new_server () static int new_server(bool reuse_port) { int fd; SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)), -1); if (reuse_port) { SAFE_CALL( setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)), -1); } struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(SERVER_PORT), .sin_addr = {.s_addr = inet_addr(SERVER_IPV4)}, .sin_zero = {0}}; SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1); SAFE_CALL(listen(fd, SERVER_BACKLOG), -1); return fd; }
- لاحظ أنه يتم إنشاء مأخذ التوصيل مبدئيًا في وضع عدم الحظر باستخدام علامة
SOCK_NONBLOCK
، بحيث في on_accept()
(لمزيد من القراءة) ، لا يتوقف استدعاء نظام accept()
عن تنفيذ الدفق. - إذا كان
reuse_port
true
، reuse_port
هذه الوظيفة بتهيئة المقبس باستخدام خيار SO_REUSEPORT
باستخدام setsockopt()
لاستخدام نفس المنفذ في بيئة متعددة الخيوط (انظر القسم "خادم متعدد الخيوط").
يتم on_accept()
معالج الأحداث on_accept()
بعد قيام نظام التشغيل بإنشاء حدث EPOLLIN
، مما يعني في هذه الحالة أنه يمكن قبول اتصال جديد. يقبل on_accept()
اتصالًا جديدًا ، on_accept()
بالتبديل إلى وضع عدم الحظر ويسجل باستخدام معالج الأحداث on_recv()
في مفاعل الإدخال / الإخراج.
إظهار وظيفة on_accept () static void on_accept(void *arg, int fd, uint32_t events) { int incoming_conn; SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1); set_nonblocking(incoming_conn); SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv, request_buffer_new()), -1); }
يتم on_recv()
معالج الأحداث on_recv()
بعد قيام نظام التشغيل بإنشاء حدث EPOLLIN
، مما يعني في هذه الحالة أن الاتصال المسجل on_accept()
جاهز لاستقبال البيانات.
on_recv()
بقراءة البيانات من الاتصال حتى يتم استلام طلب HTTP الكامل ، ثم يقوم بتسجيل معالج on_send()
لإرسال استجابة HTTP. في حالة قطع اتصال العميل ، يتم إلغاء تثبيت مأخذ التوصيل close()
.
إظهار وظيفة on_recv () static void on_recv(void *arg, int fd, uint32_t events) { RequestBuffer *buffer = arg;
يتم on_send()
معالج الأحداث on_send()
بعد قيام نظام التشغيل بإنشاء حدث EPOLLOUT
، مما يعني أن الاتصال المسجل بواسطة on_recv()
جاهز لإرسال البيانات. ترسل هذه الوظيفة استجابة HTTP تحتوي على HTML مع الصورة إلى العميل ، ثم تقوم بتغيير معالج الأحداث إلى on_recv()
مرة أخرى.
إظهار وظيفة on_send () static void on_send(void *arg, int fd, uint32_t events) { const char *content = "<img " "src=\"https://habrastorage.org/webt/oh/wl/23/" "ohwl23va3b-dioerobq_mbx4xaw.jpeg\">"; char response[1024]; sprintf(response, "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: " "text/html" DOUBLE_CRLF "%s", strlen(content), content); SAFE_CALL(send(fd, response, strlen(response), 0), -1); SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1); }
وأخيرًا ، في الملف http_server.c
، في الوظيفة main()
، نقوم بإنشاء مفاعل reactor_new()
/ إخراج باستخدام reactor_new()
، وإنشاء مقبس خادم وتسجيله ، وبدء تشغيل المفاعل باستخدام reactor_run()
دقيقة واحدة بالضبط ، ثم تحرير الموارد والخروج من البرنامج.
عرض http_server.c #include "reactor.h" static Reactor *reactor; #include "common.h" int main(void) { SAFE_CALL((reactor = reactor_new()), NULL); SAFE_CALL( reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL), -1); SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1); SAFE_CALL(reactor_destroy(reactor), -1); }
تحقق من أن كل شيء يعمل كما هو متوقع. نحن نجمع ( chmod a+x compile.sh && ./compile.sh
في جذر المشروع) وبدء تشغيل الخادم المكتوب ذاتيًا ، افتح http://127.0.0.1:18470 في المستعرض ولاحظ ما هو متوقع:

قياس الأداء
عرض خصائص سيارتي $ screenfetch MMMMMMMMMMMMMMMMMMMMMMMMMmds+. OS: Mint 19.1 tessa MMm----::-://////////////oymNMd+` Kernel: x86_64 Linux 4.15.0-20-generic MMd /++ -sNMd: Uptime: 2h 34m MMNso/` dMM `.::-. .-::.` .hMN: Packages: 2217 ddddMMh dMM :hNMNMNhNMNMNh: `NMm Shell: bash 4.4.20 NMm dMM .NMN/-+MMM+-/NMN` dMM Resolution: 1920x1080 NMm dMM -MMm `MMM dMM. dMM DE: Cinnamon 4.0.10 NMm dMM -MMm `MMM dMM. dMM WM: Muffin NMm dMM .mmd `mmm yMM. dMM WM Theme: Mint-Y-Dark (Mint-Y) NMm dMM` ..` ... ydm. dMM GTK Theme: Mint-Y [GTK2/3] hMM- +MMd/-------...-:sdds dMM Icon Theme: Mint-Y -NMm- :hNMNNNmdddddddddy/` dMM Font: Noto Sans 9 -dMNs-``-::::-------.`` dMM CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C] `/dMNmy+/:-------------:/yMMM GPU: NV136 ./ydNMMMMMMMMMMMMMMMMMMMMM RAM: 2544MiB / 7926MiB \.MMMMMMMMMMMMMMMMMMM
نقيس أداء خادم ذو سلسلة واحدة. دعنا نفتح محطتين: في أحدهما نركض ./http_server
، في الآخر - ضعيف . بعد دقيقة ، سيتم عرض الإحصاءات التالية في المحطة الثانية:
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive" Running 1m test @ http://127.0.0.1:18470 8 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 493.52us 76.70us 17.31ms 89.57% Req/Sec 24.37k 1.81k 29.34k 68.13% 11657769 requests in 1.00m, 1.60GB read Requests/sec: 193974.70 Transfer/sec: 27.19MB
تمكن خادمنا ذو الخيوط المفردة من معالجة أكثر من 11 مليون طلب في الدقيقة ، انطلاقًا من 100 اتصال. ليست نتيجة سيئة ، ولكن هل يمكن تحسينها؟
خادم متعدد مؤشرات الترابط
كما ذكر أعلاه ، يمكن إنشاء مفاعل الإدخال / الإخراج في تدفقات منفصلة ، وبالتالي الاستفادة من جميع النوى وحدة المعالجة المركزية. لنطبق هذا النهج في الممارسة:
عرض http_server_multithreaded.c #include "reactor.h" static Reactor *reactor; #pragma omp threadprivate(reactor) #include "common.h" int main(void) { #pragma omp parallel { SAFE_CALL((reactor = reactor_new()), NULL); SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN, on_accept, NULL), -1); SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1); SAFE_CALL(reactor_destroy(reactor), -1); } }
الآن كل خيط يملك مفاعله الخاص :
static Reactor *reactor; #pragma omp threadprivate(reactor)
لاحظ أن الوسيطة إلى new_server()
true
. هذا يعني أننا نقوم بتعيين مقبس الخادم على خيار SO_REUSEPORT
لاستخدامه في بيئة متعددة الخيوط. يمكنك قراءة المزيد هنا .
المدى الثاني
الآن قياس أداء خادم متعدد مؤشرات الترابط:
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive" Running 1m test @ http://127.0.0.1:18470 8 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.14ms 2.53ms 40.73ms 89.98% Req/Sec 79.98k 18.07k 154.64k 78.65% 38208400 requests in 1.00m, 5.23GB read Requests/sec: 635876.41 Transfer/sec: 89.14MB
زاد عدد الطلبات التي تمت معالجتها في دقيقة واحدة بنسبة 3.28 مرة تقريبًا! ولكن حتى الرقم المستدير ، لم يكن يكفي سوى مليوني شخص ، فلنحاول إصلاحه.
أولاً ، إلقاء نظرة على الإحصاءات الناتجة عن perf :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded Performance counter stats for './http_server_multithreaded': 242446,314933 task-clock (msec) # 4,000 CPUs utilized 1 813 074 context-switches # 0,007 M/sec 4 689 cpu-migrations # 0,019 K/sec 254 page-faults # 0,001 K/sec 895 324 830 170 cycles # 3,693 GHz 621 378 066 808 instructions # 0,69 insn per cycle 119 926 709 370 branches # 494,653 M/sec 3 227 095 669 branch-misses # 2,69% of all branches 808 664 cache-misses 60,604330670 seconds time elapsed
باستخدام تقارب وحدة المعالجة المركزية (CPU) ، فإن التحويل البرمجي باستخدام -march=native
، PGO ، وزيادة عدد مرات الدخول في ذاكرة التخزين المؤقت ، وزيادة MAX_EVENTS
واستخدام EPOLLET
لم يوفر زيادة كبيرة في الأداء. ولكن ماذا يحدث إذا قمت بزيادة عدد الاتصالات المتزامنة؟
إحصائيات 352 اتصال متزامن:
$ wrk -c352 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive" Running 1m test @ http://127.0.0.1:18470 8 threads and 352 connections Thread Stats Avg Stdev Max +/- Stdev Latency 2.12ms 3.79ms 68.23ms 87.49% Req/Sec 83.78k 12.69k 169.81k 83.59% 40006142 requests in 1.00m, 5.48GB read Requests/sec: 665789.26 Transfer/sec: 93.34MB
تم الحصول على النتيجة المرغوبة ، ومعها رسم بياني مثير للاهتمام يوضح عدد طلبات المعالجة في دقيقة واحدة على عدد الاتصالات:

نرى أنه بعد بضع مئات من الاتصالات ، انخفض عدد الطلبات التي تمت معالجتها من كلا الخادمين بشكل حاد (في إصدار متعدد الخيوط ، يكون هذا أكثر وضوحًا). هل هذا يرتبط بتنفيذ مكدس Linux TCP / IP؟ لا تتردد في كتابة افتراضاتك حول مثل هذا السلوك البياني وتحسينات ذات مؤشرات ترابط متعددة وخيط واحد في التعليقات.
كما هو موضح في التعليقات ، لا يُظهر اختبار الأداء هذا سلوك مفاعل الإدخال / الإخراج عند التحميل الحقيقي ، لأن الخادم يتفاعل دائمًا مع قاعدة البيانات دائمًا ، ويعرض السجلات ، ويستخدم التشفير مع TLS ، وما إلى ذلك ، ونتيجة لذلك يصبح الحمل غير متجانس (ديناميكي). سيتم إجراء الاختبارات جنبًا إلى جنب مع مكونات الطرف الثالث في مقال عن وكيل الإدخال / الإخراج.
مساوئ I / O مفاعل
يجب أن تفهم أن مفاعل الإدخال / الإخراج لا يخلو من العيوب ، وهي:
- استخدام مفاعل الإدخال / الإخراج في بيئة متعددة الخيوط هو أكثر صعوبة إلى حد ما ، لأنه لديك لإدارة التدفقات يدويا.
- تدل الممارسة على أن الحمل غير متجانس في معظم الحالات ، مما قد يؤدي إلى حقيقة أن أحد الخيوط سيتم إخماده بينما يتم تحميل الآخر مع العمل.
- إذا قام معالج حدث واحد بحظر البث ، فسيتم أيضًا حظر محدد النظام نفسه ، مما قد يؤدي إلى أخطاء يصعب صيدها.
يتم حل هذه المشكلات عن طريق برنامج الإدخال / الإخراج ، وغالبًا ما يكون هناك برنامج جدولة يقوم بتوزيع التحميل بالتساوي على تجمع مؤشرات الترابط ، كما يحتوي على واجهة برمجة تطبيقات أكثر ملاءمة. سيتم مناقشته لاحقًا في مقالتي الأخرى.
استنتاج
في هذا ، انتهت رحلتنا من النظرية مباشرة إلى ملف تعريف العادم.
لا تفصل في هذا الأمر ، لأن هناك العديد من الأساليب الأخرى المثيرة للاهتمام بنفس القدر لكتابة برامج الشبكة بمستويات مختلفة من الراحة والسرعة. مثيرة للاهتمام ، في رأيي ، يتم إعطاء الروابط أدناه.
اراك قريبا!
مشاريع مثيرة للاهتمام
ماذا تقرأ؟