การล่าสัตว์และวิเคราะห์การใช้งาน CPU สูงใน .NET Applications
เผยแพร่แล้ว: 2022-03-11การพัฒนาซอฟต์แวร์อาจเป็นกระบวนการที่ซับซ้อนมาก เราในฐานะนักพัฒนาจำเป็นต้องคำนึงถึงตัวแปรต่างๆ มากมาย บางส่วนไม่ได้อยู่ภายใต้การควบคุมของเรา บางส่วนไม่เป็นที่รู้จักสำหรับเราในขณะที่มีการเรียกใช้โค้ดจริง และบางส่วนนั้นถูกควบคุมโดยเราโดยตรง และนักพัฒนา .NET ก็ไม่มีข้อยกเว้นสำหรับสิ่งนี้
จากความเป็นจริงนี้ สิ่งต่างๆ มักจะเป็นไปตามแผนที่วางไว้เมื่อเราทำงานในสภาพแวดล้อมที่มีการควบคุม ตัวอย่างคือเครื่องพัฒนาของเรา หรือสภาพแวดล้อมการรวมที่เราสามารถเข้าถึงได้เต็มรูปแบบ ในสถานการณ์เหล่านี้ เรามีเครื่องมือสำหรับการวิเคราะห์ตัวแปรต่างๆ ที่ส่งผลต่อโค้ดและซอฟต์แวร์ของเรา ในกรณีเหล่านี้ เราไม่ต้องจัดการกับเซิร์ฟเวอร์จำนวนมาก หรือผู้ใช้พร้อมกันที่พยายามทำสิ่งเดียวกันในเวลาเดียวกัน
ในสถานการณ์ที่อธิบายไว้และปลอดภัย โค้ดของเราจะทำงานได้ดี แต่ในการผลิตภายใต้ภาระหนักหรือปัจจัยภายนอกอื่นๆ ปัญหาที่ไม่คาดคิดอาจเกิดขึ้นได้ ประสิทธิภาพของซอฟต์แวร์ในการผลิตนั้นยากต่อการวิเคราะห์ ส่วนใหญ่เราต้องจัดการกับปัญหาที่อาจเกิดขึ้นในสถานการณ์เชิงทฤษฎี: เรารู้ว่าปัญหาอาจเกิดขึ้นได้ แต่เราไม่สามารถทดสอบได้ นั่นคือเหตุผลที่เราจำเป็นต้องพัฒนาแนวทางปฏิบัติที่ดีที่สุดและเอกสารประกอบสำหรับภาษาที่เราใช้อยู่ และหลีกเลี่ยงข้อผิดพลาดทั่วไป
ดังที่กล่าวไว้ เมื่อซอฟต์แวร์เริ่มทำงาน สิ่งต่างๆ อาจผิดพลาดได้ และโค้ดสามารถเริ่มทำงานในลักษณะที่เราไม่ได้วางแผนไว้ เราอาจจบลงในสถานการณ์เมื่อเราต้องจัดการกับปัญหาโดยไม่มีความสามารถในการดีบั๊กหรือรู้แน่ชัดว่าเกิดอะไรขึ้น เราจะทำอย่างไรในกรณีนี้?
ในบทความนี้ เราจะวิเคราะห์สถานการณ์กรณีจริงของการใช้งาน CPU สูงของเว็บแอปพลิเคชัน .NET บนเซิร์ฟเวอร์ที่ใช้ Windows กระบวนการที่เกี่ยวข้องในการระบุปัญหา และที่สำคัญกว่านั้น เหตุใดปัญหานี้จึงเกิดขึ้นตั้งแต่แรก และวิธีที่เรา แก้มัน
การใช้ CPU และการใช้หน่วยความจำเป็นหัวข้อที่กล่าวถึงกันอย่างแพร่หลาย โดยปกติแล้ว เป็นเรื่องยากมากที่จะทราบแน่นอนว่าจำนวนทรัพยากร (CPU, RAM, I/O) ที่เหมาะสมคือเท่าใดที่กระบวนการเฉพาะควรใช้ และช่วงเวลาใด แม้ว่าจะมีสิ่งหนึ่งที่แน่นอน - หากกระบวนการใช้ CPU มากกว่า 90% เป็นระยะเวลานาน เราก็ประสบปัญหาเพียงเพราะเซิร์ฟเวอร์จะไม่สามารถดำเนินการตามคำขออื่นใดภายใต้สถานการณ์นี้ได้
นี่หมายความว่ามีปัญหากับกระบวนการเองหรือไม่? ไม่จำเป็น. อาจเป็นไปได้ว่ากระบวนการต้องการพลังในการประมวลผลที่มากขึ้น หรือกำลังจัดการข้อมูลจำนวนมาก สำหรับการเริ่มต้น สิ่งเดียวที่เราทำได้คือพยายามระบุว่าเหตุใดจึงเกิดขึ้น
ระบบปฏิบัติการทั้งหมดมีเครื่องมือต่าง ๆ มากมายสำหรับตรวจสอบสิ่งที่เกิดขึ้นในเซิร์ฟเวอร์ เซิร์ฟเวอร์ Windows มีตัวจัดการงาน การตรวจสอบประสิทธิภาพโดยเฉพาะ หรือในกรณีของเรา เราใช้เซิร์ฟเวอร์ Relic ใหม่ ซึ่งเป็นเครื่องมือที่ยอดเยี่ยมสำหรับการตรวจสอบเซิร์ฟเวอร์
อาการแรกและการวิเคราะห์ปัญหา
หลังจากที่เราปรับใช้แอปพลิเคชันของเรา ในช่วงระยะเวลาสองสัปดาห์แรก เราเริ่มเห็นว่าเซิร์ฟเวอร์มีการใช้งาน CPU สูงสุด ซึ่งทำให้เซิร์ฟเวอร์ไม่ตอบสนอง เราต้องเริ่มต้นใหม่เพื่อให้ใช้งานได้อีกครั้ง และเหตุการณ์นี้เกิดขึ้นสามครั้งในช่วงเวลานั้น ดังที่ได้กล่าวไว้ก่อนหน้านี้ เราใช้ New Relic Servers เป็นมอนิเตอร์เซิร์ฟเวอร์ และพบว่า w3wp.exe
ใช้ CPU 94% ในขณะที่เซิร์ฟเวอร์หยุดทำงาน
กระบวนการของผู้ปฏิบัติงาน Internet Information Services (IIS) เป็นกระบวนการของ windows ( w3wp.exe
) ซึ่งเรียกใช้เว็บแอปพลิเคชัน และรับผิดชอบในการจัดการคำขอที่ส่งไปยังเว็บเซิร์ฟเวอร์สำหรับกลุ่มแอปพลิเคชันเฉพาะ เซิร์ฟเวอร์ IIS สามารถมีกลุ่มแอปพลิเคชันได้หลายกลุ่ม (และ w3wp.exe
ที่แตกต่างกันหลายรายการ) ซึ่งอาจทำให้เกิดปัญหาได้ จากผู้ใช้ที่กระบวนการมี (ซึ่งแสดงให้เห็นในรายงาน New Relic) เราพบว่าปัญหาคือแอปพลิเคชันดั้งเดิมของเว็บฟอร์ม .NET C#
.NET Framework ถูกรวมเข้ากับเครื่องมือแก้ไขข้อบกพร่องของ windows อย่างแน่นหนา ดังนั้นสิ่งแรกที่เราลองทำคือดูที่ตัวแสดงเหตุการณ์และไฟล์บันทึกของแอปพลิเคชันเพื่อค้นหาข้อมูลที่เป็นประโยชน์เกี่ยวกับสิ่งที่เกิดขึ้น ไม่ว่าเราจะมีข้อยกเว้นบางประการที่บันทึกไว้ในตัวแสดงเหตุการณ์หรือไม่ก็ตาม สิ่งเหล่านี้ไม่ได้ให้ข้อมูลเพียงพอที่จะวิเคราะห์ นั่นเป็นเหตุผลที่เราตัดสินใจที่จะก้าวไปอีกขั้นและรวบรวมข้อมูลเพิ่มเติม ดังนั้นเมื่อเหตุการณ์เกิดขึ้นอีกครั้ง เราก็พร้อมแล้ว
การเก็บรวบรวมข้อมูล
วิธีที่ง่ายที่สุดในการรวบรวมการถ่ายโอนข้อมูลของกระบวนการในโหมดผู้ใช้คือการใช้ Debug Diagnostic Tools v2.0 หรือ DebugDiag DebugDiag มีชุดเครื่องมือสำหรับการรวบรวมข้อมูล (DebugDiag Collection) และการวิเคราะห์ข้อมูล (DebugDiag Analysis)
ดังนั้น เรามาเริ่มกำหนดกฎสำหรับการรวบรวมข้อมูลด้วย Debug Diagnostic Tools:
เปิด DebugDiag Collection และเลือก
Performance
- เลือก ตัว
Performance Counters
และคลิกNext
- คลิก
Add Perf Triggers
- ขยายอ็อบเจ็กต์
Processor
(ไม่ใช่Process
) และเลือก% Processor Time
โปรดทราบว่าหากคุณใช้ Windows Server 2008 R2 และคุณมีโปรเซสเซอร์มากกว่า 64 ตัว โปรดเลือกอ็อบเจ็กต์Processor Information
แทนอ็อบเจ็กต์Processor
- ในรายการอินสแตนซ์ ให้เลือก
_Total
- คลิก
Add
แล้วคลิกOK
เลือกทริกเกอร์ที่เพิ่มใหม่แล้วคลิก
Edit Thresholds
- เลือก
Above
ในรายการดรอปดาวน์ - เปลี่ยนขีดจำกัดเป็น
80
ป้อน
20
สำหรับจำนวนวินาที คุณสามารถปรับค่านี้ได้หากจำเป็น แต่ระวังอย่าระบุวินาทีเล็กๆ น้อยๆ เพื่อป้องกันทริกเกอร์ที่ผิดพลาด- คลิก
OK
- คลิก
Next
- คลิก
Add Dump Target
- เลือก
Web Application Pool
จากดรอปดาวน์ - เลือกกลุ่มแอปพลิเคชันของคุณจากรายการกลุ่มแอป
- คลิก
OK
- คลิก
Next
- คลิก
Next
อีกครั้ง - ป้อนชื่อกฎของคุณ หากคุณต้องการและจดตำแหน่งที่จะบันทึกการทิ้งขยะ คุณสามารถเปลี่ยนตำแหน่งนี้ได้หากต้องการ
- คลิก
Next
- เลือก
Activate the Rule Now
ทันที และคลิกFinish
กฎที่อธิบายจะสร้างชุดของไฟล์ minidump ซึ่งจะมีขนาดค่อนข้างเล็ก ดัมพ์สุดท้ายจะเป็นดัมพ์ที่มีหน่วยความจำเต็ม และการดัมพ์นั้นจะมีขนาดใหญ่กว่ามาก ตอนนี้เราต้องรอให้เหตุการณ์ CPU สูงเกิดขึ้นอีกครั้งเท่านั้น

เมื่อเรามีไฟล์ดัมพ์ในโฟลเดอร์ที่เลือกแล้ว เราจะใช้เครื่องมือวิเคราะห์ DebugDiag เพื่อวิเคราะห์ข้อมูลที่รวบรวมได้:
เลือกตัววิเคราะห์ประสิทธิภาพ
เพิ่มไฟล์ดัมพ์
เริ่มการวิเคราะห์
DebugDiag จะใช้เวลาสองสาม (หรือหลายนาที) ในการแยกวิเคราะห์การถ่ายโอนข้อมูลและให้การวิเคราะห์ เมื่อการวิเคราะห์เสร็จสิ้น คุณจะเห็นหน้าเว็บที่มีข้อมูลสรุปและข้อมูลมากมายเกี่ยวกับชุดข้อความ ซึ่งคล้ายกับข้อความต่อไปนี้:
ดังที่คุณเห็นในบทสรุป มีคำเตือนว่า “ตรวจพบการใช้งาน CPU สูงระหว่างไฟล์ดัมพ์ในหนึ่งเธรดขึ้นไป” หากเราคลิกคำแนะนำ เราจะเริ่มเข้าใจว่าแอปพลิเคชันของเรามีปัญหาตรงไหน รายงานตัวอย่างของเรามีลักษณะดังนี้:
ดังที่เราเห็นในรายงาน มีรูปแบบเกี่ยวกับการใช้งาน CPU เธรดทั้งหมดที่มีการใช้งาน CPU สูงเกี่ยวข้องกับคลาสเดียวกัน ก่อนจะข้ามไปที่โค้ด เรามาดูอันแรกกันก่อน
นี่คือรายละเอียดสำหรับเธรดแรกที่มีปัญหาของเรา ส่วนที่เราสนใจมีดังนี้
ที่นี่เรามีการเรียกรหัสของเรา GameHub.OnDisconnected()
ซึ่งทำให้เกิดการดำเนินการที่มีปัญหา แต่ก่อนการโทรนั้น เรามีการเรียก Dictionary สองครั้ง ซึ่งอาจให้แนวคิดเกี่ยวกับสิ่งที่เกิดขึ้น ลองดูในโค้ด .NET เพื่อดูว่าวิธีการนั้นทำอะไร:
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
เห็นได้ชัดว่าเรามีปัญหาที่นี่ กองการโทรของรายงานกล่าวว่าปัญหาเกิดขึ้นกับพจนานุกรม และในรหัสนี้ เรากำลังเข้าถึงพจนานุกรม และโดยเฉพาะบรรทัดที่ก่อให้เกิดปัญหาคือสิ่งนี้:
if (onlineSessions.TryGetValue(userId, out connId))
นี่คือการประกาศพจนานุกรม:
static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();
โค้ด .NET นี้มีปัญหาอะไร
ทุกคนที่มีประสบการณ์การเขียนโปรแกรมเชิงวัตถุรู้ว่าตัวแปรสแตติกจะถูกแชร์โดยอินสแตนซ์ทั้งหมดของคลาสนี้ มาดูกันว่าสแตติกหมายถึงอะไรในโลก .NET กัน
ตามข้อกำหนด .NET C#:
ใช้ตัวแก้ไขแบบคงที่เพื่อประกาศสมาชิกแบบคงที่ซึ่งเป็นของประเภทเองแทนที่จะเป็นวัตถุเฉพาะ
นี่คือสิ่งที่ข้อกำหนด .NET C# langunge บอกเกี่ยวกับคลาสคงที่และสมาชิก:
เช่นเดียวกับกรณีของคลาสทุกประเภท ข้อมูลประเภทสำหรับคลาสสแตติกจะถูกโหลดโดยรันไทม์ภาษาทั่วไปของ .NET Framework (CLR) เมื่อโหลดโปรแกรมที่อ้างอิงคลาส โปรแกรมไม่สามารถระบุได้ว่าคลาสจะโหลดเมื่อใด อย่างไรก็ตาม รับประกันว่าจะโหลดได้และมีการเริ่มต้นฟิลด์และคอนสตรัคเตอร์แบบสแตติกเรียกก่อนที่คลาสจะถูกอ้างอิงเป็นครั้งแรกในโปรแกรมของคุณ ตัวสร้างสแตติกถูกเรียกเพียงครั้งเดียว และคลาสสแตติกยังคงอยู่ในหน่วยความจำตลอดอายุของโดเมนแอปพลิเคชันที่โปรแกรมของคุณอยู่
คลาสที่ไม่ใช่สแตติกสามารถมีเมธอด ฟิลด์ คุณสมบัติ หรือเหตุการณ์แบบสแตติก สมาชิกสแตติกสามารถเรียกใช้ในคลาสได้แม้ว่าจะไม่ได้สร้างอินสแตนซ์ของคลาสก็ตาม สมาชิกแบบคงที่สามารถเข้าถึงได้โดยชื่อคลาส ไม่ใช่ชื่ออินสแตนซ์ มีสมาชิกสแตติกเพียงสำเนาเดียวเท่านั้น โดยไม่คำนึงถึงจำนวนอินสแตนซ์ของคลาสที่ถูกสร้างขึ้น เมธอดและคุณสมบัติแบบคงที่ไม่สามารถเข้าถึงฟิลด์และเหตุการณ์ที่ไม่คงที่ในประเภทที่บรรจุ และไม่สามารถเข้าถึงตัวแปรอินสแตนซ์ของอ็อบเจกต์ใดๆ เว้นแต่ว่าจะถูกส่งผ่านในพารามิเตอร์เมธอดอย่างชัดแจ้ง
ซึ่งหมายความว่าสมาชิกสแตติกเป็นของประเภทเอง ไม่ใช่อ็อบเจ็กต์ พวกเขายังโหลดลงในโดเมนแอปพลิเคชันโดย CLR ดังนั้นสมาชิกแบบคงที่จึงเป็นของกระบวนการที่โฮสต์แอปพลิเคชันไม่ใช่เฉพาะเธรด
เนื่องจากสภาพแวดล้อมของเว็บเป็นสภาพแวดล้อมแบบมัลติเธรด เนื่องจากทุกคำขอเป็นเธรดใหม่ที่เกิดขึ้นโดย w3wp.exe
และเนื่องจากสมาชิกแบบสแตติกเป็นส่วนหนึ่งของกระบวนการ เราอาจมีสถานการณ์ที่เธรดต่างๆ พยายามเข้าถึงข้อมูลของตัวแปรสแตติก (แชร์โดยหลายเธรด) ซึ่งอาจนำไปสู่ปัญหามัลติเธรดในท้ายที่สุด
เอกสารพจนานุกรมภายใต้ความปลอดภัยของเธรดระบุสิ่งต่อไปนี้:
Dictionary<TKey, TValue>
สามารถรองรับผู้อ่านหลาย ๆ คนพร้อมกัน ตราบใดที่ไม่มีการแก้ไขคอลเล็กชัน ถึงกระนั้น การแจงนับผ่านคอลเล็กชันก็ไม่ใช่โพรซีเดอร์ที่ปลอดภัยสำหรับเธรดภายใน ในกรณีที่หายากซึ่งการแจงนับขัดแย้งกับการเข้าถึงการเขียน คอลเล็กชันต้องถูกล็อคระหว่างการแจงนับทั้งหมด ในการอนุญาตให้เข้าถึงคอลเลกชั่นได้หลายเธรดสำหรับการอ่านและการเขียน คุณต้องใช้การซิงโครไนซ์ของคุณเอง
ข้อความนี้อธิบายว่าทำไมเราจึงอาจมีปัญหานี้ จากข้อมูลดัมพ์ ปัญหาเกิดขึ้นกับเมธอด FindEntry ของพจนานุกรม:
หากเราดูที่การใช้พจนานุกรม FindEntry เราจะเห็นว่าวิธีการวนซ้ำผ่านโครงสร้างภายใน (ถัง) เพื่อค้นหาค่า
ดังนั้นโค้ด .NET ต่อไปนี้จึงระบุคอลเล็กชัน ซึ่งไม่ใช่การดำเนินการที่ปลอดภัยสำหรับเธรด
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
บทสรุป
ดังที่เราเห็นในการดัมพ์ มีหลายเธรดที่พยายามทำซ้ำและแก้ไขทรัพยากรที่ใช้ร่วมกัน (พจนานุกรมแบบคงที่) พร้อมกัน ซึ่งในที่สุดทำให้การวนซ้ำเข้าสู่การวนซ้ำไม่สิ้นสุด ทำให้เธรดใช้ CPU มากกว่า 90% .
มีวิธีแก้ปัญหาที่เป็นไปได้หลายประการสำหรับปัญหานี้ สิ่งที่เราดำเนินการก่อนคือการล็อกและซิงโครไนซ์การเข้าถึงพจนานุกรมโดยสูญเสียประสิทธิภาพ เซิร์ฟเวอร์ขัดข้องทุกวันในขณะนั้น ดังนั้นเราจึงจำเป็นต้องแก้ไขปัญหานี้โดยเร็วที่สุด แม้ว่านี่จะไม่ใช่วิธีแก้ปัญหาที่ดีที่สุด แต่ก็สามารถแก้ไขปัญหาได้
ขั้นตอนต่อไปในการแก้ปัญหานี้คือการวิเคราะห์โค้ดและค้นหาวิธีแก้ปัญหาที่เหมาะสมที่สุดสำหรับสิ่งนี้ การปรับโครงสร้างโค้ดใหม่คือตัวเลือก: คลาส ConcurrentDictionary ใหม่สามารถแก้ปัญหานี้ได้เนื่องจากจะล็อกที่ระดับบัคเก็ตเท่านั้นซึ่งจะปรับปรุงประสิทธิภาพโดยรวม แม้ว่านี่เป็นขั้นตอนใหญ่ และจำเป็นต้องมีการวิเคราะห์เพิ่มเติม