Skip to content

Commit 87bf05a

Browse files
committed
networking: don't sort reserved port ranges before adding to bitmap
During a large volume dispatch load test, I discovered that a lot of the total scheduling time is being spent calling `structs.ParsePortRanges` repeatedly, in order to parse the reserved ports configuration of the node (ex. converting `"80,8000-8001"` to `[]int{80, 8000, 8001}`). A close examination of the profiles shows that the bulk of the time is being spent hashing the keys for the map of ports we use for de-duplication, and then sorting the resulting slice. The `(*NetworkIndex) SetNode` method that calls the offending `ParsePortRanges` merges all the ports into the `UsedPorts` map of bitmaps at scheduling time. Which means the consumer of the slice is already de-duplicating and doesn't care about the order. The only other caller of `ParsePortRanges` is when we validate the configuration file, and that throws away the slice entirely. By skipping de-duplication and not sorting, we can cut down the runtime of this function by 30x and memory usage by 3x. Ref: https://github.com/hashicorp/nomad/blob/v1.10.4/nomad/structs/network.go#L201 Fixes: #26654
1 parent 1916a16 commit 87bf05a

File tree

5 files changed

+47
-78
lines changed

5 files changed

+47
-78
lines changed

.changelog/26712.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
scheduling: Improve performance of scheduling when checking reserved ports usage
3+
```

nomad/structs/funcs.go

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ func ParsePortRanges(spec string) ([]uint64, error) {
493493
return nil, nil
494494
}
495495

496-
ports := make(map[uint64]struct{})
496+
ports := []uint64{}
497497
for _, part := range parts {
498498
part = strings.TrimSpace(part)
499499
rangeParts := strings.Split(part, "-")
@@ -507,11 +507,13 @@ func ParsePortRanges(spec string) ([]uint64, error) {
507507
if err != nil {
508508
return nil, err
509509
}
510-
510+
if port == 0 {
511+
return nil, fmt.Errorf("port must be > 0")
512+
}
511513
if port > MaxValidPort {
512514
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port)
513515
}
514-
ports[port] = struct{}{}
516+
ports = append(ports, port)
515517
}
516518
case 2:
517519
// We are parsing a range
@@ -526,36 +528,25 @@ func ParsePortRanges(spec string) ([]uint64, error) {
526528
}
527529

528530
if end < start {
529-
return nil, fmt.Errorf("invalid range: starting value (%v) less than ending (%v) value", end, start)
531+
return nil, fmt.Errorf("invalid range: ending value (%v) less than starting (%v) value", end, start)
530532
}
531533

532534
// Full range validation is below but prevent creating
533535
// arbitrarily large arrays here
536+
if start == 0 {
537+
return nil, fmt.Errorf("port must be > 0")
538+
}
534539
if end > MaxValidPort {
535540
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, end)
536541
}
537542

538543
for i := start; i <= end; i++ {
539-
ports[i] = struct{}{}
544+
ports = append(ports, i)
540545
}
541546
default:
542547
return nil, fmt.Errorf("can only parse single port numbers or port ranges (ex. 80,100-120,150)")
543548
}
544549
}
545550

546-
var results []uint64
547-
for port := range ports {
548-
if port == 0 {
549-
return nil, fmt.Errorf("port must be > 0")
550-
}
551-
if port > MaxValidPort {
552-
return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port)
553-
}
554-
results = append(results, port)
555-
}
556-
557-
sort.Slice(results, func(i, j int) bool {
558-
return results[i] < results[j]
559-
})
560-
return results, nil
551+
return ports, nil
561552
}

nomad/structs/funcs_test.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -966,14 +966,16 @@ func TestVaultNamespaceSet(t *testing.T) {
966966
require.ElementsMatch(t, expected, got)
967967
}
968968

969-
// TestParsePortRanges asserts ParsePortRanges errors on invalid port ranges.
969+
// TestParsePortRanges asserts ParsePortRanges errors on invalid port ranges and
970+
// returns the expected values
970971
func TestParsePortRanges(t *testing.T) {
971972
ci.Parallel(t)
972973

973974
cases := []struct {
974-
name string
975-
spec string
976-
err string
975+
name string
976+
spec string
977+
expect []uint64
978+
err string
977979
}{
978980
{
979981
name: "UnmatchedDash",
@@ -995,14 +997,39 @@ func TestParsePortRanges(t *testing.T) {
995997
spec: "9223372036854775807", // (2**63)-1
996998
err: "port must be < 65536 but found 9223372036854775807",
997999
},
1000+
{
1001+
name: "OverlappingRanges",
1002+
spec: "1-3,2-4",
1003+
expect: []uint64{1, 2, 3, 2, 3, 4}, // we don't care about dupes
1004+
},
1005+
{
1006+
name: "ReversedRange",
1007+
spec: "3-1",
1008+
err: "invalid range: ending value (1) less than starting (3) value",
1009+
},
1010+
{
1011+
name: "ZeroRange",
1012+
spec: "0-1",
1013+
err: "port must be > 0",
1014+
},
1015+
{
1016+
name: "OverlappingOutOfOrderRanges",
1017+
spec: "2-4,1-3",
1018+
expect: []uint64{2, 3, 4, 1, 2, 3}, // we don't care about dupes
1019+
},
9981020
}
9991021

10001022
for i := range cases {
10011023
tc := cases[i]
10021024
t.Run(tc.name, func(t *testing.T) {
10031025
results, err := ParsePortRanges(tc.spec)
1004-
require.Nil(t, results)
1005-
require.EqualError(t, err, tc.err)
1026+
if tc.err == "" {
1027+
must.NoError(t, err)
1028+
must.Eq(t, tc.expect, results)
1029+
} else {
1030+
must.Nil(t, results)
1031+
must.EqError(t, err, tc.err)
1032+
}
10061033
})
10071034
}
10081035
}

nomad/structs/structs.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3739,11 +3739,6 @@ type NodeReservedNetworkResources struct {
37393739
ReservedHostPorts string
37403740
}
37413741

3742-
// ParseReservedHostPorts returns the reserved host ports.
3743-
func (n *NodeReservedNetworkResources) ParseReservedHostPorts() ([]uint64, error) {
3744-
return ParsePortRanges(n.ReservedHostPorts)
3745-
}
3746-
37473742
// AllocatedResources is the set of resources to be used by an allocation.
37483743
type AllocatedResources struct {
37493744
// Tasks is a mapping of task name to the resources for the task.

nomad/structs/structs_test.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7283,53 +7283,6 @@ func TestSpread_Validate(t *testing.T) {
72837283
}
72847284
}
72857285

7286-
func TestNodeReservedNetworkResources_ParseReserved(t *testing.T) {
7287-
ci.Parallel(t)
7288-
7289-
require := require.New(t)
7290-
cases := []struct {
7291-
Input string
7292-
Parsed []uint64
7293-
Err bool
7294-
}{
7295-
{
7296-
"1,2,3",
7297-
[]uint64{1, 2, 3},
7298-
false,
7299-
},
7300-
{
7301-
"3,1,2,1,2,3,1-3",
7302-
[]uint64{1, 2, 3},
7303-
false,
7304-
},
7305-
{
7306-
"3-1",
7307-
nil,
7308-
true,
7309-
},
7310-
{
7311-
"1-3,2-4",
7312-
[]uint64{1, 2, 3, 4},
7313-
false,
7314-
},
7315-
{
7316-
"1-3,4,5-5,6,7,8-10",
7317-
[]uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
7318-
false,
7319-
},
7320-
}
7321-
7322-
for i, tc := range cases {
7323-
r := &NodeReservedNetworkResources{ReservedHostPorts: tc.Input}
7324-
out, err := r.ParseReservedHostPorts()
7325-
if (err != nil) != tc.Err {
7326-
t.Fatalf("test case %d: %v", i, err)
7327-
}
7328-
7329-
require.Equal(out, tc.Parsed)
7330-
}
7331-
}
7332-
73337286
func TestMultiregion_CopyCanonicalize(t *testing.T) {
73347287
ci.Parallel(t)
73357288

0 commit comments

Comments
 (0)