portaldacalheta.pt
  • Основен
  • Подвижен
  • Дизайн На Марката
  • Възходът На Дистанционното
  • Жизнен Цикъл На Продукта
Back-End

Изпълнение на I / O от страна на сървъра: Node срещу PHP срещу Java срещу Go



Разбирането на модела за вход / изход (I / O) на вашето приложение може да означава разликата между приложение, което се справя с натоварването, на което е подложено, и такова, което се мачка пред реалните случаи на употреба. Може би, докато приложението ви е малко и не обслужва големи натоварвания, може да има значение далеч по-малко. Но тъй като натоварването на трафика на приложението ви се увеличава, работата с грешен I / O модел може да ви отведе в свят на нараняване.

И както повечето ситуации, при които са възможни множество подходи, не е въпрос само кой е по-добър, а въпрос на разбиране на компромисите. Нека да се разходим през I / O пейзажа и да видим какво можем да шпионираме.



В тази статия ще сравним Node, Java, Go и PHP с Apache, ще обсъдим как различните езици моделират своите I / O, предимствата и недостатъците на всеки модел и ще завършим с някои основни критерии. Ако сте загрижени за I / O производителността на следващото си уеб приложение, тази статия е за вас.



Основи на I / O: Бързо опресняване

За да разберем факторите, свързани с I / O, първо трябва да прегледаме концепциите на ниво операционна система. Въпреки че е малко вероятно да се наложи да се справят директно с много от тези понятия, вие се справяте с тях индиректно през средата на изпълнение на приложението си през цялото време. И подробностите имат значение.



Системни разговори

Първо, имаме системни обаждания, които могат да бъдат описани по следния начин:

  • Вашата програма (в „потребителска земя“, както се казва) трябва да поиска ядрото на операционната система да извърши I / O операция от нейно име.
  • „Syscall“ е начинът, по който вашата програма изисква ядрото да направи нещо. Спецификите на това как се изпълнява варират в различните операционни системи, но основната концепция е една и съща. Ще има някаква конкретна инструкция, която прехвърля контрола от вашата програма върху ядрото (като извикване на функция, но с някакъв специален сос, специално за справяне с тази ситуация). Най-общо казано, syscalls блокират, което означава, че вашата програма чака ядрото да се върне обратно към вашия код.
  • Ядрото извършва основната I / O операция на въпросното физическо устройство (диск, мрежова карта и т.н.) и отговаря на syscall. В реалния свят ядрото може да се наложи да направи редица неща, за да изпълни заявката ви, включително да изчака устройството да бъде готово, да актуализира вътрешното му състояние и т.н., но като разработчик на приложения не ви интересува това. Това е работата на ядрото.

Диаграма на Syscalls



Блокиране срещу неблокиращи обаждания

Току-що казах по-горе, че syscalls блокират и това е вярно в общ смисъл. Някои повиквания обаче са категоризирани като „неблокиращи“, което означава, че ядрото взема заявката ви, поставя я на опашка или буфер някъде и след това незабавно се връща, без да чака действителното въвеждане / изход. Така че „блокира“ само за много кратък период от време, достатъчно дълъг, за да постави заявката ви в опашка.

Някои примери (за системни повиквания на Linux) могат да помогнат за изясняване: - read() е блокиращо повикване - предавате му манипулатор, който казва кой файл и буфер къде да достави данните, които чете, и обаждането се връща, когато данните са там. Имайте предвид, че това има предимството да бъде приятно и просто. - epoll_create(), epoll_ctl() и epoll_wait() са повиквания, които, съответно, ви позволяват да създадете група манипулатори, които да слушате, да добавяте / премахвате манипулатори от тази група и след това да блокирате, докато има някаква активност. Това ви позволява ефективно да контролирате голям брой I / O операции с една нишка, но аз изпреварвам себе си. Това е чудесно, ако имате нужда от функционалността, но както виждате, със сигурност е по-сложна за използване.



Тук е важно да разберете реда на разликата във времето. Ако ядрото на процесора работи на 3GHz, без да навлиза в оптимизации, които CPU може да направи, то извършва 3 милиарда цикъла в секунда (или 3 цикъла в наносекунда). Неблокиращо системно обаждане може да отнеме 10-та цикъла, за да завърши - или „относително няколко наносекунди“. Обаждане, което блокира получаването на информация по мрежата, може да отнеме много повече време - да кажем например 200 милисекунди (1/5 от секундата). Да кажем, например, че блокиращото повикване отне 20 наносекунди, а блокиращото повикване отне 200 000 000 наносекунди. Вашият процес просто изчака 10 милиона пъти повече за блокиращото обаждане.

Блокиране срещу неблокиращи Syscalls



Ядрото предоставя средства за блокиране на I / O („прочетете от тази мрежова връзка и ми дайте данни“) и неблокиращи I / O („кажете ми, когато някоя от тези мрежови връзки има нови данни“). И кой механизъм се използва, ще блокира процеса на извикване за драматично различни периоди от време.

Планиране

Третото нещо, което е критично важно да се следва, е какво се случва, когато имате много нишки или процеси, които започват да блокират.



За нашите цели няма голяма разлика между нишка и процес. В реалния живот най-забележимата разлика, свързана с производителността, е, че тъй като нишките споделят една и съща памет и всеки процес има свое собствено пространство в паметта, което прави отделните процеси склонни да заемат много повече памет. Но когато говорим за планиране, това, което всъщност се свежда, е списък с неща (нишки и процеси), за които всеки трябва да получи част от времето за изпълнение на наличните ядра на процесора. Ако имате 300 изпълнени нишки и 8 ядра, за да ги стартирате, трябва да разделите времето, така че всяка да получи своя дял, като всяко ядро ​​се изпълнява за кратък период от време и след това преминава към следващата нишка. Това се прави чрез „контекстен превключвател“, като превключва процесора от превключване на една нишка / процес към следващата.

Тези превключватели на контекст имат свързана с тях цена - отнема известно време. В някои бързи случаи може да е по-малко от 100 наносекунди, но не е необичайно да отнеме 1000 наносекунди или повече в зависимост от подробностите за изпълнението, скоростта / архитектурата на процесора, кеша на процесора и т.н.



И колкото повече нишки (или процеси), толкова повече превключване на контекста. Когато говорим за хиляди нишки и стотици наносекунди за всяка, нещата могат да станат много бавни.

Обаче неблокиращите обаждания по същество казват на ядрото „обадете ми се само когато имате някакви нови данни или събитие на някоя от тези връзки.“ Тези неблокиращи повиквания са проектирани да се справят ефективно с големи I / O натоварвания и да намалят превключването на контекста.

С мен досега? Защото сега идва забавната част: Нека да разгледаме какво правят някои популярни езици с тези инструменти и да направим някои заключения относно компромисите между лекотата на използване и производителността ... и други интересни парчета.

Като бележка, докато примерите, показани в тази статия, са тривиални (и частични, като са показани само съответните битове); достъп до база данни, външни кеширащи системи (memcache и др. всички) и всичко, което изисква I / O, в крайна сметка ще извърши някакъв вид I / O повикване под капака, което ще има същия ефект като показаните прости примери. Също така, за сценариите, при които I / O е описан като „блокиращ“ (PHP, Java), HTTP заявката и отговорите четат и записват самите те блокират повикванията: Отново, повече I / O скрити в системата с придружаващите я проблеми с производителността да се вземат предвид.

Има много фактори, които влизат в избора на език за програмиране за даден проект. Има дори много фактори, когато имате предвид само представянето. Но ако се притеснявате, че програмата ви ще бъде ограничена предимно от I / O, ако I / O производителността е марка или пробив за вашия проект, това са неща, които трябва да знаете.

Подходът „Дръжте го просто“: PHP

Още през 90-те много хора бяха облечени Конверс обувки и писане на CGI скриптове на Perl. След това се появи PHP и колкото и някои хора да обичат да го дразнят, това улесни динамичните уеб страници.

как да вляза в python

Моделът, който PHP използва е доста прост. Има някои вариации, но средният ви PHP сървър изглежда така:

HTTP заявка идва от браузъра на потребителя и удря вашия уеб сървър на Apache. Apache създава отделен процес за всяка заявка, с някои оптимизации, за да ги използва повторно, за да сведе до минимум колко трябва да направи (създаването на процеси е, относително казано, бавно). Apache извиква PHP и му казва да стартира подходящия .php файл на диска. PHP кодът изпълнява и блокира I / O повикванията. Обаждате се file_get_contents() в PHP и под капака прави read() syscalls и чака резултатите.

И разбира се действителният код е просто вграден директно във вашата страница и операциите блокират:

query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>

По отношение на това как това се интегрира със системата, това е следното:

I / O модел PHP

Доста просто: един процес на заявка. Входно-изходните повиквания просто блокират Предимство? Това е просто и работи. Недостатък? Ударете го с 20 000 клиенти едновременно и вашият сървър ще избухне в пламъци. Този подход не се мащабира добре, тъй като инструментите, предоставени от ядрото за справяне с I / O с голям обем (epoll и др.), Не се използват. И за да добавите обида към нараняване, стартирането на отделен процес за всяка заявка обикновено използва много системни ресурси, особено паметта, което често е първото нещо, което изчерпвате при сценарий като този.

Забележка: Подходът, използван за Ruby, е много подобен на този на PHP и в широк, общ, вълнообразен начин те могат да се считат за еднакви за нашите цели.

Многонишков подход: Java

Така че Java се появява, точно по времето, когато сте закупили първото си име на домейн, и беше страхотно просто на случаен принцип да изречете „dot com“ след изречение. А Java има вграден в езика многопоточност, което (особено когато е създадено) е доста страхотно.

Повечето уеб сървъри на Java работят чрез стартиране на нова нишка на изпълнение за всяка заявка, която постъпва и след това в тази нишка в крайна сметка извиква функцията, която вие като разработчик на приложение сте написали.

Извършването на I / O в Java Servlet има тенденция да изглежда по следния начин:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream('/path/to/file'); // blocking network I/O URLConnection urlConnection = (new URL('http://example.com/example-microservice')).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println('...'); }

Тъй като нашите doGet Методът по-горе съответства на една заявка и се изпълнява в собствена нишка, вместо отделен процес за всяка заявка, който изисква собствена памет, имаме отделна нишка. Това има някои приятни предимства, като възможността да споделят състояние, кеширани данни и т.н. между нишките, защото те могат да имат достъп до паметта на другия, но въздействието върху това как взаимодейства с графика все още е почти идентично с това, което се прави в PHP пример по-рано. Всяка заявка получава нова нишка и различните блокове I / O операции вътре в нея, докато заявката бъде напълно обработена. Нишките се обединяват, за да минимизират разходите за създаването и унищожаването им, но въпреки това хиляди връзки означават хиляди нишки, което е лошо за планиращия.

Важен етап е, че във версия 1.4 Java (и значително надграждане отново в 1.7) получи възможността да извършва неблокиращи I / O повиквания. Повечето приложения, в мрежата и по друг начин, не го използват, но поне е налице. Някои уеб сървъри на Java се опитват да се възползват от това по различни начини; обаче по-голямата част от внедрените Java приложения все още работят, както е описано по-горе.

I / O модел Java

Java ни сближава и със сигурност има добра добра функционалност за I / O, но все още не решава проблема какво се случва, когато имате силно обвързано I / O приложение, което се забива в земята с много хиляди блокиращи нишки.

Неблокиращ I / O като гражданин от първи клас: Възел

Популярното дете в блока, когато става въпрос за по-добри I / O е Node.js. На всеки, който е имал дори най-краткото въведение в Node, е казано, че той „не блокира“ и че се справя ефективно с I / O. И това е вярно в общ смисъл. Но дяволът е в детайлите и средствата, с които е постигнато това магьосничество, имат значение, когато става въпрос за изпълнение.

По същество промяната на парадигмата, която Node изпълнява, е, че вместо по същество да казва „напишете кода си тук, за да обработвате заявката“, те вместо това казват „напишете код тук, за да започнете да обработвате заявката“. Всеки път, когато трябва да направите нещо, което включва I / O, вие правите заявката и давате функция за обратно извикване, която Node ще извика, когато приключи.

Типичният код на възел за извършване на I / O операция в заявка върви по следния начин:

http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });

Както можете да видите, тук има две функции за обратно извикване. Първият се извиква при стартиране на заявка, а вторият се извиква, когато данните за файла са налични.

Това, което прави по същество, дава възможност на Node да обработва ефективно I / O между тези обратни обаждания. Сценарий, при който би било още по-подходящо, е когато правите извикване на база данни в Node, но няма да се занимавам с примера, защото това е абсолютно същият принцип: стартирате повикването на базата данни и давате на Node функция за обратно извикване, изпълнява I / O операциите поотделно, като използва неблокиращи повиквания и след това извиква вашата функция за обратно извикване, когато данните, които сте поискали, са налични. Този механизъм за поставяне на опашки за I / O повиквания и оставяне на Node да се справи и след това получаване на обратно извикване се нарича „Loop Event“. И работи доста добре.

I / O Model Node.js

Този модел обаче има уловка. Под капака причината за това има много повече общо с това как е внедрен V8 JavaScript двигателят (JS двигателят на Chrome, който се използва от Node) един от всичко друго. JS кодът, който пишете, се изпълнява в една нишка. Помислете за това за момент. Това означава, че докато I / O се извършва с помощта на ефективни неблокиращи техники, вашият JS може, който прави операции, свързани с процесора, се изпълнява в една нишка, като всяка част от кода блокира следващата. Често срещан пример за това къде може да се появи е прелистване на записите в базата данни, за да ги обработи по някакъв начин, преди да ги изведе на клиента. Ето пример, който показва как работи това:

var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i

Докато Node се справя ефективно с I / O, това for цикъл в примера по-горе използва цикли на процесора във вашата единствена и единствена основна нишка. Това означава, че ако имате 10 000 връзки, този цикъл може да доведе до обхождане на цялото ви приложение в зависимост от това колко време отнема. Всяка заявка трябва да споделя част от времето, един по един, във вашата основна нишка.

Предпоставката, на която се основава цялата концепция, е, че операциите за входно / изходни операции са най-бавната част, поради което е най-важно да се справим ефективно с тях, дори ако това означава извършване на друга обработка последователно. Това е вярно в някои случаи, но не във всички.

twitter набор от данни за копаене на данни

Другата точка е, че макар това да е само мнение, може да е доста уморително да пишете куп вложени обратни извиквания и някои твърдят, че прави кода значително по-труден за следване. Не е необичайно да видите обратни обаждания, вложени четири, пет или дори повече нива дълбоко в кода на Node.

Върнахме се отново към компромисите. Моделът Node работи добре, ако основният ви проблем с производителността е I / O. Неговата ахилесова пета обаче е, че можете да влезете във функция, която обработва HTTP заявка и да вкарате интензивен CPU код и да свържете всяка връзка с обхождане, ако не сте внимателни.

Естествено не блокира: Върви

Преди да вляза в раздела за Go, е подходящо да разкрия, че съм фен на Go. Използвал съм го за много проекти и открито съм привърженик на неговите предимства за производителност и ги виждам в работата си, когато го използвам.

Въпреки това, нека да разгледаме как се справя с I / O. Една ключова характеристика на езика Go е, че той съдържа свой собствен график. Вместо всяка нишка на изпълнение, съответстваща на отделна нишка на ОС, тя работи с концепцията за „goroutines“. И средата за изпълнение Go може да присвои goroutine на нишка на OS и да я накара да изпълни или да я спре и да не бъде свързана с нишка на OS въз основа на това, което прави тази goroutine. Всяка заявка, която идва от HTTP сървъра на Go, се обработва в отделна Goroutine.

Диаграмата на това как работи планировщикът изглежда така:

I / O Model Go

Под капака това се реализира от различни точки в изпълнението на Go, които изпълняват I / O повикването, като отправят заявката за писане / четене / свързване / и т.н., поставяне на текущата програма в режим на заспиване, с информация за събуждане на програмата обратно нагоре, когато могат да бъдат предприети по-нататъшни действия.

Всъщност изпълнението на Go прави нещо, което не е много различно от това, което Node прави, с изключение на това, че механизмът за обратно извикване е вграден в изпълнението на I / O повикването и взаимодейства автоматично с планиращия механизъм. Той също така не страда от ограничението да се налага целият ви код на манипулатора да се изпълнява в една и съща нишка, Go автоматично ще преобразува вашите Goroutines към толкова нишки на операционната система, които смята за подходящи въз основа на логиката в неговия планировчик. Резултатът е код като този:

func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query('SELECT ...') for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }

Както можете да видите по-горе, основната кодова структура на това, което правим, прилича на тази на по-опростените подходи и въпреки това постига неблокиращи I / O под капака.

В повечето случаи това се оказва „най-доброто от двата свята“. Неблокиращият вход / изход се използва за всички важни неща, но вашият код изглежда блокира и по този начин е по-лесен за разбиране и поддържане. С останалото се справя взаимодействието между планиращия механизъм Go и планировчика на операционната система. Това не е пълна магия и ако изградите голяма система, струва си да отделите време, за да разберете повече подробности за това как работи; но в същото време средата, която получавате „извън кутията“, работи и се мащабира доста добре.

Go може да има своите грешки, но най-общо казано, начинът, по който се справя с I / O, не е сред тях.

Лъжи, проклети лъжи и еталони

Трудно е да се дадат точни времена за превключване на контекста, свързани с тези различни модели. Бих могъл също да твърдя, че това е по-малко полезно за вас. Затова вместо това ще ви дам някои основни показатели, които сравняват общата производителност на HTTP сървъра на тези сървърни среди. Имайте предвид, че много фактори участват в изпълнението на целия HTTP заявка / отговор на HTTP от край до край, а представените тук номера са само някои мостри, които събрах, за да дам основно сравнение.

За всяка от тези среди написах подходящия код за четене във 64k файл с произволни байтове, пуснах SHA-256 хеш върху него N броя пъти (N е посочен в низа на заявката на URL, например .../test.php?n=100 ) и отпечатайте получения хеш в шестнадесетичен. Избрах това, защото това е много прост начин за стартиране на едни и същи бенчмаркове с някои последователни I / O и контролиран начин за увеличаване на използването на процесора.

Вижте тези бележки за сравнение за малко повече подробности относно използваната среда.

Първо, нека разгледаме някои примери с ниска съвпадение. Изпълнението на 2000 итерации с 300 едновременни заявки и само един хеш на заявка (N = 1) ни дава това:

Среден брой милисекунди за изпълнение на заявка за всички едновременни заявки, N = 1

Времената са средният брой милисекунди за изпълнение на заявка за всички едновременни заявки. Долната е по-добра.

Трудно е да се направи заключение само от тази графика, но това ми се струва, че при този обем на свързване и изчисление виждаме времена, които имат повече общо с общото изпълнение на самите езици, още повече, че I / O. Имайте предвид, че езиците, които се считат за „скриптови езици“ (свободно писане, динамично тълкуване) се представят най-бавно.

Но какво се случва, ако увеличим N до 1000, все още с 300 едновременни заявки - същото натоварване, но 100 пъти повече хеш итерации (значително повече натоварване на процесора):

Среден брой милисекунди за изпълнение на заявка за всички едновременни заявки, N = 1000

Времената са средният брой милисекунди за изпълнение на заявка за всички едновременни заявки. Долната е по-добра.

Изведнъж производителността на Node спада значително, тъй като интензивните процесори във всяка заявка се блокират взаимно. И достатъчно интересно, производителността на PHP става много по-добра (спрямо останалите) и побеждава Java в този тест. (Струва си да се отбележи, че в PHP изпълнението на SHA-256 е написано на C и пътят за изпълнение прекарва много повече време в този цикъл, тъй като сега правим 1000 хеш итерации).

Сега нека опитаме 5000 едновременни връзки (с N = 1) - или колкото се може по-близо до това. За съжаление, за повечето от тези среди процентът на отказите не беше незначителен. За тази диаграма ще разгледаме общия брой заявки в секунда. Колкото по-високо, толкова по-добре :

Общ брой заявки в секунда, N = 1, 5000 req / sec

Общ брой заявки в секунда. По-високо е по-добре.

И картината изглежда съвсем различно. Това е предположение, но изглежда, че при висок обем на връзката режийните разходи за връзка, свързани с появата на нови процеси и допълнителната памет, свързана с нея в PHP + Apache, изглежда се превръщат в доминиращ фактор и увеличава производителността на PHP. Ясно е, че Go е победителят тук, следван от Java, Node и накрая PHP.

Въпреки че факторите, свързани с цялостната ви производителност, са много и също варират в широки граници от приложение до приложение, колкото повече разбирате за вътрешността на това, което се случва под капака и свързаните компромиси, толкова по-добре ще бъдете.

В обобщение

С всичко изброено по-горе е съвсем ясно, че тъй като езиците са се развили, решенията за работа с мащабни приложения, които правят много I / O, са се развили заедно с него.

За да бъдем честни, както PHP, така и Java, въпреки описанията в тази статия, имат изпълнения на неблокиращ I / O на разположение за използване в уеб приложения . Но те не са толкова често срещани, колкото описаните по-горе подходи и трябва да се вземат предвид съпътстващите оперативни разходи за поддържане на сървъри, използващи такива подходи. Да не говорим, че вашият код трябва да бъде структуриран по начин, който работи с такива среди; вашето „нормално“ PHP или Java уеб приложение обикновено няма да работи без значителни модификации в такава среда.

За сравнение, ако разгледаме няколко важни фактора, които влияят върху производителността, както и лекотата на използване, получаваме следното:

Език Конци срещу процеси Неблокиращ I / O Лесно използване
PHP Процеси Не
Java Конци На разположение Изисква обратно извикване
Node.js Конци Да Изисква обратно извикване
Отивам Конци (Goroutines) Да Не са необходими обратни обаждания


причини за финансовата криза в Гърция

Нишките обикновено ще бъдат много по-ефективни от паметта, отколкото процесите, тъй като те споделят едно и също пространство в паметта, докато процесите не. Комбинирайки това с факторите, свързани с неблокиращия вход / изход, можем да видим, че поне с факторите, разгледани по-горе, докато се придвижваме надолу по списъка, общата настройка, тъй като тя се отнася до входно-изходните операции, се подобрява. Така че, ако трябваше да избера победител в горния конкурс, със сигурност щеше да е Go.

Въпреки това на практика изборът на среда, в която да изградите приложението си, е тясно свързан с познанията на вашия екип със споменатата среда и общата производителност, която можете да постигнете с нея. Така че може да няма смисъл всеки екип просто да се потопи и да започне да разработва уеб приложения и услуги в Node или Go. Всъщност намирането на разработчици или познаването на вашия вътрешен екип често се посочва като основната причина да не използвате различен език и / или среда. Въпреки това времената се промениха през последните петнадесет години или много, много.

Надяваме се, че горното помага да се изготви по-ясна картина на случващото се под капака и ви дава някои идеи как да се справите с реалната мащабируемост за вашето приложение. Честито въвеждане и извеждане!

Промяна за добро или лошо? Ръководство за UX иновации

Ux Дизайн

Промяна за добро или лошо? Ръководство за UX иновации
Изчерпателното ръководство за информационна архитектура

Изчерпателното ръководство за информационна архитектура

Ux Дизайн

Популярни Публикации
Създавайте данни от случаен шум с генерални състезателни мрежи
Създавайте данни от случаен шум с генерални състезателни мрежи
Миналото все още присъства - преглед на вечния дизайн
Миналото все още присъства - преглед на вечния дизайн
Финансово бедствие в криза: Не можете да предскажете, можете да подготвите
Финансово бедствие в криза: Не можете да предскажете, можете да подготвите
Бруталистки уеб дизайн, минималистичен уеб дизайн и бъдещето на Web UX
Бруталистки уеб дизайн, минималистичен уеб дизайн и бъдещето на Web UX
Разширени съвети и хакове за презентация на PowerPoint
Разширени съвети и хакове за презентация на PowerPoint
 
Архитект отпред
Архитект отпред
Студената технологична война: все още тук и все още се използва
Студената технологична война: все още тук и все още се използва
Въведение в Apache Spark с примери и случаи на употреба
Въведение в Apache Spark с примери и случаи на употреба
Комодитизирани смартфони: Привеждане на 4G в развиващите се страни
Комодитизирани смартфони: Привеждане на 4G в развиващите се страни
Как да създам API за Secure Node.js GraphQL
Как да създам API за Secure Node.js GraphQL
Популярни Публикации
  • как да направите сървър с raspberry pi
  • какво да слушате със sdr
  • какво е правилото зад модела на петте сили на Портър?
  • разлика между s и c corp
  • какви са инструментите на командния ред
Категории
  • Подвижен
  • Дизайн На Марката
  • Възходът На Дистанционното
  • Жизнен Цикъл На Продукта
  • © 2022 | Всички Права Запазени

    portaldacalheta.pt