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

chunked: store cache as binary and use a bloom filter #1870

Merged
merged 8 commits into from
Apr 19, 2024

Conversation

giuseppe
Copy link
Member

The bloom filter itself is useful to reduce page faults with the mmap'ed cache files, as it reduces lookups.

Storing the file as a binary instead reduces the file size considerably, with the quay.io/giuseppe/zstd-chunked:fedora-{38,39,40}{,-updated} images I see:

before:

# find -name '=Y2h1bmtlZC1tYW5pZmVzdC1jYWNoZQ==' -exec stat -c '%s' \{\} \;2547644
2575163
2547644
2476816
2462835
2533346

after:

# find -name '=Y2h1bmtlZC1tYW5pZmVzdC1jYWNoZQ==' -exec stat -c '%s' \{\} \;
1319206
1312332
1275803
1270629
1297565

so it is ~50% size reduction

Copy link
Contributor

openshift-ci bot commented Mar 27, 2024

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: giuseppe

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@giuseppe giuseppe changed the title chunked: use a bloom filter to speed up cache lookup and store file as binary chunked: store cache as binary and use a bloom filter Mar 27, 2024
@giuseppe
Copy link
Member Author

@kolyshkin @mtrmac @rhatdan some more improvements to the cache file

@giuseppe giuseppe force-pushed the chunked-bloom-filter branch 2 times, most recently from fb5a6e7 to de1dff9 Compare March 27, 2024 13:11
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

Thanks so much for splitting this into smaller commits. That made the review very enjoyable.

pkg/chunked/cache_linux_test.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux_test.go Outdated Show resolved Hide resolved
@@ -154,6 +162,19 @@ fallback:
return buf, nil, err
}

func getBinaryDigest(stringDigest string) ([]byte, error) {
d, err := digest.Parse(stringDigest)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider having this function accept a digest.Digest instead, and pushing the digest.Parse to callers; on various code paths that compute the digest, that can avoid a digest.Validate (regex evaluation).

OTOH then the other code paths must call Parse or Validate if dealing with untrusted data.

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
if err != nil {
return nil, err
}
digest := append([]byte(d.Algorithm()+":"), digestBytes...)
Copy link
Collaborator

Choose a reason for hiding this comment

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

(More space could be saved by using a single byte to index into a table of algorithm names, or something like that, at the cost of even more complexity. Possibly not worth it, and definitely not blocking this PR.)

bloomFilter: bloomFilter,
digestLen: int(digestLen),
fnames: fnames,
fnamesLen: int(fnamesLen),
Copy link
Collaborator

Choose a reason for hiding this comment

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

(Nit: I’d aesthetically prefer if the type declaration, and the two constructors, were all listing the fields in the same order — and if that order were somehow consistent. Here we have (data, len) for file names, but (len, data) for tags, for example.)

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
@@ -164,7 +167,8 @@ func TestWriteCache(t *testing.T) {
if digest != r.ChunkDigest {
t.Error("wrong digest found")
}
expectedLocation := generateFileLocation(r.Name, uint64(r.ChunkOffset), uint64(r.ChunkSize))
expectedLocation, err := generateFileLocation(0, uint64(r.ChunkOffset), uint64(r.ChunkSize))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Non-blocking: Testing this with at least two distinct paths, to truly exercise the indexing logic, would be nice.

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux.go Show resolved Hide resolved
@giuseppe giuseppe marked this pull request as ready for review April 4, 2024 07:41
@giuseppe
Copy link
Member Author

giuseppe commented Apr 8, 2024

the PR is ready for review

@rhatdan
Copy link
Member

rhatdan commented Apr 8, 2024

@mtrmac needs another review.

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
pkg/chunked/bloom_filter.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

I’m sorry about the delayed response.

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
@giuseppe
Copy link
Member Author

giuseppe commented Apr 9, 2024

I've fixed your comments, except #1870 (comment). What would you like me to do here?

// are stored. $DIGEST has length digestLen stored in the cache file file header.
func generateTag(digest string, offset, len uint64) string {
return fmt.Sprintf("%s%.20d@%.20d", digest, offset, len)
func appendTag(digest []byte, offset, len uint64) ([]byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

(Absolutely non-blocking: the error value is always nil now.)

pkg/chunked/bloom_filter.go Outdated Show resolved Hide resolved
pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
@mtrmac
Copy link
Collaborator

mtrmac commented Apr 9, 2024

I've fixed your comments, except #1870 (comment). What would you like me to do here?

https://github.com/containers/storage/pull/1870/files#r1557856169 , or perhaps I’m missing something.

@rhatdan
Copy link
Member

rhatdan commented Apr 16, 2024

@giuseppe This is waiting on you now?

Copy link
Contributor

@kolyshkin kolyshkin left a comment

Choose a reason for hiding this comment

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

Left some very minor comments. Overall it looks like a good case for protobuf, or is that an overkill?

It looks like appendBinaryDigest's first argument is always []byte{}. First, it could be nil, second, if it's not ever used maybe drop it (and rename the function to getBinaryDigest).

Comment on lines 377 to 378
var vdata bytes.Buffer
var tagsBuffer bytes.Buffer
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

var vdata, tagsBuffer bytes.Buffer

return "", 0, 0
}

nElements := len(cacheFile.tags) / cacheFile.tagLen

i := sort.Search(nElements, func(i int) bool {
d := byteSliceAsString(cacheFile.tags[i*cacheFile.tagLen : i*cacheFile.tagLen+cacheFile.digestLen])
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: with this change, func byteSliceAsString (defined above) can also be removed.

func generateTag(digest string, offset, len uint64) string {
return fmt.Sprintf("%s%.20d@%.20d", digest, offset, len)
func generateTag(digest []byte, offset, len uint64) []byte {
tag := append(digest[:], []byte(fmt.Sprintf("%.20d@%.20d", offset, len))...)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please explain the need for [:] after digest here? I am staring at it and can't see why it's needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, []byte() conversion is redundant here, in Go you can append a string... to a []byte slice.

Copy link
Member Author

Choose a reason for hiding this comment

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

this code is replaced by a next commit. I'll squash the two patches so it will just disappear

Comment on lines +341 to +388
sort.Slice(tags, func(i, j int) bool {
return bytes.Compare(tags[i], tags[j]) == -1
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Easier to use slices.SortFunc(tags, bytes.Compare) here

Copy link
Member Author

Choose a reason for hiding this comment

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

had to revert this change as we are using go 1.20

Copy link
Collaborator

Choose a reason for hiding this comment

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

(BTW we are now agreed on updating to Go 1.21 (around containers/skopeo#2297 ). That would probably be a separate PR.)

for _, k := range toc {
if k.Digest != "" {
digest, err := appendBinaryDigest([]byte{}, k.Digest)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: you can use nil instead of []byte{} here -- appending to nil slice is fine.

Comment on lines 30 to 32
var tagsBuffer bytes.Buffer
var vdata bytes.Buffer
var fnames bytes.Buffer
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

var tagsBuffer, vdata, fnames bytes.Buffer

(unless you don't like that style; to me it's less repetition)

Comment on lines 447 to 478
nameBytes := []byte(name)

if err := binary.Write(&fnames, binary.LittleEndian, uint32(len(nameBytes))); err != nil {
return 0, err
}
if _, err := fnames.Write(nameBytes); err != nil {
return 0, err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You can avoid []byte conversion here, using name directly (as in len(name) and fnames.WriteString(name))

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
@giuseppe
Copy link
Member Author

thanks @mtrmac and @kolyshkin. I've addressed your last comments and pushed a new version

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

LGTM otherwise.

pkg/chunked/cache_linux.go Outdated Show resolved Hide resolved
use the binary representation for a given digest, it helps reducing
the file size by ~25%.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
it helps reducing the cache file size by ~25%.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
use a bloom filter to speed up lookup of digests in a cache file.

The biggest advantage is that it reduces page faults with the mmap'ed
cache file.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
so that the same file path is stored only once in the cache file.

After this change, the cache file measured on the fedora:{38,39,40}
images is in average ~6% smaller.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
it reduces the cache file size by ~3%.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
@mtrmac
Copy link
Collaborator

mtrmac commented Apr 19, 2024

/lgtm

Thanks!

@openshift-ci openshift-ci bot added the lgtm label Apr 19, 2024
@openshift-merge-bot openshift-merge-bot bot merged commit d227439 into containers:main Apr 19, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants