Category Archives: Szakmai élet

Architektúra dilemma

Tegyük fel hogy van egy klasszikus, többrétegű alkalmazásunk. Van benne egy service layer, ami hív valamiféle alkalmazás logikát. A logika direktben hivatkozik Entity Framework DbContextre, abból olvas fel adatokat, módosítja őket, illetve direkt EF Context és DBSet hívásokkal menti őket.

Szeretnénk ezt az architektúrát egy kicsit jobban szétdarabolni, hogy átláthatóbb és tesztelhetőbb legyen. Ezért először az EF adatlekérő részeket kiszedjük Extract Method refactoringgal, majd utána átmozgatjuk őket saját osztályokba (Extract Class a vége). Ezeket az osztályokat elnevezhetjük repositoryknak. Ezek ugyan nem olyan szép, idealizált repository-k, mint amiket Martin Fowlerék álmodtak meg, de lényeg, hogy az adatelérő kódokat szeretnénk valamennyire elszigetelni a logikától. 

Miért? Mert így elszigetelve jobban meg lehet érteni őket, könnyebb optimalizálni a benne lévő adaterlését, és könnyebb az adateléréshez integrációs teszteket írni (például perf okokból).  A hívó, üzleti logika oldalon élesen szétválik az adatelérő kód és a logika, így az üzleti logikába könnyen tudunk fake adatokat bevinni, ami addig nem volt egyszerűen megoldható, amíg az entity frameworköt direktbe hivatkozva kért le adatokat a logika. Tehát a cél egy jobban kezelhető (SOLID) és jobban tesztelhető kód előállítása.

A dilemma a következő. Nyilván egy nagy osztályba nem lehet kimozgatni az összes helyen szétszórt eddigi adatelérő kódot, mert akkor kapunk egy több ezer soros monstrumot.  Valamilyen módon szét kell szedni az adatelérő metódusokat különböző osztályokba, ezeket hívtam az előbb repositoryknak. Ezek nem feltétlen 1-1 entitáshoz kapcsolódó repositoryk, hanem lehet, hogy valami üzleti területet foglalnak egybe. Például a SzámlázásRepo lehet, hogy 5-20 entitáshoz kapcsolódó adatelérést is csinál, hisz gyakori, hogy egy teljes gráfot kell letölteni (mint a Domain Aggregate a DDD-ben). De nincs jelentősége most a repó felbontásának.

Tételezzük fel, hogy itt tényleg entitás példányok jönnek vissza a repókból, tehát nem rakunk be egyelőre semmi mapping réteget, amivel DTO-k vagy Domin Objecteket alakítanánk ki. Egy ilyen refactoring során nem lehet ekkorát harapni egyszerre.

Tehát direktben entitásokat, persistent entityket adunk vissza. Minden repó mögött ugyanaz az EF DBContext példány van, ez könnyű elérni valamilyen Dependency Injection Framework Scope-olt lifetime fogalmával, általában a szerviz hívást mint scope-ot felhasználva. Ez azért fontos, mert ha letöltök egy entitást, majd letöltöm annak egy gyerekét, akkor belül az EF összekapcsolja a kettőt, azaz a szülő gyerek kollekciójába belekerül a gyerek, a gyerek szülő referenciája pedig a szülőre fog mutatni. Ez a közös Context egy kritikus koncepció az eddigi kódban, ugyanis bár lehet nem tudatos azokban, akik írták a kódot, de az EF Context révén implicit kapcsolatba kerül sok sornyi adatelérő kód, és csak azért működött helyesen az adatelérés, mert az EF így van megírva.

A get műveletek szépen és elegánsan becsomagolhatók repó műveletekbe. Ha van pl. egy GetInvoicesByCustomer metódus, akkor az visszaad Invoice entitásokat, ami mögé tetszőleges szintig betöltünk minden egyéb gyermek vagy szülő objektumokat. Igaz, ez a részlet nem látszik a metódus szignatúrából, de ezzel együtt élünk. A logikát könnyű tesztelni fake adatokkal, hisz csak ki kell cserélni a teljes repó metódust valami fix adatot visszaadó stubbal.

A módosítások repók felé történő kommunikációja már nehezebb. Az insert még elmegy, hisz pl. az InvoceRepository.AddInvoice vagy AddInvoceWithDetails vagy hasonló leírja, mit várunk a művelettől. A háttérben egyszerűen hívunk egy DbSet.Add-ot.

A Delete hasonlóan egyszerű, ha top level objektumról van szó, DeleteInvoice(Invoice).

Az egész viszont sokkal macerásabbá kezd válni, amikor például egy szülő objektum alá kell berakni egy gyerek objektumot.

Például, a repóból lekérek egy számlát, majd ehhez kellene tételeket hozzáadni.

Az InvoiceItems kollekció a visszakapott Invoice entitás része. Ha ehhez egyszerűen hozzáadunk egy új InvoceItemet, akkor az EF erről tudni fog, hisz neki végig van referenciája a gyökér Invoice objektumra, így a SaveChanges csak úgy el fogja menteni a gyerek sort, beállítva az FK-ját a szülő Invoice-ra.

A gondom az, hogy ez teljesen implicit. Nem tudok értelmes metódust felrakni a repóra, ami kommunikálná a logika azon szándékát, hogy ő itt most egy gyerek objektumot akar hozzáadni a szülőhöz. Emiatt tesztelni se tudom rendesen.

Ugyanez a probléma a módosításokkal. Lehozok a repó.GetValami metódussal egy entitást. Ezt direktben módosíthatja a logika, majd a SaveChanges-re az EF csak úgy tudja, hogy erre update SQL parancsot kell kiadni.

Megint, nem fogható meg jól tesztből, hogy a logika szándéka az volt, hogy módosít egy entitást.

Tehát, az átalakított kód úgy néz ki, hogy betölt mindenféle adatot mindenféle repo.Get hívásokkal. Ezek nem elszigetelt adatok, hanem “össze vannak ragadva”. Az EF mindet követi. A logika tetszőleges módon megbabrálja ezt a több darabból összerakott gráfot, majd az EF ezt elmenti “okosba”. De rém utálom az ilyen okosba megoldásokat.

Hogyan lehet erre tesztet írni, ami azt vizsgálja meg, hogy a logika a lehozott Invoce-hoz hozzáadott egy új tételt? Hogy lehet megfogni tesztből, hogy milyen entitás milyen property-jét mire állította be a logika? Azt feltételezem, hogy fake repók vannak a logika mögött. Ahogy megbeszéltük, a Getekben adatbázis helyett könnyű kamu adatokat behazudni memóriából, azaz visszaadhatunk teljesen feltöltött gráfokat. Például visszajön egy Invoice, benne két InvoiceItem, és referencia egy Customerre. Az logika legyen az, hogy ha a Customer neve Mari, akkor berak egy új tételt, InvoiceItemet, “Talp és egyéb masszázs” tartalommal.

A logika egyszerűen hozzáad egy new InvoiceItemet a Getben visszakapott Invoice objektumhoz, majd a repó mögötti EF DbContext.SaveChanges csak úgy el fogja menteni.

Hogyan lehet tesztből detektálni, hogy a logika tényleg hozzáadta a számlatételt?

Azt lehet tenni, hogy eltérítjük a SaveChanges hívást (mock scenario), és abban átnézzük a mit? Itt nincs igazi EF DbContext, nincs semmi state a kezünkben, amit meg vizsgálni.

Ha a repón lenne valami explicit metódus, akkor könnyebben lehetne tesztelni. Ha például lenne egy InvoceRepo.AddInvoiceItem(Invoice, InvoiceItem) metódus, akkor ezen keresztül explicit látszik a logika szándéka, hogy egy gyerekelemet akkor hozzáadni. Ez könnyen tesztelhető.

De mit csináljon az igazi AddInvoiceItem implementáció? Adja ő hozzá az itemet az átpasszolt Invoice-hoz? Ok, megteheti. De az EF kialakítása miatt a logika ezt minden további nélkül megteheti a visszakapott Invoice entitáson is, mindenféle repó hívás nélkül, mégis működni fog az igazi implementáció, míg a teszt hibát jelezne.

A probléma fő oka szerintem az, hogy a két réteg között csak úgy átmennek az entitások, így a logic layerben történt módosítások csak úgy átlátszanak a repó (data access) rétegbe.

Ez nyilvánvalóan egy nem elegáns, nem elég tiszta modell. 

De térjünk vissza az elejére. Van egy spagetti kódunk, amiben egymás után vannak logika és adatelérő kódok. Ez akarjuk elkezdeni kis kockázatú refactoringokkal darabolni tesztelés és kezelhetőség miatt. Mivel az eddigi kód erősen épített az EF-re mint change trackerre, ezt nem lehet egyszerű refactoringgal kiszedni belőle. Az átláthatóbb szerkezet érdekében kirakjuk saját osztályokba, repókba, de tudjuk, hogy erősen kilátszik az EF a repó mögül (google: Leaky Abstraction). Ezt tudjuk, elfogadjuk, mert nem lehet egy nagyon sok sorból álló kódot csak úgy átalakítani. A kérdés tehát az, hogyan lehet ilyen kötöttségek mellett úgy kialakítani egy értelmes repó interfészt és értelmes együttműködést a két réteg között, ami elfogadható szintű logika tesztelést tesz lehetővé?

RDP Brute Force Protection with PowerShell and Windows Firewall Rules

Az ötlet innen jött. Kicsit átírtam a kódot, mert az eredeti minden egyes kitiltott IP-t egyesével appendelte a log fájlhoz, ami nagyon lassúvá tette, valamint nem kezelte a duplikát bejegyzéseket.

Íme az átírt verzió:

$Last_n_Hours = [DateTime]::Now.AddHours(-24)
$badRDPlogons = Get-EventLog -LogName 'Security' -after $Last_n_Hours -InstanceId 4625 | ?{$_.Message -match 'logon type:\s+(3)\s'} | Select-Object @{n='IpAddress';e={$_.ReplacementStrings[-2]} }
$getip = $badRDPlogons | group-object -property IpAddress | where {$_.Count -gt 10} | Select -property Name

$uniqueIps = @{}

$current_ips = (Get-NetFirewallRule -DisplayName "BlockRDPBruteForce" | Get-NetFirewallAddressFilter ).RemoteAddress

foreach ($ip in $current_ips)
{
    $uniqueIps[$ip] = $true
}

foreach ($ip in $getip)
{
    $uniqueIps[$ip.Name] = $true
}

$finalBlockedIps = $uniqueIps.Keys | Sort-Object

Set-NetFirewallRule -DisplayName "BlockRDPBruteForce" -RemoteAddress $finalBlockedIps

Write-Output "Blocked addresses:"
$finalBlockedIps 

Write-Output "Blocked address count:"
$finalBlockedIps.Count

$log = "C:\ware\secu\rdp_blocked_ip.txt"
$finalBlockedIps | Select-Object {(Get-Date).ToString() + ' ' + $_} | Out-File $log

Itt látható, hogy 1 nap után már 260 címet tiltott ki.

.NET Core performance nyomozás II.

Újra nekiálltam mérni. Előző alkalommal azért voltak rosszabbak a .NET Core számai, mert szándékaim ellenére elsőre nem optimalizált kódot mértem. A régi projectről SDK projectre konverzió során valami összezagyválódott az optimalizálás beállítások körül a release configonál. Kézzel kitakarítottam a csprojokat, így már azt fordít, amit én akarok (egyébként optimize+ a default).

Mivel a mérés során láttuk, hogy a GC-nek nagy hatása van a mért teljesítményre, ezért, hogy reálisabb képet kapjunk, hogy olyan sokszor kell megismételni a mérést, hogy biztos elinduljon párszor a GC. Hisz lehet azért szépek egyes méréseknél a számok, mert nem volt elég nagy a használt memória mérete, hogy a GC elinduljon, így nincs alkalma rontani a számokat.
.NET 4.8 alatt app.configból szabályzom a GC működését:

<gcServer enabled="true|false"/>
<gcConcurrent enabled="true|false"/>

.NET 5 alatt a runtimeconfig.template.jsonből a legyegyszerűbb szabályozni a GC-t:

{
  "runtimeOptions": {
  "configProperties": {
  "System.GC.Concurrent": false
  "System.GC.Server": false }
  }
}

De nekem egyelőre erre nem hallgatott, de az csprojban beállítottra igen:

<PropertyGroup>
	<ServerGarbageCollection>true</ServerGarbageCollection>
	<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup>

Az első futások nincsenek benne az aggregált statiszikákban, de a listákban látszanak.

Jöjjenek a 4.8-as .NET Frameworks számok.

A mérések tetején ott a konfig, a GC számok azt mutatják, addig az adott generációban hányszor történt takarítás.


Először két mérés Workstation GC-vel:

Server: False, LatencyMode: Batch
1849ms, GC0: 13, GC1: 8, GC2: 4
1565ms, GC0: 17, GC1: 11, GC2: 5
1958ms, GC0: 23, GC1: 14, GC2: 6
1072ms, GC0: 26, GC1: 16, GC2: 6
1696ms, GC0: 31, GC1: 19, GC2: 7
1371ms, GC0: 36, GC1: 21, GC2: 8
1411ms, GC0: 41, GC1: 24, GC2: 9
1270ms, GC0: 45, GC1: 26, GC2: 10
1446ms, GC0: 50, GC1: 29, GC2: 11
1264ms, GC0: 54, GC1: 31, GC2: 12
1455ms, GC0: 59, GC1: 34, GC2: 13
1294ms, GC0: 63, GC1: 36, GC2: 14
1433ms, GC0: 68, GC1: 39, GC2: 15
1246ms, GC0: 72, GC1: 41, GC2: 16
1431ms, GC0: 77, GC1: 44, GC2: 17
1318ms, GC0: 81, GC1: 46, GC2: 18
1460ms, GC0: 86, GC1: 49, GC2: 19
1258ms, GC0: 90, GC1: 51, GC2: 20
1434ms, GC0: 95, GC1: 54, GC2: 21
1251ms, GC0: 99, GC1: 56, GC2: 22
Min: 1072, Avg: 1402

Server: False, LatencyMode: Interactive
1193ms, GC0: 137, GC1: 49, GC2: 7
1381ms, GC0: 209, GC1: 74, GC2: 8
1619ms, GC0: 282, GC1: 100, GC2: 10
1540ms, GC0: 355, GC1: 125, GC2: 12
1601ms, GC0: 429, GC1: 151, GC2: 14
1314ms, GC0: 500, GC1: 176, GC2: 14
1549ms, GC0: 573, GC1: 202, GC2: 16
1404ms, GC0: 645, GC1: 226, GC2: 16
1620ms, GC0: 718, GC1: 252, GC2: 18
1531ms, GC0: 791, GC1: 278, GC2: 20
1450ms, GC0: 864, GC1: 303, GC2: 21
1542ms, GC0: 937, GC1: 328, GC2: 23
1390ms, GC0: 1009, GC1: 353, GC2: 24
1353ms, GC0: 1081, GC1: 377, GC2: 24
1523ms, GC0: 1154, GC1: 403, GC2: 26
1313ms, GC0: 1225, GC1: 427, GC2: 26
1529ms, GC0: 1299, GC1: 454, GC2: 28
1435ms, GC0: 1371, GC1: 479, GC2: 29
1275ms, GC0: 1443, GC1: 503, GC2: 29
1500ms, GC0: 1516, GC1: 536, GC2: 31
Min: 1275, Avg: 1467

Érdekes látni, az interakív 15x annyi 0 generációs takarítást indított, mert igyekszik sokszor, de kicsit takarítani, hogy ha animációk vagy más időérzékeny kódok futnának, akkor ne szaggasson bele. De cserébe a futási idők kissé megnyúlnak.

Jöjjenek a server GC-k.


Server: True, LatencyMode: Batch
852ms, GC0: 2, GC1: 2, GC2: 2
683ms, GC0: 2, GC1: 2, GC2: 2
859ms, GC0: 3, GC1: 3, GC2: 3
509ms, GC0: 3, GC1: 3, GC2: 3
1061ms, GC0: 4, GC1: 4, GC2: 4
523ms, GC0: 4, GC1: 4, GC2: 4
539ms, GC0: 4, GC1: 4, GC2: 4
1084ms, GC0: 5, GC1: 5, GC2: 5
498ms, GC0: 5, GC1: 5, GC2: 5
505ms, GC0: 5, GC1: 5, GC2: 5
530ms, GC0: 5, GC1: 5, GC2: 5
894ms, GC0: 6, GC1: 6, GC2: 6
503ms, GC0: 6, GC1: 6, GC2: 6
1021ms, GC0: 7, GC1: 7, GC2: 7
495ms, GC0: 7, GC1: 7, GC2: 7
537ms, GC0: 7, GC1: 7, GC2: 7
1136ms, GC0: 8, GC1: 8, GC2: 8
498ms, GC0: 8, GC1: 8, GC2: 8
490ms, GC0: 8, GC1: 8, GC2: 8
514ms, GC0: 8, GC1: 8, GC2: 8
Min: 490, Avg: 678

Meghökkentő különbség a workstationhöz képest. Sokkal gyorsabban fut le a kód (pontosabban kevesebb időt vesz el a GC), sokkal kevesebb a takarítás minden generációban, ezért az áteresztőképesség több mint a duplája lett. Érdekes látni, mindig akkor nagyobbak a mért idők, amikor a GC0 is ugrik egyet (dőlttel megjelöltem).
Hozzáteszem, többszálú kódnál ez valószínűleg még harsányabban kijönne, mivel Server GC esetén processzoronkét 1 heap van, így kisebb a torlódás.

Server: True, LatencyMode: Interactive
868ms, GC0: 5, GC1: 3, GC2: 2
616ms, GC0: 5, GC1: 3, GC2: 2
1308ms, GC0: 7, GC1: 5, GC2: 3
613ms, GC0: 7, GC1: 5, GC2: 3
604ms, GC0: 7, GC1: 5, GC2: 3
1259ms, GC0: 8, GC1: 5, GC2: 3
538ms, GC0: 8, GC1: 5, GC2: 3
607ms, GC0: 8, GC1: 5, GC2: 3
1056ms, GC0: 9, GC1: 6, GC2: 4
487ms, GC0: 9, GC1: 6, GC2: 4
475ms, GC0: 9, GC1: 6, GC2: 4
883ms, GC0: 11, GC1: 8, GC2: 5
471ms, GC0: 11, GC1: 8, GC2: 5
489ms, GC0: 11, GC1: 8, GC2: 5
1110ms, GC0: 13, GC1: 9, GC2: 6
606ms, GC0: 13, GC1: 9, GC2: 6
599ms, GC0: 13, GC1: 9, GC2: 6
596ms, GC0: 14, GC1: 10, GC2: 7
1227ms, GC0: 15, GC1: 11, GC2: 7
477ms, GC0: 15, GC1: 11, GC2: 7
Min: 471, Avg: 738

Itt is szépek a számok, de jobban szórnak. Mindenesetre tisztán látszik, ha a nyers teljesítmény kell, a server GC sokat számít. Ezt főleg azoknak mondom, akik például Windows Service-ben futtatnak feldolgozó kódokat, még inkább, ha ezt több szálon teszik. Nagyon sokat lehet nyerni ezzel az egyszerű átkonfigurálással. Egyről már írtam 2009-ben is.

Még egy érdekes eset. Ha minden mérés között futtatom a GC-t (ezt nem belemérve az időkbe), így a mérés során kisebb a valószínűsége, hogy elinduljon (az első mérést megismételve)
Server: False, LatencyMode: Batch
1869ms, GC0: 13, GC1: 8, GC2: 4
1018ms, GC0: 17, GC1: 11, GC2: 5
1300ms, GC0: 23, GC1: 14, GC2: 6
1368ms, GC0: 27, GC1: 17, GC2: 8
1071ms, GC0: 32, GC1: 19, GC2: 9
1050ms, GC0: 36, GC1: 22, GC2: 10
1059ms, GC0: 41, GC1: 24, GC2: 11
1053ms, GC0: 45, GC1: 27, GC2: 12
986ms, GC0: 49, GC1: 30, GC2: 13
1076ms, GC0: 55, GC1: 33, GC2: 14
1049ms, GC0: 59, GC1: 36, GC2: 15
982ms, GC0: 63, GC1: 39, GC2: 16
1074ms, GC0: 69, GC1: 42, GC2: 17
1044ms, GC0: 73, GC1: 45, GC2: 18
980ms, GC0: 77, GC1: 48, GC2: 19
1072ms, GC0: 83, GC1: 51, GC2: 20
1056ms, GC0: 87, GC1: 54, GC2: 21
972ms, GC0: 91, GC1: 57, GC2: 22
1117ms, GC0: 97, GC1: 59, GC2: 23
1045ms, GC0: 101, GC1: 62, GC2: 24
Min: 972, Avg: 1072


Látható, hogy az átlag sokkal közelebb van a minimumhoz, mivel kevésbé szórnak a mért értékek, mert kevesebbszer szakadt meg a futás a GC miatt.

Mi eddig a tanulság? A GC erősen beleszól egy .NET app teljesítményébe, nem csoda, hogy az utóbbi években igen sokat tesznek a kevesebb heap szemetelés érdekében. ref return, structok erőltetett használata, stackalloc, Span, stb.

De az eredeti cél a .NET 4.8 és a .NET 5 közötti teljesítménykülönbség vizsgálata. Ezért lássuk az előző 4 esetet, de most .NET 5 alatt.

GC Server: False, GC LatencyMode: Interactive, .NETCoreApp,Version=v5.0
1098ms, GC0: 136, GC1: 50, GC2: 8
1287ms, GC0: 210, GC1: 77, GC2: 10
1122ms, GC0: 282, GC1: 102, GC2: 11
1457ms, GC0: 356, GC1: 129, GC2: 14
1188ms, GC0: 429, GC1: 154, GC2: 15
1009ms, GC0: 500, GC1: 178, GC2: 15
1345ms, GC0: 573, GC1: 203, GC2: 17
1321ms, GC0: 647, GC1: 229, GC2: 19
1059ms, GC0: 718, GC1: 253, GC2: 19
1381ms, GC0: 792, GC1: 287, GC2: 22
1154ms, GC0: 863, GC1: 323, GC2: 22
1235ms, GC0: 937, GC1: 353, GC2: 24
1179ms, GC0: 1008, GC1: 377, GC2: 24
1303ms, GC0: 1081, GC1: 403, GC2: 26
1419ms, GC0: 1154, GC1: 428, GC2: 27
1302ms, GC0: 1227, GC1: 453, GC2: 29
1310ms, GC0: 1299, GC1: 478, GC2: 30
1067ms, GC0: 1371, GC1: 502, GC2: 30
1499ms, GC0: 1445, GC1: 529, GC2: 33
1305ms, GC0: 1516, GC1: 553, GC2: 33
Min: 1009, Avg: 1260

GC Server: False, GC LatencyMode: Batch, .NETCoreApp,Version=v5.0
1536ms, GC0: 11, GC1: 8, GC2: 4
1552ms, GC0: 14, GC1: 11, GC2: 5
1729ms, GC0: 18, GC1: 15, GC2: 6
1293ms, GC0: 22, GC1: 17, GC2: 7
1691ms, GC0: 27, GC1: 20, GC2: 8
1205ms, GC0: 32, GC1: 22, GC2: 8
1368ms, GC0: 36, GC1: 24, GC2: 9
1213ms, GC0: 40, GC1: 26, GC2: 10
1262ms, GC0: 44, GC1: 29, GC2: 11
1226ms, GC0: 48, GC1: 31, GC2: 12
1366ms, GC0: 53, GC1: 34, GC2: 13
1231ms, GC0: 57, GC1: 36, GC2: 14
1373ms, GC0: 62, GC1: 39, GC2: 15
1219ms, GC0: 66, GC1: 41, GC2: 16
1364ms, GC0: 71, GC1: 44, GC2: 17
1243ms, GC0: 75, GC1: 46, GC2: 18
1392ms, GC0: 80, GC1: 49, GC2: 19
1284ms, GC0: 84, GC1: 51, GC2: 20
1379ms, GC0: 89, GC1: 54, GC2: 21
1265ms, GC0: 93, GC1: 56, GC2: 22
Min: 1205, Avg: 1350

GC Server: True, GC LatencyMode: Interactive, .NETCoreApp,Version=v5.0
792ms, GC0: 5, GC1: 3, GC2: 2
561ms, GC0: 5, GC1: 3, GC2: 2
1003ms, GC0: 6, GC1: 4, GC2: 2
563ms, GC0: 6, GC1: 4, GC2: 2
558ms, GC0: 6, GC1: 4, GC2: 2
907ms, GC0: 7, GC1: 4, GC2: 2
555ms, GC0: 7, GC1: 4, GC2: 2
550ms, GC0: 7, GC1: 4, GC2: 2
1182ms, GC0: 9, GC1: 5, GC2: 3
579ms, GC0: 9, GC1: 5, GC2: 3
551ms, GC0: 9, GC1: 5, GC2: 3
874ms, GC0: 10, GC1: 6, GC2: 4
400ms, GC0: 10, GC1: 6, GC2: 4
414ms, GC0: 11, GC1: 7, GC2: 5
536ms, GC0: 11, GC1: 7, GC2: 5
888ms, GC0: 12, GC1: 8, GC2: 5
402ms, GC0: 12, GC1: 8, GC2: 5
414ms, GC0: 12, GC1: 8, GC2: 5
799ms, GC0: 14, GC1: 9, GC2: 6
693ms, GC0: 14, GC1: 9, GC2: 6
Min: 400, Avg: 654

GC Server: True, GC LatencyMode: Batch, .NETCoreApp,Version=v5.0
804ms, GC0: 4, GC1: 2, GC2: 2
543ms, GC0: 4, GC1: 2, GC2: 2
544ms, GC0: 4, GC1: 2, GC2: 2
694ms, GC0: 5, GC1: 3, GC2: 3
672ms, GC0: 6, GC1: 4, GC2: 4
402ms, GC0: 6, GC1: 4, GC2: 4
734ms, GC0: 7, GC1: 5, GC2: 5
395ms, GC0: 7, GC1: 5, GC2: 5
419ms, GC0: 7, GC1: 5, GC2: 5
722ms, GC0: 8, GC1: 6, GC2: 6
401ms, GC0: 8, GC1: 6, GC2: 6
394ms, GC0: 8, GC1: 6, GC2: 6
626ms, GC0: 9, GC1: 7, GC2: 7
402ms, GC0: 9, GC1: 7, GC2: 7
670ms, GC0: 10, GC1: 8, GC2: 8
405ms, GC0: 10, GC1: 8, GC2: 8
408ms, GC0: 10, GC1: 8, GC2: 8
638ms, GC0: 11, GC1: 9, GC2: 9
408ms, GC0: 11, GC1: 9, GC2: 9
637ms, GC0: 12, GC1: 10, GC2: 10
Min: 394, Avg: 532

Na, azért ezek a számok már durvák! .NET 4.8 alatt a legjobb átlag 678ms volt Server: True, LatencyMode: Batch beállítások mellett.

.NET 5.0 (RC2) alatt GC Server: True, GC LatencyMode: Batch mellett 532 az átlag. Ez 21% teljesítménynövekedés, egy egyszerű újrafordítással.

Érdekes, hogy minkét esetben a server és a batch volt a nyertes.

A minimum is 471-ről leesett 394-re. Ez is 16%. Miért fontos nekem a minimum? Mert, ha tudom csökkenteni a felesleges heap allokálások számát, akkor az átlagot közel tudom húzni ehhez a számhoz. A teszt alatt gigabájtokat szemetelek, lesz mit faragni belőle.

.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.

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.