mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user