.NET Core performance nyomozás

Sok szépet lehet olvasni, milyen durva optimalizálások csináltak a .NET Core-ban. Annyira élni akartam ezekkel, hogy a tradinghez írt backtesteremet átportoltam Core 3.1-re. A WCF és a WPF részek igényeltek némi googlizást, de nem vészes a migráció.
Van egy nagyon CPU intenzív kód a backtesterben, ez több millió tömb műveletet és dátum összehasonlítást végez. Erre voltam kíváncsi, mennyit gyorsul az új .NET assembly-ket használva.
.NET 4.8:
00:00:00.6295321
00:00:01.2317440
00:00:00.6597345
00:00:01.2434665
Min: 629.5321

.NET Core 3.1:
00:00:01.4422192
00:00:01.3073163
00:00:01.6955676
00:00:01.2051358
Min: 1205.1358

Ez siralmas. Először azt hittem azért, mert debug, nem optimalizált kódban futtattam a core-os részt, de nem, abban 2.5mp a futásidő.

Ha lúd legyen kövér, felraktam a .NET 5 RC2-t, Visual Studio Previewt, és leforgattam .NET 5 alá is ugyanazt a kódot:

00:00:01.2580593
00:00:01.3729454
00:00:01.2055221
00:00:01.5244462
Min: 1205.5221

Véletlen, de msre ugyanaz jött ki, mint .NET Core 3.1 alatt (gondolom kb. ugyanaz a kód van a kettő mögött).

Aztán rájöttem, hogy sok desktop appban is Server GC-t használtam, mert többszálú terhelésnél sokkal jobban ki lehet használni a CPU-kat.

Core-ban másképp kell állítani, de itt is lehet. Hozzáteszem azonban, hogy a jó eredmények Workstation GC-vel jöttek ki .NET 4.8 alatt.

Mindenesetre background server GC esetén ezek a számok:

00:00:00.7827110
00:00:00.7870250
00:00:01.3079196
00:00:00.7937517
Min: 782.711

Ez már sokkal közelebb van a kiinduláshoz. Gondoltad volna, hogy ekkora hatása van a GC-nek?

Ha nem background (hanem blocking) server GC-t használok, akkor:

00:00:00.9668477
00:00:00.7707329
00:00:00.8736540
00:00:00.4659059
Min: 465.9059

Na, ez már igen! 630 helyett 466ms.

De akkor ez úgy igazságos, hogy .NET 4.8 alatt is megnézzük a server GC-kel a mérést.

Concurrent (background) GC:

00:00:01.5112052
00:00:01.7877981
00:00:01.7735863
00:00:01.9077548
Min: 1511.2052

Blocking GC:

00:00:01.4676091
00:00:01.7899143
00:00:01.5936619
00:00:02.0141916
Min: 1467.6091

Workstation GC (ezzel ment az eredeti mérés):

00:00:01.4541448
00:00:01.7498414
00:00:01.9155145
00:00:01.7816521
Min: 1454.1448

Na, ez meg mi? Úgy látszik VS Preview alatt a net48-windows moniker .NET 5 kódot fordított be. net48-ra átírva már jönnek a régi számok.

WS, Blocking GC:

00:00:00.9853411
00:00:00.6656316
00:00:01.1928848
00:00:00.7570509
Min: 665.6316

WS, Background GC:

00:00:00.6371798
00:00:01.0235119
00:00:00.6491366
00:00:01.2766926
Min: 637.1798

Server, Blocking GC:

00:00:00.9851811
00:00:00.6396851
00:00:01.4888114
00:00:00.6327586
Min: 632.7586

Server, Background GC:

00:00:00.7241423
00:00:01.0104857
00:00:00.6526001
00:00:01.2343597
Min: 652.6001

Ez a négy eset kb. ugyanaz, zajhatáron belül vannak a számok.

Egyelőre ennyi, majd írok még a témáról, ha bővebben belementem, de a server GC-s Core verzió mindenképpen tetszik: 630 helyett 466ms.

DDD Bounded Contextek egy hosting processzben?

Tegyük fel van egy nagy alkalmazás, amely többé-kevésbé DDD mentén van elkészítve.
Minden terület saját bounded contextben (BC) van, a BC-ek egymás felé csak az apijaikon keresztül kommunikálnak.

A fő cél az lenne, hogy a csapatok/emberek tisztán egy BC-en tudjanak dolgozni, ne kelljen a többit is mindig lefordítani, a méretek miatt. Ezért mondjuk minden BC-ből csak az API komponensét publikáljuk ki, nugetbe csomagoljuk, és így binárisan tudnak egymással kommunikálni a BC-ek. Ez eddig szerintem rendben van, bár a verziózás kérdése itt sem egyszerű, hiába próbáljuk a csatolást az API-kon és az Anti Corruption Layeren (ACL) keresztül lazítani.

A nagy kérdés számomra a közös komponensek használata. “A” BC használ mondjuk 10.1-es NewtonSoft.Json-t, “B” BC pedig 9.0-t. Amikor minden bounded contextet bemásolunk egy website bin könyvtárába, akkor esetleges lesz, hogy melyik verziójú külső komponens lesz bemásolva, illetve a verziókhoz passzoló assembly redirectek is kellenek a web.config-ba.

Hogy szoktak ebben rendet tenni? Vagy megfordítva a kérdést, jó ötlet egy processben hosztolni a BC-eket, vagy ha ennyire laza csatolást akarunk, akkor külön processzbe kell őket rakni, és elkezdeni elmenni a microservices irányba?

Ötletek, linkek, könyvek, bármi érdekel a témában.

Ha valaki játszani akar a választási adatokkal

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            new Program().Run();
        }

        private void Run()
        {
            Regex r = new Regex(@".+\\(\w+)\\(\w+).evkjkv.html", RegexOptions.Compiled | RegexOptions.IgnoreCase);

            List<string> parties = new List<string>()
            {
                "FIDESZ",
                "JOBBIK",
                "MSZP",
                "LMP",
                "EGYÜTT",
                "DK",
            };

            foreach (var file in Directory.GetFiles(
                @"C:\temp\valasztas\valasztas.hu\dyn\pv18\szavossz\hu\", "evkjkv.html", SearchOption.AllDirectories))
            {
                var d = FromHtml(file);

                IEnumerable<string> cols = parties.Select(p =>
                {
                    var part = GetVotes(d, file, p);
                    return $"{p},{part}";
                });
                string res = string.Join(",", cols);
                var m = r.Match(file);
                Console.WriteLine($"{m.Groups[1]},{m.Groups[2]},{res}");
            }
        }

        private static string GetVotes(XmlDocument d, string file, string party)
        {
            var n = d.SelectSingleNode($"//tr[td[starts-with(text(), '{party}')]]");
            if (n == null)
            {
                return "0";
                //Console.WriteLine($"Skipping {file} because there is no data for {party}");
            }

            //Console.WriteLine(n.InnerXml);
            return n.SelectSingleNode("td[4]").InnerText.Replace("&amp;", "").Replace("&nbsp;", "");
        }

        XmlDocument FromHtml(string path)
        {
            using (TextReader reader = File.OpenText(path))
            {
                XmlDocument doc;
                using (var sgmlReader = new Sgml.SgmlReader
                {
                    DocType = "HTML",
                    WhitespaceHandling = WhitespaceHandling.All,
                    CaseFolding = Sgml.CaseFolding.ToLower,
                    InputStream = reader
                })
                {
                    doc = new XmlDocument
                    {
                        PreserveWhitespace = true,
                        XmlResolver = null
                    };
                    doc.Load(sgmlReader);
                }

                return doc;
            }
        }
    }
}

.NET Rocks letöltése kocsiban hallgatáshoz

Másold be egy ps1 fájlba, és hagyd úgy éjszakára, reggelre ott lesz az egész.

function GetFeedPageCount ($url) {
  $feed=(New-Object System.Net.WebClient).DownloadString($url)
  $pageCount = $feed.rss.channel.pageCount
  return $pageCount;
}

function DownloadFeed ($url) {
  $feed=(New-Object System.Net.WebClient).DownloadString($url)
  foreach($i in $feed.rss.channel.item) {
    $url = New-Object System.Uri($i.enclosure.url)
    $url.ToString()
    $url.Segments[-1]
    $localFile = $url.Segments[-1]
    if (Test-Path($localFile)) {
      Write-Host "Skipping file, already downloaded"
    }
    else
    {
      Invoke-WebRequest $url -OutFile $url.Segments[-1]
    }
  }
}

# Related blog post here: http://blogs.msdn.com/b/cdndevs/archive/2014/11/18/azure-fridays-a-powershell-script-to-download-rss-videos.aspx

$feedUrl = "http://www.pwop.com/feed.aspx?show=dotnetrocks&filetype=master"
DownloadFeed($feedUrl)

Windows 10 system process high cpu usage: solution

Recently my laptop started to become sluggish and the ventilators was constantly on. In task manager it was clear the system process (pid 4) ate 1 cpu core.
Process explorer did not bring any interesting results.
Then I started an investigation using xperf.exe, a Windows Performance Toolkit utility. It is very similar to the profilers you might use for .NET, so I encourage my developer mates to use it when you have a native code performance problem.

Here is what I saw:

Process Explorer did not show more than the ExpWorkerThread level. Ok, I did not configure debug symbols for it, it might have shown stacks correctly, but it is just a snapshot, a sample of the state of the threads in opposite to xperf which profile processes in sampling mode.

However, xperf clearly shows us that volsnap.sys constantly uses the disk. I suppose it stuck into some infinite loop or some.
Volsnap is the volume snapshot service, which supports backup and recovery. So, at first I stopped Volume Shadow Copy service. It did not help.
Then, I deleted system restore points for my drives.
It took several minutes to finish deleting old restore points. After a reset system process is now behave correctly.
System restore points are very important for certain scenarios, so be careful to turn it off. This is just a workaround, to clean up bogus system restore points.
After deletion and restart you should have to reenable it!
I re-enabled it, and the problem did not manifest again. So, I think deleting and recreating restore points solve this kind of weird problem.

CIG visszatekintés – avagy miért nagyon sok a 2% alapkezelési díj a semmiért

Annak idején írtam a nem etikus (szépen fogalmaztam) CIG-ról, a befektetésről programozóknak sorozatban.
Azóta eltelt két év, és a pénz a leírtaknak megfelelően már nem a rablóknál van, hanem direktben vettem BRK-B részvényeket, ami a CIG-nál Warren Buffett Részvény Eszközalap néven fut.
A nevetséges, eszement költségekről, amikből többek között a brokernetes ügynököket fizetik most nem írok újra, korábban már leírtam.

Most a csekélynek tűnő 2%-os alapkezelési költség hatását akarom bemutatni. Ez fel se merül a legtöbb emberben, hisz csak jönnek-mennek a vonalak, nem látszik, hogy milyen manipuláció történik a háttérben. Milyen értéket ad hozzá az alapkezelő ahhoz képest, mintha direktben fektetnék be egy részvénybe? Magyarországról nézve egy dolgot, hedge-eli az USD/HUF mozgásokat. Azaz, mint a példában, ha erős dollár esetén váltottam forintot dollárra, és abból vettem amerikai részvényt, majd most a gyenge dollár esetén hiába hoz sokat egy részvény, ha visszaváltanám, akkor kevesebbet kapnék Ft-ban, mivel közben sokat gyengült a dollár. Más esetben ez fordítva lenne. Azaz, ha én csakis forintban gondolkodok, és külföldi részvényt veszek, akkor nem csak a részvény árfolyama befolyásolja a nyereségemet, hanem a dollár-forint árfolyam is. Ezt ki lehet ütni, másodlagos ügyletekkel (forex vagy opciós ügyletekkel).
Nekem ez nem érték, én egy alapkezelőtől az várnám, hogy ügyesen trade-elve generáljon a benchmark fölött profitot. Nem tudok egyetlen magyar befektetési intézetről sem, ami hosszútávon (de akár rövidtávon is) ezt meg tudná lépni. Mondhatnám tehetségtelen kutyaütők, de nem mondom, mert nagyon is okosok. Bevágják a pénzt valami ETF-be vagy részvénypakkba, beizzítják a hedge-elést, ami nem nagy művészet, majd hátradőlnek, és beszedik minden évben a TELJES BENN LEVŐ PÉNZ 2%-át!
Azaz olyan, mintha minden évben beszoroznák a meglévő pénzt 0,98-cal. Tegyük fel én egy kis-közepes magyar intézet vagyok, és kapok 100 milliárd forintot befektetésre. Minden évben 2 milliárd forintot kapok a semmiért. Hát nem jó biznisz az alapkezelés?
Még egyszer, nincs semmi bajom a 2%-kal, ha ezért kapok valamit, dolgoznak a pénzemen. De lófaszt nem kapok.
Ez nyilván akkor is így van, ha a bankokban fektetsz be alapokba, csak ott legalább nincs a különös feltételekben leírt extra rablás, ami 1-3 év befizetését égeti el.
Csak érdekességképpen belenéztem, a CIB-nél (nem CIG) az első két alapjukban (10. oldal) 300 milliárd forint van. Ennek 2%-a 6 millárd forint. A semmiért, és mégis veszteséges a bank. Eh.

Na, akkor a lényeg. Az évi 2% folyamatosan szívja le a pénzed, akkor ha, ha minden felfele megy, és akkor is, ha lefele.
Így:

Eltelt idő	2% hatása	Mi marad?
1 év	0.98	4,900,000 Ft
2 év	0.9604	4,802,000 Ft
3 év	0.941192	4,705,960 Ft
4 év	0.92236816	4,611,841 Ft
5 év	0.903920797	4,519,604 Ft
6 év	0.885842381	4,429,212 Ft
7 év	0.868125533	4,340,628 Ft
8 év	0.850763023	4,253,815 Ft
9 év	0.833747762	4,168,739 Ft
10 év	0.817072807	4,085,364 Ft

Tíz év alatt elégetnek 19%-ot. 5 millió induló tőkéből lenyesnek 1 millót, neked marad 4. Hosszabb távon nyilván még durvább a hatás az exponenciális jelleg miatt. Csak engem zavar ez? Biztos.

Vissza a CIG-re. Két éve elhoztam a pénzt a rablóktól, és azonnal átraktam a KBC-nél a CIG alap által is viszonteladott BRK-B részvénybe. A mellékelt excelben CIGLopas pontosan meg lehet nézni az eredményeket (csak 10M-ra módosítottam a számot, hogy könnyebb legyen vele számolni).
A CIG-nél 10M két év alatt 13.15M-ra menne fel, csak az alapkezelőből kieső pénzt számolva (durva bull van még). Persze, ha évente 1M-et fizettem volna be, akkor az első 3 év 8.5%-át is elviszik 10 éven keresztül, ami kb. 250000 évente (kicsit kevesebb, mert fogy a pénz, amiből a 8,5-et vonják).
Direkben befektetve, Ft hedge nélkül 13,94M lenne. Ha most kellene forintra visszaváltani pont szopó a hedge hiánya, mivel valójában a részvény felment 58%-kal, de ebből csak 39 marad, annyit gyengült a dollár. Ha pl. 2010 körül fektettem volna be, amikor még 1.5-szer erősebb volt a forint, akkor meg lenne egy 50%-os extra hozamom (mint ahogy a CIG-nél így volt, de persze ezt is lenyelték).
Tehát annak ellenére, hogy a dollár 8%-ot gyengült még mindig 7.81%-kal több pénzem van, mint a CIG-nél (és még több, mivel a különös lopótényezőt (8.5% első 3 évből) bele se raktam). 780000 különbség csak két év alatt. Ez szerintem durva.

Ha az UDF/HUF nem mozgott volna, akkor a 10M most két évvel később 15,8M lenne (és a portfóliómban dollárban kifejezve tényleg ez látszik, 60,8%-ban áll).
Ha hosszútávú befektetésről van szó, én nem félek attól, hogy a forint majd olyan brutál erős lesz 10-20 év múlva, hogy megszívom a visszaváltást, így engem nem érdekel a hedge-elés.

Összegezve, igen sok milliót veszítesz az alapkezelők 2%-án. Ha nem farhátat akarsz enni nyugdíjas korodban, míg a pénzeden szingapúri kuplerájba viszik a “befektetési tanácsadókat” és autókat sorsolnak ki nekik karácsonyra, akkor tanulj, és fektess be direktben, hagyd ki a vámpírokat a pipeline-ból.

Python pandas lassú io.parsers.read_csv metódus

Elkezdtem pythonozni, mivel machine learninget tanulok, és ahhoz vagy python vagy R javasolt. Legjobb mindkettőhöz érteni, most a pyhon van soron.

Akit érdekel bátran vágjon bele, a nyelv nem nagy szám (nekem ronda ez a kettőspontos mindenség, de majd megszokom), a tanulást a libek megismerése viszi el (kb. mint a legtöbb nyelvnél).

Mivel van sok tőzsdei adatom ezeken futtatom az ML libeket. A legtöbb példában napos adatokat használnak, de én intraday akarok keresgélni, ami sokkal több adatot jelent. Valószínűleg bajban leszek, pl. a Support Vector Machine o(n^4)-es alg, quadratikus, így nem tolhatok rá túl sok adatot.
De már az elején elakadtam, mert 1-2M CSV betöltése is 10mp volt.

Mindenféléket írtak a stackoverflown, csak a megoldást nem.

sym1 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.txt' % symbol),
header=None, index_col=0, nrows = rows, parse_dates=[['Date', 'Time']],
infer_datetime_format=True,
names=['Date', 'Time','Open','High','Low','Close', 'Up', 'Down'], usecols=['Date', 'Time','Close'])

Az infer_datetime_format=True hozott megoldást, valamiért a dátum parsolása csapnivaló, ezen flag nélkül. A read_csv doksija írta, onnan jött az ötlet:
“infer_datetime_format : boolean, default False

If True and parse_dates is enabled, pandas will attempt to infer the format of the datetime strings in the columns, and if it can be inferred, switch to a faster method of parsing them. In some cases this can increase the parsing speed by 5-10x.”

IIS lassulás probléma – megoldás

Annak idején írtam róla, hogy egy cégnél a web szerverek és a mögöttük levő webszervizek közötti kommunikációban jelen van egy 200ms-os plusz késleltetés, amire nem találtunk racionális magyarázatot.
Semmilyen eddig bevált eszköz, profilerek, WinDebug, Dynatrace, logok elemézése nem adott magyarázatot a jelenségre.
A Google a Nagle és a Delayed Ack irányába tolta a vizsgálódást.
Végül wiresharkkal alaposan kielemezve a forgalmat kiderült, hogy a protokoll leírással szemben csak 200ms késleltetéssel küldtek Acknowledge-et egymásnak a felek, ez adott késleltetést minden híváshoz. Miután a delayed ackot kikapcsoltuk mindkét oldalon, a jelenség megoldódott. A megoldás szerintem csak workaround, mivel a tcp protokol rfcje alapján nem így kellene működni a hálózatnak, vagy a Windows vagy a VMWare network card driver a bugos.

Röviden, mi zajlik itt le. A kliens (web app) TCP kommunikációt kezdeményez a webszerviz felé. A TCP protokollban minden csomag vételét meg kell erősíteni a másik oldalnak. Esetünkben olyan kicsi kérések mentek a szerviz felé, hogy belefértek egy hálózati csomagba. A Nagle algoritmus már eleve bufferelhetné a hívó oldalon a csomag kiküldését, de ezt úgy néz ki a generált webszerviz proxy kikapcsolja a Naglét, így ez nem okozott problémát.
Átmegy a kérés a szervizbe. Onnan egy acknowledgenek kell visszamenni a kliensre, jelezve, hogy a szerviz megkapta a csomagot. Ezt nem akarja visszaküldeni a szerviz azon mód, pár bájt miatt nem akar egy teljes csomagot visszaküldeni. A protokol alapján vár arra, amíg amúgy is küldene valamit vissza a kliensnek, és annak a hátára rakná rá az acknowledget (piggybacking).
Esetünkben volt sok kérés, lett volna alkalma visszaküldeni gyorsan a megerősítést, de nem tette. A protokollban van egy másik elem is. Ha eltelik 200ms, akkor ha eddig nem volt alkalmunk visszaküldeni a választ, legalább 200ms múlva meg kell tenni. Ez a delayed ack. Meg is tette a szerver, de ezzel minden egyes kérésre az ackot 200ms múlva küldte csak vissza. A kikapcsolással pazarló módon minden egyes csomagot azonnal visszajelez a szerver, azaz kikapcsoltuk ezt az optimalizálást, de cserébe a bugos késleltetés kiesett.
Összegezve, a válasznak vissza kellett volna menni más kérésekre adott válaszok hátán, de nem mentek vissza, ezért állítom, hogy ez bug, a delayed ack kikapcsolás csak workaround, de legalább megoldotta a késleltetést.
Két kép, ami szemlélteti a hibát.
Az első egy csatornán a kommunikáció lépéseit mutatja meg (a http keepalive miatt sok kérés meg át egy tcp csatornán). Látható, hogy nagyon sok kérésre csak 200ms múlva jön meg a válasz.

A második képen sok kérés ackjának maximuma látható. Jól megfigyelhető a “fék” hatása.

Sajnos nincs kéznél képem, de a delayed ack kikapcsolása után lemegy pár száz mikro!secre a válasz, mivel ilyenkor buzgó módon azonnal megy vissza az ack.

IIS lassulás probléma – help needed

Kivételesen nem megoldást írok le, hanem kérdést teszek fel.

Egyik ügyfelemnél vagy egy eset, amit egyelőre nem sikerült visszafejteni. Adott 3 IIS publikus web láb NLBS-sel összenyalábolva, és 3 belső IIS alatt futó WS szintén NLBS mögött, ezeket hívják a külső webappok. A Windowsok VmWare alatt vannak virtualizálva.

A hiba az, hogy random időpontban belassulnak a webszerverek webszervizek irányába mutató hívásai. Normál esetben egy gyors ws metódus hívása 2-5ms, amikor beáll ez az állapotváltás a webszerver worker process belsejében, akkor felmegy kb. 200 ms-re.

Azért írok állapotváltást, mert nem azért lassulnak be a dolgok, mert nap közben nagyobb a terhelés, hanem egyszer csak “elborul” a webapp, és belassul. Dynatrace alapján a ws hívások mélyén a recv windows hívás válaszol lassan. Ilyenkor a ws iis logjában is lassú a hívás, vélhetően mert a webapp mint kliens lassan viszi el az adatot.
Hamarosan lesz DotTrace lenyomat és FREB log is, illetve System.NET trace is (csak ez nagyon sok adatot termel).

IIS App reset megoldja a problémát egy ideig. Ha egy app beteg, akkor egy console appból ugyanaz a ws hívás ugyanezen a gépről a wsek felé gyors, tehát nem valószínű, hogy a wsek lassulnak be.

Nehézkes megfogni az estet, mert pl. egy config módosítás a trace-ek kedvéért azonnal appol resetet csinál, így elillan a hiba, aztán lehet megint egy napot várni rá.

A .NET perf counterek nem mutatnak kiugró értékeket, minden normálisnak tűnik. A web processben kb. 500 szál fut 1-3 kérés/sec esetén, ez mondjuk kicsit soknak tűnik, de a procmon nem mutatta meg a managed stacket (debug symbolokkal se), majd csak dottrace-ből látszik, mit csinálnak. A procik 10%-ig vannak kiterhelve. A diszk terhelés minimális, paging nincs, van 0.5G szabad memória, más proceszek nem eszik el az erőforrásokat. A resource monitorban a wsek irányába futó kérések jelentős része 200ms körüli latency-t mutat, ez egybevág más megfigyelésekkel.

Látott már ilyet valaki? Mit lehetne még mérni, amire nem gondoltam?

Forceseek delete-hez

Volt egy delete-em, ami nem akart rendesen index mentén lefutni. A delete-et kicserélve select-re ugyanez volt a helyzet, de select esetén egy oszlopokkal kikényszerített forceseek segített.
Viszont delete-et nem lehet hintelni. A megoldás CTE volt, így indirekten mégiscsak lehet hintelni. A lekérdezés pár százszor gyorsabb lett. :)

Érdemes megjegyezni három trükköt tanulságul:
1. CTE kimenetén lehet futtatni DDL-eket
2. Így indirekten lehet hintelni
3. Néha jó index esetén se seekel a szerver, ilyenkor csakis az oszlopokkal megsegített forceseek segít.

;with B
as
(
select * from dbo.Bar with(forceseek(IX_Natural_Key(TickerId, BDT)))
where TickerId = @tickerId and BDT in 
    (select DATEADD(day, DATEDIFF(day,'19000101',DATEADD(DAY, 1, cast(StartDate as datetime))), CAST(ClosingTime AS DATETIME2(7)))
    from TradingHours where TickerId = @timeTemplateTickerId and StartDate is not null and EndDate is null and IsEnabled = 1 and Priority > 10)
)
delete from B;

Nagy táblák join-olása eredmények

Korábbi bejegyzésemben írtam, hogy demó környezetben a columnstore indexek nagyon jelentős gyorsulást okoznak.

Élő adatbázisban azt tapasztaltuk, hogy integer kulcsokon végzett join-okon 5-10-szeres gyorsulást okozott a normál indexekhez képest.

Azonban GUID-os oszlopokkal nem okozott értékelhető gyorsulást, annak ellenére, hogy batch módúak voltak a műveletek. Erre úgy látszik nem gyúrtak még rá, vagy a guid randomsága miatt nem tud érdelmes teljesítményt elérni.

Nagy táblák joinolása

Egyik folyó munkámban több tízmilló soros táblákon végzett joinokat kellett optimalizálni. Általában ez nem kihívás, mert szinte mindig vannak szűrési feltételek, amelyeket kellő közelségbe víve a táblákhoz és rendes indexekat alápakolva már csak pár ezer joint kell végrehajtani, ami gyors lesz.
De most tényleg össze kellett joinolni sok millió sort, szűrés nélkül.
Mit lehet ezzel kezdeni? Sajnos itt már eléggé behatárolt területen mozgunk. A normál indexelős megoldások nem segítenek, mivel minden táblát teljes egészében be kell járni (nincs where).
Ráadásul ha *-os a select, akkor a cover NC index se játszik, hogy legalább az IO csökkenne.
Merge joinra lehet játszani clu indexekkel, de azért ez korlátos terület sok tábla esetén, illetve párhuzamos tervek esetén magától nem fog merge joint használni (itt írnak egy trace flagről, amivel mégis rá lehet venni).
Mit lehet tenni. Egyik lehetőség előre elkészíteni a join indexelt view-ban. Erre ügyesen ráharap az optimizer, ha van olyan join amit aztán többször futtatunk, akkor megéri ez a denormalizálás.
Ha viszont van újabb szerverünk (2016), akkor van sokkal durvább lehetőség: Columnstore index.
Az a baj ugye a nagy joinnal, hogy akárhogy is trükközünk, ez nagy meló a prociknak és az IO alrendszernek (vinkóknak). Az indexed view ezt úgy oldja meg, hogy egyszer kell megcsinálni, aztán sokszor élvezni az előre összepakolt adatokat.
A columnstore viszont (dióhéjban) azért piszok gyors mert:
1. 5-10-szeresen tömörítve tárolja az adatokat, kevesebb IO, illetve a memóriában a buffer cache-t is jobban ki tudja használni (mintha több RAM-unk lenne)
2. Képes az adatok csak egy részét felolvasni, ha csak kevés oszlop kell (select *-on ez nem segít persze)
3. Képes batch módban belülről párhuzamosan végrehajtani a műveletek egy részét (ez nagyon durván megdobja)
4. Képes a sorok egy részét felolvasni where feltétel alapján, mivel minden 1m sorhoz (szegmens) nyilván tarja az adott oszlop min és max értékét
5. Le tud nyomni operátorokat (pl. sum) a storage engine-be, így nem kell adatokat passzolgatni a rétegek között.

No, lássuk a medvét. Létrehoztam két másolatot egy 100 millió soros táblából. A tesztgép egy két éves laptop 2 core-ral és 8G RAM-mal, SSD-vel. Nem egy szerver.
A két táblát a kulcsai mentés join-olom, így mind a 100 millió sort végig kell néznie, és ennyi találat is lesz.

Először sima Clu index:
create clustered index IX_Clu1 on B1(Id)
create clustered index IX_Clu2 on B2(Id)

select count(*) from B1 join B2 on B1.Id = B2.Id

SQL Server parse and compile time:
CPU time = 15 ms, elapsed time = 18 ms.

(1 row(s) affected)
Table ‘B1’. Scan count 5, logical reads 1141262, physical reads 6,
read-ahead reads 1138814, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B2’. Scan count 5, logical reads 1140956, physical reads 4,
read-ahead reads 1138821, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.
Table ‘Workfile’. Scan count 896, logical reads 480256, physical reads
2688, read-ahead reads 477568, lob logical reads 0, lob physical reads
0, lob read-ahead reads 0.
Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob
read-ahead reads 0.

SQL Server Execution Times:
CPU time = 477262 ms, elapsed time = 377318 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.

SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

377 másodperc.

Jöhet a columnstore index:
create clustered columnstore index IX_CStore1 on B1
create clustered columnstore index IX_CStore2 on B2

select count(*) from B1 join B2 on B1.Id = B2.Id

SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 6 ms.

(1 row(s) affected)
Table ‘B2’. Scan count 4, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 105018, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B2’. Segment reads 103, segment skipped 0.
Table ‘B1’. Scan count 4, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 104998, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B1’. Segment reads 102, segment skipped 0.
Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob
read-ahead reads 0.

SQL Server Execution Times:
CPU time = 79920 ms, elapsed time = 27834 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.

SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

377 sec vs. 28 sec. Azért ez masszív különbség. :)

Érdekességképpen megnéztem NC Columnstore index-szel is, úgy 60 sec jön ki. Ez se rossz.

A jövő héten lehet ki tudjuk próbálni egy nagyobb géppel is, kíváncsi vagyok, ott mit tudunk vele kihozni.

Ha esetleg valakinek vannak már gyakorlati sikerei, érdekelnek a számok.

Mit is jelent pontosan, hogy Accent Insensitive?

Bólyai magyar verseny kapcsán gondolkodtam a magyar nyelv rendezési szabályain.

string[] words = new string[] { "írógép", "írat", "irkál", "irodalom", "író", "írás" };
Array.Sort(words, StringComparer.Create(new System.Globalization.CultureInfo("hu-hu"), false));
foreach (var w in words)
{
    Console.WriteLine(w);
}

Kimenet:

írás
írat
irkál
író
irodalom
írógép

Ezek szerint az alap magyar kultúra szerint a C# (valójában a Windows) accent insensitive módon rendez. A gyerekek is azt mondták, így tanulják az iskolában. Az én fejemben ez nem így volt. Vagy bugos a fejem, vagy mi még nem így tanultuk, nem tudom.

Nézzük SQL Serverben!

CREATE TABLE [dbo].[A](
	[Nev] [nvarchar](50) NOT NULL
) ON [PRIMARY]

select * from A
order by Nev collate hungarian_CS_AI

Azaz Accent Insensitive a rendezés, így joggal azonos a C#-pos kimenettel:

írás
írat
irkál
író
irodalom
írógép

Ami viszont bizarr:

select * from A
order by Nev collate hungarian_CS_AS

Kimenet:

írás
írat
irkál
író
irodalom
írógép

Ugyanaz! Magyar nyelv esetén nem számít az ékezet érzékenység, akkor se vesszük figyelembe az ékezeteket, ha kifejezetten kérjük. Ez nekem furcsa, de nem én írom a magyar nyelvi szabályokat.

Van egy másik collation is, annál meg mindig accent insensitive a rendezés, függetlenül a beállítástól:

select * from A
order by Nev collate Hungarian_Technical_CI_AS
select * from A
order by Nev collate Hungarian_Technical_CI_AI

Mindét esetben ez a kimenet:

irkál
irodalom
írat
írás
író
írógép

Tehát a magyar az egy olyan állatfajta, amiben nem az AI/AS szabályozza az Accent Sensitivity-t, hanem, hogy melyik collationt használjuk.

Ami viszont végképp fura, hogy a Latin1_General_100_CI_AS vs Latin1_General_100_CI_AI sem változtatja meg a sorrendet. Hogy van ez?

A mi aá, stb. variánsaink nem tartoznak az accentek közé?

Itt és itt érdekeseket írnak a témáról.

SQL Server memória hiány miatti lassú lekérdezés

Érdekes hibába futottam bele mostanában. A lapotopomban csak 8G RAM van, így az azon futó SQL servernek nem sok marad, amikor a Chrome, a Visual Studio 2015 és más memóriazabáló alkalmazások elkezdenek terjeszkedni.
A probléma az volt, hogy egy olyan lekérdezés, amely Sortot tartalmazott a szokásos 2-3 mp helyett fél-egy percig futott. A végrehajtási tervben volt egy figyelmeztetés, hogy a sort operátor kénytelen kipakolni a rendezést a tempdb-be (spill). De ettől még nem kellett volna ilyen lassúnak lennie. Csak 3500 sorról volt szó, ez azért belefért volna memóriába.

A select * from sys.dm_exec_query_memory_grants lekérdezésből kiderült, hogy a lekérdezés sortjának kellett volna 10M memória, de nem tudott annyit kapni, ezért várakozott, és aztán 20 másodperc múlva egyszer csak timeout lett, és kényszeredetten csak nekifogott végrehajtani a sortot a tempdbben.

Azaz egy egy nem szokványos lassú lekérdezés volt, nem sok lapolvasással járt, mint a tipikus rosszul optimalizált lekérdezések, hanem a memory grantre várt.
A megoldás az lett, hogy beállítottam 1G min memory-t az SQL Servernek, így már kiszámíthatóan jól érzi magát.

Egy rendes szerveren valószínűleg ritkább az ilyen memória kényszer, de azért jó tudni róla.