เหตุผลและวิธีที่ Dropbox ย้ายจาก Nginx ไปสู่ Envoy
(dropbox.tech)<p>บทความที่อธิบายข้อดีของ Envoy เมื่อเทียบกับ Nginx ได้อย่างดี โดย Dropbox ซึ่งกำลังใช้งานการเชื่อมต่อพร้อมกันหลายสิบล้าน รายการคำขอต่อวินาทีหลายล้านครั้ง และแบนด์วิดท์ระดับเทราไบต์<br />
<br />
เดิมที: nginx (เวอร์ชันโอเพนซอร์ส) + python2 + Jinja2 + YAML <br />
→ เปลี่ยนแค่อย่างเดียวก็ต้อง redeploy ทั้งหมด<br />
→ ส่วนที่เป็น dynamic พัฒนาด้วย Lua<br />
→ ลอจิกที่ซับซ้อนประมวลผลใน Bandaid ซึ่งเป็นพร็อกซีที่พัฒนาขึ้นเองบน Go<br />
<br />
แม้จะทำงานได้ดีมาตลอดเกือบ 10 ปี แต่ก็ไม่ค่อยเหมาะกับสภาพแวดล้อมปัจจุบันแล้ว<br />
→ API ภายในและภายนอก (แบบไม่เปิดเผยสู่สาธารณะ) กำลังค่อยๆ ย้ายจาก REST ไปเป็น gRPC จึงต้องการความสามารถ transcoding ที่พร็อกซีรองรับ<br />
→ Protocol Buffers กลายเป็นมาตรฐานการนิยามบริการภายใน<br />
→ ซอฟต์แวร์ทั้งหมด build และ test ด้วย Bazel โดยไม่ขึ้นกับภาษา<br />
→ พนักงานมีส่วนร่วมอย่างเข้มข้นกับโอเพนซอร์สคอมมูนิตี้ของโครงการโครงสร้างพื้นฐานหลักหลายตัว<br />
<br />
Nginx ยังมีต้นทุนในการดูแลรักษาสูงในเชิงปฏิบัติการด้วย<br />
→ ลอจิกการสร้าง config ยืดหยุ่นเกินไปและกระจายอยู่ใน YAML, Jinja2, Python<br />
→ การมอนิเตอร์เป็นการผสมกันของ Lua / การ parse log / การมอนิเตอร์ระดับระบบ<br />
→ เมื่อพึ่งพา third-party module มากขึ้น ก็ส่งผลต่อเสถียรภาพ/ประสิทธิภาพ และมีต้นทุนจากการอัปเกรดบ่อยครั้ง<br />
→ การ deploy และการจัดการโปรเซสของ nginx เองก็แตกต่างจากบริการอื่นมาก โดยพึ่งพาสิ่งที่ต่างจากระบบพื้นฐานอย่าง syslog, logrotate เป็นต้น<br />
<br />
ดังนั้นจึงตัดสินใจหาตัวแทน Nginx เป็นครั้งแรกในรอบ 10 ปี<br />
<br />
* ทำไมไม่ไปใช้ Bandaid (พร็อกซีที่ Dropbox พัฒนาขึ้นเองบน Go) แทน? <br />
→ Go ใช้ทรัพยากรมากกว่า C/C++ <br />
→ TLS stack ของ Go ไม่รองรับ FIPS (มาตรฐานการประมวลผลข้อมูลของรัฐบาลกลางสหรัฐฯ)<br />
→ เป็นเครื่องมือภายใน จึงไม่ได้รับการสนับสนุนจากคอมมูนิตี้ภายนอก <br />
<br />
ปัจจุบัน: กำลังย้ายไปสู่โครงสร้างพื้นฐานทราฟฟิกที่ใช้ Envoy เป็นฐาน <br />
<br />
----- จุดที่ Envoy ดีกว่า Nginx ------<br />
<br />
* ประสิทธิภาพ *<br />
<br />
สถาปัตยกรรมของ Nginx เป็นแบบ event-driven / multi-process รองรับ SO_REUSEPORT & EPOLLEXCLUSIVE<br />
แม้จะอิง event loop แต่ก็ไม่ได้ non-blocking อย่างสมบูรณ์ เวลามีการเปิดไฟล์หรือ logging event loop อาจหยุดได้ (แม้จะเปิดใช้ aio, aio_write และ threadpool แล้วก็ตาม)<br />
สิ่งนี้ทำให้เกิด tail latency และบางครั้งมีดีเลย์ระดับหลายวินาที<br />
<br />
Envoy ก็มีสถาปัตยกรรมแบบ event-driven คล้ายกัน แต่ใช้ thread แทน process <br />
รองรับ SO_REUSEPORT (พร้อมรองรับ BPF filter) และ event loop ผ่าน libevent <br />
ไม่มี blocking I/O บน event loop และ event logging ก็ทำแบบ non-blocking เช่นกัน<br />
<br />
ในทางทฤษฎีดูเหมือนจะมีลักษณะประสิทธิภาพใกล้เคียงกัน และผลทดสอบ workload ส่วนใหญ่ก็คล้ายกันจริง<br />
แต่ Nginx มี latency ที่ปลายหางสูงกว่า เพราะ event loop หยุดเมื่อมี I/O มาก<br />
<br />
หากไม่มีการเก็บสถิติ Nginx จะมีประสิทธิภาพใกล้เคียง Envoy แต่เครื่องมือเก็บสถิติภายในที่ทำด้วย Lua ทำให้ Nginx ช้าลง 3 เท่าในการทดสอบ high-RPS (สาเหตุคือ `lua_shared_dict` ที่ซิงโครไนซ์ด้วย mutex) แม้วิธีเก็บสถิติของ Dropbox เองก็มีปัญหา แต่ก็เลิกพยายามเขียนใหม่ให้มีประสิทธิภาพกว่าเดิม เพราะคาดว่าถ้าทำ instrumentation ภายใน Nginx จะทำให้อัปเกรดยากขึ้นในอนาคต<br />
<br />
อย่างไรก็ดี ปัญหาเหล่านี้ไม่มีใน Envoy จึงทำให้หลังการย้ายสามารถปล่อยคืนเซิร์ฟเวอร์ได้สูงสุด 60% ของจำนวนที่ Nginx เดิมใช้อยู่<br />
<br />
* Observability *<br />
<br />
Nginx รุ่นฟรีมีเพียง 7 สถิติจากโมดูล stub status <br />
ซึ่งแน่นอนว่าไม่เพียงพอ จึงต้องผูก `log_by_lua` handler เพื่อให้มีสถิติเพิ่มขึ้น<br />
นอกจากนี้ยังมี parser ของ `error.log` สำหรับส่งออกข้อมูลข้อผิดพลาด และมี exporter แยกต่างหากเพื่อส่งออกสถานะภายในของ nginx<br />
<br />
การตั้งค่า Envoy แบบพื้นฐานให้เมตริกได้หลายพันรายการในฟอร์แมต Prometheus <br />
ตั้งแต่ข้อมูลทราฟฟิกของพร็อกซีไปจนถึงสถานะภายในของเซิร์ฟเวอร์<br />
รวมทั้งสถิติตามคลัสเตอร์/อัปสตรีม/virtual host และสถิติ TCP/HTTP/TLS downstream ตาม listener ฯลฯ<br />
<br />
พร้อมกับสถิติที่หลากหลายเหล่านี้ Envoy ยังสามารถเสียบ Tracing Provider ได้แบบปลั๊กอิน<br />
จึงมีประโยชน์ไม่เฉพาะทีมทราฟฟิก แต่รวมถึงนักพัฒนาแอปพลิเคชันด้วย<br />
<br />
ท้ายที่สุด Envoy ยังสามารถสตรีม access log ผ่าน gRPC ได้<br />
ซึ่งช่วยลดภาระในการรองรับ syslog-to-hive bridge ของทีมทราฟฟิกลง<br />
การรัน gRPC service ทั่วไปนั้นง่ายและปลอดภัยกว่าการต่อ custom TCP/UDP listener มาก<br />
<br />
* Integration *<br />
<br />
การผสานรวมของ Nginx มีความเป็น Unix สูงมาก โดย configuration ค่อนข้าง static<br />
ต้องพึ่งพาไฟล์สำหรับ config, TLS certificate, allowlist/blocklist เป็นต้น<br />
มันเรียบง่ายและเข้ากันได้ย้อนหลัง จึงทำ automation ได้ด้วย shell script ไม่กี่ตัว<br />
แต่เมื่อระบบใหญ่ขึ้น ความสามารถในการทดสอบและการทำให้เป็นมาตรฐานก็สำคัญมากขึ้นเรื่อยๆ<br />
<br />
Envoy มีแนวทางของตัวเองสำหรับการผสานรวมลักษณะนี้<br />
มี API ที่เรียกว่า xDS ซึ่งสนับสนุนการใช้ protobuf และ gRPC<br />
Envoy จะค้นหา dynamic resource ผ่านการ query ไปยัง xDS เหล่านี้<br />
<br />
- ตอนนี้ xDS กำลังวิวัฒน์ไปไกลกว่า Envoy ภายใต้ชื่อ Universal Data Place API (UDPA) เพื่อมุ่งเป็นมาตรฐานโดยพฤตินัยของ L4/L7 load balancer และจากประสบการณ์ของเราก็เป็นไปได้ดี ขณะนี้กำลังพยายามใช้ UDPA กับ Katran eBPF/XDP L4 load balancer ที่ไม่ใช่ของ Envoy ด้วย<br />
<br />
สำหรับ Dropbox ที่ภายในเชื่อมต่อบริการกันผ่าน gRPC อยู่แล้ว จึงเหมาะกว่ามาก<br />
<br />
* Configuration *<br />
<br />
Nginx มีจุดเด่นใหญ่คือไฟล์คอนฟิกที่มนุษย์อ่านง่าย <br />
แต่ข้อดีนี้ค่อยๆ หายไปเมื่อคอนฟิกซับซ้อนขึ้นและถูกสร้างอัตโนมัติ<br />
ที่ Dropbox คอนฟิกถูกสร้างผ่าน Python2, Jinja2, YAML ทำให้ data model พันกันและซับซ้อนมากขึ้นด้วย<br />
<br />
Envoy มี data model แบบรวมศูนย์สำหรับ configuration ค่าตั้งทั้งหมดถูกนิยามไว้ใน Protocol Buffer ทำให้แก้ปัญหาการทำ data modeling และเพิ่มข้อมูลชนิดให้กับค่าคอนฟิกได้<br />
เนื่องจากภายใน Dropbox ใช้ protobuf อย่างแพร่หลายอยู่แล้ว การผสานรวมจึงง่าย <br />
<br />
* Extensibility * <br />
<br />
การขยายความสามารถของ Nginx ต้องเขียน C module ซึ่งการเขียนโมดูลให้ปลอดภัยต้องอาศัยวิศวกรอาวุโส แม้จะมีอินเทอร์เฟซ Perl / JS สำหรับทำโมดูลแบบเบากว่า แต่ก็จำกัดมาก ดังนั้นวิธีที่ใช้กันทั่วไปที่สุดคือผ่าน `lua-nginx-module` <br />
<br />
กลไกหลักในการขยายของ Envoy คือ C++ plugin ซึ่งเอกสารอาจไม่ดีเท่า nginx แต่ตัวมันง่ายมาก เพราะมีอินเทอร์เฟซที่สะอาดและใส่คอมเมนต์ไว้ดี รวมถึงใช้ภาษา C++14 และ standard library <br />
<br />
จุดต่างสำคัญของ Envoy เมื่อเทียบกับเว็บเซิร์ฟเวอร์อื่นคือการรองรับ WebAssembly (WASM)<br />
ซึ่งทำให้รองรับการพัฒนาส่วนขยายด้วยภาษาต่างๆ อย่าง Rust ได้ <br />
แม้ Dropbox ยังไม่ได้ใช้ WASM แต่หากวันหนึ่งมีการรองรับ Go SDK for proxy-wasm ก็อาจเปลี่ยนแปลงได้<br />
<br />
* Building and Testing *<br />
<br />
Nginx โดยพื้นฐานใช้การตั้งค่าแบบ custom shell และการ build แบบ make ซึ่งเรียบง่ายและยอดเยี่ยม แต่การนำไปผสานกับ monorepo ที่ build ด้วย Bazel ต้องใช้ความพยายามมากพอสมควร <br />
Nginx มี integration test ที่เขียนด้วย Perl แต่ไม่มี unit test<br />
<br />
Envoy มีระบบ build แบบ Bazel อยู่แล้ว และสามารถผสานเข้ากับ monorepo ของเราได้ง่าย<br />
รองรับ unit test ที่ใช้ gtest/gmock และ framework สำหรับ integration test<br />
<br />
* Security *<br />
<br />
โค้ดของ Nginx มีขนาดเล็กมากและมี dependency ภายนอกน้อย จึงมีช่องโหว่ด้านความปลอดภัยไม่มากนัก<br />
<br />
Envoy มีโค้ดจำนวนมาก จึงดูเหมือนมีพื้นที่ให้โจมตีได้มากกว่า ด้วยเหตุนี้ Envoy จึงพึ่งพาแนวปฏิบัติด้านความปลอดภัยสมัยใหม่จำนวนมาก เช่น AddressSanitizer, ThreadSanitizer, MemorySanitizer เป็นต้น <br />
<br />
* Features * <br />
<br />
ส่วนนี้มีความเห็นเชิงอัตวิสัยอยู่มาก โปรดใช้วิจารณญาณ<br />
<br />
Nginx เริ่มต้นจากการเป็นเว็บเซิร์ฟเวอร์สำหรับให้บริการ static file ด้วยทรัพยากรน้อยมาก <br />
กล่าวคือหน้าที่หลักคือ static serving, caching, range caching<br />
แต่ในมุมของพร็อกซี Nginx ยังขาดฟีเจอร์หลายอย่างที่โครงสร้างพื้นฐานยุคนี้ต้องการ <br />
ไม่รองรับการเชื่อมต่อ HTTP/2 กับ backend, ทำ gRPC proxy แบบหลายการเชื่อมต่อไม่ได้, และทำ gRPC transcoding ก็ไม่ได้ เป็นต้น<br />
ด้วยโมเดลไลเซนส์แบบ open-core ฟีเจอร์สำคัญบางอย่างจึงไม่มีใน "community version"<br />
<br />
Envoy เริ่มต้นมาเพื่อเป็น ingress/egress proxy โดยตรง และถูกใช้มากในสภาพแวดล้อมที่มีโหลด gRPC สูง<br />
ความสามารถด้านเว็บเซอร์วิสยังอยู่ในระดับพื้นฐานมาก ไม่รองรับ file serving, caching ยังอยู่ระหว่างพัฒนา, และยังไม่รองรับ brotli เป็นต้น <br />
สำหรับสภาพแวดล้อมเช่นนี้ก็ยังมีการใช้ชุดติดตั้ง Nginx ที่ใช้ Envoy เป็น upstream cluster อยู่ <br />
คาดว่าเมื่อ Envoy รองรับ HTTP cache ได้แล้ว ก็จะย้ายสภาพแวดล้อมการให้บริการ static ลักษณะนี้ได้ด้วย <br />
<br />
Envoy รองรับความสามารถที่เกี่ยวข้องกับ gRPC จำนวนมาก<br />
- gRPC proxying<br />
- HTTP/2 to backends<br />
- gRPC → HTTP bridge (+ reverse.) <br />
- gRPC-WEB <br />
- gRPC JSON transcoder<br />
<br />
นอกจากนี้ Envoy ยังใช้เป็น outbound proxy ได้ด้วย <br />
- Egress Proxy<br />
- Third-party software service discovery with Courier gRPC library <br />
<br />
* Community *<br />
<br />
การพัฒนาของ Nginx มีลักษณะรวมศูนย์และส่วนใหญ่ซ่อนอยู่ <br />
การพัฒนาของ Envoy เปิดกว้างและกระจายศูนย์ ดำเนินผ่าน GitHub issue/PR และยังคึกคักผ่านเมลลิงลิสต์/Slack เป็นต้น </p><p>----- สถานะการย้ายระบบของ Dropbox ในปัจจุบัน -----<br />
<br />
ได้รัน Nginx และ Envoy ควบคู่กันมาครึ่งปีแล้ว พร้อมค่อยๆ ย้ายทราฟฟิกผ่าน DNS <br />
ไม่ได้ย้ายได้โดยไม่มีปัญหาเลย ยังมีปัญหาเล็กๆ น้อยๆ อยู่บ้าง แต่ไม่มีเหตุขัดข้องร้ายแรง<br />
มีการสรุปวิธีแก้ปัญหาที่พบจากพฤติกรรมแบบ "unusual" หรือ "non-RFC" ไว้ด้วย (รายละเอียดอยู่ในบทความต้นฉบับ)<br />
<br />
** สิ่งที่จะทำต่อไป **<br />
<br />
- HTTP/3 : Envoy เริ่มรองรับแบบทดลองแล้ว และมีแผนจะทดลองเมื่ออัปเกรด Linux kernel เพื่อเร่งความเร็ว UDP<br />
- internal xDS-based load balancer และ Outlier Detection<br />
- ส่วนขยาย Envoy ที่ใช้ WASM <br />
- แทนที่ Bandaid (พร็อกซีที่พัฒนาด้วย Go) ด้วย Envoy <br />
- นำ Envoy Mobile ไปใช้กับแอปมือถือด้วย</p>
3 ความคิดเห็น