Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Бұл Биржаның жұмысын қамтамасыз ететін қуатты, жоғары жүктемелі жүйені құрудағы қиын жолымыз туралы ұзақ әңгіменің жалғасы. Бірінші бөлім мына жерде: habr.com/en/post/444300

Жұмбақ қате

Көптеген сынақтардан кейін жаңартылған сауда және клиринг жүйесі іске қосылды және біз детектив-мистикалық әңгіме жаза алатын қатеге тап болдық.

Негізгі серверде іске қосылғаннан кейін көп ұзамай транзакциялардың бірі қатемен өңделді. Дегенмен, резервтік серверде бәрі жақсы болды. Негізгі сервердегі көрсеткішті есептеудің қарапайым математикалық операциясы нақты аргументтен теріс нәтиже бергені анықталды! Біз зерттеуімізді жалғастырдық және SSE2 регистрінде өзгермелі нүкте сандарымен жұмыс істеу кезінде дөңгелектеуге жауап беретін бір биттің айырмашылығын таптық.

Дөңгелектеу бит жиынымен көрсеткішті есептеу үшін қарапайым сынақ утилитасын жаздық. Біз пайдаланған RedHat Linux нұсқасында қате бит енгізілген кезде математикалық функциямен жұмыс істеуде қате бар екені белгілі болды. Біз бұл туралы RedHat-ке хабарладық, біраз уақыттан кейін біз олардан патч алдық және оны шығардық. Қате бұдан былай орын алмады, бірақ бұл бит қайдан шыққаны белгісіз болды ма? Бұл функция жауапты болды fesetround Си тілінен.Біз болжамды қатені іздеу үшін кодымызды мұқият талдадық: біз барлық мүмкін жағдайларды тексердік; дөңгелектеуді пайдаланатын барлық функцияларды қарастырды; сәтсіз сеансты қайта шығаруға тырысты; әртүрлі нұсқалары бар әртүрлі компиляторларды пайдаланды; Статикалық және динамикалық талдаулар қолданылды.

Қатенің себебін табу мүмкін болмады.

Содан кейін олар аппараттық құралдарды тексеруге кірісті: олар процессорлардың жүктемелік сынақтарын жүргізді; жедел жадты тексерді; Біз тіпті бір ұяшықтағы көп разрядты қатенің өте екіталай сценарийі үшін сынақтарды жүргіздік. Нәтижесіз.

Соңында біз жоғары энергия физикасы әлемінің теориясына тоқтадық: кейбір жоғары энергиялы бөлшектер біздің деректер орталығына ұшып келіп, корпустың қабырғасын тесіп, процессорға соғылып, триггер ысырмасының дәл сол жерде жабысып қалуына себеп болды. Бұл абсурдтық теория «нейтрино» деп аталды. Егер сіз бөлшектер физикасынан алыс болсаңыз: нейтрино дерлік сыртқы әлеммен әрекеттеспейді және процессордың жұмысына әсер ете алмайды.

Ақаулықтың себебін табу мүмкін болмағандықтан, «бұзушы» сервер кез келген жағдайда жұмыстан шығарылды.

Біраз уақыттан кейін біз ыстық резервтік жүйені жетілдіре бастадық: біз «жылы резервтер» (жылы) деп аталатын асинхронды көшірмелерді енгіздік. Олар әртүрлі деректер орталықтарында орналасуы мүмкін транзакциялар ағынын алды, бірақ жылытулар басқа серверлермен белсенді әрекеттеспеді.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Бұл не үшін жасалды? Сақтық көшірме сервері сәтсіз болса, негізгі серверге қосылған жылы жаңа сақтық көшірме болады. Яғни, сәтсіздіктен кейін жүйе сауда сессиясының соңына дейін бір негізгі серверде қалмайды.

Жүйенің жаңа нұсқасы сыналып, іске қосылған кезде, дөңгелектеу бит қатесі қайтадан пайда болды. Сонымен қатар, жылы серверлер санының артуымен қате көбінесе пайда бола бастады. Сонымен бірге, Жеткізушіде ештеңе жоқ, өйткені нақты дәлелдер болғандықтан.

Жағдайды келесі талдау кезінде мәселе ОЖ-мен байланысты болуы мүмкін деген теория пайда болды. Біз шексіз циклде функцияны шақыратын қарапайым бағдарламаны жаздық fesetround, ағымдағы күйді есте сақтайды және оны ұйқы арқылы тексереді және бұл көптеген бәсекелес ағындарда орындалады. Ұйқыға арналған параметрлерді және ағындар санын таңдап, біз утилитаны іске қосқаннан кейін шамамен 5 минуттан кейін бит сәтсіздігін дәйекті түрде қайталай бастадық. Алайда Red Hat қолдауы оны қайта шығара алмады. Басқа серверлерімізді тестілеу қатеге белгілі бір процессорлары барлар ғана бейім екенін көрсетті. Сонымен қатар, жаңа ядроға ауысу мәселені шешті. Соңында біз ОЖ-ны жай ғана ауыстырдық, ал қатенің нақты себебі түсініксіз болып қалды.

Өткен жылы кенеттен Хабреде мақала жарияланды.Intel Skylake процессорларында қатені қалай таптым" Онда сипатталған жағдай біздікіне өте ұқсас болды, бірақ автор тергеуді әрі қарай жалғастырып, қате микрокодта болды деген теорияны алға тартты. Linux ядролары жаңартылған кезде, өндірушілер микрокодты да жаңартады.

Жүйені одан әрі дамыту

Қатеден құтылғанымызбен, бұл оқиға бізді жүйе архитектурасын қайта қарауға мәжбүр етті. Өйткені, біз мұндай қателердің қайталануынан сақтанбадық.

Төмендегі принциптер брондау жүйесін келесі жетілдірулер үшін негіз болды:

  • Сіз ешкімге сене алмайсыз. Серверлер дұрыс жұмыс істемеуі мүмкін.
  • Көпшілік резерві.
  • Консенсусты қамтамасыз ету. Көпшілік резервіне логикалық қосымша ретінде.
  • Екі жақты сәтсіздіктер болуы мүмкін.
  • Өмірлік. Жаңа ыстық күту схемасы бұрынғыдан нашар болмауы керек. Сауда соңғы серверге дейін үзіліссіз жалғасуы керек.
  • Кідірістің шамалы артуы. Кез келген үзіліс үлкен қаржылық шығындарға әкеледі.
  • Кідірісті мүмкіндігінше төмен ұстау үшін желілік өзара әрекеттесу аз.
  • Секундтарда жаңа негізгі серверді таңдау.

Нарықтағы шешімдердің ешқайсысы бізге сәйкес келмеді, ал Raft хаттамасы әлі бастапқы кезеңде болды, сондықтан біз өз шешімімізді жасадық.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Желі құру

Брондау жүйесінен басқа, біз желілік өзара әрекеттесуді жаңғыртуды бастадық. Енгізу/шығару ішкі жүйесі көптеген процестерден тұрды, олар діріл мен кідіріске ең нашар әсер етті. TCP қосылымдарын өңдейтін жүздеген процестермен біз олардың арасында үнемі ауысуға мәжбүр болдық және микросекундтық масштабта бұл өте көп уақытты қажет ететін операция. Бірақ ең сорақысы процесс өңдеуге пакетті алған кезде оны бір SystemV кезегіне жіберіп, содан кейін басқа SystemV кезегінен оқиғаны күтеді. Дегенмен, түйіндердің көп саны болғанда, бір процесте жаңа TCP пакетінің келуі және басқасында кезектегі деректердің түсуі ОЖ үшін бәсекелес екі оқиғаны білдіреді. Бұл жағдайда екі тапсырма үшін де қол жетімді физикалық процессорлар болмаса, біреуі өңделеді, ал екіншісі күту кезегіне қойылады. Оның салдарын болжау мүмкін емес.

Мұндай жағдайларда динамикалық процестің басымдылығын басқаруды қолдануға болады, бірақ бұл үшін ресурстарды көп қажет ететін жүйелік шақыруларды пайдалану қажет болады. Нәтижесінде біз классикалық epoll көмегімен бір ағынға ауыстық, бұл жылдамдықты едәуір арттырды және транзакцияны өңдеу уақытын қысқартты. Біз сондай-ақ жекелеген желілік байланыс процестерінен және SystemV арқылы байланыстан құтылдық, жүйелік қоңыраулар санын айтарлықтай қысқарттық және операциялардың басымдықтарын бақылауға кірістік. Тек енгізу/шығару ішкі жүйесінде сценарийге байланысты шамамен 8-17 микросекундты үнемдеуге болады. Бұл бір ағынды схема сол уақыттан бері өзгеріссіз пайдаланылды; барлық қосылымдарға қызмет көрсету үшін маржа бар бір эполл ағыны жеткілікті.

Транзакцияны өңдеу

Жүйеміздегі өсіп келе жатқан жүктеме оның барлық дерлік компоненттерін жаңартуды қажет етті. Бірақ, өкінішке орай, соңғы жылдардағы процессордың тактілік жиілігінің өсуінің тоқырауы бұдан былай процестерді бетпе-бет масштабтауға мүмкіндік бермеді. Сондықтан біз Engine процесін үш деңгейге бөлуді шештік, олардың ішіндегі ең қарқындысы шоттардағы қаражаттың болуын бағалайтын және транзакцияларды өздері жасайтын тәуекелдерді тексеру жүйесі болып табылады. Бірақ ақша әртүрлі валютада болуы мүмкін және сұраныстарды өңдеуді қандай негізде бөлу керектігін анықтау қажет болды.

Логикалық шешім оны валютаға бөлу болып табылады: бір сервер доллармен, екіншісі фунтпен, ал үшіншісі еуромен сауда жасайды. Бірақ егер мұндай схемамен әртүрлі валюталарды сатып алуға екі транзакция жіберілсе, әмиянды синхрондау мәселесі туындайды. Бірақ синхрондау қиын және қымбат. Сондықтан әмияндар бойынша бөлек және құралдар бойынша бөлек сындыру дұрыс болар еді. Айтпақшы, батыстық биржалардың көпшілігінде тәуекелдерді біздегідей өткір тексеру міндеті жоқ, сондықтан бұл көбінесе офлайн режимде жүзеге асырылады. Бізге онлайн тексеруді енгізу қажет болды.

Мысалмен түсіндірейік. Трейдер $30 сатып алғысы келеді және сұрау транзакцияны тексеруге жіберіледі: біз бұл трейдерге осы сауда режиміне рұқсат етілгенін және оның қажетті құқықтары бар-жоғын тексереміз. Егер бәрі тәртіппен болса, сұрау тәуекелді тексеру жүйесіне өтеді, яғни. мәміле жасау үшін қаражаттың жеткіліктілігін тексеру. Қажетті соманың қазіргі уақытта бұғатталғаны туралы ескертпе бар. Содан кейін сұрау транзакцияны мақұлдайтын немесе қабылдамайтын сауда жүйесіне жіберіледі. Мәміле мақұлданды делік - содан кейін тәуекелді тексеру жүйесі ақшаның бұғаттан босатылғанын белгілейді, ал рубль долларға айналады.

Жалпы алғанда, тәуекелді тексеру жүйесі күрделі алгоритмдерді қамтиды және өте көп ресурстарды қажет ететін есептеулерді орындайды және бір қарағанда «шот балансын» жай ғана тексермейді.

Қозғалтқыш процесін деңгейлерге бөлуді бастағанда, біз мәселеге тап болдық: сол кезде қол жетімді код валидация және тексеру кезеңдерінде бірдей деректер массивін белсенді түрде пайдаланды, бұл бүкіл код базасын қайта жазуды талап етті. Нәтижесінде біз заманауи процессорлардан нұсқауларды өңдеу техникасын алдық: олардың әрқайсысы шағын кезеңдерге бөлінген және бір циклде бірнеше әрекеттер параллель орындалады.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Кодты шағын бейімдеуден кейін біз транзакцияны параллельді өңдеуге арналған құбырды құрдық, онда транзакция құбырдың 4 кезеңіне бөлінді: желілік өзара әрекеттесу, тексеру, орындау және нәтижені жариялау

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Мысал қарастырайық. Бізде екі өңдеу жүйесі бар, сериялық және параллель. Бірінші транзакция келеді және екі жүйеде де тексеруге жіберіледі. Екінші транзакция бірден келеді: параллельді жүйеде ол бірден жұмысқа қабылданады, ал дәйекті жүйеде бірінші транзакцияның ағымдағы өңдеу кезеңінен өтуін күту үшін кезекке қойылады. Яғни, құбырды өңдеудің басты артықшылығы - транзакция кезегін тезірек өңдейміз.

Осылайша біз ASTS+ жүйесін ойлап таптық.

Рас, конвейерлермен де бәрі бірдей тегіс емес. Көрші транзакциядағы деректер массивтеріне әсер ететін транзакция бар делік; бұл алмасу үшін әдеттегі жағдай. Мұндай транзакцияны конвейерде орындау мүмкін емес, себебі ол басқаларға әсер етуі мүмкін. Бұл жағдай деректер қаупі деп аталады және мұндай транзакциялар жай ғана бөлек өңделеді: кезектегі «жылдам» транзакциялар біткенде, құбыр тоқтайды, жүйе «баяу» транзакцияны өңдейді, содан кейін құбырды қайтадан іске қосады. Бақытымызға орай, мұндай транзакциялардың жалпы ағындағы үлесі өте аз, сондықтан құбыр өте сирек тоқтайды, бұл жалпы өнімділікке әсер етпейді.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Содан кейін біз орындаудың үш ағынын синхрондау мәселесін шеше бастадық. Нәтиже бекітілген өлшемді ұяшықтары бар сақиналы буферге негізделген жүйе болды. Бұл жүйеде барлығы өңдеу жылдамдығына бағынады, деректер көшірілмейді.

  • Барлық кіріс желілік пакеттер бөлу кезеңіне өтеді.
  • Біз оларды массивке орналастырамыз және оларды №1 кезең үшін қолжетімді деп белгілейміз.
  • Екінші транзакция келді, ол №1 кезең үшін қайтадан қолжетімді.
  • Бірінші өңдеу ағыны қол жетімді транзакцияларды көреді, оларды өңдейді және екінші өңдеу ағынының келесі кезеңіне жылжытады.
  • Содан кейін ол бірінші транзакцияны өңдейді және сәйкес ұяшықты белгілейді deleted — ол енді жаңа пайдалану үшін қол жетімді.

Бүкіл кезек осылай өңделеді.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Әрбір кезеңді өңдеу бірліктерді немесе ондаған микросекундтарды алады. Ал егер біз стандартты ОЖ синхрондау схемаларын қолданатын болсақ, онда біз синхрондаудың өзіне көбірек уақыт жоғалтамыз. Сондықтан біз спинлокты қолдана бастадық. Дегенмен, бұл нақты уақыттағы жүйеде өте нашар пішін және RedHat мұны қатаң түрде ұсынбайды, сондықтан біз 100 мс үшін спинлокты қолданамыз, содан кейін тығырықтану мүмкіндігін жою үшін семафорлық режимге ауысамыз.

Нәтижесінде секундына шамамен 8 миллион транзакция өнімділігіне қол жеткіздік. Ал екі айдан кейін мақала LMAX Disruptor туралы біз бірдей функционалдығы бар тізбектің сипаттамасын көрдік.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Енді бір кезеңде орындаудың бірнеше ағындары болуы мүмкін. Барлық транзакциялар қабылдау реті бойынша бірінен соң бірі өңделді. Нәтижесінде ең жоғары өнімділік секундына 18 мыңнан 50 мың транзакцияға дейін өсті.

Биржалық тәуекелдерді басқару жүйесі

Кемелділікке шек жоқ, көп ұзамай біз қайта жаңғыртуды бастадық: ASTS+ аясында біз тәуекелдерді басқару және есеп айырысу операциялары жүйелерін автономды құрамдас бөліктерге көшіре бастадық. Біз икемді заманауи архитектураны және жаңа иерархиялық тәуекел моделін әзірледік және сыныпты мүмкіндігінше пайдалануға тырыстық. fixed_point орнына double.

Бірақ бірден мәселе туындады: көптеген жылдар бойы жұмыс істеп келе жатқан барлық бизнес логикасын синхрондау және оны жаңа жүйеге көшіру қалай? Нәтижесінде жаңа жүйенің прототипінің бірінші нұсқасынан бас тартуға тура келді. Қазіргі уақытта өндірісте жұмыс істейтін екінші нұсқасы сауда және тәуекел бөліктерінде жұмыс істейтін бірдей кодқа негізделген. Әзірлеу кезінде ең қиын нәрсе екі нұсқа арасында git merge болды. Әріптесіміз Евгений Мазуренок апта сайын бұл операцияны жасап, әр жолы өте ұзақ қарғап жүретін.

Жаңа жүйені таңдаған кезде өзара әрекеттесу мәселесін бірден шешуге тура келді. Деректер шинасын таңдаған кезде тұрақты діріл мен ең аз кідірісті қамтамасыз ету қажет болды. Бұл үшін InfiniBand RDMA желісі ең қолайлы болды: орташа өңдеу уақыты 4 G Ethernet желілеріне қарағанда 10 есе аз. Бірақ бізді шынымен баурап алғаны - 99 және 99,9 пайыздық айырма болды.

Әрине, InfiniBand-тің өз қиындықтары бар. Біріншіден, басқа API - розеткалардың орнына ibverbs. Екіншіден, ашық бастапқы хабар алмасу шешімдері жоқтың қасы. Біз өзіміздің прототипімізді жасауға тырыстық, бірақ бұл өте қиын болды, сондықтан біз коммерциялық шешімді таңдадық - Confinity Low Latency Messaging (бұрынғы IBM MQ LLM).

Содан кейін тәуекел жүйесін дұрыс бөлу міндеті туындады. Тәуекел механизмін жай ғана алып тастасаңыз және аралық түйінді жасамасаңыз, онда екі көзден транзакцияларды араластыруға болады.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Ультра төмен кідіріс деп аталатын шешімдердің қайта тапсырыс беру режимі бар: екі көзден транзакцияларды алғаннан кейін қажетті тәртіпте реттеуге болады; бұл тапсырыс туралы ақпаратпен алмасу үшін бөлек арна арқылы жүзеге асырылады. Бірақ біз бұл режимді әлі қолданбаймыз: ол бүкіл процесті қиындатады және бірқатар шешімдерде оған мүлдем қолдау көрсетілмейді. Сонымен қатар, әрбір транзакцияға сәйкес уақыт белгілерін тағайындау керек еді, және біздің схемада бұл механизмді дұрыс енгізу өте қиын. Сондықтан біз классикалық схеманы хабарлама брокерімен, яғни тәуекелдік механизмі арасында хабарламаларды тарататын диспетчермен қолдандық.

Екінші мәселе клиенттік қатынасқа қатысты болды: егер бірнеше Тәуекел шлюзі болса, клиент олардың әрқайсысына қосылуы керек және бұл клиент деңгейіне өзгерістер енгізуді талап етеді. Біз осы кезеңде бұдан құтылғымыз келді, сондықтан қазіргі Тәуекел шлюзінің дизайны бүкіл деректер ағынын өңдейді. Бұл максималды өткізу қабілеттілігін айтарлықтай шектейді, бірақ жүйені біріктіруді айтарлықтай жеңілдетеді.

Қайталану

Біздің жүйеде бір сәтсіздік нүктесі болмауы керек, яғни барлық компоненттер, соның ішінде хабарлама брокері қайталануы керек. Біз бұл мәселені CLLM жүйесін пайдалана отырып шештік: оның құрамында екі диспетчер негізгі-бағдарламалық режимде жұмыс істей алатын RCMS кластері бар және біреуі істен шыққан кезде жүйе автоматты түрде екіншісіне ауысады.

Сақтық көшірме деректер орталығымен жұмыс істеу

InfiniBand жергілікті желі ретінде жұмыс істеу үшін оңтайландырылған, яғни тірекке орнатылатын жабдықты қосу үшін және InfiniBand желісін екі географиялық бөлінген деректер орталығының арасында салу мүмкін емес. Сондықтан біз хабарламаларды сақтауға кәдімгі Ethernet желілері арқылы қосылатын және барлық транзакцияларды екінші IB желісіне өткізетін көпір/диспетчерді іске асырдық. Деректер орталығынан көшу қажет болғанда, қазір қай деректер орталығымен жұмыс істеу керектігін таңдай аламыз.

Нәтижелері

Жоғарыда айтылғандардың барлығы бірден жасалған жоқ, жаңа архитектураны әзірлеу үшін бірнеше итерация қажет болды. Біз прототипті бір айда жасадық, бірақ оны жұмыс жағдайына келтіруге екі жылдан астам уақыт кетті. Біз транзакцияны өңдеу уақытын ұлғайту мен жүйе сенімділігін арттыру арасында ең жақсы ымыраға қол жеткізуге тырыстық.

Жүйе қатты жаңартылғандықтан, біз екі тәуелсіз көзден деректерді қалпына келтіруді жүзеге асырдық. Егер хабарламалар қоймасы қандай да бір себептермен дұрыс жұмыс істемесе, транзакциялар журналын екінші көзден - тәуекел механизмінен алуға болады. Бұл принцип бүкіл жүйеде сақталады.

Басқа нәрселермен қатар, біз брокерлер де, басқалар да жаңа архитектура үшін маңызды қайта өңдеуді қажет етпеуі үшін клиент API-ін сақтай алдық. Кейбір интерфейстерді өзгертуге тура келді, бірақ операциялық модельге елеулі өзгерістер енгізудің қажеті болмады.

Біз платформамыздың қазіргі нұсқасын Rebus деп атадық - бұл архитектурадағы ең көрнекті екі жаңалықтың аббревиатурасы ретінде, Risk Engine және BUS.

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Бастапқыда біз тек клиринг бөлігін ғана бөлгіміз келді, бірақ нәтиже үлкен бөлінген жүйе болды. Клиенттер енді Сауда шлюзімен, клиринг шлюзімен немесе екеуімен де әрекеттесе алады.

Ақырында қол жеткізген нәрсеміз:

Мәскеу биржасының сауда-клиринг жүйесінің архитектурасының эволюциясы. 2 бөлім

Кешіктіру деңгейі төмендеді. Транзакциялардың шағын көлемімен жүйе алдыңғы нұсқамен бірдей жұмыс істейді, бірақ сонымен бірге әлдеқайда жоғары жүктемеге төтеп бере алады.

Ең жоғары өнімділік секундына 50 мыңнан 180 мың транзакцияға дейін өсті. Одан әрі ұлғаюға тапсырыстарды сәйкестендірудің жалғыз ағыны кедергі жасайды.

Әрі қарай жетілдірудің екі жолы бар: сәйкестікті параллельдеу және оның шлюзмен жұмыс істеу тәсілін өзгерту. Енді барлық шлюздер репликация схемасына сәйкес жұмыс істейді, ол мұндай жүктеме кезінде қалыпты жұмысын тоқтатады.

Ақырында, мен кәсіпорын жүйелерін аяқтайтындарға бірнеше кеңес бере аламын:

  • Барлық уақытта ең жаманға дайын болыңыз. Мәселелер әрқашан күтпеген жерден пайда болады.
  • Әдетте архитектураны тез қайта жасау мүмкін емес. Әсіресе бірнеше көрсеткіштер бойынша максималды сенімділікке қол жеткізу қажет болса. Неғұрлым көп түйіндер болса, қолдау үшін соғұрлым көп ресурстар қажет.
  • Барлық реттелетін және меншікті шешімдер зерттеу, қолдау және техникалық қызмет көрсету үшін қосымша ресурстарды қажет етеді.
  • Жүйенің сенімділігі мен ақаулардан кейін қалпына келтіру мәселелерін шешуді кейінге қалдырмаңыз, оларды бастапқы жобалау кезеңінде ескеріңіз.

Ақпарат көзі: www.habr.com

пікір қалдыру