ช่วงสี่ถึงห้าปีที่ผ่านมาหลายๆคนอาจจะเคยได้ยิน protocol เริ่มถูกนำมาใช้กันอย่างแพร่หลายมากขึ้นอย่าง gRPC ซึ่งจุดขายของ gRPC มีสองอย่างคือ
- ความเร็วในการส่งผ่านข้อมูลระหว่าง service
- Strong contract ระหว่าง client กับ server
ในส่วนของความเร็วนั้นเป็นเพราะ gRPC ใช้งาน HTTP2 ซึ่งใช้ long live connection ในการส่งข้อมูลระหว่าง service ทำให้ไม่เสีย overhead ในการทำ handshake และสามารถส่งข้อมูลไปกลับได้อย่างต่อเนื่อง ต่างจาก RESTful ที่เราคุ้นเคยกันที่ใช้ HTTP1.1 ซึ่งแต่ละ request จะใช้คนละ connection กันและจะมี overhead ตรงนี้เพิ่มขึ้นมา
สำหรับข้อดีอย่างที่สองผมคิดว่ามันมีประโยชน์มากๆโดยเฉพาะในโลกของ microservice ที่การคุยกันระหว่าง service ทั้งหลายต้องทำผ่าน API การมี contract ที่ชัดเจนและสามารถทำ semantic versioning ในโค้ดโดยตรงได้เลยเป็นการลดความปวดหัวในการเจรจา API contract และเพิ่มความง่ายในการทำใช้ท่าอย่าง contract testing ระหว่างทีมในองค์กร
วันนี้ผมจะมาเล่าให้ฟังว่าเราจะนำ protobuf มาใช้อย่างไรให้ระบบที่เป็น REST สามารถมี strong contract เช่นเดียวกับ gRPC ได้
สำหรับข้อดีอย่างที่สองผมคิดว่ามันมีประโยชน์มากๆโดยเฉพาะในโลกของ microservice ที่การคุยกันระหว่าง service ทั้งหลายต้องทำผ่าน API การมี contract ที่ชัดเจนและสามารถทำ semantic versioning ในโค้ดโดยตรงได้เลยเป็นการลดความปวดหัวในการเจรจา API contract และเพิ่มความง่ายในการทำใช้ท่าอย่าง contract testing ระหว่างทีมในองค์กร
วันนี้ผมจะมาเล่าให้ฟังว่าเราจะนำ protobuf มาใช้อย่างไรให้ระบบที่เป็น REST สามารถมี strong contract เช่นเดียวกับ gRPC ได้
เคยต้องเขียน OpenAPI Spec หรือ Postman Collection ทุกครั้งหลังแก้โค้ดกันไหมครับ ?
การมี documentation เป็นทางเลือกนึงในการเจรจาและ maintain API contract ระหว่างทีม แต่ปัญหาใหญ่ของ documentation ก็คือการทำให้มัน up-to-date อยู่ตลอดเวลา โดยเฉพาะเวลาที่เราต้องการ deprecate field ต่างๆออกจาก API ของเรา เราจะมั่นใจได้อย่างไรว่าฝั่ง client จะไม่ส่งอะไรแปลกๆมา
ข้อเสียของการแก้ปัญหาด้วย documentation ก็คือการที่มันไม่ได้ถูกบังคับใช้ในโค้ด นี่เป็นปัญหาหลักที่หลายๆทีมต้องเจอเวลา maintain API หลายๆตัว ต้องอัพเดตทั้งโค้ด ต้องอัพเดตทั้ง documentation ตอนอัพเดตถ้าไม่มี process ของการ review ที่ดีก็อาจจะมีความต่างระหว่าง actual implementation กับตัว documentation ที่เขียนก็เป็นได้
เคยต้องนั่งเขียน validation ให้ request parameter เพื่อเช็คว่าค่าที่ส่งมาเป็น type ที่เราบอกไว้ใน doc ไหมครับ ?
ถึงเราจะใช้ภาษา static type ก็ไม่มีอะไรมาการันตีว่า JSON payload ที่ client ส่งมามันจะเป็น type ที่ถูกต้องเพราะมันส่งมาเป็น text (JSON string) ผมเคยเจอหลายๆระบบที่พังเพราะแค่ลืม parse ตัวเลขจาก String ที่ส่งมาใน request payload ซึ่งมันก็ป้องกันได้ด้วยหลายวิธี แต่จะดีกว่าไหมถ้าเราไม่ต้องมีปัญหาแบบนี้เลย?
ปัญหาเหล่านี้ของคุณจะหมดไปถ้าคุณได้รู้จักกับ protobuf
message HelloWorldRequest { string name = 1; } message HelloWorldResponse { string greeting_text = 1; }
Protobuf คืออะไร
Protobuf หรือ Protocol Buffer เป็น open-source library จาก Google ที่ใช้ในการ generate code จาก resource ที่เรา define ไว้ในไฟล์ .proto ซึ่ง protobuf ถูกใช้เป็น mechanic หลักในการ generate code ที่ใช้ทั้งฝั่ง client และ server สำหรับ gRPC
หลายๆคนเข้าใจว่ามันเป็นส่วนหนึ่งของ gRPC protocol แต่ว่าความจริงแล้วทั้งสอง library เป็นคนละส่วนกันและเราสามารถนำ protobuf มาใช้กับ RESTful API ของเราได้
ด้วยความที่ทั้ง client และ server ใช้ proto file ร่วมกันและโค้ดที่ได้จาก proto file version เดียวกันจะเหมือนกันเสมอ ทำให้เราสามารถทิ้งภาพเดิมๆเวลาคุยกันเกี่ยวกับ REST API ไปได้เลย แทนที่จะต้องเขียน OpenAPI Spec หรือ Postman Collection ตัว contract ที่ใช้จะอยู่ที่ proto file แทน
// Sample message definition in a proto file message UserRequest { string first_name = 1; string last_name = 2; }
ข้อดีของการทำแบบนี้คือเราสามารถนำ contract ของเราเข้ามาใน development process ของเราได้อย่างตรงไปตรงมา เพราะ proto file เป็นแค่โค้ดเพิ่มอีกไฟล์นึงที่เราจะเก็บไว้ใน repo ของ service เรา หรือ จะเก็บเป็น repo แยกก็สามารถทำได้ แบบนี้แปลว่า
- เราสามารถใช้ proto file generate library สำหรับเรียก API ที่ใช้ได้ทั้งฝั่ง client และ server ซึ่งทำให้ contract ไม่ใช่อยู่แต่ใน document แต่ใช้เป็น dependency ของ project ได้เลย
- เมื่อมี library เราก็สามารถทำ semantic versioning ได้ง่าย ทำให้การสื่อสารเวลามี breaking change ง่ายขึ้นไปอีก
- เวลามี contract update เราสามารถ trigger CI pipeline เพื่อให้ publish generated code version ใหม่ได้อัตโนมัติ ไม่ต้องแก้โค้ดแล้วไปแก้ doc เหมือนอย่างเก่า
- Protobuf มี type แปลว่าเราสามารถมั่นใจได้เลยว่าถ้า client ส่งอะไรมาเราจะไม่ต้องห่วงเรื่อง type อีกต่อไป เมื่อนำมาใช้กับภาษาที่เป็น static type จะค่อนข้างฟินมากๆ
- ฝั่ง client ไม่ต้องเขียน data model เพื่อยิง API เพราะโค้ดถูก generate มาให้อัตโนมัติ
- ฝั่ง server ไม่ต้องเขียน mapper หรือ parser เพราะโค้ดถูก generate มาให้เช่นเดียวกัน
- Performance ของการ parse ข้อมูลที่เป็น protobuf เร็วกว่าการ JSON ค่อนข้างมาก เพราะข้อมูลที่ส่งเป็น binary (octet-stream)
อีกหนึ่งเรื่องที่ต้องคิดในกรณีที่เราคือใช้ protobuf คือถ้า client เป็น external เราต้องแชร์ proto file หรือ generated code ของเราให้ด้วย ไม่อย่างนั้นฝั่ง client จะไม่สามารถสร้าง payload สำหรับ API เราได้ ผมยังมองว่ามันเป็นข้อดีเพราะแทนที่ client จะต้องมานั่งเขียน custom API client จาก document ที่ไม่รู้ว่าจะตรง spec ทั้งหมดหรือไม่ ฝั่งนั้นสามารถมั่นใจได้เลยว่า API client ไม่มี bug พวก field ผิด หรือ type ข้อมูลผิดแน่นอน
ถึงตรงนี้หลายๆคนอาจจะยังไม่เห็นภาพ ผมทำโค้ดตัวอย่างที่ client เป็น JavaScript กับ Ruby และ server เป็น Go เป็นตัวอย่างไว้ที่ repo นี้ครับ
https://github.com/vtno/restful-protobuf
โปรเจคนี้มี client สองตัวเป็นภาษา Ruby และ JavaScript ที่ใช้ auto-generated code จาก proto file ที่ชื่อ hello.proto และฝั่ง server ก็นำ payload ที่เป็น protobuf มาแกะและประกอบ response ที่เป็น protobuf อีกตัวส่งกลับไป
สำหรับคนที่อย่างลองเล่นสามารถอ่าน README.md แล้วลองนำไปรันบนเครื่องได้เลยครับ
สำหรับคนที่อย่างลองเล่นสามารถอ่าน README.md แล้วลองนำไปรันบนเครื่องได้เลยครับ
Protobuf ยังรองรับอีกหลากหลายภาษาทั้งที่เป็น official support จาก Google อย่าง
ภาษาอื่นๆอีกหลายภาษาก็มี 3rd party support เช่น Elixir และ Rust และตัว library ของ Protobuf เองก็ออกมาพักใหญ่ๆและเรียกได้ว่า battle-tested มาแล้ว (ที่ Unity เองก็ใช้ทำ gRPC service บน production) ผมเลยค่อนข้างมั่นใจที่จะแนะนำให้ได้ลองใช้กันครับ
Protobuf เป็น library ที่น่าสนใจสำหรับทีมที่อยาก maintain API contract ร่วมกับตัวโค้ดเลย การนำมันมาเป็นส่วนหนึ่งของ development process สามารถช่วยลดความน่าปวดหัวในการจัดการกับ API ทั้งฝั่ง client และ server ได้มากเลยครับ
ซึ่ง process การสร้างและ maintain API ด้วย protobuf จะเป็นไปประมาณนี้
- service maintainer เขียน proto file ออกมาแชร์กับทีมที่เกี่ยวข้อง
- ใช้ code review ในการปรับ contract จนได้ stable version
- ทำ pipeline generate code และ publish เป็น shared libraryในภาษาที่แต่ละทีมใช้ (ทีมภายในเป็น polyglot ก็ยังไม่มีปัญหา) ซึ่งตรงนี้ก็ทำ semantic versioning ไปเลย
- ฝั่ง server และ client เพิ่ม library (API) ตัวนี้เป็น dependency ในโปรเจค
- client ยิง API ด้วย generated code / server parse payload และคืน response ด้วย generated code
- ถ้ามีการปรับ version ให้เปิด pull request, merge และ publish version ใหม่
- แต่ละโปรเจคที่มี dependency ก็แค่ update library เพื่อใช้ API version ใหม่
เมื่อเรามี API contract เป็น dependency ของโปรเจค มันช่วยลด human error ลดเวลาในการเขียน data model เพิ่มความเร็วในการ serialize / deserialize payload และยังเปิดช่องให้ทีมสามารถเริ่มใช้เทคนิคอย่าง Contract Testing ได้ง่ายยิ่งขึ้น
ของดี ลองเถอะ!
ใครที่ลองแล้วมีปัญหา อยากลองแล้วไม่รู้จะเริ่มทางไหน หรือ อยากสอบถามเพิ่มเติม สามารถทักมาถามบน Twitter ที่ @v_tno ได้เลยครับ