Garbage Collection

Auch Speicher kann defragmentiert werden. Die Garbage Collection (GC) optimiert die RAM-Belegung aber hat auch ihren Preis. Die Abkürzung GC steht oft auch für den Active Directory Global Catalog. Der ist hier aber nicht involviert.

Worum geht es ?

Beim Betrieb eines Computers arbeiten die Programme permanent mit Daten im Speicher. Sie fordern Platz zur Ablage von Informationen in Variablen an und geben Platz auch wieder frei. Auch der Inhalt von Variablen ist durchaus nicht statische Hier ein Beispiel:

[string]$test = "Dies ist ein Test"

PowerShell fordert also Platz an, um die 17 Zeichen zu speichern. Intern dürften das natürlich ein paar Byte mehr sein. Nun kann ich den Inhalt ja verändern, z.B.

[string]$test = $test + " für Exchange"

Rein Technisch muss das Speichermanagement nun noch 13 Zeichen dranhängen. Da die Software aber nicht weiß, wie viel vielleicht noch dazu kommt, kann Sie nicht vorab schon Platz freilassen. Also kann das Speichermanagement nun entweder die komplette Variable in einen anderen grö0eren Bereich kopieren oder Speicher über Pointer segmentieren. Irgendwann sieht mein Speicher aber ziemlich zerfleddert aus, d.h. überall sind einige Bytes belegt aber die freien Stücke werden immer kleiner und das Speichermanagement immer aufwändiger.

Daher gibt es den Prozess der "Garbage Collection", die den Speicher wieder reorganisiert. Die Applikation merkt davon fast nichts, denn Sie wird einfach kurz angehalten, die Speicherbereiche verschoben und darf wieder weiter machen. Aber dieses Anhalten und der Bedarf für die Optimierung kostet natürlich doch etwas Zeit.

GC Server Mode oder Workstation Mode

Es gibt ganze Diplomarbeiten über das Thema Speichermanagement aber ich möchte mich auf zwei Szenarien beschränken. Im -NET Framework kann man durchaus die Arbeit des GC konfigurieren. Ich vergleiche das immer etwas mit der Müllabfuhr. Wenn die freundlichen Endsorger jede Tag vorbekommen würden, um die Mülltonnen zu leeren, dann hätte ich zwar immer viel Platz für neue Reste aber auf Dauer würde es schon nerven, wenn täglich der Müllwagen durch die Straße rumpelt und ungünstig abgestellte PKW immer umparken müssten. Zu selten dürfen die Abräumer aber auch nicht kommen, da ansonsten auch mal eine Tonne "übervoll" wird. Der richtige Mittelweg pro "Kunde" ist hier zu suchen. Bei einem Einfamilienhaus reicht vielleicht der wöchentliche oder 14-tägige Abfuhrtermin. Industriebetriebe, Gaststätten o.ä. müssen aber häufiger bedient werden. Und genau so macht das auch die Garbage Collection.

Microsoft hat selbst für Entwickler einige Empfehlungen gegeben, wie die Garbage Collection für den eigenen Code konfiguriert werden sollte

If your application uses multiple threads and object instances, use server garbage collection instead of workstation garbage collection. Server garbage collection operates on multiple threads, whereas workstation garbage collection requires multiple instances of an application to run their own garbage collection threads and compete for CPU time. + An application that has a low load and that performs tasks infrequently in the background, such as a service, could use workstation garbage collection with concurrent garbage collection disabled.
Quelle: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/performance

Der Entwickler einer Software kann also selbst bestimmen, wie der GC seine Applikation optimiert und sogar manuell einen GC-Lauf anstoßen.

The Garbage Collector in the CLR is a very complicated, configurable and self-tuning creature that may change behavior based on application needs. To satisfy different memory usage requirements the GC has some options to configure how it operates. There are two main modes: Workstation mode (designed to minimize delays) and Server mode (designed for maximum application throughput). The GC also supports one of two “sub-modes” – concurrent or non-concurrent.
Quelle: https://blogs.msdn.microsoft.com/seteplia/2017/01/05/understanding-different-gc-modes-with-concurrency-visualizer/ 

Auf der MSXFAQ möchte ich das Thema hier nicht weiter vertiefen. Darum können sich dann eher die Entwickler kümmern. einen Einfluss auf Exchange kann es aber doc haben und der ist sehr wohl im Scope der MSXFAQ.

GC mit Performance Counter

Wie die Garbage Collection funktioniert, können Administratoren am einfachsten über Windows Performance Counter ermitteln. Auf jedem Server aber auch Workstations mit installiertem .NET-Framework gibt es die entsprechenden Counter.

PS C:\> (Get-Counter "\.NET CLR-Speicher(_global_)\*").countersamples | ft -AutoSize

Path
----
\\nawnbfc\.net clr-speicher(_global_)\auflistungsanzahl der generation 0
\\nawnbfc\.net clr-speicher(_global_)\auflistungsanzahl der generation 1
\\nawnbfc\.net clr-speicher(_global_)\auflistungsanzahl der generation 2
\\nawnbfc\.net clr-speicher(_global_)\von generation 0 avancierter speicher
\\nawnbfc\.net clr-speicher(_global_)\von generation 1 avancierter speicher
\\nawnbfc\.net clr-speicher(_global_)\von generation 0 avancierte bytes/sek.
\\nawnbfc\.net clr-speicher(_global_)\von generation 1 avancierte bytes/sek.
\\nawnbfc\.net clr-speicher(_global_)\von gen 0 hochgestufter finalization-s...
\\nawnbfc\.net clr-speicher(_global_)\prozess-id
\\nawnbfc\.net clr-speicher(_global_)\heapgröße der generation 0
\\nawnbfc\.net clr-speicher(_global_)\heapgröße der generation 1
\\nawnbfc\.net clr-speicher(_global_)\heapgröße der generation 2
\\nawnbfc\.net clr-speicher(_global_)\objektheapgröße
\\nawnbfc\.net clr-speicher(_global_)\aufgrund ausstehender objektfestlegung...
\\nawnbfc\.net clr-speicher(_global_)\anzahl der gc-handle
\\nawnbfc\.net clr-speicher(_global_)\zugeordnete bytes/sek.
\\nawnbfc\.net clr-speicher(_global_)\anzahl der ausgelösten gc
\\nawnbfc\.net clr-speicher(_global_)\gc-zeitdauer in prozent
\\nawnbfc\.net clr-speicher(_global_)\anzahl der bytes in den heaps
\\nawnbfc\.net clr-speicher(_global_)\festgelegte bytes insgesamt
\\nawnbfc\.net clr-speicher(_global_)\gesamtanzahl der reservierten bytes
\\nawnbfc\.net clr-speicher(_global_)\anzahl der fixierten objekte
\\nawnbfc\.net clr-speicher(_global_)\anzahl der verwendeten sinkblöcke

Die Counter gibt es natürlich pro Prozess. Ich habe hier mich auf "_global" der Übersichtlichkeit wegen beschränkt. Leider werden diese Counter auch übersetzt, so dass Skripte hier sprachabhängig auszuführen sind.

Noch ein wichtiger Grund, warum ich zumindest Server und Produkte ohne direkten Bezug der Anwender in englisch installieren würde.

(Get-Counter "\.NET CLR Memory(*)\*").countersamples
(Get-Counter "\.NET CLR-Speicher(*)\*").countersamples | ft 

Interessant ist hier insbesondere der Counter für die anteilige Zeit, die der GC gelaufen ist.

Wie die Beschreibung deutlich macht, ist dieser Counter auch kein Mittelwert, sondern zeigt nur die letzte Messung an. Sie müssen also schon recht häufig messen, um selbst einen Mittelwert bilden zu können.

PS C:\> (Get-Counter "\.NET CLR-Speicher(*)\GC-Zeitdauer in Prozent").countersamples | ft

Path                       InstanceName                             CookedValue
----                       ------------                             -----------
\\nawnbfc\.net clr-spei... _global_                         0,00130364205718591
\\nawnbfc\.net clr-spei... powershell                       0,00571339628844966
\\nawnbfc\.net clr-spei... outlook                          0,00029140182052041
\\nawnbfc\.net clr-spei... lsb                             4,12604120649846E-05
\\nawnbfc\.net clr-spei... scnotification                  0,000235528501372719
\\nawnbfc\.net clr-spei... btoeservice                     0,000236642265851589

Lesen Sie die Zahlen sehr genau. Auf den Ersten Blick könnte man meinen, dass der Prozess LSB (bei mir ist das die Lenovo Service Bridge für die Hardware) signifikant länger den GC benötigt. Am Ende der Zeile steht aber ein "E-05", d.h. das Komma muss um 5 Stellen nach links verschoben werden.

Der Wert sollte immer unter 10% liegen. Quelle ist hierbei KB2995145 Performance issues or delays when you connect to Exchange Server 2013 that is running in Windows Server (https://support.microsoft.com/kb/2995145 )

GC und Exchange

Anders sieht es natürlich aus, wenn die Garbage Collection einen Prozess anhält, der einen direkten Einfluss auf Anwender hat. Das ist bei Exchange durchaus möglich, da die Clients sich z.B. per RPC/HTTP oder MAPI/HTTP mit dem Server verbinden und wenn der Request gerade in ein Zeitfenster fällt, wenn der IIS durch den GC "angehalten" ist, dann sieht der Anwender eine verzögerte Antwort. Wenige Millisekunden sind kein Problem und wenn die Anwender im Cached-Mode arbeiten, bemerken Sie auch nicht. Sollten die Anwender aber z.B. im Online-Mode auf eines Kalender eines Kollegen oder auf den Posteingang einer Shared-Mailbox zugreifen, dann könnte es manchmal zu Stockungen kommen.

Allerdings hat auch hierzu das Exchange Team schon eine Aussage getroffen:

In general, each individual service that makes up the Exchange server product has been tuned as carefully as possible to be a good consumer of memory resources, and wherever possible, we utilize the Workstation Garbage Collector to avoid a dramatic and typically unnecessary increase in memory consumption. While it’s possible that adjusting a service to use Server GC rather than Workstation GC might temporarily mitigate an issue, it’s not a long-term fix that the product group recommends. When it comes to .NET Garbage Collector settings, our advice is to ensure that you are running with default settings and the only time these settings should be adjusted is with the advice and consent of Microsoft Support
Quelle: Ask the Perf Guy: How big is too BIG? https://blogs.technet.microsoft.com/exchange/2015/06/19/ask-the-perf-guy-how-big-is-too-big/

Das Thema wurde auch auf der Ignite 2016 thematisiert: (Min 29)

Microsoft Ignite 2016 Investigate tools and techniques for Exchange Performance Troubleshooting
https://channel9.msdn.com/Events/Ignite/2016/BRK3007


Quelle: https://channel9.msdn.com/Events/Ignite/2016/BRK3007  Minute 29ff

Also in der Regel sollten Sie die Finger davon lassen und die Exchange Entwickler wissen schon, was Sie tun und konfigurieren ihren Dienste schon korrekt. Dennoch gibt es auch Hinweise, dass der Server Mode  in manchen Umgebungen helfen kann. Ingo Gegenwarth hat das in seinem Blog sehr schön beschrieben. Er schildert hier Probleme beim Einsatz von MAPI/HTTP

GC und IIS Application Pool

Ehe Sie nun aber an den GC-Einstellungen ihres Exchange Servers herum schrauben, sollten Sie prüfen, ob der GC überhaupt in den Kreis der Verdächtigen aufgenommen werden muss. Ich beschränke mich auf die beiden aktuellen Protokolle MAPI/HTTP und RPC/HTTP, da MAPI/TCP mit Exchange 2013 entfallen ist und die Anzahl der Exchange 2010 und älter-Server doch stark abnimmt. POP3 und IMAP4 sind noch weniger relevant.

Auf https://ingogegenwarth.wordpress.com/2016/06/23/exchange-performancegarbage-collection/ hat Ingo Gegenwarth schön beschrieben, wie man den GC-Counter für den jeweiligen Prozess ermittelt. Leider hat Microsoft bei den Performance Counter es nicht gerade einfach gemacht, einen Applicationpool des IIS zu ermitteln. Der Applicationpool erscheint nämlich nicht mit Namen als Instanz beim ".NET CRL Memory"-Counter, sondern nur als Nummer. Daher muss man sich diese Daten erst herleiten. Etwas PowerShell hilft dabei hilfreich

# %Time in GC fuer MAPI/HTTP Application pool ermitteln
#. Suche nach counter W3SVC_W3WP(*)\Active Requests mit "MSExchangeMapiFrontEndAppPool" im Namen um die "Process ID" zu erhalten

$countersamples = (Get-Counter "\W3SVC_W3WP(*)\Active Requests").CounterSamples 
$instancename = $countersamples `
   |where {$_.instancename -like "*MSExchangeMapiFrontEndAppPool"} `
   | select Instancename -expandproperty Instancename
$w3sworkerpid = $instancename.Split("_")[0]


# Passenden WorkerProcess ermitteln.

$Worker = (Get-Counter "\.NET CLR Memory(w3w*)\Process ID").countersamples `
   | ? {$_.cookedValue -eq $w3sworkerpid }
$GCCounter = $worker.path -replace '^.+\((w3wp#\d+)\).*$', '$1' 


# Relevanten Counter auslesen 

$GCTime = ((Get-Counter "\.NET CLR Memory($GCCounter)\% Time in GC").countersamples).cookedvalue

# und ausgeben
write-host "MAPI/HTTP Time in GC: $GCTime"

# Nebenbei kan nan auch noch die Anzahl der ausstehenden HTTP-Requests auslesen.
((Get-Counter "\.NET CLR Memory($GCCounter)\HttpOutStandingProxyRequests").countersamples).cookedvalue

Wer das ganze dann für eine Serverfarm machen will, erweiterte die "Get-Counter"-Befehle um den Parameter "-computername" und füttert die Zeilen mit einer "for"-Schleife mit den Computernamen, die man mit Get-ExchangeServer ermitteln kann.

Da das Auslesen der Performance Counter ganz ohne Exchange Commandlets auskommt, können Sie die Exchange Server natürlich auch per LDAP einfach suchen.

Get-ADobject `
   -LDAPFilter "(objectCategory=msExchExchangeServer)" `
   -SearchBase "cn=configuration,dc=msxfaq,dc=net"

Und hier dann das ganze Skript um einmalig von allen Exchange Servern einfach mal die "% Time in GC" auszugeben. Hier ohne Filterung auf einen besonderen Application Pool aber mit einer Beschränkung auf Counter > 5

$exserverlist = Get-ADobject `
   -LDAPFilter "(objectCategory=msExchExchangeServe   -SearchBase "cn=configuration,dc=msxfaq,dc=net"

foreach ($exserver in $exserverlist){
   Write-host " Processig ExServer $($exserver.name)"
   Get-Counter `
      -computername $exserver.name `
      -counter "\.NET CLR Memory(*)\% Time in GC" `
   | select countersamples -expandproperty countersamples `
   | select path,cookedvalue `
   | where {$_.cookedvalue -ge 1}
}

Das Skript ist natürlich nur eine Basis für eigene zielgerichtete Anpassungen.

Zusammenfassung

Die Funktion der "Garbace Collection" ist wichtig und für besondere Fälle kann die Arbeit auch angepasst werden. In den allermeisten Exchange Umgebungen sollten Sie aber keine Änderungen vornehmen, da Microsoft dedizierte Entwickler hat, um die Exchange Performance zu optimieren. Das lässt sich handfest am Faktor "Geld" festmachen. Wenn Microsoft es per Code z.B. schafft, die Exchange Server nur 1% schneller zu machen, dann merken Sie das als Kunde eigentlich nicht. Aber im Bezug auf das Serversizing bei Office 365, wo tausende von Servern betrieben werden, bedeutet dies eine effektivere Nutzung und der Bedarf von 1% weniger Servern. Das dürfte ein sehr großes Einsparungspotential haben.

Nutzen Sie vorher erst alle anderen Optionen, um einen etwaigen Performance Engpass auf ihren Servern zu ermitteln.

Es gibt einige Skripte und Tools dazu, z.B.

Weitere Links