Soci (Soczó Zsolt) szakmai blogja

2009.02.26.

.NET teljesítményhangolási tapasztalatok 7. – Hatékony UNC fájlelérés

Filed under: .NET,Optimalizálás,Szakmai élet — Soczó Zsolt @ 11:13

Hogyan közelítsük meg azt a problémát, hogy egy managed alkalmazásból naponta több milliószor el szeretnénk érni másik szerveren levő, viszonylag ritkán, pár percenként frissülő fájlokat, a lehető leghatékonyabb módon?

Először is érdemes tudni, hogy a workstation service, a windows fájok távoli elérésének kliense cache-el, az OS File Cache-ét használva, ami a helyi fájlokat is cache-eli. Ha azonban módosítják a megosztáson levő fájlt, kikapcsolja a cache-elése erre a fájlra, egészen addig, míg újra meg nem nyitjuk. Bővebben majd egy teljes cikkben is megemlékezek erről, amikor a Windows Cache részleteiről fogok írni (a technet site-on).

Szóval kapuk valamennyi teljesítménynövekedést az OS cache miatt, de mivel a fájl olvasása során a PInvoke-nak át kell mozgatni az adatokat a natív oldalról a managed oldalra, jelentős veszteségeket élünk meg azzal szemben, ha inprocess, managed memóriában tudnánk tartani a fájlok tartalmát, de kiütve őket onnan, ha megváltoznak.

Gondolom a legtöbben már hallottak a System.Web.Caching.CacheDependency típusról. Ezt alapvetően webalkalmazások használják, de 2.0 óta már egyéb appokban is használható. A segítségével bámulatosan egyszerűen lehet cache-elni a felolvasott fájlok tartalmát.

object o = HttpRuntime.Cache[cacheKey];
if (o != null)
{
string s = (string)o;
}
else
{
CacheDependency d = new CacheDependency(file);
string s = ProtectedRead(file);
HttpRuntime.Cache.Add(cacheKey, s, d,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
Console.WriteLine(“Content (re)cached”);
}

Az alábbi kód egy teljes, futtatható teszt, ami a demonstrálja a direkt fájlolvasás (implicit OS cache), és a kézi cache-elés közötti teljesítménykülönbséget:

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Web;
using System.Web.Caching;

class Program
{
const string file = @”c:\temp\a.log”;
const string cacheKey = @”alog”;
const int loopLen = 50000;
private static HttpRuntime httpRuntime;
const int HRSharingViolation = -2147024864;

static void Main(string[] args)
{
httpRuntime = new HttpRuntime();

Thread t = new Thread(FileChanger);
t.IsBackground = true;
t.Start();

int cc = GetSumGCCount();
Stopwatch w = Stopwatch.StartNew();
for (int i = 0; i < loopLen; i++) { TestWithoutCaching(); ShowProgress(i); } w.Stop(); Console.WriteLine("Elapsed time: {0}, collections during cached test: {1}", w.Elapsed, GetSumGCCount() - cc); cc = GetSumGCCount(); w = Stopwatch.StartNew(); for (int i = 0; i < loopLen; i++) { TestWithCaching(); ShowProgress(i); } w.Stop(); Console.WriteLine("Elapsed time: {0}, collections during noncached test: {1}", w.Elapsed, GetSumGCCount() - cc); } private static void ShowProgress(int i) { //Console.WriteLine(i); //if (i % (loopLen / 100) == 0) Console.WriteLine("{0}%", i * 100 / loopLen); } static void FileChanger(object state) { while (true) { string s = ProtectedRead(file); ProtectedWrite(file, s); Console.WriteLine("File has been modified sucessfully."); Thread.Sleep(100); } } private static int GetSumGCCount() { GC.Collect(); GC.WaitForPendingFinalizers(); int cc = 0; for (int gen = 0; gen < GC.MaxGeneration; ++gen) { cc += GC.CollectionCount(gen); } return cc; } private static void TestWithoutCaching() { string s = ProtectedRead(file); for (int i = 0; i < s.Length; i++) { char c = s[i]; } } private static void TestWithCaching() { object o = HttpRuntime.Cache[cacheKey]; if (o != null) { string s = (string)o; for (int i = 0; i < s.Length; i++) { char c = s[i]; } } else { CacheDependency d = new CacheDependency(file); string s = ProtectedRead(file); HttpRuntime.Cache.Add(cacheKey, s, d, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); Console.WriteLine("Content (re)cached"); } } static string ProtectedRead(string file) { string s = null; bool retry = false; do { try { s = File.ReadAllText(file); break; } catch (IOException ex) { int hr = Marshal.GetHRForException(ex); if (hr == HRSharingViolation) { retry = true; Console.WriteLine("File is locked while reading, retrying..."); RandomSleep(); } else { throw; } } } while (retry); return s; } static void ProtectedWrite(string file, string content) { bool retry = false; do { try { File.WriteAllText(file, content); break; } catch (IOException ex) { int hr = Marshal.GetHRForException(ex); if (hr == HRSharingViolation) { retry = true; Console.WriteLine("File is locked while writing, retrying..."); RandomSleep(); } else { throw; } } } while (retry); } private static void RandomSleep() { int r = new Random().Next(30, 200); Thread.Sleep(r); } } [/source] Hogy realisztikus legyen a teszt egy külön szál írogatja is a kérdéses fájt, ami az egyszerűség kedvéért most egy lokális, 256ks állomány. Az író-olvasó folyamatok persze időnként összeakadnak, ezért a hibakezelő és ismétlő logika a kódban. Bár elég sok mesterséges késleltetés van a példában, így is eléggé marakodik a két oldal. Nem csak az időket mérem a kódban, hanem a szemétgyűjtések számát is a két esetben. A direkt fájlolvasás esetén minden egyes olvasáskor újabb és újabb buffert allokálunk, ami persze jelentős memóriakényszert okoz. Habár lehetne mondjuk valami thread-local buffert előre allokálni és újrahasznosítani, a legtöbben úgyis ezt a kevésbé hatékony kódot használják, így realisztikus a teszt. A cache-elős példa esetén egyszerűen csak kapunk egy referenciát a memóriában már benn levő fájltartalomra, így nem történik nagyméretű memóriafoglalás sem. Érdemes tudni, hogy a nagyobb memóriatartalmakat a CLR a Large Object Heap-en tárolja, amely kevésbé hatékony mint a sima heap, szóval érdemes ésszel élni vele.

Lássuk a program kimenetét, azaz az eredményeket:


File is locked while reading, retrying…
File has been modified sucessfully.
File has been modified sucessfully.
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while reading, retrying…
File has been modified sucessfully.
File is locked while reading, retrying…
File has been modified sucessfully.
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while writing, retrying…
File is locked while writing, retrying…
Elapsed time: 00:00:01.2796481, collections during noncached test: 252
Content (re)cached
Elapsed time: 00:00:00.0826961, collections during cached test: 2

Látható, hogy a saját cache nagyon sokat gyorsít, ráadásul a memóriát is sokkal jobban kíméli. Valójában annyira gyors a cache-elős eset, hogy az író szálnak nincs is elég ideje módosítani a fájt, ami amiatt fontos, hogy lássuk, működik-e a cache invalidálás, azaz érzékeli-e a cache, hogy módosult a fájl. Ha az iterációk számát felvesszük a példában 5000-re, akkor már lesznek olyan esetek, amikor a CacheDependency észreveszi a fájváltozást:

File has been modified sucessfully.
Content (re)cached
File has been modified sucessfully.
Content (re)cached
File has been modified sucessfully.
Content (re)cached
File has been modified sucessfully.
File is locked while reading, retrying...
File has been modified sucessfully.
Content (re)cached
Content (re)cached
File has been modified sucessfully.
File is locked while reading, retrying...
File has been modified sucessfully.
Content (re)cached
Content (re)cached
Elapsed time: 00:00:00.7075406, collections during cached test: 5

Remélem tanulságos volt, de ha benéztem valamit, és nem érvényes a teszt, szóljatok, javítom.

1 Comment

  1. Szia Soci,
    Szerintem a peldadban a ket vegeredmeny kozti nagysagrendbeli kulonbseget nem a caching adja, hanem egyreszt az, hogy a fajlolvasast soronkent vegzed, masreszt pedig hogy a noncached verzional ugye egy oprendszer hivassal olvasol, azaz eselyt adsz arra, hogy a rendszer atkapcsoljon egy masik szalra. Ez utobbi miatt az iro es olvaso szal kozott pattog a vezerles, ami tenyleg rengeteg idot emeszt fel. Realisztikusabb lenne az iro reszt egy masik processzbe tenni. Probakepp annyit modositottam a programodon, hogy egyreszt kiiktattam az iro szalat, masreszt binaris olvasasra es feldolgozasra alakitottam az egeszet (File.ReadAllBytes()).
    Az eredmeny a kovetkezo lett egy lassu gepen, es tenylegesen halozati meghajton levo fajllal:
    Elapsed time: 00:00:46.6153295, collections during noncached test: 20000
    Content (re)cached
    Elapsed time: 00:00:22.0872630, collections during cached test: 3
    Ez azert mar nem olyan nagy kulonbseg, es nem kellett a memoriaba toltenem egy egesz fajlt…

    Comment by cb2 — 2009.03.03. @ 23:04

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress