Skip to content
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 option to specify VU network interface #1293

Closed
wants to merge 6 commits into from
Closed

Add option to specify VU network interface #1293

wants to merge 6 commits into from

Conversation

srguglielmo
Copy link

This adds a "nic" option (e.g. k6 run --nic eth0) which will cause the
Virtual User to use the IP address(es) assigned to the given network
interface. If the NIC has multiple IPs, they will be used by each VU at
random.

Credit for the original code goes to @ofauchon.

Closes GitHub issue loadimpact#476 and original PR loadimpact#757.

@srguglielmo
Copy link
Author

My local build is reporting a syntax error: js/runner.go:161:41: syntax error: missing channel element type I'm not sure how to fix this. Any advise?

This is a much-needed feature by my team and I'm sure others too.

Thank you!

@na-- na-- requested review from imiric, na--, mstoykov and cuonglm January 7, 2020 11:06
Copy link
Contributor

@cuonglm cuonglm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why the CI fails.

Copy link
Contributor

@imiric imiric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see some tests for this. Not integration/E2E as that would be tricky to setup, but at least that we're properly setting the option from CLI and env var, since that isn't currently working. See https://github.com/loadimpact/k6/blob/7b6b2a684b7f26bbcf1b8549db89614ac398ad54/cmd/config_consolidation_test.go .

js/runner.go Outdated Show resolved Hide resolved
js/runner.go Outdated Show resolved Hide resolved
js/runner.go Outdated Show resolved Hide resolved
lib/options.go Outdated Show resolved Hide resolved
@codecov-io
Copy link

codecov-io commented Jan 9, 2020

Codecov Report

Merging #1293 into master will decrease coverage by 0.19%.
The diff coverage is 6.45%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #1293     +/-   ##
=========================================
- Coverage   75.44%   75.24%   -0.2%     
=========================================
  Files         150      150             
  Lines       10885    10916     +31     
=========================================
+ Hits         8212     8214      +2     
- Misses       2207     2234     +27     
- Partials      466      468      +2
Impacted Files Coverage Δ
js/runner.go 75.34% <0%> (-7.8%) ⬇️
lib/options.go 92.09% <0%> (-1.06%) ⬇️
cmd/options.go 71.09% <100%> (+0.45%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7b6b2a6...73e2e23. Read the comment docs.

@ofauchon
Copy link
Contributor

ofauchon commented Jan 9, 2020

Hi Steve !

Thanks for finishing the implementation of this feature.
Great job ! I hope it'll be merged soon !

Olivier

@srguglielmo
Copy link
Author

@ofauchon Olivier, I couldn't have done it without your initial work! Thank you!

At this point all suggested changes are committed. I've tested functionality locally and found no issues so far. The only thing pending from what I can tell is to add tests for this. I looked at the link @imiric posted above, but I'm not really sure where to start.

@@ -82,6 +82,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.StringP("nic", "n", "", "Use given network interface")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use a flags.StringSlice() here? That would allow users to specify multiple NICs by k6 run --nic nic1 --nic nic2, and would also spare us the need to do strings.Split(ifacesList, ",") in js/runner.go.

Comment on lines +128 to +141
var addresses []net.Addr

for _, ifaceName := range strings.Split(ifacesList, ",") {
targetIf, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, errors.New("Cannot find network interface '" + ifaceName + "'")
}

addrs, err := targetIf.Addrs()
if err != nil {
return nil, errors.New("Cannot get addresses on network interface '" + ifaceName + "'")
}

addresses = append(addresses, addrs...)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should probably be called only one time, it doesn't make sense to execute it for every VU initialization. For now a sync.Once() should do, though long-term (i.e. after most of #883 is resolved), we can move most of this logic in a proper config validation layer and have the list of all possible IPs before we even start doing any VU initialization.

return nil, errors.New("Cannot find any usable addresses on network interface(s) '" + ifacesList + "'")
}

rand.Shuffle(len(addresses), func(i, j int) { addresses[i], addresses[j] = addresses[j], addresses[i] })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't make sense to shuffle an array every time you want to choose a random member of it... You can either use something like addr := addresses[rand.Int()], or better yet, also do the array shuffle only once in the sync.Once() block I mentioned above. Then you can just iterate through it (with an atomic counter to avoid races) like this:

myIPIndex := atomic.AddUint32(&globalIPAddrCounter, 1) -1
myIPAddr := addresses[myIPIndex % len(addresses)]

Also, if you're using random numbers of shuffling, you should probably make sure that you Seed() the random number generator, or better yet, make your own local one with https://golang.org/pkg/math/rand/#NewSource

@@ -285,6 +285,8 @@ type Options struct {

// Redirect console logging to a file
ConsoleOutput null.String `json:"-" envconfig:"K6_CONSOLE_OUTPUT"`

Nic null.String `json:"nic,omitempty" envconfig:"K6_NIC"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the CLI flag configuration, this should probably be a string slice.

lib/options.go Show resolved Hide resolved
Comment on lines +150 to +157
tip, _, err := net.ParseCIDR(addresses[0].String())
if err != nil {
return nil, errors.New("Cannot parse IP address from string '" + addresses[0].String() + "'")
}

localAddr, err := net.ResolveIPAddr("ip", tip.String())
if err != nil {
return nil, errors.New("Cannot resolve IP '" + addresses[0].String() + "'")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, these string-y conversions seem strange... addresses already is of type []net.Addr, and that's what Dialer.LocalAddr is as well. So why do we need to go through this?

@mstoykov
Copy link
Contributor

I did try divfor's rebased branch as I was testing for #1303 (comment link).

I had the following .. problems:

  1. by default my nic gets an ipv6 address that can't be used as ... well, I haven't configured ipv6 explicitly. I don't know why this happens, but it is probably going to be a problem in other places as well. The end result is that some requests fail because they try to use the ipv6 address which doesn't work :(. Here someone with more expertise can explain if this is a common problem and whether we should really worry about it
  2. I myself don't really know how my nic's are named .. this is probably even more true in clouds such as AWS. But I do usually have good idea what my IPs are if there will be multiple
    As such I find it much more obvious to just give a list of IPs. This way they can be on multiple NICs, only parts of the IPs on a given nic and so on and so forth.

Of course, after that, we should check that those IPs do actually happen to be assigned to a nic :)

@ofauchon, @divfor, @srguglielmo do you think it's better to list the nic and what are your arguments for it?

  1. I do think that the IP should be given in sequence to the VUs so the first VU gets the first IP, the second gets the second IP and so on and then loop them instead of on a random. The random thing was one of the problems with me diagnosing point 1 of my issues.

Other than that this approach seems to work and I did need it to saturate the 1gbps in the test I did in the linked comment above.

@na--
Copy link
Member

na-- commented Feb 10, 2020

@mstoykov, I think I'd also prefer to specify source IPs (and IP ranges) instead of NICs, for multiple reasons:

  • it seems more flexible, for example it'd allow you to spread VUs in different ratios between NICs - say, if you specify the IPs of nic1 two times and the IPs of nic2 just once, you'd get VUs 2:1 across these NICs...
  • better UX (especially in cloud or container environments... I think...)
  • will work exactly the same cross-platform (who knows the NIC names on Windows...)
  • no IPv6 issues, or potential NIC->IP enumeration issues (who knows)

It just seems somewhat more sensible in general... Somewhat squishy reasoning, I know, but I don't see any negatives if we go with specifying IP ranges, only positives compared to NIC names.

@srguglielmo
Copy link
Author

Hi everyone,

Thanks for the feedback. To be honest, I'd also agree that specifying IP ranges would be better than the NIC. I built this PR on top of @ofauchon's original PR. I'm not sure I have the skill/time to refactor it entirely to accept CIDR blocks, but I'll try.

Should this PR and the original one #757 be closed? I can open a new one referencing issue #476

Thanks!

@mstoykov
Copy link
Contributor

@srguglielmo just to clarify (because you used the word range) what I meant was that I prefer that we can do --ips-to-use "10.2.3.4,10.2.3.5,10.2.3.6", not --ips-to-use "10.2.3.4-10.2.3.6" or any other (mask based way) of specifying a group of IPs.

I find this to be more useful as I am VERY likely in any kind of setup to know what my IPs will be but not so much what my network interface is called (I don't even know if on windows interfaces have names).

Also while I agree that listing 20 IPs will be more work then 1 network interface or 1 IP range it is also IMO more easy to understand :D.

Also AFAIK you can have 5 network interfaces having the same IP and through which one will actually be used to send the request is up to the OS not us as an application - we can only request that it goes out of certain IP.

p.s. --ips-to-use isn't what I actually propose we call it in the end :D
p.s.s. I don't think you need to close any PRs yet - we are likely to clean up some of them in the near future, so we will probably close them then(or when we merge an IP based approach)

@srguglielmo
Copy link
Author

srguglielmo commented Feb 17, 2020

@mstoykov For my specific use case (not sure about others), it wouldn't be practical to specify every IP on the command line. Here's our use case:

We are load testing a emergency alerting website running in Azure cloud. Azure has a rate limit on SSL handshakes per IP address (confirmed by Azure support). I think this is a global DDoS protection on their end; Azure support was unable to disable it for our account/service. Our goal was to support 10,000 users hitting the site, so we created five on-prem VMs and assigned them each 400 public IPs (total of 2,000) to work around the Azure limit. I'm able to run the load test now using the --nic ens192 option.

I would propose supporting an --ip argument that accepts a comma-separated list of individual IPs, a CIDR block, or range. Examples:

--ip 129.32.128.2
--ip 129.32.128.3-129.32.128.36
--ip 129.32.128.0/21
--ip 129.32.128.20-129.32.128.30,129.32.128.40-129.32.128.50
--ip 129.32.128.21,129.32.128.23,129.32.128.25,129.32.128.27,129.32.128.29

It might look at little messy, but it would work for any use case. We could even keep the current code to let it accept a NIC too! The string would be split on ,, then each part checked for a range -, or CIDR /, or NIC.

@divfor
Copy link
Contributor

divfor commented Feb 23, 2020

I did try divfor's rebased branch as I was testing for #1303 (comment link).

I had the following .. problems:

  1. by default my nic gets an ipv6 address that can't be used as ... well, I haven't configured ipv6 explicitly. I don't know why this happens, but it is probably going to be a problem in other places as well. The end result is that some requests fail because they try to use the ipv6 address which doesn't work :(. Here someone with more expertise can explain if this is a common problem and whether we should really worry about it
  2. I myself don't really know how my nic's are named .. this is probably even more true in clouds such as AWS. But I do usually have good idea what my IPs are if there will be multiple
    As such I find it much more obvious to just give a list of IPs. This way they can be on multiple NICs, only parts of the IPs on a given nic and so on and so forth.

Of course, after that, we should check that those IPs do actually happen to be assigned to a nic :)

@ofauchon, @divfor, @srguglielmo do you think it's better to list the nic and what are your arguments for it?

  1. I do think that the IP should be given in sequence to the VUs so the first VU gets the first IP, the second gets the second IP and so on and then loop them instead of on a random. The random thing was one of the problems with me diagnosing point 1 of my issues.

Other than that this approach seems to work and I did need it to saturate the 1gbps in the test I did in the linked comment above.

@mstoykov Of cause I think IPs is better than nic name. On the number of IPs, I think additionally supporting IP range or cidr is better because k6 is often used in load testing cloud services that expect huge number of users from different source IP addresses. a few number of IPs are better for L3/L4 or L7 throughput testing but not better for distributed connection rate testing.

@mstoykov
Copy link
Contributor

Yeah I guess if you need to specify 200 IPs even using a script to generate the values will make it pretty hard to read and I am pretty sure implementing a IP range or cidr parsing will not be all that complicated so 👍

@CLAassistant
Copy link

CLA assistant check
All committers have signed the CLA.

@mstoykov
Copy link
Contributor

Hi @divfor , @srguglielmo, we have released v0.27.0 last week (:tada: ) and are now again open for non-1007 PRs :tada:!
@srguglielmo I hope you will want to rebase this and finish it with that nice ip-range parsing functionality, so we can hopefully get this in v0.28.0?

This adds a "nic" option (e.g. `k6 run --nic eth0`) which will cause the
Virtual User to use the IP address(es) assigned to the given network
interface. If the NIC has multiple IPs, they will be used by each VU at
random.

Credit for the original code goes to @ofauchon.

Closes GitHub issue loadimpact#476 and original PR loadimpact#757.
@codecov-commenter
Copy link

codecov-commenter commented Jul 21, 2020

Codecov Report

Merging #1293 into master will decrease coverage by 1.86%.
The diff coverage is 6.45%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1293      +/-   ##
==========================================
- Coverage   77.11%   75.24%   -1.87%     
==========================================
  Files         162      150      -12     
  Lines       13192    10916    -2276     
==========================================
- Hits        10173     8214    -1959     
+ Misses       2499     2234     -265     
+ Partials      520      468      -52     
Impacted Files Coverage Δ
js/runner.go 75.34% <0.00%> (-7.70%) ⬇️
lib/options.go 92.09% <0.00%> (+4.39%) ⬆️
cmd/options.go 71.09% <100.00%> (+7.76%) ⬆️
lib/runtime_options.go 55.55% <0.00%> (-44.45%) ⬇️
api/v1/group.go 71.42% <0.00%> (-19.49%) ⬇️
cmd/common.go 40.00% <0.00%> (-10.00%) ⬇️
js/modules/k6/k6.go 91.48% <0.00%> (-8.52%) ⬇️
js/bundle.go 85.18% <0.00%> (-5.67%) ⬇️
cmd/config.go 78.10% <0.00%> (-5.48%) ⬇️
lib/types/types.go 84.26% <0.00%> (-2.53%) ⬇️
... and 126 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 73d5ee7...b634712. Read the comment docs.

@srguglielmo
Copy link
Author

@mstoykov It has been rebased! I might be getting in over my head, but I'll see what I can do regarding the additional improvements discussed above. I'd appreciate any help!

Thanks,
Steve

@divfor
Copy link
Contributor

divfor commented Aug 10, 2020

hello,

I found a way to use a lots of IP as source IP addresses but avoid to add them to interfaces - just add an local route entry, for example:

root@STGAX05:~# ip route add local 1.2.3.0/24 dev lo
root@STGAX05:~# ip route show table local | grep /
local 1.2.3.0/24 dev lo scope host
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1

then k6 can bind any of 1.2.3.0/24 to start loading testing, as long as those IPs are routable back from outside to k6.

@divfor
Copy link
Contributor

divfor commented Aug 11, 2020

hello,

I found a way to use a lots of IP as source IP addresses but avoid to add them to interfaces - just add an local route entry, for example:

root@STGAX05:~# ip route add local 1.2.3.0/24 dev lo
root@STGAX05:~# ip route show table local | grep /
local 1.2.3.0/24 dev lo scope host
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1

then k6 can bind any of 1.2.3.0/24 to start loading testing, as long as those IPs are routable back from outside to k6.

@srguglielmo
can you let k6 also pick IP addresses up from local route table that has no ip address configured on a Linux kernel NIC interface? actually, all kernel IPs are also able to show from local table:
grep kernel IPaddresses:

root@STGAX04:~# ip route show table local | grep kernel
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
local 192.168.250.104 dev eth0 proto kernel scope host src 192.168.250.104

root@STGAX04:~# ping 127.0.2.4
PING 127.0.2.4 (127.0.2.4) 56(84) bytes of data.
64 bytes from 127.0.2.4: icmp_seq=1 ttl=64 time=0.063 ms
^C
--- 127.0.2.4 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.063/0.063/0.063/0.000 ms

grep non-kernel IP addresses:

root@STGAX04:~# ip route show table local | grep -v kernel
local 27.126.195.0/24 dev lo scope host
local 27.126.207.0/24 dev lo scope host
local 103.3.24.0/24 dev lo scope host

root@STGAX04:~# ping 103.3.24.123
PING 103.3.24.123 (103.3.24.123) 56(84) bytes of data.
64 bytes from 103.3.24.123: icmp_seq=1 ttl=64 time=0.048 ms
^C

if so, k6 can enable to use IP addresses that are not configured on any interfaces as src IP -- this is good news for cloud-based OS like kvm/docker.

@divfor
Copy link
Contributor

divfor commented Aug 11, 2020

or simply, k6 does not need to check any existence of given cidr/ip-range, just shuffle and use them to connect servers.
it is k6 user who ensures the IP existence or routable in local route table, for example:

Setup:

root@STGAX06:~# ip link set k6 type dummy
root@STGAX06:~# ifconfig k6
k6: flags=130<BROADCAST,NOARP>  mtu 1500
        inet 10.60.0.1  netmask 255.255.0.0  broadcast 0.0.0.0
        ether 42:80:20:9f:8f:12  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

root@STGAX06:~# ip route add local 10.12.0.0/16 dev k6
root@STGAX06:~# ip route show table local | grep ^local | grep /
local 10.12.0.0/16 dev k6 scope host

Run K6 using 65536 source IP addresses for VUs

root@STGAX06:~# k6 run --cidr 10.12.0.0/16 test.js

or run K6 using 2 x 256 source IP addresses for VUs

root@STGAX06:~# k6 run --cidr 10.12.0.0/24 --cidr 10.17.0.0/24 test.js

Teardown:

root@STGAX06:~# ip route del local 10.12.0.0/16 dev k6
root@STGAX06:~# ip link del k6

If someone want to multiple NICs to using k6, just set to Linux the egress routes via correct NIC interface.

@na-- na-- mentioned this pull request Sep 8, 2020
@divfor
Copy link
Contributor

divfor commented Sep 14, 2020

Assigning millions of IPs to a server:
https://blog.cloudflare.com/how-we-built-spectrum/

@mstoykov
Copy link
Contributor

@srguglielmo sorry I didn't see you were asking for help ... I don't know how I can help you apart from writing the code :D .

We have internally decided that after almost 2 years of this and the previous PRs being around we will actually push this ourselves to a mergeable PR.

Thanks a lot to everybody for the PRs and discussions, this has been really helpful, and sorry that this taking so long, but we will get it in 0.29.0 ... I promise ™️ ;)

So you should expect that we will make a new PR withing a couple of weeks fixing the remaining issues ... I am not really certain how we will manage to test this but will try to figure something out ;)

@srguglielmo
Copy link
Author

Thank you so much! Should I close this PR or leave it open for now?

@mstoykov
Copy link
Contributor

@srguglielmo I guess I will close it :D
Thanks again for pushing this 🎉

@mstoykov mstoykov closed this Oct 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants