Skip to content

Commit b2b0f29

Browse files
committed
Automatic offloading target discovery
1 parent 32a8933 commit b2b0f29

10 files changed

Lines changed: 153 additions & 49 deletions

File tree

docs/registry.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Registry
2+
3+
### Node Registration
4+
5+
Each node is configured to belong to a certain geographical **Area** (e.g.,
6+
an Edge zone in Rome, a Cloud region in London).
7+
At startup, a (likely) unique random **Key** is associated with each node.
8+
Together, area and key represent a node's identifier.
9+
10+
Each node registers itself in Etcd under `registry/<area>/<key>`. The value
11+
associated with this Etcd key is a string `apiPort;udpPort`, which reports the
12+
port numbers used by the node for the API server and the UDP local monitoring
13+
server.
14+
15+
16+
### Load Balancer
17+
18+
Load balancer nodes register themselves under `registry/<area>/lb/<key>`.
19+
20+
21+
22+
23+
24+

internal/api/api.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@ func GetServerStatus(c echo.Context) error {
240240

241241
// TODO: use a different type
242242
response := registration.StatusInformation{
243-
Url: registration.SelfRegistration.RemoteURL,
244243
AvailableWarmContainers: node.WarmStatus(),
245244
AvailableMemMB: node.Resources.AvailableMemMB,
246245
AvailableCPUs: node.Resources.AvailableCPUs,

internal/config/keys.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ const ETCD_ADDRESS = "etcd.address"
77
const API_PORT = "api.port"
88
const API_IP = "api.ip"
99

10-
// REMOTE SERVER URL
11-
const CLOUD_URL = "cloud.server.url"
12-
1310
// Forces runtime container images to be pulled the first time they are used,
1411
// even if they are locally available (true/false).
1512
const FACTORY_REFRESH_IMAGES = "factory.images.refresh"

internal/lb/lb.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func StartReverseProxy(e *echo.Echo, region string) {
4242
}
4343

4444
func getTargets(region string) ([]*middleware.ProxyTarget, error) {
45-
cloudNodes, err := registration.GetAllInArea(region, false)
45+
cloudNodes, err := registration.GetNodesInArea(region, false, 0)
4646
if err != nil {
4747
return nil, err
4848
}
@@ -51,7 +51,7 @@ func getTargets(region string) ([]*middleware.ProxyTarget, error) {
5151
for _, target := range cloudNodes {
5252
log.Printf("Found target: %v\n", target.Key)
5353
// TODO: etcd should NOT contain URLs, but only host and port...
54-
parsedUrl, err := url.Parse(target.RemoteURL)
54+
parsedUrl, err := url.Parse(target.APIUrl())
5555
if err != nil {
5656
return nil, err
5757
}

internal/node/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type NodeID struct {
1919
var LocalNode NodeID
2020

2121
func (n NodeID) String() string {
22-
return fmt.Sprintf("%s/%s", n.Area, n.Key)
22+
return fmt.Sprintf("(%s)%s", n.Area, n.Key)
2323
}
2424

2525
func NewIdentifier(area string) NodeID {

internal/registration/registry.go

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import (
66
"github.com/serverledge-faas/serverledge/internal/node"
77
"golang.org/x/exp/maps"
88
"log"
9-
"net/url"
109
"path"
1110
"sort"
11+
"strconv"
12+
"strings"
1213
"sync"
1314
"time"
1415

@@ -19,14 +20,15 @@ import (
1920
)
2021

2122
const registryBaseDirectory = "registry"
22-
const registryLoadBalancerDirectory = "lb"
23+
const registryLoadBalancerDirectory = "__lb"
2324
const etcdLeaseTTL = 120
2425

2526
var mutex sync.RWMutex
2627

2728
var nearestNeighbors []NodeRegistration
2829
var neighborInfo map[string]*StatusInformation
2930
var neighbors map[string]NodeRegistration
31+
var remoteOffloadingTarget NodeRegistration
3032

3133
var VivaldiClient *vivaldi.Client
3234
var SelfRegistration *NodeRegistration
@@ -42,6 +44,10 @@ func (r *NodeRegistration) toEtcdKey() (key string) {
4244
}
4345
}
4446

47+
func (r *NodeRegistration) APIUrl() (url string) {
48+
return fmt.Sprintf("http://%s:%d", r.IPAddress, r.APIPort)
49+
}
50+
4551
func areaEtcdKey(area string) string {
4652
return fmt.Sprintf("%s/%s/", registryBaseDirectory, area)
4753
}
@@ -71,14 +77,17 @@ func registerToEtcd(asLoadBalancer bool) error {
7177
etcdLease = resp.ID
7278

7379
registeredLocalIP := config.GetString(config.API_IP, defaultAddressStr)
74-
hostport := fmt.Sprintf("http://%s:%d", registeredLocalIP, config.GetInt(config.API_PORT, 1323))
80+
apiPort := config.GetInt(config.API_PORT, 1323)
81+
udpPort := config.GetInt(config.LISTEN_UDP_PORT, 9876)
7582

76-
SelfRegistration = &NodeRegistration{NodeID: node.LocalNode, IPAddress: registeredLocalIP, RemoteURL: hostport, IsLoadBalancer: asLoadBalancer}
83+
payload := fmt.Sprintf("%d;%d", apiPort, udpPort)
84+
85+
SelfRegistration = &NodeRegistration{NodeID: node.LocalNode, IPAddress: registeredLocalIP, APIPort: apiPort, UDPPort: udpPort, IsLoadBalancer: asLoadBalancer}
7786

7887
// save couple (id, hostport) to the correct Area-dir on etcd
7988
etcdKey := SelfRegistration.toEtcdKey()
8089
log.Printf("Registering to etcd: %s\n", etcdKey)
81-
_, err = etcdClient.Put(ctx, etcdKey, hostport, clientv3.WithLease(etcdLease))
90+
_, err = etcdClient.Put(ctx, etcdKey, payload, clientv3.WithLease(etcdLease))
8291
if err != nil {
8392
log.Fatal(IdRegistrationErr)
8493
return IdRegistrationErr
@@ -109,36 +118,73 @@ func keepAliveLease() {
109118
}
110119
}
111120

112-
// GetAllInArea is used to obtain the list of other server's addresses under a specific local Area
113-
func GetAllInArea(area string, includeSelf bool) (map[string]NodeRegistration, error) {
121+
func parseEtcdRegisteredNode(area string, key string, payload []byte) (NodeRegistration, error) {
122+
payloadStr := string(payload)
123+
split := strings.Split(payloadStr, ";")
124+
if len(split) < 2 {
125+
return NodeRegistration{}, fmt.Errorf("invalid payload: %s", payloadStr)
126+
}
127+
128+
apiPort, err := strconv.Atoi(split[0])
129+
if err != nil {
130+
return NodeRegistration{}, err
131+
}
132+
133+
udpPort, err := strconv.Atoi(split[1])
134+
if err != nil {
135+
return NodeRegistration{}, err
136+
}
137+
138+
return NodeRegistration{NodeID: node.NodeID{Area: area, Key: key}, APIPort: apiPort, UDPPort: udpPort}, nil
139+
}
140+
141+
// GetNodesInArea is used to obtain the list of other server's addresses under a specific local Area
142+
func GetNodesInArea(area string, includeSelf bool, limit int64) (map[string]NodeRegistration, error) {
114143
baseDir := areaEtcdKey(area)
144+
lbPrefix := path.Join(baseDir, registryLoadBalancerDirectory)
115145

116146
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
117147

118-
resp, err := etcdClient.Get(ctx, baseDir, clientv3.WithPrefix())
148+
if limit < 0 {
149+
limit = 0 // no limit
150+
}
151+
resp, err := etcdClient.Get(ctx, baseDir, clientv3.WithPrefix(), clientv3.WithLimit(limit))
119152
if err != nil {
120153
return nil, fmt.Errorf("Could not read from etcd: %v", err)
121154
}
122155

123156
servers := make(map[string]NodeRegistration)
124157
for _, s := range resp.Kvs {
125-
key := path.Base(string(s.Key))
126-
if key == registryLoadBalancerDirectory {
127-
// TODO: is this check needed?
158+
if strings.HasPrefix(string(s.Key), lbPrefix) {
159+
// skip LB
128160
continue
129161
}
162+
key := path.Base(string(s.Key))
130163
if !includeSelf && area == SelfRegistration.Area && key == SelfRegistration.Key {
131164
continue
132165
}
133166

134-
remoteURL := string(s.Value)
135-
servers[key] = NodeRegistration{NodeID: node.NodeID{Area: area, Key: key}, RemoteURL: remoteURL}
136-
fmt.Printf("Server found: %v\n", servers[key])
167+
reg, err := parseEtcdRegisteredNode(area, key, s.Value)
168+
if err == nil {
169+
servers[key] = reg
170+
fmt.Printf("Server found: %v\n", servers[key])
171+
}
137172
}
138173

139174
return servers, nil
140175
}
141176

177+
func GetOneNodeInArea(area string, includeSelf bool) (NodeRegistration, error) {
178+
nodes, err := GetNodesInArea(area, includeSelf, 1)
179+
if err == nil {
180+
for _, n := range nodes {
181+
return n, nil
182+
}
183+
}
184+
185+
return NodeRegistration{}, err
186+
}
187+
142188
func GetLBInArea(area string) (map[string]NodeRegistration, error) {
143189
baseDir := areaEtcdKey(area) + "/" + registryLoadBalancerDirectory
144190

@@ -152,26 +198,23 @@ func GetLBInArea(area string) (map[string]NodeRegistration, error) {
152198
servers := make(map[string]NodeRegistration)
153199
for _, s := range resp.Kvs {
154200
key := path.Base(string(s.Key))
155-
remoteURL := string(s.Value)
156-
servers[key] = NodeRegistration{NodeID: node.NodeID{Area: area, Key: key}, RemoteURL: remoteURL, IsLoadBalancer: true}
157-
fmt.Printf("Server found: %v\n", servers[key])
201+
reg, err := parseEtcdRegisteredNode(area, key, s.Value)
202+
if err == nil {
203+
reg.IsLoadBalancer = true
204+
servers[key] = reg
205+
fmt.Printf("Server found: %v\n", servers[key])
206+
}
158207
}
159208

160209
return servers, nil
161210
}
162211

163212
// Deregister deletes from etcd the key, value pair previously inserted
164213
func Deregister() (e error) {
165-
etcdClient, err := utils.GetEtcdClient()
166-
if err != nil {
167-
log.Fatal(UnavailableClientErr)
168-
return UnavailableClientErr
169-
}
170-
171214
ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
172-
_, err = etcdClient.Revoke(ctx, etcdLease)
215+
_, err := etcdClient.Revoke(ctx, etcdLease)
173216
if err != nil {
174-
return err
217+
log.Printf("Error revoking lease: %v", err)
175218
}
176219

177220
return nil
@@ -217,7 +260,7 @@ func runMonitor() {
217260
func globalMonitoring() {
218261

219262
// gets info from Etcd about other nodes in the area
220-
newNeighbors, err := GetAllInArea(SelfRegistration.Area, false)
263+
newNeighbors, err := GetNodesInArea(SelfRegistration.Area, false, 0)
221264
if err != nil {
222265
log.Println(err)
223266
return
@@ -235,6 +278,38 @@ func globalMonitoring() {
235278
delete(neighborInfo, key)
236279
}
237280
}
281+
282+
updateRemoteOffloadingTarget()
283+
}
284+
285+
func updateRemoteOffloadingTarget() {
286+
// If there is a LB in the remote area, it is used.
287+
// Otherwise, a random node in the area is chosen.
288+
289+
remoteArea := config.GetString(config.REGISTRY_REMOTE_AREA, "")
290+
if remoteArea == "" {
291+
log.Printf("No remote area is configured; vertical offloading disabled")
292+
remoteOffloadingTarget = NodeRegistration{}
293+
return
294+
}
295+
296+
lbs, err := GetLBInArea(remoteArea)
297+
if err != nil {
298+
log.Println(err)
299+
}
300+
if err == nil && len(lbs) > 0 {
301+
for _, lb := range lbs {
302+
log.Printf("Using LB as offloading target: %v", lb.NodeID)
303+
remoteOffloadingTarget = lb
304+
return
305+
}
306+
}
307+
308+
remoteNode, err := GetOneNodeInArea(remoteArea, false)
309+
if err == nil {
310+
log.Printf("Using as offloading target: %v", remoteNode.NodeID)
311+
remoteOffloadingTarget = remoteNode
312+
}
238313
}
239314

240315
// computeNearestNeighbors finds servers nearby to the current one
@@ -278,22 +353,17 @@ func nearbyMonitoring(vivaldiClient *vivaldi.Client) {
278353
mutex.RUnlock()
279354

280355
for _, registeredNode := range peersToUpdate {
281-
u, err := url.Parse(registeredNode.RemoteURL)
282-
if err != nil {
283-
panic(err)
284-
}
285-
hostname := u.Hostname()
286-
newInfo, rtt := statusInfoRequest(hostname)
356+
newInfo, rtt := statusInfoRequest(&registeredNode)
287357

288358
if newInfo == nil {
289-
log.Printf("Unreachable neighbor: %s\n", registeredNode.RemoteURL)
359+
log.Printf("Unreachable neighbor: %s\n", registeredNode.NodeID)
290360
continue
291361
}
292362

293363
mutex.Lock()
294364
neighborInfo[registeredNode.Key] = newInfo
295365

296-
_, err = vivaldiClient.Update("node", &newInfo.Coordinates, rtt)
366+
_, err := vivaldiClient.Update("node", &newInfo.Coordinates, rtt)
297367
if err != nil {
298368
log.Printf("Error while updating node coordinates: %s\n", err)
299369
}
@@ -308,6 +378,13 @@ func GetNearestNeighbors() []NodeRegistration {
308378
return nearestNeighbors
309379
}
310380

381+
func GetRemoteOffloadingTarget() *NodeRegistration {
382+
if remoteOffloadingTarget.Key != "" {
383+
return &remoteOffloadingTarget
384+
}
385+
return nil
386+
}
387+
311388
func GetFullNeighborInfo() map[string]*StatusInformation {
312389
mutex.RLock()
313390
defer mutex.RUnlock()

internal/registration/types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ var IdRegistrationErr = errors.New("etcd error: could not complete the registrat
1212
type NodeRegistration struct {
1313
node.NodeID
1414
IPAddress string
15-
RemoteURL string
15+
APIPort int
16+
UDPPort int
1617
IsLoadBalancer bool
1718
}
1819

1920
type StatusInformation struct {
20-
Url string
2121
AvailableWarmContainers map[string]int // <k, v> = <function name, warm container number>
2222
AvailableMemMB int64
2323
AvailableCPUs float64

internal/registration/udp.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ func handleUDPConnection(conn *net.UDPConn) {
6969
// TODO: this function should reuse the code in api.go for the /status API
7070
func getCurrentStatusInformation() (status []byte, err error) {
7171
response := StatusInformation{
72-
Url: SelfRegistration.RemoteURL,
7372
AvailableWarmContainers: node.WarmStatus(),
7473
AvailableMemMB: node.Resources.AvailableMemMB,
7574
AvailableCPUs: node.Resources.AvailableCPUs,
@@ -80,8 +79,10 @@ func getCurrentStatusInformation() (status []byte, err error) {
8079

8180
}
8281

83-
func statusInfoRequest(hostname string) (info *StatusInformation, duration time.Duration) {
84-
port := config.GetInt(config.LISTEN_UDP_PORT, 9876)
82+
func statusInfoRequest(peer *NodeRegistration) (info *StatusInformation, duration time.Duration) {
83+
84+
hostname := peer.IPAddress
85+
port := peer.UDPPort
8586
address := fmt.Sprintf("%s:%d", hostname, port)
8687

8788
remoteAddr, err := net.ResolveUDPAddr("udp", address)

internal/scheduling/offloading.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func pickEdgeNodeForOffloading(r *scheduledRequest) (url string) {
2727
}
2828

2929
randomItem := nearestNeighbors[rand.Intn(len(nearestNeighbors))]
30-
return randomItem.RemoteURL
30+
return randomItem.APIUrl()
3131
}
3232

3333
func Offload(r *function.Request, serverUrl string) (function.ExecutionReport, error) {

0 commit comments

Comments
 (0)