socialgekon.com
  • หลัก
  • ไลฟ์สไตล์
  • บุคลากรและทีมงานของผลิตภัณฑ์
  • กระบวนการและเครื่องมือ
  • การเพิ่มขึ้นของระยะไกล
ส่วนหลัง

ประสิทธิภาพ I / O ฝั่งเซิร์ฟเวอร์: โหนดเทียบกับ PHP เทียบกับ Java เทียบกับ Go

การทำความเข้าใจโมเดลอินพุต / เอาท์พุต (I / O) ของแอปพลิเคชันของคุณอาจหมายถึงความแตกต่างระหว่างแอปพลิเคชันที่เกี่ยวข้องกับการโหลดที่อยู่ภายใต้และแอปพลิเคชันที่พังเมื่อเผชิญกับกรณีการใช้งานจริง บางทีในขณะที่แอปพลิเคชันของคุณมีขนาดเล็กและไม่รองรับการโหลดสูง แต่อาจมีความสำคัญน้อยกว่ามาก แต่เมื่อปริมาณการใช้งานแอปพลิเคชันของคุณเพิ่มขึ้นการทำงานกับโมเดล I / O ที่ไม่ถูกต้องอาจทำให้คุณเข้าสู่โลกแห่งความเจ็บปวดได้

และเช่นเดียวกับสถานการณ์ส่วนใหญ่ที่สามารถทำได้หลายวิธีไม่ใช่แค่เรื่องที่วิธีใดดีกว่า แต่เป็นเรื่องของการทำความเข้าใจกับการแลกเปลี่ยน เรามาดูภาพรวมของ I / O และดูว่าเราสามารถสอดแนมอะไรได้บ้าง

ในบทความนี้เราจะเปรียบเทียบ Node, Java, Go และ PHP กับ Apache โดยจะพูดถึงวิธีที่ภาษาต่างๆสร้างโมเดล I / O ข้อดีและข้อเสียของแต่ละรุ่นและสรุปด้วยเกณฑ์มาตรฐานเบื้องต้นบางประการ หากคุณกังวลเกี่ยวกับประสิทธิภาพ I / O ของเว็บแอปพลิเคชันถัดไปบทความนี้เหมาะสำหรับคุณ



ข้อมูลเบื้องต้นเกี่ยวกับ I / O: รีเฟรชด่วน

เพื่อทำความเข้าใจปัจจัยที่เกี่ยวข้องกับ I / O อันดับแรกเราต้องทบทวนแนวคิดในระดับระบบปฏิบัติการ แม้ว่าจะเป็นไปได้ยากที่จะต้องจัดการกับแนวคิดเหล่านี้โดยตรง แต่คุณจัดการกับแนวคิดเหล่านี้ทางอ้อมผ่านสภาพแวดล้อมรันไทม์ของแอปพลิเคชันตลอดเวลา และรายละเอียดมีความสำคัญ

การโทรของระบบ

ประการแรกเรามีการเรียกระบบซึ่งสามารถอธิบายได้ดังนี้:

  • โปรแกรมของคุณ (ใน“ user land” ตามที่กล่าวไว้) ต้องขอให้เคอร์เนลของระบบปฏิบัติการดำเนินการ I / O ในนามของมัน
  • “ syscall” คือวิธีที่โปรแกรมของคุณขอให้เคอร์เนลทำบางสิ่ง ลักษณะเฉพาะของวิธีการใช้งานนี้แตกต่างกันไประหว่าง OSes แต่แนวคิดพื้นฐานเหมือนกัน จะมีคำสั่งเฉพาะบางอย่างที่ถ่ายโอนการควบคุมจากโปรแกรมของคุณไปยังเคอร์เนล (เช่นการเรียกใช้ฟังก์ชัน แต่มีซอสพิเศษสำหรับจัดการกับสถานการณ์นี้โดยเฉพาะ) โดยทั่วไปแล้ว syscalls จะปิดกั้นหมายความว่าโปรแกรมของคุณรอให้เคอร์เนลกลับไปที่รหัสของคุณ
  • เคอร์เนลทำการดำเนินการ I / O พื้นฐานบนอุปกรณ์ฟิสิคัลที่เป็นปัญหา (ดิสก์การ์ดเครือข่าย ฯลฯ ) และตอบกลับไปยัง syscall ในโลกแห่งความเป็นจริงเคอร์เนลอาจต้องทำหลายอย่างเพื่อตอบสนองคำขอของคุณรวมถึงรอให้อุปกรณ์พร้อมอัปเดตสถานะภายใน ฯลฯ แต่ในฐานะนักพัฒนาแอปพลิเคชันคุณไม่สนใจเรื่องนั้น นั่นคืองานของเคอร์เนล

แผนภาพ Syscalls

การบล็อกเทียบกับการโทรแบบไม่ปิดกั้น

ตอนนี้ฉันเพิ่งพูดไปข้างต้นว่า syscalls กำลังบล็อกและนั่นก็เป็นความจริงในแง่ทั่วไป อย่างไรก็ตามการเรียกบางสายถูกจัดประเภทเป็น“ non-block” ซึ่งหมายความว่าเคอร์เนลรับคำขอของคุณวางไว้ในคิวหรือบัฟเฟอร์ที่ใดที่หนึ่งจากนั้นจะส่งกลับทันทีโดยไม่ต้องรอให้ I / O เกิดขึ้นจริง ดังนั้นจึง 'บล็อก' ในช่วงเวลาสั้น ๆ เท่านั้นนานพอที่จะบังคับใช้คำขอของคุณ

ตัวอย่างบางส่วน (ของ Linux syscalls) อาจช่วยชี้แจง: - read() คือการบล็อกการโทร - คุณส่งแฮนเดิลที่บอกว่าไฟล์ใดและบัฟเฟอร์ของตำแหน่งที่จะส่งข้อมูลที่อ่านและการโทรจะส่งกลับเมื่อข้อมูลอยู่ที่นั่น สังเกตว่าสิ่งนี้มีข้อดีคือเป็นคนดีและเรียบง่าย - epoll_create(), epoll_ctl() และ epoll_wait() คือการโทรตามลำดับให้คุณสร้างกลุ่มแฮนเดิลเพื่อฟังเพิ่ม / ลบตัวจัดการจากกลุ่มนั้นจากนั้นบล็อกจนกว่าจะมีกิจกรรมใด ๆ สิ่งนี้ช่วยให้คุณควบคุมการดำเนินการ I / O จำนวนมากได้อย่างมีประสิทธิภาพด้วยเธรดเดียว แต่ฉันจะก้าวไปข้างหน้า นี่เป็นสิ่งที่ดีหากคุณต้องการฟังก์ชันการทำงาน แต่อย่างที่คุณเห็นว่ามันซับซ้อนกว่าที่จะใช้

สิ่งสำคัญคือต้องเข้าใจลำดับความแตกต่างของเวลาที่นี่ หากแกน CPU ทำงานที่ 3GHz โดยไม่ได้รับการเพิ่มประสิทธิภาพ CPU จะทำงานได้ 3 พันล้านรอบต่อวินาที (หรือ 3 รอบต่อนาโนวินาที) การเรียกระบบที่ไม่ปิดกั้นอาจใช้เวลา 10 วินาทีของรอบจึงจะเสร็จสมบูรณ์หรือ“ ไม่กี่นาโนวินาที” การโทรที่บล็อกการรับข้อมูลผ่านเครือข่ายอาจใช้เวลานานกว่ามากตัวอย่างเช่น 200 มิลลิวินาที (1/5 วินาที) และสมมติว่าการโทรแบบไม่ปิดกั้นใช้เวลา 20 นาโนวินาทีและการโทรที่บล็อกใช้เวลา 200,000,000 นาโนวินาที กระบวนการของคุณรอนานกว่า 10 ล้านครั้งสำหรับการโทรที่บล็อก

การบล็อกเทียบกับ Syscall ที่ไม่ปิดกั้น

เคอร์เนลให้วิธีการทำทั้งการบล็อก I / O (“ อ่านจากการเชื่อมต่อเครือข่ายนี้และให้ข้อมูลแก่ฉัน”) และ I / O ที่ไม่ปิดกั้น (“ บอกฉันเมื่อการเชื่อมต่อเครือข่ายเหล่านี้มีข้อมูลใหม่”) และกลไกใดที่ใช้จะขัดขวางกระบวนการโทรสำหรับระยะเวลาที่แตกต่างกันอย่างมาก

การตั้งเวลา

สิ่งที่สามที่ต้องติดตามคือสิ่งที่จะเกิดขึ้นเมื่อคุณมีเธรดหรือกระบวนการจำนวนมากที่เริ่มบล็อก

สำหรับวัตถุประสงค์ของเราไม่มีความแตกต่างอย่างมากระหว่างเธรดและกระบวนการ ในชีวิตจริงความแตกต่างที่เกี่ยวข้องกับประสิทธิภาพที่เห็นได้ชัดเจนที่สุดก็คือเนื่องจากเธรดใช้หน่วยความจำเดียวกันและแต่ละกระบวนการมีพื้นที่หน่วยความจำของตัวเองทำให้กระบวนการแยกกันมีแนวโน้มที่จะใช้หน่วยความจำมากขึ้น แต่เมื่อเรากำลังพูดถึงการตั้งเวลาสิ่งที่สำคัญที่สุดคือรายการของสิ่งต่าง ๆ (เธรดและกระบวนการเหมือนกัน) ที่แต่ละอย่างต้องได้รับช่วงเวลาดำเนินการกับแกน CPU ที่มีอยู่ หากคุณมี 300 เธรดที่รันและ 8 คอร์เพื่อรันบนเธรดคุณต้องแบ่งเวลาเพื่อให้แต่ละเธรดได้รับส่วนแบ่งโดยแต่ละคอร์จะทำงานในช่วงเวลาสั้น ๆ จากนั้นจึงย้ายไปยังเธรดถัดไป สิ่งนี้ทำได้ผ่าน 'สวิตช์บริบท' ทำให้ CPU เปลี่ยนจากการเรียกใช้เธรด / กระบวนการหนึ่งไปยังอีกกระบวนการหนึ่ง

สวิตช์บริบทเหล่านี้มีต้นทุนที่เกี่ยวข้องซึ่งต้องใช้เวลาพอสมควร ในบางกรณีที่รวดเร็วอาจน้อยกว่า 100 นาโนวินาที แต่ไม่ใช่เรื่องแปลกที่จะใช้เวลา 1,000 นาโนวินาทีหรือนานกว่านั้นขึ้นอยู่กับรายละเอียดการใช้งานความเร็ว / สถาปัตยกรรมของโปรเซสเซอร์แคชของ CPU ฯลฯ

และยิ่งเธรด (หรือกระบวนการ) มากเท่าไหร่การสลับบริบทก็ยิ่งมากขึ้น เมื่อเรากำลังพูดถึงเธรดหลายพันเธรดและหลายร้อยนาโนวินาทีสำหรับแต่ละเธรดสิ่งต่างๆอาจช้ามาก

อย่างไรก็ตามการโทรที่ไม่ปิดกั้นโดยพื้นฐานแล้วจะบอกเคอร์เนลว่า“ โทรหาฉันเมื่อคุณมีข้อมูลหรือเหตุการณ์ใหม่ ๆ ในการเชื่อมต่อเหล่านี้เท่านั้น” การโทรที่ไม่ปิดกั้นเหล่านี้ได้รับการออกแบบมาเพื่อจัดการกับโหลด I / O ขนาดใหญ่อย่างมีประสิทธิภาพและลดการสลับบริบท

กับฉันจนถึงตอนนี้? เพราะตอนนี้มาถึงส่วนที่น่าสนุกแล้วเรามาดูกันว่าภาษายอดนิยมบางภาษาทำอะไรกับเครื่องมือเหล่านี้และหาข้อสรุปบางประการเกี่ยวกับการแลกเปลี่ยนระหว่างความง่ายในการใช้งานและประสิทธิภาพ ... และเกร็ดน่ารู้อื่น ๆ ที่น่าสนใจ

โปรดทราบว่าในขณะที่ตัวอย่างที่แสดงในบทความนี้เป็นเรื่องเล็กน้อย (และเป็นบางส่วนโดยแสดงเฉพาะบิตที่เกี่ยวข้องเท่านั้น) การเข้าถึงฐานข้อมูลระบบแคชภายนอก (memcache และอื่น ๆ ทั้งหมด) และสิ่งใดก็ตามที่ต้องใช้ I / O จะสิ้นสุดลงด้วยการเรียกใช้ I / O บางประเภทภายใต้ประทุนซึ่งจะมีผลเช่นเดียวกับตัวอย่างง่ายๆที่แสดง นอกจากนี้สำหรับสถานการณ์ที่ I / O ถูกอธิบายว่าเป็น 'การบล็อก' (PHP, Java) คำขอและการตอบกลับของ HTTP จะอ่านและเขียนเป็นการปิดกั้นการโทร: อีกครั้งมี I / O เพิ่มเติมที่ซ่อนอยู่ในระบบด้วยปัญหาด้านประสิทธิภาพของผู้ดูแล ที่ต้องคำนึงถึง

มีหลายปัจจัยในการเลือกภาษาโปรแกรมสำหรับโครงการ แม้จะมีหลายปัจจัยเมื่อคุณพิจารณาเฉพาะประสิทธิภาพ แต่ถ้าคุณกังวลว่าโปรแกรมของคุณจะถูก จำกัด โดย I / O เป็นหลักหากประสิทธิภาพของ I / O ถูกสร้างหรือทำลายสำหรับโปรเจ็กต์ของคุณสิ่งเหล่านี้คือสิ่งที่คุณต้องรู้

แนวทาง“ Keep It Simple”: PHP

ย้อนกลับไปในยุค 90 ผู้คนจำนวนมากสวมใส่ Converse รองเท้าและการเขียนสคริปต์ CGI ใน Perl จากนั้น PHP ก็เข้ามาและเช่นเดียวกับที่บางคนชอบที่จะใช้มันทำให้การสร้างหน้าเว็บแบบไดนามิกง่ายขึ้นมาก

รูปแบบ 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

ค่อนข้างง่าย: หนึ่งกระบวนการต่อคำขอ การโทร I / O เพียงแค่บล็อก ความได้เปรียบ? ง่ายและใช้งานได้จริง เสียเปรียบ? เข้าถึงลูกค้า 20,000 รายพร้อมกันและเซิร์ฟเวอร์ของคุณจะลุกเป็นไฟ แนวทางนี้ไม่สามารถปรับขนาดได้ดีเนื่องจากไม่ได้ใช้เครื่องมือที่เคอร์เนลจัดเตรียมไว้สำหรับจัดการกับ I / O ปริมาณมาก (epoll ฯลฯ ) และเพื่อเพิ่มการดูถูกการบาดเจ็บการเรียกใช้กระบวนการแยกต่างหากสำหรับแต่ละคำขอมีแนวโน้มที่จะใช้ทรัพยากรระบบจำนวนมากโดยเฉพาะหน่วยความจำซึ่งมักเป็นสิ่งแรกที่คุณใช้หมดในสถานการณ์เช่นนี้

หมายเหตุ: วิธีการที่ใช้สำหรับ Ruby นั้นคล้ายกับ PHP มากและในทางกว้างทั่วไปหยักด้วยมือถือได้ว่าเหมือนกันสำหรับวัตถุประสงค์ของเรา

วิธีการมัลติเธรด: Java

ดังนั้น Java จึงมาพร้อมกับเวลาที่คุณซื้อชื่อโดเมนแรกของคุณและเป็นเรื่องดีที่จะสุ่มพูดว่า 'ดอทคอม' หลังประโยค และ 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 ได้อย่างมีประสิทธิภาพ และนี่เป็นความจริงในความหมายทั่วไป แต่ปีศาจอยู่ในรายละเอียดและวิธีการที่คาถานี้ประสบความสำเร็จเป็นเรื่องสำคัญเมื่อพูดถึงประสิทธิภาพ

โดยพื้นฐานแล้วการเปลี่ยนกระบวนทัศน์ที่โหนดดำเนินการคือแทนที่จะพูดว่า 'เขียนโค้ดของคุณที่นี่เพื่อจัดการกับคำขอ' แต่จะพูดว่า 'เขียนโค้ดที่นี่เพื่อเริ่มจัดการคำขอ' ทุกครั้งที่คุณต้องทำสิ่งที่เกี่ยวข้องกับ 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 จัดการแล้วเรียกกลับเรียกว่า“ Event Loop” และทำงานได้ดี

โหนดโมเดล I / O js

อย่างไรก็ตามมีการจับรุ่นนี้ ภายใต้ประทุนเหตุผลของมันยังมีอีกมากที่เกี่ยวข้องกับการใช้งานเอ็นจิ้น V8 JavaScript (เครื่องมือ JS ของ Chrome ที่ใช้โดยโหนด) หนึ่ง มากกว่าสิ่งอื่นใด โค้ด 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

ในขณะที่ Node จัดการ I / O ได้อย่างมีประสิทธิภาพนั้น for วนซ้ำในตัวอย่างด้านบนคือการใช้รอบ CPU ภายในเธรดหลักเพียงเธรดเดียวของคุณ ซึ่งหมายความว่าหากคุณมีการเชื่อมต่อ 10,000 ครั้งการวนซ้ำนั้นอาจนำแอปพลิเคชันทั้งหมดของคุณไปสู่การรวบรวมข้อมูลขึ้นอยู่กับระยะเวลาที่ใช้ แต่ละคำขอต้องแบ่งเวลาทีละส่วนในเธรดหลักของคุณ

สมมติฐานทั้งหมดนี้ตั้งอยู่บนพื้นฐานคือการดำเนินการ I / O เป็นส่วนที่ช้าที่สุดดังนั้นจึงเป็นสิ่งสำคัญที่สุดในการจัดการกับสิ่งเหล่านี้อย่างมีประสิทธิภาพแม้ว่าจะหมายถึงการประมวลผลอื่น ๆ ตามลำดับ นี่เป็นเรื่องจริงในบางกรณี แต่ไม่ใช่ทั้งหมด

อีกประเด็นหนึ่งก็คือและแม้ว่านี่จะเป็นเพียงความคิดเห็น แต่ก็อาจเป็นเรื่องที่น่าเบื่อหน่ายในการเขียนการเรียกกลับแบบซ้อนกันและบางคนโต้แย้งว่ามันทำให้โค้ดติดตามยากขึ้นอย่างมาก ไม่ใช่เรื่องแปลกที่จะเห็นการเรียกกลับที่ซ้อนกันสี่ห้าระดับหรือมากกว่านั้นลึกเข้าไปในโค้ดโหนด

เรากลับมาอีกครั้งกับการแลกเปลี่ยน โมเดลโหนดทำงานได้ดีหากปัญหาด้านประสิทธิภาพหลักของคุณคือ I / O อย่างไรก็ตาม Achilles Heel คือคุณสามารถเข้าสู่ฟังก์ชันที่จัดการคำขอ HTTP และใส่รหัสที่ใช้ CPU มากและนำทุกการเชื่อมต่อไปยังการรวบรวมข้อมูลหากคุณไม่ระมัดระวัง

ไม่ปิดกั้นโดยธรรมชาติ: ไป

ก่อนที่ฉันจะเข้าสู่หัวข้อ Go ฉันควรเปิดเผยว่าฉันเป็นแฟนบอยของ Go ฉันใช้มันในหลาย ๆ โครงการและฉันเป็นผู้เสนอข้อได้เปรียบด้านการผลิตอย่างเปิดเผยและฉันเห็นสิ่งเหล่านี้ในงานของฉันเมื่อฉันใช้มัน

ที่กล่าวมานั้นมาดูกันว่ามันเกี่ยวข้องกับ I / O อย่างไร คุณลักษณะสำคัญอย่างหนึ่งของภาษา Go คือมีตัวกำหนดตารางเวลาของตัวเอง แทนที่จะใช้แต่ละเธรดของการดำเนินการที่สอดคล้องกับเธรด OS เดียวจะทำงานร่วมกับแนวคิดของ“ goroutines” และรันไทม์ Go สามารถกำหนด goroutine ให้กับเธรด OS และเรียกใช้งานหรือระงับและไม่ให้เชื่อมโยงกับเธรด OS ตามสิ่งที่ goroutine กำลังทำอยู่ แต่ละคำขอที่มาจากเซิร์ฟเวอร์ HTTP ของ Go จะได้รับการจัดการใน Goroutine แยกกัน

แผนภาพวิธีการทำงานของตัวกำหนดตารางเวลามีลักษณะดังนี้:

I / O Model Go

ภายใต้ประทุนนี้จะถูกนำไปใช้โดยจุดต่างๆใน Go runtime ที่ใช้การโทร I / O โดยการร้องขอให้เขียน / อ่าน / เชื่อมต่อ / ฯลฯ ทำให้ goroutine ปัจจุบันเข้าสู่โหมดสลีปพร้อมข้อมูลที่จะปลุก goroutine ให้กลับมา เมื่อสามารถดำเนินการเพิ่มเติมได้

ผลรันไทม์ Go กำลังทำสิ่งที่ไม่แตกต่างกันมากกับสิ่งที่ Node กำลังทำยกเว้นว่ากลไกการโทรกลับถูกสร้างขึ้นในการใช้งานการเรียก I / O และโต้ตอบกับตัวกำหนดตารางเวลาโดยอัตโนมัติ นอกจากนี้ยังไม่ต้องทนทุกข์ทรมานจากข้อ จำกัด ที่จะต้องมีโค้ดตัวจัดการทั้งหมดของคุณทำงานในเธรดเดียวกัน Go จะแมป Goroutines ของคุณโดยอัตโนมัติกับเธรด OS จำนวนมากที่เห็นว่าเหมาะสมตามตรรกะในตัวกำหนดตารางเวลา ผลลัพธ์คือรหัสดังนี้:

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 อาจมีข้อผิดพลาด แต่โดยทั่วไปแล้ววิธีจัดการ I / O ไม่ได้อยู่ในกลุ่มนี้

คำโกหกคำโกหกที่น่าอับอายและเกณฑ์มาตรฐาน

เป็นการยากที่จะกำหนดเวลาที่แน่นอนในการสลับบริบทที่เกี่ยวข้องกับโมเดลต่างๆเหล่านี้ ฉันสามารถโต้แย้งได้ว่าสิ่งนี้มีประโยชน์น้อยสำหรับคุณ ดังนั้นฉันจะให้เกณฑ์มาตรฐานพื้นฐานที่เปรียบเทียบประสิทธิภาพเซิร์ฟเวอร์ HTTP โดยรวมของสภาพแวดล้อมเซิร์ฟเวอร์เหล่านี้แทน โปรดทราบว่ามีหลายปัจจัยที่เกี่ยวข้องกับประสิทธิภาพของเส้นทางคำขอ / การตอบกลับ HTTP แบบ end-to-end ทั้งหมดและตัวเลขที่นำเสนอนี้เป็นเพียงตัวอย่างบางส่วนที่ฉันรวบรวมเพื่อเปรียบเทียบขั้นพื้นฐาน

สำหรับแต่ละสภาพแวดล้อมเหล่านี้ฉันเขียนโค้ดที่เหมาะสมเพื่ออ่านในไฟล์ 64k ที่มีไบต์แบบสุ่มรันแฮช SHA-256 บน N จำนวนครั้ง (N ถูกระบุในสตริงการสืบค้นของ URL เช่น .../test.php?n=100 ) และพิมพ์แฮชที่ได้เป็นเลขฐานสิบหก ฉันเลือกสิ่งนี้เพราะเป็นวิธีที่ง่ายมากในการเรียกใช้เกณฑ์มาตรฐานเดียวกันกับ I / O ที่สอดคล้องกันและวิธีควบคุมเพื่อเพิ่มการใช้งาน CPU

ดู หมายเหตุมาตรฐานเหล่านี้ สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับสภาพแวดล้อมที่ใช้

ขั้นแรกมาดูตัวอย่างการเกิดพร้อมกันต่ำ การรันการทำซ้ำ 2,000 ครั้งโดยมีคำขอพร้อมกัน 300 คำขอและมีเพียงหนึ่งแฮชต่อคำขอ (N = 1) ทำให้เราได้สิ่งนี้:

จำนวนมิลลิวินาทีโดยเฉลี่ยในการดำเนินการตามคำขอสำหรับคำขอพร้อมกันทั้งหมด N = 1

เวลาคือจำนวนมิลลิวินาทีเฉลี่ยในการดำเนินการตามคำขอสำหรับคำขอที่เกิดขึ้นพร้อมกันทั้งหมด ต่ำกว่าจะดีกว่า

มันยากที่จะหาข้อสรุปจากกราฟเพียงกราฟเดียว แต่สำหรับฉันแล้วดูเหมือนว่าในปริมาณการเชื่อมต่อและการคำนวณนี้เราเห็นว่าเวลาที่เกี่ยวข้องกับการดำเนินการทั่วไปของภาษานั้นมากขึ้นและมากขึ้นเพื่อให้ I / O โปรดทราบว่าภาษาที่ถือว่าเป็น 'ภาษาสคริปต์' (การพิมพ์แบบหลวม ๆ การตีความแบบไดนามิก) ทำงานได้ช้าที่สุด

แต่จะเกิดอะไรขึ้นถ้าเราเพิ่ม N เป็น 1,000 โดยยังคงมีคำขอพร้อมกัน 300 คำขอ - โหลดเท่าเดิม แต่มีการแฮชซ้ำเพิ่มขึ้น 100 เท่า (โหลด CPU มากขึ้นอย่างมาก):

จำนวนมิลลิวินาทีโดยเฉลี่ยในการดำเนินการตามคำขอสำหรับคำขอที่เกิดขึ้นพร้อมกันทั้งหมด N = 1000

เวลาคือจำนวนมิลลิวินาทีเฉลี่ยในการดำเนินการตามคำขอสำหรับคำขอที่เกิดขึ้นพร้อมกันทั้งหมด ต่ำกว่าจะดีกว่า

ทันใดนั้นประสิทธิภาพของโหนดจะลดลงอย่างมากเนื่องจากการดำเนินการที่ใช้ CPU มากในแต่ละคำขอจะปิดกั้นซึ่งกันและกัน และที่น่าสนใจก็คือประสิทธิภาพของ PHP ดีขึ้นมาก (เมื่อเทียบกับตัวอื่น ๆ ) และเอาชนะ Java ในการทดสอบนี้ (เป็นที่น่าสังเกตว่าใน PHP การใช้งาน SHA-256 เขียนด้วยภาษา C และเส้นทางการดำเนินการใช้เวลามากขึ้นในลูปนั้นเนื่องจากเรากำลังทำซ้ำ 1,000 แฮชในขณะนี้)

ตอนนี้เรามาลองการเชื่อมต่อพร้อมกัน 5,000 รายการ (ด้วย N = 1) หรือใกล้เคียงกับที่ฉันทำได้ น่าเสียดายที่สำหรับสภาพแวดล้อมเหล่านี้ส่วนใหญ่อัตราความล้มเหลวไม่ได้มีนัยสำคัญ สำหรับแผนภูมินี้เราจะดูจำนวนคำขอทั้งหมดต่อวินาที ยิ่งสูงยิ่งดี :

จำนวนคำขอทั้งหมดต่อวินาที N = 1, 5,000 req / วินาที

จำนวนคำขอทั้งหมดต่อวินาที สูงกว่าจะดีกว่า

และภาพดูแตกต่างกันมาก เป็นการคาดเดา แต่ดูเหมือนว่าค่าใช้จ่ายต่อการเชื่อมต่อที่มีปริมาณการเชื่อมต่อสูงจะเกี่ยวข้องกับการสร้างกระบวนการใหม่และหน่วยความจำเพิ่มเติมที่เกี่ยวข้องกับมันใน PHP + Apache ดูเหมือนจะกลายเป็นปัจจัยที่โดดเด่นและทำให้ประสิทธิภาพของ PHP ลดลง เห็นได้ชัดว่า Go เป็นผู้ชนะที่นี่ตามด้วย Java, Node และสุดท้ายคือ PHP

แม้ว่าปัจจัยที่เกี่ยวข้องกับทรูพุตโดยรวมของคุณนั้นมีมากมายและยังแตกต่างกันอย่างมากในแต่ละแอปพลิเคชัน แต่ยิ่งคุณเข้าใจมากขึ้นเกี่ยวกับความกล้าของสิ่งที่เกิดขึ้นภายใต้ประทุนและการแลกเปลี่ยนที่เกี่ยวข้องคุณก็จะยิ่งดีขึ้น

สรุป

จากทั้งหมดที่กล่าวมาเป็นที่ชัดเจนว่าเมื่อภาษามีการพัฒนาขึ้นโซลูชันในการจัดการกับแอปพลิเคชันขนาดใหญ่ที่มี I / O จำนวนมากได้รับการพัฒนาไปด้วย

เพื่อความเป็นธรรมทั้ง PHP และ Java แม้จะมีคำอธิบายในบทความนี้ก็ตาม การใช้งาน ของ I / O ที่ไม่ปิดกั้น พร้อมใช้งาน ใน เว็บแอปพลิเคชัน . แต่สิ่งเหล่านี้ไม่ได้เกิดขึ้นบ่อยเหมือนวิธีการที่อธิบายไว้ข้างต้นและจำเป็นต้องคำนึงถึงค่าใช้จ่ายในการดูแลเซิร์ฟเวอร์โดยใช้วิธีการดังกล่าวด้วย ไม่ต้องพูดถึงว่าโค้ดของคุณจะต้องมีโครงสร้างในลักษณะที่ใช้งานได้กับสภาพแวดล้อมดังกล่าว โดยปกติแล้วเว็บแอปพลิเคชัน PHP หรือ Java ของคุณจะไม่ทำงานหากไม่มีการแก้ไขที่สำคัญในสภาพแวดล้อมดังกล่าว

จากการเปรียบเทียบหากเราพิจารณาปัจจัยสำคัญบางประการที่ส่งผลต่อประสิทธิภาพและความสะดวกในการใช้งานเราจะได้รับสิ่งนี้:

ภาษา เธรดเทียบกับกระบวนการ I / O แบบไม่ปิดกั้น สะดวกในการใช้
PHP กระบวนการ ไม่
Java เธรด มีจำหน่าย ต้องมีการโทรกลับ
โหนด js เธรด ใช่ ต้องมีการโทรกลับ
ไป เธรด (Goroutines) ใช่ ไม่จำเป็นต้องโทรกลับ


โดยทั่วไปแล้วเธรดจะมีหน่วยความจำที่มีประสิทธิภาพมากกว่ากระบวนการมากเนื่องจากใช้พื้นที่หน่วยความจำเดียวกันในขณะที่กระบวนการต่างๆไม่ เมื่อรวมเข้ากับปัจจัยที่เกี่ยวข้องกับ I / O ที่ไม่ปิดกั้นเราจะเห็นว่าอย่างน้อยก็ด้วยปัจจัยที่พิจารณาข้างต้นเมื่อเราเลื่อนรายการการตั้งค่าทั่วไปลงในรายการที่เกี่ยวข้องกับการปรับปรุง I / O ดังนั้นถ้าฉันต้องเลือกผู้ชนะในการแข่งขันข้างต้นก็ต้องเป็นไปอย่างแน่นอน

อย่างไรก็ตามในทางปฏิบัติแล้วการเลือกสภาพแวดล้อมที่จะสร้างแอปพลิเคชันของคุณนั้นเชื่อมโยงอย่างใกล้ชิดกับความคุ้นเคยที่ทีมของคุณมีต่อสภาพแวดล้อมดังกล่าวและผลผลิตโดยรวมที่คุณสามารถทำได้ด้วย ดังนั้นจึงอาจไม่สมเหตุสมผลสำหรับทุกทีมที่จะดำน้ำและเริ่มพัฒนาเว็บแอปพลิเคชันและบริการใน Node หรือ Go อันที่จริงการค้นหานักพัฒนาหรือความคุ้นเคยของทีมงานภายในของคุณมักถูกอ้างว่าเป็นเหตุผลหลักที่จะไม่ใช้ภาษาและ / หรือสภาพแวดล้อมที่แตกต่างกัน ที่กล่าวว่าเวลามีการเปลี่ยนแปลงในช่วงสิบห้าปีที่ผ่านมาหรือมากกว่านั้นมาก

หวังว่าข้อมูลข้างต้นจะช่วยให้เห็นภาพที่ชัดเจนขึ้นเกี่ยวกับสิ่งที่เกิดขึ้นภายใต้ประทุนและให้แนวคิดบางประการเกี่ยวกับวิธีจัดการกับความสามารถในการปรับขนาดในโลกแห่งความเป็นจริงสำหรับแอปพลิเคชันของคุณ มีความสุขในการป้อนและส่งออก!

บทที่ 11 การล้มละลาย: มันคืออะไรและจะเกิดอะไรขึ้นต่อไป?

กระบวนการทางการเงิน

บทที่ 11 การล้มละลาย: มันคืออะไรและจะเกิดอะไรขึ้นต่อไป?
การสร้างรหัสโมดูลาร์อย่างแท้จริงโดยไม่ต้องพึ่งพา

การสร้างรหัสโมดูลาร์อย่างแท้จริงโดยไม่ต้องพึ่งพา

การจัดการโครงการ

โพสต์ยอดนิยม
การทำให้ชัดเจนในการค้นหา iOS 9 Spotlight สำหรับนักพัฒนา
การทำให้ชัดเจนในการค้นหา iOS 9 Spotlight สำหรับนักพัฒนา
วิธีเรียกใช้แจกของรางวัลบน Instagram: กฎ แนวคิด และแอพ
วิธีเรียกใช้แจกของรางวัลบน Instagram: กฎ แนวคิด และแอพ
เริ่มต้นใช้งาน Microservices: บทช่วยสอน Dropwizard
เริ่มต้นใช้งาน Microservices: บทช่วยสอน Dropwizard
การส่งเสริมโอกาสและการดำเนินการในพื้นที่ทำงานระยะไกล
การส่งเสริมโอกาสและการดำเนินการในพื้นที่ทำงานระยะไกล
อย่าเกลียด WordPress: 5 อคติทั่วไปที่ถูกหักล้าง
อย่าเกลียด WordPress: 5 อคติทั่วไปที่ถูกหักล้าง
 
เปิดใช้ Angular 2 ของคุณ: อัปเกรดจาก 1.5
เปิดใช้ Angular 2 ของคุณ: อัปเกรดจาก 1.5
ทำความคุ้นเคย - คำแนะนำเกี่ยวกับขั้นตอนการเริ่มต้นใช้งานของผู้ใช้
ทำความคุ้นเคย - คำแนะนำเกี่ยวกับขั้นตอนการเริ่มต้นใช้งานของผู้ใช้
แนวทางปฏิบัติที่ดีที่สุดของการออกแบบ UI และข้อผิดพลาดที่พบบ่อยที่สุด
แนวทางปฏิบัติที่ดีที่สุดของการออกแบบ UI และข้อผิดพลาดที่พบบ่อยที่สุด
เหตุใดจึงต้องพิจารณาการออกแบบเว็บไซต์ใหม่ - คำแนะนำและคำแนะนำ
เหตุใดจึงต้องพิจารณาการออกแบบเว็บไซต์ใหม่ - คำแนะนำและคำแนะนำ
ภาพรวมโดยย่อของ Vulkan API
ภาพรวมโดยย่อของ Vulkan API
หมวดหมู่
การจัดการวิศวกรรมส่วนหลังเคล็ดลับและเครื่องมือเทคโนโลยีกำลังแก้ไขการออกแบบตราสินค้าอนาคตของการทำงานพรสวรรค์ว่องไวอื่น ๆว่องไว

© 2023 | สงวนลิขสิทธิ์

socialgekon.com