github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/roachtest/toxiproxy.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package main 12 13 import ( 14 "context" 15 gosql "database/sql" 16 "fmt" 17 "net/url" 18 "regexp" 19 "runtime" 20 "strconv" 21 "time" 22 23 toxiproxy "github.com/Shopify/toxiproxy/client" 24 "github.com/cockroachdb/errors" 25 ) 26 27 // cockroachToxiWrapper replaces the cockroach binary. It modifies the listening port so 28 // that the nodes in the cluster will communicate through toxiproxy instead of 29 // directly. 30 const cockroachToxiWrapper = `#!/usr/bin/env bash 31 set -eu 32 33 cd "$(dirname "${0}")" 34 35 orig_port="" 36 37 args=() 38 39 if [[ "$1" != "start" ]]; then 40 ./cockroach.real "$@" 41 exit $? 42 fi 43 44 for arg in "$@"; do 45 capture=$(echo "${arg}" | sed -E 's/^--port=([0-9]+)$/\1/') 46 if [[ "${capture}" != "${arg}" ]] && [[ -z "${orig_port}" ]] && [[ -n "${capture}" ]]; then 47 orig_port="${capture}" 48 fi 49 args+=("${arg}") 50 done 51 52 if [[ -z "${orig_port}" ]]; then 53 orig_port=26257 54 fi 55 56 args+=("--advertise-port=$((orig_port+10000))") 57 58 echo "toxiproxy interception:" 59 echo "original args: $@" 60 echo "modified args: ${args[@]}" 61 ./cockroach.real "${args[@]}" 62 ` 63 64 const toxiServerWrapper = `#!/usr/bin/env bash 65 set -eu 66 67 mkdir -p logs 68 ./toxiproxy-server -host 0.0.0.0 -port $1 2>&1 > logs/toxiproxy.log & </dev/null 69 until nc -z localhost $1; do sleep 0.1; echo "waiting for toxiproxy-server..."; done 70 ` 71 72 // A ToxiCluster wraps a cluster and sets it up for use with toxiproxy. 73 // See Toxify() for details. 74 type ToxiCluster struct { 75 *cluster 76 toxClients map[int]*toxiproxy.Client 77 toxProxies map[int]*toxiproxy.Proxy 78 } 79 80 // Toxify takes a cluster and sets it up for use with toxiproxy on the given 81 // nodes. On these nodes, the cockroach binary must already have been populated 82 // and the cluster must not have been started yet. The returned ToxiCluster 83 // wraps the original cluster, whose returned addresses will all go through 84 // toxiproxy. The upstream (i.e. non-intercepted) addresses are accessible via 85 // getters prefixed with "External". 86 func Toxify(ctx context.Context, c *cluster, node nodeListOption) (*ToxiCluster, error) { 87 toxiURL := "https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-linux-amd64" 88 if local && runtime.GOOS == "darwin" { 89 toxiURL = "https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-darwin-amd64" 90 } 91 if err := func() error { 92 if err := c.RunE(ctx, c.All(), "curl", "-Lfo", "toxiproxy-server", toxiURL); err != nil { 93 return err 94 } 95 if err := c.RunE(ctx, c.All(), "chmod", "+x", "toxiproxy-server"); err != nil { 96 return err 97 } 98 99 if err := c.RunE(ctx, node, "mv cockroach cockroach.real"); err != nil { 100 return err 101 } 102 if err := c.PutString(ctx, cockroachToxiWrapper, "./cockroach", 0755, node); err != nil { 103 return err 104 } 105 return c.PutString(ctx, toxiServerWrapper, "./toxiproxyd", 0755, node) 106 }(); err != nil { 107 return nil, errors.Wrap(err, "toxify") 108 } 109 110 tc := &ToxiCluster{ 111 cluster: c, 112 toxClients: make(map[int]*toxiproxy.Client), 113 toxProxies: make(map[int]*toxiproxy.Proxy), 114 } 115 116 for _, i := range node { 117 n := c.Node(i) 118 119 toxPort := 8474 + i 120 if err := c.RunE(ctx, n, fmt.Sprintf("./toxiproxyd %d 2>/dev/null >/dev/null < /dev/null", toxPort)); err != nil { 121 return nil, errors.Wrap(err, "toxify") 122 } 123 124 externalAddr, port := addrToHostPort(c, c.ExternalAddr(ctx, n)[0]) 125 tc.toxClients[i] = toxiproxy.NewClient(fmt.Sprintf("http://%s:%d", externalAddr, toxPort)) 126 proxy, err := tc.toxClients[i].CreateProxy("cockroach", fmt.Sprintf(":%d", tc.poisonedPort(port)), fmt.Sprintf("127.0.0.1:%d", port)) 127 if err != nil { 128 return nil, errors.Wrap(err, "toxify") 129 } 130 tc.toxProxies[i] = proxy 131 } 132 133 return tc, nil 134 } 135 136 func (tc *ToxiCluster) poisonedPort(port int) int { 137 // NB: to make a change here, you also have to change 138 _ = cockroachToxiWrapper 139 return port + 10000 140 } 141 142 // Proxy returns the toxiproxy Proxy intercepting the given node's traffic. 143 func (tc *ToxiCluster) Proxy(i int) *toxiproxy.Proxy { 144 proxy, found := tc.toxProxies[i] 145 if !found { 146 tc.cluster.t.Fatalf("proxy for node %d not found", i) 147 } 148 return proxy 149 } 150 151 // ExternalAddr gives the external host:port of the node(s), bypassing the 152 // toxiproxy interception. 153 func (tc *ToxiCluster) ExternalAddr(ctx context.Context, node nodeListOption) []string { 154 return tc.cluster.ExternalAddr(ctx, node) 155 } 156 157 // PoisonedExternalAddr gives the external host:port of the toxiproxy process 158 // for the given nodes (i.e. the connection will be affected by toxics). 159 func (tc *ToxiCluster) PoisonedExternalAddr(ctx context.Context, node nodeListOption) []string { 160 var out []string 161 162 extAddrs := tc.ExternalAddr(ctx, node) 163 for _, addr := range extAddrs { 164 host, port := addrToHostPort(tc.cluster, addr) 165 out = append(out, fmt.Sprintf("%s:%d", host, tc.poisonedPort(port))) 166 } 167 return out 168 } 169 170 // PoisonedPGAddr gives a connection to the given node that passes through toxiproxy. 171 func (tc *ToxiCluster) PoisonedPGAddr(ctx context.Context, node nodeListOption) []string { 172 var out []string 173 174 urls := tc.ExternalPGUrl(ctx, node) 175 exts := tc.PoisonedExternalAddr(ctx, node) 176 for i, s := range urls { 177 u, err := url.Parse(s) 178 if err != nil { 179 tc.cluster.t.Fatal(err) 180 } 181 u.Host = exts[i] 182 out = append(out, u.String()) 183 } 184 return out 185 } 186 187 // PoisonedConn returns an SQL connection to the specified node through toxiproxy. 188 func (tc *ToxiCluster) PoisonedConn(ctx context.Context, node int) *gosql.DB { 189 url := tc.PoisonedPGAddr(ctx, tc.cluster.Node(node))[0] 190 db, err := gosql.Open("postgres", url) 191 if err != nil { 192 tc.cluster.t.Fatal(err) 193 } 194 return db 195 } 196 197 var _ = (*ToxiCluster)(nil).PoisonedConn 198 var _ = (*ToxiCluster)(nil).PoisonedPGAddr 199 var _ = (*ToxiCluster)(nil).PoisonedExternalAddr 200 201 var measureRE = regexp.MustCompile(`real[^0-9]+([0-9.]+)`) 202 203 // Measure runs a statement on the given node (bypassing toxiproxy for the 204 // client connection) and measures the duration (including the invocation time 205 // of `./cockroach sql`. This is simplistic and does not perform proper 206 // escaping. It's not useful for anything but simple sanity checks. 207 func (tc *ToxiCluster) Measure(ctx context.Context, fromNode int, stmt string) time.Duration { 208 _, port := addrToHostPort(tc.cluster, tc.ExternalAddr(ctx, tc.Node(fromNode))[0]) 209 b, err := tc.cluster.RunWithBuffer(ctx, tc.cluster.l, tc.cluster.Node(fromNode), "time", "-p", "./cockroach", "sql", "--insecure", "--port", strconv.Itoa(port), "-e", "'"+stmt+"'") 210 tc.cluster.l.Printf("%s\n", b) 211 if err != nil { 212 tc.cluster.t.Fatal(err) 213 } 214 matches := measureRE.FindSubmatch(b) 215 if len(matches) != 2 { 216 tc.cluster.t.Fatalf("unable to extract duration from output: %s", b) 217 } 218 f, err := strconv.ParseFloat(string(matches[1]), 64) 219 if err != nil { 220 tc.cluster.t.Fatalf("unable to parse %s as float: %s", b, err) 221 } 222 return time.Duration(f * 1e9) 223 }