Artikel ini mula-mula memperkenalkan secara ringkas konsep asas yang berkaitan dengan I/O, kemudian membandingkan prestasi I/O Node, PHP, Java dan Go secara mendatar dan memberikan cadangan pemilihan. Mari perkenalkan di bawah, rakan-rakan yang memerlukan boleh merujuknya.
Memahami model input/output (I/O) aplikasi boleh memahami dengan lebih baik cara ia mengendalikan beban secara ideal dan dalam amalan. Mungkin permohonan anda kecil dan tidak perlu menyokong beban yang tinggi, jadi kurang untuk dipertimbangkan. Walau bagaimanapun, apabila beban trafik aplikasi meningkat, menggunakan model I/O yang salah boleh membawa akibat yang sangat serius.
Dalam artikel ini, kami akan membandingkan Node, Java, Go dan PHP dengan Apache, membincangkan cara bahasa berbeza model I/O, kelebihan dan kekurangan setiap model, dan beberapa Kajian prestasi asas. Jika anda lebih mengambil berat tentang prestasi I/O aplikasi web anda yang seterusnya, artikel ini akan membantu anda.
Untuk memahami faktor yang berkaitan dengan I/O, kita mesti memahami konsep ini pada peringkat sistem pengendalian terlebih dahulu. Walaupun tidak mungkin anda akan terdedah kepada terlalu banyak konsep secara langsung pada permulaan, anda akan sentiasa menghadapinya semasa pengendalian aplikasi, sama ada secara langsung atau tidak langsung. Butiran penting.
Pertama, mari kita kenali panggilan sistem Penerangan khusus adalah seperti berikut:
Aplikasi meminta kernel sistem pengendalian. untuk melaksanakan operasi I/O untuknya.
"panggilan sistem" ialah apabila atur cara meminta kernel untuk melaksanakan beberapa operasi. Butiran pelaksanaan berbeza antara sistem pengendalian, tetapi konsep asasnya adalah sama. Apabila "panggilan sistem" dilaksanakan, beberapa arahan khusus untuk mengawal atur cara akan dipindahkan ke kernel. Secara umumnya, panggilan sistem disekat, yang bermaksud bahawa program menunggu sehingga kernel mengembalikan hasilnya.
Inti menjalankan operasi I/O peringkat rendah pada peranti fizikal (cakera, kad rangkaian, dll.) dan membalas panggilan sistem. Di dunia nyata, kernel mungkin perlu melakukan banyak perkara untuk memenuhi permintaan anda, termasuk menunggu peranti bersedia, mengemas kini keadaan dalamannya, dsb., tetapi sebagai pembangun aplikasi, anda tidak perlu mengambil berat tentang itu, ia adalah perniagaan inti.
Saya katakan di atas bahawa panggilan sistem biasanya disekat. Walau bagaimanapun, sesetengah panggilan adalah "tidak menyekat", yang bermaksud bahawa kernel meletakkan permintaan dalam baris gilir atau penimbal dan kembali serta-merta tanpa menunggu I/O sebenar berlaku. Jadi, ia hanya "menghalang" untuk masa yang singkat, tetapi baris gilir mengambil masa tertentu.
Untuk menggambarkan perkara ini, berikut ialah beberapa contoh (panggilan sistem Linux):
read() ialah panggilan menyekat. Kita perlu menyerahkan pemegang fail dan penimbal untuk menyimpan data, dan kembali apabila data disimpan ke penimbal. Ia mempunyai kelebihan yang elegan tetapi ringkas.
epoll_create(), epoll_ctl() dan epoll_wait() boleh digunakan untuk mencipta kumpulan pemegang untuk didengari, menambah/mengalih keluar pemegang dalam kumpulan ini dan menyekat program sehingga di sana adalah sebarang aktiviti pada pemegang. Panggilan sistem ini membolehkan anda mengawal sebilangan besar operasi I/O dengan cekap menggunakan hanya satu utas. Ciri-ciri ini, walaupun sangat berguna, agak rumit untuk digunakan.
Adalah penting untuk memahami susunan magnitud perbezaan masa di sini. Jika teras CPU yang tidak dioptimumkan berjalan pada 3GHz, ia boleh melaksanakan 3 bilion kitaran sesaat (iaitu 3 kitaran setiap nanosaat). Panggilan sistem tidak menyekat mungkin mengambil masa lebih daripada 10 kitaran, atau beberapa nanosaat. Menyekat panggilan untuk menerima maklumat daripada rangkaian mungkin mengambil masa yang lebih lama, katakan 200 milisaat (1/5 saat).
Katakan panggilan tidak menyekat mengambil masa 20 nanosaat dan panggilan menyekat mengambil masa 200,000,000 nanosaat. Dengan cara ini, proses mungkin perlu menunggu 10 juta kitaran untuk menyekat panggilan.
Inti menyediakan dua kaedah: menyekat I/O ("baca data daripada rangkaian") dan I/O tidak menyekat ("beritahu saya apabila terdapat data baharu pada sambungan rangkaian"), dan kedua-duanya Tempoh masa mekanisme menyekat proses panggilan adalah berbeza sama sekali.
Perkara ketiga yang sangat kritikal ialah apa yang berlaku apabila terdapat banyak rangkaian atau proses yang mula disekat.
Bagi kami, tidak banyak perbezaan antara benang dan proses. Pada hakikatnya, perbezaan paling ketara yang berkaitan dengan prestasi ialah memandangkan benang berkongsi memori yang sama dan setiap proses mempunyai ruang ingatan sendiri, satu proses cenderung untuk menduduki lebih banyak memori. Walau bagaimanapun, apabila kita bercakap tentang penjadualan, kita sebenarnya bercakap tentang menyelesaikan satu siri perkara, dan setiap perkara memerlukan jumlah masa pelaksanaan tertentu pada teras CPU yang tersedia.
Jika anda mempunyai 8 teras untuk menjalankan 300 utas, maka anda perlu memotong masa supaya setiap utas mendapat potongan masanya dan setiap teras berjalan untuk masa yang singkat dan kemudian beralih ke urutan seterusnya. Ini dilakukan melalui "suis konteks", yang membolehkan CPU bertukar dari satu utas/proses ke seterusnya.
Penukaran konteks jenis ini mempunyai kos tertentu, iaitu, ia mengambil masa tertentu. Ia mungkin kurang daripada 100 nanosaat apabila ia pantas, tetapi jika butiran pelaksanaan, kelajuan/seni bina pemproses, cache CPU dan perisian serta perkakasan lain adalah berbeza, adalah perkara biasa untuk mengambil masa 1000 nanosaat atau lebih lama.
Semakin banyak bilangan utas (atau proses), semakin banyak bilangan suis konteks. Jika terdapat beribu-ribu benang, dan setiap benang mengambil ratusan nanosaat untuk bertukar, sistem akan menjadi sangat perlahan.
Walau bagaimanapun, panggilan tidak menyekat pada dasarnya memberitahu kernel "hanya hubungi saya apabila data atau peristiwa baharu tiba pada sambungan ini". Panggilan tidak menyekat ini mengendalikan beban I/O yang besar dengan cekap dan mengurangkan suis konteks.
Perlu diambil perhatian bahawa walaupun contoh dalam artikel ini adalah kecil, akses pangkalan data, sistem caching luaran (memcache dan seumpamanya), dan apa sahaja yang memerlukan I/O akhirnya akan melaksanakan beberapa jenis Panggilan I/O , ini adalah prinsip yang sama seperti contoh.
Terdapat banyak faktor yang mempengaruhi pemilihan bahasa pengaturcaraan dalam projek Walaupun anda hanya mempertimbangkan prestasi, terdapat banyak faktor. Walau bagaimanapun, jika anda bimbang bahawa program anda dihadkan terutamanya oleh I/O, dan prestasi merupakan faktor penting dalam menentukan kejayaan atau kegagalan projek, maka cadangan berikut adalah perkara yang perlu anda pertimbangkan.
Pada tahun 90-an, terdapat ramai orang yang memakai kasut Converse menulis skrip CGI dalam Perl. Kemudian, PHP datang dan ramai orang menyukainya dan ia memudahkan untuk membuat halaman web dinamik.
Model yang digunakan oleh PHP adalah sangat mudah. Walaupun adalah mustahil untuk menjadi betul-betul sama, prinsip pelayan PHP umum adalah seperti berikut:
Pelayar pengguna mengeluarkan permintaan HTTP, dan permintaan itu memasuki pelayan web Apache. Apache mencipta proses berasingan untuk setiap permintaan dan menggunakan semula proses ini melalui beberapa kaedah pengoptimuman untuk meminimumkan operasi yang perlu dilakukan (proses mencipta adalah agak perlahan).
Apache memanggil PHP dan memberitahunya untuk menjalankan fail .php tertentu pada cakera.
Kod PHP mula melaksanakan dan menyekat panggilan I/O. File_get_contents() yang anda panggil dalam PHP sebenarnya memanggil panggilan sistem read() dan menunggu hasil yang dikembalikan.
<?php// blocking file I/O$file_data = file_get_contents(‘/path/to/file.dat’); // blocking network I/O$curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>
Mudah sahaja: satu proses setiap permintaan. Panggilan I/O disekat. Bagaimana pula dengan kelebihannya? Mudah tetapi berkesan. Bagaimana pula dengan keburukan? Jika terdapat 20,000 pelanggan serentak, pelayan akan lumpuh. Pendekatan ini sukar untuk skala kerana alat yang disediakan oleh kernel untuk mengendalikan sejumlah besar I/O (epoll, dsb.) tidak digunakan sepenuhnya. Lebih buruk lagi, menjalankan proses berasingan untuk setiap permintaan cenderung untuk mengambil banyak sumber sistem, terutamanya memori, yang selalunya yang pertama habis.
*Nota: Pada ketika ini, situasi Ruby sangat serupa dengan PHP.
Jadi, Java muncul. Dan Java mempunyai pelbagai benang terbina dalam bahasa, yang sangat bagus terutamanya apabila ia datang untuk mencipta benang.
Kebanyakan pelayan web Java akan memulakan urutan pelaksanaan baharu untuk setiap permintaan dan kemudian memanggil fungsi bertulis pembangun dalam urutan ini.
Melaksanakan I/O dalam Java Servlet selalunya kelihatan seperti ini:
publicvoiddoGet(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("..."); }
Memandangkan kaedah doGet di atas sepadan dengan permintaan dan berjalan dalam urutannya sendiri, bukannya apabila diperlukan Ia berjalan dalam proses berasingan dengan ingatan bebas, jadi kami akan mencipta benang berasingan. Setiap permintaan mendapat urutan baharu, dan pelbagai operasi I/O disekat di dalam utas tersebut sehingga permintaan diproses. Aplikasi ini akan mencipta kumpulan benang untuk meminimumkan kos mencipta dan memusnahkan benang, tetapi beribu-ribu sambungan bermakna beribu-ribu benang, yang bukan perkara yang baik untuk penjadual.
Perlu diingat bahawa Java versi 1.4 (dinaik taraf semula dalam versi 1.7) menambah keupayaan untuk tidak menyekat panggilan I/O. Walaupun kebanyakan aplikasi tidak menggunakan ciri ini, ia sekurang-kurangnya tersedia. Sesetengah pelayan web Java sedang bereksperimen dengan ciri ini, tetapi sebahagian besar aplikasi Java yang digunakan masih berfungsi mengikut prinsip yang diterangkan di atas.
Java menyediakan banyak ciri luar kotak untuk I/O, tetapi jika anda menghadapi situasi mencipta sebilangan besar benang penyekat untuk melaksanakan sejumlah besar operasi I/O, Java tidak mempunyai penyelesaian yang baik.
Yang berprestasi lebih baik dalam I/O dan lebih popular dalam kalangan pengguna ialah Node.js. Sesiapa sahaja yang mempunyai pemahaman asas tentang Node tahu bahawa ia adalah "tidak menyekat" dan mengendalikan I/O dengan cekap. Ini benar dalam pengertian umum. Tetapi butiran dan cara ia dilaksanakan adalah penting.
Apabila anda perlu melakukan beberapa operasi yang melibatkan I/O, anda perlu membuat permintaan dan memberikan fungsi panggil balik Node akan memanggil fungsi ini selepas memproses permintaan.
Kod biasa untuk melaksanakan operasi I/O dalam permintaan adalah seperti berikut:
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
Seperti yang ditunjukkan di atas, terdapat dua fungsi panggil balik. Fungsi pertama dipanggil apabila permintaan bermula, dan fungsi kedua dipanggil apabila data fail tersedia.
这样,Node就能更有效地处理这些回调函数的I/O。有一个更能说明问题的例子:在Node中调用数据库操作。首先,你的程序开始调用数据库操作,并给Node一个回调函数,Node会使用非阻塞调用来单独执行I/O操作,然后在请求的数据可用时调用你的回调函数。这种对I/O调用进行排队并让Node处理I/O调用然后得到一个回调的机制称为“事件循环”。这个机制非常不错。
然而,这个模型有一个问题。在底层,这个问题出现的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的实现有关,即:你写的JS代码都运行在一个线程中。请思考一下。这意味着,尽管使用高效的非阻塞技术来执行I/O,但是JS代码在单个线程操作中运行基于CPU的操作,每个代码块都会阻塞下一个代码块的运行。有一个常见的例子:在数据库记录上循环,以某种方式处理记录,然后将它们输出到客户端。下面这段代码展示了这个例子的原理:
var handler = function(request, response) { connection.query('SELECT ...', function(err, rows) {if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };
虽然Node处理I/O的效率很高,但是上面例子中的for循环在一个主线程中使用了CPU周期。这意味着如果你有10000个连接,那么这个循环就可能会占用整个应用程序的时间。每个请求都必须要在主线程中占用一小段时间。
这整个概念的前提是I/O操作是最慢的部分,因此,即使串行处理是不得已的,但对它们进行有效处理也是非常重要的。这在某些情况下是成立的,但并非一成不变。
另一点观点是,写一堆嵌套的回调很麻烦,有些人认为这样的代码很丑陋。在Node代码中嵌入四个、五个甚至更多层的回调并不罕见。
又到了权衡利弊的时候了。如果你的主要性能问题是I/O的话,那么这个Node模型能帮到你。但是,它的缺点在于,如果你在一个处理HTTP请求的函数中放入了CPU处理密集型代码的话,一不小心就会让每个连接都出现拥堵。
在介绍Go之前,我透露一下,我是一个Go的粉丝。我已经在许多项目中使用了Go。
让我们看看它是如何处理I/O的吧。 Go语言的一个关键特性是它包含了自己的调度器。它并不会为每个执行线程对应一个操作系统线程,而是使用了“goroutines”这个概念。Go运行时会为一个goroutine分配一个操作系统线程,并控制它执行或暂停。Go HTTP服务器的每个请求都在一个单独的Goroutine中进行处理。
实际上,除了回调机制被内置到I/O调用的实现中并自动与调度器交互之外,Go运行时正在做的事情与Node不同。它也不会受到必须让所有的处理代码在同一个线程中运行的限制,Go会根据其调度程序中的逻辑自动将你的Goroutine映射到它认为合适的操作系统线程中。因此,它的代码是这样的:
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。
在大多数情况下,这真正做到了“两全其美”。非阻塞I/O可用于所有重要的事情,但是代码却看起来像是阻塞的,因此这样往往更容易理解和维护。 剩下的就是Go调度程序和OS调度程序之间的交互处理了。这并不是魔法,如果你正在建立一个大型系统,那么还是值得花时间去了解它的工作原理的。同时,“开箱即用”的特点使它能够更好地工作和扩展。
Go可能也有不少缺点,但总的来说,它处理I/O的方式并没有明显的缺点。
对于这些不同模型的上下文切换,很难进行准确的计时。当然,我也可以说这对你并没有多大的用处。这里,我将对这些服务器环境下的HTTP服务进行基本的性能评测比较。请记住,端到端的HTTP请求/响应性能涉及到的因素有很多。
我针对每一个环境都写了一段代码来读取64k文件中的随机字节,然后对其运行N次SHA-256散列(在URL的查询字符串中指定N,例如.../test.php?n=100)并以十六进制打印结果。我之所以选择这个,是因为它可以很容易运行一些持续的I/O操作,并且可以通过受控的方式来增加CPU使用率。
在这种存在大量连接和计算的情况下,我们看到的结果更多的是与语言本身的执行有关。请注意,“脚本语言”的执行速度最慢。
Tiba-tiba, prestasi Node menurun dengan ketara apabila operasi intensif CPU dalam setiap permintaan menyekat satu sama lain. Menariknya, dalam ujian ini, prestasi PHP menjadi lebih baik (berbanding dengan yang lain), malah lebih baik daripada Java. (Perlu diingat bahawa dalam PHP, pelaksanaan SHA-256 ditulis dalam C, tetapi laluan pelaksanaan memerlukan lebih banyak masa dalam gelung ini kerana kita melakukan 1000 lelaran cincang kali ini).
Saya rasa pada bilangan sambungan yang lebih tinggi, aplikasi untuk proses dan memori baharu dalam PHP Apache nampaknya menjadi faktor utama yang mempengaruhi prestasi PHP. Jelas sekali, Go adalah pemenang kali ini, diikuti oleh Java, Node, dan akhirnya PHP.
Walaupun terdapat banyak faktor yang terlibat dalam pemprosesan keseluruhan, dan ia sangat berbeza dari satu aplikasi ke satu aplikasi, semakin anda memahami prinsip asas dan pertukaran yang terlibat, Aplikasi akan berprestasi lebih baik.
Ringkasnya, apabila bahasa berkembang, begitu juga penyelesaian untuk aplikasi besar yang mengendalikan sejumlah besar I/O.
Untuk bersikap adil, kedua-dua PHP dan Java mempunyai pelaksanaan I/O tidak menyekat yang tersedia untuk aplikasi web. Walau bagaimanapun, pelaksanaan ini tidak digunakan secara meluas seperti kaedah yang diterangkan di atas, dan terdapat kos penyelenggaraan yang perlu dipertimbangkan. Apatah lagi bahawa kod aplikasi mesti distrukturkan dengan cara yang sesuai untuk persekitaran ini.
Mari kita bandingkan beberapa faktor penting yang mempengaruhi prestasi dan kemudahan penggunaan:
语言 | 线程与进程 | 非阻塞I/O | 易于使用 |
---|---|---|---|
PHP | 进程 | 否 | - |
Java | 线程 | 有效 | 需要回调 |
Node.js | 线程 | 是 | 需要回调 |
Go | 线程 (Goroutines) | 是 | 无需回调 |
Oleh kerana benang berkongsi ruang memori yang sama, tetapi proses tidak, benang biasanya lebih besar daripada Prosesnya jauh lebih cekap ingatan. Dalam senarai di atas, melihat dari atas ke bawah, faktor yang berkaitan dengan I/O adalah lebih baik daripada yang terakhir. Jadi, jika saya terpaksa memilih pemenang dalam perbandingan di atas, ia pastinya Go.
Secara praktikal, persekitaran yang anda pilih untuk membina aplikasi anda berkait rapat dengan kebiasaan pasukan anda dengan persekitaran dan produktiviti keseluruhan yang boleh dicapai oleh pasukan anda. Jadi, menggunakan Node atau Go untuk membangunkan aplikasi dan perkhidmatan web mungkin bukan pilihan terbaik untuk pasukan.
Semoga ini membantu anda memahami dengan lebih jelas perkara yang berlaku di bawah hud dan memberi anda beberapa cadangan tentang cara mengendalikan kebolehskalaan aplikasi.
Pembelajaran yang disyorkan: tutorial video php
Atas ialah kandungan terperinci Pertandingan prestasi I/O pelayan Node, PHP, Java dan Go, pada pendapat anda, siapa yang akan menang?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!