Skip to content

Commit

Permalink
changed method for retrieving Windows network statistics
Browse files Browse the repository at this point in the history
Currently, we are running a Powershell method (Get-NetAdapterStatistics) to retrieve the network statistics in case of awsvpc network mode. However, this can cause CPU spikes on smaller instance types. Therefore, we are switching the method to use native Win32 API for retrieving the same.

GetIfTable2Ex is used for the same. Reference: https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getiftable2ex

In this refactor, we have abstracted the details of Win32 API in a new package which is used in Windows workflow.
  • Loading branch information
Harsh Rawat committed Oct 12, 2022
1 parent bd4ecb1 commit 13adf28
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 174 deletions.
15 changes: 10 additions & 5 deletions agent/api/eni/eni.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type ENI struct {
// LinkName is the name of the ENI on the instance.
// Currently, this field is being used only for Windows and is used during task networking setup.
LinkName string
// InterfaceIndex is the mapping between interface name and index.
// Currently, this field is being used only for Windows.
InterfaceIndex int
// MacAddress is the mac address of the eni
MacAddress string
// IPV4Addresses is the ipv4 address associated with the eni
Expand Down Expand Up @@ -195,8 +198,8 @@ func (eni *ENI) GetHostname() string {
return eni.PrivateDNSName
}

// GetLinkName returns the name of the ENI on the instance.
func (eni *ENI) GetLinkName() string {
// GetLinkNameAndIndex returns the name and index of the ENI on the instance.
func (eni *ENI) GetLinkNameAndIndex() (string, int) {
eni.guard.Lock()
defer eni.guard.Unlock()

Expand All @@ -205,24 +208,26 @@ func (eni *ENI) GetLinkName() string {
ifaces, err := netInterfaces()
if err != nil {
seelog.Errorf("Failed to find link name: %v.", err)
return ""
return "", 0
}
// Iterate over the list and find the interface with the ENI's MAC address.
for _, iface := range ifaces {
if strings.EqualFold(eni.MacAddress, iface.HardwareAddr.String()) {
eni.LinkName = iface.Name
// Additionally, store index in addition to link name.
eni.InterfaceIndex = iface.Index
break
}
}
// If the ENI is not matched by MAC address above, we will fail to
// assign the LinkName. Log that here since CNI will fail with the empty
// name.
if eni.LinkName == "" {
seelog.Errorf("Failed to find LinkName for MAC %s", eni.MacAddress)
seelog.Errorf("Failed to find LinkName and index for MAC %s", eni.MacAddress)
}
}

return eni.LinkName
return eni.LinkName, eni.InterfaceIndex
}

// IsStandardENI returns true if the ENI is a standard/regular ENI. That is, if it
Expand Down
16 changes: 10 additions & 6 deletions agent/api/eni/eni_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
customSearchDomain = "us-west-2.compute.internal"

linkName = "eth1"
linkIndex = 18
macAddr = "02:22:ea:8c:81:dc"
ipv4Addr = "1.2.3.4"
ipv4Gw = "1.2.3.1"
Expand Down Expand Up @@ -70,6 +71,7 @@ var (
net.Interface{
Name: linkName,
HardwareAddr: parsedMAC,
Index: linkIndex,
},
}, nil
}
Expand Down Expand Up @@ -148,26 +150,28 @@ func TestGetSubnetGatewayIPv4Address(t *testing.T) {
assert.Equal(t, ipv4Gw, testENI.GetSubnetGatewayIPv4Address())
}

// TestGetLinkNameSuccess tests the retrieval of ENIs name on the instance.
func TestGetLinkNameSuccess(t *testing.T) {
// TestGetLinkNameAndIndexSuccess tests the retrieval of ENIs name and index on the instance.
func TestGetLinkNameAndIndexSuccess(t *testing.T) {
netInterfaces = validNetInterfacesFunc
eni := &ENI{
MacAddress: macAddr,
}

eniLinkName := eni.GetLinkName()
eniLinkName, eniInterfaceIndex := eni.GetLinkNameAndIndex()
assert.EqualValues(t, linkName, eniLinkName)
assert.EqualValues(t, linkIndex, eniInterfaceIndex)
}

// TestGetLinkNameFailure tests the retrieval of ENI Name in case of failure.
func TestGetLinkNameFailure(t *testing.T) {
// TestGetLinkNameAndIndexFailure tests the retrieval of ENI Name and index in case of failure.
func TestGetLinkNameAndIndexFailure(t *testing.T) {
netInterfaces = invalidNetInterfacesFunc
eni := &ENI{
MacAddress: macAddr,
}

eniLinkName := eni.GetLinkName()
eniLinkName, eniInterfaceIndex := eni.GetLinkNameAndIndex()
assert.EqualValues(t, "", eniLinkName)
assert.EqualValues(t, 0, eniInterfaceIndex)
}

func TestENIToString(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion agent/api/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2780,6 +2780,6 @@ func (task *Task) UpdateTaskENIsLinkName() {

// Update the link name of the task eni.
for _, eni := range task.ENIs {
eni.GetLinkName()
eni.GetLinkNameAndIndex()
}
}
3 changes: 2 additions & 1 deletion agent/ecscni/netconfig_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ func NewVPCENIPluginConfigForTaskNSSetup(eni *eni.ENI, cfg *Config) (*libcni.Net
"task network namespace due to failed data validation")
}

linkName, _ := eni.GetLinkNameAndIndex()
eniConf := VPCENIPluginConfig{
Type: ECSVPCENIPluginName,
DNS: dns,
ENIName: eni.GetLinkName(),
ENIName: linkName,
ENIMACAddress: eni.MacAddress,
ENIIPAddresses: []string{eni.GetPrimaryIPv4AddressWithPrefixLength()},
GatewayIPAddresses: []string{eni.GetSubnetGatewayIPv4Address()},
Expand Down
64 changes: 64 additions & 0 deletions agent/stats/statsretriever/mock/stats_retriever.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions agent/stats/statsretriever/stats_retriever.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package statsretriever

import dockerstats "github.com/docker/docker/api/types"

//go:generate mockgen -destination=mock/$GOFILE -copyright_file=../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/stats/statsretriever StatsRetriever

// StatsRetriever defines methods for retrieving network stats.
type StatsRetriever interface {
// GetNetworkAdapterStatisticsPerContainer returns the network stats for the queried device averaged per container.
GetNetworkAdapterStatisticsPerContainer(int, int) (*dockerstats.NetworkStats, error)
}
82 changes: 82 additions & 0 deletions agent/stats/statsretriever/stats_retriever_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//go:build windows
// +build windows

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package statsretriever

import (
"syscall"
"unsafe"

log "github.com/cihub/seelog"
dockerstats "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"golang.org/x/sys/windows"
)

type retriever struct {
// funcGetIfEntry2Ex is the system call for GetIfEntry2Ex Win32 API.
funcGetIfEntry2Ex func(a ...uintptr) (r1 uintptr, r2 uintptr, lastErr error)
}

// NewStatsRetriever returns an instance of StatsRetriever interface.
func NewStatsRetriever() StatsRetriever {
log.Debugf("Initializing stats retriever for Windows")
obj := &retriever{}

// We would be using GetIfEntry2Ex Win32 APIs from IP Helper.
moduleIPHelper := windows.NewLazySystemDLL("iphlpapi.dll")
procGetIfEntry2Ex := moduleIPHelper.NewProc("GetIfEntry2Ex")

obj.funcGetIfEntry2Ex = procGetIfEntry2Ex.Call
return obj
}

// GetNetworkAdapterStatisticsPerContainer returns the network stats for the queried device averaged per container.
func (retriever *retriever) GetNetworkAdapterStatisticsPerContainer(ifIndex int, numberOfContainers int) (*dockerstats.NetworkStats, error) {
iface, err := retriever.getIfRowForDevice(ifIndex)
if err != nil {
return nil, err
}

stats := &dockerstats.NetworkStats{}
stats.RxBytes = iface.inOctets / uint64(numberOfContainers)
stats.RxPackets = (iface.inNUcastPkts + iface.inUcastPkts) / uint64(numberOfContainers)
stats.RxErrors = iface.inErrors / uint64(numberOfContainers)
stats.RxDropped = iface.inDiscards / uint64(numberOfContainers)
stats.TxBytes = iface.outOctets / uint64(numberOfContainers)
stats.TxPackets = (iface.outNUcastPkts + iface.outUcastPkts) / uint64(numberOfContainers)
stats.TxErrors = iface.outErrors / uint64(numberOfContainers)
stats.TxDropped = iface.outDiscards / uint64(numberOfContainers)

return stats, nil
}

// getIfRowForDevice return the MIB_IF_ROW2 object for the queried device.
// This method would internally invoke the GetIfEntry2Ex Windows API to get the interface row.
func (retriever *retriever) getIfRowForDevice(ifIndex int) (*mibIfRow2, error) {
row := &mibIfRow2{
interfaceIndex: uint32(ifIndex),
}

// GetIfEntry2Ex function retrieves the MIB-II interface for the given interface index..
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex
retVal, _, _ := retriever.funcGetIfEntry2Ex(uintptr(0), uintptr(unsafe.Pointer(row)))
if retVal != 0 {
return nil, errors.Errorf("error occured while calling GetIfEntry2Ex: %s", syscall.Errno(retVal))
}

return row, nil
}
70 changes: 70 additions & 0 deletions agent/stats/statsretriever/types_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//go:build windows
// +build windows

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package statsretriever

import "golang.org/x/sys/windows"

const (
ifMaxStringSize = 256
ifMaxPhysAddressLength = 32
)

// mibIfRow2 structure stores information about a particular interface.
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_if_row2
type mibIfRow2 struct {
interfaceLUID uint64
interfaceIndex uint32
interfaceGUID windows.GUID
alias [ifMaxStringSize + 1]uint16
description [ifMaxStringSize + 1]uint16
physicalAddressLength uint32
physicalAddress [ifMaxPhysAddressLength]byte
permanentPhysicalAddress [ifMaxPhysAddressLength]byte
mtu uint32
ifType uint32
tunnelType uint32
mediaType uint32
physicalMediumType uint32
accessType uint32
directionType uint32
interfaceAndOperStatusFlags uint8
operStatus uint32
adminStatus uint32
mediaConnectState uint32
networkGUID windows.GUID
connectionType uint32
transmitLinkSpeed uint64
receiveLinkSpeed uint64
inOctets uint64
inUcastPkts uint64
inNUcastPkts uint64
inDiscards uint64
inErrors uint64
inUnknownProtos uint64
inUcastOctets uint64
inMulticastOctets uint64
inBroadcastOctets uint64
outOctets uint64
outUcastPkts uint64
outNUcastPkts uint64
outDiscards uint64
outErrors uint64
outUcastOctets uint64
outMulticastOctets uint64
outBroadcastOctets uint64
outQLen uint64
}
Loading

0 comments on commit 13adf28

Please sign in to comment.