Technology Choices (ตัวเลือกเทคโนโลยี)
มี เทคโนโลยีมากมายที่เราอาจดูได้ แต่แทนที่จะมองอย่างกว้างๆ ที่รายการตัวเลือกยาวเหยียด ผมจะเน้นตัวเลือกที่ได้รับความนิยมและน่าสนใจบางส่วน นี่คือตัวเลือกที่เราจะดูกัน:
Remote procedure calls
Framework ที่อนุญาตให้การเรียก method แบบ local ถูก invoke บน process ที่อยู่ระยะไกล ตัวเลือกทั่วไปได้แก่ SOAP และ gRPC
REST
สไตล์สถาปัตยกรรมที่คุณเปิดเผย resource (Customer, Order เป็นต้น) ที่สามารถเข้าถึงได้ด้วยชุด verb ทั่วไป (GET, POST) มีรายละเอียดมากกว่านี้เกี่ยวกับ REST แต่เราจะพูดถึงในไม่ช้า
GraphQL
โปรโตคอลที่ค่อนข้างใหม่ ซึ่งอนุญาตให้ผู้เรียกใช้กำหนด query แบบกำหนดเองที่สามารถดึงข้อมูลจากไมโครเซอร์วิส downstream หลายตัว โดยกรองผลลัพธ์ให้คืนเฉพาะสิ่งที่ต้องการ
Message brokers
Middleware ที่อนุญาตให้เกิดการสื่อสารแบบ asynchronous ผ่าน queue หรือ topic
Remote Procedure Calls (การเรียก procedure ระยะไกล)
Remote procedure call (RPC) หมายถึง เทคนิคของการเรียกแบบ local แต่ให้มันไปทำงานบน service ระยะไกลที่ไหนสักแห่ง มี RPC implementation ที่แตกต่างกันหลายแบบที่ใช้กันอยู่ เทคโนโลยีส่วนใหญ่ในพื้นที่นี้ต้องการ schema ที่ชัดเจน เช่น SOAP หรือ gRPC ในบริบทของ RPC มักเรียก schema นี้ว่า interface definition language (IDL) โดย SOAP เรียก schema format ของตัวเองว่า web service definition language (WSDL) การใช้ schema แยกต่างหากทำให้ง่ายขึ้นที่จะ generate client และ server stub สำหรับ technology stack ที่ต่างกัน—ตัวอย่างเช่น ผมสามารถมี Java server ที่เปิดเผย SOAP interface และ .NET client ที่ generate มาจาก WSDL definition ของ interface เดียวกัน เทคโนโลยีอื่นๆ เช่น Java RMI ต้องการ coupling ที่แน่นกว่าระหว่าง client และ server โดยกำหนดให้ทั้งสองฝั่งใช้เทคโนโลยีพื้นฐานเดียวกัน แต่หลีกเลี่ยงความจำเป็น ในการมี service definition ที่ชัดเจน เพราะ service definition ถูกให้มาโดยนัยผ่าน Java type definition อยู่แล้ว เทคโนโลยีเหล่านี้ทั้งหมดมีลักษณะสำคัญร่วมกัน: พวกมันทำให้การเรียกระยะไกลดูเหมือนการเรียกแบบ local
โดยทั่วไปแล้ว การใช้เทคโนโลยี RPC หมายความว่าคุณกำลังผูกตัวเองเข้ากับ serialization protocol หนึ่งๆ RPC framework จะกำหนดว่าข้อมูลถูก serialize และ deserialize อย่างไร ตัวอย่างเช่น gRPC ใช้ protocol buffer serialization format สำหรับจุดประสงค์นี้ บาง implementation ผูกติดกับ networking protocol เฉพาะ (เช่น SOAP ซึ่งใช้ HTTP แบบผิวเผิน) ในขณะที่ตัวอื่นๆ อาจให้คุณใช้ networking protocol ประเภทต่างๆ ซึ่งสามารถให้ feature เพิ่มเติมได้ ตัวอย่างเช่น TCP ให้การรับประกันเรื่องการส่งข้อมูล ในขณะที่ UDP ไม่ให้ แต่มี overhead ที่ต่ำกว่ามาก สิ่งนี้ทำให้คุณสามารถใช้ networking technology ที่ต่างกันสำหรับ use case ที่ต่างกันได้
RPC framework ที่มี schema ชัดเจนทำให้การ generate client code เป็นเรื่องง่ายมาก สิ่งนี้สามารถหลีกเลี่ยงความจำเป็นในการมี client library เพราะ client ใดๆ ก็สามารถ generate code ของตัวเองตาม service specification นี้ได้ อย่างไรก็ตาม สำหรับการ generate code ฝั่ง client ให้ทำงานได้ client จำเป็นต้องมีวิธีดึง schema แบบ out of band—กล่าวคือ ผู้เรียกใช้ต้องมีสิทธิ์เข้าถึง schema ก่อนที่จะวางแผนเรียกใช้งาน Avro RPC เป็นข้อยกเว้นที่น่าสนใจตรงนี้ เพราะมันมีตัวเลือกที่จะส่ง schema ทั้งหมดไปพร้อมกับ payload ทำให้ client สามารถตีความ schema แบบ dynamic ได้
ความง่ายในการ generate client-side code เป็นหนึ่งในจุดขายหลักของ RPC ความจริงที่ว่าผมแค่ทำการเรียก method แบบปกติ และในทางทฤษฎีก็ไม่ต้องสนใจส่วนที่เหลือ เป็นข้อได้เปรียบที่ยิ่งใหญ่มาก
Challenges (ความท้าทาย)
อย่างที่เราได้เห็นกันไป RPC มีข้อได้เปรียบที่ดีมาก แต่ก็ไม่ได้ปราศจากข้อเสีย—และ RPC implementation บางตัวก็มีปัญหามากกว่าตัวอื่นๆ ปัญหาหลายอย่างในนี้สามารถจัดการได้ แต่ก็ควรค่าแก่การสำรวจเพิ่มเติม
Technology coupling (การผูกติดกับเทคโนโลยี)
RPC mechanism บางตัว เช่น Java RMI ผูกติดอย่างแน่นหนากับ platform เฉพาะ ซึ่งจำกัดเทคโนโลยีที่สามารถใช้ได้ทั้งฝั่ง client และ server Thrift และ gRPC มีการรองรับภาษาทางเลือกในระดับที่น่าประทับใจ ซึ่งช่วยลดข้อเสียนี้ลงได้บ้าง แต่ควรตระหนักว่าเทคโนโลยี RPC บางครั้งก็มาพร้อมข้อจำกัดด้าน interoperability
ในแง่หนึ่ง technology coupling นี้อาจถือเป็นรูปแบบหนึ่งของการเปิดเผยรายละเอียด internal technical implementation ตัวอย่างเช่น การใช้ RMI ผูก client เข้ากับ JVM แต่ก็ผูก server เข้ากับ JVM ด้วยเช่นกัน
พูดตามตรง มี RPC implementation จำนวนหนึ่งที่ไม่มีข้อจำกัดนี้—gRPC, SOAP และ Thrift ล้วนเป็นตัวอย่างที่รองรับ interoperability ระหว่าง technology stack ที่ต่างกัน
Local calls are not like remote calls (การเรียกแบบ local ไม่เหมือนการเรียกแบบ remote)
แนวคิด หลักของ RPC คือการซ่อนความซับซ้อนของการเรียกระยะไกล อย่างไรก็ตาม สิ่งนี้อาจนำไปสู่การซ่อนมากเกินไป แนวโน้มในบางรูปแบบของ RPC ที่จะทำให้ remote method call ดูเหมือน local method call นั้นซ่อนความจริงที่ว่าสองสิ่งนี้แตกต่างกันมาก ผมสามารถทำการเรียกแบบ local, in-process จำนวนมากได้โดยไม่ต้องกังวลเรื่อง performance มากนัก แต่กับ RPC ต้นทุนของการ marshaling และ unmarshaling payload อาจมีนัยสำคัญ ยังไม่นับเวลาที่ใช้ในการส่งข้อมูลผ่านเครือข่าย นั่นหมายความว่าคุณต้องคิดเกี่ยวกับ การออกแบบ API ต่างออกไปสำหรับ remote interface เทียบกับ local interface การเอา local API มาแล้วพยายามทำให้มันเป็น service boundary โดยไม่คิดอะไรเพิ่มเติมมีแนวโน้มที่จะทำให้คุณเจอปัญหา ในตัวอย่างที่แย่ที่สุด นักพัฒนาอาจใช้การเรียกระยะไกลโดยไม่รู้ตัว ถ้า abstraction นั้นทึบเกินไป
คุณต้องคิดถึงตัวเครือข่ายเอง เป็นที่รู้กันดีว่าข้อแรกของ fallacies of distributed computing คือ "The network is reliable" เครือข่าย ไม่ น่าเชื่อถือ พวกมันสามารถและจะล้มเหลวได้ แม้ว่า client และ server ที่คุณกำลังคุยด้วยจะปกติดีก็ตาม พวกมันอาจล้มเหลวเร็ว อาจล้มเหลวช้า และอาจถึงขั้นทำให้ packet ของคุณผิดรูปได้ด้วย คุณควรสมมติว่าเครือข่ายของคุณเต็มไปด้วยสิ่งชั่วร้าย ที่พร้อมจะปล่อยความโกรธเกรี้ยวออกมาแบบไม่มีปี่มีขลุ่ย ดังนั้นคุณควรคาดหวังที่จะเจอกับ failure mode ประเภทต่างๆ ที่คุณอาจไม่เคยต้องรับมือมาก่อนใน software แบบ monolithic ที่เรียบง่ายกว่า failure อาจเกิดจากการที่ remote server คืน error กลับมา หรือจากการที่คุณเรียกผิดพลาด คุณบอกความแตกต่างได้ไหม และถ้าบอกได้ คุณทำอะไรกับมันได้บ้าง? แล้วคุณจะทำอย่างไรเมื่อ remote server เริ่มตอบสนองช้าลง? เราจะพูดถึงหัวข้อนี้เมื่อคุยเรื่อง resiliency ใน Chapter 12
Brittleness (ความเปราะบาง)
Implementation ยอดนิยมบางตัวของ RPC อาจนำไปสู่ความเปราะบางแบบร้ายกาจ Java RMI เป็นตัวอย่างที่ดีมาก ลองพิจารณา Java interface แบบง่ายๆ ที่เราตัดสินใจทำเป็น remote API สำหรับ service Customer ของเรา Example 5-1 ประกาศ method ที่เราจะเปิดเผยออกไปแบบ remote Java RMI จะ generate client และ server stub สำหรับ method ของเรา
Example 5-1. Defining a service endpoint using Java RMI
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface CustomerRemote extends Remote {
public Customer findCustomer(String id) throws RemoteException;
public Customer createCustomer(
String firstname, String surname, String emailAddress)
throws RemoteException;
}
ใน interface นี้ createCustomer รับชื่อจริง, นามสกุล และ email address จะเกิดอะไรขึ้นถ้าเราตัดสินใจอนุญาตให้ object Customer ถูกสร้างได้ด้วย email address เพียงอย่างเดียว? เราสามารถเพิ่ม method ใหม่ ณ จุดนี้ได้ง่ายๆ แบบนี้:
...
public Customer createCustomer(String emailAddress) throws RemoteException;
...
ปัญหาคือตอนนี้เราต้อง regenerate client stub ด้วย client ที่ต้องการใช้ method ใหม่จำเป็นต้องมี stub ใหม่ และขึ้นอยู่กับลักษณะของการเปลี่ยนแปลง specification ผู้เรียกใช้ที่ไม่ต้องการ method ใหม่ก็อาจต้อง upgrade stub ของตัวเองด้วย แน่นอนว่าเรื่องนี้จัดการได้ แต่ก็มีขีดจำกัด ความจริงคือการเปลี่ยนแปลงแบบนี้เกิดขึ้นบ่อยพอสมควร RPC endpoint มักจะจบลงด้วยการมี method จำนวนมากสำหรับวิธีต่างๆ ในการสร้างหรือปฏิสัมพันธ์กับ object ส่วนหนึ่งเป็นเพราะเรายังคงคิดว่าการเรียกระยะไกลเหล่านี้ เป็นการเรียกแบบ local
แต่ก็มีความเปราะบางอีกประเภทหนึ่ง มาดูกันว่า object Customer ของเราหน้าตาเป็นอย่างไร:
public class Customer implements Serializable {
private String firstName;
private String surname;
private String emailAddress;
private String age;
}
จะเกิดอะไรขึ้นถ้าปรากฏว่า แม้เราจะเปิดเผยฟิลด์ age ใน object Customer ของเรา แต่ไม่มีผู้เรียกใช้รายไหนใช้มันเลย? เราตัดสินใจว่าอยากลบฟิลด์นี้ทิ้ง แต่ถ้า server implementation ลบ age ออกจาก definition ของ type นี้ และเราไม่ทำแบบเดียวกันกับผู้เรียกใช้ทั้งหมด แม้ว่าพวกเขาจะไม่เคยใช้ฟิลด์นั้นเลย code ที่เกี่ยวข้องกับการ deserialize object Customer ที่ฝั่งผู้เรียกใช้ก็จะพัง ในการ roll out การเปลี่ยนแปลงนี้ เราต้องเปลี่ยน client code ให้รองรับ definition ใหม่ และ deploy client ที่อัปเดตแล้วพร้อมๆ กับการ roll out server version ใหม่ นี่คือความท้าทายสำคัญของ RPC mechanism ใดๆ ที่สนับสนุนการใช้ binary stub generation: คุณจะไม่สามารถแยก client และ server deployment ออกจากกันได้ ถ้าคุณใช้เทคโนโลยีนี้ lockstep release อาจอยู่ในอนาคตของคุณ
ปัญหาคล้ายๆ กันจะเกิดขึ้นถ้าเราต้องการปรับโครงสร้าง object Customer ใหม่ แม้เราจะไม่ได้ลบฟิลด์ใดออก—ตัวอย่างเช่น ถ้าเราต้องการห่อหุ้ม firstName และ surname ไว้ใน type naming ใหม่เพื่อให้จัดการง่ายขึ้น แน่นอนว่าเราสามารถแก้ปัญหานี้ได้โดยส่ง dictionary type เป็น parameter ของการเรียก แต่ถึงจุดนั้น เราก็จะเสียประโยชน์หลายอย่างของ generated stub ไป เพราะเรายังต้องจับคู่และดึงฟิลด์ที่ต้องการด้วยมืออยู่ดี
ในทางปฏิบัติ object ที่ใช้เป็นส่วนหนึ่งของ binary serialization ผ่านสายส่งสามารถคิดได้ว่าเป็น type แบบ "expand-only" ความเปราะบางนี้ส่งผลให้ type ที่เปิดเผยผ่านสายส่งกลายเป็นกองฟิลด์ ซึ่งบางฟิลด์ก็ไม่ได้ถูกใช้แล้วแต่ก็ไม่สามารถลบออกได้อย่างปลอดภัย
Where to use it (จะใช้มันเมื่อไหร่)
แม้จะมีข้อเสีย แต่จริงๆ แล้วผมค่อนข้างชอบ RPC และ implementation ที่ทันสมัยกว่าอย่าง gRPC ก็ยอดเยี่ยม ในขณะที่ implementation อื่นๆ มีปัญหาที่มากพอที่จะทำให้ผมหลีกเลี่ยงมัน ยกตัวอย่างเช่น Java RMI มีปัญหาหลายอย่างเกี่ยวกับความเปราะบาง และตัวเลือกเทคโนโลยีที่จำกัด ส่วน SOAP ก็ค่อนข้างหนักจากมุมมองของนักพัฒนา โดยเฉพาะเมื่อเทียบกับตัวเลือกที่ทันสมัยกว่า
แค่ตระหนักถึงข้อควรระวังบางอย่างที่เกี่ยวข้องกับ RPC ถ้าคุณจะเลือกใช้โมเดลนี้ อย่า abstract การเรียกระยะไกลของคุณ จนถึงจุดที่เครือข่ายถูกซ่อนไว้อย่างสมบูรณ์ และให้แน่ใจว่าคุณสามารถพัฒนา server interface ต่อไปได้โดยไม่ต้องยืนกรานให้ client ต้อง upgrade แบบ lockstep การหาจุดสมดุลที่เหมาะสมสำหรับ client code ของคุณเป็นเรื่องสำคัญ ตัวอย่างเช่น ตรวจสอบให้แน่ใจว่า client ของคุณไม่ได้ไม่รู้ตัวเลยว่ากำลังมีการเรียกผ่านเครือข่ายเกิดขึ้น Client library มักถูกใช้ในบริบทของ RPC และถ้าไม่ได้ถูกจัดโครงสร้างอย่างเหมาะสม มันอาจกลายเป็นปัญหาได้ เราจะพูดถึงเรื่องนี้เพิ่มเติมในไม่ช้า
ถ้าผมกำลังมองหาตัวเลือกในพื้นที่นี้ gRPC จะอยู่อันดับต้นๆ ในรายการของผม มันถูกสร้างมาเพื่อใช้ประโยชน์จาก HTTP/2 มี performance characteristic ที่น่าประทับใจ และใช้งานง่ายโดยทั่วไป ผมยังชื่นชอบ ecosystem รอบๆ gRPC รวมถึงเครื่องมืออย่าง Protolock ซึ่งเป็นสิ่งที่เราจะพูดถึงในภายหลังของบทนี้เมื่อคุยเรื่อง schema
gRPC เหมาะกับโมเดล synchronous request-response แต่ก็สามารถทำงานร่วมกับ reactive extensions ได้ด้วย มันเป็นตัวเลือกอันดับต้นๆ ของผมเมื่อไหร่ก็ตามที่ผมมีการควบคุมทั้งฝั่ง client และ server ค่อนข้างมาก ถ้าคุณต้องรองรับ application อื่นๆ ที่หลากหลายที่อาจต้องคุยกับไมโครเซอร์วิสของคุณ ความจำเป็นในการ compile client-side code ตาม server-side schema อาจเป็นปัญหาได้ ในกรณีนั้น API แบบ REST over HTTP บางรูปแบบน่าจะเหมาะสมกว่า
REST
Representational State Transfer (REST) เป็น สไตล์สถาปัตยกรรมที่ได้แรงบันดาลใจจากเว็บ มีหลักการและข้อจำกัดมากมายที่อยู่เบื้องหลังสไตล์ REST แต่เราจะเน้นเฉพาะส่วนที่ช่วยเราจริงๆ เมื่อเราเผชิญกับความท้าทายด้าน integration ในโลกไมโครเซอร์วิส และเมื่อเรากำลังมองหาทางเลือกอื่นแทน RPC สำหรับ interface service ของเรา
สิ่งสำคัญที่สุดเมื่อคิดถึง REST คือแนวคิดของ resource คุณสามารถคิดว่า resource เป็นสิ่งที่ตัว service เองรู้จัก เช่น Customer Server จะสร้าง representation ที่ต่างกันของ Customer นี้ตามคำขอ วิธีที่ resource ถูกแสดงออกไปภายนอกนั้นแยกออกจากกันโดยสิ้นเชิงกับวิธีที่มันถูกจัดเก็บภายใน client อาจขอ JSON representation ของ Customer ได้ ตัวอย่างเช่น แม้ว่ามันจะถูกเก็บในรูปแบบที่ต่างออกไปโดยสิ้นเชิงก็ตาม เมื่อ client มี representation ของ Customer นี้แล้ว มันก็สามารถส่ง request เพื่อเปลี่ยนแปลงมันได้ และ server อาจจะทำตามหรือไม่ทำตามก็ได้
มี REST หลายสไตล์ และผมจะแตะเพียงเล็กน้อยที่นี่ ผมขอแนะนำอย่างยิ่งให้คุณลองดู Richardson Maturity Model ซึ่งเปรียบเทียบสไตล์ต่างๆ ของ REST ไว้
REST เองไม่ได้พูดถึง underlying protocol จริงๆ แม้ว่ามันจะถูกใช้บน HTTP บ่อยที่สุด ผมเคยเห็น implementation ของ REST ที่ใช้ protocol ที่ต่างกันมากมาก่อน แม้ว่ามันอาจต้องใช้ความพยายามมาก feature บางอย่างที่ HTTP มอบให้เราเป็นส่วนหนึ่งของ specification เช่น verb ทำให้การ implement REST บน HTTP ง่ายขึ้น ในขณะที่กับ protocol อื่นๆ คุณจะต้องจัดการ feature เหล่านี้ด้วยตัวเอง
REST and HTTP (REST กับ HTTP)
HTTP เองก็กำหนด capability ที่มีประโยชน์บางอย่างที่เข้ากันได้ดีมากกับสไตล์ REST ตัวอย่างเช่น HTTP verb (เช่น GET, POST และ PUT) มีความหมายที่เข้าใจกันดีอยู่แล้วใน HTTP specification ว่าควรทำงานอย่างไรกับ resource สไตล์สถาปัตยกรรม REST บอกเราจริงๆ ว่า verb เหล่านี้ควรทำงานแบบเดียวกันบน resource ทุกตัว และ HTTP specification ก็บังเอิญกำหนด verb หลายตัวที่เราสามารถใช้ได้ GET ดึง resource มาแบบ idempotent ตัวอย่างเช่น และ POST สร้าง resource ใหม่ นั่นหมายความว่าเราสามารถหลีกเลี่ยง method createCustomer หรือ editCustomer ที่ต่างกันจำนวนมากได้ แทนที่จะทำแบบนั้น เราแค่ POST customer representation เพื่อขอให้ server สร้าง resource ใหม่ แล้วเราก็สามารถเริ่ม GET request เพื่อดึง representation ของ resource ได้ ในเชิงแนวคิด มี endpoint เดียวในรูปแบบของ resource Customer ในกรณีเหล่านี้ และ operation ที่เราสามารถทำได้กับมันก็ถูกฝังไว้ใน HTTP protocol อยู่แล้ว
HTTP ยังนำ ecosystem ขนาดใหญ่ของเครื่องมือและเทคโนโลยีสนับสนุนมาด้วย เราได้ใช้ HTTP caching proxy อย่าง Varnish และ load balancer อย่าง mod_proxy และเครื่องมือ monitoring หลายตัวก็มีการรองรับ HTTP อยู่แล้วในตัว building block เหล่านี้ทำให้เราสามารถจัดการ HTTP traffic ปริมาณมากและ route มันได้อย่างชาญฉลาด และค่อนข้างโปร่งใส เรายังได้ใช้ security control ที่มีอยู่ทั้งหมดกับ HTTP เพื่อรักษาความปลอดภัยการสื่อสารของเรา ตั้งแต่ basic auth ไปจนถึง client cert HTTP ecosystem มอบเครื่องมือมากมายให้เรา เพื่อทำให้กระบวนการรักษาความปลอดภัยง่ายขึ้น และเราจะสำรวจหัวข้อนั้นเพิ่มเติมใน Chapter 11 แต่กระนั้น เพื่อให้ได้ประโยชน์เหล่านี้ คุณต้องใช้ HTTP อย่างถูกวิธี ใช้มันไม่ดี มันก็จะไม่ปลอดภัยและ scale ได้ยากพอๆ กับเทคโนโลยีอื่นๆ ที่มีอยู่ แต่ใช้มันอย่างถูกต้อง คุณจะได้รับความช่วยเหลือมากมาย
ควรสังเกตว่า HTTP สามารถใช้ implement RPC ได้ด้วย SOAP ตัวอย่างเช่น ถูก route ผ่าน HTTP แต่น่าเสียดายที่มันใช้ specification เพียงเล็กน้อยเท่านั้น verb ถูกละเลย เช่นเดียวกับสิ่งง่ายๆ อย่าง HTTP error code ในทางกลับกัน gRPC ถูกออกแบบมาเพื่อใช้ประโยชน์จาก capability ของ HTTP/2 เช่น ความสามารถในการส่ง request-response stream หลายชุดผ่าน connection เดียว แต่แน่นอนว่า เมื่อใช้ gRPC คุณไม่ได้ทำ REST เพียงเพราะคุณกำลังใช้ HTTP!
Hypermedia as the engine of application state (Hypermedia ในฐานะกลไกขับเคลื่อน application state)
อีก หลักการหนึ่งที่ถูกนำเสนอใน REST ที่สามารถช่วยเราหลีกเลี่ยง coupling ระหว่าง client และ server คือแนวคิดของ hypermedia as the engine of application state (มักย่อว่า HATEOAS และเชื่อไหมว่ามันต้องการคำย่อจริงๆ) นี่เป็นถ้อยคำที่ค่อนข้างซับซ้อนและเป็นแนวคิดที่ค่อนข้างน่าสนใจ มาแยกย่อยมันกันสักหน่อย
Hypermedia เป็นแนวคิดที่เนื้อหาชิ้นหนึ่งมี link ไปยังเนื้อหาชิ้นอื่นๆ หลากหลายในรูปแบบต่างๆ (เช่น ข้อความ, รูปภาพ, เสียง) สิ่งนี้น่าจะคุ้นเคยกับคุณดี เพราะมันคือสิ่งที่เกิดขึ้นในหน้าเว็บทั่วไป: คุณคลิก link ซึ่งเป็นรูปแบบหนึ่งของ hypermedia control เพื่อดูเนื้อหาที่เกี่ยวข้อง แนวคิดเบื้องหลัง HATEOAS คือ client ควรทำ interaction กับ server (ซึ่งอาจนำไปสู่ state transition) ผ่าน link เหล่านี้ไปยัง resource อื่นๆ client ไม่จำเป็นต้องรู้ว่า customer อยู่ตรงไหนบน server โดยการรู้ว่าจะยิงไปที่ URI ไหน แต่ client จะมองหาและ navigate ผ่าน link เพื่อค้นหาสิ่งที่ต้องการ
นี่เป็นแนวคิดที่ค่อนข้างแปลก ดังนั้นมาถอยหลังกันก่อนแล้วพิจารณาว่าคนเรามีปฏิสัมพันธ์กับหน้าเว็บอย่างไร ซึ่งเราได้กำหนดไปแล้วว่าอุดมไปด้วย hypermedia control
ลองนึกถึงเว็บช้อปปิ้งอย่าง Amazon.com ตำแหน่งของตะกร้าสินค้าเปลี่ยนไปตามกาลเวลา กราฟิกก็เปลี่ยนไป link ก็เปลี่ยนไป แต่ในฐานะมนุษย์ เราฉลาดพอที่จะยังมองเห็นตะกร้าสินค้า รู้ว่ามันคืออะไร และมีปฏิสัมพันธ์กับมันได้ เราเข้าใจว่าตะกร้าสินค้าหมายถึงอะไร แม้ว่ารูปแบบและ control พื้นฐานที่ใช้แสดงมันจะเปลี่ยนไปก็ตาม เรารู้ว่าถ้าเราอยากดูตะกร้า นี่คือ control ที่เราต้องการมีปฏิสัมพันธ์ด้วย นี่คือวิธีที่หน้าเว็บสามารถเปลี่ยนแปลงไปทีละน้อยตามกาลเวลา ตราบใดที่ contract โดยนัยเหล่านี้ระหว่างลูกค้าและเว็บไซต์ยังคงถูกรักษาไว้ การเปลี่ยนแปลงก็ไม่จำเป็นต้องเป็น breaking change
ด้วย hypermedia control เรากำลังพยายามบรรลุระดับความ "ฉลาด" แบบเดียวกันนี้สำหรับผู้เรียกใช้แบบอิเล็กทรอนิกส์ของเรา มาดู hypermedia control ที่เราอาจมีสำหรับ MusicCorp กัน เราได้เข้าถึง resource ที่แสดง catalog entry สำหรับอัลบั้มหนึ่งใน Example 5-2 พร้อมกับข้อมูลเกี่ยวกับอัลบั้ม เราเห็น hypermedia control หลายตัว
Example 5-2. Hypermedia controls used on an album listing
<album>
<name>Give Blood</name>
<link rel="/artist" href="/artist/theBrakes" />
<description>
Awesome, short, brutish, funny and loud. Must buy!
</description>
<link rel="/instantpurchase" href="/instantPurchase/1234" />
</album>
Hypermedia control นี้บอกเราว่าจะหาข้อมูลเกี่ยวกับศิลปินได้ที่ไหน
และถ้าเราอยากซื้ออัลบั้ม ตอนนี้เราก็รู้แล้วว่าต้องไปที่ไหน
ในเอกสารนี้ เรามี hypermedia control สองตัว client ที่อ่านเอกสารแบบนี้ต้องรู้ว่า control ที่มี relation เป็น artist คือจุดที่ต้อง navigate ไปเพื่อดูข้อมูลเกี่ยวกับศิลปิน และ instantpurchase เป็นส่วนหนึ่งของ protocol ที่ใช้ซื้ออัลบั้ม client ต้องเข้าใจ semantic ของ API ในแบบเดียวกับที่มนุษย์ต้องเข้าใจว่า บนเว็บไซต์ช้อปปิ้ง ตะกร้าคือที่ที่สินค้าที่จะถูกซื้อจะอยู่
ในฐานะ client ผมไม่จำเป็นต้องรู้ว่า URI scheme ไหนที่ต้องเข้าถึงเพื่อ ซื้อ อัลบั้ม ผมแค่ต้องเข้าถึง resource ค้นหา control สำหรับซื้อ และ navigate ไปที่นั่น control สำหรับซื้ออาจเปลี่ยนตำแหน่ง URI อาจเปลี่ยน หรือแม้แต่ site อาจส่งผมไปยัง service อื่นเลยก็ได้ และในฐานะ client ผมก็ไม่ต้องสนใจ สิ่งนี้ทำให้เกิด decoupling จำนวนมากระหว่าง client และ server
เราถูก abstract ออกจากรายละเอียดพื้นฐานที่นี่อย่างมาก เราสามารถเปลี่ยน implementation ของวิธีที่ control ถูกนำเสนอได้ทั้งหมด ตราบใดที่ client ยังสามารถหา control ที่ตรงกับความเข้าใจของมันเกี่ยวกับ protocol ได้ ในลักษณะเดียวกับที่ control ตะกร้าสินค้า อาจเปลี่ยนจาก link ธรรมดาเป็น JavaScript control ที่ซับซ้อนกว่า เรายังมีอิสระที่จะเพิ่ม control ใหม่ๆ เข้าไปในเอกสาร ซึ่งอาจแสดง state transition ใหม่ที่เราสามารถทำได้กับ resource นั้นๆ เราจะทำให้ผู้เรียกใช้ของเราพังก็ต่อเมื่อเราเปลี่ยน semantic ของ control ตัวใดตัวหนึ่งอย่างพื้นฐานจนมันทำงานแตกต่างไปมาก หรือถ้าเราลบ control ออกไปทั้งหมด
ทฤษฎีคือว่า โดยการใช้ control เหล่านี้เพื่อ decouple client และ server เราจะได้รับประโยชน์ที่สำคัญเมื่อเวลาผ่านไป ซึ่งหวังว่าจะชดเชยเวลาที่เพิ่มขึ้นในการทำให้ protocol เหล่านี้ใช้งานได้ น่าเสียดายที่แม้แนวคิดเหล่านี้จะฟังดูสมเหตุสมผลในทางทฤษฎี แต่ผมพบว่า REST รูปแบบนี้แทบไม่ถูกนำมาใช้จริง ด้วยเหตุผลที่ผมยังไม่เข้าใจถ่องแท้ สิ่งนี้ทำให้ HATEOAS โดยเฉพาะเป็นแนวคิดที่ยากกว่า สำหรับผมที่จะสนับสนุนให้กับผู้ที่ใช้ REST อยู่แล้ว โดยพื้นฐานแล้ว แนวคิดหลายอย่างใน REST ถูกสร้างขึ้นบนสมมติฐานของการสร้างระบบ hypermedia แบบกระจาย ซึ่งไม่ใช่สิ่งที่คนส่วนใหญ่กำลังสร้างกันอยู่
Challenges (ความท้าทาย)
ในแง่ของความง่ายในการใช้งาน ในอดีตคุณจะไม่สามารถ generate client-side code สำหรับ REST over HTTP application protocol ของคุณได้เหมือนที่คุณทำได้กับ RPC implementation สิ่งนี้มักนำไปสู่การที่คนสร้าง REST API ที่มี client library ให้ผู้เรียกใช้ใช้งาน Client library เหล่านี้ให้ binding กับ API เพื่อทำให้ client integration ง่ายขึ้น ปัญหาคือ client library อาจก่อให้เกิดความท้าทาย บางอย่างเกี่ยวกับ coupling ระหว่าง client และ server ซึ่งเป็นสิ่งที่เราจะพูดถึงใน "DRY and the Perils of Code Reuse in a Microservice World"
ใน ช่วงหลายปีที่ผ่านมา ปัญหานี้ได้รับการบรรเทาลงบ้าง OpenAPI specification ที่เติบโตมาจากโปรเจกต์ Swagger ตอนนี้ให้ความสามารถในการกำหนดข้อมูลที่เพียงพอบน REST endpoint เพื่อให้สามารถ generate client-side code ได้ในหลากหลายภาษา จากประสบการณ์ของผม ผมไม่ค่อยเห็นทีมที่ใช้ประโยชน์จาก functionality นี้จริงๆ แม้ว่าพวกเขาจะใช้ Swagger สำหรับเอกสารอยู่แล้วก็ตาม ผมสงสัยว่านี่อาจเป็นเพราะความยากลำบากในการนำมันมาใช้กับ API ที่มีอยู่แล้ว ผมยังมีความกังวลเกี่ยวกับ specification ที่เคยถูกใช้แค่สำหรับเอกสาร แต่ตอนนี้ถูกใช้เพื่อกำหนด contract ที่ชัดเจนกว่า สิ่งนี้อาจนำไปสู่ specification ที่ซับซ้อนกว่ามาก—การเปรียบเทียบ OpenAPI schema กับ protocol buffer schema ตัวอย่างเช่น เป็นความแตกต่างที่ชัดเจนมาก แม้จะมีข้อกังวล แต่ก็เป็นเรื่องดีที่ตัวเลือกนี้มีอยู่แล้ว
Performance ก็อาจเป็นปัญหาได้เช่นกัน REST over HTTP payload อาจกระชับกว่า SOAP ได้จริง เพราะ REST รองรับ format ทางเลือกอย่าง JSON หรือแม้แต่ binary แต่มันก็ยังไม่ประหยัดเท่า binary protocol อย่าง Thrift Overhead ของ HTTP สำหรับแต่ละ request ก็อาจเป็นข้อกังวลสำหรับความต้องการ low-latency ด้วย HTTP protocol กระแสหลักทั้งหมดที่ใช้กันในปัจจุบันต้องการการใช้ Transmission Control Protocol (TCP) อยู่เบื้องหลัง ซึ่งมีความไม่มีประสิทธิภาพเมื่อเทียบกับ networking protocol อื่นๆ และ RPC implementation บางตัวอนุญาตให้คุณใช้ networking protocol ทางเลือกแทน TCP เช่น User Datagram Protocol (UDP)
ข้อจำกัดที่วางไว้บน HTTP เนื่องจากความจำเป็นต้องใช้ TCP กำลังถูกแก้ไข HTTP/3 ซึ่งกำลังอยู่ระหว่างกระบวนการสรุปในปัจจุบัน กำลังมองที่จะเปลี่ยนไปใช้ QUIC protocol ที่ใหม่กว่า QUIC ให้ความสามารถแบบเดียวกับ TCP (เช่น การรับประกันที่ดีขึ้นกว่า UDP) แต่มีการปรับปรุงที่สำคัญที่แสดงให้เห็นว่าให้ผลลัพธ์ที่ดีขึ้นทั้งในเรื่อง latency และลด bandwidth มันน่าจะใช้เวลาอีกหลายปี กว่า HTTP/3 จะส่งผลกระทบในวงกว้างต่อ internet สาธารณะ แต่ก็สมเหตุสมผลที่จะสมมติว่าองค์กรต่างๆ อาจได้รับประโยชน์ได้เร็วกว่านั้น ภายใน network ของตัวเอง
เมื่อพูดถึง HATEOAS โดยเฉพาะ คุณอาจเจอปัญหา performance เพิ่มเติม เนื่องจาก client ต้อง navigate control หลายตัวเพื่อหา endpoint ที่ถูกต้องสำหรับ operation หนึ่งๆ สิ่งนี้อาจนำไปสู่ protocol ที่ chatty มาก—อาจต้องมีการ round trip หลายครั้งสำหรับ แต่ละ operation ท้ายที่สุดแล้ว นี่คือ trade-off ถ้าคุณตัดสินใจใช้ REST สไตล์ HATEOAS ผมขอแนะนำให้คุณเริ่มด้วยการให้ client navigate control เหล่านี้ก่อน แล้วค่อย optimize ทีหลังถ้าจำเป็น จำไว้ว่าการใช้ HTTP ให้ความช่วยเหลือเราจำนวนมากได้ทันทีอยู่แล้ว ตามที่เราคุยกันไปก่อนหน้านี้ ความเลวร้ายของการ optimize ก่อนเวลาอันควรได้ถูกบันทึกไว้เป็นอย่างดีแล้ว ผมจึงไม่จำเป็นต้องขยายความ เพิ่มเติมที่นี่ ควรสังเกตด้วยว่าแนวทางเหล่านี้จำนวนมากถูกพัฒนาขึ้นเพื่อสร้างระบบ distributed hypertext และไม่ใช่ทุกแนวทางที่เหมาะสม! บางครั้งคุณจะพบว่าตัวเองแค่อยากได้ RPC แบบเก่าดีๆ
แม้จะมีข้อเสียเหล่านี้ REST over HTTP ก็เป็นตัวเลือกเริ่มต้นที่สมเหตุสมผลสำหรับการ interaction ระหว่าง service ต่อ service ถ้าคุณอยากรู้เพิ่มเติม ผมขอแนะนำ REST in Practice: Hypermedia and Systems Architecture (O'Reilly) โดย Jim Webber, Savas Parastatidis และ Ian Robinson ซึ่งครอบคลุมหัวข้อ REST over HTTP อย่างละเอียด
Where to use it (จะใช้มันเมื่อไหร่)
เนื่องจาก การใช้งานอย่างแพร่หลายในวงการ REST-over-HTTP-based API จึงเป็นตัวเลือกที่ชัดเจนสำหรับ synchronous request-response interface ถ้าคุณต้องการให้เข้าถึงได้จาก client หลากหลายที่สุดเท่าที่จะทำได้ การคิดว่า REST API เป็นแค่ตัวเลือกที่ "ใช้ได้สำหรับส่วนใหญ่" จะเป็นความผิดพลาด แต่มันก็มีส่วนจริงอยู่บ้าง มันเป็นสไตล์ interface ที่เข้าใจกันอย่างกว้างขวาง ที่คนส่วนใหญ่คุ้นเคย และรับประกัน interoperability จากเทคโนโลยีที่หลากหลายมาก
ส่วนใหญ่เป็นเพราะความสามารถของ HTTP และขอบเขตที่ REST ต่อยอดจากความสามารถเหล่านั้น (แทนที่จะซ่อนมัน) REST-based API จึงโดดเด่นในสถานการณ์ที่คุณต้องการ caching ของ request ขนาดใหญ่และมีประสิทธิภาพ ด้วยเหตุผลนี้ พวกมันจึงเป็นตัวเลือกที่ชัดเจน สำหรับการเปิดเผย API ให้ฝ่ายภายนอกหรือ client interface พวกมันอาจด้อยกว่าเมื่อเทียบกับ communication protocol ที่มีประสิทธิภาพกว่า และแม้ว่าคุณจะสามารถสร้าง asynchronous interaction protocol บน REST-based API ได้ แต่มันก็ไม่ได้เหมาะสมนักเมื่อเทียบกับทางเลือกอื่น สำหรับการสื่อสารระหว่างไมโครเซอร์วิสทั่วไป
แม้จะชื่นชมเป้าหมายเบื้องหลัง HATEOAS ในทางความคิด แต่ผมไม่เห็นหลักฐานมากนักว่างานเพิ่มเติมในการ implement REST สไตล์นี้ ให้ประโยชน์ที่คุ้มค่าในระยะยาว และผมก็จำไม่ได้ว่าในช่วงไม่กี่ปีที่ผ่านมาได้คุยกับทีมที่ implement สถาปัตยกรรมไมโครเซอร์วิสที่สามารถ พูดถึงคุณค่าของการใช้ HATEOAS ได้ ประสบการณ์ของผมเองแน่นอนว่าเป็นแค่ชุดข้อมูลชุดหนึ่งเท่านั้น และผมไม่สงสัยเลยว่าสำหรับบางคน HATEOAS อาจทำงานได้ดี แต่แนวคิดนี้ดูเหมือนจะไม่ได้รับความนิยมมากเท่าที่ผมคิดไว้ อาจเป็นเพราะแนวคิดเบื้องหลัง HATEOAS แปลกเกินไปสำหรับเราที่จะเข้าใจ หรืออาจเป็นเพราะขาดเครื่องมือหรือมาตรฐานในพื้นที่นี้ หรือบางทีโมเดลนี้ก็ใช้ไม่ได้กับระบบประเภทที่เรา สร้างกันขึ้นมา แน่นอนว่ามันก็เป็นไปได้เช่นกันที่แนวคิดเบื้องหลัง HATEOAS จะเข้ากันไม่ค่อยดีกับวิธีที่เราสร้างไมโครเซอร์วิส
ดังนั้นสำหรับการใช้ที่ perimeter มันทำงานได้ดีเยี่ยม และสำหรับการสื่อสารแบบ synchronous request-response ระหว่างไมโครเซอร์วิส มันก็ยอดเยี่ยม
GraphQL
ใน ช่วงหลายปีที่ผ่านมา GraphQL ได้รับความนิยมมากขึ้น ส่วนใหญ่เป็นเพราะข้อเท็จจริงที่ว่ามันโดดเด่นในพื้นที่เฉพาะหนึ่ง กล่าวคือ มันทำให้อุปกรณ์ฝั่ง client สามารถกำหนด query ที่หลีกเลี่ยงความจำเป็นในการยิง request หลายครั้งเพื่อดึงข้อมูลเดียวกันได้ สิ่งนี้สามารถให้การปรับปรุงที่สำคัญ ในแง่ของ performance ของอุปกรณ์ฝั่ง client ที่มีข้อจำกัด และยังหลีกเลี่ยงความจำเป็นในการ implement server-side aggregation แบบเฉพาะกิจ
ลองยกตัวอย่างง่ายๆ ลองนึกภาพอุปกรณ์มือถือที่อยากแสดงหน้าภาพรวม order ล่าสุดของลูกค้า หน้านี้ต้องมีข้อมูลเกี่ยวกับลูกค้า พร้อมกับข้อมูลเกี่ยวกับ order ห้ารายการล่าสุดของลูกค้า หน้าจอต้องการเพียงไม่กี่ฟิลด์จาก customer record และต้องการแค่วันที่, มูลค่า และสถานะการจัดส่งของแต่ละ order อุปกรณ์มือถือสามารถยิง call ไปยังไมโครเซอร์วิส downstream สองตัวเพื่อดึงข้อมูลที่ต้องการได้ แต่นี่จะเกี่ยวข้องกับการยิง call หลายครั้ง รวมถึงการดึงข้อมูลที่ไม่จำเป็นจริงๆ กลับมาด้วย โดยเฉพาะกับอุปกรณ์มือถือ สิ่งนี้อาจสิ้นเปลือง— มันใช้ data plan ของอุปกรณ์มือถือมากกว่าที่จำเป็น และอาจใช้เวลานานกว่า
GraphQL อนุญาตให้อุปกรณ์มือถือยิง query เดียวที่สามารถดึงข้อมูลที่ต้องการทั้งหมดกลับมาได้ สำหรับสิ่งนี้จะทำงานได้ คุณต้องมี ไมโครเซอร์วิสที่เปิดเผย GraphQL endpoint ให้กับอุปกรณ์ client GraphQL endpoint นี้คือทางเข้าสำหรับ query ของ client ทั้งหมด และเปิดเผย schema ให้อุปกรณ์ client ใช้ schema นี้เปิดเผย type ที่มีให้ client และยังมี graphical query builder ที่ดีอยู่ด้วย เพื่อทำให้การสร้าง query เหล่านี้ง่ายขึ้น การลดจำนวน call และปริมาณข้อมูลที่ดึงมาโดยอุปกรณ์ client ช่วยให้คุณจัดการความท้าทาย บางอย่างที่เกิดขึ้นเมื่อสร้าง user interface ด้วยสถาปัตยกรรมไมโครเซอร์วิสได้อย่างเรียบร้อย
Challenges (ความท้าทาย)
ในช่วงแรก ความท้าทายอย่างหนึ่งคือการขาดการรองรับภาษาสำหรับ GraphQL specification โดยมี JavaScript เป็นตัวเลือกเดียวในตอนแรก สิ่งนี้ได้รับการปรับปรุงอย่างมาก โดยเทคโนโลยีหลักๆ ทั้งหมดตอนนี้รองรับ specification แล้ว ที่จริงแล้ว มีการปรับปรุงที่สำคัญ ใน GraphQL และ implementation ต่างๆ โดยรวม ทำให้ GraphQL เป็นทางเลือกที่มีความเสี่ยงน้อยกว่าเมื่อเทียบกับเมื่อไม่กี่ปีก่อน กระนั้น คุณอาจอยากรู้เกี่ยวกับความท้าทายบางส่วนที่ยังคงเหลืออยู่กับเทคโนโลยีนี้
อย่างแรก อุปกรณ์ client สามารถยิง query ที่เปลี่ยนแปลงแบบ dynamic ได้ และผมเคยได้ยินเรื่องทีมที่มีปัญหากับ GraphQL query ที่ทำให้เกิด load อย่างมากบนฝั่ง server อันเป็นผลมาจากความสามารถนี้ เมื่อเราเปรียบเทียบ GraphQL กับอะไรบางอย่างเช่น SQL เราเห็นปัญหาที่คล้ายกัน SQL statement ที่มีต้นทุนสูงสามารถก่อให้เกิดปัญหาสำคัญกับฐานข้อมูล และอาจส่งผลกระทบขนาดใหญ่ต่อระบบ ในวงกว้างได้ ปัญหาเดียวกันนี้เกิดขึ้นกับ GraphQL ความแตกต่างคือกับ SQL เราอย่างน้อยก็มีเครื่องมืออย่าง query planner สำหรับฐานข้อมูลของเรา ซึ่งช่วยเราวินิจฉัย query ที่มีปัญหาได้ ในขณะที่ปัญหาคล้ายกันกับ GraphQL อาจติดตามได้ยากกว่า Server-side throttling ของ request เป็นทางแก้ที่เป็นไปได้อย่างหนึ่ง แต่เนื่องจากการประมวลผล call อาจกระจายไปยังไมโครเซอร์วิสหลายตัว นี่จึงไม่ใช่เรื่องง่ายเลย
เมื่อเทียบกับ REST-based HTTP API ทั่วไป caching ก็ซับซ้อนกว่าด้วยเช่นกัน กับ REST-based API ผมสามารถตั้งค่า response header หนึ่งในหลายๆ ตัวเพื่อช่วยอุปกรณ์ฝั่ง client หรือ intermediate cache อย่าง content delivery network (CDN) ให้ cache response เพื่อที่พวกมันจะได้ไม่ต้องถูก request อีก สิ่งนี้ไม่สามารถทำได้ในลักษณะเดียวกันกับ GraphQL คำแนะนำที่ผมเคยเห็นเกี่ยวกับปัญหานี้ ดูเหมือนจะวนอยู่กับการผูก ID กับทุก resource ที่ถูกคืนกลับมา (และจำไว้ว่า GraphQL query อาจมี resource หลายตัว) แล้วให้อุปกรณ์ client cache request นั้นไว้ตาม ID นั้น เท่าที่ผมทราบ สิ่งนี้ทำให้การใช้ CDN หรือ caching reverse proxy เป็นเรื่องยากมากโดยไม่ต้องทำงานเพิ่มเติม
แม้ผมจะเคยเห็นทางแก้เฉพาะ implementation สำหรับปัญหานี้ (เช่นที่พบใน Apollo implementation ของ JavaScript) caching ดูเหมือนจะถูกละเลยไม่ว่าจะโดยรู้ตัวหรือไม่รู้ตัว ในฐานะส่วนหนึ่งของการพัฒนา GraphQL ในช่วงแรก ถ้า query ที่คุณยิงนั้นเฉพาะเจาะจง มากสำหรับผู้ใช้คนหนึ่งๆ การขาด request-level caching นี้ก็อาจไม่ใช่ตัวขวางกั้นสำคัญ เพราะอัตราส่วน cache-hit ของคุณน่าจะต่ำอยู่แล้ว แต่ผมก็สงสัยว่าข้อจำกัดนี้จะหมายความว่าคุณจะยังจบลงด้วยทางออกแบบผสมสำหรับอุปกรณ์ client โดยมี request บางส่วน (ที่ทั่วไปกว่า) ไปผ่าน REST-based HTTP API ปกติ และ request อื่นๆ ไปผ่าน GraphQL หรือไม่
อีกปัญหาหนึ่งคือ แม้ในทางทฤษฎี GraphQL สามารถจัดการการเขียนได้ แต่มันดูเหมือนจะไม่เหมาะเท่ากับการอ่าน สิ่งนี้นำไปสู่สถานการณ์ ที่ทีมใช้ GraphQL สำหรับการอ่านแต่ใช้ REST สำหรับการเขียน
ปัญหาสุดท้ายอาจเป็นเรื่องที่ค่อนข้างเป็นความเห็นส่วนตัวโดยสิ้นเชิง แต่ผมก็ยังคิดว่ามันควรค่าแก่การพูดถึง GraphQL ทำให้รู้สึกเหมือน คุณกำลังทำงานกับข้อมูลเพียงอย่างเดียว ซึ่งอาจตอกย้ำแนวคิดที่ว่าไมโครเซอร์วิสที่คุณกำลังคุยด้วยเป็นแค่ wrapper ทับฐานข้อมูล อันที่จริง ผมเคยเห็นหลายคนเปรียบเทียบ GraphQL กับ OData ซึ่งเป็นเทคโนโลยีที่ออกแบบมาเป็น generic API สำหรับเข้าถึงข้อมูล จากฐานข้อมูล อย่างที่เราคุยกันไปอย่างละเอียดแล้ว แนวคิดของการปฏิบัติต่อไมโครเซอร์วิสเป็นแค่ wrapper ทับฐานข้อมูลอาจเป็นปัญหาได้มาก ไมโครเซอร์วิสเปิดเผยฟังก์ชันผ่าน networked interface ฟังก์ชันบางส่วนนั้นอาจต้องการหรือส่งผลให้ข้อมูลถูกเปิดเผย แต่พวกมันก็ยังควรมี logic และพฤติกรรมภายในของตัวเอง แค่เพราะคุณกำลังใช้ GraphQL อย่าหลงคิดว่าไมโครเซอร์วิสของคุณเป็นแค่ API บนฐานข้อมูล—มันสำคัญมากที่ GraphQL API ของคุณจะไม่ผูกติดกับ datastore ภายในของไมโครเซอร์วิสของคุณ
Where to use it (จะใช้มันเมื่อไหร่)
จุดแข็งของ GraphQL คือการใช้ที่ perimeter ของระบบ เปิดเผยฟังก์ชันให้ external client Client เหล่านี้มักเป็น GUI และมันเหมาะกับอุปกรณ์มือถืออย่างชัดเจน เนื่องจากข้อจำกัดของมันในแง่ความสามารถที่จำกัดในการแสดงข้อมูลให้ผู้ใช้ปลายทาง และธรรมชาติของเครือข่ายมือถือ แต่ GraphQL ก็ยังถูกใช้สำหรับ external API ด้วยเช่นกัน โดย GitHub เป็นผู้นำมาใช้แต่แรกๆ ถ้าคุณมี external API ที่มักต้องการให้ external client ยิง call หลายครั้งเพื่อได้ข้อมูลที่ต้องการ GraphQL ก็สามารถช่วยทำให้ API มีประสิทธิภาพและเป็นมิตรมากขึ้นได้
โดยพื้นฐานแล้ว GraphQL เป็นกลไก call aggregation และ filtering ดังนั้นในบริบทของสถาปัตยกรรมไมโครเซอร์วิส มันจะถูกใช้เพื่อ รวม call จากไมโครเซอร์วิส downstream หลายตัว ดังนั้นมันจึงไม่ใช่สิ่งที่จะมาแทนที่การสื่อสารระหว่างไมโครเซอร์วิสทั่วไป
ทางเลือกอื่นแทนการใช้ GraphQL คือการพิจารณา pattern ทางเลือกอย่าง backend for frontend (BFF) pattern—เราจะดู pattern นั้น และเปรียบเทียบกับ GraphQL และเทคนิค aggregation อื่นๆ ใน Chapter 14
Message Brokers
Message broker เป็นตัวกลาง ซึ่งมักถูกเรียกว่า middleware ที่อยู่ระหว่าง process เพื่อจัดการการสื่อสารระหว่างพวกมัน มันเป็นตัวเลือกยอดนิยมที่ช่วย implement การสื่อสารแบบ asynchronous ระหว่างไมโครเซอร์วิส เพราะมันมี capability ที่ทรงพลังหลากหลาย
อย่างที่เราคุยกันไปก่อนหน้านี้ message เป็นแนวคิดทั่วไปที่กำหนดสิ่งที่ message broker ส่ง message อาจมี request, response หรือ event แทนที่จะให้ไมโครเซอร์วิสหนึ่งสื่อสารกับอีกไมโครเซอร์วิสโดยตรง ไมโครเซอร์วิสจะส่ง message ให้ message broker แทน พร้อมข้อมูลว่า message นั้นควรถูกส่งอย่างไร
Topics and queues (Topic และ queue)
Broker มักให้ queue หรือ topic หรือทั้งสองอย่าง Queue โดยทั่วไปเป็นแบบ point to point ผู้ส่งวาง message บน queue และผู้เรียกใช้อ่านจาก queue นั้น กับระบบแบบ topic ผู้เรียกใช้หลายคนสามารถ subscribe topic ได้ และผู้เรียกใช้ที่ subscribe แต่ละคนจะได้รับสำเนาของ message นั้น
ผู้เรียกใช้อาจแทนไมโครเซอร์วิสหนึ่งตัวหรือมากกว่า—มักถูก model เป็น consumer group สิ่งนี้จะมีประโยชน์เมื่อคุณมี instance ของไมโครเซอร์วิสหลายตัว และคุณต้องการให้ตัวใดตัวหนึ่งสามารถรับ message ได้ ใน Figure 5-1 เราเห็นตัวอย่างที่ Order Processor มี instance ที่ deploy อยู่สามตัว ทั้งหมดเป็นส่วนหนึ่งของ consumer group เดียวกัน เมื่อ message ถูกวางเข้า queue จะมีเพียงสมาชิกหนึ่งตัวของ consumer group เท่านั้นที่จะได้รับ message นั้น นี่หมายความว่า queue ทำหน้าที่เป็นกลไก load distribution นี่คือตัวอย่างของ competing consumers pattern ที่เราแตะไปเล็กน้อยใน Chapter 4
Figure 5-1. A queue allows for one consumer group (Queue อนุญาตให้มี consumer group เดียว)
กับ topic คุณสามารถมี consumer group หลายตัวได้ ใน Figure 5-2 event ที่แสดง order ที่ถูกจ่ายเงินแล้วถูกวางเข้า topic Order Status สำเนาของ event นั้นจะถูกรับโดยทั้งไมโครเซอร์วิส Warehouse และไมโครเซอร์วิส Notifications ซึ่งอยู่ใน consumer group ที่แยกกัน จะมีเพียง instance เดียวของแต่ละ consumer group เท่านั้นที่จะเห็น event นั้น
เมื่อ มองแวบแรก queue ก็ดูเหมือนแค่ topic ที่มี consumer group เดียว ส่วนสำคัญของความแตกต่างระหว่างสองสิ่งนี้คือ เมื่อ message ถูกส่งผ่าน queue จะมีความรู้ว่า message นั้นถูกส่งไปที่ไหน กับ topic ข้อมูลนี้ถูกซ่อนจากผู้ส่ง message— ผู้ส่งไม่รู้ว่าใคร (ถ้ามี) ที่จะได้รับ message ในที่สุด
Topic เหมาะกับ event-based collaboration ในขณะที่ queue จะเหมาะกว่ากับการสื่อสารแบบ request/response อย่างไรก็ตาม สิ่งนี้ควรถูกพิจารณาเป็นแนวทางทั่วไปมากกว่ากฎเกณฑ์ที่เข้มงวด
Figure 5-2. Topics allow for multiple subscribers to receive the same messages, which is useful for event broadcast (Topic อนุญาตให้ผู้ subscribe หลายคนรับ message เดียวกันได้ ซึ่งมีประโยชน์สำหรับการกระจาย event)
Guaranteed delivery (การรับประกันการส่งมอบ)
แล้ว ทำไมถึงต้องใช้ broker? โดยพื้นฐานแล้ว มันมี capability บางอย่างที่มีประโยชน์มากสำหรับการสื่อสารแบบ asynchronous คุณสมบัติที่พวกมันมีนั้นแตกต่างกันไป แต่ feature ที่น่าสนใจที่สุดคือ guaranteed delivery ซึ่งเป็นสิ่งที่ broker ที่ใช้กันอย่างแพร่หลายทั้งหมดรองรับในรูปแบบใดรูปแบบหนึ่ง Guaranteed delivery อธิบายถึงคำมั่นสัญญาของ broker ที่จะทำให้แน่ใจว่า message จะถูกส่งมอบ
จากมุมมองของไมโครเซอร์วิสที่ส่ง message สิ่งนี้มีประโยชน์มาก มันไม่เป็นปัญหาถ้าปลายทาง downstream ไม่พร้อมใช้งาน—broker จะเก็บ message ไว้จนกว่าจะสามารถส่งมอบได้ สิ่งนี้ช่วยลดจำนวนสิ่งที่ไมโครเซอร์วิส upstream ต้องกังวลได้ เทียบกับการเรียกโดยตรง แบบ synchronous—ตัวอย่างเช่น HTTP request: ถ้าปลายทาง downstream ไม่สามารถเข้าถึงได้ ไมโครเซอร์วิส upstream จะต้องคิดว่า จะทำอย่างไรกับ request นั้น ควรลองใหม่หรือยอมแพ้?
เพื่อให้ guaranteed delivery ทำงานได้ broker จำเป็นต้องมั่นใจว่า message ใดๆ ที่ยังไม่ถูกส่งมอบจะถูกเก็บไว้ในลักษณะที่คงทน จนกว่าจะสามารถส่งมอบได้ เพื่อให้เป็นไปตามคำมั่นสัญญานี้ broker มักจะทำงานเป็นระบบแบบ cluster-based เพื่อให้แน่ใจว่าการสูญเสีย เครื่องเดียวจะไม่ทำให้ message สูญหาย โดยทั่วไปแล้วมีอะไรมากมายที่เกี่ยวข้องกับการรัน broker อย่างถูกต้อง ส่วนหนึ่งเป็นเพราะความ ท้าทายในการจัดการ software แบบ cluster-based บ่อยครั้งที่คำมั่นสัญญาของ guaranteed delivery อาจถูกบั่นทอนถ้า broker ไม่ได้ถูก set up อย่างถูกต้อง ตัวอย่างเช่น RabbitMQ ต้องการให้ instance ใน cluster สื่อสารผ่านเครือข่าย low-latency ค่อนข้างมาก มิฉะนั้น instance อาจเริ่มสับสนเกี่ยวกับสถานะปัจจุบันของ message ที่กำลังถูกจัดการ ส่งผลให้ข้อมูลสูญหาย ผมไม่ได้เน้นข้อจำกัดเฉพาะนี้เพื่อบอกว่า RabbitMQ แย่แต่อย่างใด—broker ทั้งหมดมีข้อจำกัดในเรื่องวิธีที่พวกมันต้องถูกรันเพื่อให้ เป็นไปตามคำมั่นสัญญาของ guaranteed delivery ถ้าคุณวางแผนจะรัน broker ของคุณเอง ให้แน่ใจว่าคุณได้อ่านเอกสารอย่างละเอียด
ควรสังเกตด้วยว่าสิ่งที่ broker แต่ละตัวหมายถึงด้วย guaranteed delivery อาจแตกต่างกันไป การอ่านเอกสารอีกครั้งเป็นจุดเริ่มต้นที่ดี
Trust (ความไว้วางใจ)
หนึ่ง ในจุดดึงดูดใหญ่ของ broker คือคุณสมบัติของ guaranteed delivery แต่เพื่อให้สิ่งนี้ทำงานได้ คุณต้องไว้ใจไม่เพียงแค่คนที่สร้าง broker เท่านั้น แต่ยังต้องไว้ใจวิธีที่ broker นั้นถูกดำเนินการด้วย ถ้าคุณสร้างระบบที่อยู่บนสมมติฐานว่าการส่งมอบได้รับการรับประกัน และปรากฏว่าไม่เป็นเช่นนั้นเนื่องจากปัญหากับ broker ที่อยู่เบื้องหลัง มันอาจก่อให้เกิดปัญหาสำคัญได้ ความหวังคือ แน่นอนว่า คุณกำลังผลักภาระงานนั้นไปให้ software ที่สร้างโดยคนที่สามารถทำงานนั้นได้ดีกว่าคุณ ท้ายที่สุดแล้ว คุณต้องตัดสินใจว่าคุณอยากไว้ใจ broker ที่คุณกำลังใช้อยู่มากแค่ไหน
Other characteristics (คุณสมบัติอื่นๆ)
นอกจาก guaranteed delivery แล้ว broker ยังสามารถให้คุณสมบัติอื่นๆ ที่คุณอาจพบว่ามีประโยชน์ได้
Broker ส่วนใหญ่สามารถรับประกันลำดับที่ message จะถูกส่งมอบได้ แต่นี่ไม่ใช่สิ่งที่เป็นสากล และแม้แต่ตอนที่มี ขอบเขตของการรับประกันนี้ ก็อาจถูกจำกัด กับ Kafka ตัวอย่างเช่น การเรียงลำดับได้รับการรับประกันเฉพาะภายใน partition เดียวเท่านั้น ถ้าคุณไม่สามารถแน่ใจได้ว่า message จะถูกรับตามลำดับ ผู้เรียกใช้ของคุณอาจต้องชดเชย บางทีอาจโดยการเลื่อนการประมวลผล message ที่ได้รับนอกลำดับออกไปจนกว่า message ที่หายไปจะมาถึง
Broker บางตัวมี transaction สำหรับการเขียน—ตัวอย่างเช่น Kafka อนุญาตให้คุณเขียนไปยัง topic หลายตัวใน transaction เดียว Broker บางตัวยังสามารถให้ read transactionality ได้ ซึ่งเป็นสิ่งที่ผมเคยใช้ประโยชน์เมื่อใช้ broker หลายตัวผ่าน Java Message Service (JMS) API สิ่งนี้มีประโยชน์ถ้าคุณต้องการให้แน่ใจว่า message สามารถถูกประมวลผลโดยผู้เรียกใช้ก่อนที่จะลบมันออกจาก broker
อีกหนึ่ง feature ที่ค่อนข้างเป็นที่ถกเถียงกัน ที่ broker บางตัวสัญญาไว้คือ exactly once delivery หนึ่งในวิธีที่ง่ายกว่าในการให้ guaranteed delivery คือการอนุญาตให้ message ถูกส่งซ้ำ สิ่งนี้อาจส่งผลให้ผู้เรียกใช้เห็น message เดียวกันมากกว่าหนึ่งครั้ง (แม้ว่านี่จะเป็นสถานการณ์ที่หายาก) Broker ส่วนใหญ่จะทำทุกทางที่ทำได้เพื่อลดโอกาสนี้ หรือซ่อนข้อเท็จจริงนี้จากผู้เรียกใช้ แต่ broker บางตัวก็ไปไกลกว่านั้นโดยรับประกัน exactly once delivery นี่เป็นหัวข้อที่ซับซ้อน เพราะผมเคยคุยกับผู้เชี่ยวชาญบางคนที่บอกว่า การรับประกัน exactly once delivery ในทุกกรณีเป็นไปไม่ได้ ในขณะที่ผู้เชี่ยวชาญคนอื่นบอกว่าคุณสามารถทำได้โดยพื้นฐานด้วยวิธีแก้ปัญหา ง่ายๆ ไม่กี่อย่าง ไม่ว่าจะแบบไหน ถ้า broker ที่คุณเลือกอ้างว่า implement สิ่งนี้ ให้ใส่ใจ อย่างจริงจังมาก ว่ามันถูก implement อย่างไร ที่ดีกว่านั้นคือ สร้างผู้เรียกใช้ของคุณในลักษณะที่พวกมันพร้อมรับความจริงที่ว่าพวกมันอาจได้รับ message มากกว่าหนึ่งครั้ง และสามารถจัดการสถานการณ์นี้ได้ ตัวอย่างง่ายๆ มากคือให้แต่ละ message มี ID ซึ่งผู้เรียกใช้สามารถตรวจสอบทุกครั้ง ที่ message ถูกรับ ถ้า message ที่มี ID นั้นถูกประมวลผลไปแล้ว message ที่ใหม่กว่าก็สามารถถูกละเลยได้
Choices (ตัวเลือก)
มี message broker หลากหลายแบบ ตัวอย่างยอดนิยมได้แก่ RabbitMQ, ActiveMQ และ Kafka (ซึ่งเราจะสำรวจเพิ่มเติมในไม่ช้า) ผู้ให้บริการ public cloud หลักๆ ก็ให้ product หลากหลายที่ทำหน้าที่นี้เช่นกัน ตั้งแต่ managed version ของ broker เหล่านั้น ที่คุณสามารถติดตั้งบน infrastructure ของคุณเอง ไปจนถึง implementation เฉพาะที่จำเพาะกับ platform หนึ่งๆ AWS ตัวอย่างเช่น มี Simple Queue Service (SQS), Simple Notification Service (SNS) และ Kinesis ซึ่งทั้งหมดให้ managed broker ที่แตกต่างกันไป SQS ที่จริงแล้วเป็น product ที่สองที่เคยเปิดตัวโดย AWS โดยเปิดตัวมาตั้งแต่ปี 2006
Kafka
Kafka ควรค่าแก่การกล่าวถึงในฐานะ broker เฉพาะเจาะจง ส่วนใหญ่เป็นเพราะความนิยมของมันเมื่อเร็วๆ นี้ ส่วนหนึ่งของความนิยมนั้น มาจากการใช้ Kafka เพื่อช่วยเคลื่อนย้ายข้อมูลปริมาณมากไปมา ในฐานะส่วนหนึ่งของการ implement stream processing pipeline สิ่งนี้สามารถช่วยเปลี่ยนจากการประมวลผลแบบ batch-oriented ไปสู่การประมวลผลแบบ real-time มากขึ้น
มีคุณสมบัติสองสามอย่างของ Kafka ที่ควรค่าแก่การกล่าวถึง อย่างแรก มันถูกออกแบบมาสำหรับ scale ที่ใหญ่มาก—มันถูกสร้างขึ้นที่ LinkedIn เพื่อแทนที่ message cluster ที่มีอยู่หลายตัวด้วย platform เดียว Kafka ถูกสร้างมาเพื่อรองรับ consumer และ producer หลายตัว— ผมเคยคุยกับผู้เชี่ยวชาญคนหนึ่งที่บริษัทเทคโนโลยีขนาดใหญ่ที่มี producer และ consumer มากกว่าห้าหมื่นตัวทำงานบน cluster เดียวกัน พูดตามตรง องค์กรน้อยมากที่มีปัญหาในระดับ scale นั้น แต่สำหรับบางองค์กร ความสามารถในการ scale Kafka ได้ง่าย (ค่อนข้างพูดได้ว่า) ก็มีประโยชน์มาก
Feature ที่ค่อนข้างพิเศษอีกอย่างของ Kafka คือ message permanence กับ message broker ทั่วไป เมื่อผู้เรียกใช้คนสุดท้ายได้รับ message แล้ว broker ก็ไม่จำเป็นต้องเก็บ message นั้นไว้อีกต่อไป กับ Kafka message สามารถถูกเก็บไว้เป็นระยะเวลาที่กำหนดค่าได้ นั่นหมายความว่า message สามารถถูกเก็บไว้ตลอดไป สิ่งนี้อนุญาตให้ผู้เรียกใช้ reingest message ที่พวกมันเคยประมวลผลไปแล้ว หรืออนุญาตให้ผู้เรียกใช้ที่เพิ่ง deploy ใหม่ประมวลผล message ที่ถูกส่งไปก่อนหน้านี้ได้
สุดท้าย Kafka ได้ทยอย roll out การรองรับ stream processing แบบ built-in แทนที่จะใช้ Kafka เพื่อส่ง message ไปยังเครื่องมือ stream processing เฉพาะทางอย่าง Apache Flink งานบางอย่างสามารถทำได้ภายใน Kafka เองแทน การใช้ KSQL คุณสามารถกำหนด statement แบบคล้าย SQL ที่สามารถประมวลผล topic หนึ่งตัวหรือมากกว่าแบบ on the fly ได้ สิ่งนี้สามารถให้คุณสิ่งที่คล้ายกับ dynamically updating materialized database view โดยมีแหล่งข้อมูลเป็น Kafka topic แทนที่จะเป็นฐานข้อมูล capability เหล่านี้เปิดโอกาส ที่น่าสนใจมากมายสำหรับวิธีที่ข้อมูลถูกจัดการใน distributed system ถ้าคุณอยากสำรวจแนวคิดเหล่านี้ในรายละเอียดมากขึ้น ผมขอแนะนำ Designing Event-Driven Systems (O'Reilly) โดย Ben Stopford (ผมต้องแนะนำหนังสือของ Ben เพราะผมเป็นคนเขียนคำนำให้!) สำหรับการเจาะลึก Kafka โดยทั่วไป ผมขอแนะนำ Kafka: The Definitive Guide (O'Reilly) โดย Neha Narkhede, Gwen Shapira และ Todd Palino