สร้างแอปพลิเคชันที่ขับเคลื่อนด้วยเหตุการณ์ที่ปรับขนาดได้ด้วย Nest.js
ในบทความนี้ ฉันต้องการพูดคุยเกี่ยวกับองค์ประกอบของแอปพลิเคชันที่ขับเคลื่อนด้วยเหตุการณ์ที่ปรับขนาดได้ซึ่งมีให้สำหรับนักพัฒนาด้วยเฟรมเวิร์ก Nest.js ฉันจะสาธิตว่าการใช้กรอบงานที่ทันสมัยสำหรับการสร้างแอปพลิเคชัน Node.js นั้นง่ายเพียงใด
Agenda
What is Nest.js?
How Does Nest.js Help Build Highly-Scalable Apps?
Demo App and Tools
Demo App in Action
Nest.js คืออะไร
เป็นเฟรมเวิร์กสำหรับสร้างแอปพลิเคชัน Node.js
ได้รับแรงบันดาลใจจาก Angular และใช้ TypeScript เป็นอย่างมาก
ดังนั้นจึงให้ประสบการณ์การพัฒนาที่ค่อนข้างปลอดภัย ยังคงเป็น JavaScript หลังจากการทรานสไพล์ ดังนั้นคุณควรระมัดระวังเมื่อต้องรับมือกับความเสี่ยงด้านความปลอดภัยทั่วไป
เป็นเฟรมเวิร์กที่ค่อนข้างได้รับความนิยมอยู่แล้ว และคุณคงเคยได้ยินมาบ้างแล้ว
ทำไมต้องใช้กรอบอื่น
- การฉีดพึ่งพา
- การรวมเข้ากับฐานข้อมูลที่เป็นนามธรรม
- กรณีการใช้งานทั่วไปที่เป็นนามธรรม: การแคช, การกำหนดค่า, การกำหนดเวอร์ชัน API และเอกสารประกอบ, การจัดตารางงาน, คิว, การบันทึก, คุกกี้, เหตุการณ์และเซสชัน, การตรวจสอบคำขอ, เซิร์ฟเวอร์ HTTP (Express หรือ Fastify), รับรองความถูกต้อง
- TypeScript (และมัณฑนากร)
- องค์ประกอบการออกแบบอื่นๆ สำหรับการใช้งานที่ยอดเยี่ยม: Middleware, ตัวกรองข้อยกเว้น, Guards, Pipes และอื่นๆ
- และอื่น ๆ ซึ่งฉันจะพูดถึงในภายหลัง
ข้อดีหลักประการหนึ่งของการใช้เฟรมเวิร์กคือมีการฉีดการพึ่งพา มันลบค่าใช้จ่ายในการสร้างและสนับสนุนแผนผังการขึ้นต่อกันของคลาส
มีการผสานรวมเชิงนามธรรมกับฐานข้อมูลส่วนใหญ่ คุณจึงไม่ต้องคิดเกี่ยวกับมัน แพ็คเกจที่ได้รับการพัฒนาและได้รับความนิยมมากที่สุดบางส่วนที่รองรับ ได้แก่ mongoose, TypeORM, MikroORM และ Prisma
มีการแยกกรณีการใช้งานทั่วไปสำหรับการพัฒนาเว็บ เช่น การแคช การกำหนดค่า การกำหนดเวอร์ชัน API และการจัดทำเอกสาร คิว ฯลฯ
สำหรับเซิร์ฟเวอร์ HTTP คุณสามารถเลือกระหว่าง Express หรือ Fastify
มันใช้ TypeScript และมัณฑนากร ทำให้การอ่านโค้ดง่ายขึ้น โดยเฉพาะอย่างยิ่งในโครงการขนาดใหญ่ และช่วยให้ทีมนักพัฒนามีความเข้าใจตรงกันเมื่อให้เหตุผลเกี่ยวกับส่วนประกอบต่างๆ
นอกจากนี้ เช่นเดียวกับเฟรมเวิร์กอื่น ๆ มันมีองค์ประกอบการออกแบบแอปพลิเคชันอื่น ๆ เช่น มิดเดิลแวร์ ตัวกรองข้อยกเว้น ตัวป้องกัน ท่อ และอื่น ๆ
และสุดท้าย เราจะพูดถึงคุณสมบัติอื่นๆ บางอย่างที่เกี่ยวข้องกับความสามารถในการปรับขนาดในภายหลัง
Nest.js ช่วยสร้างแอปที่ปรับขนาดได้สูงได้อย่างไร
ก่อนอื่นมาสรุปกลยุทธ์หลักสำหรับการสร้างแอปพลิเคชันที่ปรับขนาดได้สูง
นี่คือตัวเลือก:
- เสาหิน (โมดูลาร์)
- ไมโครเซอร์วิส
- ขับเคลื่อนด้วยเหตุการณ์
- ผสม
วิธีแรกที่ฉันต้องการพูดถึงคือการใช้หินใหญ่ก้อนเดียว
เป็นแอปพลิเคชั่นเดียวที่มีส่วนประกอบเชื่อมต่อกันแน่น
พวกเขาใช้งานร่วมกัน สนับสนุนกัน และโดยปกติแล้ว พวกเขาไม่สามารถอยู่ได้โดยปราศจากกันและกัน
หากคุณเขียนแอปพลิเคชันด้วยวิธีนี้ วิธีที่ดีที่สุดคือใช้วิธีโมดูลาร์ ซึ่ง Nest.js เชี่ยวชาญมาก
เมื่อใช้วิธีการแบบโมดูลาร์ คุณสามารถมีฐานโค้ดเดียวได้อย่างมีประสิทธิภาพ แต่ส่วนประกอบของระบบของคุณทำหน้าที่เป็นเอนทิตีที่ค่อนข้างเป็นอิสระ และสามารถทำงานได้โดยทีมต่างๆ สิ่งนี้จะยากขึ้นเมื่อทีมและโครงการของคุณเติบโตขึ้น นั่นเป็นเหตุผลที่เรามีโมเดลอื่นๆ สำหรับการพัฒนาสถาปัตยกรรม
ไมโครเซอร์วิส
Microservices คือเมื่อคุณมีการปรับใช้แยกต่างหากสำหรับแต่ละบริการ โดยปกติแล้ว แต่ละบริการจะรับผิดชอบเฉพาะหน่วยงานเล็กๆ เท่านั้น และจะมีร้านค้าของตน
แนวทางที่ขับเคลื่อนด้วยเหตุการณ์นั้นคล้ายกับไมโครเซอร์วิส
ตอนนี้ คุณไม่มีการสื่อสารโดยตรงระหว่างบริการต่างๆ แต่ละบริการจะปล่อยเหตุการณ์แทน และจากนั้นก็ไม่แคร์
กิจกรรมนี้สามารถมีผู้ฟังได้ แต่ไม่สามารถมีผู้ฟังได้ หากมีคนใช้เหตุการณ์ ก็จะสามารถสร้างเหตุการณ์อื่นอีกครั้งที่บริการอื่นสามารถใช้ และอื่น ๆ
ในที่สุดจะมีใครบางคนตอบกลับสำหรับลูกค้าที่รออยู่ อาจเป็นการตอบสนองของ WebSocket หรือเว็บฮุคหรืออะไรก็ตาม
บริการจะสื่อสารกับบริการอื่นๆ ผ่านคำขอ HTTP หรือการส่งข้อความ
สถาปัตยกรรมแบบผสมผสาน
โดยปกติแล้ว โครงการขนาดใหญ่ของเราจะเป็นการผสมผสานของการออกแบบทั้งหมด — ส่วนประกอบบางอย่างเชื่อมต่อกันแน่นและปรับใช้ร่วมกัน ส่วนประกอบบางอย่างถูกปรับใช้แยกกัน และบางส่วนสื่อสารผ่านการส่งข้อความเหตุการณ์เท่านั้น
Nest.js = การพัฒนาแอปพลิเคชันที่ขับเคลื่อนด้วยเหตุการณ์อย่างง่าย
ลองคิดดูว่าเหตุใดเฟรมเวิร์กนี้จึงลดความซับซ้อนของการพัฒนาที่ขับเคลื่อนด้วยเหตุการณ์
- ผสานรวมกับ Redis/Bull เพื่อการจัดการคิว ( github.com/OptimalBits/bull )
- ผสานรวมกับโบรกเกอร์ข้อความส่วนใหญ่
- ส่งเสริมการพัฒนาโมดูลาร์
- เอกสารและตัวอย่างที่ยอดเยี่ยม
- การทดสอบหน่วยและการรวมเป็นบูตสแตรป (DI, Jest)
สำหรับการพัฒนาไมโครเซอร์วิสและการสื่อสาร มีการผสานรวมกับโบรกเกอร์รับส่งข้อความยอดนิยมอย่าง Redis, Kafka, RabbitMQ, MQTT, NATS และอื่นๆ
ประการที่สาม ส่งเสริมการพัฒนาโมดูลาร์ ดังนั้นจึงเป็นเรื่องง่ายสำหรับคุณที่จะแยกหน่วยของงานเดี่ยวในภายหลังในวงจรชีวิตของโครงการ
ประเด็นต่อไปของฉันคือมันมีเอกสารประกอบและตัวอย่างที่ยอดเยี่ยมซึ่งเป็นสิ่งที่ดีเสมอ คุณสามารถเรียกใช้แอปแบบกระจายแอปแรกของคุณได้ในไม่กี่นาที
และอีกสิ่งหนึ่งที่ฉันต้องการทราบคือการทดสอบหน่วยและการรวมนั้นบูตสำหรับคุณ มี DI สำหรับการทดสอบและคุณสมบัติที่มีประสิทธิภาพอื่น ๆ ทั้งหมดของเฟรมเวิร์กการทดสอบ Jest
คิว (npm/bull)
ทีนี้ มาดูกันว่าสามารถสร้างคิวอย่างง่ายใน NestJS ได้อย่างไร
คิว: เพิ่มการเชื่อมต่อ
ก่อนอื่น คุณติดตั้งการพึ่งพาที่จำเป็นด้วยคำสั่งต่อไปนี้:
npm install --save @nestjs/bull bull
npm install --save-dev @types/bull
และสุดท้ายลงทะเบียนคิว
คิว: ผู้ผลิตเหตุการณ์ฉีดคิว
ถัดไป ที่อื่นในตัวสร้างบริการ คุณพิมพ์คำใบ้คิวของคุณ และคิวของคุณจะถูกฉีดเข้าไปโดยคอนเทนเนอร์ Dependency Injection — ตอนนี้คุณมีสิทธิ์เข้าถึงคิวโดยสมบูรณ์และสามารถเริ่มปล่อยเหตุการณ์ได้
Queues: ผู้บริโภคเหตุการณ์ประมวลผลคิว
ที่ไหนสักแห่งในโมดูลอื่น คุณตกแต่งคลาสโปรเซสเซอร์ของคุณด้วยProcessor() and Process()
การตั้งค่าเพียงเล็กน้อยเพื่อให้ระบบคิวทำงาน
คุณสามารถมีผู้ผลิตและผู้บริโภคอยู่ในแอปพลิเคชันเดียวหรือแยกกันก็ได้ พวกเขาจะสื่อสารผ่านนายหน้าข้อความที่คุณเลือก
การรวมข้อความ — การเชื่อมต่อ
การเชื่อมต่อผู้ให้บริการข้อความเริ่มต้นด้วยการเพิ่มการเชื่อมต่อโมดูลไคลเอนต์ ในตัวอย่างนี้ เรามีการขนส่ง Redis และควรมีตัวเลือกการเชื่อมต่อเฉพาะ Redis
การรวมข้อความ — ผู้ผลิต
ขั้นตอนต่อไปคือการใส่อินเทอร์เฟซพร็อกซีไคลเอนต์ลงในบริการผู้ผลิตของเรา
ตัวเลือกของเราเพิ่มเติมคือSEND
วิธีEMIT
หรือ
SEND
โดยปกติจะเป็นการดำเนินการแบบซิงโครนัส คล้ายกับคำขอ HTTP แต่ถูกแยกโดยเฟรมเวิร์กเพื่อดำเนินการผ่านการขนส่งที่เลือก
ในตัวอย่างด้านล่างaccumulate()
วิธีการตอบกลับจะไม่ถูกส่งไปยังไคลเอ็นต์จนกว่าข้อความจะได้รับการประมวลผลโดยแอปพลิเคชัน Listener
EMIT
คำสั่งเป็นการเริ่มต้นเวิร์กโฟลว์แบบอะซิงโครนัส ซึ่งจะทำหน้าที่เป็นไฟและลืม หรือในการขนส่งบางอย่าง ซึ่งจะทำหน้าที่เป็นเหตุการณ์คิวที่คงทน สิ่งนี้จะขึ้นอยู่กับการขนส่งที่เลือกและการกำหนดค่า
SEND
และEMIT
รูปแบบมีรูปแบบการใช้งานที่แตกต่างกันเล็กน้อยในฝั่ง CONSUMER มาดูกัน.
การผสานการส่งข้อความ — ผู้บริโภค
MessagePattern
มัณฑนากรมีไว้สำหรับเมธอดที่เหมือนการซิงค์เท่านั้น (ผลิตด้วยSEND
คำสั่ง) และใช้ได้เฉพาะในคลาสที่ตกแต่งโดยคอนโทรลเลอร์เท่านั้น
ดังนั้นเราจึงคาดหวังว่าจะได้รับการตอบสนองตามคำขอที่ได้รับผ่านโปรโตคอลการส่งข้อความของเรา
ในทางกลับกันEventPattern
มัณฑนากรสามารถใช้ในคลาสที่กำหนดเองของแอปพลิเคชันของคุณ และจะฟังเหตุการณ์ที่เกิดขึ้นในคิวหรือบัสเหตุการณ์เดียวกัน และจะไม่คาดหวังว่าแอปพลิเคชันของเราจะส่งคืนบางสิ่ง
การตั้งค่านี้คล้ายกับโบรกเกอร์การส่งข้อความอื่นๆ และหากเป็นสิ่งที่กำหนดเอง คุณยังสามารถใช้คอนเทนเนอร์ DI และสร้างผู้ให้บริการระบบย่อยเหตุการณ์ที่กำหนดเองด้วยอินเทอร์เฟซ Nest.js
นี่เป็นวิธีที่ง่ายในการรวมเข้ากับโบรกเกอร์รับส่งข้อความทั่วไปโดยใช้ Nest.js abstractions
แอพสาธิตและเครื่องมือ
มีให้ที่ GitHubต่อไปนี้
ในส่วนนี้ ฉันจะตรวจสอบส่วนหนึ่งของแอปพลิเคชันจริง (แบบง่าย) คุณสามารถรับซอร์สโค้ดที่หน้า GitHub ของฉันเพื่อติดตามหรือลองใช้ในภายหลัง ฉันจะแสดงให้เห็นว่า EDA ที่ออกแบบอย่างเหมาะสมสามารถเผชิญกับความท้าทายได้อย่างไร และวิธีที่เราสามารถแก้ไขปัญหาเหล่านั้นได้อย่างรวดเร็วด้วยเครื่องมือของเฟรมเวิร์ก
ภาพรวมแอปสาธิต
เรามาทำความเข้าใจภาพรวมกันก่อน เวิร์กโฟลว์ที่เราคาดไว้จะเป็นดังนี้:
เรามีการกระทำที่เกิดขึ้นในเกตเวย์ API ของเรา และมีผลกับบริการการค้าซึ่งปล่อยเหตุการณ์
งานนี้ไปต่อคิวหรือรถอีเวนท์ จากนั้นเรามีบริการอื่นอีกสี่รายการที่รับฟังและประมวลผล
ในการสังเกตการทำงานของแอปพลิเคชันนี้ ฉันใช้แอปพลิเคชันข้างเคียงซึ่งก็คือ “ตัวตรวจสอบช่องสัญญาณ” ของฉัน นี่เป็นรูปแบบที่มีประสิทธิภาพในการปรับปรุงความสามารถในการสังเกตและสามารถช่วยให้ปรับขนาดขึ้นและลงโดยอัตโนมัติตามเมตริกของแชนเนล
ฉันจะแสดงให้คุณเห็นว่ามันทำงานอย่างไร
แอปสาธิตกำลังทำงาน — สภาวะปกติ
ฉันเตรียมMakefile
เพื่อให้คุณทำตามได้
ขั้นแรก ให้เรียกใช้make start
คำสั่งที่จะเริ่มนักเทียบท่าด้วยบริการที่จำเป็นทั้งหมด ถัดไป เรียกใช้make monitor
คำสั่งเพื่อดูเมตริกแอปพลิเคชัน
จอภาพแสดงชื่อคิว จำนวนงานที่รอ จำนวนงานที่ประมวลผล และจำนวนอินสแตนซ์ของผู้ปฏิบัติงานออนไลน์
อย่างที่คุณเห็น ภายใต้สภาวะปกติjobs_waiting
จำนวนจะเป็นศูนย์ ลำดับเหตุการณ์จะช้า และเราไม่มีงานกองพะเนิน
แอปพลิเคชันนี้ทำงานได้ดีเมื่อมีจำนวนเหตุการณ์ต่ำ แต่จะเกิดอะไรขึ้นหากการจราจรเพิ่มขึ้นอย่างกระทันหัน
แอปตัวอย่างที่ใช้งานจริง — Traffic Spike
คุณสามารถเริ่มการสาธิตนี้ได้โดยเรียกใช้make start-issue1
คำสั่งและรีสตาร์ทจอภาพด้วยmake monitor
คำสั่ง ลำดับเหตุการณ์ของเราเพิ่มขึ้นสามเท่า
คุณจะสังเกตได้ในแอปมอนิเตอร์ในที่สุดว่าjobs_waiting
จำนวนจะเริ่มเพิ่มขึ้น และในขณะที่เรายังคงประมวลผลงานกับคนงานคนเดียว คิวได้ช้าลงแล้วเมื่อเทียบกับปริมาณการใช้งานที่เพิ่มขึ้น
ตอนนี้เราเห็นแล้วว่านี่เป็นการจำกัดการยืนยันบริการการค้าที่สำคัญต่อพันธกิจของเรา
ผู้ปฏิบัติงานจะประมวลผลเหตุการณ์ทั้งหมดโดยไม่มีลำดับความสำคัญ ดังนั้นการยืนยันการแลกเปลี่ยนใหม่แต่ละครั้งจะต้องรอให้เหตุการณ์โอเวอร์บางอย่างเสร็จสิ้นเสียก่อน
คุณสามารถจินตนาการได้ว่าสิ่งนี้ทำให้เวลาตอบสนองช้าลงในแอปพลิเคชันไคลเอนต์ส่วนหน้าของเราสำหรับการประมวลผลการค้า
โซลูชั่น?
มาสำรวจตัวเลือกที่เราต้องแก้ไขกัน:
- ปรับขนาดอินสแตนซ์ของผู้ปฏิบัติงานเพื่อให้ประมวลผลคิวได้เร็วขึ้น
- เพิ่มจำนวนอินสแตนซ์ของผู้ปฏิบัติงาน
- การเพิ่มประสิทธิภาพแอปพลิเคชัน
- แยกคิว
- จัดลำดับความสำคัญของเหตุการณ์
ประการที่สองคือการเพิ่มจำนวนอินสแตนซ์ของผู้ปฏิบัติงาน นี่เป็นตัวเลือกที่ถูกต้อง แต่บางครั้งก็ไม่คุ้มทุน
ต่อไป เราสามารถนึกถึงการปรับแต่งแอปพลิเคชัน รวมถึงการทำโปรไฟล์ การตรวจสอบการค้นหาฐานข้อมูล และกิจกรรมที่คล้ายกัน การดำเนินการนี้อาจใช้เวลานานและไม่เกิดผลลัพธ์หรือมีการปรับปรุงที่จำกัดมาก
สองตัวเลือกสุดท้ายของเราคือที่ที่ Nest.js สามารถช่วยเราได้ เป็นการแยกคิวและจัดลำดับความสำคัญของเหตุการณ์บางอย่าง
ขั้นตอนที่ 1 — แยกคิว
ฉันจะเริ่มต้นด้วยการใช้วิธีแยกคิว
คิวการค้าจะรับผิดชอบในการประมวลผลกิจกรรมการยืนยันการค้าเท่านั้น
รหัสของฉันสำหรับสิ่งนี้จะมีลักษณะดังนี้:
ขั้นตอนแรกคือการขอให้เราPRODUCER
ปล่อยTRADE CONFIRM
เหตุการณ์ไปยังคิวใหม่TRADES
-
ในฝั่งผู้บริโภค ฉันได้แยกคลาสใหม่ที่เรียกTradesService
และกำหนดให้เป็นผู้ฟังTRADES
คิว
บริการ ผู้QUEUE DEFAULT
ฟังยังคงเหมือนเดิม ฉันไม่ต้องทำการเปลี่ยนแปลงใด ๆ ที่นี่
ตอนนี้ ไม่ว่าจะเกิดอะไรขึ้น ไม่ว่าเราจะพุ่งขึ้นสูงแค่ไหนก็ตาม การเทรดจะไม่มีวันหยุดประมวลผล (จะช้าลงแต่จะไม่รอให้เกิดเหตุการณ์ที่ไม่สำคัญ)
คุณสามารถรันตัวอย่างนี้ด้วยstart-step1
คำสั่งและรีสตาร์ทมอนิเตอร์
คุณจะสังเกตเห็นว่าคิวการซื้อขายมีjobs_waiting
จำนวนเป็นศูนย์ แต่คิวเริ่มต้นยังคงประสบปัญหาอยู่
และตอนนี้ ฉันจะใช้ขั้นตอนที่สองของเราในการปรับขนาดตามข้อมูลที่ฉันมี ฉันจะเพิ่มจำนวนอินสแตนซ์ของผู้ปฏิบัติงาน3
เป็นDEFAULT QUEUE
เฉพาะ
ขั้นตอนที่ 2 — พนักงานชั่ง
คุณสามารถเริ่มการสาธิตนี้ได้โดยรันstart-step2
คำสั่งและรีสตาร์ทจอภาพ เมื่อเวลาผ่านไป แอปพลิเคชันนี้จะกลายเป็นศูนย์ jobs_waiting
ในทั้งสองคิว ทำได้ดีมาก!
อย่างที่คุณเข้าใจ ตัวอย่างของฉันค่อนข้างถูกประดิษฐ์ขึ้นเล็กน้อยและส่วนใหญ่ใช้เพื่อจุดประสงค์ในการสาธิต คุณสามารถดูได้อย่างง่ายดายว่าเราสามารถใช้ประโยชน์จากchannel monitor patterns
การตอบสนองทางโปรแกรมต่อการเปลี่ยนแปลงประสิทธิภาพแอปของเราได้อย่างไร โดยการเพิ่มหรือลดขนาดผู้ปฏิบัติงานในคิวที่แยกจากกัน
วิธีแก้ไข — สรุป
มาสรุปกัน ฉันใช้วิธีแก้ไขปัญหาสามข้อที่นี่จากรายการของฉัน:
- ปรับขนาดอินสแตนซ์ของผู้ปฏิบัติงานเพื่อให้ประมวลผลคิวได้เร็วขึ้น
- เพิ่มจำนวนอินสแตนซ์ของผู้ปฏิบัติงาน
- การเพิ่มประสิทธิภาพแอปพลิเคชัน
- แยกคิว
- จัดลำดับความสำคัญของเหตุการณ์
ต่อไป ฉันเพิ่มจำนวนอินสแตนซ์ของผู้ปฏิบัติงานสำหรับDEFAULT QUEUE
ถึง3
ทั้งหมดนี้ทำเพื่อฉันโดยนักเทียบท่าและเฟรมเวิร์ก Nest.js
ขั้นตอนต่อไปที่คุณสามารถนำไปใช้ได้เพียงแค่ใช้เครื่องมือของเฟรมเวิร์กคือการจัดลำดับความสำคัญของเหตุการณ์อื่น ๆ เหนือเหตุการณ์อื่น ๆ ตัวอย่างเช่น สิ่งใดก็ตามที่เกี่ยวข้องกับการบันทึกหรือเมตริกภายในอาจล่าช้าเพื่อให้เหตุการณ์ที่มีความสำคัญต่อภารกิจมากขึ้น เช่น การโต้ตอบกับฐานข้อมูล การแจ้งเตือน เป็นต้น
พื้นที่เก็บข้อมูลพร้อมรหัสทดสอบอยู่ที่นี่: github.com/dkhorev/conf42-event-driven-nestjs-demo
สำหรับการพัฒนาคอนเทนเนอร์และโมดูลาร์ ฉันใช้Container Role Pattern
คำอธิบายที่ลิงก์นี้
ฉันหวังว่านี่จะเป็นประโยชน์ ขอให้โชคดีและมีความสุขกับวิศวกรรม!
Nest.js ที่น่าสนใจเพิ่มเติมอ่าน: