compressio: Remove chunk size from the wire format for SimpleRW when key=nil.

This change optimizes checkpoint/restore when --compression=none is being used.
Note that runsc never uses a key in SimpleRW.

In practice, the SimpleRW structs are only used for checkpoint/restore.
The caller of the Read/Write methods is the wire package. All the types defined
in the wire package (except String and Ref) translate their save()/load()
implementations to wire.Uint.save()/load().

wire.Uint attempts to be smart and compress the uint64 by reading or writing it
out byte by byte using a particular format (where there MSB indicates whether
more bits are needed to construct this uint64).

So what ends up happening is, the entire kernel is serialized byte by byte
and compressio mostly receives one byte slices to read/write.

For each call to Read/Write in SimpleRW, it adds a 4 byte header representing
"chunk size". So we have 4 bytes for chunk size, followed by 1 byte of data.
This is atrociously wasteful. 80% of the checkpoint file is such chunk sizes.

We only require such chunk size headers when a key is provided to compressio
and there is a hash appended after each chunk. So the chunk size would be
needed to figure out where the data ends and the has begins. But in runsc, key
is never used. So this change gets rid of chunk size from the wire format of
SimpleRW when key=nil.

This should reduce the checkpoint.img file size by 80% and speed up kernel
save and load.

PiperOrigin-RevId: 669404610
This commit is contained in:
Ayush Ranjan
2024-08-30 12:11:10 -07:00
committed by gVisor bot
parent b1cbae9a50
commit 68f0b41bf9
3 changed files with 58 additions and 62 deletions
+53 -57
View File
@@ -27,7 +27,7 @@ import (
// nocompressio provides data storage that does not use data compression but
// offers optional data integrity via SHA-256 hashing.
//
// The stream format is defined as follows.
// When using data integrity option, the stream format is defined as follows:
//
// /------------------------------------------------------\
// | data size (4-bytes) |
@@ -46,14 +46,11 @@ import (
// data
// data size
// SimpleReader is a reader from uncompressed image.
// SimpleReader is a reader for uncompressed image containing hashes.
type SimpleReader struct {
// in is the source.
in io.Reader
// key is the key used to create hash objects.
key []byte
// h is the hash object.
h hash.Hash
@@ -73,20 +70,20 @@ const (
defaultBufSize = 256 * 1024
)
// NewSimpleReader returns a new (uncompressed) reader. If key is non-nil, the data stream
// is assumed to contain expected hash values. See package comments for
// details.
func NewSimpleReader(in io.Reader, key []byte) (*SimpleReader, error) {
r := &SimpleReader{
in: bufio.NewReaderSize(in, defaultBufSize),
key: key,
// NewSimpleReader returns a new (uncompressed) reader. If key is non-nil, the
// data stream is assumed to contain expected hash values. See package comments
// for details.
func NewSimpleReader(in io.Reader, key []byte) io.Reader {
bin := bufio.NewReaderSize(in, defaultBufSize)
if key == nil {
// Since there is no key, this image doesn't use the data integrity stream
// format mentioned in package comments. We can just use the bufio reader.
return bin
}
if key != nil {
r.h = hmac.New(sha256.New, key)
return &SimpleReader{
in: bin,
h: hmac.New(sha256.New, key),
}
return r, nil
}
// Read implements io.Reader.Read.
@@ -103,9 +100,7 @@ func (r *SimpleReader) Read(p []byte) (int, error) {
r.chunkSize = binary.BigEndian.Uint32(r.scratch[:])
r.done = 0
if r.key != nil {
r.h.Reset()
}
r.h.Reset()
if r.chunkSize == 0 {
// this must not happen
@@ -130,28 +125,23 @@ func (r *SimpleReader) Read(p []byte) (int, error) {
return n, err
}
if r.key != nil {
_, _ = r.h.Write(p[:n])
}
_, _ = r.h.Write(p[:n])
r.done += uint32(n)
if r.done >= r.chunkSize {
if r.key != nil {
binary.BigEndian.PutUint32(r.scratch[:], r.chunkSize)
r.h.Write(r.scratch[:])
binary.BigEndian.PutUint32(r.scratch[:], r.chunkSize)
r.h.Write(r.scratch[:])
sum := r.h.Sum(nil)
readerSum := make([]byte, len(sum))
if _, err := io.ReadFull(r.in, readerSum); err != nil {
if err == io.EOF {
return n, io.ErrUnexpectedEOF
}
return n, err
sum := r.h.Sum(nil)
readerSum := make([]byte, len(sum))
if _, err := io.ReadFull(r.in, readerSum); err != nil {
if err == io.EOF {
return n, io.ErrUnexpectedEOF
}
return n, err
}
if !hmac.Equal(readerSum, sum) {
return n, ErrHashMismatch
}
if !hmac.Equal(readerSum, sum) {
return n, ErrHashMismatch
}
r.done = 0
@@ -169,8 +159,8 @@ type SimpleWriter struct {
// out is a buffered writer.
out *bufio.Writer
// key is the key used to create hash objects.
key []byte
// h is the hash object.
h hash.Hash
// closed indicates whether the file has been closed.
closed bool
@@ -182,15 +172,18 @@ type SimpleWriter struct {
var _ io.Writer = (*SimpleWriter)(nil)
var _ io.Closer = (*SimpleWriter)(nil)
// NewSimpleWriter returns a new non-compressing writer. If key is non-nil, hash values are
// generated and written out for compressed bytes. See package comments for
// details.
func NewSimpleWriter(out io.Writer, key []byte) (*SimpleWriter, error) {
return &SimpleWriter{
// NewSimpleWriter returns a new non-compressing writer. If key is non-nil,
// hash values are generated and written out for compressed bytes. See package
// comments for details.
func NewSimpleWriter(out io.Writer, key []byte) *SimpleWriter {
w := &SimpleWriter{
base: out,
out: bufio.NewWriterSize(out, defaultBufSize),
key: key,
}, nil
}
if key != nil {
w.h = hmac.New(sha256.New, key)
}
return w
}
// Write implements io.Writer.Write.
@@ -200,6 +193,10 @@ func (w *SimpleWriter) Write(p []byte) (int, error) {
return 0, io.ErrUnexpectedEOF
}
if w.h == nil {
return w.out.Write(p)
}
l := uint32(len(p))
// chunk length
@@ -214,20 +211,19 @@ func (w *SimpleWriter) Write(p []byte) (int, error) {
return n, err
}
if w.key != nil {
h := hmac.New(sha256.New, w.key)
// Write out the hash.
// chunk data
_, _ = h.Write(p)
// chunk data
_, _ = w.h.Write(p)
// chunk length
binary.BigEndian.PutUint32(w.scratch[:], l)
h.Write(w.scratch[:])
// chunk length
binary.BigEndian.PutUint32(w.scratch[:], l)
w.h.Write(w.scratch[:])
sum := h.Sum(nil)
if _, err := io.CopyN(w.out, bytes.NewReader(sum), int64(len(sum))); err != nil {
return n, err
}
sum := w.h.Sum(nil)
w.h.Reset()
if _, err := io.CopyN(w.out, bytes.NewReader(sum), int64(len(sum))); err != nil {
return n, err
}
return n, nil
+2 -2
View File
@@ -54,10 +54,10 @@ func TestNoCompress(t *testing.T) {
Name: fmt.Sprintf("len(data)=%d, blockSize=%d, key=%s, corruptData=%v", len(data), blockSize, string(key), corruptData),
Data: data,
NewWriter: func(b *bytes.Buffer) (io.Writer, error) {
return NewSimpleWriter(b, key)
return NewSimpleWriter(b, key), nil
},
NewReader: func(b *bytes.Buffer) (io.Reader, error) {
return NewSimpleReader(b, key)
return NewSimpleReader(b, key), nil
},
CorruptData: corruptData,
})
+3 -3
View File
@@ -227,13 +227,13 @@ func NewWriter(w io.Writer, key []byte, metadata map[string]string) (io.WriteClo
// Wrap in compression. When using "best compression" mode, there is usually
// only a little gain in file size reduction, which translate to even smaller
// gain in restore latency reduction, while inccuring much more CPU usage at
// gain in restore latency reduction, while incurring much more CPU usage at
// save time.
if compression == CompressionLevelFlateBestSpeed {
return compressio.NewWriter(w, key, compressionChunkSize, flate.BestSpeed)
}
return compressio.NewSimpleWriter(w, key)
return compressio.NewSimpleWriter(w, key), nil
}
// MetadataUnsafe reads out the metadata from a state file without verifying any
@@ -336,7 +336,7 @@ func NewReader(r io.Reader, key []byte) (io.Reader, map[string]string, error) {
if compression == CompressionLevelFlateBestSpeed {
cr, err = compressio.NewReader(r, key)
} else if compression == CompressionLevelNone {
cr, err = compressio.NewSimpleReader(r, key)
cr = compressio.NewSimpleReader(r, key)
} else {
// Should never occur, as it has the default path.
return nil, nil, fmt.Errorf("metadata contains invalid compression flag value: %v", compression)