blob: 9b9fa8ce3e2a76042d1bdd2237ab69c0dbc12896 [file] [log] [blame]
package ultimate_test_harness
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"testing"
"text/tabwriter"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util"
client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
)
const (
UserAgent = "Traffic Router Load Tests"
)
type TRDetails struct {
Hostname string
IPAddresses []string
ClientIP string
ClientIPAddressMap IPAddressMap
Port int
DSHost string
CDNName tc.CDNName
}
type IPAddressMap struct {
Zones []string
Map map[string]tc.CoverageZoneLocation
}
type HTTPBenchmark struct {
Benchmark
ClientIP *string
PathCount int
MaxPathLength int
}
var ClientIPAddress *string
var PathCount *int
var MaxPathLength *int
func init() {
ClientIPAddress = flag.String("ip_address", "", "spoof your client IP address to Traffic Router's geolocation")
PathCount = flag.Int("path_count", 10000, "the number of paths to generate for use in requests to Delivery Services")
MaxPathLength = flag.Int("max_path_length", 100, "the maximum length for each generated path")
}
func getCoverageZoneURL(cdnName tc.CDNName) (string, error) {
snapshot, _, err := TOSession.GetCRConfig(string(cdnName), client.RequestOptions{})
if err != nil {
return "", fmt.Errorf("getting the Snapshot of CDN '%s': %s", cdnName, err.Error())
}
czPollingURLInterface, ok := snapshot.Response.Config[tc.CoverageZonePollingURL]
if !ok {
return "", fmt.Errorf("parameter %s was not found in the Snapshot of CDN '%s'", tc.CoverageZonePollingURL, cdnName)
}
czPollingURL := czPollingURLInterface.(string)
return czPollingURL, nil
}
func getCoverageZoneFile(czPollingURL string) (tc.CoverageZoneFile, error) {
czMap := tc.CoverageZoneFile{}
czMapRequest, err := http.NewRequest("GET", czPollingURL, nil)
if err != nil {
return czMap, fmt.Errorf("creating HTTP request for URL %s: %s", czPollingURL, err.Error())
}
czMapRequest.Header.Set("User-Agent", UserAgent)
httpClient := http.Client{Timeout: time.Duration(TOConfig.TOTimeout) * time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: TOConfig.TOInsecure}}}
czMapResponse, err := httpClient.Do(czMapRequest)
if err != nil {
return czMap, fmt.Errorf("getting Coverage Zone File from URL %s: %s", czPollingURL, err.Error())
}
defer log.Close(czMapResponse.Body, "closing the Coverage Zone File response")
czMapBytes, err := ioutil.ReadAll(czMapResponse.Body)
if err != nil {
return czMap, fmt.Errorf("reading Coverage Zone File bytes: %s", err.Error())
}
if err = json.Unmarshal(czMapBytes, &czMap); err != nil {
return czMap, fmt.Errorf("unmarshalling Coverage Zone Map bytes: %s", err.Error())
}
return czMap, nil
}
func (i *IPAddressMap) buildFromCoverageZoneMap(czMap tc.CoverageZoneFile) error {
i.Zones = make([]string, len(czMap.CoverageZones))
i.Map = map[string]tc.CoverageZoneLocation{}
zoneIndex := 0
for location, networks := range czMap.CoverageZones {
coverageZoneLocation := tc.CoverageZoneLocation{
Network: make([]string, 2*len(networks.Network)),
Network6: make([]string, 2*len(networks.Network6)),
}
for index, ipAddress := range networks.Network {
_, ipNet, err := net.ParseCIDR(ipAddress)
if err != nil {
return fmt.Errorf("parsing IP address %s in CIDR notation: %s", ipAddress, err.Error())
}
coverageZoneLocation.Network[index*2] = util.FirstIP(ipNet).To4().String()
coverageZoneLocation.Network[index*2+1] = util.LastIP(ipNet).To4().String()
}
for index, ipAddress6 := range networks.Network6 {
_, ipNet, err := net.ParseCIDR(ipAddress6)
if err != nil {
return fmt.Errorf("parsing IP address %s in CIDR notation: %s", ipAddress6, err.Error())
}
coverageZoneLocation.Network6[index*2] = util.FirstIP(ipNet).To16().String()
coverageZoneLocation.Network6[index*2+1] = util.LastIP(ipNet).To16().String()
}
i.Map[location] = coverageZoneLocation
i.Zones[zoneIndex] = location
zoneIndex++
}
return nil
}
func buildIPAddressMap(cdnName tc.CDNName) (IPAddressMap, error) {
ipAddressMap := IPAddressMap{}
czPollingURL, err := getCoverageZoneURL(cdnName)
if err != nil {
return ipAddressMap, fmt.Errorf("getting Coverage Zone Polling URL from the Snapshot of CDN '%s': %s", cdnName, err.Error())
}
czMap, err := getCoverageZoneFile(czPollingURL)
if err != nil {
return ipAddressMap, fmt.Errorf("getting Coverage Zone File: %s", err.Error())
}
if err = ipAddressMap.buildFromCoverageZoneMap(czMap); err != nil {
return ipAddressMap, fmt.Errorf("building IP Address Map from Coverage Zone File: %s", err.Error())
}
return ipAddressMap, nil
}
func RunHTTPBenchmarksAgainstTrafficRouters(t *testing.T, benchmark HTTPBenchmark) {
passedTests := 0
failedTests := 0
fmt.Printf("Passing criteria: Routing at least %d requests per second\n", benchmark.RequestsPerSecondThreshold)
writer := tabwriter.NewWriter(os.Stdout, 20, 8, 1, '\t', tabwriter.AlignRight)
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "Traffic Router", "Protocol", "Delivery Service", "Passed?", "Requests Per Second", "Redirects", "Failures")
for trafficRouterIndex, trafficRouter := range benchmark.TrafficRouters {
for ipAddressIndex, ipAddress := range trafficRouter.IPAddresses {
trafficRouterURL := fmt.Sprintf("http://%s:%d/", ipAddress, trafficRouter.Port)
isIPv4 := strings.Contains(ipAddress, ".")
if trafficRouter.ClientIP == "" && trafficRouter.ClientIPAddressMap.Zones != nil {
if benchmark.CoverageZoneLocation != "" {
location := trafficRouter.ClientIPAddressMap.Map[benchmark.CoverageZoneLocation]
trafficRouter.ClientIP = location.GetFirstIPAddressOfType(isIPv4)
}
if trafficRouter.ClientIP == "" {
for _, location := range trafficRouter.ClientIPAddressMap.Map {
trafficRouter.ClientIP = location.GetFirstIPAddressOfType(isIPv4)
if trafficRouter.ClientIP != "" {
break
}
}
}
}
redirects, failures := 0, 0
redirectsChannels := make([]chan int, benchmark.ThreadCount)
failuresChannels := make([]chan int, benchmark.ThreadCount)
for threadIndex := 0; threadIndex < benchmark.ThreadCount; threadIndex++ {
redirectsChannels[threadIndex] = make(chan int)
failuresChannels[threadIndex] = make(chan int)
go benchmark.Run(t, redirectsChannels[threadIndex], failuresChannels[threadIndex], trafficRouterIndex, trafficRouterURL, ipAddressIndex)
}
for threadIndex := 0; threadIndex < benchmark.ThreadCount; threadIndex++ {
redirects += <-redirectsChannels[threadIndex]
failures += <-failuresChannels[threadIndex]
}
protocol := "IPv6"
if isIPv4 {
protocol = "IPv4"
}
var passed string
requestsPerSecond := redirects / benchmark.BenchmarkSeconds
if requestsPerSecond > benchmark.RequestsPerSecondThreshold {
passedTests++
passed = "Yes"
} else {
failedTests++
passed = "No"
}
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%d\t%d\t%d\n", trafficRouter.Hostname, protocol, trafficRouter.DSHost, passed, requestsPerSecond, redirects, failures)
writer.Flush()
}
}
summary := fmt.Sprintf("%d out of %d load tests passed", passedTests, passedTests+failedTests)
if failedTests < 1 {
t.Logf(summary)
} else {
t.Fatal(summary)
}
}
func (b HTTPBenchmark) Run(t *testing.T, redirectsChannel chan int, failuresChannel chan int, trafficRouterIndex int, trafficRouterURL string, ipAddressIndex int) {
paths := generatePaths(b.PathCount, b.MaxPathLength)
stopTime := time.Now().Add(time.Duration(b.BenchmarkSeconds) * time.Second)
redirects, failures := 0, 0
var req *http.Request
var resp *http.Response
var err error
httpClient := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: 10 * time.Second,
}
trafficRouter := b.TrafficRouters[trafficRouterIndex]
for time.Now().Before(stopTime) {
requestURL := trafficRouterURL + paths[rand.Intn(len(paths))]
if req, err = http.NewRequest("GET", requestURL, nil); err != nil {
t.Fatalf("creating GET request to Traffic Router '%s' (IP address %s): %s",
trafficRouter.Hostname, trafficRouter.IPAddresses[ipAddressIndex], err.Error())
}
req.Header.Set("User-Agent", UserAgent)
if trafficRouter.ClientIP != "" {
req.Header.Set(tc.X_MM_CLIENT_IP, trafficRouter.ClientIP)
}
req.Host = trafficRouter.DSHost
resp, err = httpClient.Do(req)
if err == nil && resp.StatusCode == http.StatusFound {
redirects++
} else {
failures++
}
}
redirectsChannel <- redirects
failuresChannel <- failures
}
func TestHTTPLoad(t *testing.T) {
var trafficRouterDetails = GetTrafficRouterDetails(t)
benchmark := HTTPBenchmark{
Benchmark: Benchmark{
RequestsPerSecondThreshold: *HTTPRequestsPerSecondThreshold,
BenchmarkSeconds: *BenchTime,
ThreadCount: *ThreadCount,
TrafficRouters: trafficRouterDetails,
CoverageZoneLocation: *CoverageZoneLocation,
},
PathCount: *PathCount,
MaxPathLength: *MaxPathLength,
}
RunHTTPBenchmarksAgainstTrafficRouters(t, benchmark)
}
func generatePaths(pathCount, maxPathLength int) []string {
const alphabetSize = 26 + 26 + 10
alphabet := make([]rune, alphabetSize)
index := 0
for char := 'A'; char <= 'Z'; char++ {
alphabet[index] = char
index++
}
for char := 'a'; char <= 'z'; char++ {
alphabet[index] = char
index++
}
for char := '0'; char <= '9'; char++ {
alphabet[index] = char
index++
}
paths := make([]string, pathCount)
for index = 0; index < pathCount; index++ {
pathLength := rand.Intn(maxPathLength)
generatedURL := make([]rune, pathLength)
for runeIndex := 0; runeIndex < pathLength; runeIndex++ {
generatedURL[runeIndex] = alphabet[rand.Intn(alphabetSize)]
}
paths[index] = string(generatedURL)
}
return paths
}
func getTrafficRouters(trafficRouterName string, cdnName tc.CDNName) ([]tc.ServerV40, error) {
requestOptions := client.RequestOptions{QueryParameters: url.Values{
"type": {tc.RouterTypeName},
"status": {tc.CacheStatusOnline.String()},
}}
if trafficRouterName != "" {
requestOptions.QueryParameters.Set("hostName", trafficRouterName)
}
if cdnName != "" {
cdnRequestOptions := client.RequestOptions{QueryParameters: url.Values{
"name": {string(cdnName)},
}}
cdnResponse, _, err := TOSession.GetCDNs(cdnRequestOptions)
if err != nil {
return nil, fmt.Errorf("requesting a CDN named '%s': %s", cdnName, err.Error())
}
cdns := cdnResponse.Response
if len(cdns) != 1 {
return nil, fmt.Errorf("did not find exactly 1 CDN with name '%s'", cdnName)
}
requestOptions.QueryParameters.Set("cdn", string(cdnName))
}
response, _, err := TOSession.GetServers(requestOptions)
if err != nil {
return nil, fmt.Errorf("requesting %s-status Traffic Routers: %s", requestOptions.QueryParameters["status"], err.Error())
}
trafficRouters := response.Response
if len(trafficRouters) < 1 {
return trafficRouters, fmt.Errorf("no Traffic Routers were found with these criteria: %v", requestOptions.QueryParameters)
}
return trafficRouters, nil
}
func getDSes(t *testing.T, cdnId int, dsTypeName tc.DSType, dsName tc.DeliveryServiceName) []tc.DeliveryServiceV40 {
requestOptions := client.RequestOptions{QueryParameters: url.Values{"name": {dsTypeName.String()}}}
var dsType tc.Type
{
response, _, err := TOSession.GetTypes(requestOptions)
if err != nil {
t.Fatalf("getting type %s: %s", requestOptions.QueryParameters["name"], err.Error())
}
types := response.Response
if len(types) != 1 {
t.Fatalf("did not find exactly 1 type with name '%s'", requestOptions.QueryParameters["name"])
}
dsType = types[0]
}
requestOptions = client.RequestOptions{QueryParameters: url.Values{
"cdn": {strconv.Itoa(cdnId)},
"type": {strconv.Itoa(dsType.ID)},
"status": {tc.CacheStatusOnline.String()},
}}
if dsName != "" {
requestOptions.QueryParameters.Set("xmlId", dsName.String())
}
response, _, err := TOSession.GetDeliveryServices(requestOptions)
if err != nil {
t.Fatalf("getting Delivery Services with type '%s' (type ID %d): %s", dsType.Name, dsType.ID, err.Error())
}
httpDSes := response.Response
return httpDSes
}