ทำความเข้าใจ gRPC, OpenAPI, REST และช่วงเวลาที่ควรใช้ในการออกแบบ API (2020)
(cloud.google.com)- จุดเริ่มต้นของการออกแบบ API คือ ความแตกต่างระหว่าง RPC กับ REST และแม้ทั้งสามแนวทางจะเชื่อมโยงกับ HTTP ได้ แต่โมเดลในการสร้างการเรียกและจัดโครงสร้างไคลเอนต์แตกต่างกัน
- REST เป็นโมเดลไฮเปอร์เท็กซ์ที่ไคลเอนต์ตาม URL ที่เซิร์ฟเวอร์ให้มา จึงแตกต่างจาก API แบบ OpenAPI ที่ไคลเอนต์ประกอบรูปแบบ URL เอง
- OpenAPI นิยามงานด้วยเทมเพลตพาธ URL และเมธอด HTTP ใช้งานได้จริงและแพร่หลาย แต่ใกล้เคียงกับโมเดล RPC ที่แมปลงบน HTTP มากกว่า REST
- gRPC นิยาม RPC API โดยตรงผ่าน IDL, การสร้างโค้ด, เพย์โหลดแบบไบนารี และการจัดการการเชื่อมต่อ HTTP/2 พร้อมซ่อนรายละเอียดของ HTTP
- gRPC ต้องใช้ซอฟต์แวร์เฉพาะทั้งสองฝั่ง และมีข้อจำกัดด้านการเสริมความสามารถผ่านพร็อกซี การป้องกันการอัปเดตพร้อมกัน และการอัปเดตบางส่วน จึงเหมาะเป็นพิเศษเมื่อใช้กับ API ภายในหรือเมื่อมีเลเยอร์แปลงอย่าง Cloud Endpoints
สามโมเดลในการมอง API ที่ใช้ HTTP
- แนวทางหลักในการใช้ HTTP เป็นเลเยอร์ส่งข้อมูลของ API แบ่งได้เป็น REST, OpenAPI และ gRPC
- เหตุผลหนึ่งที่ API สาธารณะและ API แบบกระจายภายในจำนวนมากใช้ HTTP คือองค์กรต่าง ๆ คุ้นเคยกับปัญหาความปลอดภัยของทราฟฟิก HTTP บนพอร์ต 80 และ 443
- ทั้งสามแนวทางเชื่อมโยงกับ HTTP ได้ แต่ต่างกันตรงสิ่งที่ใช้เป็นที่อยู่ และวิธีที่ไคลเอนต์สร้างการเรียก
REST: วิธีตาม URL โดยไม่ประกอบ URL เอง
- ใน REST ไคลเอนต์ใช้ URL ที่เซิร์ฟเวอร์ส่งมาให้ตามเดิม และไม่เข้าใจหรือประกอบรูปแบบ URL ในฐานะส่วนหนึ่งของสเปก API
- เบราว์เซอร์จะตามหน้าปัจจุบัน บุ๊กมาร์ก หรือ URL ที่ผู้ใช้ป้อน และทำเพียงดึงข้อมูลคำขอ HTTP จาก URL หรือแปลง URL สัมพัทธ์ให้เป็น URL แบบสมบูรณ์
- แกนหลักของ REST API คือการใช้ ไฮเปอร์เท็กซ์/ไฮเปอร์มีเดีย ที่แสดงการอ้างอิงระหว่างรีซอร์สเป็น URL ของรีซอร์สอื่น
- ในแนวทาง REST ตัวระบุทั้งหมดจะถูกส่งรับเป็น URL
POST /accounts→account_URL- ส่ง
account_URLไปที่POST /subscriptions→subscription_URL GET {account_URL}→ คืนทรีข้อมูลบัญชี
- ข้อดีของ REST ใกล้เคียงกับเสถียรภาพ ความสม่ำเสมอ และความเป็นสากลของเว็บเอง และ โมเดลที่เน้นเอนทิตี ของ HTTP/REST สามารถทำให้ API เรียบง่ายและเป็นระบบมากขึ้น
OpenAPI: ใกล้เคียง RPC ที่แมปตรงกับ HTTP
- OpenAPI นิยามเทมเพลตพาธ URL ไว้ใต้
pathsและเรียกการผสมกันของพาธกับเมธอด HTTP ว่า งาน (operation) - พาธอย่าง
/pets/{petId}กำหนดให้ไคลเอนต์ต้องรู้ค่า{petId}แล้วใส่ลงในเทมเพลต URL เพื่อสร้างคำขอ HTTP - ด้วยแนวทางนี้ ไคลเอนต์ต้องรู้รายละเอียดรูปแบบ URL จึงตรงข้ามกับโมเดลไฮเปอร์เท็กซ์ของ REST
- เหตุผลที่ OpenAPI ใช้กันอย่างแพร่หลายนั้นชัดเจน
- คล้ายกับ โมเดล RPC แบบดั้งเดิม จึงคุ้นเคยสำหรับโปรแกรมเมอร์
- สามารถแมปแนวคิด RPC เข้ากับคำขอ HTTP ได้โดยตรง
- สำหรับ API สาธารณะ ข้อดีสำคัญคือเข้าถึงได้จากแทบทุกภาษาและสภาพแวดล้อมด้วยเทคโนโลยี HTTP มาตรฐานเท่านั้น
- แต่ก็ต้องออกแบบพาธ URL, เมธอด HTTP และการแมปพารามิเตอร์ ทำให้รายละเอียดที่ทั้งผู้ให้บริการและผู้ใช้ API ต้องเรียนรู้เพิ่มขึ้น
gRPC: เทคโนโลยี RPC ที่ซ่อน HTTP/2
- gRPC ใช้ HTTP/2 เป็นเลเยอร์ส่งข้อมูล แต่ไม่เปิดเผยรายละเอียด HTTP ให้ผู้ออกแบบ API และโค้ดฝั่งไคลเอนต์·เซิร์ฟเวอร์
- ลำดับการเรียกของไคลเอนต์ gRPC นั้นเรียบง่าย
- ตัดสินใจว่าจะเรียกโพรซีเยอร์ใด
- คำนวณค่าพารามิเตอร์ที่จะใช้
- ส่งพารามิเตอร์ผ่าน stub ที่สร้างโค้ดไว้เพื่อทำการเรียก
- gRPC นิยามโพรซีเยอร์ระยะไกลบนพื้นฐาน interface description language จึงไม่จำเป็นต้องแสดงการแมปพาธ URL, พารามิเตอร์ และเมธอด HTTP ไปพร้อมกันเหมือน OpenAPI
- การสร้างโค้ด เฟรมเวิร์ก และไลบรารีอาจช่วยให้สร้างไลบรารีไคลเอนต์และอิมพลีเมนเทชันของเซิร์ฟเวอร์ได้ง่ายขึ้น
- มีข้อได้เปรียบด้านประสิทธิภาพจากการใช้เพย์โหลดแบบไบนารีและการจัดการการเชื่อมต่อ HTTP/2 และแม้จะใช้เทคโนโลยีเดียวกันโดยตรงโดยไม่ใช้ gRPC ได้ แต่ต้องเรียนรู้เทคโนโลยีมากกว่า
เมื่อควรพิจารณา gRPC แทน OpenAPI
- เมื่อออกแบบ API ด้วย OpenAPI ต้องแสดงงานและพารามิเตอร์เป็นการผสมกันของพาธ URL กับเมธอด HTTP ซึ่งมีทางเลือกมากและอาจยุ่งยาก
- หากยังใช้โมเดลแบบ RPC ต่อไป gRPC จะช่วยลดภาระในการออกแบบ การแมปแบบกำหนดเอง ลงบน HTTP ด้วยตัวเอง
- gRPC และ OpenAPI มีโมเดล API พื้นฐานคล้ายกัน แต่วิธีเปิดเผย HTTP ต่างกัน
- OpenAPI เปิดเผยรายละเอียดการส่งข้อมูลของ HTTP ให้ไคลเอนต์ และให้ผู้ออกแบบควบคุมการแมป
- gRPC ซ่อนรายละเอียด HTTP ผ่านการแมปที่กำหนดไว้ล่วงหน้าและโค้ดที่สร้างขึ้น
- ข้อดีใหญ่ของ OpenAPI คือไคลเอนต์สามารถใช้เครื่องมือและเทคโนโลยี HTTP มาตรฐานได้โดยตรง และสำหรับผู้ออกแบบ API จำนวนมาก ข้อดีนี้ทำให้ต้นทุนการออกแบบเพิ่มเติมสมเหตุสมผล
ผสานโมเดลที่เน้นเอนทิตีเข้ากับ RPC
- แม้จะใช้ gRPC หรือ OpenAPI หากจำกัดเมธอด RPC ให้ยึดตามเอนทิตีเป็นหลัก ก็จะได้ข้อดีบางส่วนของ REST
- แนวทางคือไม่เริ่มจากการนิยามโพรซีเยอร์ แต่เริ่มนิยาม ประเภทรีซอร์ส ก่อน แล้วจับคู่แต่ละประเภทกับงานเอนทิตีมาตรฐาน
- งานพื้นฐานคือ Create, Retrieve, Update, Delete, List ซึ่งมักมองได้ว่าเป็น CRUD plus List
- หากจำเป็นสามารถมีงานเพิ่มเติมได้ แต่เมื่อแนวคิดแบบเน้นเอนทิตีผสมกับแนวคิดแบบเน้นโพรซีเยอร์ ข้อดีบางส่วนอาจลดลง
- การจัดกลุ่มโพรซีเยอร์ตามประเภทเอนทิตียังเป็นหนึ่งในแนวคิดหลักของภาษาเชิงวัตถุด้วย
ข้อจำกัดและข้อควรระวังของ gRPC
- gRPC ต้องใช้ ซอฟต์แวร์เฉพาะ ทั้งฝั่งไคลเอนต์และเซิร์ฟเวอร์ และต้องผนวกโค้ดที่สร้างขึ้นเข้าในกระบวนการ build ของทั้งสองฝั่ง
- สำหรับผู้ใช้ภาษาไดนามิกอย่าง JavaScript หรือ Python ซึ่งสภาพแวดล้อมพัฒนาอาจแทบไม่มีกระบวนการ build ข้อกำหนดนี้อาจเป็นภาระ
- Google Cloud Endpoints ทำให้เข้าถึง gRPC API ผ่าน HTTP และ JSON ได้ จึงคืนทางเลือกให้ไคลเอนต์ แต่ไม่ใช่ผู้ใช้ทุกคนจะใช้สิ่งนี้หรือสร้างฟังก์ชันเทียบเท่าได้
- REST API ทำให้สร้างบอตที่ crawl ทั้งระบบได้ง่ายแม้ไม่มีเมทาดาทา แต่ API แบบ RPC อย่าง gRPC หรือ OpenAPI ต้องมี API และเมทาดาทาที่ต่างกันตามประเภทเอนทิตี หรือซอฟต์แวร์เฉพาะ
- HTTP API มักเพิ่มฟังก์ชันด้านความปลอดภัย การตรวจสอบอินพุต การแมปรูปแบบข้อมูล และการแก้ไขเฮดเดอร์·บอดี้ผ่านเครื่องมือจัดการ API และพร็อกซีอย่าง Apigee Edge แต่ใน gRPC การ เสริมความสามารถผ่านพร็อกซี เช่นนี้อาจทำได้ยากกว่ามาก
- gRPC ไม่ได้นิยามกลไกมาตรฐานในการป้องกันข้อมูลสูญหายจากการอัปเดตพร้อมกัน
- HTTP มีเฮดเดอร์
EtagและIf-Matchสำหรับเรื่องนี้
- HTTP มีเฮดเดอร์
- gRPC ไม่ได้นิยามกลไกการอัปเดตบางส่วนเช่นกัน
- HTTP มี
PATCHและสำหรับ JSON มีมาตรฐาน JSON merge patch กับ JSON patch - JSON merge patch เรียบง่ายกว่า แต่จัดการไม่ได้ทุกกรณี เช่น การอัปเดตอาร์เรย์
- JSON patch จัดการได้มากกรณีกว่า แต่ใช้งานซับซ้อนกว่า
- HTTP มี
เกณฑ์การเลือก
- หากคุณออกแบบโมเดลไฮเปอร์เท็กซ์ของ REST เป็นอยู่แล้ว หรือเต็มใจเรียนรู้ REST อาจเป็นตัวเลือกที่ดีเมื่อเป้าหมายคือเสถียรภาพ ความสม่ำเสมอ และความเป็นสากล
- OpenAPI ทำให้สร้าง API ที่เข้าถึงได้ด้วยเทคโนโลยี HTTP มาตรฐานเท่านั้น แต่จำนวนตัวเลือกในการออกแบบเพื่อแมปแนวคิด RPC ลงบน HTTP จะเพิ่มขึ้น ทำให้การออกแบบ การใช้งาน และการเรียนรู้ยากขึ้นได้
- หากกำลังพิจารณา API แบบ OpenAPI ก็ควรพิจารณา gRPC ร่วมด้วย โมเดล API พื้นฐานของทั้งสองแนวทางเปรียบเทียบกันได้ และ gRPC ลดความจำเป็นในการสร้างการแมป HTTP เอง
- gRPC น่าสนใจเป็นพิเศษภายใต้เงื่อนไขต่อไปนี้
- เมื่อใช้ผลิตภัณฑ์อย่าง Cloud Endpoints ทำให้ไคลเอนต์ไม่จำเป็นต้องนำเทคโนโลยี gRPC มาใช้เสมอไป
- เมื่อเป็น API ภายในที่ควบคุมตัวเลือกเทคโนโลยีของเซิร์ฟเวอร์และไคลเอนต์ทั้งหมดได้
- เมื่อนำ gRPC มาใช้แทน OpenAPI หรือ REST ควรพิจารณาว่าโอกาสในการเสริมหรือปรับแก้พฤติกรรมของ API ผ่านพร็อกซีที่ใช้เครื่องมือจัดการ API อย่าง Apigee Edge อาจถูกจำกัดกว่ามาก
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ถ้าย้อนเวลาได้ ผมอยากถึงขั้นกันไม่ให้ตัวเองได้เรียน gRPC ตั้งแต่แรก
ตอนแรกก็หลงใหลในวิสัยทัศน์ของมัน แต่ผ่านไปไม่กี่ปีถึงเห็นว่ามีเรื่องปวดหัวเยอะเกินไป คำกล่าวที่ว่ามันช่วยซ่อนรายละเอียดภายในก็แทบจะเป็นเรื่องล้อเล่น และสุดท้ายต้องเท debug log ออกมาเพื่อหาสาเหตุว่าทำไม request ถึงล้มเหลว 1 ครั้งจาก 10 ครั้ง พร้อมปรับตั้งค่า timeout/retry ชื่อคลุมเครืออีก 10–20 รายการ
ตั้งแต่ Maven plugin,
deadline exceededแปลก ๆ, load balancer ที่ไม่ชอบ HTTP/2, สถานการณ์ที่สุดท้ายต้องใช้ API มาตรฐานเพราะ firewall, เอกสารที่ขาดแคลน ไปจนถึงการพยายามให้ได้ข้อความ error ที่ใช้ได้จริงสำหรับ observability ล้วนกินเวลาไปหมดdeveloper experience ของโค้ดที่ generate ออกมาไม่ดี, client stub เป็น concrete class แบบ
finalทำให้ mock ในการทดสอบได้ยาก, และการ implement server ก็ต้องสืบทอดจาก concrete class แทนที่จะเป็น interfacemethod ฝั่ง server มี signature แบบ async จนทำให้พฤติกรรมที่อิง AOP อย่าง
@Transactionalพัง, ไม่มีการรองรับ exception, และแม้ immutable value class จะดี แต่ทั้งหมดต้องสร้างผ่าน builderสุดท้ายถ้าจะใช้ gRPC ใน SOA ก็ต้องเขียนโค้ด plumbing จำนวนมากเพื่อซ่อน noise ของ gRPC และให้ได้โค้ดสะอาดที่ทดสอบได้ ส่วน RPC compiler ของ Thrift ก็มีปัญหาคล้ายกันและยังมีปัญหาอื่นเพิ่มเข้ามาอีก
ประสบการณ์ gRPC บน .NET และ C# รุ่นใหม่ค่อนข้างดี ถึงขนาด Microsoft ก็ยุติเทคโนโลยี RPC เดิมอย่าง WCF แล้วหันมาโฟกัสที่ gRPC
protocgenerate ออกมายืดยาวและพลาดง่ายกว่าการ serialize เองเสียอีกwire protocol ไม่ได้ type-safe และแม้จะมี type tag แต่ก็ใช้ tag เดียวกันซ้ำกับ data type หลายแบบ การเข้ารหัสจำนวนเต็มแบบ zig-zag ก็ช้า
ในฐานะ RPC library ถือว่าแย่มาก และในสิ่งที่เคยเจอเอง มีแค่ FlatBuffers เท่านั้นที่แย่กว่า
เลยสงสัยว่าความต่างนี้เป็นเพราะ Java หรือเพราะเทคโนโลยี gRPC เอง กันแน่
เลยสงสัยว่าเป็นปัญหาที่เฉพาะกับ Java+gRPC หรือเปล่า
ผมทำ API มานาน เคยใช้ทั้ง gRPC และ HTTP/REST และยังเคยเปิดซอร์สไลบรารี https://github.com/oapi-codegen/oapi-codegen สำหรับสร้างไคลเอนต์และเซิร์ฟเวอร์ Go จากสเปก OpenAPI ด้วย
ในบทความนี้ผมเห็นด้วยยากกับวิธีที่ แยก OpenAPI ออกจาก REST OpenAPI เป็นวิธีบันทึกเอกสารพฤติกรรมของ HTTP API และสามารถอธิบายได้ทั้ง API แบบ RESTful หรือ API ที่กำหนดขึ้นเองโดยสมบูรณ์ ในแง่ที่เป็นภาษา schema สำหรับอธิบาย API ให้เครื่องมือเข้าใจได้ แนวคิดจึงคล้ายกับไฟล์ Protocol Buffer ที่กำหนดโปรโตคอล gRPC
gRPC เป็นกลไก RPC ที่รับส่ง proto กัน โดยตอนที่ Google เปิดเผย protobuf นั้นไม่ได้เปิดเผย Stubby ซึ่งเป็นเลเยอร์ RPC ภายใน และ gRPC ก็ไม่ได้ยอดเยี่ยมเท่า Stubby แต่ถึงอย่างนั้นประสิทธิภาพการส่งข้อมูลก็ดีและขยายได้ค่อนข้างง่าย
อย่างไรก็ตาม gRPC ไม่ใช่ ecosystem ที่แข็งแรงเท่าไลบรารี HTTP กระแสหลัก จึงมักต้องลงมือทำ middleware อย่าง logging หรือ authentication เองอยู่บ่อย ๆ โดยเฉพาะในการทำ RPC ระหว่างบริการที่ implement ด้วยภาษาต่างกัน
ปัญหาจริง ๆ ของ gRPC ผมมองว่าอยู่ที่ไฟล์ proto เพราะไคลเอนต์ทุกตัวต้อง build ด้วยไฟล์
.protoที่เข้ากันได้กับเซิร์ฟเวอร์ จึงไม่ใช่โปรโตคอลที่ค้นพบได้เอง HTTP API สามารถเรียกได้ด้วยcurlหรือโค้ดที่เขียนเองแม้ไม่มีคำอธิบาย OpenAPI ทำให้ coupling หลวมกว่า และด้วยเหตุนี้การทำงานกับมันรวมถึงการ debug จึงง่ายกว่าREST ที่ Roy Fielding นิยามไว้ในวิทยานิพนธ์ปริญญาเอกปี 2000 คือเมื่อ
GETไปที่ root URL แล้วควรมีลิงก์ต่าง ๆ อยู่ใน response200 OKและต้องสามารถสำรวจ resource ทั้งหมดที่ API มีให้ได้ด้วยการตามลิงก์เหล่านั้น อนุญาตให้มีโครงสร้างแบบลำดับชั้นได้ แต่ทุกอย่างต้องเข้าถึงได้จากที่ใดที่หนึ่งใน tree ของลิงก์ ซึ่งตั้งใจให้เกิด discoverabilityในที่ทำงานจริงตลอด 20 ปีที่ผ่านมา แทบทุกแห่งใช้
POST resource_name/resource_id/sub_resource/sub_resource_id/mutation_typeหรือใช้PUT resource_name/resource_id/sub_resource/sub_resource_idตามวิธีจัดการ idempotency ของบริษัท และไคลเอนต์ก็ประกอบ URL วิเศษขึ้นจากความรู้เชิงโครงสร้างอย่าง Swagger/OpenAPIดังนั้นคนที่เคร่งในนิยามมักเรียกแนวทางในงานจริงว่า “RESTful” ไม่ใช่ “REST” เพื่อสื่อว่าไม่ได้ implement นิยาม REST ของ Fielding
ในฐานะคนที่ดูแล API ขนาดค่อนข้างใหญ่ซึ่งมีทั้งไคลเอนต์ภายในและภายนอก ผมก็เข้าใจ workflow การ generate โค้ดจากสเปก OpenAPI ได้ยากเช่นกัน พอเติม stub ที่ generate มาแล้วปรับปรุงสเปก API ซ้ำไปเรื่อย ๆ เครื่องมือก็จะสร้าง stub ใหม่ให้อีก ต้อง merge ด้วยมือ และยิ่ง API ใหญ่ขึ้นก็ยิ่งหาการเปลี่ยนแปลงที่เกี่ยวข้องได้ยาก
เลยทำสัตว์ประหลาดที่ใช้
go/astและอื่น ๆ เพื่อ generate สเปก OpenAPI จากโค้ดขึ้นมา มันไม่สมบูรณ์แบบ แต่เป็นทางออก 95% ที่ใช้ได้ทั้งกับ Echo และ Gin และเมื่อจำเป็นต้องมี endpoint ใหม่ ก็สร้าง struct สำหรับ request/response กับ handler เปล่า ๆ แล้ว generate เอกสารส่งให้ frontend developer ได้อย่างรวดเร็วdeveloper ส่วนใหญ่ไม่จำเป็นต้องกังวลว่าจะอธิบาย API ด้วย OpenAPI อย่างไร และเอกสารก็ตรงกับโค้ดเสมอ
Apipe.new(GitHun) |> from("search/repositories") |> eq(:language, "elixir") |> order_by(:updated) |> limit(1) |> execute()ไม่จำเป็นต้องรู้ฟังก์ชัน gRPC ที่ใช้ได้หรือจุดเฉพาะแบบ RESTful ของ API third party และยังคงเข้าถึงเอกสารในตัวกับ type ได้
https://github.com/cpursley/apipe
กำลังพิจารณาเลเยอร์ adapter ของ TypeScript เพื่อให้ใส่ในโปรเจกต์ JS/TS ได้เหมือน Supabase
const { data, error } = await apipe.from('search/repositories').eq('language', 'elixir').order_by('updated').limit(1)การเรียกนี้สามารถส่งผ่าน proxy ของ Elixir เพื่อให้รับงานหนัก ๆ อย่างการประมวลผลแบบ asynchronous หรือ rate limiting ได้
สามารถสร้างคำอธิบาย OpenAPI จาก JSON serialization ของ Protobuf แล้วให้บริการผ่าน Swagger ได้ และตัว gRPC เองก็มี reflection ในตัวรวมถึง utility
grpcurlที่ใช้สิ่งนี้จากมุมมองของคนที่เคยทำงานในบริษัทกลุ่ม FAANG บางแห่ง Thrift/gRPC มีประโยชน์มากจริง ๆ สำหรับการ routing ระหว่างบริการภายใน
แต่ความซับซ้อนส่วนใหญ่ของมันถูกจัดการให้โดยทีมที่สร้างไลบรารี, ชั้น service discovery, routing ฯลฯ การใช้โปรโตคอล RPC ทำให้ทำสิ่งเหล่านี้ได้ในสเกลและความเร็วที่บริการ JSON/REST ทั่วไปทำได้ยาก
ผมไม่เคยเห็น REST API ที่ไม่ปล่อยให้ verb หลุดออกมาเลย และถ้าต้องสร้าง service mesh ฝั่งแบ็กเอนด์ หรือเชื่อมบริการโลคัลสองตัวเข้าด้วยกันผ่าน network stream ผมจะเลือก gRPC เสมอ
แต่ถ้าเป็นการเปิดให้ลูกค้าหรือเว็บใช้งาน ผมจะไม่ใช้ gRPC เด็ดขาด RPC ทรงพลังเพราะมันตรึงการตัดสินใจไว้หลายอย่างและบังคับให้ใช้ “วิธีเดียว” ในทางกลับกัน ถ้าคลients ที่มีเทคสแต็กหลากหลายต้องใช้บริการ REST ดีกว่ามาก
POST /api/doThingyแล้วส่ง body เป็น JSONมันเป็น RPC แบบง่าย ๆ ที่ใครก็ตามที่มี HTTP client พื้นฐานที่สุดก็เข้าร่วมได้ และทำงานได้ดีบนทุก OS และเบราว์เซอร์ ไม่ต้องมานั่งสู้กันว่าจะใส่ไว้ใน path ของ URL, query parameter หรือ body
ถ้าใช้สายเซิร์ฟเวอร์อย่าง Buf หรือ Connect ที่พยายามทำให้ลำบากน้อยลง gRPC ก็ยอมรับ JSON ผ่าน HTTP ได้อย่างเต็มใจ
ผมนึกถึงกรณีอย่างเกมออนไลน์หรือ MMO ที่ต้องการการสื่อสารแบบเรียลไทม์มากกว่า REST มาก ๆ แต่ก็ไม่ค่อยแน่ใจว่าสมัยนี้เขาเอาอะไรบางอย่างไปวางบนการเชื่อมต่อ socket กันหรือเปล่า
อยากรู้ด้วยว่าเคยลองอย่างอื่นอะไรมาบ้าง
ถ้าไม่ได้ทำ bidirectional streaming ผมมองว่า gRPC โดยรวมแล้วเสียเวลาเปล่า
มีทั้งนรกของ transitive dependency ตอน runtime, นรกของ toolchain และดูเหมือนว่าทีมที่ดูแลแต่ละ implementation ภายใน Google เองก็ยังตกลงกันในเชิงปรัชญาไม่ได้ว่าฟังก์ชันพื้นฐานควรทำงานอย่างไร
ลองเปิด gRPC API ให้ทีมที่ไม่ได้ใช้ภาษาของคุณ โดยเฉพาะถ้าไม่ใช่ Go/Python/Java หรือแม้แต่ใช้เวอร์ชันเก่า, หรือลอง integrate กับผลิตภัณฑ์สำเร็จรูปเชิงพาณิชย์, หรือลองเปิดให้เบราว์เซอร์ใช้ดู ทั้งหมดจะต้องมี ชั้นกลาง เข้ามา
gRPC โดยรวมค่อนข้างราบรื่น
การใช้ REST เพื่อสื่อสารระหว่างบริการฝั่งแบ็กเอนด์ก็ไม่ค่อยมีความหมายหากมีข้อกำหนดด้านประสิทธิภาพ และถ้าไม่ใช่ call ที่ดึงข้อมูลเล็ก ๆ เป็นครั้งคราว ก็แทบไม่มีเหตุผลที่จะใช้โปรโตคอล/API ที่มนุษย์อ่านได้
เมื่อส่งคืน subtype ของ
oneofที่เป็นชนิดที่ตอนนี้ไม่มีฟิลด์ ผลการแปลงอัตโนมัติออกมาเป็น{ "id": "id", "sub_type_two": { } }ในเชิงฟังก์ชันมันทำงานได้ และถ้ามีฟิลด์เพิ่มในภายหลัง โค้ดนั้นก็ยังทำงานต่อได้ แต่ในโลกเว็บ การใช้ออบเจ็กต์ว่างเพื่อแสดงชนิดของ response มันแปลก และตอนเขียน protobuf ปัญหาแบบนี้อาจมองไม่ค่อยเห็น
เมื่อเทียบกับโปรโตคอลไบนารีแทบทุกแบบที่จินตนาการได้ มันใกล้เคียงกับ กึ่งต่อต้าน streaming
ประสบการณ์เดียวที่เคยใช้ gRPC ในบริษัทคือโปรเจกต์ที่นักพัฒนาซีเนียร์อีกคนผลักดันโดยบอกว่า “ต้องการประสิทธิภาพ”
สุดท้ายก็ต้องทำ JSON API ด้วย เพราะฟรอนต์เอนด์ต้อง consume ได้ และนอกจากนักพัฒนาคนนั้นก็ไม่มีใครมีประสบการณ์ gRPC เลย เอาเข้าจริงนักพัฒนาคนนั้นเองก็ไม่ได้ไปไกลกว่า quick start guide ของ gRPC Python และไม่ได้ช่วยแก้บั๊กด้วย
โปรเจกต์เละเทะด้วยเหตุผลมากมาย และไม่เคยไปถึงสเกลที่พอจะ justify การใช้ gRPC ได้เลย
ถึงอย่างนั้น gRPC ที่ผมลองใช้เองนิดหน่อยก็ชอบนะ และรู้สึกว่าต้องใช้ทั้งงานและการคิดไตร่ตรองมากกว่านี้มาก นั่นอาจเป็นเพราะผมเคยทำ JSON API มามากกว่ามากก็ได้
กำลังใช้ ConnectRPC https://connectrpc.com/ อย่างสนุก
มันแก้ส่วนที่มีปัญหาของ gRPC ได้เยอะ และหวังว่าเมื่อ Safari ยอมรับ WebTransport แล้ว ConnectRPC จะพัฒนา streaming ที่ดีกว่านี้ได้
ตอนแรกคิดว่า https://buf.build เยอะเกินจำเป็น แต่ความสามารถในการดึงไฟล์ proto ของ third-party มาใช้ได้โดยไม่ต้องดาวน์โหลดทีละไฟล์คือจุดตัดสินใจ
deps:- buf.build/landeed/protopatch- buf.build/googleapis/googleapisการสร้าง SDK อัตโนมัติก็สำคัญ เมื่อก่อนกำลังจะชมว่ามันสร้าง SDK อัตโนมัติสำหรับประมาณ 9 ภาษา แต่เพิ่งอัปเดตในช่วงหนึ่งสองวันที่ผ่านมา ตอนนี้เห็นว่าเป็น 16 ภาษา พร้อม OpenAPI และฟีเจอร์ใหม่อื่น ๆ แล้ว
ผมเองก็เคยหวั่นไหวกับคำสัญญาลวงของ gRPC streaming และเอกสารนี้ตรงกับประสบการณ์ของผมเป๊ะ: https://connectrpc.com/docs/go/streaming/
ด้วยเหตุนี้จึงใช้ bidirectional streaming บนเบราว์เซอร์ได้ด้วย
รู้สึกเหมือน Google ทำสงครามจิตวิทยากับทั้งวงการจนทำให้ทุกคนใช้ gRPC สำหรับการสื่อสารระหว่างบริการภายใน
ประสบการณ์นักพัฒนาของ gRPC แย่กว่า REST ค่อนข้างมาก
คุณไม่สามารถส่งคำสั่งง่าย ๆ เพียงคำสั่งเดียวให้ใครสักคนเพื่อเรียก endpoint ได้ และต้องใช้เครื่องมือเสริมที่ไม่ได้เป็นมาตรฐาน แถมโค้ดฝั่งไคลเอนต์ที่สร้างออกมายังเป็นก้อนโค้ดน่าเกลียดแบบที่แทบไม่ค่อยเห็นในภาษาใด ๆ
แค่มีการเปลี่ยนแปลงโปรโตคอลหนึ่งครั้ง ก็สามารถรู้แบบ static ได้ว่าผู้บริโภค downstream รายใดต้องอัปเดตและ redeploy ทำให้งานที่อาจกินเวลาหลายสัปดาห์ลดเหลือการเปลี่ยนแปลงภายในหนึ่งชั่วโมงได้
ยังรู้ด้วยว่าข้อความที่รับเข้าและส่งออกถูกตรวจสอบความถูกต้องทันที และสามารถจัดเก็บไว้ด้วยต้นทุนต่ำเพื่อกู้คืนในภายหลังได้
จาก proto เราได้เอกสาร API ที่อ่านง่ายมาก และไม่ถูกทำให้คลุมเครือด้วยโค้ดหรือ business logic นอกจากนี้ยังมี version management และความหมายของการเลิกใช้ที่ฝังมาในตัว และถ้าไม่นับ map ก็รองรับโครงสร้างข้อมูลที่สมบูรณ์กว่า
เมื่อเทียบกันแล้ว JSON ดูบวมและล้าสมัยในฝั่งแบ็กเอนด์
เมื่อเขียน data type และ function signature ไว้ ก็จะได้สิ่งที่เรียกได้เหมือนฟังก์ชันจริง และสามารถโฟกัสกับ business logic แทน boilerplate สำหรับ serialization/deserialization
Thrift ก็เช่นกัน ดีกว่าวิธีทำเองทั้งหมดมาก และผมมองว่า GraphQL ดียิ่งกว่า
แม้แต่ใน Go การทำความเข้าใจเรื่องการ regenerate และการจัดการเวอร์ชัน proto ที่แชร์กันก็ยุ่งยาก และยิ่งเพิ่มภาษาเข้ามาทีละภาษาก็ยิ่งแย่ลง
ถึงอย่างนั้นดูเหมือนสตาร์ทอัพทุกแห่งจะคิดว่าตัวเองต้องมี microservice 100 ตัวกับ gRPC
ประโยคที่ว่า “ถ้า API เป็น REST API ไคลเอนต์ไม่จำเป็นต้องเข้าใจรูปแบบ URL และรูปแบบนั้นไม่ใช่ส่วนหนึ่งของสเปก API ที่ให้แก่ไคลเอนต์” สอดคล้องกับนิยาม REST ของ Roy Fielding
Fielding เขียนไว้ว่า REST API ควรเข้าใช้งานได้โดยไม่ต้องมีความรู้ล่วงหน้านอกจาก URI เริ่มต้นและชุด media type ที่เป็นมาตรฐาน และหลังจากนั้นการเปลี่ยนสถานะของแอปพลิเคชันทั้งหมดควรเกิดจากการที่ไคลเอนต์เลือกตัวเลือกที่เซิร์ฟเวอร์จัดให้
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypert...
เป็นเรื่องที่ถูกพูดถึงกันมามากแล้ว แต่ก็ยังน่าสนใจที่ในระบบ RESTful จริง ๆ “สเปก API” ที่ให้แก่ไคลเอนต์ควรมีเพียง URI/URL จุดเข้าเริ่มต้นเท่านั้น
โดยส่วนตัวผมมองว่ามันบวมและไม่ได้แก้ปัญหาจริง
https://en.m.wikipedia.org/wiki/HATEOAS
โดยเฉพาะไม่รู้ว่าไคลเอนต์ควรค้นพบ resource ที่จัดการได้หรือโมเดล request/response อย่างไร
การตั้งค่าอาจเป็นแบบนั้นได้ แต่สเปก API ควรมีมากกว่า URL มาก และต้องอธิบาย media type ที่ระบบใช้อย่างละเอียด กล่าวคือส่วนใหญ่ต้องอธิบายเนื้อหา HTTP request/response body
ในลิงก์ก็ระบุว่า “REST API ควรทุ่มความพยายามในการอธิบายแทบทั้งหมดไปกับนิยาม media type ที่ใช้แทน resource และขับเคลื่อนสถานะของแอปพลิเคชัน”
สุดท้ายแล้วมันไม่ใช่แค่ส่งคืน
application/jsonแต่เป็นการส่งคืนบางอย่างแบบ+jsonเฉพาะ และส่วนใหญ่มีข้อมูลธุรกิจที่แอปพลิเคชันต้องเข้าใจ ไม่ใช่ JSON ทั่วไปในการถกเถียงทั่วไปมักโฟกัสแค่ URL เริ่มต้น และงานส่วนใหญ่ที่ Fielding บอกว่า “จงอธิบาย media type” ถูกละทิ้งไป ดังนั้นจึงเป็นธรรมดาที่คนได้ยินว่า “มี URL เดียวก็พอ” แล้วจะถามว่า “แล้วสเปกที่เหลืออยู่ไหน?”
ตอนแรกผมกดเข้าไปอ่านเพราะนึกว่าเป็นบล็อกเกอร์อีกคนที่ไม่เข้าใจ REST แต่ดูเหมือนผู้เขียนอย่างน้อยก็เข้าใจแนวคิดพื้นฐาน
ผมก็ชอบ gRPC และในโปรเจกต์เชิงพาณิชย์มันก็น่าสนใจพอสมควร แต่ถ้าเป็นโปรเจกต์ส่วนตัวหรือเชิงอุดมคติ ผมคิดว่า REST ดีกว่า
ไม่ชอบการใช้ gRPC ภายในดาต้าเซ็นเตอร์
มักเลือกใช้ด้วยเหตุผลด้านประสิทธิภาพ แต่ gRPC เรียกได้ยากว่ามีประสิทธิภาพสูง และคุณภาพของไคลเอนต์สาธารณะก็ต่ำมาก โดยเฉพาะนอก implementation หลักของ C++/Java เช่น implementation ของ Node.js
ไม่ได้คัดค้านการใช้ protobuf เป็นสเปก API แต่ควรใช้ร่วมกับ framing protocol บน TCP ได้ อย่างไรก็ตาม ใน RPC รูปแบบนั้นยังไม่มีตัวเลือกที่โดดเด่นชัดเจน
สำหรับ API บนเว็บ ผมชอบ payload ที่อ่านได้มากกว่า แต่โดยทั่วไปพอใช้ JSON ความเฉพาะเจาะจงของ type ก็จะหลวมลง ทำให้เกิดปัญหา interoperability ระหว่างภาษาแบ็กเอนด์ โดยเฉพาะใน Node.js ที่
JSON.parseถูกใช้เหมือนเป็น implementation สำหรับ mapping schemaถ้าจะทำให้ถูกต้อง ต้องสร้าง encoder และ decoder จาก schema อย่างชัดเจน และเมื่อทำแบบนั้น ข้อดีของการใช้ JSON ในบริบท JS ก็ลดลงไปพอสมควร
กำลังจับตาโปรเจกต์ TypeSpec ของ Microsoft อยู่ด้วย: typespec.io
ถ้าเป็นเทคโนโลยี stack ภาษาเดียว ความสำคัญก็ลดลง และถ้าใช้ภาษานอกเหนือจากภาษาหลัก ๆ ของ Google ประสบการณ์ก็น่าจะไม่ดีเท่านั้น
gRPC ทำให้คนส่วนใหญ่ที่อยู่นอก Google รู้สึกว่าเข้าถึงยากโดยไม่จำเป็น
ไคลเอนต์ gRPC JS หนักโดยไม่จำเป็นและค่อนข้างไม่โปร่งใส แนวคิดดี แต่เมื่อเทียบกับคนที่คุ้นกับ “ความเรียบง่าย” ของ REST แล้ว การนำไปใช้ยังน่าผิดหวัง
RPC ดูแลรักษาง่ายกว่าในเชิงความหมาย เพราะไม่ต้องฝืนยัดคาร์ดินาลิตีหรือความสัมพันธ์ของโมเดลข้อมูลให้เข้ากับแพตเทิร์นเชิงกำหนดแบบเดียว ในโลกที่ API เปลี่ยนเร็ว การทำให้เอนทิตีแบบ RESTful สวยงามลงตัวเป็นเรื่องยาก และสำหรับทีมใหญ่กับความต้องการ/ความเป็นเจ้าของที่เปลี่ยนไป การออกแบบแบบเน้นบริการจะดีกว่า
ฝั่งฟรอนต์เอนด์ไม่ได้ดูแลระบบแบ็กเอนด์ พวกเขาต้องการ API ที่เข้าใจง่าย และต้องการเอนทิตีที่สามารถ abstractions ด้วย REST ได้ พวกเขาคือผู้ได้ประโยชน์ปลายทางของการออกแบบแบบนั้น
ความพยายามที่ต้องใช้กับ REST มีความหมายสำหรับบริษัทที่ขาย API และมีนักพัฒนาภายนอกเป็นลูกค้าหลัก
สำหรับการพัฒนาแบ็กเอนด์ protobuf และการเข้ารหัสบนสายแบบไบนารีทำได้ง่ายกว่า สามารถนิยาม API แล้วแชร์ระหว่างบริการด้วยวิธีแบบ static type ได้ และยังลดเวลา encode/decode ข้อความด้วย JSON ไม่ได้มีความหมายเชิง semantic ไม่ได้มี type และมี overhead สูง
ในทางกลับกัน ฟรอนต์เอนด์จัดการข้อความและ JSON ได้แบบ native ไม่อยากดาวน์โหลดนิยาม protobuf หรือ扱กับข้อมูลไบนารีเหมือนพลเมืองชั้นสอง และมันก็ไม่เข้ากับเครื่องมืออย่างสะอาดนัก
gRPC มีเรื่อง routing, retry, side channel, streaming และ semantics ของการเลิกใช้โปรโตคอลอยู่ดี แต่แทบไม่ปรากฏให้ฟรอนต์เอนด์เห็น ทั้งหมดนี้ทำมาเพื่อผู้บริโภคฝั่งแบ็กเอนด์
สุดท้ายแล้วมันคือ ช่องว่างของเครื่องมือระหว่างฟรอนต์เอนด์/แบ็กเอนด์ 100% และเป็นความไม่สอดคล้องกันของอินเทอร์เฟซกับ usability
โดยพื้นฐานแล้วดู source ได้ คนอ่านได้ และตรวจสอบง่าย
gRPC มีไว้ให้เครื่องคุยกันอย่างมีประสิทธิภาพ และถ้ามีมนุษย์เข้ามาเกี่ยว ไม่ว่าจะเขียนโค้ดหรือตรวจ request/response ก็จะไม่สะดวกอยู่บ้าง
ความต่างด้าน usability แบบนี้เข้าใจได้ เพราะบริบทและเป้าหมายต่างกัน
ในมุมมองของผม implementation ของฝั่ง buf.build ดีกว่า
https://buf.build/blog/protobuf-es-the-protocol-buffers-type...
ดูเหมือนหลายคนเลือกมันเพราะต้องการโปรโตคอลบางอย่างอย่าง binary RPC ที่มี contract แต่ยิ่งห่างจาก GoLang ก็ยิ่งแย่ลง
ถ้าเป็นบริการ CRUD ธรรมดา REST ก็เพียงพอแล้ว