End2End ClientCheck

Diese Skriptsammlung hilft mir zum einen dabei unklare Fehlersituationen eines Clients bei Netzwerkverbindungen analysieren und zweites eine Performance-Messung von Clients und Servern erlauben. Die erste Version ist ein PowerShell-Script mit statischen Einstellungen, die lokal protokollieren. Sie führt verschiedene Checks im Hintergrund aus, die alle als eigener Thread jede Sekunden einen Check durchführen und an das Hauptprogramm melden. Das Hauptprogramm sammelt die Ergebnisse wieder ein. Die Lösung ist modular aufgebaut und wird je nach Client immer wieder angepasst.

Bausteine

Zuerst habe ich mir überlege, welche Daten ich ermitteln will. Folgende Bausteine sind zusammen gekommen. All die Bausteine können Sie direkt so in einer PowerShell starten und sollten einfach einen Wert zurück geben.

Check

PowerShell

Computername

Der lokale Computername kann über mehrere Wege ausgelesen werden. Ich nutze gerne die Information über den FQDN

# Kurzer Name
$env:computername

# kurzer Name per DNS
[system.net.dns]::GetHostName()

# FQDN 
([system.net.dns]::GetHostEntry("localhost")).hostname

HyperV-Host

(get-item "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters").GetValue("HostName")
if ($error) {"nohyperv";$error.clear()}

Aktuelle CPU-Last

(Get-WmiObject win32_processor).loadpercentage

Abfrage dauert aber relative lange

Aktuelle RAM-Free

$os = Get-WmiObject Win32_OperatingSystem
$pctFree = [math]::Round(($os.FreePhysicalMemory/$os.TotalVisibleMemorySize)*100,2)

https://www.petri.com/display-memory-usage-powershell

Aktuelle Netzwerk-Last

Diese Abfrage macht man wohl am besten per Performance Counter wobei die Aufgabe wohl die ist, die Karte mit dem Default Gateway zu finden.

 

Primäre Netzwerkverbindung

LAN, WLAN, UMTS ? wäre nett, wenn man das wüsste um z.B. auch "Changes" zu erkennen.

System.Net.NetworkInformation.NetworkInterface-Klasse
https://msdn.microsoft.com/de-de/library/system.net.networkinformation.networkinterface(v=vs.110).aspx 

Default Gw finden (ca. 34ms)

Für die Ausgabe möchte ich die IP-Adresse des Default Gateway haben. In einem Firmen-LAN ist das ein guter Indikator für den Standort. Aber auch HomeOffice-Anwender kann man so leichter ermitteln. Vor allem aber sieht man einen Netzwerk-Change.

(Get-WmiObject -Class Win32_IP4RouteTable | where { $_.destination -eq '0.0.0.0' -and $_.mask -eq '0.0.0.0'} | Sort-Object metric1).nexthop

Ping DefGw less 800ms and 10k

Wenn ich das Default Gateway habe, dann kann ich das ja mal mit einem Ping pro Sekunde belasten um die Laufzeit zu messen. Mein Ziel war es ja "einen" Ping zu senden, der nach maximal 1 Sek auch wieder die Kontrolle zurück gibt. Das geht mit PING.EXE aber die Zeit ist damit nicht gut zu messen. Test-Connection macht alles aber unterstützt kein Timeout aber kommt zumindest nach 2,5 Sekunden zurück. Ein direkter WMI-Aufruf hat bei mir aber den angegebenen Timeout nicht respektiert. Daher bleibe ich mal bei Test-Connection

Um etwas "Musik" auf die Leitung zu bringen, sende ich einen 1kByte Paket. So lassen sich sehr langsame Verbindungen, z.B. schlechtes WLAN ermitteln. Um die Daten später besser anzeigen zu können, beschränke ich mich auf ganze Millisekunden.

# send single ping
$gw=(Get-WmiObject -Class Win32_IP4RouteTable | where { $_.destination -eq '0.0.0.0' -and $_.mask -eq '0.0.0.0'} | Sort-Object metric1).nexthop;
#$filter="Address="""+$gw+""" and timeout=800 and buffersize=1500"
#$time = [int]((Get-WmiObject -Class Win32_PingStatus -Filter $filter).ResponseTime)
#$time=[int](measure-command {$result= ping -w 800 -l 10000 -n 1 $gw}).totalmilliseconds;
$result = (test-connection -Computername $gw -BufferSize 1000 -Count 1 -erroraction silentlycontinue).responsetime
if ($error) {
   "Error";
   $error.clear();
}
else
if ($result) {
   $result
} 
else {
   "noreply"
}

Office 365 Entry Point

Hinsichtlich der Cloud möchte ich natürlich wissen, welchen Office 365 "Eingangspunkt" der Client bei einer DNS-Anfrage geliefert bekommt.

([system.net.dns]::GetHostEntry("outlook.office365.com")).hostname

Office 365 CAS-Server

Und natürlich interessiert mich dann auch der Exchange Server, bei dem der Client landet.

try {
   $error.clear();
   $httprequest=[system.Net.HttpWebRequest]::Create("https://outlook.office365.com/owa/healthcheck.htm");
   $httprequest.timeout=[int]800;
   $data=$httprequest.getresponse();
   if ($data.statuscode -eq 200){
      $data.headers["X-FEServer"];
   }
   $data.Close();
}
catch [system.Net.WebException] {
   $error.exception.message;
}
catch {
   "Failed";
}

Office 365 CAS Latenz

Die Dauer, bis Office 365 den Request bedient, möchte ich auch gerne wissen. Da jeder Bausteine aber unabhängig sein soll und immer nur eine Information zurückgeben kann, ist der Code fast zum CAS-Server identisch. Um die Daten später besser anzeigen zu können, beschränke ich mich auf ganze Millisekunden.

try {
   $error.clear();
   $httprequest=[system.Net.HttpWebRequest]::Create("https://outlook.office365.com/owa/healthcheck.htm");
   $duration=[int](measure-command {$data=$httprequest.getresponse()}).milliseconds;
   if ($data.statuscode -eq 200){
      $duration;
   }
   $data.Close(); 
}
catch [system.Net.WebException] {
   $error.exception.message;
}
catch {
   "Failed";
}

Internet Connection

Jeder Windows Client "prüft", ob er Zugriff auf das Internet hat. Dazu fragt der Client im Rahmen des Network Location Awareness die URL http://www.msftncsi.com/ncsi.txt ab und prüft den Inhalt. Sicher kann ein böser Provider oder Netzwerkadministrator diese URL auf einen internen Server lenken und die Antwort "spoofen". Aber es ist durchaus ein legitimer Check um eine Aussage zur Erreichbarkeit diesbezüglich zu machen:

if ((Invoke-WebRequest http://www.msftncsi.com/ncsi.txt -UseBasicParsing).content -eq "Microsoft NCSI"){
   "1"
}
else{
   "0"
}

Die Bausteine muss ich dann später so starten, dass Sie permanent laufen und Ergebnisse liefern. Daher bette ich jeden Baustein in folgenden While-Schleife ein:

While ($true) {
   #
   #  hier muss dann der Check-Code rein
   #
   #Warten auf nächste voll Sekunde
   start-sleep -Milliseconds (1000-(get-date).millisecond)
}

In einer ersten Version habe ich den Prüf-Code als TXT-Datei abgespeichert und per Get-Content geladen und als SkriptBlock dann dem Start-Job vorgeworfen. Das ist aber unschön, da der Code im Textfile keinen Umbruch haben darf oder jede Zeile mit ";" getrennt sein muss und ich konnte den Code alleine nicht mal eben ausführen. Daher sind die Module nun PS1-Dateien inklusive der While-Schliefe

Es sind durchaus noch andere Checks denkbar, wie z.B.:

  • Zertifikatnamencheck (erkennt Inspection Firewalls)
  • Bandbreitencheck (z.B. UDP-Pings)
  • Get-PublicIP  um die externe Adresse (NAT/Proxy) zu berichten (mit WebService)
  • Erreichbarkeit LDAP von AD-Servern
  • Weitere HTTPS-Dienste

Bei der Analyse der URL https://outlook.office365.com/owa/healthcheck.htm ist mit aufgefallen, dass das ausgelieferte Zertifikat auch die Namen für Hotmail im SAN-Eintrag enthalten. Anscheinend hat Microsoft nun wirklich die Welten verschmolzen.

Start-Job und Last

Damit die Checks skalieren, kann ich diese nicht sequentiell von einem Skript ausführen lassen. Ein "blockender" Aufruf würde alle anderen Checks verzögern. Daher startet das Hauptskript die einzelnen Checks als "Job". Die Jobs arbeiten dann komplett autark ihre Prüfung ab und melden die Ergebnisse über die Pipeline zurück. Das Hauptprogramm muss dann nur noch die Jobs überwachen und die Ergebnisse einsammeln.

Dazu habe ich natürlich ein paar Checks gemacht. Für Zeitmessungen nutze ich z.B. sehr intensiv das "Get-Date"-Commandlet Daher habe ich mal geprüft, ob das signifikant zu Verzögerungen führt. Dem ist aber wohl nicht so.

(measure-command {get-date}).TotalMilliseconds
3,0074

(measure-command {1..1000| %{get-date}}).TotalMilliseconds
127,2234

Dann stellt sich natürlich die Frage, wie "belastend" die vielen Job sein können. Auch hier habe ich auf meinem PC einfach die oben genannten Jobs gestartet.

Id Name                      PSJobTypeName State   HasMoreData Location  Command
-- ----                      ------------- -----   ----------- --------  -------
38 dns-office365outlook      BackgroundJob Running True        localhost ...
40 host-computername         BackgroundJob Running True        localhost ...
42 host-cpuload              BackgroundJob Running True        localhost ...
44 host-hypervhost           BackgroundJob Running True        localhost ...
46 host-ramfree              BackgroundJob Running True        localhost ...
48 http-o365casserverlatency BackgroundJob Running True        localhost ...
50 http-o365casservername    BackgroundJob Running True        localhost ...
52 net-defaultgateway        BackgroundJob Running True        localhost ...
54 net-defgatewayping        BackgroundJob Running True        localhost ...

Die Jobs sind ja die meiste Zeit im "Schlafmodus" und warten auf die nächste Sekunde. Und wenn Sie dann diesen einen Befehl ausführen, dann haben sie ein klein wenig CPU-Last. Nur die PowerShell-"Umgebung" braucht natürlich etwas RAM.

Wir brauchen also nicht allzu viel CPU-Last aber aufgrund des Speicherbedarfs sollten wir allzu viele Jobs aufstarten. ein C#- Code könnte mit Threads das vielleicht noch optimieren.

Ausführung

Ich starte also in einer PowerShell das Hauptmodul. Es lädt alle Checks, die als TXT-Datei vorliegen und startet dieses als Job.

Nach ein paar Sekunden Einschwingzeit startet es dann die Sammlung der Rückgaben und speichert diese in einer CSV-Datei. Wenn Sie das Skript per CTRL-C abbrechen, dann laufen die Jobs weiter!. Mit einem "X" hat das Skript noch die Zeit wieder aufzuräumen. Sollte das doch mal passiert sein, dann können Sie entweder die PowerShell schließen oder die Jobs manuell wie folgt entfernen:

get-job | remove-job -force

Ich habe das Skript noch nicht öffentlich gemacht. Ich möchte erst noch einige Tage meine Erfahrungen damit sammeln um es noch zu optimieren. Insbesondere bei den Langzeit -Tests mit den Prüfungen gegen Office 365-Dienste möchte ich sehen, ob diese von Microsoft vielleicht künstlich "gedrosselt" werden o.ä.

Ein Problem, welches ich sicher noch lösen muss, ist der Speicherbedarf. Nach einigen Tagen Laufzeit hat die aufrufende PowerShell sich richtig "voll" gefressen.

Das Beenden der "Jobs" mit einem "X" bringt aber noch keine Besserung. "Jobs" sind zumindest nicht sichtbar der Speicherfresser. Das Schließen der steuernden PowerShell gibt den Speicher sofort wieder frei:

Das "Vollfressen" auf etwas über 3 GB hat bei mir aber 7 Tage gedauert. In der Zeit habe ich mehrfach das Netzwerk gewechselt und den Schlafmode genutzt. Dabei wurde ein 36MB Protkolldatei geschrieben. Das Skript ist aktuell also besser kein Dauerläufer sondern nur für den überwachten kurzfristigen Betrieb gedacht.

Auswerten

Ein 8 Stunden-Lauf produziert generiert eine 5 MB CSV-Datei. Eine 7-Tage-Analyse kommt so schon mal auf ca. 100 Megabyte. Die Ergebnis-CSV ist der Menge (eine Zeile pro Sekunde) schnell zu groß, um mit den RAW-Daten genutzt zu werden. Hier ist dann eine Auswertung und Gruppierung erforderlich. Das kann per PowerShell passieren, z.B.: eine Liste der Outlook Zugriffspunkte zu erhalten.

import-csv ClientConnectionCheck.result.20171123173528.csv | group dns-office365outlook -NoElement | ft -AutoSize

Count Name
----- ----
  665
 3329 outlook-emeaeast.office365.com
 6631 outlook.ms-acdc.office.com
 2975 outlook-emeaeast2.office365.com
 3201 outlook-emeaeast3.office365.com

Diese wechseln nämlich schon das ein oder andere mal. Auch die "Pingzeit" des Default Gateway ist interessant. Ist es doch der erste Hop und der sollte immer "schnelle" sein, es sei denn es ist wieder mal ein WLAN

import-csv ClientConnectionCheck.result.20171123173528.csv | measure-object net-defgatewayping -Average -Minimum -Maximum

Count    : 16822
Average  : 363,385150398288
Sum      :
Maximum  : 850
Minimum  : 0
Property : net-defgatewayping

Mindestens ein Paket war 850ms unterwegs. und selbst ein Average von 363,4 ist echt nicht gut. Da brauchen wir über WAN gar nicht mehr zu reden. Interessanter ist natürlich  eine grafisch Aufbereitung, z.B. mit Power Bi. Dann können Sie auch über die Zeit hinweg, z.B. einen Tag sehen, was passiert ist und wie die Verteilung der Antwortzeiten ist. Mit dem Timestamp auf der X-Achse können Sie sehen, wann die Ping-Zeit (rot) und die Dauer eines HTTP-Abrufs (grün) erhöht war.

Es gibt hier fast nur ganz wenig Übereinstimmung zwischen "langen PING-Zeiten" und langsamen HTTP-Requests. Insofern könnte man sagen, dass zumindest der erste Hob hier kein Problem hat. Interessant wird es natürlich, wenn ein Check auf dem Client den gesamten Weg durch das firmeneigene LAN bis zur Firewall prüft und so auch Transferstrecken mit erfasst werden. Allerdings würde ich solch einen Check dann nicht mehr auf dem Client sehen, sondern wieder im Bereich Netzwerk-Management.

Weiterentwicklung

Ich nutze das Skript aktuell immer interaktiv mit meiner Kennung, um z.B. die Leistung eines Clients Richtung Office 365 und andere Netzwerkprobleme zu diagnostizieren. Dazu passe ich die Skripte und Module natürlich auch entsprechend an, um die richtigen Ziele mit geeigneten Methoden und Intervallen zu messen. Ich will mit dem Skript ja nicht eine Gegenstelle oder Leitung überlasten. Für meine Anforderungen reicht dieses Funktion erst einmal. Es sind natürlich viele Optionen einer Weiterentwicklung möglich, z.B. eine zentrale Konfiguration, zentrales Reporting als auch eine Anzeige für den Anwender.

Weitere Links