.NET-এ GC-এর অপেক্ষা ও মেমরি লিক আলাদা করা ── বাড়তে থাকা মেমরি পর্যবেক্ষণ, তুলনা ও প্রমাণের বাস্তব ধাপ

· হালনাগাদের তারিখ: · · .NET, CSharp, GC, মেমরি লিক, Diagnostics, dotnet-counters, dotnet-dump, অপারেশন, বিদ্যমান সম্পদের পুনর্ব্যবহার

1. আগে যা বুঝে নিতে হবে

.NET অ্যাপ্লিকেশন চালাতে গেলে মেমরি ব্যবহার আস্তে আস্তে বাড়তে দেখা যায়।

টাস্ক ম্যানেজার বা top দেখলে প্রক্রিয়ার মেমরি বাড়ছে। কন্টেইনারের মেমরি ব্যবহারও বাড়ছে। মনিটরিংয়ে Working Set বা RSS-এর গ্রাফ ডানদিকে উপরের দিকে উঠছে।

এ অবস্থা দেখে সঙ্গে সঙ্গে «মেমরি লিক নয় তো» ভাবা স্বাভাবিক। কিন্তু .NET-এ প্রক্রিয়ার মেমরি বাড়া আর মেমরি লিক এক জিনিস নয়।

.NET-এ garbage collection আছে। অবজেক্ট অপ্রয়োজনীয় হওয়ার মুহূর্তেই মেমরি OS-এ ফিরে যায় না। GC বরাদ্দের অবস্থা, heap-এর থ্রেশহোল্ড, মেমরি চাপ, generation ও ওয়ার্কলোড দেখে চলে।

তাই এমন অবস্থা ঘটে।

  • অপ্রয়োজনীয় অবজেক্ট এখনো GC হয়নি
  • GC হয়ে গেছে, কিন্তু প্রক্রিয়ার Working Set সঙ্গে সঙ্গে নামে না
  • প্রথম অ্যাক্সেস, JIT, ক্যাশ, কানেকশন পুলে একবার বেড়ে তারপর স্থির হয়
  • managed heap স্থির, কিন্তু native মেমরি, থ্রেড, সকেট বা ইমেজ-প্রসেসিং লাইব্রেরির দিকে বাড়ছে
  • সত্যিই অপ্রয়োজনীয় হওয়া উচিত অবজেক্ট কোথাও থেকে রেফারেন্স হয়েই আছে

এই নিবন্ধে শেষের কেস — «সত্যি লিক» — কীভাবে আলাদা করতে হয় তা নিয়ে। দেখতে হবে নিছক মেমরি ব্যবহার নয়, এই তিনটি।

  1. GC-এর পরেও বেঁচে থাকা মেমরি বাড়ছে কি না
  2. কোন টাইপ বাড়ছে
  3. সেই অবজেক্টকে কে রেফারেন্স করছে

.NET মেমরি লিক তদন্ত «মেমরি বাড়ছে»-তে থেমে যায় না। কাজটি হলো «এই টাইপের অবজেক্ট বাড়ছে, আর এই রুট থেকে রেফারেন্স হয়েই আছে» পর্যন্ত নিয়ে যাওয়া।

এই নিবন্ধের কোড GitHub-এ বিল্ড ও চালানো যায় এমন নমুনা সেট হিসেবে প্রকাশিত (সাধারণ লিক প্যাটার্নের লাইব্রেরি, GC-এর অপেক্ষা ও বেঁচে থাকার পার্থক্য দেখার ডেমো, WeakReference দিয়ে ধরে রাখা ও collect যাচাই করা ইউনিট টেস্ট)।

dotnet-gc-or-memory-leak - komurasoft-blog-samples (GitHub)

এই নিবন্ধের জ্ঞান মানচিত্র

.NET অ্যাপের মেমরি বাড়া দুই ভাগে যায়: garbage collection এখনো collect করেনি কেবল সেই অবস্থা, আর static কালেকশন, সীমাহীন ক্যাশ, ইভেন্ট unsubscribe না করা, Timer Dispose না করা, IDisposable ছেড়ে না দেওয়া, DI lifetime ভুল চেনা ইত্যাদির কারণে সত্যিই রেফারেন্স থেকে যাওয়া অবস্থা। আলাদা করতে প্রথমে dotnet-counters দিয়ে GC heap, Gen 2 ও LOH-এর প্রবণতা দেখা হয়, আর ক্রমবর্ধমান মান Total Allocated বা OS-এর দৃষ্টির Working Set বাড়া একা দিয়ে বিচার না করা জরুরি হয়ে ওঠে। তারপর dotnet-gcdump দিয়ে লোডের আগে-পরে টাইপপ্রতি Count ও Size তুলনা করে কোন টাইপ বেড়েছে চিহ্নিত করা হয়, আর dotnet-dump-এর dumpheap ও gcroot কমান্ডে সেই অবজেক্টকে কে রেফারেন্স করে রাখছে পর্যন্ত অনুসরণ করা হয়। Forced GC-তে মেমরি নামলেও মূল কারণ মুছে যায় না, প্রোডাকশনে রাখার সমাধান হয় না।

.NET-এ GC-এর অপেক্ষা ও মেমরি লিক আলাদা করার জ্ঞান মানচিত্র.NET-এ মেমরি বাড়া GC-এর অপেক্ষা নাকি সত্যি মেমরি লিক, তা dotnet-counters, dotnet-gcdump, dotnet-dump ও gcroot দিয়ে আলাদা করার সম্পর্ক দেখানো চিত্রদিয়ে যাচাইদিয়ে যাচাইদিয়ে যাচাইদিয়ে যাচাইদিয়ে যাচাইদিয়ে যাচাইদিয়ে যাচাইব্যবহার নিরুৎসাহিতব্যবহার নিরুৎসাহিতব্যবহার নিরুৎসাহিতকারণ হতে পারেকারণ হতে পারেকারণ হতে পারেকারণ হতে পারেকারণ হতে পারেকারণ হতে পারেকারণ হতে পারেআগে করা উচিতআগে করা উচিতসামঞ্জস্যহীনসামঞ্জস্যহীনব্যবহার করেপূর্বশর্ত.NET-এর গার্বেজ কালেকশন (GC).NET-এর মেমোরি লিক (অনিচ্ছাকৃত ধরে রাখা)dotnet-countersdotnet-gcdumpdotnet-dumpgcroot কমান্ডGC হিপ (ম্যানেজড হিপ)dumpheap -stat কমান্ডdotnet-traceGen 2 (দ্বিতীয় জেনারেশনের হিপ)Total Allocated (ক্রমবর্ধমান অ্যালোকেশন)Working Set / RSSজোরপূর্বক GC (Induced Collection)static কালেকশনে ধরে রাখাসীমা ও মেয়াদ ছাড়া ক্যাশইভেন্ট আনসাবস্ক্রাইব না করার লিকTimer Dispose না করার লিকIDisposable Dispose না করার লিকDI lifetime ভুল চেনাLOH (Large Object Heap).NET Framework.NET (Core-এর পর থেকে)

চিত্রে, টানা রেখা সবসময় সত্য এমন সম্পর্ককে এবং ভাঙা রেখা শর্তসাপেক্ষ সম্পর্ককে নির্দেশ করে (শর্তগুলো বিস্তারিত পাতায় প্রতিটি সম্পর্কের ব্যাখ্যায় আছে)। সম্পর্কের সম্পূর্ণ তালিকা (মোট 23, প্রমাণ ও নিশ্চয়তার মাত্রাসহ) এবং প্রধান ধারণাগুলোর সংজ্ঞা জ্ঞান মানচিত্রের বিস্তারিত পাতায় সংগ্রহ করা আছে (জাপানি ভাষায়)। তথ্য: JSON-LD / Turtle

2. আগে «মেমরি লিক»-এর অর্থ এক করা

.NET-এ মেমরি লিক শুধু C বা C++-এর মতো «বরাদ্দ করা মেমরি ফ্রি করতে ভুলে যাওয়া» আকারেই আসে না।

Managed কোডে GC অবজেক্ট collect করে। GC collect করতে পারবে কি না তা নির্ভর করে «সেই অবজেক্টে পৌঁছানো যায় এমন রেফারেন্স এখনো আছে কি না»-এর উপর।

অর্থাৎ .NET-এর সাধারণ মেমরি লিক এরকম।

ব্যবসায়িকভাবে আর দরকার নেই, তবু static ফিল্ড, ক্যাশ, ইভেন্ট, Timer, কালেকশন, DI lifetime, অ্যাসিঙ্ক কনটেক্সট ইত্যাদি থেকে রেফারেন্স হয়েই আছে বলে GC-এর চোখে এখনো ব্যবহৃত মনে হয়।

GC চতুর, কিন্তু ব্যবসায়িকভাবে দরকার কি না বোঝে না। রেফারেন্স থাকলে জীবিত বলেই ধরে।

তাই .NET-এ «লিক»-এর চেয়ে «অনিচ্ছাকৃত ধরে রাখা» ভাবা সহজ হয়।

অন্যদিকে নিচের অবস্থা সঙ্গে সঙ্গে মেমরি লিক বলা যায় না।

অবস্থা লিক নাও হতে পারে কেন
Working Set / RSS বাড়ছে OS প্রক্রিয়াকে যে মেমরি দিয়েছে; managed heap-এর জীবিত অবজেক্টের পরিমাণের সাথে মিলে না
Total Allocated বাড়ছে স্টার্টআপের পর থেকে বরাদ্দের ক্রমবর্ধমান যোগ; অ্যাপ চললে মূলত বাড়েই
মুহূর্তে GC Heap বেড়ে ওঠা পরের GC পর্যন্ত এখনো collect না হওয়া অবজেক্ট থাকতে পারে
স্টার্টআপের ঠিক পরে বাড়ে JIT, টাইপ লোড, প্রাথমিক ক্যাশ, কানেকশন পুল, টেমপ্লেট এক্সপ্যানশনে সাধারণ
LOH বড় বড় অ্যারে ও বাফার পুনর্ব্যবহার, fragmentation বা পুল কৌশলের প্রভাব হতে পারে
মেমরি নামছে না GC collect করলেও প্রক্রিয়া সঙ্গে সঙ্গে OS-এ মেমরি ফেরায় না

উল্টো, নিচের অবস্থা যত একসঙ্গে মিলবে, মেমরি লিকের সন্দেহ তত জোরালো।

পর্যবেক্ষণ অর্থ
একই কাজ বারবার করলে GC-এর পরের heap বাড়ে বেঁচে থাকা অবজেক্ট বাড়ছে
Gen 2 বা LOH-এর সাইজ বাড়তেই থাকে দীর্ঘজীবী অবজেক্ট, অথবা বড় অবজেক্ট থেকে যাচ্ছে
একাধিক dump-এ একই টাইপের Count / Size বাড়ে কোন টাইপ বাড়ছে চিহ্নিত করা যায়
gcroot-এ static, ইভেন্ট, ক্যাশ, দীর্ঘজীবী সার্ভিস থেকে রেফারেন্স দেখা যায় GC কেন collect করতে পারছে না তা ব্যাখ্যা করা যায়
লোড থামিয়েও যথেষ্ট সময় বা যাচাই-এর GC-এর পরে ফেরে না নিছক অস্থায়ী বরাদ্দ নয়, সম্ভাবনা বেশি

3. «কোন মেমরি» দেখছেন তা আলাদা করা

মেমরি তদন্তে প্রথম বিভ্রান্তি নানা মেমরি সূচক মিশে যাওয়া। সবই «মেমরি», কিন্তু অর্থ আলাদা।

সূচক কী দেখে কীভাবে পড়বেন
Working Set / RSS ভৌত মেমরিতে থাকা প্রক্রিয়ার পেজ OS-এর দৃষ্টির মেমরি; GC heap নিজে নয়
Private Bytes / Commit প্রক্রিয়ার private commit করা মেমরি native মেমরি, স্ট্যাক, JIT কোড, GC সেগমেন্টও ধরে
GC Heap Size managed heap-এর অবজেক্টের পরিমাণ .NET-এর GC-টার্গেট মেমরি দেখার প্রবেশপথ
Total Allocated স্টার্টআপের পর থেকে বরাদ্দের ক্রমবর্ধমান যোগ মূলত বাড়েই; একা একা লিক বিচার করা যায় না
Gen 0 / Gen 1 / Gen 2 generation অনুযায়ী heap Gen 2-এ যা থাকে তা দীর্ঘজীবী
LOH 85,000 বাইট বা তার বেশি বড় অবজেক্টের heap বড় অ্যারে, স্ট্রিং, বাফারে বাড়ে
POH pin করা অবজেক্টের heap native interop ও pinning-এর প্রভাব দেখার ইঙ্গিত
Finalization Queue finalize-এর অপেক্ষায় অবজেক্ট Dispose না করা, finalizer জমে যাওয়ার ইঙ্গিত

কোন সূচক কোথায় দেখছে ছবিতে এভাবে দাঁড়ায়।

প্রক্রিয়ার মেমরি সূচকগুলোর সম্পর্কপ্রক্রিয়ার মেমরি managed GC Heap ও native অংশে ভাগ হয়ে Gen 0/1, Gen 2, LOH, POH ও স্ট্যাক, JIT, interop-এ যায়; দুই দিকই Private Bytes/Commit-এ গণনা হয়, Working Set/RSS শুধু ভৌত মেমরিতে থাকা অংশগণনা হয়গণনা হয়শুধু ভৌত মেমরিতে থাকা অংশপ্রক্রিয়া যে মেমরি ব্যবহার করেmanaged দিকGC Heap Size-এ দেখা যায় এমন অংশnative দিকGC Heap Size-এ আসে না এমন অংশGen 0 / Gen 1স্বল্পজীবী বরাদ্দGen 2বেঁচে থাকা দীর্ঘজীবী অবজেক্টLOH85,000 বাইট বা তার বেশি বড় অবজেক্টPOHpin করা অবজেক্টথ্রেড স্ট্যাকJIT করা কোড, লোড করা অ্যাসেম্বলিP/Invoke, COM, বাহ্যিক লাইব্রেরির বাফারPrivate Bytes / Commitপ্রক্রিয়ার private commit করা মেমরিWorking Set / RSS

এই ছবিতে দুটি কথা ধরে রাখুন।

  1. dumpheap-এ যা দেখা যায় তা শুধু managed দিক। Native দিক বাড়লে heap যতই তাকান, অপরাধী বেরোয় না।
  2. Working Set ও Commit একে অন্যের ভিতরে থাকা নেস্টেড সম্পর্ক নয়। Commit করা থাকলেও ভৌত মেমরিতে না থাকলে Working Set-এ আসে না; উল্টো শেয়ার করা লাইব্রেরির পেজের মতো private নয় এমন কিছু Working Set-এ গোনা যায়। «Working Set নামছে না বলে GC collect করছে না» বলা যায় না, এই কারণেই।

শুরুতে সবকিছু বিস্তারিত দেখতে হয় না। আগে প্রশ্ন ভাগ করুন।

প্রক্রিয়ার মেমরি বাড়ছে
  ↓
managed heap-ও কি বাড়ছে?
  ↓
GC-এর পরেও বেঁচে থাকা পরিমাণ কি বাড়ছে?
  ↓
কোন টাইপ বাড়ছে?
  ↓
কে রেফারেন্স করছে?

এই ক্রম রাখলে «দেখতে বাড়ছে এমন মেমরি» আর «সত্যি লিক» গুলিয়ে ফেলা কঠিন হয়।

4. সিদ্ধান্তের ধারা

কাজে নিচের ধারায় আলাদা করলে এগোতে সুবিধা হয়।

1. পুনরুৎপাদনের শর্ত ঠিক করুন
   - কোন API, স্ক্রিন, জব, ব্যাচে বাড়ে
   - কতবার চালালে বাড়ে
   - লোড থামালে কী হয়

2. dotnet-counters দিয়ে প্রবণতা দেখুন
   - Working Set
   - GC Heap
   - Gen 2 / LOH
   - Total Allocated
   - GC সংখ্যা

3. সময়ের ব্যবধানে তুলনা করুন
   - স্টার্টআপের ঠিক পরে
   - warm-up-এর পরে
   - লোড চলাকালীন
   - লোড থামার পরে
   - একই কাজ N বার করার পরে

4. dump দুইবার বা তার বেশি নিন
   - before
   - after
   - সম্ভব হলে লোড থামার পরেও নিন

5. বাড়তে থাকা টাইপ খুঁজুন
   - dumpheap -stat
   - gcdump report
   - Visual Studio / PerfView

6. রেফারেন্সের উৎস দেখুন
   - gcroot
   - gchandles
   - finalizequeue

7. রায় দিন
   - GC-এর অপেক্ষা
   - স্বাভাবিক ক্যাশ বৃদ্ধি
   - managed মেমরি লিক
   - native মেমরির সমস্যা
   - LOH fragmentation অথবা অস্থায়ী বড় বরাদ্দ

গুরুত্বপূর্ণ কথা, একবারের সংখ্যায় বিচার নয়। মেমরি লিক «বাড়তেই থাকা প্রবণতা», তাই এক বিন্দুর মান নয়, একই শর্তে সময়ের ব্যবধানে তুলনা করুন।

5. যে টুল ব্যবহার করব

এই নিবন্ধে মূলত এই টুলগুলো ব্যবহার করি।

টুল কোথায় কাজে লাগে
dotnet-counters চলমান প্রক্রিয়ার GC ও Working Set-এর প্রবণতা দেখা
dotnet-gcdump জীবিত managed অবজেক্টের পরিসংখ্যান হালকা করে নেওয়া
dotnet-dump heap বিস্তারিত দেখে dumpheapgcroot দিয়ে রেফারেন্সের উৎস পর্যন্ত যাওয়া
Visual Studio Memory Usage Windows-এ GUI তুলনা চাইলে
PerfView Windows-এ GC / heap / trace গভীরভাবে দেখতে চাইলে
dotnet-trace বরাদ্দ ও GC ইভেন্ট সময়রেখায় অনুসরণ করতে চাইলে

আগে CLI টুল বসান।

dotnet tool install --global dotnet-counters
dotnet tool install --global dotnet-dump
dotnet tool install --global dotnet-gcdump
dotnet tool install --global dotnet-trace

আগে থেকে থাকলে আপডেট করুন।

dotnet tool update --global dotnet-counters
dotnet tool update --global dotnet-dump
dotnet tool update --global dotnet-gcdump
dotnet tool update --global dotnet-trace

তদন্তের প্রক্রিয়া খুঁজুন।

dotnet-counters ps

এর পরের উদাহরণে লক্ষ্য প্রক্রিয়ার ID <PID> লেখা।

Linux, macOS বা কন্টেইনার পরিবেশে ডায়াগনস্টিক টুল ও লক্ষ্য প্রক্রিয়া একই ইউজার হিসেবে চলতে হয়। পরিবেশভেদে TMPDIR, ডায়াগনস্টিক পোর্ট, কন্টেইনারের PID নেমস্পেসের প্রভাবও পড়ে।

প্রোডাকশনে চালালে সঙ্গে সঙ্গে dump না নিয়ে আগে যাচাই পরিবেশে লোড ও প্রভাব দেখুন।

5.1 তদন্তের লক্ষ্য .NET Framework 4.x হলে

dotnet-counters, dotnet-dump, dotnet-gcdump .NET Core 3.0 ও পরের রানটাইমের ডায়াগনস্টিক ফিচার ব্যবহার করে। তদন্তের লক্ষ্য .NET Framework 4.x অ্যাপ হলে এগুলো চলে না। রক্ষণাবেক্ষণের লক্ষ্য Windows Forms, WPF বা ASP.NET-এর পুরনো অ্যাপ হলে এখানেই পড়েন।

বদলি এই ম্যাপে ভাবা যায়।

এই নিবন্ধের সরঞ্জাম .NET Framework 4.x-এ বিকল্প
dotnet-counters দিয়ে প্রবণতা দেখা পারফরম্যান্স মনিটর, অথবা Get-Counter দিয়ে .NET CLR Memory ক্যাটাগরির কাউন্টার দেখা
dotnet-gcdump দিয়ে টাইপ পরিসংখ্যান তুলনা PerfView-এর GC heap dump, অথবা Visual Studio-এর «Memory Usage» দিয়ে স্ন্যাপশট তুলনা
dotnet-dump collect দিয়ে dump নেওয়া ProcDump, টাস্ক ম্যানেজারের «Create dump file», অথবা Windows ত্রুটি রিপোর্ট সেটিং দিয়ে dump বের করা
dotnet-dump analyze দিয়ে dumpheap / gcroot WinDbg-এ .loadby sos clr চালিয়ে তারপর !dumpheap -stat!gcroot
dotnet-trace দিয়ে বরাদ্দ অনুসরণ PerfView-এর GC heap allocation সংগ্রহ

ভাবনা একই: «প্রবণতা দেখা», «টাইপ পরিসংখ্যান দুইবার তুলনা», «রেফারেন্সের উৎস অনুসরণ» — তিন ধাপ। শুধু সরঞ্জাম বদলায়।

পারফরম্যান্স মনিটরে দেখলে .NET CLR Memory ক্যাটাগরিতে আগে দেখতে চাইবেন এগুলো।

কাউন্টার কী দেখে
# Bytes in all Heaps Gen 1, Gen 2, LOH-এর যোগ। এই নিবন্ধের GC Heap Size-এর কাছাকাছি
Gen 2 heap size Gen 2-এর বর্তমান বাইট। বাড়তেই থাকলে লিক সন্দেহ
Large Object Heap size LOH-এর বর্তমান সাইজ
# Gen 2 Collections ফুল GC-এর সংখ্যা। হঠাৎ বেড়ে উঠলে অতিরিক্ত বরাদ্দ সন্দেহ
% Time in GC সাম্প্রতিক GC চক্রে GC-তে কাটা সময়ের অনুপাত
Finalization Survivors finalize-এর অপেক্ষায় বেঁচে থাকা অবজেক্টের সংখ্যা। Dispose না করার ইঙ্গিত
# Total committed Bytes GC যে ভার্চুয়াল মেমরি commit করেছে

জাপানি পরিবেশে কাউন্টার নাম জাপানিতে দেখা যেতে পারে। না পেলে ইংরেজি নাম খোঁজার বদলে .NET CLR メモリ মতো localized ক্যাটাগরি নামও খুঁজুন।

WinDbg-এ বিশ্লেষণের প্রবেশ SOS লোড করা।

0:000> .loadby sos clr
0:000> !dumpheap -stat
0:000> !gcroot <OBJECT_ADDRESS>

.NET Framework-এ SOS কমান্ডে ! লাগে। এই নিবন্ধের ৯ অধ্যায় থেকে যে dumpheap -statgcroot আসে, সেগুলো !dumpheap -stat, !gcroot হিসেবে পড়লে একই ধাপ হিসেবেই ব্যবহার করা যায়।

6. আগে dotnet-counters দিয়ে প্রবণতা দেখা

প্রথমে দেখতে হয় বিস্তারিত dump নয়, প্রবণতা।

dotnet-counters monitor \
  --process-id <PID> \
  --refresh-interval 3 \
  --counters System.Runtime

আউটপুট .NET সংস্করণভেদে কিছুটা আলাদা। .NET 9 ও পরে System.Runtime Meter নামে, .NET 8 ও আগে পুরনো EventCounter নামে দেখা যেতে পারে।

মূলত এই আইটেমগুলো দেখুন।

যে আইটেম কী দেখবেন
dotnet.process.memory.working_set OS-এর দৃষ্টিতে প্রক্রিয়ার resident মেমরি
dotnet.gc.last_collection.heap.size সাম্প্রতিক GC-এর পর generation অনুযায়ী heap সাইজ
dotnet.gc.last_collection.memory.committed_size GC যে মেমরি commit করেছে
dotnet.gc.heap.total_allocated স্টার্টআপের পর থেকে ক্রমবর্ধমান বরাদ্দ
dotnet.gc.collections generation অনুযায়ী GC সংখ্যা
dotnet.gc.pause.time GC pause time-এর ক্রমবর্ধমান যোগ

আগে লক্ষ্য সীমিত করেও মনিটর করতে পারেন।

dotnet-counters monitor \
  --process-id <PID> \
  --refresh-interval 3 \
  --counters System.Runtime[dotnet.process.memory.working_set,dotnet.gc.last_collection.heap.size,dotnet.gc.last_collection.memory.committed_size,dotnet.gc.heap.total_allocated,dotnet.gc.collections]

পরে দেখতে চাইলে CSV-তে রাখুন।

dotnet-counters collect \
  --process-id <PID> \
  --refresh-interval 5 \
  --format csv \
  --output counters.csv \
  --counters System.Runtime

এই পর্যায়ে যে পার্থক্য দেখতে চান তা নিচেরগুলো।

6.1 শুধু Total Allocated বাড়ে

dotnet.gc.heap.total_allocated ক্রমবর্ধমান মান। অ্যাপ রিকোয়েস্ট প্রক্রিয়া করলে অবজেক্ট বরাদ্দ হয়, আর সেই অবজেক্ট সঙ্গে সঙ্গে অপ্রয়োজনীয় হয়ে GC-তে collect হলেও ক্রমবর্ধমান বরাদ্দ বাড়ে।

তাই শুধু Total Allocated বাড়লেই মেমরি লিক বলা যায় না। দেখতে হবে বরাদ্দের পরে থেকে যায় কি না।

Total Allocated: বাড়ে
GC Heap Size:    কিছুটা ওঠানামা করে স্থির থাকে
Gen 2 / LOH:     বাড়তেই থাকে না

এ ক্ষেত্রে লিক নয়, বরাদ্দ বেশি এমন অ্যাপ।

সমাধান লিক ঠিক করা নয়; বরাদ্দ কমানো, বাফার পুনর্ব্যবহার, অতিরিক্ত LINQ পুনর্বিবেচনা, স্ট্রিং তৈরি কমানো, সিরিয়ালাইজেশন পুনর্বিবেচনা ইত্যাদি।

6.2 Working Set বাড়ে কিন্তু GC Heap স্থির

Working Set বা RSS বাড়ছে, অথচ GC Heap স্থির — এমন হয়। তখন managed অবজেক্টের লিক নাও হতে পারে।

সম্ভাব্য কারণ এদিকে।

  • JIT করা কোড
  • লোড করা অ্যাসেম্বলি
  • থ্রেড স্ট্যাক
  • native লাইব্রেরির মেমরি
  • Marshal.AllocHGlobal ইত্যাদি unmanaged মেমরি
  • ইমেজ, কম্প্রেশন, ক্রিপ্টো, DB ড্রাইভারের native বাফার
  • সকেট, ফাইল হ্যান্ডেল, SSL, HTTP/2, gRPC-এর অভ্যন্তরীণ বাফার
  • OS প্রক্রিয়া থেকে ভৌত পেজ এখনো ফেরত নেয়নি মাত্র

এ অবস্থায় dumpheap যতই দেখুন, মূল অপরাধী নাও মেলে।

বিচারের মাপকাঠি এরকম।

Working Set / RSS: বাড়ে
GC Heap Size:      স্থির
Gen 2 / LOH:       স্থির

এ ক্ষেত্রে .NET managed heap leak নয়; native মেমরি, হ্যান্ডেল, থ্রেড সংখ্যা, সকেট, বাহ্যিক লাইব্রেরি সন্দেহ করুন।

dotnet-counters-এ থেমে যাবেন না। OS টুল, কন্টেইনার মেট্রিক্স, হ্যান্ডেল সংখ্যা, থ্রেড সংখ্যা, native heap, বাহ্যিক লাইব্রেরির মেট্রিক্সও দেখুন।

6.3 GC Heap বাড়ে, কিন্তু লোড থামার পরে ফেরে

লোড চলাকালীন GC Heap বাড়া স্বাভাবিক।

রিকোয়েস্ট বেশি। অস্থায়ী অবজেক্ট বেশি। বড় JSON সামলানো। অস্থায়ী লিস্ট বা অ্যারে তৈরি।

এমন হলে পরের GC পর্যন্ত heap বাড়ে। লোড থামলে GC চলে, heap ফিরতে পারে।

লোড চলাকালীন:     GC Heap বাড়ে
লোড থামার পরে:    GC Heap নামে, অথবা এক মানে ফেরে
বারবার করার পরে:  baseline বাড়তেই থাকে না

এ ক্ষেত্রে «এখনো GC হয়নি» অথবা «অস্থায়ী বরাদ্দ বেশি» বলা যায়।

তবে লোড চলাকালীন অস্থায়ী বরাদ্দ অতিরিক্ত হলে GC সংখ্যা ও pause time বেড়ে পারফরম্যান্স সমস্যা হয়। লিক না হলেও পারফরম্যান্স উন্নতির লক্ষ্য হয়।

6.4 GC-এর পর Gen 2 / LOH বাড়তেই থাকে

সাবধান থাকতে হবে এই প্যাটার্নে।

একই কাজ বারবার করা
  ↓
Gen 2 বাড়ে
  ↓
LOH বাড়ে
  ↓
লোড থামিয়েও ফেরে না
  ↓
পরের মাপে আরও বাড়ে

Gen 2 দীর্ঘজীবী অবজেক্ট যে generation-এ থাকে। LOH-এ বড় অ্যারে ও স্ট্রিং পড়ে সহজে।

এখানে বাড়তেই থাকলে লিক, unbounded ক্যাশ, বিশাল বাফার ধরে রাখা, ইভেন্ট unsubscribe না করা, static কালেকশন, দীর্ঘজীবী সার্ভিসের ধরে রাখা সন্দেহ করুন।

এই পর্যায়ে পরের ধাপে যান।

7. «GC-এর অপেক্ষা» কি না যাচাইয়ের ভাবনা

«এখনো GC হয়নি মাত্র কি না» দেখতে হলে GC-এর যথেষ্ট সুযোগ পাওয়ার পরের অবস্থা দেখুন।

তবে প্রোডাকশন কোডে সহজে GC.Collect() ঢোকাবেন না।

GC.Collect() GC জোর করে চালায়। বিশেষ করে সব generation-এর blocking GC অ্যাপের pause time তৈরি করে। সাধারণ পরিচালনায় GC-এর উপর ছেড়ে দেওয়াই নিয়ম।

তবু তদন্তে নিয়ন্ত্রিত যাচাই পরিবেশে «forced GC-এর পরেও থাকে কি না» দেখা হয়।

যাচাইয়ের কনসোল অ্যাপ বা পুনরুৎপাদন পরিবেশে নিচের মতো কোডে ফুল GC-এর পরের অবস্থা দেখা যায়।

static void ForceFullGcForDiagnosticsOnly()
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

কথা হলো, এটাকে সমাধান হিসেবে ব্যবহার করবেন না। শুধু তদন্তের জন্য।

যাচাই করতে চান এই ধারা।

কাজের আগে
  ↓
কাজ N বার করুন
  ↓
লোড থামান
  ↓
যথেষ্ট অপেক্ষা, অথবা যাচাই পরিবেশে ফুল GC induce করুন
  ↓
GC-এর পরের heap কি কাজের আগের কাছাকাছি মানে ফেরে

ফিরলে GC-এর অপেক্ষা অথবা অস্থায়ী বরাদ্দের সম্ভাবনা বেশি। না ফিরলে, আর একই কাজ বারবার করলেই baseline উঠলে, কিছু বেঁচে থাকছে। সেই «কিছু» dump-এ খুঁজুন।

8. dotnet-gcdump দিয়ে হালকা তুলনা

প্রথম তুলনায় dotnet-gcdump সুবিধাজনক।

dotnet-gcdump চলমান .NET প্রক্রিয়া থেকে GC dump নিয়ে heap-এর টাইপপ্রতি পরিসংখ্যান দেখতে কাজে লাগে।

dotnet-gcdump collect --process-id <PID> --output before.gcdump

লোড দেওয়ার পর আর একবার নিন।

dotnet-gcdump collect --process-id <PID> --output after.gcdump

CLI-তে সরল রিপোর্টও দেখা যায়।

dotnet-gcdump report before.gcdump > before-heap.txt
dotnet-gcdump report after.gcdump  > after-heap.txt

দেখতে হবে টাইপপ্রতি CountSize

যেমন after-এ নিচের মতো টাইপ তীব্র বেড়ে থাকলে তদন্তের লক্ষ্য হয়।

Size (Bytes)   Count       Type
============   =====       ====
180,000,000    2,000,000   System.String
120,000,000    1,000,000   MyApp.Models.Customer
 90,000,000       25,000   System.Byte[]

গুরুত্বপূর্ণ কথা «বড় টাইপ» নয়, «বেড়ে যাওয়া টাইপ»।

System.StringSystem.Byte[] অনেক অ্যাপে উপরের দিকে আসে। উপরে থাকা মাত্র অপরাধী নয়।

তুলনার দৃষ্টি এভাবে।

before → after পড়া
Count প্রায় এক সেই টাইপ মূল অপরাধী নয়, সম্ভাবনা বেশি
Count ও Size দুইই বাড়ে প্রার্থী
MyApp.* টাইপ বাড়ে ব্যবসায়িক লজিকে ধরে রাখা সন্দেহ সহজ
System.Byte[] বাড়ে বাফার, সিরিয়ালাইজেশন, ইমেজ, কম্প্রেশন, HTTP, DB সন্দেহ
System.String বাড়ে ক্যাশ, লগ, JSON, ডিকশনারি কী, ডুপ্লিকেট স্ট্রিং সন্দেহ
Task, Timer, CancellationTokenSource বাড়ে অ্যাসিঙ্ক কাজ, Timer, ক্যান্সেল/আনসাবস্ক্রাইব না করা সন্দেহ

dotnet-gcdump তুলনার প্রবেশপথ হিসেবে সহজ, অন্যদিকে নেওয়ার সময় Gen 2 GC induce করে। Heap বড় বা লেটেন্সি-সংবেদনশীল পরিবেশে pause time ও অতিরিক্ত মেমরি খরচ দেখুন।

Windows হলে .gcdump Visual Studio বা PerfView-এ খুলে তুলনা করা যায়। Windows নয় এমন পরিবেশে CLI report দিয়ে টাইপ পরিসংখ্যান দেখে রেফারেন্সের গভীরে dotnet-dump-এ যাওয়াই বাস্তব।

9. dotnet-dump দিয়ে heap ও রেফারেন্সের উৎস দেখা

«বাড়তে থাকা টাইপ» দেখা গেলে পরের প্রশ্ন «কেন collect হচ্ছে না»।

তার জন্য dotnet-dump দিয়ে dump নিয়ে SOS কমান্ডে বিশ্লেষণ করুন।

dotnet-dump collect \
  --process-id <PID> \
  --type Heap \
  --output myapp-1.dmp

সময় দিয়ে আর একবার নিন।

dotnet-dump collect \
  --process-id <PID> \
  --type Heap \
  --output myapp-2.dmp

Dump নেওয়া ভারী কাজ। বিশেষ করে Full / Heap dump সাইজ বড়, প্রক্রিয়া ও কন্টেইনারে লোড দেয়। প্রোডাকশনে নিলে সময়, ডিস্ক খালি জায়গা, কন্টেইনার মেমরি সীমা, ব্যক্তিগত বা গোপন তথ্য মিশে যাওয়া দেখুন।

নেওয়া dump বিশ্লেষণ করুন।

dotnet-dump analyze myapp-2.dmp

আগে পুরো heap-এর পরিসংখ্যান দেখুন।

> dumpheap -stat

আউটপুট টাইপপ্রতি সংখ্যা ও সাইজ।

MT               Count       TotalSize Class Name
00007f...        120000      3840000   MyApp.Models.Order
00007f...        250000      8000000   System.String
00007f...         10000     40000000   System.Byte[]

নির্দিষ্ট টাইপে সীমিত করুন।

> dumpheap -stat -type MyApp.Models.Order

অথবা নির্দিষ্ট MethodTable দিয়ে সীমিত করুন।

> dumpheap -mt <MT>

ইনস্ট্যান্সের ঠিকানা জানা গেলে রেফারেন্সের উৎস দেখুন।

> gcroot <OBJECT_ADDRESS>

এখানেই সবচেয়ে গুরুত্বপূর্ণ জায়গা। gcroot দিয়ে সেই অবজেক্ট কেন জীবিত তা নিশ্চিত করুন।

যেমন এমন রেফারেন্স পথ দেখা গেল ধরুন।

static MyApp.CustomerCache._items
  -> System.Collections.Concurrent.ConcurrentDictionary<string, Customer>
  -> MyApp.Models.Customer
  -> System.String

এ ক্ষেত্রে GC কেন collect করছে না স্পষ্ট। Customer static ক্যাশ থেকে রেফারেন্স, তাই GC-এর চোখে এখনো ব্যবহৃত।

এখানেই পরের বিচার করা যায়।

  • সেই ক্যাশ সত্যিই দরকার কি না
  • সীমা আছে কি না
  • মেয়াদ আছে কি না
  • কী বাড়তেই থাকে এমন ডিজাইন কি না
  • টেন্যান্ট, ইউজার, তারিখ, রিকোয়েস্ট ID ইত্যাদি কী করে সীমাহীন বাড়ছে কি না

মেমরি লিক তদন্তে গুরুত্বপূর্ণ কথা, dumpheap -stat-এ থেমে যাওয়া নয়। dumpheap -stat বলে «কী বেশি», gcroot বলে «কেন থেকে গেছে»। ঠিক করার পথ দেয় পরেরটা।

10. আলাদা করার দ্রুত সারণি

কাজে যে প্যাটার্ন ঘন ঘন আসে তা সাজানো।

পর্যবেক্ষণ সম্ভাবনা তারপর কী দেখবেন
শুধু Total Allocated বাড়ে স্বাভাবিক বরাদ্দ, অথবা অতিরিক্ত বরাদ্দ Allocation Rate, GC সংখ্যা, CPU, dotnet-trace
Working Set বাড়ে কিন্তু GC Heap স্থির native মেমরি, JIT, স্ট্যাক, OS-এর ধরে রাখা থ্রেড সংখ্যা, হ্যান্ডেল সংখ্যা, native টুল, বাহ্যিক লাইব্রেরি
GC Heap শুধু লোডে বাড়ে, থামার পরে ফেরে GC-এর অপেক্ষা, অস্থায়ী বরাদ্দ লোড থামার পর Gen 2 / LOH, GC সংখ্যা
GC-এর পর Gen 2 বাড়তেই থাকে দীর্ঘজীবী অবজেক্ট ধরে রাখা dumpheap -stat, gcroot
LOH বাড়তেই থাকে বড় অ্যারে, বাফার, fragmentation, বিশাল স্ট্রিং System.Byte[], System.Char[], LOH, Free অঞ্চল
System.String বড় স্ট্রিং ক্যাশ, JSON, লগ, ডিকশনারি কী স্ট্রিং ধরে রাখা নিজের টাইপ খুঁজুন
System.Byte[] বড় বাফার, সিরিয়ালাইজেশন, ইমেজ, কম্প্রেশন, যোগাযোগ মালিক টাইপ, ArrayPool ফেরত না দেওয়া, native interop
Task বাড়ে শেষ না হওয়া অ্যাসিঙ্ক কাজ, অপেক্ষার কিউ async সমন্বয়, ক্যান্সেল, চ্যানেল, কিউ
Timer বাড়ে Timer Dispose না করা Dispose, নিবন্ধন তুলে নেওয়া, দীর্ঘজীবী সার্ভিস
CancellationTokenSource বাড়ে CTS Dispose না করা, অতিরিক্ত লিংকড টোকেন Dispose, আনলিংক, টাইমআউট তৈরির জায়গা
EventHandler বা delegate থেকে যায় ইভেন্ট unsubscribe না করা publisher / subscriber-এর আয়ুর ফারাক
Finalization Queue বাড়ে Dispose না করা, finalizer জমে যাওয়া finalizequeue, finalizer থ্রেড
Pinned handle বেশি pin করা বাফার, native interop gchandles, POH, pin করার জায়গা

11. সাধারণ লিকের আকার

সাতটি প্যাটার্ন দিচ্ছি, মাথা থেকে ক্রমে পড়তে হয় না। যে লক্ষণ দেখছেন তার কাছের সারি থেকে ঢুকুন।

অধ্যায় প্যাটার্ন সাধারণ লক্ষণ আগে যে সূচক দেখবেন
11.1 static কালেকশন কাজের সংখ্যার অনুপাতে বাড়ে, লোড থামিয়েও ফেরে না Gen 2। gcroot-এ static field আসে কি না
11.2 unbounded ক্যাশ চলার সময়ের অনুপাতে বাড়ে। রিস্টার্ট করলে ফেরে Gen 2। ক্যাশের সংখ্যা ও System.String-এর বাড়া
11.3 ইভেন্ট unsubscribe না করা স্ক্রিন বা স্কোপ খুলে বন্ধ করলেই বাড়ে সংশ্লিষ্ট ViewModel বা হ্যান্ডলার টাইপের Count। delegate দিয়ে gcroot
11.4 Timer Dispose না করা স্বল্পজীবী ভাবা অবজেক্ট মরে না, কলব্যাকও চলতেই থাকে System.Threading.Timer বা TimerQueueTimer-এর Count
11.5 IDisposable ছেড়ে না দেওয়া GC Heap স্থির, অথচ হ্যান্ডেল সংখ্যা বা প্রক্রিয়ার মেমরি বাড়ে হ্যান্ডেল সংখ্যা, Finalization Queue, Working Set
11.6 AsyncLocal বা কনটেক্সট ধরে রাখা রিকোয়েস্ট শেষ হলেও DTO থেকে যায় async state machine দিয়ে gcroot
11.7 DI lifetime ভুল রিকোয়েস্ট সংখ্যার অনুপাতে বাড়ে singleton টাইপ থেকে gcroot

«আগে যে সূচক দেখবেন» কলাম ১০ অধ্যায়ের দ্রুত সারণির জোড়ায় ব্যবহার করুন। ১০ অধ্যায় «পর্যবেক্ষণ থেকে সম্ভাবনা সীমিত করার সারণি», এই সারণি «প্যাটার্ন থেকে যাচাইয়ের সূচকে ফেরার সারণি»।

11.1 static কালেকশন

সবচেয়ে সহজ আকার।

public static class CustomerStore
{
    private static readonly List<Customer> Customers = new();

    public static void Add(Customer customer)
    {
        Customers.Add(customer);
    }
}

এই কোডে Customers-এ যোগ করা Customer প্রক্রিয়া বাঁচলেই থেকে যায়। অস্থায়ী রাখা ভেবে থাকলেও, static থেকে রেফারেন্স থাকলে GC collect করে না।

ঠিক করার দিক ব্যবহারভেদে বদলায়।

  • সীমা রাখুন
  • মেয়াদ রাখুন
  • MemoryCache ইত্যাদি ক্যাশ ব্যবস্থা ব্যবহার করুন
  • স্পষ্ট করে মুছুন
  • static ছাড়ুন, উপযুক্ত lifetime-এর সার্ভিসে সরান
  • স্থায়ী রাখাই উদ্দেশ্য হলে DB বা বাহ্যিক স্টোরেজে সরান

গুরুত্বপূর্ণ কথা «static খারাপ» নয়; static-এ রাখা জিনিস দীর্ঘজীবী হয়, সেই ধর্ম বুঝে ব্যবহার করা।

11.2 unbounded ক্যাশ

ক্যাশ ইচ্ছে করেই মেমরি ব্যবহার করে, তাই ডিজাইনমতো বাড়া লিক নয়। কিন্তু সীমা ও মেয়াদ ছাড়া ক্যাশ কার্যত মেমরি লিক হয়ে যায়।

public sealed class ReportCache
{
    private readonly Dictionary<string, Report> _cache = new();

    public Report GetOrCreate(string userId, DateTime date)
    {
        var key = $"{userId}:{date:O}";

        if (_cache.TryGetValue(key, out var report))
        {
            return report;
        }

        report = BuildReport(userId, date);
        _cache[key] = report;
        return report;
    }
}

এই উদাহরণে userIddate-এর জোড় বাড়তে থাকলে ক্যাশও বাড়তেই থাকে।

বিশেষ বিপদ কীতে এমন মান রাখা।

  • রিকোয়েস্ট ID
  • বর্তমান সময়
  • GUID
  • সেশন ID
  • ইউজার ইনপুট নরমালাইজ না করে স্ট্রিং হিসেবে ব্যবহার
  • SQL বা সার্চ শর্ত যেমন আছে তেমন স্ট্রিং করা

ক্যাশে এই শর্ত আগে ঠিক করুন।

শর্ত উদাহরণ
সর্বোচ্চ সংখ্যা 10,000 এন্ট্রি পর্যন্ত
সর্বোচ্চ সাইজ 256MB পর্যন্ত
স্লাইডিং মেয়াদ শেষ অ্যাক্সেসের ৩০ মিনিট পরে
পরম মেয়াদ তৈরির ৬ ঘণ্টা পরে
সরিয়ে দেওয়ার শর্ত টেন্যান্ট মুছা, ইউজার মুছা, সেটিং বদল
মনিটর আইটেম সংখ্যা, আনুমানিক সাইজ, হিট রেট, সরিয়ে দেওয়ার সংখ্যা

নিয়ম «ক্যাশ বলে বাড়তেই পারে» নয়, «কতদূর বাড়তে পারে» ঠিক করা।

11.3 ইভেন্ট unsubscribe না করা

দীর্ঘজীবী publisher স্বল্পজীবী subscriber-কে রেফারেন্স করে রাখলে ইভেন্ট লিক হয়।

public sealed class OrderViewModel
{
    private readonly OrderService _service;

    public OrderViewModel(OrderService service)
    {
        _service = service;
        _service.OrderChanged += OnOrderChanged;
    }

    private void OnOrderChanged(object? sender, OrderChangedEventArgs e)
    {
        // update view model
    }
}

OrderService singleton আর OrderViewModel স্ক্রিনপ্রতি তৈরি হলে OrderService-এর ইভেন্ট OrderViewModel-কে রেফারেন্স করেই রাখে। স্ক্রিন বন্ধ করলেও unsubscribe না করলে ViewModel থেকে যায়।

ঠিক করার উদাহরণ।

public sealed class OrderViewModel : IDisposable
{
    private readonly OrderService _service;

    public OrderViewModel(OrderService service)
    {
        _service = service;
        _service.OrderChanged += OnOrderChanged;
    }

    public void Dispose()
    {
        _service.OrderChanged -= OnOrderChanged;
    }

    private void OnOrderChanged(object? sender, OrderChangedEventArgs e)
    {
        // update view model
    }
}

gcroot-এ delegate বা event handler দিয়ে রেফারেন্স হিসেবে দেখা যেতে পারে।

এই প্যাটার্ন WPF, WinForms, দীর্ঘজীবী সার্ভিস, মেসেজ ব্রোকার, ইভেন্ট অ্যাগ্রিগেটরে ঘন ঘন আসে।

11.4 Timer Dispose না করা

System.Threading.Timer, PeriodicTimer, Reactive Extensions-এর subscription-ও Dispose না করলে থেকে যায়।

public sealed class PollingWorker
{
    private readonly Timer _timer;

    public PollingWorker()
    {
        _timer = new Timer(_ => Poll(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
    }

    private void Poll()
    {
        // polling
    }
}

এই PollingWorker অস্থায়ী অবজেক্ট ভাবা হলে Timer Dispose করার ডিজাইন লাগে।

public sealed class PollingWorker : IDisposable
{
    private readonly Timer _timer;

    public PollingWorker()
    {
        _timer = new Timer(_ => Poll(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
    }

    public void Dispose()
    {
        _timer.Dispose();
    }

    private void Poll()
    {
        // polling
    }
}

Timer কলব্যাকের delegate ধরে, সেখান থেকে লক্ষ্য অবজেক্টে রেফারেন্স শৃঙ্খল যেতে পারে।

11.5 IDisposable ছেড়ে না দেওয়া

IDisposable ছেড়ে না দেওয়া সবসময় managed heap লিক হিসেবে দেখা যায় না।

ফাইল, সকেট, DB কানেকশন, native হ্যান্ডেল, বাফার ইত্যাদি রিসোর্স সমস্যা হিসেবে বেরোয়।

public async Task<string> ReadAsync(string path)
{
    var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}

এই উদাহরণে StreamReader stream বন্ধ করে, তাই বড় সমস্যা না হওয়াই সাধারণ; কিন্তু মালিকানা অস্পষ্ট কোডে লিক হয়।

মূল নিয়ম using / await using দিয়ে মালিকানা স্পষ্ট করা।

public async Task<string> ReadAsync(string path)
{
    await using var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}

Dispose না করা এমন লক্ষণে বেরোয়।

  • হ্যান্ডেল সংখ্যা বাড়ে
  • সকেট জমে
  • ফাইল বন্ধ হয় না
  • native মেমরি বাড়ে
  • Finalization Queue বাড়ে
  • GC Heap স্থির, অথচ প্রক্রিয়ার মেমরি বাড়ে

এ ক্ষেত্রে শুধু dumpheap যথেষ্ট নয়। OS-এর হ্যান্ডেল ও সকেট, বাহ্যিক লাইব্রেরির অবস্থাও দেখুন।

11.6 AsyncLocal বা কনটেক্সট ধরে রাখা

AsyncLocal<T> সুবিধাজনক, কিন্তু রাখা জিনিস বড় হলে দীর্ঘক্ষণ থেকে যেতে পারে।

লগের correlation ID-এর মতো ছোট মানে সমস্যা কম। কিন্তু ইউজার তথ্য, রিকোয়েস্ট বডি, বড় DTO, DB কনটেক্সট রাখলে অনিচ্ছাকৃত ধরে রাখায় যায়।

public static class RequestContext
{
    public static readonly AsyncLocal<RequestInfo?> Current = new();
}

AsyncLocal অ্যাসিঙ্ক ফ্লোতে চড়ে, তাই সরল static ফিল্ডের চেয়ে খুঁজে পাওয়া কঠিন হতে পারে।

রাখা জিনিস ছোট ও স্পষ্ট রাখুন, আর দরকার না থাকলে null-এ ফেরানো ডিজাইন ভাবা যায়।

11.7 DI lifetime ভুল

ASP.NET Core ইত্যাদি DI-তে singleton, scoped, transient-এর আয়ু আলাদা।

দীর্ঘজীবী singleton রিকোয়েস্টপ্রতি ডেটা ধরে রাখলে রিকোয়েস্ট শেষ হলেও অবজেক্ট থেকে যেতে পারে।

public sealed class AuditBuffer
{
    private readonly List<RequestAudit> _items = new();

    public void Add(RequestAudit item)
    {
        _items.Add(item);
    }
}

এটা singleton হলে _items অ্যাপের আয়ুর সমান।

ডিজাইন হিসেবে বাফার করলে সীমা, পাঠানো, মুছা, ব্যাকপ্রেশার লাগে। শুধু «পরে দেখা যেতে পারে» হলে লগ বা বাহ্যিক স্টোরেজে পাঠানো উচিত।

12. LOH বিশেষ করে ভুল পড়া সহজ

LOH মানে Large Object Heap। .NET-এ বড় অবজেক্ট সাধারণ ছোট অবজেক্ট থেকে আলাদা heap-এ রাখা হয়। ধরন হলো বড় অ্যারে।

var buffer = new byte[1024 * 1024 * 10]; // 10MB

LOH-এ সাধারণ সমস্যা তিনটি।

  1. বড় অবজেক্ট ঘন ঘন তৈরি করা
  2. বড় অবজেক্ট দীর্ঘক্ষণ ধরে রাখা
  3. বড় অবজেক্ট তৈরি ও ফেলে fragmentation

LOH বাড়ছে বলেই সঙ্গে সঙ্গে লিক নয়। বড় বাফার পুনর্ব্যবহারের ডিজাইন হলে এক সাইজ পর্যন্ত বেড়ে স্থির হতে পারে, আর GC collect করলেও Working Set সঙ্গে সঙ্গে নামে না।

তবু নিচের অবস্থা সন্দেহ করা উচিত।

  • System.Byte[] কাজপ্রতি বাড়ে
  • System.Char[] বা বিশাল String বাড়ে
  • ইমেজ, PDF, Excel, ZIP, ক্রিপ্টো, কম্প্রেশনের পরে ফেরে না
  • ArrayPool<T>.Rent করা অ্যারে ফেরত দিচ্ছেন না
  • বড় রেসপন্স পুরোটা মেমরিতে তুলছেন
  • MemoryStream.ToArray() বেশি ব্যবহার করছেন

ArrayPool<T> ব্যবহার করলে অবশ্যই ফেরত দিন।

var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(1024 * 1024);

try
{
    // use buffer
}
finally
{
    pool.Return(buffer);
}

তবে পুলে ফেরালেই প্রক্রিয়ার মেমরি সঙ্গে সঙ্গে নামে না। পুল পুনর্ব্যবহারের জন্য মেমরি ধরে রাখতে পারে।

এখানেও দেখতে হবে «বাড়তেই থাকে কি না», «সীমা আছে কি না», «পুনর্ব্যবহার হচ্ছে কি না»।

13. gcroot কীভাবে পড়বেন

gcroot দেখায় কোন অবজেক্ট কোথা থেকে রেফারেন্স হচ্ছে।

সাধারণ রুট সারণিতে।

রুট অর্থ
static field টাইপের static ফিল্ড থেকে রেফারেন্স
local variable / stack চলমান থ্রেডের স্ট্যাক থেকে রেফারেন্স
GC handle GCHandle, pin, delegate, interop ইত্যাদি থেকে রেফারেন্স
finalization queue finalize-এর অপেক্ষায় ধরে রাখা
thread / async state machine চলমান বা অপেক্ষমাণ অ্যাসিঙ্ক কাজ ধরে রেখেছে

তদন্তে যে কথা ঘন ঘন দেখতে হয় তা আয়ুর ফারাক।

দীর্ঘজীবী অবজেক্ট
  -> স্বল্পজীবী হওয়া উচিত অবজেক্ট

এই আকার দেখা গেলে লিকের প্রার্থী।

যেমন এটা সন্দেহজনক।

SingletonService
  -> List<RequestContext>
  -> RequestContext
  -> LargeDto

SingletonService পুরো অ্যাপে বাঁচে। তার ভিতরে রিকোয়েস্ট-প্রতি RequestContext জমলে ডিজাইন পুনর্বিবেচনা লাগে।

অন্যদিকে এমন রুট সময়ভেদে সম্পূর্ণ স্বাভাবিক হতে পারে।

Thread stack
  -> Controller action local variable
  -> RequestDto

রিকোয়েস্ট চলাকালীন লোকাল ভেরিয়েবল থাকাই স্বাভাবিক।

তাই dump-এর সময় গুরুত্বপূর্ণ।

শুধু লোড চলাকালীন নয়, লোড থামার পরে, কিউ খালি হওয়ার পরে, কিছুক্ষণ idle রাখার পরেও dump নিলে বিচার সহজ হয়।

14. «Forced GC-এ নামল বলে সমাধান» নয়

তদন্তে GC.Collect() ডাকলেন, মেমরি নামল। তখন «তাহলে নিয়মিত GC.Collect() চালালেই হয়» ভাবা বিপজ্জনক।

Forced GC মূল কারণ মুছে না। শুধু এখনো collect না হওয়া অবজেক্ট সেই মুহূর্তে সংগ্রহ করে।

বরাদ্দের হার বেশি সমস্যা হলে forced GC pause time বাড়িয়ে পারফরম্যান্স খারাপ করে। সত্যি লিক হলে রেফারেন্স থাকা অবজেক্ট forced GC-তেও collect হয় না।

তদন্তে যে পার্থক্য দেখতে হবে।

Forced GC-এর পরে বিচার
তীব্র নামে, তারপর baseline স্থির GC-এর অপেক্ষা, অথবা অস্থায়ী বরাদ্দই মূল কারণ
একটু নামে, কিন্তু বারবার করলেই floor উঠে কিছু বেঁচে থাকছে। লিকের প্রার্থী
প্রায় নামে না রেফারেন্স হয়েই আছে, অথবা মূল কারণ GC heap-এর বাইরে
GC Heap নামে কিন্তু Working Set নামে না OS / GC সেগমেন্ট / native দিকের ধরে রাখা সম্ভব

প্রোডাকশনে GC.Collect() নিয়মিত চালানোর আগে অবশ্যই «কী বাড়ছে» চিহ্নিত করুন।

15. কাজে ব্যবহারের তদন্ত ধাপ

এখান থেকে আসল তদন্তের ধাপ হিসেবে সাজাই।

15.1 পুনরুৎপাদন সিনারিও স্থির করুন

আগে তদন্তের শর্ত স্থির করুন।

লক্ষ্য:       /api/report/export
কাজ:         একই শর্তে ১০০ বার চালানো
মাপের ব্যবধান: ৫ সেকেন্ড
পর্যবেক্ষণ:   warm-up ৫ মিনিট + লোড ১০ মিনিট + idle ৫ মিনিট
পরিবেশ:      staging / Release build / প্রোডাকশন-সমতুল্য সেটিং

মেমরি তদন্তে প্রতিবার আলাদা কাজ করতে করতে দেখলে বিচার হয় না। «কী করলে বেড়েছে» স্থির করুন।

15.2 baseline নিন

স্টার্টআপের ঠিক পরে নয়, warm-up-এর পরকে baseline করুন।

কারণ স্টার্টআপের ঠিক পরে একবারের বৃদ্ধি হয়।

  • JIT
  • DI কন্টেইনার তৈরি
  • কনফিগ লোড
  • প্রথম DB কানেকশন
  • প্রথম TLS / HTTP কানেকশন
  • JSON সিরিয়ালাইজার মেটাডেটা তৈরি
  • Razor / টেমপ্লেট ইনিশিয়ালাইজেশন
  • লগার ও মেট্রিক্স ইনিশিয়ালাইজেশন

ক্রম এভাবে।

1. অ্যাপ চালু
2. হেলথ চেক ও প্রতিনিধি API কয়েকবার মারুন
3. ১–৫ মিনিট অপেক্ষা
4. baseline হিসেবে counters ও dump নিন

15.3 লোড চলাকালীন counters নিন

dotnet-counters collect \
  --process-id <PID> \
  --refresh-interval 5 \
  --format csv \
  --output report-export-counters.csv \
  --counters System.Runtime

সঙ্গে সঙ্গে পুনরুৎপাদনের কাজ চালান। দেখতে চান গ্রাফের আকার।

স্বাভাবিকের কাছাকাছি আকার:
  লোডে বাড়ে
  GC-তে ওঠানামা করে
  লোড থামার পরে ফেরে
  baseline বাড়তেই থাকে না

সন্দেহজনক আকার:
  কাজের সংখ্যার অনুপাতে বাড়ে
  Gen 2 / LOH-এর floor উঠে
  লোড থামিয়েও ফেরে না
  পরের লোডে floor আরও উঠে

15.4 dump দুইবার নিন

লোডের আগে-পরে নিন।

dotnet-dump collect --process-id <PID> --type Heap --output before.dmp
# লোড দিন
dotnet-dump collect --process-id <PID> --type Heap --output after.dmp

সময় থাকলে লোড থামার পরেও নিন।

# লোড থামার পর, কিউ খালি হয়ে, কিছুক্ষণ অপেক্ষা করে
dotnet-dump collect --process-id <PID> --type Heap --output idle-after.dmp

তুলনায় beforeafter-এর পাশাপাশি idle-after গুরুত্বপূর্ণ।

লোডে বেড়ে থাকলেও idle-এর পরে ফিরলে লিক নাও হতে পারে।

15.5 বেড়ে যাওয়া টাইপ দেখুন

dotnet-dump analyze after.dmp
> dumpheap -stat

before দিকেও একইভাবে দেখুন। হাতে করলেও চলে, আগে উপরের টাইপ তুলনা করুন।

দেখার দৃষ্টি।

  • নিজের নেমস্পেসের টাইপ বাড়ছে কি না
  • System.String-এর পেছনে নিজের টাইপ আছে কি না
  • System.Byte[] কে ধরে আছে
  • List<T> বা Dictionary<TKey,TValue> বাড়ছে কি না
  • Task বা async state machine বাড়ছে কি না
  • Timer বা CancellationTokenSource বাড়ছে কি না

15.6 রেফারেন্সের উৎস দেখুন

প্রার্থী অবজেক্টের ঠিকানা তুলে gcroot করুন।

> dumpheap -type MyApp.Models.ReportResult
> gcroot <OBJECT_ADDRESS>

gcroot-এর ফল থেকে যে প্যারেন্ট ধরে রেখেছে খুঁজুন।

MyApp.Services.ReportCache
  -> Dictionary<string, ReportResult>
  -> ReportResult

এতদূর এলে কোড রিভিউয়ের লক্ষ্য দেখা যায়।

  • ReportCache singleton কি না
  • সীমা আছে কি না
  • মুছা হয় কি না
  • কী বাড়তেই থাকে কি না
  • ReportResult অতিরিক্ত বড় কি না
  • ক্যাশ নয়, DB বা ফাইলে সরানো উচিত কি না

16. dotnet-trace যেখানে কাজে লাগে

dotnet-dump এক মুহূর্তের স্ন্যাপশট; «ফলে কী থেকে গেছে» দেখতে উপযোগী। অন্যদিকে «কখন, কোথায় প্রচুর বরাদ্দ হচ্ছে» দেখতে চাইলে dotnet-trace ব্যবহার করুন।

যেমন GC-সম্পর্কিত ইভেন্টসহ ট্রেস।

dotnet-trace collect \
  --process-id <PID> \
  --duration 00:00:01:00 \
  --clrevents gc+gchandle \
  --clreventlevel informational \
  --output gc-trace.nettrace

বরাদ্দ স্যাম্পলিং পর্যন্ত দেখতে চাইলে ইভেন্টের পরিমাণ বাড়ে, তাই যাচাই পরিবেশে ছোট সময় থেকে শুরু করুন।

dotnet-trace collect \
  --process-id <PID> \
  --duration 00:00:00:30 \
  --clrevents gc+gcsampledobjectallocationhigh \
  --clreventlevel informational \
  --output allocation-trace.nettrace

ট্রেস dump থেকে আলাদা কোণে কাজে লাগে।

যা দেখতে চান উপযোগী সরঞ্জাম
কী থেকে গেছে dump / gcdump
কে রেফারেন্স করছে dump + gcroot
কখন প্রচুর বরাদ্দ হয়েছে trace
GC কখন হয়েছে counters / trace
pause time সমস্যা কি না counters / trace

লিক তদন্তে আগে dump দিয়ে «যা থেকে গেছে» দেখে, দরকার হলে trace দিয়ে «যেখানে তৈরি হচ্ছে» দেখাই কার্যকর।

17. কোডে যাচাইয়ের মেট্রিক্স বের করা

গুরুতর ডায়াগনসিস বাইরের টুলেই হওয়া উচিত, তবু অ্যাপের দিকে সরল ডায়াগনস্টিক লগ রাখলে কাজে লাগে।

যেমন অ্যাডমিন এন্ডপয়েন্ট বা নিয়মিত লগে GC তথ্য বের করার উপায়।

public static class GcDiagnostics
{
    public static object Snapshot()
    {
        var info = GC.GetGCMemoryInfo();

        return new
        {
            TotalMemory = GC.GetTotalMemory(forceFullCollection: false),
            HeapSizeBytes = info.HeapSizeBytes,
            FragmentedBytes = info.FragmentedBytes,
            MemoryLoadBytes = info.MemoryLoadBytes,
            HighMemoryLoadThresholdBytes = info.HighMemoryLoadThresholdBytes,
            Gen0Collections = GC.CollectionCount(0),
            Gen1Collections = GC.CollectionCount(1),
            Gen2Collections = GC.CollectionCount(2)
        };
    }
}

এই তথ্য একা লিক বিচার করতে পারে না। তবু ঘটনার সময় নিচের বিচার সহজ হয়।

  • Gen 2 হঠাৎ বাড়ছে কি না
  • HeapSize বাড়ছে কি না
  • FragmentedBytes বাড়ছে কি না
  • TotalMemory ও প্রক্রিয়ার মেমরির ফারাক বড় কি না
  • ডিপ্লয়ের পর প্রবণতা বদলেছে কি না

অ্যাপ লগে রাখলে পরিমাণ দেখুন। উচ্চ হারে ভারী ডায়াগনসিস নিজেই লোড হয়ে যায়।

18. «মেমরি লিক» বলে রায় দেওয়ার মাপকাঠি

তদন্তের শেষে নিচের আকারে ব্যাখ্যা করতে পারা উচিত। রিপোর্টে নামানোর ছাঁচ হিসেবে যে ঘর ভরতে হবে আগে সাজাই।

ঘর কী লিখবেন
ঘটনা কে অসুবিধায়। কোন কাজে কত বাড়ে
পর্যবেক্ষণের শর্ত পরিবেশ, বিল্ড কনফিগ, ডেটার পরিমাণ, চালানোর সংখ্যা, warm-up সময়, পর্যবেক্ষণের ব্যবধান, ব্যবহৃত টুল ও সংস্করণ
পর্যবেক্ষণ counters-এর মান কীভাবে চলেছে। Working Set ও GC Heap আলাদা করে লিখুন
তুলনা before ও after dump-এ কোন টাইপ কতটা বেড়েছে
রেফারেন্সের উৎস gcroot-এ দেখা ধরে রাখার পথ
কারণ কোডের কোন ডিজাইন সেই ধরে রাখা জন্মাচ্ছে
ব্যবস্থা কী বদলাবেন। প্রভাব কীভাবে মাপবেন
বাকি কাজ দরকার হলে। এই পর্যবেক্ষণে ব্যাখ্যা হয়নি কী, তারপর কী দেখবেন

পর্যবেক্ষণের শর্ত বাদ না দেওয়া বিশেষ গুরুত্বপূর্ণ। শর্ত না থাকলে ঠিক করার পরের মাপের সাথে তুলনা যায় না, ২০ অধ্যায়ের যাচাই অর্থ হারায়।

এই ছাঁচ ভরলে যেমন দাঁড়ায়।

ঘটনা:
  /api/report/export ১০০ বার চালালে লোড থামার পরেও GC Heap 300MB বেড়ে থেকে যায়, ফেরে না।

পর্যবেক্ষণের শর্ত:
  staging পরিবেশ / Release build / প্রোডাকশন-সমতুল্য সেটিং ও ডেটার পরিমাণ।
  warm-up ৫ মিনিটের পর একই শর্তে ১০০ বার চালিয়ে তারপর ৫ মিনিট idle।
  dotnet-counters ৫ সেকেন্ড ব্যবধানে সংগ্রহ।

পর্যবেক্ষণ:
  dotnet-counters-এ Gen 2 heap size কাজের সংখ্যার অনুপাতে বেড়েছে।
  Working Set ছাড়াও GC Heap বাড়ছিল।

তুলনা:
  before.dmp ও after.dmp তুলনায় MyApp.Models.ReportResult 12,000টা বেড়েছে।

রেফারেন্সের উৎস:
  gcroot-এ MyApp.Services.ReportCache._items থেকে রেফারেন্স।

কারণ:
  ReportCache singleton, ইউজার ID + বর্তমান সময় কী, মুছা·মেয়াদ·সীমা ছিল না।

ব্যবস্থা:
  MemoryCache-এ বদলে সাইজ সীমা ও মেয়াদ সেট করা।
  ক্যাশ সংখ্যা মেট্রিক্স করা।

এতদূর ব্যাখ্যা করতে পারলে নিছক «মেমরি বাড়ছে» নয়; পুনরুৎপাদনের শর্ত, পর্যবেক্ষণ, বেড়ে যাওয়া টাইপ, রেফারেন্সের উৎস, কারণ, ব্যবস্থা জোড়া রিপোর্ট হয়।

19. তদন্তের সতর্কতা

19.1 Release বিল্ডে দেখুন

Debug বিল্ড অপটিমাইজেশন, লোকাল ভেরিয়েবলের আয়ু, ডিবাগ তথ্যের প্রভাবে বাস্তব পরিচালনা থেকে আলাদা দেখাতে পারে।

প্রোডাকশন-সমতুল্য তদন্তে Release বিল্ড, পরিচালনার কাছের সেটিং, কাছের ডেটার পরিমাণে যাচাই করুন।

19.2 শুধু স্টার্টআপের ঠিক পরে বিচার করবেন না

স্টার্টআপের ঠিক পরে নানা ইনিশিয়ালাইজেশনে মেমরি বাড়ে।

warm-up-এর পর baseline নিয়ে সেখান থেকে বাড়ে কি না দেখুন।

19.3 এক dump-এ অপরাধী সাজাবেন না

Heap-এ উপরের টাইপই অপরাধী নয়।

System.StringSystem.Byte[] অনেক অ্যাপে বড় দেখায়।

গুরুত্বপূর্ণ কথা, সময়ের ব্যবধানে বেড়েছে কি না, আর কে ধরে আছে।

19.4 Dump-এ গোপন তথ্য থাকে

মেমরি dump-এ রিকোয়েস্ট, ক্রেডেনশিয়াল, কানেকশন স্ট্রিং, ব্যক্তিগত তথ্য, ব্যবসায়িক ডেটা থাকতে পারে।

রাখার জায়গা, বাইরে নেওয়া, শেয়ার, মুছার নিয়ম ঠিক করুন।

19.5 কন্টেইনারে dump নেওয়া নিজেই ঝুঁকি

কন্টেইনারের মেমরি সীমা কঠোর হলে dump নেওয়ার অতিরিক্ত মেমরি ও page-in-এ OOM Kill হতে পারে।

প্রোডাকশন কন্টেইনারে নেওয়ার আগে স্টেজিংয়ে চেষ্টা করে সীমা, ডিস্ক খালি জায়গা, অনুমতি, PID নেমস্পেস দেখুন।

19.6 GC Heap-এর বাইরেও লিক আছে

.NET তদন্ত বলেই সব GC heap-এ বেরোয় না।

নিচের সমস্যায় GC Heap স্থির থাকলেও প্রক্রিয়ার মেমরি বাড়তে পারে।

  • native লাইব্রেরি
  • P/Invoke
  • COM
  • ইমেজ প্রসেসিং
  • কম্প্রেশন লাইব্রেরি
  • ক্রিপ্টো
  • DB ড্রাইভার
  • সকেট
  • Marshal.AllocHGlobal
  • NativeMemory.Alloc
  • অতিরিক্ত থ্রেড

এ ক্ষেত্রে dotnet-dump-এর dumpheap যথেষ্ট নয়। OS-এর ডায়াগনসিস, বাহ্যিক লাইব্রেরির মেট্রিক্স, হ্যান্ডেল, থ্রেড, native মেমরি দেখতে হয়।

19.7 প্রোডাকশনে dump নেওয়ার আগে সংশ্লিষ্টদের সাথে যা ঠিক করবেন

প্রোডাকশনে dump নেওয়া প্রযুক্তিগত কাজের আগে সমন্বয়ের কাজ। এ ধাপ এড়িয়ে শুধু «তদন্ত বলে নিতে দিন» বললে সাধারণত থেমে যায়।

আগে প্রভাব হিসেবে যা ব্যাখ্যা করতে হবে সাজান।

  • Dump নেওয়া লক্ষ্য প্রক্রিয়ার জন্য ভারী কাজ; নেওয়ার সময় রেসপন্স থেমে গেছে মনে হতে পারে। কতক্ষণ থামবে heap সাইজ, ডিস্ক গতি, পরিবেশে বদলায়, তাই স্টেজিংয়ে একই ধাপ একবার চালিয়ে মাপ লিখে রাখুন।
  • রেসপন্স থামার সময় লম্বা হলে হেলথ চেকের টাইমআউট, লোড ব্যালান্সার থেকে আলাদা হওয়া, ক্লাস্টার failover ট্রিগার করতে পারে।
  • Full বা Heap dump প্রক্রিয়ার মেমরি ব্যবহার অনুযায়ী বড় হয়। লেখার জায়গার খালি ডিস্ক আগে দেখুন।
  • কন্টেইনারে dump নেওয়ার page-in মেমরি সীমা ছাড়িয়ে কন্টেইনার জোর করে বন্ধ হতে পারে (১৯.৫)।
  • Dump-এ ব্যক্তিগত তথ্য বা ক্রেডেনশিয়াল থাকতে পারে (১৯.৪)।

তারপর নেওয়ার আগে নিচেরগুলো ঠিক করুন।

যা ঠিক করবেন উদাহরণ
কে অনুমোদন দেবে সার্ভিস দায়িত্বশীল ও তথ্য-ব্যবস্থা বিভাগ। দুই পক্ষের সম্মতি আগে নিন
কখন নেবেন অফিস সময়ের বাইরে ইত্যাদি, সাময়িক রেসপন্স দেরি সহনীয় সময়
কোথায় লিখবেন যথেষ্ট খালি থাকা লোকাল ডিস্ক। শেয়ার ফোল্ডারে সরাসরি লিখবেন না
কে অ্যাক্সেস পাবে রাখার জায়গার অ্যাক্সেস তদন্তের দায়িত্বে সীমিত করুন
কখন মুছবেন তদন্ত শেষে মুছার সময়সীমা আগে ঠিক করে লিখুন
ব্যর্থ হলে কী হবে রেসপন্স না ফিরলে রিস্টার্ট ধাপ ও যে ব্যক্তি সিদ্ধান্ত নেবেন
আগে যাচাই স্টেজিংয়ে একই ধাপ একবার চালিয়ে সময় ও ফাইল সাইজ লিখে রাখুন

অনুরোধ মুখে নয়, এই বিষয় এক পাতায় করে দেওয়াই নির্ভরযোগ্য। ছাঁচের উদাহরণ।

উদ্দেশ্য:     /api/report/export-এর পরে মেমরি না ফেরার কারণ চিহ্নিত করা
পদ্ধতি:      dotnet-dump collect --type Heap লোডের আগে-পরে দুইবার নেওয়া
প্রভাব:      নেওয়ার সময় লক্ষ্য প্রক্রিয়ার রেসপন্স দেরি হয়।
             স্টেজিংয়ের মাপ আলাদা পাতায় সংযুক্ত
সময়:        অফিস সময়ের বাইরে। দায়িত্বশীল ও তথ্য-ব্যবস্থা একসঙ্গে থাকতে পারে এমন সময়
লেখার জায়গা: লক্ষ্য সার্ভারের লোকাল ডিস্ক। আগে খালি জায়গা দেখা
যা থাকতে পারে: মেমরির রিকোয়েস্ট ডেটা, কানেকশন স্ট্রিং ইত্যাদি থাকতে পারে
ব্যবহার:     রাখার জায়গার অ্যাক্সেস শুধু তদন্তের দায়িত্ব। বাইরে নেওয়া হবে না
মুছা:        তদন্ত শেষে, দেরি করেও ১ মাসের মধ্যে মুছে মুছার রেকর্ড রাখা
Rollback:    নেওয়ার পর রেসপন্স না ফিরলে প্রক্রিয়া রিস্টার্ট

«কী উদ্দেশ্যে», «কতক্ষণ থামবে», «কোথায় রাখবে, কখন মুছবে» থাকলে সিদ্ধান্তের পক্ষ সহজে হ্যাঁ/না দিতে পারে। উল্টো এখানে অস্পষ্ট থাকলে তদন্ত নিজেই থেমে যায়।

20. ঠিক করার পরের যাচাই

লিক মনে হওয়া জায়গা ঠিক করলে একই ধাপে আবার মাপুন।

ঠিক করার আগে:
  ১০০ বার চালানোর পর Gen 2 +300MB
  ReportResult +12,000টা

ঠিক করার পরে:
  ১০০ বার চালানোর পর Gen 2 +20MB-এর মধ্যে স্থির
  ReportResult লোড থামার পরে baseline-এ ফেরে
  ক্যাশ সংখ্যা সীমা 1,000-এ স্থির

ঠিক করার যাচাইয়ে অবশ্যই একই শর্তে তুলনা করুন।

  • একই ডেটার পরিমাণ
  • একই সংখ্যা
  • একই লোড সময়
  • একই warm-up
  • একই পর্যবেক্ষণ ব্যবধান
  • একই টুল

মেমরি তদন্তে ঠিক করার আগে-পরের তুলনা দুর্বল হলে বিশ্বাসযোগ্যতা আসে না।

21. সারাংশ

.NET-এ মেমরি বাড়লে সঙ্গে সঙ্গে লিক বলে ধরে না নিয়ে এই ক্রমে আলাদা করুন।

  1. শুধু Working Set / RSS দিয়ে বিচার করবেন না
  2. dotnet-counters দিয়ে GC Heap, Gen 2, LOH, GC সংখ্যা দেখুন
  3. লোড চলাকালীন, লোড থামার পরে, সময়ের ব্যবধানে তুলনা করুন
  4. dotnet-gcdump বা dotnet-dump দিয়ে বেড়ে যাওয়া টাইপ দেখুন
  5. gcroot দিয়ে রেফারেন্সের উৎস দেখুন
  6. static, ক্যাশ, ইভেন্ট, Timer, DI lifetime, অ্যাসিঙ্ক কনটেক্সট যাচাই করুন
  7. GC Heap স্থির হলে native মেমরি বা OS-এর সমস্যাও সন্দেহ করুন

«এখনো GC হয়নি» আর «মেমরি লিক হচ্ছে»-এর পার্থক্য শেষে রেফারেন্সেই ঠিক হয়।

অপ্রয়োজনীয় অবজেক্টে রেফারেন্স না থাকলে GC-এর সময়ে collect হয়। অপ্রয়োজনীয় হওয়া উচিত হয়েও রেফারেন্স হয়েই থাকলে GC collect করতে পারে না।

অর্থাৎ তদন্তের লক্ষ্য এটা।

কী বাড়ছে।
কোন GC-এর পরেও থেকে যাচ্ছে।
কে রেফারেন্স করছে।
সেই রেফারেন্স ডিজাইনে দরকার কি না।

এতদূর জানা গেলে মেমরির গ্রাফে না ঘুরে কোডের ঠিক করার জায়গায় নামানো যায়।

তথ্যসূত্র

কাছাকাছি বিষয়ে গভীরে যেতে একই ট্যাগযুক্ত সাম্প্রতিক নিবন্ধ।

কর্পোরেট প্রক্সি ও Windows অ্যাপ — WinINET, WinHTTP ও .NET-এ প্রক্সি রেজোলিউশন গুছিয়ে নেওয়া

ব্রাউজার পার হয়, কিন্তু শুধু ব্যবসায়িক অ্যাপ কর্পোরেট প্রক্সি পেরোতে পারে না। কারণ সাধারণত WinINET, WinHTTP, এনভায়রনমেন্ট ভেরিয়েবল ও ...

ব্যবহারিক মাল্টিথ্রেডিং সেরা অনুশীলন: .NET সংস্করণ — আরও থ্রেড যোগ করার আগে কী ঠিক করবেন

.NET/C#-এ মাল্টিথ্রেডেড কোড যেন মাঝে মাঝে ক্র্যাশ বা হ্যাং না করে, তার ডিজাইন নিয়ম: নিজে থ্রেড না বানিয়ে Task-এ চড়া, ভাগ করা পরিবর্তনয...

C# ও PowerShell থেকে WMI/CIM ব্যবহার — হার্ডওয়্যার তথ্য, প্রক্রিয়া নজরদারি ও দূরবর্তী অনুসন্ধানের বাস্তব নির্দেশিকা

PC-র সিরিয়াল নম্বর, ডিস্কের খালি জায়গা ও প্রক্রিয়া চালু হওয়া শনাক্তের নিয়মিত উপায় WMI/CIM। Get-CimInstance-এর মতো CIM কমান্ডলেট, পু...

Windows I/O-এর গভীরতা (৫ম কিস্তি) — NTFS-এর অভ্যন্তরীণ কাঠামো: MFT থেকে বোঝা ফাইল সিস্টেম

NTFS-এর অভ্যন্তরীণ কাঠামো চিত্রে ব্যাখ্যা করা ধারাবাহিকের ৫ম কিস্তি। MFT ও ফাইল রেকর্ড, একাধিক ডেটা স্ট্রিম (Zone.Identifier), হার্ড লিঙ্...

Windows I/O-এর গভীরতা (৪র্থ কিস্তি) — ক্যাশ ম্যানেজার: আপনার WriteFile কখন ডিস্কে পৌঁছায়

Windows-এর ক্যাশ ম্যানেজার চিত্রে ব্যাখ্যা করা ধারাবাহিকের ৪র্থ কিস্তি। ফাইল ম্যাপিং হিসেবে বাস্তবায়িত ক্যাশ, আগে পড়া ও বিলম্বিত লেখা, ...

এই পৃষ্ঠাগুলো বিষয়টিকে সেবা ও সিদ্ধান্তের বৃহত্তর প্রেক্ষাপটে স্থাপন করে।

নিবন্ধটি নিচের সেবাগুলোর সাথে সরাসরি সম্পর্কিত।

প্রায়শ জিজ্ঞাসিত প্রশ্ন

এই নিবন্ধের বিষয়ে পরামর্শে প্রায়ই আসা প্রশ্ন।

.NET অ্যাপের মেমরি ব্যবহার বাড়ছে বলে কি সেটা মেমরি লিক?
প্রক্রিয়ার মেমরি বাড়া আর মেমরি লিক এক জিনিস নয়। .NET-এ GC বরাদ্দের অবস্থা ও heap-এর থ্রেশহোল্ড দেখে চলে, তাই অপ্রয়োজনীয় অবজেক্ট এখনো collect না হওয়া, অথবা GC-এর পরেও OS সঙ্গে সঙ্গে মেমরি না ফেরত নেওয়া, দুইই ঘটে। দেখতে হবে তিনটি বিষয়: «GC-এর পরেও বেঁচে থাকা মেমরি বাড়ছে কি না», «কোন টাইপ বাড়ছে», «সেই অবজেক্টকে কে রেফারেন্স করছে»।
.NET মেমরি লিক খুঁজতে কোন টুল ব্যবহার করব?
আগে dotnet-counters দিয়ে Working Set, GC Heap, Gen 2/LOH ও GC সংখ্যার প্রবণতা দেখুন। তারপর dotnet-gcdump দিয়ে লোডের আগে-পরে GC dump নিয়ে টাইপপ্রতি Count ও Size তুলনা করে কোন টাইপ বেড়েছে চিহ্নিত করুন। শেষে dotnet-dump দিয়ে heap dump নিয়ে dumpheap -stat ও gcroot দিয়ে «কেন collect হচ্ছে না» পর্যন্ত রেফারেন্সের উৎস অনুসরণ করুন। একবারের সংখ্যা নয়, একই শর্তে সময়ের ব্যবধানে তুলনাই গুরুত্বপূর্ণ।
.NET-এ সাধারণ মেমরি লিকের প্যাটার্ন কী?
ধরনগুলো হলো static কালেকশনে যোগ করেই রাখা, সীমা ও মেয়াদ ছাড়া unbounded ক্যাশ, দীর্ঘজীবী publisher-এ ইভেন্ট unsubscribe না করা, Timer Dispose না করা, IDisposable ছেড়ে না দেওয়া, আর singleton যেখানে per-request ডেটা ধরে রাখে এমন DI lifetime ভুল। .NET-এর লিককে «মুছে দিতে ভুলে যাওয়া» নয়, অপ্রয়োজনীয় হয়েও রেফারেন্স থেকে যাওয়া «অনিচ্ছাকৃত ধরে রাখা» হিসেবে ভাবা সহজ।
GC.Collect() নিয়মিত চালালে মেমরি সমস্যা সারে?
সারে না। Forced GC শুধু সেই মুহূর্তে এখনো collect না হওয়া অবজেক্ট সংগ্রহ করে, মূল কারণ মুছে না। বরাদ্দের হার বেশি হলে pause time বাড়িয়ে পারফরম্যান্স খারাপ করে, আর সত্যি লিক হলে রেফারেন্স থাকা অবজেক্ট forced GC-তেও collect হয় না। তদন্তের জন্য নিয়ন্ত্রিত যাচাই পরিবেশে «forced GC-এর পরেও থাকে কি না» দেখা যায়, কিন্তু সমাধান হিসেবে প্রোডাকশনে রাখার আগে অবশ্যই কী বাড়ছে তা চিহ্নিত করতে হয়।

লেখকের প্রোফাইল

নিবন্ধের লেখকের পরিচিতি পৃষ্ঠা।

Go Komura

KomuraSoft LLC-এর প্রতিনিধি

Windows সফটওয়্যার ডেভেলপমেন্ট, প্রযুক্তিগত পরামর্শ ও বাগ তদন্তে বিশেষজ্ঞ, বিশেষ করে বিদ্যমান সিস্টেমযুক্ত প্রকল্প ও পুনরুৎপাদন করা কঠিন বাগে।

পাবলিক লিঙ্ক

ব্লগে ফিরে যান