การรักษา Node.js ให้รวดเร็ว: เครื่องมือ เทคนิค และเคล็ดลับสำหรับการสร้างเซิร์ฟเวอร์ Node.js ประสิทธิภาพสูง

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ โหนดเป็นแพลตฟอร์มที่ใช้งานได้หลากหลายมาก แต่หนึ่งในแอปพลิเคชันที่โดดเด่นคือการสร้างกระบวนการในเครือข่าย ในบทความนี้ เราจะเน้นที่การทำโปรไฟล์โดยทั่วไป: เว็บเซิร์ฟเวอร์ HTTP

หากคุณสร้างสิ่งใด ๆ ด้วย Node.js มานานพอ ไม่ต้องสงสัยเลยว่าคุณจะประสบปัญหาเกี่ยวกับความเร็วที่ไม่คาดคิดมาก่อนอย่างแน่นอน JavaScript เป็นภาษาที่มีเหตุการณ์แบบอะซิงโครนัส ที่สามารถให้เหตุผลเกี่ยวกับประสิทธิภาพที่ ยุ่งยาก อย่างที่จะเห็นได้ชัดเจน ความนิยมที่เพิ่มขึ้นของ Node.js ทำให้เห็นถึงความจำเป็นในการใช้เครื่องมือ เทคนิค และการคิดที่เหมาะสมกับข้อจำกัดของ JavaScript ฝั่งเซิร์ฟเวอร์

เมื่อพูดถึงประสิทธิภาพ สิ่งที่ใช้งานได้ในเบราว์เซอร์อาจไม่เหมาะกับ Node.js เสมอไป แล้วเราจะแน่ใจได้อย่างไรว่าการติดตั้ง Node.js นั้นรวดเร็วและเหมาะสมกับวัตถุประสงค์ มาดูตัวอย่างเชิงปฏิบัติกัน

เครื่องมือ

Node เป็นแพลตฟอร์มที่ใช้งานได้หลากหลายมาก แต่หนึ่งในแอปพลิเคชั่นที่โดดเด่นคือการสร้างกระบวนการในเครือข่าย เราจะมุ่งเน้นไปที่การทำโปรไฟล์โดยทั่วไป: เว็บเซิร์ฟเวอร์ HTTP

เราต้องการเครื่องมือที่สามารถทำลายเซิร์ฟเวอร์ที่มีคำขอจำนวนมากในขณะที่วัดประสิทธิภาพ ตัวอย่างเช่น เราสามารถใช้ AutoCannon:

 npm install -g autocannon

เครื่องมือเปรียบเทียบ HTTP ที่ดีอื่น ๆ ได้แก่ Apache Bench (ab) และ wrk2 แต่ AutoCannon เขียนด้วย Node ให้แรงกดในการโหลดที่คล้ายกัน (หรือบางครั้งสูงกว่า) และติดตั้งได้ง่ายมากบน Windows, Linux และ Mac OS X

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

หลังจากที่เราสร้างการวัดประสิทธิภาพพื้นฐานแล้ว หากเราตัดสินใจว่ากระบวนการของเราอาจเร็วขึ้น เราจะต้องใช้วิธีใดในการวินิจฉัยปัญหาของกระบวนการ เครื่องมือที่ยอดเยี่ยมสำหรับการวินิจฉัยปัญหาด้านประสิทธิภาพต่างๆ คือ Node Clinic ซึ่งสามารถติดตั้งได้ด้วย npm:

 npm install -g clinic

สิ่งนี้จะติดตั้งชุดเครื่องมือจริงๆ เราจะใช้ Clinic Doctor และ Clinic Flame (เสื้อคลุมประมาณ 0x) ในขณะที่เราไป

หมายเหตุ : สำหรับตัวอย่างเชิงปฏิบัตินี้ เราจำเป็นต้องใช้ Node 8.11.2 หรือสูงกว่า

รหัส

กรณีตัวอย่างของเราคือเซิร์ฟเวอร์ REST อย่างง่ายที่มีทรัพยากรเดียว: เพย์โหลด JSON ขนาดใหญ่เปิดเผยเป็นเส้นทาง GET ที่ /seed/v1 เซิร์ฟเวอร์เป็นโฟลเดอร์ app ที่ประกอบด้วยไฟล์ package.json (ขึ้นอยู่กับการ restify 7.1.0 ) ไฟล์ index.js และไฟล์ util.js

ไฟล์ index.js สำหรับเซิร์ฟเวอร์ของเรามีลักษณะดังนี้:

 'use strict' const restify = require('restify') const { etagger, timestamp, fetchContent } = require('./util')() const server = restify.createServer() server.use(etagger().bind(server)) server.get('/seed/v1', function (req, res, next) { fetchContent(req.url, (err, content) => { if (err) return next(err) res.send({data: content, url: req.url, ts: timestamp()}) next() }) }) server.listen(3000)

เซิร์ฟเวอร์นี้เป็นตัวแทนของกรณีทั่วไปของการให้บริการเนื้อหาไดนามิกที่แคชโดยไคลเอ็นต์ สิ่งนี้ทำได้ด้วยมิดเดิลแวร์ etagger ซึ่งคำนวณส่วนหัว ETag สำหรับสถานะล่าสุดของเนื้อหา

ไฟล์ util.js จัดเตรียมชิ้นส่วนการใช้งานที่มักใช้ในสถานการณ์ดังกล่าว ฟังก์ชันเพื่อดึงเนื้อหาที่เกี่ยวข้องจากแบ็กเอนด์ มิดเดิลแวร์ etag และฟังก์ชันประทับเวลาที่จัดเตรียมการประทับเวลาแบบนาทีต่อนาที:

 'use strict' require('events').defaultMaxListeners = Infinity const crypto = require('crypto') module.exports = () => { const content = crypto.rng(5000).toString('hex') const ONE_MINUTE = 60000 var last = Date.now() function timestamp () { var now = Date.now() if (now — last >= ONE_MINUTE) last = now return last } function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } } function fetchContent (url, cb) { setImmediate(() => { if (url !== '/seed/v1') cb(Object.assign(Error('Not Found'), {statusCode: 404})) else cb(null, content) }) } return { timestamp, etagger, fetchContent } }

ไม่ควรใช้โค้ดนี้เป็นตัวอย่างแนวทางปฏิบัติที่ดีที่สุด! ไฟล์นี้มีกลิ่นโค้ดหลายกลิ่น แต่เราจะระบุตำแหน่งขณะที่เราวัดและกำหนดโปรไฟล์ของแอปพลิเคชัน

เพื่อให้ได้แหล่งที่มาเต็มรูปแบบสำหรับจุดเริ่มต้นของเรา เซิร์ฟเวอร์ที่ช้าสามารถพบได้ที่นี่

โปรไฟล์

ในการสร้างโปรไฟล์ เราจำเป็นต้องมีเทอร์มินัลสองเครื่อง เทอร์มินัลหนึ่งสำหรับเริ่มแอปพลิเคชัน และอีกเครื่องหนึ่งสำหรับการทดสอบโหลด

ในเทอร์มินัลเดียว ภายใน app โฟลเดอร์ที่เราเรียกใช้ได้:

 node index.js

ในเทอร์มินัลอื่น เราสามารถกำหนดโปรไฟล์ได้ดังนี้:

 autocannon -c100 localhost:3000/seed/v1

การดำเนินการนี้จะเปิดการเชื่อมต่อพร้อมกัน 100 รายการและโจมตีเซิร์ฟเวอร์ด้วยคำขอเป็นเวลาสิบวินาที

ผลลัพธ์ควรเป็นสิ่งที่คล้ายกับต่อไปนี้ (รันการทดสอบ 10 วินาที @ https://localhost:3000/seed/v1 — 100 การเชื่อมต่อ):

สถิติ เฉลี่ย Stdev แม็กซ์
เวลาในการตอบสนอง (มิลลิวินาที) 3086.81 1725.2 5554
คำขอ/วินาที 23.1 19.18 65
ไบต์/วินาที 237.98 kB 197.7 kB 688.13 kB
คำขอ 231 รายการใน 10 วินาที อ่าน 2.4 MB

ผลลัพธ์จะแตกต่างกันไปขึ้นอยู่กับเครื่อง อย่างไรก็ตาม เมื่อพิจารณาว่าเซิร์ฟเวอร์ Node.js ของ “Hello World” นั้นสามารถขอสามหมื่นคำขอต่อวินาทีบนเครื่องที่สร้างผลลัพธ์เหล่านี้ได้อย่างง่ายดาย 23 คำขอต่อวินาทีโดยมีเวลาแฝงเฉลี่ยเกิน 3 วินาทีนั้นช่างน่าหดหู่

การวินิจฉัย

การค้นพบพื้นที่ปัญหา

เราสามารถวินิจฉัยแอปพลิเคชันด้วยคำสั่งเดียวด้วยคำสั่ง –on-port ของ Clinic Doctor ภายในโฟลเดอร์ app พที่เราเรียกใช้:

 clinic doctor --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

สิ่งนี้จะสร้างไฟล์ HTML ที่จะเปิดโดยอัตโนมัติในเบราว์เซอร์ของเราเมื่อโปรไฟล์เสร็จสมบูรณ์

ผลลัพธ์ควรมีลักษณะดังนี้:

หมอคลินิกตรวจพบปัญหาวนรอบเหตุการณ์
ผลลัพธ์ของแพทย์คลินิก

หมอกำลังบอกเราว่าเราน่าจะมีปัญหา Event Loop

นอกจากข้อความที่ด้านบนสุดของ UI แล้ว เรายังพบว่าแผนภูมิวนรอบเหตุการณ์เป็นสีแดง และแสดงการหน่วงเวลาที่เพิ่มขึ้นอย่างต่อเนื่อง ก่อนที่เราจะเจาะลึกลงไปถึงความหมายนี้ ก่อนอื่นมาทำความเข้าใจผลกระทบที่ปัญหาที่ได้รับการวินิจฉัยมีต่อเมตริกอื่นๆ ก่อน

เราจะเห็นว่า CPU อยู่ที่ 100% หรือสูงกว่า 100% เนื่องจากกระบวนการทำงานอย่างหนักเพื่อประมวลผลคำขอที่อยู่ในคิว เอ็นจิ้น JavaScript ของโหนด (V8) ใช้คอร์ CPU สองคอร์ในกรณีนี้เพราะเครื่องเป็นแบบมัลติคอร์และ V8 ใช้สองเธรด อันหนึ่งสำหรับ Event Loop และอีกอันสำหรับ Garbage Collection เมื่อเราเห็น CPU เพิ่มขึ้นถึง 120% ในบางกรณี กระบวนการกำลังรวบรวมวัตถุที่เกี่ยวข้องกับคำขอที่ได้รับการจัดการ

เราเห็นสิ่งนี้สัมพันธ์กันในกราฟหน่วยความจำ เส้นทึบในแผนภูมิ Memory คือเมตริก Heap Used ทุกครั้งที่มี CPU พุ่งสูงขึ้น เราจะเห็นการตกในบรรทัด Heap Used ซึ่งแสดงว่าหน่วยความจำกำลังถูกจัดสรรคืน

Active Handles จะไม่ได้รับผลกระทบจากความล่าช้าของ Event Loop หมายเลขอ้างอิงที่ใช้งานอยู่คืออ็อบเจ็กต์ที่แสดงถึง I/O (เช่น ซ็อกเก็ตหรือตัวจัดการไฟล์) หรือตัวจับเวลา (เช่น setInterval ) เราสั่งให้ AutoCannon เปิดการเชื่อมต่อ 100 ครั้ง ( -c100 ) แฮนเดิลที่แอ็คทีฟมีจำนวนคงที่ที่ 103 อีกสามแฮนเดิลสำหรับ STDOUT, STDERR และแฮนเดิลสำหรับเซิร์ฟเวอร์เอง

หากเราคลิกแผงคำแนะนำที่ด้านล่างของหน้าจอ เราควรจะเห็นสิ่งต่อไปนี้:

เปิดแผงแนะนำแพทย์
กำลังดูคำแนะนำเฉพาะปัญหา

การบรรเทาผลกระทบระยะสั้น

การวิเคราะห์สาเหตุของปัญหาด้านประสิทธิภาพที่ร้ายแรงอาจต้องใช้เวลา ในกรณีของโปรเจ็กต์ที่ใช้งานจริง การเพิ่มการป้องกันโอเวอร์โหลดให้กับเซิร์ฟเวอร์หรือบริการก็คุ้มค่า แนวคิดของการป้องกันการโอเวอร์โหลดคือการตรวจสอบการหน่วงเวลาของลูปเหตุการณ์ (เหนือสิ่งอื่นใด) และตอบสนองด้วย "503 บริการไม่พร้อมใช้งาน" หากผ่านขีดจำกัด วิธีนี้ทำให้ตัวโหลดบาลานซ์สามารถเฟลโอเวอร์ไปยังอินสแตนซ์อื่นได้ หรือในกรณีที่เลวร้ายที่สุด ผู้ใช้จะต้องรีเฟรช โมดูลป้องกันการโอเวอร์โหลดสามารถจัดให้มีโอเวอร์เฮดขั้นต่ำสำหรับ Express, Koa และ Restify กรอบงาน Hapi มีการตั้งค่าการกำหนดค่าโหลดซึ่งให้การป้องกันแบบเดียวกัน

การทำความเข้าใจพื้นที่ปัญหา

ตามคำอธิบายสั้นๆ ใน Clinic Doctor อธิบายว่า หาก Event Loop ล่าช้าไปถึงระดับที่เรากำลังสังเกตอยู่ มีความเป็นไปได้สูงที่ฟังก์ชันอย่างน้อย 1 ฟังก์ชันจะ "บล็อก" Event Loop

เป็นสิ่งสำคัญโดยเฉพาะอย่างยิ่งกับ Node.js ที่จะต้องรู้จักคุณลักษณะ JavaScript หลักนี้: เหตุการณ์แบบอะซิงโครนัสไม่สามารถเกิดขึ้นได้จนกว่าการรันโค้ดในปัจจุบันจะเสร็จสิ้น

นี่คือสาเหตุที่ setTimeout ไม่สามารถแม่นยำได้

ตัวอย่างเช่น ลองเรียกใช้สิ่งต่อไปนี้ใน DevTools ของเบราว์เซอร์หรือ Node REPL:

 console.time('timeout') setTimeout(console.timeEnd, 100, 'timeout') let n = 1e7 while (n--) Math.random()

การวัดเวลาที่ได้จะไม่เท่ากับ 100 มิลลิวินาที มีแนวโน้มว่าจะอยู่ในช่วง 150ms ถึง 250ms setTimeout กำหนดเวลาการดำเนินการแบบอะซิงโครนัส ( console.timeEnd ) แต่โค้ดที่กำลังดำเนินการอยู่ยังไม่เสร็จสมบูรณ์ มีอีกสองบรรทัด รหัสที่กำลังดำเนินการอยู่เรียกว่า "ขีด" ปัจจุบัน เพื่อให้ติ๊กสมบูรณ์ ต้องเรียก Math.random สิบล้านครั้ง หากใช้เวลา 100 มิลลิวินาที เวลาทั้งหมดก่อนที่จะหมดเวลาแก้ไขจะเป็น 200 มิลลิวินาที (บวกกับเวลาที่ฟังก์ชัน setTimeout ใช้เวลานานในการจัดคิวการหมดเวลาก่อนจริง ๆ โดยทั่วไปคือสองสามมิลลิวินาที)

ในบริบทฝั่งเซิร์ฟเวอร์ หากการดำเนินการในขีดปัจจุบันใช้เวลานานในการดำเนินการตามคำขอ จะไม่สามารถจัดการได้ และการดึงข้อมูลจะไม่เกิดขึ้นเนื่องจากโค้ดอะซิงโครนัสจะไม่ถูกดำเนินการจนกว่าขีดปัจจุบันจะเสร็จสิ้น ซึ่งหมายความว่าโค้ดที่มีราคาแพงในการประมวลผลจะทำให้การโต้ตอบทั้งหมดกับเซิร์ฟเวอร์ช้าลง ดังนั้นจึงแนะนำให้แยกงานที่ต้องใช้ทรัพยากรมากออกเป็นกระบวนการแยกต่างหากและเรียกใช้จากเซิร์ฟเวอร์หลัก ซึ่งจะหลีกเลี่ยงกรณีที่เส้นทางที่ไม่ค่อยได้ใช้แต่ราคาแพงจะทำให้ประสิทธิภาพของเส้นทางอื่นที่ใช้บ่อยแต่ราคาถูกช้าลง

เซิร์ฟเวอร์ตัวอย่างมีรหัสที่บล็อก Event Loop ดังนั้นขั้นตอนต่อไปคือการค้นหารหัสนั้น

กำลังวิเคราะห์

วิธีหนึ่งในการระบุโค้ดที่มีประสิทธิภาพต่ำอย่างรวดเร็วคือการสร้างและวิเคราะห์กราฟเปลวไฟ กราฟเปลวไฟแสดงถึงการเรียกใช้ฟังก์ชันเป็นบล็อกที่วางทับกัน — ไม่ใช่เมื่อเวลาผ่านไปแต่เป็นการรวม เหตุผลที่เรียกว่า 'กราฟเปลวไฟ' ก็เพราะว่าโดยทั่วไปแล้วจะใช้รูปแบบสีส้มถึงแดง โดยที่ฟังก์ชันยิ่งสีแดงยิ่ง "ร้อนแรง" มากขึ้นเท่านั้น หมายความว่ายิ่งมีแนวโน้มที่จะบล็อกการวนซ้ำของเหตุการณ์มากขึ้นเท่านั้น การจับข้อมูลสำหรับกราฟเปลวไฟจะดำเนินการผ่านการสุ่มตัวอย่าง CPU ซึ่งหมายความว่าจะมีการถ่ายภาพสแนปชอตของฟังก์ชันที่กำลังดำเนินการอยู่และเป็นกองซ้อน ความร้อนถูกกำหนดโดยเปอร์เซ็นต์ของเวลาระหว่างการทำโปรไฟล์ที่ฟังก์ชันที่กำหนดอยู่ที่ด้านบนของสแต็ก (เช่น ฟังก์ชันที่กำลังดำเนินการอยู่) สำหรับแต่ละตัวอย่าง หากไม่ใช่ฟังก์ชันสุดท้ายที่เคยถูกเรียกใช้ภายในสแต็กนั้น ก็มีแนวโน้มว่าจะบล็อกการวนซ้ำของเหตุการณ์

ลองใช้ clinic flame เพื่อสร้างกราฟเปลวไฟของแอปพลิเคชันตัวอย่าง:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

ผลลัพธ์ควรเปิดขึ้นในเบราว์เซอร์ของเราโดยมีลักษณะดังนี้:

กราฟเปลวไฟของคลินิกแสดงว่า server.on เป็นคอขวด
การแสดงกราฟเปลวไฟของคลินิก

ความกว้างของบล็อกแสดงถึงระยะเวลาที่ใช้กับ CPU โดยรวม สามกองหลักสามารถสังเกตได้กินเวลามากที่สุด โดยทั้งหมดเน้นว่า server.on เป็นฟังก์ชันที่ร้อนแรงที่สุด อันที่จริงทั้งสามกองเหมือนกัน พวกเขาแตกต่างกันเพราะในระหว่างการทำโปรไฟล์ที่ปรับให้เหมาะสมและฟังก์ชั่นที่ไม่ได้ปรับให้เหมาะสมจะถือว่าเป็นเฟรมการโทรที่แยกจากกัน ฟังก์ชันที่นำหน้าด้วย * ได้รับการปรับให้เหมาะสมโดยเอ็นจิ้น JavaScript และฟังก์ชันที่นำหน้าด้วย ~ จะไม่ได้รับการปรับให้เหมาะสม หากสถานะที่ปรับให้เหมาะสมไม่สำคัญสำหรับเรา เราสามารถลดความซับซ้อนของกราฟเพิ่มเติมได้โดยกดปุ่มผสาน สิ่งนี้ควรนำไปสู่การดูคล้ายกับต่อไปนี้:

รวมกราฟไฟ
ผสานกราฟเปลวไฟ

จากจุดเริ่มต้น เราสามารถอนุมานได้ว่ารหัสที่ละเมิดอยู่ในไฟล์ util.js ของรหัสแอปพลิเคชัน

ฟังก์ชันที่ช้ายังเป็นตัวจัดการเหตุการณ์อีกด้วย: ฟังก์ชันที่นำไปสู่ฟังก์ชันนี้เป็นส่วนหนึ่งของโมดูล events หลัก และ server.on เป็นชื่อทางเลือกสำหรับฟังก์ชันที่ไม่ระบุตัวตนที่จัดเตรียมไว้เป็นฟังก์ชันการจัดการเหตุการณ์ นอกจากนี้เรายังสามารถเห็นได้ว่ารหัสนี้ไม่อยู่ในเครื่องหมายเดียวกับรหัสที่จัดการกับคำขอจริงๆ ถ้าเป็นเช่นนั้น ฟังก์ชันจากโมดูล core http , net และ stream จะอยู่ในสแต็ก

ฟังก์ชันหลักดังกล่าวสามารถพบได้โดยการขยายส่วนอื่น ๆ ที่เล็กกว่ามากของกราฟเปลวไฟ ตัวอย่างเช่น ลองใช้อินพุตการค้นหาที่ด้านบนขวาของ UI เพื่อค้นหาการ send (ชื่อของวิธีการภายในทั้ง restify และ http ) ควรอยู่ทางขวาของกราฟ (ฟังก์ชันเรียงตามตัวอักษร):

กราฟเปลวไฟมีการไฮไลต์สองบล็อกเล็ก ๆ ซึ่งแสดงถึงฟังก์ชันการประมวลผล HTTP
ค้นหากราฟเปลวไฟสำหรับฟังก์ชันการประมวลผล HTTP

สังเกตว่าบล็อกการจัดการ HTTP จริงทั้งหมดมีขนาดเล็กเพียงใด

เราสามารถคลิกหนึ่งในบล็อกที่ไฮไลต์เป็นสีฟ้า ซึ่งจะขยายเพื่อแสดงฟังก์ชันต่างๆ เช่น writeHead และ write ในไฟล์ http_outgoing.js (ส่วนหนึ่งของไลบรารี Node core http ):

กราฟเปลวไฟได้ซูมเข้าในมุมมองอื่นที่แสดงสแต็คที่เกี่ยวข้องกับ HTTP
การขยายกราฟเปลวไฟเป็นสแต็คที่เกี่ยวข้องกับ HTTP

เราสามารถคลิก กองทั้งหมด เพื่อกลับไปที่มุมมองหลัก

จุดสำคัญที่นี่คือแม้ว่าฟังก์ชัน server.on จะไม่อยู่ในเครื่องหมายเดียวกับรหัสการจัดการคำขอจริง แต่ก็ยังส่งผลต่อประสิทธิภาพเซิร์ฟเวอร์โดยรวมโดยการชะลอการทำงานของโค้ดที่มีประสิทธิภาพ

แก้จุดบกพร่อง

เราทราบจากกราฟเปลวไฟว่าฟังก์ชันที่เป็นปัญหาคือตัวจัดการเหตุการณ์ที่ส่งผ่านไปยัง server.on ในไฟล์ util.js

ลองดู:

 server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag })

เป็นที่ทราบกันดีว่าการเข้ารหัสมีแนวโน้มที่จะมีราคาแพง เช่นเดียวกับการทำให้เป็นอนุกรม ( JSON.stringify ) แต่ทำไมไม่ปรากฏในกราฟเปลวไฟ การดำเนินการเหล่านี้อยู่ในตัวอย่างที่จับได้ แต่ซ่อนอยู่หลังตัวกรอง cpp หากเรากดปุ่ม cpp เราจะเห็นสิ่งต่อไปนี้:

บล็อกเพิ่มเติมที่เกี่ยวข้องกับ C ++ ถูกเปิดเผยในกราฟเปลวไฟ (มุมมองหลัก)
เปิดเผยการทำให้เป็นอนุกรมและการเข้ารหัสเฟรม C++

คำแนะนำ V8 ภายในที่เกี่ยวข้องกับทั้งการทำให้เป็นอนุกรมและการเข้ารหัสจะแสดงเป็นสแต็กที่ร้อนแรงที่สุดและใช้เวลาส่วนใหญ่ เมธอด JSON.stringify เรียกโค้ด C++ โดยตรง นี่คือเหตุผลที่เราไม่เห็นฟังก์ชัน JavaScript ในกรณีของการเข้ารหัส ฟังก์ชันต่างๆ เช่น createHash และ update จะอยู่ในข้อมูล แต่ฟังก์ชันเหล่านี้อาจอยู่ในแนวเดียวกัน (ซึ่งหมายความว่าฟังก์ชันเหล่านี้จะหายไปในมุมมองที่ผสาน) หรือมีขนาดเล็กเกินไปที่จะแสดงผล

เมื่อเราเริ่มให้เหตุผลเกี่ยวกับโค้ดในฟังก์ชัน etagger แล้ว จะเห็นได้ชัดเจนว่าโค้ดนี้ออกแบบมาไม่ดี เหตุใดเราจึงใช้อินสแตนซ์ของ server จากบริบทของฟังก์ชัน มีการแฮชเกิดขึ้นมากมาย จำเป็นทั้งหมดหรือไม่ นอกจากนี้ ยังไม่มีการรองรับส่วนหัว If-None-Match ในการใช้งานซึ่งจะช่วยลดภาระงานบางส่วนในสถานการณ์จริงบางสถานการณ์ เนื่องจากลูกค้าจะส่งคำขอหลักเพื่อกำหนดความสดเท่านั้น

ให้ละเว้นประเด็นเหล่านี้ทั้งหมดในขณะนี้และตรวจสอบการค้นพบว่างานจริงที่กำลังดำเนินการใน server.on เป็นคอขวดอย่างแท้จริง สิ่งนี้สามารถทำได้โดยการตั้งค่ารหัส server.on เป็นฟังก์ชันว่างและสร้างแฟล็กกราฟใหม่

เปลี่ยนฟังก์ชัน etagger ดังต่อไปนี้:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

ฟังก์ชันตัวฟังเหตุการณ์ที่ส่งผ่านไปยัง server.on ตอนนี้เป็นแบบ no-op

มาเรียกใช้ clinic flame มกันอีกครั้ง:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

สิ่งนี้ควรสร้างกราฟเปลวไฟที่คล้ายกับต่อไปนี้:

กราฟเปลวไฟแสดงให้เห็นว่าสแต็กระบบเหตุการณ์ Node.js ยังคงเป็นคอขวด
กราฟเปลวไฟของเซิร์ฟเวอร์เมื่อ server.on เป็นฟังก์ชันว่าง

สิ่งนี้ดูดีขึ้น และเราน่าจะสังเกตเห็นการเพิ่มขึ้นของคำขอต่อวินาที แต่ทำไมโค้ดการเปล่งเหตุการณ์ถึงร้อนมาก? เราคาดว่า ณ จุดนี้รหัสการประมวลผล HTTP จะใช้เวลาส่วนใหญ่ของ CPU ไม่มีอะไรดำเนินการเลยในเหตุการณ์ server.on

ปัญหาคอขวดประเภทนี้เกิดจากฟังก์ชันที่ถูกเรียกใช้งานมากกว่าที่ควรจะเป็น

รหัสที่น่าสงสัยต่อไปนี้ที่ด้านบนของ util.js อาจเป็นเบาะแส:

 require('events').defaultMaxListeners = Infinity

มาลบบรรทัดนี้และเริ่มกระบวนการของเราด้วย --trace-warnings :

 node --trace-warnings index.js

หากเราสร้างโปรไฟล์ด้วย AutoCannon ในเทอร์มินัลอื่น เช่น:

 autocannon -c100 localhost:3000/seed/v1

กระบวนการของเราจะส่งออกสิ่งที่คล้ายกับ:

 (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10) (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)

โหนดกำลังบอกเราว่ามีการแนบเหตุการณ์จำนวนมากกับวัตถุ เซิร์ฟเวอร์ สิ่งนี้แปลกเพราะมีบูลีนที่ตรวจสอบว่ามีการแนบเหตุการณ์แล้วกลับมาก่อนกำหนดโดยพื้นฐานแล้วทำให้ AttachAfterEvent เป็น no-op หลังจากแนบกิจกรรมแรกแล้ว

มาดูฟังก์ชัน attachAfterEvent กัน:

 var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) }

การตรวจสอบเงื่อนไขไม่ถูกต้อง! จะตรวจสอบว่า attachAfterEvent เป็นจริงหรือไม่ แทนที่จะเป็น afterEventAttached ซึ่งหมายความว่าจะมีการแนบเหตุการณ์ใหม่กับอินสแตนซ์ของ server ในทุกคำขอ จากนั้นกิจกรรมที่แนบก่อนหน้าทั้งหมดจะถูกไล่ออกหลังจากแต่ละคำขอ อ๊ะ!

เพิ่มประสิทธิภาพ

ตอนนี้เราได้ค้นพบส่วนปัญหาแล้ว มาดูกันว่าเราจะทำให้เซิร์ฟเวอร์เร็วขึ้นหรือไม่

ผลไม้ห้อยต่ำ

มาใส่รหัสตัวฟังของ server.on กลับกัน (แทนที่จะเป็นฟังก์ชันว่าง) และใช้ชื่อบูลีนที่ถูกต้องในการตรวจสอบตามเงื่อนไข ฟังก์ชัน etagger ของเรามีลักษณะดังนี้:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (afterEventAttached === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

ตอนนี้เราตรวจสอบการแก้ไขด้วยการทำโปรไฟล์อีกครั้ง เริ่มเซิร์ฟเวอร์ในเทอร์มินัลเดียว:

 node index.js

จากนั้นโปรไฟล์ด้วย AutoCannon:

 autocannon -c100 localhost:3000/seed/v1

เราควรเห็นผลลัพธ์ในช่วงของการปรับปรุง 200 เท่า (การทดสอบรัน 10 วินาที @ https://localhost:3000/seed/v1 — การเชื่อมต่อ 100 ครั้ง):

สถิติ เฉลี่ย Stdev แม็กซ์
เวลาในการตอบสนอง (มิลลิวินาที) 19.47 4.29 103
คำขอ/วินาที 5011.11 506.2 5487
ไบต์/วินาที 51.8 MB 5.45 MB 58.72 MB
คำขอ 50k ใน 10 วินาที, อ่าน 519.64 MB

สิ่งสำคัญคือต้องปรับสมดุลการลดต้นทุนเซิร์ฟเวอร์ที่อาจเกิดขึ้นกับต้นทุนการพัฒนา เราจำเป็นต้องกำหนดในบริบทสถานการณ์ของเราเองว่าเราต้องใช้ความพยายามในการเพิ่มประสิทธิภาพโครงการมากเพียงใด มิฉะนั้น มันอาจจะง่ายเกินไปที่จะใส่ 80% ของความพยายามลงใน 20% ของการปรับปรุงความเร็ว ข้อ จำกัด ของโครงการพิสูจน์สิ่งนี้หรือไม่?

ในบางสถานการณ์ อาจเหมาะสมที่จะบรรลุการปรับปรุง 200 เท่าด้วยผลไม้ห้อยต่ำและเรียกว่าเป็นวันเดียว ในส่วนอื่นๆ เราอาจต้องการทำให้การติดตั้งใช้งานของเรารวดเร็วที่สุดเท่าที่จะทำได้ มันขึ้นอยู่กับลำดับความสำคัญของโครงการจริงๆ

วิธีหนึ่งในการควบคุมการใช้ทรัพยากรคือการกำหนดเป้าหมาย ตัวอย่างเช่น ปรับปรุง 10 เท่า หรือ 4000 คำขอต่อวินาที การพิจารณาความต้องการทางธุรกิจเป็นสิ่งที่สมเหตุสมผลที่สุด ตัวอย่างเช่น หากเซิร์ฟเวอร์มีค่าใช้จ่ายเกินงบประมาณ 100% เราสามารถตั้งเป้าหมายการปรับปรุงได้ 2 เท่า

ก้าวต่อไป

หากเราสร้างกราฟเปลวไฟใหม่บนเซิร์ฟเวอร์ของเรา เราควรเห็นสิ่งต่อไปนี้:

กราฟเปลวไฟยังคงแสดง server.on เป็นคอขวด แต่คอขวดเล็กกว่า
กราฟเปลวไฟหลังจากแก้ไขข้อผิดพลาดด้านประสิทธิภาพแล้ว

ตัวฟังเหตุการณ์ยังคงเป็นคอขวด แต่ก็ยังใช้เวลา CPU ไปหนึ่งในสามระหว่างการทำโปรไฟล์ (ความกว้างประมาณหนึ่งในสามของกราฟทั้งหมด)

สามารถทำกำไรเพิ่มเติมอะไรได้บ้าง และการเปลี่ยนแปลง (พร้อมกับการหยุดชะงักที่เกี่ยวข้อง) คุ้มค่าหรือไม่?

ด้วยการใช้งานที่ปรับให้เหมาะสมที่สุด ซึ่งยังคงมีข้อจำกัดมากกว่าเล็กน้อย คุณสามารถบรรลุคุณลักษณะด้านประสิทธิภาพต่อไปนี้ (รันการทดสอบ 10 วินาที @ https://localhost:3000/seed/v1 — การเชื่อมต่อ 10 ครั้ง):

สถิติ เฉลี่ย Stdev แม็กซ์
เวลาในการตอบสนอง (มิลลิวินาที) 0.64 0.86 17
คำขอ/วินาที 8330.91 757.63 8991
ไบต์/วินาที 84.17 MB 7.64 MB 92.27 MB
คำขอ 92k ใน 11 วินาที อ่าน 937.22 MB

แม้ว่าการปรับปรุง 1.6 เท่าจะมีความสำคัญ แต่ก็สามารถโต้แย้งได้ขึ้นอยู่กับสถานการณ์ว่าความพยายาม การเปลี่ยนแปลง และการหยุดชะงักของโค้ดที่จำเป็นในการสร้างการปรับปรุงนี้มีความสมเหตุสมผลหรือไม่ โดยเฉพาะอย่างยิ่งเมื่อเทียบกับการปรับปรุง 200x ในการใช้งานดั้งเดิมด้วยการแก้ไขข้อผิดพลาดเพียงครั้งเดียว

เพื่อให้บรรลุการปรับปรุงนี้ มีการใช้เทคนิคการทำซ้ำแบบเดียวกันของโปรไฟล์ สร้าง flamegraph วิเคราะห์ ดีบัก และเพิ่มประสิทธิภาพเพื่อไปยังเซิร์ฟเวอร์ที่ปรับให้เหมาะสมขั้นสุดท้าย ซึ่งสามารถดูรหัสได้ที่นี่

การเปลี่ยนแปลงขั้นสุดท้ายเพื่อให้ถึง 8000 req/s คือ:

  • อย่าสร้างวัตถุแล้วทำให้เป็นอนุกรม สร้างสตริงของ JSON โดยตรง
  • ใช้สิ่งที่ไม่ซ้ำใครเกี่ยวกับเนื้อหาเพื่อกำหนดเป็น Etag แทนที่จะสร้างแฮช
  • อย่าแฮช URL ใช้เป็นคีย์โดยตรง

การเปลี่ยนแปลงเหล่านี้มีความเกี่ยวข้องมากขึ้นเล็กน้อย ก่อกวนต่อฐานโค้ดเล็กน้อย และทำให้มิดเดิลแวร์ etagger มีความยืดหยุ่นน้อยลงเล็กน้อย เนื่องจากเป็นภาระบนเส้นทางในการจัดเตรียมค่า Etag แต่มันบรรลุคำขอพิเศษ 3000 ต่อวินาทีบนเครื่องสร้างโปรไฟล์

มาดูกราฟเปลวไฟสำหรับการปรับปรุงขั้นสุดท้ายเหล่านี้กัน:

กราฟเปลวไฟแสดงให้เห็นว่ารหัสภายในที่เกี่ยวข้องกับโมดูลเน็ตตอนนี้เป็นคอขวด
กราฟเปลวไฟที่มีสุขภาพดีหลังจากการปรับปรุงประสิทธิภาพทั้งหมด

ส่วนที่ร้อนแรงที่สุดของกราฟเปลวไฟเป็นส่วนหนึ่งของแกนโหนดในโมดูล net นี้เหมาะ

การป้องกันปัญหาด้านประสิทธิภาพ

เพื่อเป็นการปิดท้าย ต่อไปนี้คือคำแนะนำบางประการเกี่ยวกับวิธีป้องกันปัญหาด้านประสิทธิภาพก่อนที่จะนำไปใช้งาน

การใช้เครื่องมือประสิทธิภาพเป็นจุดตรวจสอบที่ไม่เป็นทางการระหว่างการพัฒนาสามารถกรองจุดบกพร่องด้านประสิทธิภาพออกก่อนที่จะนำไปใช้จริง แนะนำให้ทำ AutoCannon and Clinic (หรือเทียบเท่า) เป็นส่วนหนึ่งของเครื่องมือในการพัฒนาทุกวัน

เมื่อซื้อกรอบงาน ให้ค้นหาว่านโยบายเกี่ยวกับประสิทธิภาพคืออะไร หากกรอบงานไม่ได้จัดลำดับความสำคัญของประสิทธิภาพ สิ่งสำคัญคือต้องตรวจสอบว่าสอดคล้องกับแนวทางปฏิบัติด้านโครงสร้างพื้นฐานและเป้าหมายทางธุรกิจหรือไม่ ตัวอย่างเช่น Restify ได้ลงทุนอย่างชัดเจน (ตั้งแต่เปิดตัวเวอร์ชัน 7) ในการเพิ่มประสิทธิภาพของไลบรารี อย่างไรก็ตาม หากต้นทุนต่ำและความเร็วสูงเป็นลำดับความสำคัญสูงสุด ให้พิจารณา Fastify ซึ่งผู้มีส่วนร่วมของ Restify วัดได้เร็วกว่า 17%

ระวังตัวเลือกห้องสมุดอื่นๆ ที่ส่งผลกระทบในวงกว้าง — โดยเฉพาะการพิจารณาการบันทึก ในขณะที่นักพัฒนาแก้ไขปัญหา พวกเขาอาจตัดสินใจเพิ่มเอาต์พุตบันทึกเพิ่มเติมเพื่อช่วยดีบักปัญหาที่เกี่ยวข้องในอนาคต หากใช้คนตัดไม้ที่ไม่มีประสิทธิภาพ สิ่งนี้อาจทำให้ประสิทธิภาพการทำงานแย่ลงเมื่อเวลาผ่านไปหลังจากแฟชั่นของนิทานกบที่กำลังเดือด ตัวบันทึก pino เป็นตัวบันทึก JSON ที่คั่นด้วยการขึ้นบรรทัดใหม่ที่เร็วที่สุดสำหรับ Node.js

สุดท้ายนี้ โปรดจำไว้เสมอว่า Event Loop เป็นทรัพยากรที่ใช้ร่วมกัน เซิร์ฟเวอร์ Node.js ถูกจำกัดด้วยตรรกะที่ช้าที่สุดในเส้นทางที่ร้อนแรงที่สุด