Skip to content

Commit

Permalink
Merge pull request #71 from go-faster/feat/faker
Browse files Browse the repository at this point in the history
feat(faker): allocate ip pools in model
  • Loading branch information
ernado authored Jul 20, 2023
2 parents 74aae72 + 879471f commit 6e6a6d7
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 34 deletions.
53 changes: 53 additions & 0 deletions internal/faker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# faker

Faker models a cluster of nodes, where services are deployed, exposing an API that
is used by clients to interact with.

## Overview

```mermaid
graph LR
F[Frontend]-->A[API]
A-->B[Backend]
B-->C[Cache]
B-->D[DB]
```

```mermaid
sequenceDiagram
Frontend->>API: HTTP GET /
API->>Backend: gRPC Get()
Backend->>Cache: INCR counter
Cache-->>Backend: counter
Backend->>DB: SELECT * FROM table
DB-->>Backend: posts (psql)
Backend-->>API: posts (pb)
API-->>Frontend: posts (json)
```

## Services

### Frontend

Represents a web browser.

#### Static
- IP address
- User agent
- Target web domain

#### Dynamic
- External port

### API

Represents a web server that exposes an HTTP API.
Accepts an HTTP request, forwards it to backend via gRPC and
returns the response in json format.

#### Static
- IP address
- Server

#### Dynamic
- External port
4 changes: 4 additions & 0 deletions internal/faker/faker.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Package faker implement a fake telemetry generator.
package faker

import "math/rand"

// Config models a single cluster of multiple nodes, where services are
// deployed on each node.
//
Expand All @@ -14,6 +16,8 @@ type Config struct {
RPS int `json:"rps" yaml:"rps"`
// Services configuration.
Services Services `json:"services" yaml:"services"`
// Random number generator.
Rand *rand.Rand
}

// Services wraps all services configuration, describing topology of the
Expand Down
30 changes: 30 additions & 0 deletions internal/faker/ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package faker

import (
"encoding/binary"
"net/netip"
"sync/atomic"
)

// ipAllocator is sequential ip address allocator without garbage collection.
type ipAllocator struct {
offset uint32
}

// Next allocates new ip address. It panics if the address space is exhausted.
// It is safe for concurrent use.
func (a *ipAllocator) Next() netip.Addr {
data := binary.BigEndian.AppendUint32(nil, atomic.AddUint32(&a.offset, 1))
ip, ok := netip.AddrFromSlice(data)
if !ok {
panic("ip address overflow")
}
return ip
}

func newIPAllocator(ip netip.Addr) *ipAllocator {
addr := binary.BigEndian.Uint32(ip.AsSlice())
return &ipAllocator{
offset: addr,
}
}
19 changes: 19 additions & 0 deletions internal/faker/ip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package faker

import (
"net/netip"
"testing"
)

func TestIPAllocator_Next(t *testing.T) {
allocator := newIPAllocator(netip.MustParseAddr("10.0.5.0"))
seen := make(map[netip.Addr]struct{})
for i := 0; i < 1600; i++ {
ip := allocator.Next()
if _, ok := seen[ip]; ok {
t.Fatalf("duplicate ip address: %s", ip)
}
seen[ip] = struct{}{}
}
t.Logf("%s", allocator.Next())
}
124 changes: 91 additions & 33 deletions internal/faker/model.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package faker

import (
"math/rand"
"net/netip"
"strconv"
)
Expand All @@ -10,6 +11,10 @@ type cluster struct {
servers []server
}

func (c *cluster) getRandomServer(source *rand.Rand) int {
return source.Intn(len(c.servers))
}

func (c *cluster) addServer(s server) {
c.servers = append(c.servers, s)
}
Expand All @@ -21,6 +26,7 @@ type service interface {
type server struct {
name string // hostname
ip netip.Addr // address
id int // unique id
services []service
}

Expand All @@ -29,76 +35,128 @@ func (s *server) addService(service service) {
}

type model struct {
rps int
cluster cluster
rps int
cluster cluster
frontends []frontendService
rand *rand.Rand
}

type apiService struct {
replica int
id int
ip netip.Addr
port int
}

func (s apiService) Name() string { return "api" }

type dbService struct {
replica int
id int
ip netip.Addr
port int
}

func (s dbService) Name() string { return "db" }

type cacheService struct {
replica int
id int
ip netip.Addr
port int
}

func (s cacheService) Name() string { return "cache" }

type backendService struct {
replica int
id int
ip netip.Addr
port int
}

func (s backendService) Name() string { return "backend" }

type frontendService struct {
replica int
id int
ip netip.Addr
}

func (s frontendService) Name() string { return "frontend" }

func modelFromConfig(c Config) model {
m := model{
rps: c.RPS,
rps: c.RPS,
rand: c.Rand,
}
m.cluster.name = "msk1"

// Generate clients.
residentialPool := newIPAllocator(netip.MustParseAddr("95.24.0.0"))
for i := 0; i < c.Services.Frontend.Replicas; i++ {
m.frontends = append(m.frontends, frontendService{
id: i,
ip: residentialPool.Next(),
})
}

// Pool of external IP addresses.
serverPool := newIPAllocator(netip.MustParseAddr("103.21.244.0"))
for i := 0; i < c.Nodes; i++ {
s := server{
m.cluster.addServer(server{
name: "node-" + strconv.Itoa(i),
ip: netip.MustParseAddr("192.168.0." + strconv.Itoa(i)),
}
for j := 0; j < c.Services.API.Replicas; j++ {
s.addService(apiService{
replica: j,
})
}
for j := 0; j < c.Services.Backend.Replicas; j++ {
s.addService(backendService{
replica: j,
})
ip: serverPool.Next(),
id: i,
})
}

// Distribute HTTP API.
for i := 0; i < c.Services.API.Replicas; i++ {
// Select random node.
j := m.cluster.getRandomServer(m.rand)
// Using note IP address as being exposed on node 80 port.
s := apiService{
id: i,
ip: m.cluster.servers[j].ip,
port: 80,
}
for j := 0; j < c.Services.DB.Replicas; j++ {
s.addService(dbService{
replica: j,
})
m.cluster.servers[j].addService(s)
}

// Distribute internal services.
pool := newIPAllocator(netip.MustParseAddr("10.43.0.0"))

// Distribute Backend.
for i := 0; i < c.Services.Backend.Replicas; i++ {
s := backendService{
id: i,
ip: pool.Next(),
port: 8080,
}
for j := 0; j < c.Services.Cache.Replicas; j++ {
s.addService(cacheService{
replica: j,
})
// Select random node.
j := m.cluster.getRandomServer(m.rand)
m.cluster.servers[j].addService(s)
}

// Distribute DB.
for i := 0; i < c.Services.DB.Replicas; i++ {
s := dbService{
id: i,
ip: pool.Next(),
port: 5432,
}
for j := 0; j < c.Services.Frontend.Replicas; j++ {
s.addService(frontendService{
replica: j,
})
// Select random node.
j := m.cluster.getRandomServer(m.rand)
m.cluster.servers[j].addService(s)
}

// Distribute Cache.
for i := 0; i < c.Services.Cache.Replicas; i++ {
s := cacheService{
id: i,
ip: pool.Next(),
port: 6379,
}
m.cluster.addServer(s)
// Select random node.
j := m.cluster.getRandomServer(m.rand)
m.cluster.servers[j].addService(s)
}

return m
}
10 changes: 9 additions & 1 deletion internal/faker/model_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package faker

import (
"math/rand"
"testing"

"github.com/stretchr/testify/assert"
)

func TestModel(t *testing.T) {
m := modelFromConfig(Config{
Rand: rand.New(rand.NewSource(42)),
Nodes: 10,
RPS: 1000,
Services: Services{
Expand All @@ -30,5 +32,11 @@ func TestModel(t *testing.T) {
})
assert.Equal(t, 10, len(m.cluster.servers))
assert.Equal(t, 1000, m.rps)
assert.Equal(t, 12, len(m.cluster.servers[0].services))
assert.Equal(t, 3, len(m.frontends))

var services int
for _, s := range m.cluster.servers {
services += len(s.services)
}
assert.Equal(t, 9, services)
}
5 changes: 5 additions & 0 deletions internal/faker/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package faker

// request models full request trip from client to backend and back.
type request struct {
}

0 comments on commit 6e6a6d7

Please sign in to comment.