-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add multi source IP support #1682
Conversation
cmd/options.go
Outdated
@@ -93,6 +93,7 @@ func optionFlagSet() *pflag.FlagSet { | |||
flags.StringSlice("tag", nil, "add a `tag` to be applied to all samples, as `[name]=[value]`") | |||
flags.String("console-output", "", "redirects the console logging to the provided output file") | |||
flags.Bool("discard-response-bodies", false, "Read but don't process or save HTTP response bodies") | |||
flags.String("client-ips", "", "Client IP Ranges and/or CIDRs from which each VU will be making requests") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--local-ips
or --local-addr
seems slightly better as a name. This is also what Go uses, see https://golang.org/pkg/net/#IPConn.LocalAddr, https://golang.org/pkg/net/#DialTCP
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, besides the option name and the few minor nitpicks I noted inline.
lib/types/ipblock.go
Outdated
return ipBlockFromCIDR(s) | ||
default: | ||
if net.ParseIP(s) == nil { | ||
return nil, fmt.Errorf("%s is not a valid ip, range ip or CIDR", s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("%s is not a valid ip, range ip or CIDR", s) | |
return nil, fmt.Errorf("%s is not a valid IP, IP range or CIDR", s) |
block.firstIP.SetBytes(ip0.To4()) | ||
block.count.SetBytes(ip1.To4()) | ||
} | ||
block.count.Sub(block.count, block.firstIP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment that ip0<ip1 is validated elsewhere, I missed the negative ip range
check on the first reading
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also having some tests for this would be good.
block.firstIP.SetBytes(ip0.To4()) | ||
block.count.SetBytes(ip1.To4()) | ||
} | ||
block.count.Sub(block.count, block.firstIP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also having some tests for this would be good.
Codecov Report
@@ Coverage Diff @@
## master #1682 +/- ##
==========================================
+ Coverage 72.10% 72.37% +0.27%
==========================================
Files 167 176 +9
Lines 12882 13536 +654
==========================================
+ Hits 9288 9797 +509
- Misses 3031 3126 +95
- Partials 563 613 +50
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just a comment nitpick.
lib/types/ipblock.go
Outdated
} | ||
|
||
// ipPoolBlock is almost the same as ipBlock but is put in a IPPool and knows it's start id indest | ||
// of it's size/count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revise this comment. I'm not sure what it's start id indest
means, and it's
should be its
in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"ipPoolBlock is similar to ipBlock but instead of knowing its count/size it knows the first index from which it starts in an IPPool" ?
} | ||
|
||
func ipBlockFromCIDR(s string) (*ipBlock, error) { | ||
_, pnet, err := net.ParseCIDR(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_, pnet, err := net.ParseCIDR(strings.TrimSpace(s))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at least it should keep as same as ipBlockFromRange() which is using trimming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong preference either way, but we should keep it consistent, so removing trimming LGTM.
lib/types/ipblock.go
Outdated
// bigger than startIndex but it will be true always for the first block which is with | ||
// startIndex 0. This can also be fixed by iterating in reverse but this seems better | ||
for i := 0; i < len(pool.list)/2; i++ { | ||
pool.list[i], pool.list[len(pool.list)/2-i] = pool.list[len(pool.list)/2-i], pool.list[i] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a reverse should be: pool.list[i], pool.list[len(pool.list)-i] = pool.list[len(pool.list)-i], pool.list[i]
js/runner.go
Outdated
@@ -170,6 +170,10 @@ func (r *Runner) newVU(id int64, samplesOut chan<- stats.SampleContainer) (*VU, | |||
BlockedHostnames: r.Bundle.Options.BlockedHostnames.Trie, | |||
Hosts: r.Bundle.Options.Hosts, | |||
} | |||
if r.Bundle.Options.LocalIPs.Valid { | |||
dialer.Dialer.LocalAddr = &net.TCPAddr{IP: r.Bundle.Options.LocalIPs.Pool.GetIP(uint64(id - 1))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my testing, id will be 0 in the first VU allocation. so a check of id > 0 should be set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if r.Bundle.Options.LocalIPs.Valid && id > 0 {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is a fact: VU (id==0) will never sends requests... I think this is a bug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VU with id 0 is generally used for stuff that "don't" do requests but ... setup
and teardown
functions can make them and they need to also use one of the provided IPs to make the requests.
Actually speaking of this ... maybe they should use some "randomish" one but instead the first one? Currently They will use (2**64 -1) % IPPool.count
which IMO is fine - they use an IP from the provided ones and arguably every IP should be viable, but maybe they should use the first one instead for more consistancy?
cc @imiric @na--
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should add any special handling for 0 ID VUs, but we should fix the uint64
underflow for the id=0
case. Picking the first one seems fine to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from my view, take the first IP is not good since the vu id == 0 will take initialising requests - I guess it is DNS resolving?
if skipped, first VU(id == 0) will be using OS interface ip address (OS to decide what interface & its IP address to reach dns servers). this is needed because most of outside dns servers may not have route to responding to k6's --local-ips.
thanks to @divfor
Co-authored-by: na-- <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just remember to squash.
@@ -388,6 +388,9 @@ type Options struct { | |||
|
|||
// Redirect console logging to a file | |||
ConsoleOutput null.String `json:"-" envconfig:"K6_CONSOLE_OUTPUT"` | |||
|
|||
// Specify client IP ranges and/or CIDR from which VUs will make requests | |||
LocalIPs types.NullIPPool `json:"-" envconfig:"K6_LOCAL_IPS"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure why this shouldn't be configured via the JS options
and from config.json
. We can validate so we don't allow it in the cloud, but I don't see to disable it in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should it be configurable? I find it very unlikely that will be a needed thing for anyone and setting an environment variable won't be a good enough solution as well.
I am for leaving this for when someone actually asks for it as we can always add it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should it be configurable?
Because there's no good reason not to be? And, whenever possible, most things in k6 are configurable in 4 different ways - CLI flags, env vars, JS options, JSON options. When that's not the case, it's usually because there is a good reason for it. For example, we need to know the option's value before we execute the init context. Or we think the option is so minor that it'll overcrowd the CLI flags. Or there's a bug in envconfig
, or something like that.
The more inconsistencies we have with the k6 options, the worse the user experience is. It's not a big deal, but constantly breaking expectations in this way is bound to get annoying.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM besides the lack of js/json option support, but we can fix that in the future, along with all of the already existing config inconsistencies 😞
Regarding future developments in this direction (weights, randomization and such), I was thinking that shoehorning them in a super-complex global option might not be the best way to implement the feature. I had a brief look at the other PR and it was very difficult to understand things. Rather, given that this is essentially a per-VU setting, why can't we have a JS API to control the source IPs for each VU?
Basically, leave the --local-ips
exactly as it is, without any further complications. Instead, in the future, we can add a new JS API that allow users to control different aspects of the VU, including the local IPs. I don't see a problem with setting the local IP in the init context, or even on every iteration... The --local-ips
value can be used for validation - if set, allow the JS function to only select IPs from its pool, otherwise allow users to set any local IPs.
More thought obviously has to go into this, but I think it would be better than having increasingly more complex global options. "Pick a random IP out of this pool of IP ranges with different weights" is much more cleanly expressed in a programming language like JS than through some obscure configuration syntax.
And we actually won't have to add this huge complexity to k6 for the few users like @divfor who might need it. We'd just offer a simple API for setting the source IP. Then every user can have an arbitrary complex scheme for picking random or sequential values however they like. The new HTTP client API should also probably have the ability to set a local IP... And this way, local IPs will work much better in conjunction with scenarios - different scenarios can have different pools of local IPs, which seems potentially useful.
@@ -388,6 +388,9 @@ type Options struct { | |||
|
|||
// Redirect console logging to a file | |||
ConsoleOutput null.String `json:"-" envconfig:"K6_CONSOLE_OUTPUT"` | |||
|
|||
// Specify client IP ranges and/or CIDR from which VUs will make requests | |||
LocalIPs types.NullIPPool `json:"-" envconfig:"K6_LOCAL_IPS"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should it be configurable?
Because there's no good reason not to be? And, whenever possible, most things in k6 are configurable in 4 different ways - CLI flags, env vars, JS options, JSON options. When that's not the case, it's usually because there is a good reason for it. For example, we need to know the option's value before we execute the init context. Or we think the option is so minor that it'll overcrowd the CLI flags. Or there's a bug in envconfig
, or something like that.
The more inconsistencies we have with the k6 options, the worse the user experience is. It's not a big deal, but constantly breaking expectations in this way is bound to get annoying.
fixes grafana#476 thanks to @divfor Co-authored-by: divfor <[email protected]> Co-authored-by: na-- <[email protected]>
fixes #476