mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Test harness to assess whether fnlb works properly (#573)
* Initial commit. * Update README.md * Update README.md * Update README.md. * Update README.md * Changes from PR code review.
This commit is contained in:
88
test/fnlb-test-harness/README.md
Normal file
88
test/fnlb-test-harness/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# fnlb-test-harness
|
||||||
|
Test harness that exercises the fnlb load balancer in order to verify that it works properly.
|
||||||
|
## How it works
|
||||||
|
This is a test harness that makes calls to an IronFunctions route through the fnlb load balancer, which routes traffic to multiple IronFunctions nodes.
|
||||||
|
The test harness keeps track of which node each request was routed to so we can assess how the requests are being distributed across the nodes. The functionality
|
||||||
|
of fnlb is to normally route traffic to the same small number of nodes so that efficiences can be achieved and to support reuse of hot functions.
|
||||||
|
### Primes function
|
||||||
|
The test harness utilizes the "primes" function, which calculates prime numbers as an excuse for consuming CPU resources. The function is invoked as follows:
|
||||||
|
```
|
||||||
|
curl http://host:8080/r/primesapp/primes?max=1000000&loops=1
|
||||||
|
```
|
||||||
|
where:
|
||||||
|
- *max*: calculate all primes <= max (increasing max will increase memory usage, due to the Sieve of Eratosthenes algorithm)
|
||||||
|
- *loops*: number of times to calculate the primes (repeating the count consumes additional CPU without consuming additional memory)
|
||||||
|
|
||||||
|
## How to use it
|
||||||
|
The test harness requires running one or more IronFunctions nodes and one instance of fnlb. The list of nodes must be provided both to fnlb and to the test harness
|
||||||
|
because the test harness must call each node directly one time in order to discover the node's container id.
|
||||||
|
|
||||||
|
After it has run, examine the results to see how the requests were distributed across the nodes.
|
||||||
|
### How to run it locally
|
||||||
|
Each of the IronFunctions nodes needs to connect to the same database.
|
||||||
|
|
||||||
|
STEP 1: Create a route for the primes function. Example:
|
||||||
|
```
|
||||||
|
fn apps create primesapp
|
||||||
|
fn routes create primesapp /primes jconning/primes:0.0.1
|
||||||
|
```
|
||||||
|
STEP 2: Run five IronFunctions nodes locally. Example (runs five nodes in the background using Docker):
|
||||||
|
```
|
||||||
|
sudo docker run -d -it --name functions-8082 --privileged -v ${HOME}/data-8082:/app/data -p 8082:8080 -e "DB_URL=postgres://dbUser:dbPassword@dbHost:5432/dbName" iron/functions
|
||||||
|
sudo docker run -d -it --name functions-8083 --privileged -v ${HOME}/data-8083:/app/data -p 8083:8080 -e "DB_URL=postgres://dbUser:dbPassword@dbHost:5432/dbName" iron/functions
|
||||||
|
sudo docker run -d -it --name functions-8084 --privileged -v ${HOME}/data-8084:/app/data -p 8084:8080 -e "DB_URL=postgres://dbUser:dbPassword@dbHost:5432/dbName" iron/functions
|
||||||
|
sudo docker run -d -it --name functions-8085 --privileged -v ${HOME}/data-8085:/app/data -p 8085:8080 -e "DB_URL=postgres://dbUser:dbPassword@dbHost:5432/dbName" iron/functions
|
||||||
|
sudo docker run -d -it --name functions-8086 --privileged -v ${HOME}/data-8086:/app/data -p 8086:8080 -e "DB_URL=postgres://dbUser:dbPassword@dbHost:5432/dbName" iron/functions
|
||||||
|
```
|
||||||
|
STEP 3: Run fnlb locally. Example (runs fnlb on the default port 8081):
|
||||||
|
```
|
||||||
|
fnlb -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086
|
||||||
|
```
|
||||||
|
STEP 4: Run the test harness. Note that the 'nodes' parameter should be the same that was used with fnlb. Example:
|
||||||
|
```
|
||||||
|
cd functions/test/fnlb-test-harness
|
||||||
|
go run main.go -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086 -calls 10 -v
|
||||||
|
```
|
||||||
|
STEP 5: Examine the output to determine how many times fnlb called each node. Assess whether it is working properly.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
go run main.go -help
|
||||||
|
|
||||||
|
<i>Command line parameters:</i>
|
||||||
|
- *-calls*: number of times to call the route (default 100)
|
||||||
|
- *-lb*: host and port of load balancer (default "localhost:8081")
|
||||||
|
- *-loops*: number of times to execute the primes calculation (ex: '-loops 2' means run the primes calculation twice) (default 1)
|
||||||
|
- *-max*: maximum number to search for primes (higher number consumes more memory) (default 1000000)
|
||||||
|
- *-nodes*: comma-delimited list of nodes (host:port) balanced by the load balancer (needed to discover container id of each) (default "localhost:8080")
|
||||||
|
- *-route*: path representing the route to the primes function (default "/r/primesapp/primes")
|
||||||
|
- *-v*: flag indicating verbose output
|
||||||
|
|
||||||
|
### Examples: quick vs long running
|
||||||
|
|
||||||
|
**Quick function:**: calculate primes up to 1000
|
||||||
|
```
|
||||||
|
go run main.go -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086 -max 1000 -v
|
||||||
|
```
|
||||||
|
where *-max* is default of 1M, *-calls* is default of 100, *-route* is default of "/r/primesapp/primes", *-lb* is default localhost:8081
|
||||||
|
|
||||||
|
**Normal function**: calculate primes up to 1M
|
||||||
|
```
|
||||||
|
go run main.go -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086 -v
|
||||||
|
```
|
||||||
|
where *-max* is default of 1M, *-calls* is default of 100, *-route* is default of "/r/primesapp/primes", *-lb* is default localhost:8081
|
||||||
|
|
||||||
|
**Longer running function**: calculate primes up to 1M and perform the calculation ten times
|
||||||
|
```
|
||||||
|
go run main.go -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086 -loops 10 -v
|
||||||
|
```
|
||||||
|
where *-max* is default of 1M, *-calls* is default of 100, *-route* is default of "/r/primesapp/primes", *-lb* is default localhost:8081
|
||||||
|
|
||||||
|
**1000 calls to the route**: send 1000 requests through the load balancer
|
||||||
|
```
|
||||||
|
go run main.go -nodes localhost:8082,localhost:8083,localhost:8084,localhost:8085,localhost:8086 -calls 1000 -v
|
||||||
|
```
|
||||||
|
where *-max* is default of 1M, *-calls* is default of 100, *-route* is default of "/r/primesapp/primes", *-lb* is default localhost:8081
|
||||||
|
|
||||||
|
## Planned Enhancements
|
||||||
|
- Create 1000 routes and distribute calls amongst them.
|
||||||
|
- Use concurrent programming to enable the test harness to call multiple routes at the same time.
|
||||||
127
test/fnlb-test-harness/main.go
Normal file
127
test/fnlb-test-harness/main.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"io/ioutil"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execution struct {
|
||||||
|
DurationSeconds float64
|
||||||
|
Hostname string
|
||||||
|
node string
|
||||||
|
body string
|
||||||
|
responseSeconds float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lbHostPort, nodesStr, route string
|
||||||
|
numExecutions, maxPrime, numLoops int
|
||||||
|
nodes []string
|
||||||
|
nodesByContainerId map[string]string = make(map[string]string)
|
||||||
|
verbose bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&lbHostPort, "lb", "localhost:8081", "host and port of load balancer")
|
||||||
|
flag.StringVar(&nodesStr, "nodes", "localhost:8080", "comma-delimited list of nodes (host:port) balanced by the load balancer (needed to discover container id of each)")
|
||||||
|
flag.StringVar(&route, "route", "/r/primesapp/primes", "path representing the route to the primes function")
|
||||||
|
flag.IntVar(&numExecutions, "calls", 100, "number of times to call the route")
|
||||||
|
flag.IntVar(&maxPrime, "max", 1000000, "maximum number to search for primes (higher number consumes more memory)")
|
||||||
|
flag.IntVar(&numLoops, "loops", 1, "number of times to execute the primes calculation (ex: 'loops=2' means run the primes calculation twice)")
|
||||||
|
flag.BoolVar(&verbose, "v", false, "true for more verbose output")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if maxPrime < 3 {
|
||||||
|
log.Fatal("-max must be 3 or greater")
|
||||||
|
}
|
||||||
|
if numLoops < 1 {
|
||||||
|
log.Fatal("-loops must be 1 or greater")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = strings.Split(nodesStr, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeFunction(hostPort, path string, max, loops int) (execution, error) {
|
||||||
|
var e execution
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := http.Get(fmt.Sprintf("http://%s%s?max=%d&loops=%d", hostPort, path, max, loops))
|
||||||
|
e.responseSeconds = time.Since(start).Seconds()
|
||||||
|
if err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return e, fmt.Errorf("function returned status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &e)
|
||||||
|
if err != nil {
|
||||||
|
e.body = string(body) // set the body in the execution so that it is available for logging
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
e.node = nodesByContainerId[e.Hostname]
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeLoadBalancer(hostPort, path string, numExecutions, max, loops int) {
|
||||||
|
executionsByNode := make(map[string][]execution)
|
||||||
|
fmt.Printf("All primes will be calculated up to %d, a total of %d time(s)\n", maxPrime, numLoops)
|
||||||
|
fmt.Printf("Calling route %s %d times (through the load balancer)...\n", route, numExecutions)
|
||||||
|
|
||||||
|
for i := 0; i < numExecutions; i++ {
|
||||||
|
e, err := executeFunction(hostPort, path, max, loops)
|
||||||
|
if err == nil {
|
||||||
|
if ex, ok := executionsByNode[e.node]; ok {
|
||||||
|
executionsByNode[e.node] = append(ex, e)
|
||||||
|
} else {
|
||||||
|
// Create a slice to contain the list of executions for this host
|
||||||
|
executionsByNode[e.node] = []execution{e}
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf(" %s in-function duration: %fsec, response time: %fsec\n", e.node, e.DurationSeconds, e.responseSeconds)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Ignoring failed execution on node %s: %v\n", e.node, err)
|
||||||
|
fmt.Printf(" JSON: %s\n", e.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Results (executions per node):")
|
||||||
|
for node, ex := range executionsByNode {
|
||||||
|
fmt.Printf(" %s %d\n", node, len(ex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverContainerIds() {
|
||||||
|
// Discover the Docker hostname of each node; create a mapping of hostnames to host/port.
|
||||||
|
// This is needed because IronFunctions doesn't make the host/port available to the function (as of Mar 2017).
|
||||||
|
fmt.Println("Discovering container ids for every node (use Docker's HOSTNAME env var as a container id)...")
|
||||||
|
for _, s := range nodes {
|
||||||
|
if e, err := executeFunction(s, route, 100, 1); err == nil {
|
||||||
|
nodesByContainerId[e.Hostname] = s
|
||||||
|
fmt.Printf(" %s %s\n", s, e.Hostname)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Ignoring host %s which returned error: %v\n", s, err)
|
||||||
|
fmt.Printf(" JSON: %s\n", e.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
discoverContainerIds()
|
||||||
|
invokeLoadBalancer(lbHostPort, route, numExecutions, maxPrime, numLoops)
|
||||||
|
}
|
||||||
|
|
||||||
59
test/fnlb-test-harness/primes-func/func.go
Normal file
59
test/fnlb-test-harness/primes-func/func.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// return list of primes less than N
|
||||||
|
// source: http://stackoverflow.com/a/21923233
|
||||||
|
func sieveOfEratosthenes(N int) (primes []int) {
|
||||||
|
b := make([]bool, N)
|
||||||
|
for i := 2; i < N; i++ {
|
||||||
|
if b[i] == true {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
primes = append(primes, i)
|
||||||
|
for k := i * i; k < N; k += i {
|
||||||
|
b[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
start := time.Now()
|
||||||
|
maxPrime := 1000000
|
||||||
|
numLoops := 1
|
||||||
|
|
||||||
|
// Parse the query string
|
||||||
|
s := strings.Split(os.Getenv("REQUEST_URL"), "?")
|
||||||
|
if len(s) > 1 {
|
||||||
|
for _, pair := range strings.Split(s[1], "&") {
|
||||||
|
kv := strings.Split(pair, "=")
|
||||||
|
if len(kv) > 1 {
|
||||||
|
key, value := kv[0], kv[1]
|
||||||
|
if key == "max" {
|
||||||
|
maxPrime, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
if key == "loops" {
|
||||||
|
numLoops, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat the calculation of primes simply to give the CPU more work to do without consuming additional memory
|
||||||
|
for i := 0; i < numLoops; i++ {
|
||||||
|
primes := sieveOfEratosthenes(maxPrime)
|
||||||
|
_ = primes
|
||||||
|
if i == numLoops - 1 {
|
||||||
|
//fmt.Printf("Highest three primes: %d %d %d\n", primes[len(primes) - 1], primes[len(primes) - 2], primes[len(primes) - 3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("{\"durationSeconds\": %f, \"hostname\": \"%s\", \"max\": %d, \"loops\": %d}", time.Since(start).Seconds(), os.Getenv("HOSTNAME"), maxPrime, numLoops)
|
||||||
|
}
|
||||||
|
|
||||||
6
test/fnlb-test-harness/primes-func/func.yaml
Normal file
6
test/fnlb-test-harness/primes-func/func.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
name: jconning/primes
|
||||||
|
version: 0.0.1
|
||||||
|
runtime: go
|
||||||
|
entrypoint: ./func
|
||||||
|
path: /primes
|
||||||
|
max_concurrency: 1
|
||||||
Reference in New Issue
Block a user