เทคโนโลยี JIT WireGuard
(fly.io)- Fly.io เปลี่ยนมาใช้วิธีเพิ่ม peer เข้าไปในเคอร์เนลตอนที่มีการเชื่อมต่อ โดยไม่ติดตั้ง peer ไว้ล่วงหน้า เพื่อลดภาระด้าน state ของ WireGuard gateway ขณะยังคงการสื่อสารโดยตรงระหว่าง
flyctlกับ Fly Machines - โฟลว์เดิมคือ GraphQL API ส่งการตั้งค่า peer ผ่าน NATS RPC แล้ว
wggwdลงทะเบียนไว้ใน SQLite และ Linux kernel WireGuard จากนั้นflyctlจึงเชื่อมต่อ - เมื่อ ข้อความ NATS สูญหาย มาซ้อนกับการสร้าง peer แบบใช้ครั้งเดียวของงาน CI ทำให้มี peer ที่ไม่ถูกนำกลับมาใช้ซ้ำสะสมบน gateway หลายแสนรายการ และทำให้การทำงานของเคอร์เนลกับการโหลดตอนรีบูตช้าลง
- วิธีใหม่จับแพ็กเก็ต
handshake initiationจาก BPF filter หรือเส้นทางรับ WebSockets แล้วถอดรหัสบางส่วนของ Noise handshake เพื่อระบุ public key จากนั้นดึงเฉพาะ peer ที่จำเป็นผ่าน HTTP API ภายใน - หลังนำไปใช้จริงไม่กี่สัปดาห์ จำนวน peer เก่าแทบหายไป และ gateway สามารถจัดการการตั้งค่า peer กับการรีบูตได้เร็วขึ้นโดยมี state น้อยลง
วิธีที่ Fly.io ใช้ WireGuard
- Fly.io รันคอนเทนเนอร์เป็น VM ที่ใช้ Firecracker และใช้ WireGuard ในหลายจุดราวกับเป็นส่วนหนึ่งของ API สำหรับลูกค้า
- เมื่อ
flyctlทำงาน มันจะสร้าง TCP/IP stack ที่มีที่อยู่ IPv6 ของตัวเอง และสื่อสารโดยตรงกับ Fly Machines ในเครือข่ายของ Fly.io - แนวทางนี้ทำให้ฟีเจอร์อย่าง remote Docker builder ถูกนำเสนอได้ง่ายเหมือนอยู่ใน LAN เดียวกัน แต่การทำให้รันต่อเนื่องอย่างเสถียรนั้นยากกว่า
- ในท้ายที่สุด Fly.io จึงเปลี่ยนเส้นทางหลักเป็น WireGuard-over-WebSockets
โฟลว์ provisioning ของ gateway แบบเดิม
- Fly.io เชื่อมต่อการเชื่อมต่อ WireGuard ที่เข้ามายังเซิร์ฟเวอร์ gateway หลายแห่งทั่วโลกเข้ากับ private network ที่เหมาะสม
- เมื่อ
flyctlต้องสื่อสารกับ Fly Machine เพื่อ build คอนเทนเนอร์, เปิด SSH console, คัดลอกไฟล์ หรือ proxy service มันจะรันหรือเชื่อมต่อกับโปรเซส agent ในเบื้องหลัง - เมื่อ agent รันครั้งแรก จะสร้างการตั้งค่า WireGuard peer ใหม่จาก GraphQL API
- การตั้งค่า peer ประกอบด้วย public key และที่อยู่ที่จะเชื่อมโยง
- API ส่งการตั้งค่านั้นไปยัง gateway ที่เหมาะสมผ่าน RPC ของระบบ messaging NATS
wggwdบน gateway รับการตั้งค่า บันทึกลง SQLite เพิ่มเข้าเคอร์เนลผ่านไลบรารี WireGuard Go แล้วตอบ API ว่าติดตั้งเสร็จแล้ว- เมื่อ API ส่งการตั้งค่ากลับในคำขอ GraphQL แล้ว
flyctlจะเชื่อมต่อด้วย WireGuard peer ที่ติดตั้งอยู่บน gateway แล้ว
ทำไมโครงสร้างเดิมจึงช้าลง
- NATS เร็ว แต่ไม่รับประกันการส่งมอบ จึงยากที่จะใช้เป็นฐาน API ที่เชื่อถือได้
- Fly.io ลดการใช้ NATS ภายในลง เช่น API ภายใน
flydถูกเปลี่ยนจากแบบที่ใช้ NATS เป็นแบบที่ใช้ HTTP - การลดการใช้ NATS ทำให้ WireGuard gateway ดีขึ้น แต่ยังไม่เพียงพอ
- Fly.io ลดการใช้ NATS ภายในลง เช่น API ภายใน
- WireGuard peer ที่สร้างหลังจาก
flyctlจบการทำงานยังคงอยู่บน gateway และไม่มีกระบวนการล้าง peer เก่า- มีเหตุผลที่เลือกไม่ลบ peer เพราะผู้ใช้อาจกลับมา deploy อีกครั้งในวันถัดไป หรือ debug ด้วย
fly ssh console - แต่ peer ส่วนใหญ่ถูกสร้างจาก งาน CI ที่ไม่มี persistent storage และไม่สามารถเชื่อมต่อกลับด้วย peer เดิมในการรันครั้งถัดไปได้ จึงสร้าง peer ใหม่ทุกครั้ง
- มีเหตุผลที่เลือกไม่ลบ peer เพราะผู้ใช้อาจกลับมา deploy อีกครั้งในวันถัดไป หรือ debug ด้วย
- ผลคือ gateway มี peer หลายแสนรายการที่อาจไม่ถูกใช้ซ้ำ
- เมื่อจำนวน peer เก่าเพิ่มขึ้น การทำงานของ kernel WireGuard จึงช้ามาก
- โดยเฉพาะขั้นตอนโหลด peer ทั้งหมดกลับเข้าเคอร์เนลหลังรีบูตเซิร์ฟเวอร์ gateway นั้นช้ามาก
- ยังเกิด kernel panic บางครั้งด้วย
การออกแบบให้ติดตั้ง peer ลงเคอร์เนลเฉพาะเมื่อจำเป็น
- การเก็บประวัติ WireGuard peer ทั้งหมดไว้ใน SQLite เดียวไม่ใช่เรื่องยาก แต่การคง peer ทั้งหมดไว้ใน Linux kernel กลายเป็นคอขวด
- Fly.io เลือกให้ gateway ดึง peer ตามต้องการ จาก API แทนการ push การตั้งค่าไปยัง gateway
- หากเพิ่ม peer เข้าเคอร์เนลเฉพาะเมื่อ client พยายามเชื่อมต่อ peer เก่าก็สามารถถูกลบออกจากเคอร์เนลได้ทุกเมื่อ
- แม้ peer ที่ถูกลบไปแล้วก็เพียงดึงกลับมาและติดตั้งใหม่ในการเชื่อมต่อครั้งถัดไป จึงลดความจำเป็นที่ gateway ต้องถือ state ระยะยาวไว้ตลอด
- อย่างไรก็ตาม Linux kernel WireGuard ไม่มี API สำหรับ subscribe event “incoming connection attempt”
วิธี implement JIT WireGuard peer
- อินเทอร์เฟซการตั้งค่า WireGuard ของ Linux kernel คือ Netlink และไลบรารีควบคุม WireGuard Go ใช้ wgctrl-go
- Fly.io ใช้ข้อเท็จจริงที่ว่าคำขอเชื่อมต่อ WireGuard เป็นแพ็กเก็ตที่ระบุได้ แล้วสร้าง event เองโดยตรงด้วย BPF filter และ packet socket
- ในเส้นทาง WebSockets WireGuard จะดึงแพ็กเก็ต WireGuard ดิบได้ง่ายกว่า
- เส้นทางนี้รับส่งแพ็กเก็ต UDP ที่ถูก framed ผ่านการเชื่อมต่อ WebSockets แบบไม่ต้อง authentication กับอินเทอร์เฟซ gateway
- Fly.io เป็นเจ้าของโค้ด daemon นี้ จึงสามารถ hook เข้ากับฟังก์ชันรับแพ็กเก็ตได้
- WireGuard ไม่มีแนวคิด “client” และ “server” แต่เป็น โปรโตคอล point-to-point ที่ peer เชื่อมต่อกันเมื่อมีทราฟฟิกจะส่ง
- ฝั่งที่เริ่มเชื่อมต่อก่อนคือ initiator ส่วนอีกฝั่งคือ responder
- ใน Fly.io โดยทั่วไป
flyctlเป็น initiator และ gateway เป็น responder
- แพ็กเก็ต UDP แรกตามเอกสาร WireGuard คือ
handshake initiationและประเภทแพ็กเก็ตถูกบันทึกเป็นข้อความธรรมดา 1 ไบต์- Fly.io จับการเชื่อมต่อขาเข้าด้วย BPF filter
udp and dst port 51820 and udp[8] = 1
- Fly.io จับการเชื่อมต่อขาเข้าด้วย BPF filter
การระบุ peer จาก Noise handshake
- WireGuard ใช้พื้นฐานจาก Noise Protocol Framework และ Noise ซ่อนตัวระบุระหว่าง handshake เพื่อ identity hiding
- ดังนั้นจึงใช้วิธีอ่านค่าบางอย่างอย่างชื่อผู้ใช้จากแพ็กเก็ตเพื่อค้นหาการตั้งค่าโดยตรงไม่ได้
- Fly.io รันบางส่วนของการเข้ารหัส Noise เพื่อถอดรหัส identity และระบุคำขอขาเข้า
- โค้ดนี้ค่อนข้างละเอียดอ่อน แต่มีประมาณ 200 บรรทัด
- อินเทอร์เฟซ Netlink ของเคอร์เนลสามารถให้ private key ของอินเทอร์เฟซแก่โปรเซสที่มีสิทธิ์ได้ จึงได้ค่าลับที่จำเป็น
- โค้ดที่เกี่ยวข้องถูกเผยแพร่ไว้ใน gist
- เมื่อผ่านขั้นตอนนี้ จะได้ feed event ของ public key ของผู้ใช้ที่พยายามเชื่อมต่อ WireGuard ไปยัง gateway
การปรับแต่งการติดตั้ง แคช และการลองซ้ำ
- gateway เก็บ rate-limit cache ไว้ใน SQLite และเมื่อพบ peer ใหม่ จะเรียก HTTP API ภายในเพื่อดึงข้อมูล peer ที่สอดคล้องกันมาติดตั้ง
- ลอจิกนี้เข้ากันได้ดีกับ daemon ขนาดเล็กเดิมที่จัดการ WireGuard บน gateway
- peer เก่าสามารถถูกลบเชิงรุกได้ด้วยงาน
cron - การ query API สำหรับ peer ใหม่อาจไม่เร็วพอที่จะตอบกลับข้อความ
handshake initiationแรกได้ทันที- WireGuard retry อย่างรวดเร็ว จึงไม่มีปัญหาต่อการทำงานเอง
- ใช้ฟีเจอร์ Linux WireGuard Netlink ที่ Jason Donenfeld แนะนำเพื่อทำให้การเชื่อมต่อเกิดขึ้นได้เร็วขึ้น
- จากข้อความ initiation ขาเข้า จะได้ 4-tuple address รวมถึง source port ชั่วคราวของ
flyctl - gateway ติดตั้ง peer ราวกับว่าตัวเองเป็น initiator และ
flyctlเป็น responder - Linux kernel เริ่มการเชื่อมต่อ WireGuard ไปยังฝั่ง
flyctlและโปรโตคอลไม่ได้พึ่งพาบทบาท server/client มากนัก - การเชื่อมต่อใหม่เกิดขึ้นได้ใกล้เคียงกับความเร็วที่ติดตั้งได้
- จากข้อความ initiation ขาเข้า จะได้ 4-tuple address รวมถึง source port ชั่วคราวของ
ผลลัพธ์หลังนำไปใช้งานจริง
- วิธีนี้รันอยู่ใน production มาเป็นเวลาหลายสัปดาห์
- จำนวน WireGuard peer เก่าที่เคยมีตั้งแต่หลักพันถึงหลักแสนต่อ gateway ลดลงจนแทบเป็น 0
- state ที่ gateway ต้องถือไว้ลดลง
- การตั้งค่า peer เร็วขึ้น
- ตอนรีบูต ไม่จำเป็นต้องโหลด peer ที่ไม่ได้ใช้งานกลับเข้าเคอร์เนลมากเท่าเดิม
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ผมไม่ค่อยเข้าใจคำบอกที่ว่า WireGuard ในเคอร์เนล Linux ไม่มี ความสามารถในการติดตั้ง peer เมื่อจำเป็น เพราะดูเหมือนว่าสามารถเพิ่ม peer ระหว่างรันไทม์ได้อยู่: https://serverfault.com/questions/1101002/wireguard-client-a...
ถ้าผมเข้าใจถูก จุดนั้นก็สายไปแล้ว และดูเหมือนเขาต้องการยืนยันตัวตนก่อนเพิ่ม peer เพื่อไม่ให้มีรายการเก่าค้างอยู่บนอินเทอร์เฟซ
ดังนั้นโครงสร้างที่เห็นจึงเหมือนวาง eBPF filter ไว้หน้าอินเทอร์เฟซ แล้วลองเชื่อมต่อเองก่อนว่าเป็นคู่สื่อสารที่ได้รับอนุมัติตามการ route ด้วยคีย์เข้ารหัสหรือไม่ จากนั้นถ้าผ่านจึงค่อยเพิ่ม peer เข้าอินเทอร์เฟซ และลบออกเมื่อหมดเวลา
peer ทั้งหมดอาจเก็บไว้ใน SQLite หรือที่คล้ายกัน แล้วค่อยติดตั้งเมื่อไคลเอนต์พยายามเชื่อมต่อ
ในมุมของผู้ให้บริการ VPN API ปัจจุบันค่อนข้างหยาบไปหน่อย แม้ในทางปฏิบัติจะมีเพียงบางส่วนของ peer เท่านั้นที่ถูกใช้งาน ณ ช่วงเวลาใดช่วงเวลาหนึ่ง แต่ถ้าจำนวน peer โตจากหลักแสนไปเป็นหลายล้าน การเก็บทั้งหมดไว้ในเคอร์เนลอินสแตนซ์เดียวก็แทบเป็นไปไม่ได้
ถ้าต้องติดตั้ง peer ไว้ล่วงหน้า สุดท้ายก็จะผูกติดกับเครื่องเซิร์ฟเวอร์เฉพาะตัว
อย่างที่บทความบอก ทุกวันนี้ก็ยังสร้างอะไรคล้ายอินเทอร์เฟซที่ต้องการได้ด้วยการดักแพ็กเก็ตแบบง่าย ๆ และเพราะ Jason ออกแบบ API ไว้ดีมาก จึงสลับทิศทางการเริ่มต้นระหว่างเซิร์ฟเวอร์กับไคลเอนต์ได้ง่ายมาก
แม้เคอร์เนลจะทิ้งข้อความเริ่มต้นตัวแรกไป ผู้ใช้ก็ยังรู้สึกเหมือนเชื่อมต่อได้อย่างลื่นไหล
Jann Horn ไปไกลกว่านั้นอีก โดยบอกว่าสามารถเก็บ แพ็กเก็ตเริ่มต้น ที่ดักจับไว้ แล้วฉีดกลับเข้าเคอร์เนลหลังติดตั้ง peer เสร็จได้ ซึ่งก็ดูเป็นไอเดียที่ดีมาก
ผมไม่คิดว่านี่เป็นอะไรที่เปลี่ยนชีวิต แต่ออกจะเป็นทริกเรียบ ๆ หลายอย่างที่คนทั่วไปน่าจะอยากรู้มากกว่า
ขั้นถัดไปคือใช้สิ่งนี้สร้าง floating peers เพื่อแยก peer ออกจาก locality อย่างสมบูรณ์ แบบนั้นผู้ใช้ก็ไม่ต้องสนใจว่า peer ถูกตั้งไว้ใน region ไหน และนี่น่าจะให้ประโยชน์เชิงผลิตภัณฑ์จริง ไม่ได้มีไว้สนุกสำหรับสายเนิร์ดอย่างเดียว
คำว่า JIT WireGuard ฟังดูแปลกนิดหน่อย ความคิดแรกของผมคือ “ทำไมล่ะ? คอขวดด้านประสิทธิภาพคือการเข้ารหัส และ JIT รายไคลเอนต์ก็ไม่น่าจะช่วยเรื่องนั้น”
ถ้าเป็นผมคงไปทาง user space ตรง ๆ ใช้พวก tokio-uring หรือ glommio เพื่อดึงประสิทธิภาพเอา
ถ้ายังฝืนดันทุกอย่างไว้ในเคอร์เนลต่อไป ก็คงชนข้อจำกัดเรื่อย ๆ เพราะ Linux ไม่ได้ถูกสร้างมาเพื่อรองรับ tunnel ที่ active พร้อมกันเป็นหลักล้าน แม้แต่การมี TCP connection หลักล้านบนเคอร์เนลเดียวก็ยังยุ่งยากเป็นบางครั้ง
ทุกครั้งที่ชนลิมิตก็ต้องมีแฮ็กใหม่ และทุกแฮ็กก็ตามมาด้วย system setting ที่ต้องปรับใช้และดูแล เครื่องมือสาย provisioning สำหรับ Linux physical server ยังตามหลังเครื่องมือพัฒนาแอปและบริการ รวมถึงเครื่องมือจัดการคอนฟิก อยู่มาก
หรือว่าผมโง่เองและเข้าใจอะไรผิดไป?
ถ้าอยากสร้าง WireGuard peer แบบ user space ในแอป Go ลองดูโปรเจ็กต์ทดลองล่าสุด https://github.com/dpeckett/noisysockets ได้
มันต่อยอดจากงานอันยอดเยี่ยมของ wireguard-go แต่พยายามทำให้เรียบง่ายและเป็น Go มากขึ้นสำหรับการใช้งานในฐานะไลบรารี
ถ้าเอาไปทำ service mesh ก็น่าจะน่าสนใจ แม้อาจรองรับหลายภาษาได้ยาก แต่ก็น่าจะทำ socket API ขึ้นมาได้
เพียงแต่ผมยังไม่เห็น hardware acceleration สำหรับการเข้ารหัสของ WireGuard ดังนั้นในแง่ประสิทธิภาพ มันอาจแข่งกับ mTLS ได้ยาก
อนึ่ง ตอนนี้ผมกำลังหางานฟรีแลนซ์อยู่ ถ้าต้องการ Golang ฟรีแลนซ์สายเครือข่ายความเร็วสูงและความปลอดภัย ก็ติดต่อมาได้
มันจะเหมือน Magic Wormhole สำหรับ tunnel ตามต้องการ และหวังว่าจะช่วยปรับปรุงปัญหาที่การส่งไฟล์บนเครือข่ายระยะไกลแบนด์วิดท์สูงมักตกจาก 20~30 MB/s ลงฮวบได้มากด้วย
0 - https://github.com/slackhq/nebula
โดยรวมผมเห็นด้วยว่าการส่งข้อความแบบจุดต่อจุดเดี่ยว บางครั้งใช้ HTTP request ตรง ๆ อาจเชื่อถือได้กว่าการผ่าน message queue แต่ก็น่าแปลกใจอยู่บ้างที่ ข้อความใน NATS สูญหายมากขนาดนั้น จนส่งผลกระทบต่อบริการอย่างมีนัยสำคัญ
ถ้าข้อความหาย NATS ไม่น่าจะส่งซ้ำจนกว่าจะสำเร็จหรือ? อยากรู้ว่ามีใครทราบไหมว่าทำไมถึงเจอความไม่เสถียรที่รู้สึกได้ขนาดนั้น
สถาปัตยกรรมของ NATS นั้นเข้าใจง่ายและน่าสนใจ แต่ก็ชวนสงสัยว่ามันพลาดตรงไหน JetStream มีพารามิเตอร์ที่ปรับจูนได้เยอะมาก
เช่น memory stream ที่มีหน้าต่างตรวจจับข้อมูลซ้ำตามเวลา, รูปแบบ push/pull, รวมถึงการตั้งค่านโยบาย retry และ acknowledgment
แต่ก็อาจไม่เหมาะกับ การเชื่อมต่อข้อความเดี่ยวแบบชั่วครั้งชั่วคราว ไม่ว่าจะอย่างไร ถ้ามีรายละเอียดที่เฉพาะเจาะจงกว่านี้ก็น่าจะมีประโยชน์มาก
แต่สุดท้ายเราก็ไม่ได้ต้องการมัน เพราะชั้นข้อความไม่ได้เพิ่ม expressive power ให้เรา กลับมีแต่ทำให้การทดสอบและการมอนิเตอร์ยากขึ้น
ส่วนที่บอกว่า “เราติดตั้ง peer ราวกับว่าเราเป็น initiator และให้ flyctl เป็น responder จากนั้น Linux kernel จะเริ่มการเชื่อมต่อ WireGuard ใหม่ไปทางฝั่ง flyctl” นี่เท่ากับเพิ่ม half-RTT ให้กับ handshake โดยพฤตินัยหรือเปล่า?
ตัวอย่างเช่นเป็นลำดับแบบ 1) flyctl ส่ง Initiation, 2) เพิ่ม peer ผ่าน netlink แล้วส่ง Initiation ใหม่, 3) flyctl ส่ง Response กลับมา อะไรทำนองนั้นหรือเปล่า
กล่าวคืออาจไม่ต้องมีหรือไม่ต้องรอขั้นตอนที่ 3 ก็ได้ และถ้ากันไม่ให้มีการ initiate ใหม่ในขั้นตอนที่ 2 ก็น่าจะเป็นแบบนั้นแน่
1.a) Bob ไม่รับสาย แต่เพิ่มเบอร์จาก caller ID ลงในสมุดรายชื่อ
ไม่ค่อยเข้าใจว่าประโยคที่ว่า “ทุกครั้งที่รัน flyctl, CLI อันน่ารักและขนาดมหึมาของเราจะสร้าง TCP/IP stack ขึ้นมาจากอากาศ มี IPv6 address ของตัวเอง และคุยกับ Fly Machines ที่รันอยู่ในเครือข่ายของเราได้โดยตรง” หมายถึงอะไร
ที่พูดว่า “สร้าง TCP/IP stack ขึ้นมาจากอากาศ” เพราะปกติแล้วระบบปฏิบัติการจะให้ TCP/IP stack มาเป็นส่วนหนึ่งของ kernel
แต่ใน
wireguard-goนั้น TCP/IP stack รันอยู่ใน user space เลยสร้างมันขึ้นมาใน process user space ทั่วไปอย่าง command-line interface ของflyctlได้สำหรับคนที่ทำงานกับระบบมานาน มันอาจดูเหมือนเวทมนตร์พอสมควร จริง ๆ แล้ว TCP/IP stack แบบ user space ที่ใช้งานได้จริงภายใน process เพิ่งถือว่าใหม่และแปลกใหม่พอสมควร
สงสัยว่าอะไรเป็นตัวกันไม่ให้ inject แพ็กเก็ต handshake แรกกลับเข้าไปใน network stack แบบนั้นก็น่าจะไม่มี packet loss
แล้วก็สงสัยด้วยว่าจุดประสงค์ของการเช็ก
udp[8] = 1ใน eBPF filter คืออะไรอย่างที่บอกในคอมเมนต์ข้าง ๆ BPF filter แค่ดักจับ initiation packet ซึ่งนั่นคือพฤติกรรมที่ต้องการ เป็นเวอร์ชันของ WireGuard ที่เทียบได้กับการ sniff SYN เพื่อดูการเริ่มต้นของ TCP connection
udp[8] = 1ใช้ กรองเฉพาะแพ็กเก็ต handshake ถ้าไม่มีเงื่อนไขนี้ data packet ก็จะถูกส่งไปยัง daemon ใน user space ด้วยไม่แน่ใจว่าจะ replay handshake แรกได้ไหม แต่เนื่องจาก WireGuard จะเมิน client ที่ไม่รู้จัก มันก็อาจทำได้
น่าสนใจที่ค่าเริ่มต้นคือทำ tunnel WireGuard บน WebSocket ถึงจะไม่ดีต่อประสิทธิภาพนัก แต่ก็น่าจะโอเคสำหรับงานสาย DevOps ที่
flyctlใช้ตอนคิดถึงอนาคตของ QUIC/HTTP3 ผมก็สงสัยเรื่องทำนองนี้เหมือนกัน ว่าผู้ดูแลเครือข่ายอาจไม่ได้จัดการ UDP 443 ให้ดี แต่อาจเลือกบล็อกไปเลยก็ไม่ใช่ว่าจะเป็นไปไม่ได้
flyctlก็มีตัวเลือกตั้งค่าถ้า UDP ใช้ไม่ได้ มันก็คือใช้ไม่ได้เลยและ debug ก็ยาก ดังนั้นค่าเริ่มต้นเลยตั้งเป็นทางที่เรารู้แน่ว่าทำงานได้
แอบขมขื่นนิดหน่อยที่ผมแพ้ในข้อถกเถียงเรื่องจะเลือกค่าเริ่มต้นแบบไหน
สตาร์ทอัพของผมใช้ Fly มาเกือบ 1 ปี ฟีเจอร์หลักที่ทำให้โค้ดกลายเป็นโค้ดที่ deploy แล้วได้ภายในไม่ถึง 1 นาทีนั้นสวยงามจริง ๆ
การยก node ใหม่ขึ้นมาแล้วปิดลงเพื่อทำ backfill ก็ใช้เวลาแค่ไม่กี่วินาที
แต่ตัวบริษัทเองให้ความรู้สึกว่ายังไม่ค่อยนิ่งนัก ครั้งหนึ่ง API server เข้าใช้งานจาก Fly ไม่ได้อยู่ 48 ชั่วโมง และผมก็ไม่แน่ใจว่านั่นเป็นเพราะตั้งค่าพลาดเองหรือเป็น outage แบบ “เงียบ ๆ” อีกครั้ง
มีผลิตภัณฑ์ “db” แต่ก็ออกแนว “ไม่ใช่ managed Postgres นะ” และฝั่งนั้นก็มีอาการสะดุดอยู่เรื่อย ๆ
รู้สึกแปลกที่เอา Postgres มาใส่เป็นคำนามระดับบนสุดใน CLI แต่กลับจำกัดขอบเขตฟีเจอร์ที่รองรับ
แม้แต่การเข้าถึง service API หลักก็ล่มบ่อยจนต้องรอ deploy แก้ service ใหม่
ผมคิดถึงประสบการณ์การ deploy แบบนั้นอยู่ แต่พูดตามตรงตอนนี้พอใจกับ GCP Cloud Run มากกว่า เพราะมี “เรื่องให้ประหลาดใจ” น้อยกว่ามาก และเอกสารก็สมบูรณ์กว่ามาก
น่าแปลกที่ผู้ให้บริการ VPS แทบไม่มีการช่วยลด latency ของ backend service ให้ผู้ใช้เลย ไม่มีเจ้าไหนรองรับ Anycast และตัวเลือก GeoDNS ก็มีน้อยมาก
แต่ GeoDNS ก็เพิ่มความซับซ้อนอีกแบบหนึ่ง
ผมอยากให้ค่า data transfer ของ Fly.io ถูกกว่านี้ ตอนนี้เลยตกอยู่ในสถานการณ์ที่ต้องค่อย ๆ ทำฟีเจอร์จำนวนมากของ Fly.io ขึ้นใหม่แบบงุ่มง่ามในบริการคล้าย ngrok ที่กำลังทำอยู่
[0]: https://lastlogin.io
[1]: โค้ดเฉพาะของ Fly ที่ต้องใช้เพื่อรัน LastLogin แบบกระจายทั่วโลกมีประมาณนี้: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
ตอนที่มันทำงานดีมันลื่นมากจริง ๆ แต่ความถี่ของช่วงเวลาดี ๆ นั้นยังไม่มากพอ
อยากถือโอกาสนี้แนะนำ Netmaker[0]
ไม่ได้เป็นผู้เกี่ยวข้องอะไร แค่เป็นคนที่ใช้อย่างพอใจเพราะต้องการเข้าถึง AWS VPC แบบ private ข้ามหลายแอ็กเคานต์ และอยากให้มีการนำไปใช้กันแพร่หลายกว่านี้
[0] https://www.netmaker.io/
ที่ทำงานก่อนหน้านี้ผมเคยใช้ Ansible ตั้งค่าและดูแล
wgบน Windows กับ Linux อยู่ไม่กี่เครื่อง ซึ่งก็โอเค แต่พอท้าย ๆ แล้วเริ่มเละนิดหน่อยเว็บไซต์คลุมเครือเกินไป
ตรงที่บอกว่า “เกตเวย์ที่มี peer หลายแสนตัว ซึ่งในนั้นมี peer ที่จะไม่มีวันถูกใช้อีก” นี่คือสิ่งที่ผมนึกขึ้นได้พอดีตอนอ่านย่อหน้าแรก ๆ
ไอเดียที่ว่า “ไม่มี API call สำหรับ subscribe อีเวนต์การพยายามเชื่อมต่อขาเข้า ไม่เป็นไร เราสร้างอีเวนต์เองก็ได้ คำขอเชื่อมต่อ WireGuard เป็นแพ็กเก็ตและระบุได้ง่าย ดังนั้นเราสามารถดักจับมันได้อย่างมีประสิทธิภาพด้วย BPF filter และ packet socket” ก็ดีเหมือนกัน
เขาบอกว่าเมื่อได้รับข้อความเริ่มต้นขาเข้า ก็จะได้ ที่อยู่แบบ 4-tuple ของการเชื่อมต่อที่ต้องการ รวมถึง source port ชั่วคราวที่ flyctl ใช้ด้วย แล้วติดตั้ง peer ราวกับว่าเราเป็น initiator และ flyctl เป็น responder; เลยสงสัยว่านี่ทำงานได้แม้อยู่หลัง NAT ไหม
{wggwd.fly.io, 12345, clientIP, 23456}ไม่ว่าจะเป็นแพ็กเก็ต UDP “initiator” ตัวใหม่ หรือเป็นการตอบกลับต่อข้อความเริ่มต้นขาออก มันก็ดูเหมือนกันเป๊ะสำหรับ UDP NAT ที่อยู่ตามเส้นทาง
เพราะข้อมูลที่ใช้ตัดสินมีแค่ 4-tuple และ 4-tuple นั้นก็เหมือนกัน