Skip to content

Commit c7779d9

Browse files
authored
Check for port collisions before telling Envoy (#64)
* Assign timestamp properly * Fix typo in comment
1 parent e549d66 commit c7779d9

File tree

2 files changed

+103
-7
lines changed

2 files changed

+103
-7
lines changed

envoy/adapter/adapter.go

+57-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net"
66
"strconv"
77
"strings"
8+
"time"
89

910
"github.com/NinesStack/sidecar/catalog"
1011
"github.com/NinesStack/sidecar/service"
@@ -26,8 +27,18 @@ import (
2627
)
2728

2829
const (
29-
// ServiceNameSeparator is used to join service name and port. Must not occur in service names.
30+
31+
// ServiceNameSeparator is used to join service name and port. Must not
32+
// occur in service names.
3033
ServiceNameSeparator = ":"
34+
35+
// PortCollisionLoggingBackoff is how long we wait between logging about
36+
// port collisions.
37+
PortCollisionLoggingBackoff = 1 * time.Minute
38+
)
39+
40+
var (
41+
LastLoggedPortCollision time.Time
3142
)
3243

3344
// EnvoyResources is a collection of Enovy API resource definitions
@@ -58,8 +69,8 @@ func SvcNameSplit(name string) (string, int64, error) {
5869
return svcName, svcPort, nil
5970
}
6071

61-
// LookupHost does a vv slow lookup of the DNS host for a service. Totally
62-
// not optimized for high throughput. You should only do this in development
72+
// LookupHost does a vv slow lookup of the DNS host for a service. Totally not
73+
// optimized for high throughput. You should only do this in development
6374
// scenarios.
6475
func LookupHost(hostname string) (string, error) {
6576
addrs, err := net.LookupHost(hostname)
@@ -70,17 +81,43 @@ func LookupHost(hostname string) (string, error) {
7081
return addrs[0], nil
7182
}
7283

73-
// EnvoyResourcesFromState creates a set of Enovy API resource definitions from all
74-
// the ServicePorts in the Sidecar state. The Sidecar state needs to be locked by the
75-
// caller before calling this function.
84+
// isPortCollision will make sure we don't tell Envoy about more than one
85+
// service on the same port. This leads to it going completely apeshit both
86+
// with CPU usage and logging.
87+
func isPortCollision(portsMap map[int64]string, svc *service.Service, port service.Port) bool {
88+
registeredName, ok := portsMap[port.ServicePort]
89+
// See if we already know about this port
90+
if ok {
91+
// If it is the same service, then no collision
92+
if registeredName == svc.Name {
93+
return false
94+
}
95+
96+
// Uh, oh, this is not the service assigned to this port
97+
return true
98+
}
99+
100+
// We don't know about it, so assign it.
101+
portsMap[port.ServicePort] = svc.Name
102+
return false
103+
}
104+
105+
// EnvoyResourcesFromState creates a set of Enovy API resource definitions from
106+
// all the ServicePorts in the Sidecar state. The Sidecar state needs to be
107+
// locked by the caller before calling this function.
76108
func EnvoyResourcesFromState(state *catalog.ServicesState, bindIP string,
77109
useHostnames bool) EnvoyResources {
78110

79111
endpointMap := make(map[string]*api.ClusterLoadAssignment)
80112
clusterMap := make(map[string]*api.Cluster)
81113
listenerMap := make(map[string]cache_types.Resource)
82114

83-
state.EachService(func(hostname *string, id *string, svc *service.Service) {
115+
// Used to make sure we don't map the same port to more than one service
116+
portsMap := make(map[int64]string)
117+
118+
// We use the more expensive EachServiceSorted to make sure we make a stable
119+
// port mapping allocation in the event of port collisions.
120+
state.EachServiceSorted(func(hostname *string, id *string, svc *service.Service) {
84121
if svc == nil || !svc.IsAlive() {
85122
return
86123
}
@@ -92,6 +129,19 @@ func EnvoyResourcesFromState(state *catalog.ServicesState, bindIP string,
92129
continue
93130
}
94131

132+
// Make sure we don't make Envoy go nuts by reporting the same port twice
133+
if isPortCollision(portsMap, svc, port) {
134+
// This happens A LOT when it happens, so let's back off to once a minute-ish
135+
if time.Now().UTC().Sub(LastLoggedPortCollision) > PortCollisionLoggingBackoff {
136+
log.Warnf(
137+
"Port collision! %s is attempting to squat on port %d owned by %s",
138+
svc.Name, port.ServicePort, portsMap[port.ServicePort],
139+
)
140+
LastLoggedPortCollision = time.Now().UTC()
141+
}
142+
continue
143+
}
144+
95145
envoyServiceName := SvcName(svc.Name, port.ServicePort)
96146

97147
if assignment, ok := endpointMap[envoyServiceName]; ok {

envoy/adapter/adapter_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package adapter
2+
3+
import (
4+
"testing"
5+
6+
"github.com/NinesStack/sidecar/service"
7+
. "github.com/smartystreets/goconvey/convey"
8+
)
9+
10+
func Test_isPortCollision(t *testing.T) {
11+
Convey("isPortCollision()", t, func() {
12+
portsMap := map[int64]string{
13+
int64(10001): "beowulf",
14+
int64(10002): "grendel",
15+
}
16+
17+
Convey("returns true when the port is a different service", func() {
18+
svc := &service.Service{Name: "hrothgar"}
19+
port := service.Port{ServicePort: int64(10001)}
20+
21+
result := isPortCollision(portsMap, svc, port)
22+
23+
So(result, ShouldBeTrue)
24+
So(portsMap[int64(10001)], ShouldEqual, "beowulf")
25+
})
26+
27+
Convey("returns false when the port is the same service", func() {
28+
svc := &service.Service{Name: "beowulf"}
29+
port := service.Port{ServicePort: int64(10001)}
30+
31+
result := isPortCollision(portsMap, svc, port)
32+
33+
So(result, ShouldBeFalse)
34+
})
35+
36+
Convey("returns false and assigns it when the port is not assigned", func() {
37+
svc := &service.Service{Name: "hrothgar"}
38+
port := service.Port{ServicePort: int64(10003)}
39+
40+
result := isPortCollision(portsMap, svc, port)
41+
42+
So(result, ShouldBeFalse)
43+
So(portsMap[int64(10003)], ShouldEqual, "hrothgar")
44+
})
45+
})
46+
}

0 commit comments

Comments
 (0)