- การเขียนโปรแกรมแบบ CGI ก็สามารถประมวลผลคำขอเว็บได้มากกว่า 200 ล้านครั้งต่อวัน
- จากการที่ ประสิทธิภาพฮาร์ดแวร์ ดีขึ้นมากในช่วงหลัง ข้อเสียของแนวทาง CGI จึงลดลงอย่างมาก
- โปรแกรม CGI ที่ใช้ Go และ SQLite แสดงประสิทธิภาพได้ยอดเยี่ยมบน CPU แบบ 16 เธรด
- CGI มีโครงสร้างที่เหมาะอย่างยิ่งกับการ ใช้หลายคอร์ของ CPU
- ด้วยเทคโนโลยีสมัยใหม่ แนวทางพัฒนาเว็บแอปในอดีต ก็ยังมีความเป็นไปได้ในทางปฏิบัติอย่างเพียงพอ
อดีตและปัจจุบันของ CGI
- ในช่วงปลายทศวรรษ 1990 ผู้เขียนเริ่มพัฒนาเว็บด้วย CGI และในเวลานั้นใช้ระบบอย่าง NewsPro
- CGI ทำให้เกิด การเริ่มและจบโปรเซสใหม่ ซ้ำทุกครั้งต่อคำขอเว็บ จึงมีโอเวอร์เฮดสูง
- ด้วยเหตุนี้จึงมีการพัฒนาเทคโนโลยีทดแทนที่มีประสิทธิภาพกว่า เช่น PHP, FastCGI
พัฒนาการของประสิทธิภาพฮาร์ดแวร์
- ตลอดกว่า 20 ปีที่ผ่านมา ความเร็วและประสิทธิภาพของคอมพิวเตอร์ เพิ่มขึ้นอย่างรวดเร็ว
- ในปี 2020 ผู้เขียนได้ใช้เครื่องมือที่พัฒนาด้วย Go และ Rust (เช่น ripgrep) และค้นพบอีกครั้งว่าแนวทางการรันแบบโปรเซสนั้นใช้งานได้จริง
ข้อดีของแนวทาง CGI สมัยใหม่
- หากสร้าง CGI ด้วยภาษาอย่าง Go และ Rust ที่รันได้รวดเร็ว ข้อเสียส่วนใหญ่ของ CGI แบบเก่าก็จะถูกลดทอนไป
- โปรแกรม CGI ทำงานเป็น โปรเซสแยกต่อหนึ่งคำขอ จึงเหมาะอย่างยิ่งต่อการ ใช้ CPU แบบหลายคอร์
- ตัวอย่างเช่น ในสภาพแวดล้อม 16 เธรด พบว่าสามารถรองรับได้มากกว่า 2400 คำขอ/วินาที = มากกว่า 200 ล้านคำขอ/วัน
- เซิร์ฟเวอร์ขนาดใหญ่สามารถมี CPU threads ได้ตั้งแต่ 384 เธรดขึ้นไป
มุมมองต่อวัฒนธรรมการพัฒนา
- ปัจจุบันด้วยการนำภาษาอย่าง Go และ Rust มาใช้ แนวทาง CGI แบบยุค 1990 อาจกลับมามีความหมายอีกครั้ง
- อย่างไรก็ตาม วิธีนี้ก็ยังไม่เหมาะกับทุกสภาพแวดล้อม และไม่ได้ถูกแนะนำให้เป็นแนวทางหลัก
- ประเด็นสำคัญคือ ณ เวลานี้ ได้มีการพิสูจน์เชิงทดลองแล้วว่า CGI ไม่ใช่โซลูชันที่ ไร้ประสิทธิภาพเหมือนในอดีต อีกต่อไป
บทสรุป
- ด้วย ฮาร์ดแวร์สมัยใหม่และการรองรับจากภาษาที่รวดเร็ว การเขียนโปรแกรมแบบ CGI จึงให้ประสิทธิภาพที่เทียบกับอดีตไม่ได้
- นี่เป็นกรณีตัวอย่างที่ใช้ข้อดีของสถาปัตยกรรมหลายโปรเซสได้อย่างเต็มที่ และมอบมุมมองที่น่าสนใจให้กับนักพัฒนาเว็บ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ทุกวันนี้แม้แต่ Python ก็ยังให้ความรู้สึกว่า CGI เร็วพอสมควร
ต่อให้ CGI script ใช้ CPU ตอนเริ่มต้น 400 มิลลิวินาที ถ้าเซิร์ฟเวอร์มี 64 คอร์ ก็รองรับได้ 160 คำขอต่อวินาที หรือราว 14 ล้านทราฟฟิกต่อวันต่อเครื่อง
หมายความว่าแม้ทราฟฟิกระดับหลายร้อยล้านคำขอต่อวัน (ไม่รวม static asset) ก็ไม่ได้ติดคอขวดที่การสตาร์ต CGI process
เมื่อก่อนเคยมองว่าเทคโนโลยีแบบนี้เป็น "เทคโนโลยีที่เสถียรจนแทบน่าเบื่อ" เลยอยู่ใน Python standard library มาตลอด แต่ผู้ดูแล Python ยุคนี้กลับมีท่าทีเชิงลบต่อเสถียรภาพและ backward compatibility มากกว่าเดิม
เลยค่อย ๆ เอาโมดูลที่ "น่าเบื่อแต่เสถียรเกินไป" ออกจาก standard library และจริง ๆ แล้วโมดูล
cgiก็ถูกลบในเวอร์ชัน 3.13อาจเป็นเพราะเคยชินกับการใช้ Python ทำ prototype มาเกือบ 25 ปี แต่ตอนนี้เริ่มเสียดาย
ตอนนี้เลยรู้สึกลังเลอยู่ระหว่าง JS กับ Lua
ลิงก์คำอธิบายอย่างเป็นทางการเรื่องการถอด
cgiคือ PEP 594 cgiจากลิงก์นี้จะโยงไปยัง PEP 206 ที่เขียนไว้เมื่อปี 2000 (25 ปีก่อน) ซึ่งตอนนั้นก็อธิบายไว้แล้วว่า "แพ็กเกจ
cgiออกแบบไม่ค่อยดีและแก้ไขยาก"และในรีโพ jackrosenthal/legacy-cgi ก็มี drop-in replacement ที่แทนโมดูลจาก standard library ได้โดยตรง
นักพัฒนา Python แค่ถอดโมดูลที่ชื่อ
cgiออกไปการทำ CGI script ยังรองรับอยู่ผ่าน
CGIHTTPRequestHandlerในโมดูลhttp.serverอีกทั้งโมดูล
cgiเดิมก็มีแค่ฟังก์ชันไม่กี่ตัวไว้ parse ข้อมูลฟอร์ม HTMLเข้าใจได้ถ้าจะวิจารณ์ว่าการเอาโมดูล
cgiออกจาก Python standard library ไม่ดี แต่ JS ที่มักถูกยกมาเปรียบเทียบก็แทบไม่มี standard library อยู่แล้วและ Lua เองก็ไม่มีโมดูล CGI ใน stdlib เช่นกัน
ส่วนตัวชอบ PHP หรือ JS มากกว่า
ในกรณีแบบนี้สะดวกเพราะในกล่องมี JIT มาให้พร้อมใช้
ใช้ Python มาตั้งแต่ 1.6 แต่ส่วนใหญ่เอาไปทำ OS scripting
เมื่อก่อนเคยใช้ Tcl ผูกกับ Apache หรือ IIS module แล้วต้องวนกลับไปเขียนโมดูลใหม่ด้วย C อยู่เรื่อย ๆ (ช่วงปี 1999~2003)
ถ้า CGI script ใช้ CPU 400 มิลลิวินาที เวลาตอบสนองของ endpoint นั้นก็จะอย่างน้อยเท่านั้นด้วย ซึ่งกระทบ usability
ไม่นานมานี้ใช้มินิเซิร์ฟเวอร์ราคา 350 ดอลลาร์ รัน golang binary, rabbitmq, redis และ MySQL แล้วรับโหลดต่อเนื่องได้ 5,000 req/s บนเครื่องเดียวกัน
ถ้าคิด 24 ชั่วโมงก็เท่ากับ 400 ล้านคำขอ
ยิ่งทำให้รู้สึกว่าเครื่องมือฟรีสมัยนี้ยอดเยี่ยมมาก
ถึงอย่างนั้นก็ยังคิดว่าค่าใช้จ่ายบนคลาวด์สูงเกินไป
แน่นอนว่าเทียบกันตรง ๆ ไม่ได้ แต่ความพอใจจากการพัฒนาและจูนระบบเองบนเซิร์ฟเวอร์ในชั้นใต้ดินที่บ้านมีค่าสูงมาก
มีหลายกรณีที่ใช้ก้อน microservices บน Kubernetes แล้วความเร็วในการพัฒนาช้าลง 10 เท่า
หลายคนเหมือนไม่รู้ว่าเซิร์ฟเวอร์ไม่ใช่เครื่องที่ประมวลผลได้แค่ 1 request ต่อวินาที
ความจริงคือยอมจ่าย overhead มหาศาลเพียงเพราะ Google ทำแบบนั้น
ตัวผมเองก็คิดว่าน่าจะต้องเขียนบทความเรื่องสถาปัตยกรรม "modular monolith" ที่เหมาะกับทีมของเรา
เคยอยากโฮสต์ side project เองที่บ้าน แต่มีความเสี่ยงทั้งไฟดับ, ISP ล่ม, เข้าเครื่องจากระยะไกลไม่ได้, ฮาร์ดดิสก์พัง ฯลฯ
พอคิดรวมเวลาของตัวเองแล้ว ผลประโยชน์ทางเศรษฐกิจก็ไม่ชัดเจน
บริการคลาวด์ได้ประโยชน์จาก economies of scale จึงเป็นทางเลือกที่สมเหตุสมผลในโลกจริง
ไม่จำเป็นต้องใช้คลาวด์เสมอไป จะเช่า dedicated server จาก hosting provider ก็ได้
แน่นอนว่ามักมีข้อจำกัดเรื่องแบนด์วิดท์/ทราฟฟิก
ที่คลาวด์กลายเป็นกระแสหลักก็เพราะ VC และนักลงทุนถือหุ้นในบริษัทเหล่านี้ หรือไม่ก็เพราะความกังวลว่า "วันหนึ่งทราฟฟิกอาจพุ่งแบบไร้ขีดจำกัด"
นักขายคลาวด์เก่งมากในการใช้ประโยชน์จากความกังวลของนักลงทุน
ไม่ได้มีแต่คลาวด์เท่านั้น
ในบริการจริง สาเหตุที่ค่า VM แพงไม่ใช่เพราะต้องใช้ compute แรง แต่เพราะต้องการ local disk ขนาดมหาศาล
ไม่จำเป็นต้องใช้พลังประมวลผลสูง แค่มีฮาร์ดดิสก์ 20TB สี่ลูกกับ CPU พอประมาณ ก็สร้างบริการที่น่าสนใจได้มากแล้ว
แต่บนคลาวด์แทบหา configuration แบบนี้ไม่ได้เลย
ถ้าใน
cgi-binต้องเข้าถึง DB ก็มีความยุ่งยากตรงที่ทุกครั้งจะต้องสร้าง DB connection ใหม่ทุกคำขอถ้าโค้ดทำงานแบบอยู่ในหน่วยความจำตลอด (เช่น fastcgi) ก็ไม่ใช่แค่ลดเวลาเริ่มต้น แต่ยังทำ connection pool ของ DB หรือเก็บ connection ถาวรต่อ thread ได้ด้วย
พอรันในสเกลใหญ่ จำนวน DB connection จะมากเกินไปจน DB รับภาระหนัก
ด้วยเหตุผลอย่าง "python เป็น single-threaded เลยต้องใช้หลาย process และ python ช้าจึงต้องเพิ่ม process อีก" ทำให้สุดท้ายต้องรันหลาย process จำนวนมาก
ท้ายที่สุดก็ต้องแยก shared connection pool (เช่น pg bouncer) ออกไปไว้นอก python process และต้องจูนอีกหลายอย่าง
แล้วสุดท้ายเคยมีประสบการณ์ว่าพอเขียนใหม่ด้วยภาษาที่ควบคุมได้ง่ายกว่า (รองรับ multithread และประสิทธิภาพดีกว่า) ระบบกลับง่ายขึ้นมาก
เพราะแบบนี้ CGI จึงค่อย ๆ พัฒนาไปสู่โมเดลที่เก็บสถานะบางอย่างข้ามคำขอได้ (เช่น fastcgi)
ตามธรรมเนียมเดิม บางทีก็รัน daemon แยกให้ทำหน้าที่เป็น proxy และถ้าใช้ Unix socket ก็มีประสิทธิภาพสูงกว่า TCP/IP มาก
มีคนเสนอให้ใช้ UDP
สำหรับผม
inetdก็คือ CGI นั่นเองมันทำให้อินเทอร์เน็ตสนุกขึ้นมาก
เป็นยุคที่เอา
inetdไปรัน shell script หลายแบบได้ แม้กระทั่ง HTTP ที่เขียนด้วย Bash ล้วน ๆVPS เก่า ๆ หรือโน้ตบุ๊กที่ไม่ได้ทำ backup หรือ version control อาจหายไปแล้ว แต่ก็เป็นความทรงจำที่สนุก
การ deploy ก็ง่ายแค่ Makefile +
scpและการทดสอบก็เขียนเป็น Bash script ที่ใช้netcatกับgrepได้รู้สึกว่าเป็นยุคที่น่าอยู่จริง ๆ
ความรู้สึกคือการทำ hello world app ได้ 2400 rps (requests per second) บนฮาร์ดแวร์ปัจจุบันไม่ได้ดูน่าประทับใจเท่าไร
โค้ดก็ไม่ได้ง่ายขึ้นกว่าเดิม เลยสงสัยว่าจริง ๆ แล้วเรายอมเสียประสิทธิภาพไปเพื่ออะไร
ถ้าไม่จำเป็นต้องรองรับเกิน 2000 rps ก็ไม่ถือว่าเป็นปัญหา
ประเด็นคือเว็บไซต์ที่ต้องการทราฟฟิกระดับนั้นมีน้อยมาก
แม้ตัวเลขจะไม่สูงมาก แต่ในความเป็นจริงก็เพียงพอสำหรับหลายสภาพแวดล้อม
มากพอจะรับมือกับสิ่งที่นักพัฒนา HN เรียกว่า 'hug of death' (ทราฟฟิกพุ่งฉับพลันในช่วงเวลาหนึ่ง) ได้ด้วย
ที่บริษัทของเรา ยังใช้วิธีเอา internal web app แบบง่าย ๆ ขึ้นผ่านไดเรกทอรี
cgi-binอยู่จนถึงตอนนี้ถ้าใช้แบบเรียบง่าย ประสิทธิภาพในการพัฒนาดีมาก
แม้จะเป็น CGI ก็ไม่จำเป็นต้อง
printhttp/1.0 ตรง ๆ เอง และสามารถใช้Pythonwsgiref.handlers.CGIHandlerเพื่อรัน WSGI app ใดก็ได้ในรูปแบบ CGI scriptตัวอย่างโค้ด Flask ก็ง่ายมากดังนี้
ในงานจริงใช้ cgi plugin ของ
uwsgiเพื่อรันสคริปต์ให้ความรู้สึกว่าเรียบง่ายและยืดหยุ่นกว่าการใช้
mod_cgiบน Apache หรือ lighttpd มากและเพราะ
uwsgiรันในระดับ system unit จึงใช้ทั้ง hardening และ sandboxing ของsystemdได้ครบอีกข้อดีคือในการจัดการ CGI ของ
uwsgiสามารถกำหนด interpreter แยกตามชนิดไฟล์ได้เวลาจนถึงการส่งไบต์แรกอยู่ที่ 250~350ms ซึ่งเพียงพอสำหรับการใช้งานของเรา
เอกสารเกี่ยวกับ uwsgi cgi
ข้อมูลที่ว่า
wsgiref.handlers.CGIHandlerยังไม่ deprecated มีประโยชน์มากเธรดที่เกี่ยวข้องซึ่งคุยกันเมื่อวาน ลิงก์
ช่วงหลังเวลาใช้ Apache กับ side project ก็พบว่าฟีเจอร์
.htaccessมีประโยชน์มากแค่วางไฟล์
.htaccessไว้ในไดเรกทอรีไหนก็ได้ แล้วจะโหลดการตั้งค่าเซิร์ฟเวอร์เพิ่มเติมในทุกคำขอของไดเรกทอรีนั้นเอกสารทางการของ htaccess
เมื่อก่อนมักเลี่ยง
.htaccessเพราะมี overhead จากการเข้าดิสก์ทุกคำขอ และแนะนำให้รวมไว้ใน main config ให้มากที่สุดแต่ทุกวันนี้ SSD และ RAM ก็มีเหลือเฟือ แม้แน่นอนว่ายังเสียประสิทธิภาพเล็กน้อย แต่ CPU ก็แรงพอจนคนส่วนใหญ่แทบไม่ต้องสนใจ
[โปรเจกต์ของผม StaticPatch][https://github.com/StaticPatch/StaticPatch/tree/main] ก็ใช้อยู่แบบนี้แล้ว
คำพูดขึ้นชื่อของ Rasmus Lerdorf ผู้สร้าง PHP
"ฉันไม่ใช่โปรแกรมเมอร์จริงจังหรอก แค่ทำให้มันพอใช้งานได้แล้วก็ไปต่อ โปรแกรมเมอร์จริงจะบอกว่า 'มี memory leak เยอะเกิน ต้องแก้' ส่วนฉันก็แค่ restart apache ทุก ๆ 10 คำขอ"
หลังจากนั้น PHP ก็เดินทางมาไกลมาก เติบโตขึ้นอย่างมากพร้อมกับแก้ข้อผิดพลาดยุคแรก ๆ
ยังมีเกร็ดอีกว่าเขาเคยพูดว่า "PHP 8 ยิ่งทำให้ผมเขียนโค้ดน้อยลงเท่าไรยิ่งดี"
น่าจะให้ Apache เฝ้าดู file system แล้วอ่านใหม่เฉพาะตอนที่ไฟล์เปลี่ยนก็พอ แต่กลับทำให้ต้องมี disk access ที่ไม่จำเป็นในทุกคำขอ ซึ่งเข้าใจยาก
และผลก็คือทำให้ 99.99% ของคำขอ http ช้าลง
ช่วงหลังในการทำงานก็คิดถึงโครงสร้างแบบนี้สำหรับการทำ prototype เร็ว ๆ
ถ้าไม่ใช่รูปแบบ fastcgi ภาษากลุ่ม JIT จะติดคอขวดที่
importเว็บเซิร์ฟเวอร์
h2oที่เคยใช้เองนั้นตั้งค่า handler สำหรับmrubyและ fast-cgi ได้ง่ายมาก เหมาะกับงานสคริปต์ภายในสุด ๆเอกสาร fastcgi ของ h2o
อีกข้อดีหนึ่งคือเหมาะเวลาต้องเปิดให้ลูกค้าเพิ่ม custom code เพื่อขยาย local software ด้วยตัวเอง
เช่น เมื่อก่อนถ้าจะขยายระบบต้องใช้ MCP แต่ตอนนี้แค่ทำคำขอแบบมีโครงสร้างผ่าน CGI ก็จบแล้ว
ถ้าจะใช้ในสภาพแวดล้อม end-user แนวคิดเอา MCP front ไปเชื่อมกับโปรแกรม CGI ก็ดูน่าสนใจ
บริการ MCP เองก็น่าจะทำเป็น CGI ได้เหมือนกัน
รู้สึกว่ายังต้องไปอ่านสเปกเพิ่ม
ก็สงสัยเหมือนกันว่า fastcgi ทำให้ข้อดีเกือบทั้งหมดของ cgi หายไปหรือเปล่า
เมื่อก่อนเคยใช้โปรแกรม C กับ CGI โดยตรง
ตอนนั้นยังไม่มีทั้งเครื่องที่มีคอร์เกินร้อยหรือ RAM มากพอ และยังทำงานได้บนหน่วยความจำสูงสุดแค่ประมาณ 1GB
พอนึกถึงสิ่งที่ทำได้ในยุคนั้น ก็เลยมั่นใจว่าสมัยนี้น่าจะยิ่งง่ายกว่าเดิมมาก
หลังจากจำเป็นต้องทำ load balancing แล้วจึงเริ่มขยายต่อได้ยาก แต่ก่อนหน้านั้นก็ถือว่าทำงานได้ดีพอสมควร
และเพื่ออ้างอิงเพิ่มเติม ตอนนั้นฝั่ง front กับ back office แยกเป็น executable คนละ 2 ตัว