PowerShell: Disable Virtual Machine Queuing on Hyper-V Hosts

What is VMQ?

Virtual Machine Queuing is a shortcut to deliver packet data from the external network directly to the virtual machines. This bypasses extra processing required to route the packets from the hyper-v host to the virtual machine. Additionally, this technology can leverage multiple CPU cores for packet processing; hence, this may increase overall throughput of high-bandwidth network interfaces (NICs). An in-depth guide to VMQ can be found here: https://techcommunity.microsoft.com/t5/networking-blog/synthetic-accelerations-in-a-nutshell-windows-server-2012/ba-p/447792

Why Disable this?

Certain NIC drivers have been known to cause problem with VMQ. Specifically, prior generations of Broadcom are suspects of this incompatibility, along with several other NIC vendors. Hence, disabling this feature would be necessary to solve some network problems, alleviating issues of failed machine migrations between Hyper-V nodes.

How to Enable this?

If VMQ is preferred on your systems, it’s best to properly configure such feature. Caveats would include checking whether the NIC driver is VMQ compatible, if there are different CPU counts on different nodes in the cluster, and there is consistency among Hyper-V nodes. Here’s something that can nudge you along that path.

# disableVmqOnHyperVHosts.ps1
# version 0.0.2
 
function getHyperVHostsInForest{
    function includeRSAT{
        $ErrorActionPreference='stop'
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
        $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
        $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
        $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
        $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
        $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
   
        # This command does not work on Windows 2012R2
        #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
        #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
        #NT\CurrentVersion.
        #At line:1 char:2
        #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
        #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
        #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
   
        $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
        $osVersion=[System.Environment]::OSVersion.Version
        [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
        $osName=(Get-WmiObject Win32_OperatingSystem).Name
        #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
        #    1 {'client'}
        #    2 {'domaincontroller'}
        #    3 {'memberserver'}
        #    }
   
        $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
   
        switch ($releaseId){
            1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
            1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
            1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
        }
       
        switch ($osVersionMajorMinor){
            {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
            {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
            {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
            {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
        }
  
        if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
            Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
            try{
                # If OS is Windows Server, then install RSAT using a different method
                if ($osName -match "^Microsoft Windows Server") {
                    # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                    Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                    Import-Module ServerManager
                    Add-WindowsFeature RSAT-AD-PowerShell
                    }
                else{
                    Write-Verbose "This sequence targets Windows Client versions"
                    $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                    Write-Host "Downloading RSAT from $link..."
                    Start-BitsTransfer -Source $link -Destination $destinationFile
                    $fileCheck=Get-AuthenticodeSignature $destinationFile
                    if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                    $wusaCommand = $destinationFile + " /quiet"
                    Write-host "Installing RSAT - please wait..."
                    Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                    }
                return $true
                }
            catch{
                write-warning "$($error[0].Exception)"
                return $false
                }
        }else{
            Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
            return $true
            }
    }
     function listAllHyperVNodes($verbose=$true){
        try{
            $timer=[System.Diagnostics.Stopwatch]::StartNew()
            $domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}
            foreach ($domain in $domains){
                #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                $allClusters=(get-cluster -domain $domain).Name
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: cluster names collected"
                    }

                $allHyperVNodes=@()
                foreach ($cluster in $allClusters){
                    $nodes=.{$x=Get-ClusterNode -Cluster $cluster -ea SilentlyContinue
                            if($x){
                                $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={$cluster}}
                            }else{
                                $false
                            }
                            }
                    if($nodes){$allHyperVNodes+=$nodes}
                }
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper Node names collected..."
                    }
                
                <#    
                $nodes=$allClusters|%{try{Get-ClusterGroup -Cluster $_ -ea SilentlyContinue}catch{}}
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper V node names collected"
                }
                $allHyperVNodes=$nodes|Group-Object -Property OwnerNode
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper V node names collected - Done!"
                    }
                #>
                }                
            return $allHyperVNodes
        }catch{
            Write-Error $_
            return $false
            }
    }
 
    try{
        $null=includeRSAT;
        $hyperVHosts=listAllHyperVNodes
        #$hyperVHostNames=sortArrayStringAsNumbers $hyperVHosts
        $hyperVHostNames=$hyperVHosts|sort -property Cluster
        return $hyperVHostNames
    }catch{
        Write-Error $_
        return $false
        }
    }
function disableVmq([string[]]$computerNames){
    $disableVmqCommand={
        try{
            import-module Hyper-V
            Get-NetAdapterVmq|%{Set-NetAdapterVmq -Name $_.Name -Enabled $False};
            return $true
        }catch{
            Write-Error $_;
            return $false
            }}
    $results=@{}
    if($computerNames){
        write-host "Disabling VMQ on these computers: $computerNames..."
        foreach ($computer in $computerNames){
            write-host "Processing $computer..." -nonewline
            $isVmqDisabled=try{Invoke-Command -ComputerName $computer -ScriptBlock $disableVmqCommand}catch{$false}
            if($isVmqDisabled){
                write-host 'success' -ForegroundColor Green
            }else{
                write-host 'failed' -ForegroundColor Red
            }
            $results+=@{$computer=$isVmqDisabled}
            }
    }
    return $results
}
function selectCluster($clusters){
    function pickList($list){
        # Although it's more efficient to obtain the index and set it as display,
        # humans prefer see a list that starts with 1, instead of 0
        $display=for ($i=0;$i -lt $list.count;$i++){
            "$($i+1)`:`t$($list[$i])`r`n";
            }
        $lines=($display | Measure-Object -Line).Lines
        write-host $($display)
        $maxAttempts=3
        $attempts=0;
        while ($attempts -le $maxAttempts){
            if($attempts++ -ge $maxAttempts){
                write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
                break;
                }
            $userInput = Read-Host -Prompt 'Please pick a number from the list above';
            try {
                $value=[int]$userInput;
                }catch{
                    $value=-1;
                    }
            if ($value -lt 1 -OR $value -gt $lines){
                cls;
                write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
                write-host $display
                }else{
                    $item=$list[$value-1];
                    write-host "$userInput corresponds to $item`r`n";
                    return $item
                    }
            }     
        }
    $uniqueClusters=$clusters|select -unique
    $pickedCluster=$(pickList $uniqueClusters)
    if($pickedCluster){
        return $pickedCluster
    }else{
        write-warning 'No clusternames were picked.'
        return $false
    }
}
function sortArrayStringAsNumbers([string[]]$names){
    $hashTable=@{}
    foreach ($name in $names){
        #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
        #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
        $x=.{[void]($name -match '(?:.(\d+)+)$');($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft(8,'0')}
        $hashTable.Add($name,$x)
        }
    $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
    return $sorted
}

# Get Cluster and associated hosts
$hyperVHostsInForest=getHyperVHostsInForest
$pickedCluster=selectCluster $hyperVHostsInForest.Cluster
$pickedHosts=$hyperVHostsInForest|?{$_.Cluster -eq $pickedCluster}
$pickedNames=sortArrayStringAsNumbers $pickedHosts.Name

# Get Processor 0 Utilization Before
$getProcessor0={$processor0=(Get-WmiObject -Query "select Name, PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor"|?{$_.Name -eq 0}).PercentProcessorTime; return @{$env:computername=$processor0}}
$processor0UtilizationBefore=$pickedNames|%{invoke-command -computername $_ -scriptblock $getProcessor0}
write-host "Processor-0 before:`r`n$(($processor0UtilizationBefore|out-string).trim())"

# Get ping response times before VMQ Changes
$referenceTarget='google.com'
$command={param($x);$responseTime=(test-connection $x -Count 1).ResponseTime;return @{$env:computername=$responseTime}}
$responseTimesBefore=$pickedNames|%{invoke-command -computername $_ -scriptblock $command -Args $referenceTarget}
write-host "Response times before:`r`n$responseTimesBefore"

# Disable VMQ
# disableVmq $(sortArrayStringAsNumbers $pickedNames)

# Get ping response times after VMQ Changes
$responseTimesAfter=$pickedNames|%{invoke-command -computername $_ -scriptblock $command -Args $referenceTarget}
write-host "Response times after:`r`n$responseTimesAfter"

# Get processor utilization after
$processor0UtilizationAfter=$pickedNames|%{invoke-command -computername $_ -scriptblock $getProcessor0}
write-host "Processor-0 before:`r`n$(($processor0UtilizationAfter|out-string).trim())"

Leave a Reply

Your email address will not be published. Required fields are marked *