การล่าสัตว์และวิเคราะห์การใช้งาน CPU สูงใน .NET Applications

เผยแพร่แล้ว: 2022-03-11

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

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

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

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

การใช้งาน CPU สูงคือเมื่อกระบวนการใช้ CPU มากกว่า 90% เป็นระยะเวลานาน - และเรากำลังประสบปัญหา

หากกระบวนการใช้ CPU มากกว่า 90% เป็นระยะเวลานาน แสดงว่าเรากำลังประสบปัญหา
ทวีต

ในบทความนี้ เราจะวิเคราะห์สถานการณ์กรณีจริงของการใช้งาน 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:

  1. เปิด DebugDiag Collection และเลือก Performance

    เครื่องมือวินิจฉัยดีบัก

  2. เลือก ตัว Performance Counters และคลิก Next
  3. คลิก Add Perf Triggers
  4. ขยายอ็อบเจ็กต์ Processor (ไม่ใช่ Process ) และเลือก % Processor Time โปรดทราบว่าหากคุณใช้ Windows Server 2008 R2 และคุณมีโปรเซสเซอร์มากกว่า 64 ตัว โปรดเลือกอ็อบเจ็กต์ Processor Information แทนอ็อบเจ็กต์ Processor
  5. ในรายการอินสแตนซ์ ให้เลือก _Total
  6. คลิก Add แล้วคลิก OK
  7. เลือกทริกเกอร์ที่เพิ่มใหม่แล้วคลิก Edit Thresholds

    ตัวนับประสิทธิภาพ

  8. เลือก Above ในรายการดรอปดาวน์
  9. เปลี่ยนขีดจำกัดเป็น 80
  10. ป้อน 20 สำหรับจำนวนวินาที คุณสามารถปรับค่านี้ได้หากจำเป็น แต่ระวังอย่าระบุวินาทีเล็กๆ น้อยๆ เพื่อป้องกันทริกเกอร์ที่ผิดพลาด

    คุณสมบัติของทริกเกอร์การตรวจสอบประสิทธิภาพ

  11. คลิก OK
  12. คลิก Next
  13. คลิก Add Dump Target
  14. เลือก Web Application Pool จากดรอปดาวน์
  15. เลือกกลุ่มแอปพลิเคชันของคุณจากรายการกลุ่มแอป
  16. คลิก OK
  17. คลิก Next
  18. คลิก Next อีกครั้ง
  19. ป้อนชื่อกฎของคุณ หากคุณต้องการและจดตำแหน่งที่จะบันทึกการทิ้งขยะ คุณสามารถเปลี่ยนตำแหน่งนี้ได้หากต้องการ
  20. คลิก Next
  21. เลือก Activate the Rule Now ทันที และคลิก Finish

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

เมื่อเรามีไฟล์ดัมพ์ในโฟลเดอร์ที่เลือกแล้ว เราจะใช้เครื่องมือวิเคราะห์ DebugDiag เพื่อวิเคราะห์ข้อมูลที่รวบรวมได้:

  1. เลือกตัววิเคราะห์ประสิทธิภาพ

    เครื่องมือวิเคราะห์ DebugDiag

  2. เพิ่มไฟล์ดัมพ์

    DebugDiag Analysis Toll Dump Files

  3. เริ่มการวิเคราะห์

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

สรุปการวิเคราะห์

ดังที่คุณเห็นในบทสรุป มีคำเตือนว่า “ตรวจพบการใช้งาน CPU สูงระหว่างไฟล์ดัมพ์ในหนึ่งเธรดขึ้นไป” หากเราคลิกคำแนะนำ เราจะเริ่มเข้าใจว่าแอปพลิเคชันของเรามีปัญหาตรงไหน รายงานตัวอย่างของเรามีลักษณะดังนี้:

10 อันดับสูงสุดเรียงตาม CPU เฉลี่ย

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

.NET Call Stack

นี่คือรายละเอียดสำหรับเธรดแรกที่มีปัญหาของเรา ส่วนที่เราสนใจมีดังนี้

.NET Call Stack รายละเอียด

ที่นี่เรามีการเรียกรหัสของเรา 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 ของพจนานุกรม:

.NET Call Stack รายละเอียด

หากเราดูที่การใช้พจนานุกรม 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 ใหม่สามารถแก้ปัญหานี้ได้เนื่องจากจะล็อกที่ระดับบัคเก็ตเท่านั้นซึ่งจะปรับปรุงประสิทธิภาพโดยรวม แม้ว่านี่เป็นขั้นตอนใหญ่ และจำเป็นต้องมีการวิเคราะห์เพิ่มเติม