mirror of
https://github.com/moby/moby.git
synced 2026-08-08 17:11:38 +00:00
daemon: report network stats for Windows containerd runtime
Windows network stats were only available via the builtin HCS runtime; the containerd runtime (runhcs shim) reports no network data, so `docker stats` returned empty Networks (0B NET I/O) and TestStatsNetworkStats was skipped. Implement getNetworkStats on Windows to query stats from HNS per endpoint (GetHNSEndpointStats, keyed by the driver's hnsid). GetContainerStats only falls back to it when Networks is empty, leaving the builtin runtime unchanged. Un-skip TestStatsNetworkStats and add a conversion unit test. Signed-off-by: Dawei Wei <wei.dawei.cn@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
containertypes "github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/v2/daemon/container"
|
||||
"github.com/moby/moby/v2/daemon/server/backend"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ContainerStats writes information about the container to the stream
|
||||
@@ -124,8 +125,10 @@ func (daemon *Daemon) GetContainerStats(ctr *container.Container) (*containertyp
|
||||
goto done
|
||||
}
|
||||
|
||||
// We already have the network stats on Windows directly from HCS.
|
||||
if !ctr.Config.NetworkDisabled && runtime.GOOS != "windows" {
|
||||
// On Windows, the builtin (HCS) runtime already provides network stats
|
||||
// embedded in the stats above; the containerd runtime does not, so query
|
||||
// them separately when they're missing.
|
||||
if !ctr.Config.NetworkDisabled && (runtime.GOOS != "windows" || len(stats.Networks) == 0) {
|
||||
stats.Networks, err = daemon.getNetworkStats(ctr)
|
||||
}
|
||||
|
||||
@@ -144,3 +147,18 @@ done:
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// getNetworkSandboxID resolves the network SandboxID, following the chain in
|
||||
// case the container reuses another container's network stack.
|
||||
func (daemon *Daemon) getNetworkSandboxID(c *container.Container) (string, error) {
|
||||
curr := c
|
||||
for curr.HostConfig.NetworkMode.IsContainer() {
|
||||
containerID := curr.HostConfig.NetworkMode.ConnectedContainer()
|
||||
connected, err := daemon.GetContainer(containerID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "Could not get container for %s", containerID)
|
||||
}
|
||||
curr = connected
|
||||
}
|
||||
return curr.NetworkSettings.SandboxID, nil
|
||||
}
|
||||
|
||||
@@ -259,20 +259,6 @@ func (daemon *Daemon) statsV2(s *containertypes.StatsResponse, stats *statsV2.Me
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Resolve Network SandboxID in case the container reuse another container's network stack
|
||||
func (daemon *Daemon) getNetworkSandboxID(c *container.Container) (string, error) {
|
||||
curr := c
|
||||
for curr.HostConfig.NetworkMode.IsContainer() {
|
||||
containerID := curr.HostConfig.NetworkMode.ConnectedContainer()
|
||||
connected, err := daemon.GetContainer(containerID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "Could not get container for %s", containerID)
|
||||
}
|
||||
curr = connected
|
||||
}
|
||||
return curr.NetworkSettings.SandboxID, nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) getNetworkStats(c *container.Container) (map[string]containertypes.NetworkStats, error) {
|
||||
sandboxID, err := daemon.getNetworkSandboxID(c)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/Microsoft/hcsshim"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
containertypes "github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/v2/daemon/container"
|
||||
@@ -78,9 +79,55 @@ func (daemon *Daemon) stats(c *container.Container) (*containertypes.StatsRespon
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Windows network stats are obtained directly through HCS, hence this is a no-op.
|
||||
// getNetworkStats collects network statistics for a container. The builtin HCS
|
||||
// runtime already embeds these in the container stats, but the containerd
|
||||
// runtime does not, so they are queried here directly from HNS by endpoint.
|
||||
func (daemon *Daemon) getNetworkStats(c *container.Container) (map[string]containertypes.NetworkStats, error) {
|
||||
return make(map[string]containertypes.NetworkStats), nil
|
||||
sandboxID, err := daemon.getNetworkSandboxID(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sb, err := daemon.netController.SandboxByID(sandboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make(map[string]containertypes.NetworkStats)
|
||||
for _, ep := range sb.Endpoints() {
|
||||
info, err := ep.DriverInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hnsid, ok := info["hnsid"].(string)
|
||||
if !ok || hnsid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
epStats, err := hcsshim.GetHNSEndpointStats(hnsid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats[epStats.EndpointID] = hnsStatsToNetworkStats(epStats)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// hnsStatsToNetworkStats maps HNS endpoint statistics onto the API network stats
|
||||
// shape. Errors are not reported by HNS, so RxErrors/TxErrors are left zeroed
|
||||
// (matching the builtin HCS path, which also does not populate them on Windows).
|
||||
func hnsStatsToNetworkStats(epStats *hcsshim.HNSEndpointStats) containertypes.NetworkStats {
|
||||
return containertypes.NetworkStats{
|
||||
RxBytes: epStats.BytesReceived,
|
||||
RxPackets: epStats.PacketsReceived,
|
||||
RxDropped: epStats.DroppedPacketsIncoming,
|
||||
TxBytes: epStats.BytesSent,
|
||||
TxPackets: epStats.PacketsSent,
|
||||
TxDropped: epStats.DroppedPacketsOutgoing,
|
||||
EndpointID: epStats.EndpointID,
|
||||
InstanceID: epStats.InstanceID,
|
||||
}
|
||||
}
|
||||
|
||||
// getSystemCPUUsage returns the host system's cpu usage in
|
||||
|
||||
37
daemon/stats_windows_test.go
Normal file
37
daemon/stats_windows_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Microsoft/hcsshim"
|
||||
containertypes "github.com/moby/moby/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestHNSStatsToNetworkStats(t *testing.T) {
|
||||
in := &hcsshim.HNSEndpointStats{
|
||||
EndpointID: "endpoint-1",
|
||||
InstanceID: "instance-1",
|
||||
BytesReceived: 100,
|
||||
PacketsReceived: 10,
|
||||
DroppedPacketsIncoming: 1,
|
||||
BytesSent: 200,
|
||||
PacketsSent: 20,
|
||||
DroppedPacketsOutgoing: 2,
|
||||
}
|
||||
|
||||
got := hnsStatsToNetworkStats(in)
|
||||
|
||||
assert.Check(t, is.DeepEqual(got, containertypes.NetworkStats{
|
||||
RxBytes: 100,
|
||||
RxPackets: 10,
|
||||
RxDropped: 1,
|
||||
TxBytes: 200,
|
||||
TxPackets: 20,
|
||||
TxDropped: 2,
|
||||
EndpointID: "endpoint-1",
|
||||
InstanceID: "instance-1",
|
||||
// RxErrors/TxErrors are not reported by HNS and remain zero.
|
||||
}))
|
||||
}
|
||||
@@ -101,8 +101,6 @@ func TestStatsContainerNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStatsNetworkStats(t *testing.T) {
|
||||
// FIXME(thaJeztah): Broken on Windows + containerd combination, see https://github.com/moby/moby/pull/41479
|
||||
skip.If(t, testEnv.RuntimeIsWindowsContainerd(), "FIXME: Broken on Windows + containerd combination")
|
||||
skip.If(t, testEnv.IsRootless() && testEnv.DaemonInfo.CgroupVersion == "1", "Rootless Mode does not support cgroups v1 stats")
|
||||
skip.If(t, testEnv.IsRemoteDaemon(), "Test requires a local daemon")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user