Skip to content

libnetwork: use go-immutable-radix instead of radix#44501

Merged
thaJeztah merged 1 commit intomoby:masterfrom
tiborvass:immutable_radix
Dec 6, 2022
Merged

libnetwork: use go-immutable-radix instead of radix#44501
thaJeztah merged 1 commit intomoby:masterfrom
tiborvass:immutable_radix

Conversation

@tiborvass
Copy link
Contributor

This commit allows to remove dependency on the mutable version armon/go-radix.

The go-immutable-radix package is better maintained.

It is likely that a bit more memory will be used when using the immutable version, though discarded nodes are being reused in a pool. These changes happen when networks are added/removed or nodes come and go in a cluster, so we are still talking about a relatively low frequency event.

The major changes compared to the old radix are when modifying (insert or delete) a tree, and those are pretty self-contained: we replace the entire immutable tree under a lock.

Signed-off-by: Tibor Vass teabee89@gmail.com

@tiborvass
Copy link
Contributor Author

I'm curious to see if things break in CI because of this.

Comment on lines 396 to 398
// The lock is taken at the beginning of the cycle and the deletion is inline
for _, nid := range nodeNetworks {
nDB.Lock()
Copy link
Member

Choose a reason for hiding this comment

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

Still trying to get my head around this weird construct (added in moby/libnetwork@3feb3aa) even more with the "timeCompensate" below.

Do we know why does the lock/unlock have to happen inside the loop here (but the others lock outside)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comment above the for loop is attempting to explain it but I'm not sure I understand it. Could it be that releasing the lock in every iteration allows other goroutines to continue work instead of lagging too much? Either way, this PR is not modifying that logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the commit message of the commit you posted confirms it.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, was looking at that description and wondered if that changed now that there's an RWMutex, but yeah, this code is complicated, so perhaps that didn't work

}

params := strings.Split(path[1:], "/")
params := strings.Split(string(path[1:]), "/")
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps a micro-optimisation, but if these loops run many times, perhaps worth it while we're changing;

how about using bytes.Split or even bytes.SplitN (if we expect these to have 4 components)? Quick (naive) benchmark;

Details
package main

import (
	"bytes"
	"strings"
	"testing"
)

func BenchmarkSplitString(b *testing.B) {
	path := []byte("/one/two/three/four")
	sep := "/"
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		params := strings.Split(string(path[1:]), sep)
		nid := params[0]
		tname := params[1]
		key := params[2]
		_, _, _ = nid, tname, key
	}
}

func BenchmarkSplitBytes(b *testing.B) {
	path := []byte("/one/two/three/four")
	sep := []byte("/")
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		params := bytes.Split(path[1:], sep)
		nid := string(params[0])
		tname := string(params[1])
		key := string(params[2])
		_, _, _ = nid, tname, key
	}
}

func BenchmarkSplitNString(b *testing.B) {
	path := []byte("/one/two/three/four")
	sep := "/"
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		params := strings.SplitN(string(path[1:]), sep, 4)
		nid := params[0]
		tname := params[1]
		key := params[2]
		_, _, _ = nid, tname, key
	}
}

func BenchmarkSplitNBytes(b *testing.B) {
	path := []byte("/one/two/three/four")
	sep := []byte("/")
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		params := bytes.SplitN(path[1:], sep, 4)
		nid := string(params[0])
		tname := string(params[1])
		key := string(params[2])
		_, _, _ = nid, tname, key
	}
}
BenchmarkSplitString
BenchmarkSplitString-10     	14783635	        79.58 ns/op	      88 B/op	       2 allocs/op
BenchmarkSplitBytes
BenchmarkSplitBytes-10      	16331946	        72.41 ns/op	      96 B/op	       1 allocs/op
BenchmarkSplitNString
BenchmarkSplitNString-10    	16576970	        71.87 ns/op	      88 B/op	       2 allocs/op
BenchmarkSplitNBytes
BenchmarkSplitNBytes-10     	19375464	        63.30 ns/op	      96 B/op	       1 allocs/op

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially had that that but whatever you gain by that is offset by the fact that you need to redo the conversion in deleteEntry.


params := strings.Split(path[1:], "/")
params := strings.Split(string(path[1:]), "/")
nid := params[0]
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is redundant, as we already know the prefix (which is what we're passing to iterate), which means we can also (instead of only stripping the / prefix), strip /+nid+/, and take params[0], params[1] (instead of 1 and 2);

// Format is "/<networkID>/<tableName>/<endpointID>/<value>",
// trim "/+nid+/" before splitting.
params := bytes.Split(path[len(nid)+2:], []byte("/"))
tname := string(params[0])
key := string(params[1])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

deleteEntry needs both /<tname>/<nid>/<key> and /<nid>/<tname>/<key> so there's no way around it. I mean we could pass the whole string for /<nid>/<tname>/<key> but then we'd have to pass tname nid and key anyway for the first path.

params := strings.Split(string(path[1:]), "/")
nid := params[0]
tname := params[1]
key := params[2]
Copy link
Member

Choose a reason for hiding this comment

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

Silly question (I can't comment on the line below); as the problem was that we were deleting inside the loop, and as we're already taking a lock, would it make sense to collect the list of entries to delete in WalkPrefix, and then delete them outside of it? Would that make it more performant?

Also wondering; it seems we're using the WalkPrefix to collect all entries with the given prefix so that we can delete all of them; if (IIUC), the format is /<networkID>/<tableName>/<endpointID>/<value>, but we only collect /<networkID>/<tableName>/<endpointID>, that means we're manually performing a delete multiple times (once for each <value>). Perhaps we could make use of DeletePrefix() 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I thought about this too. The current logic is very hairy because it's not simply a deleting a bunch of radixes, it also checks for each entry if it was successfully deleted and only then it updates the state of the number of entries in that network: https://github.com/moby/moby/pull/44501/files#diff-7b5363044492af3bcc6d8117470bfc51476d389c7a90670cfd5c62a83c32445aR777

And there's that whole timecompensation logic too. So yeah, again, I tried to change the least amount of logic in an area I'm not very comfortable in. If somebody wants to decipher the timecompensation logic and use DeletePrefix that's just an optimization which is not the purpose of this PR.


func (nDB *NetworkDB) getEntry(tname, nid, key string) (*entry, error) {
e, ok := nDB.indexes[byTable].Get(fmt.Sprintf("/%s/%s/%s", tname, nid, key))
e, ok := nDB.indexes[byTable].Get([]byte(fmt.Sprintf("/%s/%s/%s", tname, nid, key)))
Copy link
Member

Choose a reason for hiding this comment

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

We should probably also consider using concatenation for these simple cases (instead of Sprintf())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it's worth to sacrifice readability in this case.

Comment on lines +770 to +772
func (nDB *NetworkDB) deleteEntry(nid, tname, key string) (okTable bool, okNetwork bool) {
nDB.indexes[byTable], _, okTable = nDB.indexes[byTable].Delete([]byte(fmt.Sprintf("/%s/%s/%s", tname, nid, key)))
nDB.indexes[byNetwork], _, okNetwork = nDB.indexes[byNetwork].Delete([]byte(fmt.Sprintf("/%s/%s/%s", nid, tname, key)))
Copy link
Member

Choose a reason for hiding this comment

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

See my other comment; perhaps if we drop the key for these, we can use DeletePrefix;

Suggested change
func (nDB *NetworkDB) deleteEntry(nid, tname, key string) (okTable bool, okNetwork bool) {
nDB.indexes[byTable], _, okTable = nDB.indexes[byTable].Delete([]byte(fmt.Sprintf("/%s/%s/%s", tname, nid, key)))
nDB.indexes[byNetwork], _, okNetwork = nDB.indexes[byNetwork].Delete([]byte(fmt.Sprintf("/%s/%s/%s", nid, tname, key)))
func (nDB *NetworkDB) deleteEntry(nid, tname string) (okTable bool, okNetwork bool) {
nDB.indexes[byTable], okTable = nDB.indexes[byTable].DeletePrefix([]byte(fmt.Sprintf("/%s/%s", tname, nid)))
nDB.indexes[byNetwork], okNetwork = nDB.indexes[byNetwork].DeletePrefix([]byte(fmt.Sprintf("/%s/%s", nid, tname)))

We may want to do the actual delete outside of the WalkPrefix function in that case though to remove duplicates first (although it looks like DeletePrefix will just ignore if it was already deleted).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What happens when for some unknown reason (something something timeCompensation that I don't understand) not all entries in a prefix are to be deleted? Then you cannot use DeletePrefix, you have to redo a walk for each entry.

This commit allows to remove dependency on the mutable version armon/go-radix.

The go-immutable-radix package is better maintained.

It is likely that a bit more memory will be used when using the
immutable version, though discarded nodes are being reused in a pool.
These changes happen when networks are added/removed or nodes come and
go in a cluster, so we are still talking about a relatively low
frequency event.

The major changes compared to the old radix are when modifying (insert
or delete) a tree, and those are pretty self-contained: we replace the
entire immutable tree under a lock.

Signed-off-by: Tibor Vass <teabee89@gmail.com>
@tiborvass
Copy link
Contributor Author

Rebased

@thaJeztah thaJeztah added status/2-code-review area/networking Networking kind/refactor PR's that refactor, or clean-up code labels Dec 6, 2022
@thaJeztah thaJeztah added this to the v-next milestone Dec 6, 2022
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Networking kind/refactor PR's that refactor, or clean-up code status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants