Skip to content

Commit bf8b6ec

Browse files
jiceathomejuagargi
authored andcommitted
testing: an improved router benchmark that only runs one router (scionproto#4444)
This is an alternative to router_benchmark and meant to eventually replace it. It's main feature is that it only runs a single router. As a result it's measured performance is far less dependent on the number of cores available on the test system. It is heavily inspired by the braccept test, in that: * it relies on the test harness to create an isolated network and veth interfaces to connect the test driver to the router. * it manufactures packets that are fed directly into the router's interfaces and it optionally captures packets for verification in the same manner. * it assumes a custom topology and cannot be usefully run on any other topology. It differs from braccept in that: * It does far less verification of the forwarded packets (it's braccept's job to do that). * It is considerably simplified, mainly because of the lighter verifications. * It relies on a simple address-assignment scheme so code maintainers don't have to keep tens of addresses in mind. * It does not assume that the router runs in a container network; it could also talk to a real router if given the names and mac addresses of the relevant interfaces. The test harness (or the user) is responsible for supplying the interface names and mac addresses to the test driver. * It does not require the test harness (or the user) to have any understanding of the topology (only to configure the router with it). The specifics of the captive network to be configured are supplied by the test driver. * The test harness doesn't need the "pause" container anymore. The similarity between this and braccept means that, in the future, we could re-converge them. Notably, the benefits of the simple addressing scheme would make the braccept test cases easier to maintain or expand. Collateral: the go version required was bumped to 1.21 Fixes scionproto#4442
1 parent 027fe01 commit bf8b6ec

File tree

21 files changed

+1763
-4
lines changed

21 files changed

+1763
-4
lines changed

acceptance/common/docker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def assert_no_networks(writer=None):
197197
writer.write("Docker networking assertions are OFF\n")
198198
return
199199

200-
allowed_nets = ['bridge', 'host', 'none']
200+
allowed_nets = ['bridge', 'host', 'none', 'benchmark']
201201
unexpected_nets = []
202202
for net in _get_networks():
203203
if net.name not in allowed_nets:

acceptance/router_benchmark/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def _run(self):
141141
"-name", "router_benchmark",
142142
"-cmd", "./bin/end2endblast",
143143
"-attempts", 1500000,
144-
"-timeout", "120s", # Timeout is for all attempts together
144+
"-timeout", "180s", # Timeout is for all attempts together
145145
"-parallelism", 100,
146146
"-subset", "noncore#core#remoteISD"
147147
].run_tee()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
load("//acceptance/common:raw.bzl", "raw_test")
2+
3+
exports_files([
4+
"conf",
5+
"test.py",
6+
])
7+
8+
args = [
9+
"--executable",
10+
"brload:$(location //acceptance/router_newbenchmark/brload:brload)",
11+
"--container-loader=posix-router:latest#$(location //docker:posix_router)",
12+
]
13+
14+
data = [
15+
":conf",
16+
"//docker:posix_router",
17+
"//acceptance/router_newbenchmark/brload:brload",
18+
]
19+
20+
raw_test(
21+
name = "test",
22+
src = "test.py",
23+
args = args,
24+
data = data,
25+
homedir = "$(rootpath //docker:posix_router)",
26+
# This test uses sudo and accesses /var/run/netns.
27+
local = True,
28+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//tools/lint:go.bzl", "go_library")
2+
load("//:scion.bzl", "scion_go_binary")
3+
4+
go_library(
5+
name = "go_default_library",
6+
srcs = ["main.go"],
7+
importpath = "github.com/scionproto/scion/acceptance/router_newbenchmark/brload",
8+
visibility = ["//visibility:private"],
9+
deps = [
10+
"//acceptance/router_newbenchmark/cases:go_default_library",
11+
"//pkg/log:go_default_library",
12+
"//pkg/private/serrors:go_default_library",
13+
"//pkg/scrypto:go_default_library",
14+
"//pkg/slayers:go_default_library",
15+
"//private/keyconf:go_default_library",
16+
"@com_github_google_gopacket//:go_default_library",
17+
"@com_github_google_gopacket//afpacket:go_default_library",
18+
"@com_github_google_gopacket//layers:go_default_library",
19+
"@com_github_spf13_cobra//:go_default_library",
20+
],
21+
)
22+
23+
scion_go_binary(
24+
name = "brload",
25+
embed = [":go_default_library"],
26+
visibility = ["//visibility:public"],
27+
)
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// Copyright 2023 SCION Association
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"hash"
21+
"net"
22+
"os"
23+
"path/filepath"
24+
"reflect"
25+
"strings"
26+
"time"
27+
28+
"github.com/google/gopacket"
29+
"github.com/google/gopacket/afpacket"
30+
"github.com/google/gopacket/layers"
31+
"github.com/spf13/cobra"
32+
33+
"github.com/scionproto/scion/acceptance/router_newbenchmark/cases"
34+
"github.com/scionproto/scion/pkg/log"
35+
"github.com/scionproto/scion/pkg/private/serrors"
36+
"github.com/scionproto/scion/pkg/scrypto"
37+
"github.com/scionproto/scion/pkg/slayers"
38+
"github.com/scionproto/scion/private/keyconf"
39+
)
40+
41+
type Case func(payload string, mac hash.Hash, numDistinct int) (string, string, [][]byte)
42+
43+
type caseChoice string
44+
45+
func (c *caseChoice) String() string {
46+
return string(*c)
47+
}
48+
49+
func (c *caseChoice) Set(v string) error {
50+
_, ok := allCases[v]
51+
if !ok {
52+
return errors.New("No such case")
53+
}
54+
*c = caseChoice(v)
55+
return nil
56+
}
57+
58+
func (c *caseChoice) Type() string {
59+
return "string enum"
60+
}
61+
62+
func (c *caseChoice) Allowed() string {
63+
return fmt.Sprintf("One of: %v", reflect.ValueOf(allCases).MapKeys())
64+
}
65+
66+
var (
67+
allCases = map[string]Case{
68+
"in": cases.In,
69+
"out": cases.Out,
70+
"in_transit": cases.InTransit,
71+
"out_transit": cases.OutTransit,
72+
"br_transit": cases.BrTransit,
73+
}
74+
logConsole string
75+
dir string
76+
numPackets int
77+
numStreams int
78+
caseToRun caseChoice
79+
interfaces []string
80+
)
81+
82+
func main() {
83+
rootCmd := &cobra.Command{
84+
Use: "brload",
85+
Short: "Generates traffic into a specific router of a specific topology",
86+
}
87+
intfCmd := &cobra.Command{
88+
Use: "show-interfaces",
89+
Short: "Provides a terse list of the interfaces that this test requires",
90+
Run: func(cmd *cobra.Command, args []string) {
91+
os.Exit(showInterfaces(cmd))
92+
},
93+
}
94+
runCmd := &cobra.Command{
95+
Use: "run",
96+
Short: "Executes the test",
97+
Run: func(cmd *cobra.Command, args []string) {
98+
os.Exit(run(cmd))
99+
},
100+
}
101+
runCmd.Flags().IntVar(&numPackets, "num-packets", 10, "Number of packets to send")
102+
runCmd.Flags().IntVar(&numStreams, "num-streams", 4,
103+
"Number of independent streams (flowID) to use")
104+
runCmd.Flags().StringVar(&logConsole, "log.console", "error",
105+
"Console logging level: debug|info|error|etc.")
106+
runCmd.Flags().StringVar(&dir, "artifacts", "", "Artifacts directory")
107+
runCmd.Flags().Var(&caseToRun, "case", "Case to run. "+caseToRun.Allowed())
108+
runCmd.Flags().StringArrayVar(&interfaces, "interface", []string{},
109+
`label=host_interface,mac,peer_mac where:
110+
host_interface: use this to exchange traffic with interface <label>
111+
mac: the mac address of interface <label>
112+
peer_mac: the mac address of <host_interface>`)
113+
runCmd.MarkFlagRequired("case")
114+
runCmd.MarkFlagRequired("interface")
115+
116+
rootCmd.AddCommand(intfCmd)
117+
rootCmd.AddCommand(runCmd)
118+
rootCmd.CompletionOptions.HiddenDefaultCmd = true
119+
120+
if rootCmd.Execute() != nil {
121+
os.Exit(1)
122+
}
123+
os.Exit(0)
124+
}
125+
126+
func showInterfaces(cmd *cobra.Command) int {
127+
fmt.Println(cases.ListInterfaces())
128+
return 0
129+
}
130+
131+
func run(cmd *cobra.Command) int {
132+
logCfg := log.Config{Console: log.ConsoleConfig{Level: logConsole}}
133+
if err := log.Setup(logCfg); err != nil {
134+
fmt.Fprintf(os.Stderr, "%s\n", err)
135+
return 1
136+
}
137+
defer log.HandlePanic()
138+
139+
caseFunc := allCases[string(caseToRun)] // key already checked.
140+
141+
artifactsDir := dir
142+
if v := os.Getenv("TEST_ARTIFACTS_DIR"); v != "" {
143+
artifactsDir = v
144+
}
145+
146+
if artifactsDir == "" {
147+
log.Error("Artifacts directory not configured")
148+
return 1
149+
}
150+
151+
hfMAC, err := loadKey(artifactsDir)
152+
if err != nil {
153+
log.Error("Loading keys failed", "err", err)
154+
return 1
155+
}
156+
157+
cases.InitInterfaces(interfaces)
158+
handles, err := openDevices()
159+
if err != nil {
160+
log.Error("Loading devices failed", "err", err)
161+
return 1
162+
}
163+
164+
registerScionPorts()
165+
166+
log.Info("BRLoad acceptance tests:")
167+
168+
payloadString := "actualpayloadbytes"
169+
caseDevIn, caseDevOut, rawPkts := caseFunc(payloadString, hfMAC, numStreams)
170+
171+
writePktTo, ok := handles[caseDevIn]
172+
if !ok {
173+
log.Error("device not found", "device", caseDevIn)
174+
return 1
175+
}
176+
177+
readPktFrom, ok := handles[caseDevOut]
178+
if !ok {
179+
log.Error("device not found", "device", caseDevOut)
180+
return 1
181+
}
182+
183+
// Try and pick-up one packet and check the payload. If that works, we're content
184+
// that this test works.
185+
packetSource := gopacket.NewPacketSource(readPktFrom, layers.LinkTypeEthernet)
186+
packetChan := packetSource.Packets()
187+
listenerChan := make(chan int)
188+
189+
go func() {
190+
defer log.HandlePanic()
191+
defer close(listenerChan)
192+
listenerChan <- receivePackets(packetChan, payloadString)
193+
}()
194+
195+
// We started everything that could be started. So the best window for perf mertics
196+
// opens somewhere around now.
197+
metricsBegin := time.Now().Unix()
198+
for i := 0; i < numPackets; i++ {
199+
if err := writePktTo.WritePacketData(rawPkts[i%numStreams]); err != nil {
200+
log.Error("writing input packet", "case", string(caseToRun), "error", err)
201+
return 1
202+
}
203+
}
204+
metricsEnd := time.Now().Unix()
205+
// The test harness looks for this output.
206+
fmt.Printf("metricsBegin: %d metricsEnd: %d\n", metricsBegin, metricsEnd)
207+
208+
// Get the results from the packet listener.
209+
// Give it one second as in very short tests (<1M pkts) we get here before the first packet.
210+
outcome := 0
211+
timeout := time.After(1 * time.Second)
212+
for outcome == 0 {
213+
select {
214+
case outcome = <-listenerChan:
215+
if outcome == 0 {
216+
log.Error("Listener never saw a valid packet being forwarded")
217+
return 1
218+
}
219+
case <-timeout:
220+
// If our listener is still stuck there, unstick it. Closing the device doesn't cause
221+
// the packet channel to close (presumably a bug). Close the channel ourselves. After
222+
// this, the next loop is guaranteed an outcome.
223+
close(packetChan)
224+
}
225+
}
226+
227+
fmt.Printf("Listener results: %d\n", outcome)
228+
return 0
229+
}
230+
231+
// receivePkts consume some or all (at least one if it arrives) of the packets
232+
// arriving on the given handle and checks that they contain the given payload.
233+
// The number of consumed packets is returned.
234+
// Currently we are content with receiving a single correct packet and we terminate after
235+
// that.
236+
func receivePackets(packetChan chan gopacket.Packet, payload string) int {
237+
numRcv := 0
238+
239+
for {
240+
got, ok := <-packetChan
241+
if !ok {
242+
// No more packets
243+
log.Info("No more Packets")
244+
return numRcv
245+
}
246+
if err := got.ErrorLayer(); err != nil {
247+
log.Error("error decoding packet", "err", err)
248+
continue
249+
}
250+
layer := got.Layer(gopacket.LayerTypePayload)
251+
if layer == nil {
252+
log.Error("error fetching packet payload: no PayLoad")
253+
continue
254+
}
255+
if string(layer.LayerContents()) == payload {
256+
// To return the count of all packets received, just remove the "return" below.
257+
// Return will occur once packetChan closes (which happens after a short timeout at
258+
// the end of the test.
259+
numRcv++
260+
return numRcv
261+
}
262+
}
263+
}
264+
265+
// initDevices inventories the available network interfaces, picks the ones that a case may inject
266+
// traffic into, and associates them with a AF Packet interface. It returns the packet interfaces
267+
// corresponding to each network interface.
268+
func openDevices() (map[string]*afpacket.TPacket, error) {
269+
devs, err := net.Interfaces()
270+
if err != nil {
271+
return nil, serrors.WrapStr("listing network interfaces", err)
272+
}
273+
274+
handles := make(map[string]*afpacket.TPacket)
275+
276+
for _, dev := range devs {
277+
if !strings.HasPrefix(dev.Name, "veth_") || !strings.HasSuffix(dev.Name, "_host") {
278+
continue
279+
}
280+
handle, err := afpacket.NewTPacket(afpacket.OptInterface(dev.Name))
281+
if err != nil {
282+
return nil, serrors.WrapStr("creating TPacket", err)
283+
}
284+
handles[dev.Name] = handle
285+
}
286+
287+
return handles, nil
288+
}
289+
290+
// loadKey loads the keys that the router under test uses to sign hop fields.
291+
func loadKey(artifactsDir string) (hash.Hash, error) {
292+
keysDir := filepath.Join(artifactsDir, "conf", "keys")
293+
mk, err := keyconf.LoadMaster(keysDir)
294+
if err != nil {
295+
return nil, err
296+
}
297+
macGen, err := scrypto.HFMacFactory(mk.Key0)
298+
if err != nil {
299+
return nil, err
300+
}
301+
return macGen(), nil
302+
}
303+
304+
// registerScionPorts registers the following UDP ports in gopacket such as SCION is the
305+
// next layer. In other words, map the following ports to expect SCION as the payload.
306+
func registerScionPorts() {
307+
for i := 30041; i < 30043; i++ {
308+
layers.RegisterUDPPortLayerType(layers.UDPPort(i), slayers.LayerTypeSCION)
309+
}
310+
for i := 30000; i < 30010; i++ {
311+
layers.RegisterUDPPortLayerType(layers.UDPPort(i), slayers.LayerTypeSCION)
312+
}
313+
for i := 50000; i < 50010; i++ {
314+
layers.RegisterUDPPortLayerType(layers.UDPPort(i), slayers.LayerTypeSCION)
315+
}
316+
}

0 commit comments

Comments
 (0)