Improve chunk and chunkDesc loading.

Also, clean up some things in the code (especially introduction of the
chunkLenWithHeader constant to avoid the same expression all over the place).

Benchmark results:

BEFORE
BenchmarkLoadChunksSequentially     5000            283580 ns/op          152143 B/op        312 allocs/op
BenchmarkLoadChunksRandomly        20000             82936 ns/op           39310 B/op         99 allocs/op
BenchmarkLoadChunkDescs            10000            110833 ns/op           15092 B/op        345 allocs/op

AFTER
BenchmarkLoadChunksSequentially    10000            146785 ns/op          152285 B/op        315 allocs/op
BenchmarkLoadChunksRandomly        20000             67598 ns/op           39438 B/op        103 allocs/op
BenchmarkLoadChunkDescs            20000             99631 ns/op           12636 B/op        192 allocs/op

Note that everything is obviously loaded from the page cache (as the
benchmark runs thousands of times with very small series files). In a
real-world scenario, I expect a larger impact, as the disk operations
will more often actually hit the disk. To load ~50 sequential chunks,
this reduces the iops from 100 seeks and 100 reads to 1 seek and 1
read.
This commit is contained in:
beorn7 2015-04-13 20:20:26 +02:00
parent c563398c68
commit b02d900e61
5 changed files with 64 additions and 45 deletions

View file

@ -192,6 +192,7 @@ type chunk interface {
newIterator() chunkIterator newIterator() chunkIterator
marshal(io.Writer) error marshal(io.Writer) error
unmarshal(io.Reader) error unmarshal(io.Reader) error
unmarshalFromBuf([]byte)
encoding() chunkEncoding encoding() chunkEncoding
// values returns a channel, from which all sample values in the chunk // values returns a channel, from which all sample values in the chunk
// can be received in order. The channel is closed after the last // can be received in order. The channel is closed after the last

View file

@ -191,8 +191,8 @@ func (p *persistence) sanitizeSeries(
return fp, false return fp, false
} }
bytesToTrim := fi.Size() % int64(chunkLen+chunkHeaderLen) bytesToTrim := fi.Size() % int64(chunkLenWithHeader)
chunksInFile := int(fi.Size()) / (chunkLen + chunkHeaderLen) chunksInFile := int(fi.Size()) / chunkLenWithHeader
modTime := fi.ModTime() modTime := fi.ModTime()
if bytesToTrim != 0 { if bytesToTrim != 0 {
glog.Warningf( glog.Warningf(

View file

@ -223,18 +223,20 @@ func (c deltaEncodedChunk) marshal(w io.Writer) error {
// unmarshal implements chunk. // unmarshal implements chunk.
func (c *deltaEncodedChunk) unmarshal(r io.Reader) error { func (c *deltaEncodedChunk) unmarshal(r io.Reader) error {
*c = (*c)[:cap(*c)] *c = (*c)[:cap(*c)]
readBytes := 0 if _, err := io.ReadFull(r, *c); err != nil {
for readBytes < len(*c) { return err
n, err := r.Read((*c)[readBytes:])
if err != nil {
return err
}
readBytes += n
} }
*c = (*c)[:binary.LittleEndian.Uint16((*c)[deltaHeaderBufLenOffset:])] *c = (*c)[:binary.LittleEndian.Uint16((*c)[deltaHeaderBufLenOffset:])]
return nil return nil
} }
// unmarshalFromBuf implements chunk.
func (c *deltaEncodedChunk) unmarshalFromBuf(buf []byte) {
*c = (*c)[:cap(*c)]
copy(*c, buf)
*c = (*c)[:binary.LittleEndian.Uint16((*c)[deltaHeaderBufLenOffset:])]
}
// values implements chunk. // values implements chunk.
func (c deltaEncodedChunk) values() <-chan *metric.SamplePair { func (c deltaEncodedChunk) values() <-chan *metric.SamplePair {
n := c.len() n := c.len()

View file

@ -231,18 +231,20 @@ func (c doubleDeltaEncodedChunk) marshal(w io.Writer) error {
// unmarshal implements chunk. // unmarshal implements chunk.
func (c *doubleDeltaEncodedChunk) unmarshal(r io.Reader) error { func (c *doubleDeltaEncodedChunk) unmarshal(r io.Reader) error {
*c = (*c)[:cap(*c)] *c = (*c)[:cap(*c)]
readBytes := 0 if _, err := io.ReadFull(r, *c); err != nil {
for readBytes < len(*c) { return err
n, err := r.Read((*c)[readBytes:])
if err != nil {
return err
}
readBytes += n
} }
*c = (*c)[:binary.LittleEndian.Uint16((*c)[doubleDeltaHeaderBufLenOffset:])] *c = (*c)[:binary.LittleEndian.Uint16((*c)[doubleDeltaHeaderBufLenOffset:])]
return nil return nil
} }
// unmarshalFromBuf implements chunk.
func (c *doubleDeltaEncodedChunk) unmarshalFromBuf(buf []byte) {
*c = (*c)[:cap(*c)]
copy(*c, buf)
*c = (*c)[:binary.LittleEndian.Uint16((*c)[doubleDeltaHeaderBufLenOffset:])]
}
// values implements chunk. // values implements chunk.
func (c doubleDeltaEncodedChunk) values() <-chan *metric.SamplePair { func (c doubleDeltaEncodedChunk) values() <-chan *metric.SamplePair {
n := c.len() n := c.len()

View file

@ -63,13 +63,19 @@ const (
chunkHeaderTypeOffset = 0 chunkHeaderTypeOffset = 0
chunkHeaderFirstTimeOffset = 1 chunkHeaderFirstTimeOffset = 1
chunkHeaderLastTimeOffset = 9 chunkHeaderLastTimeOffset = 9
chunkLenWithHeader = chunkLen + chunkHeaderLen
chunkMaxBatchSize = 64 // How many chunks to load at most in one batch.
indexingMaxBatchSize = 1024 * 1024 indexingMaxBatchSize = 1024 * 1024
indexingBatchTimeout = 500 * time.Millisecond // Commit batch when idle for that long. indexingBatchTimeout = 500 * time.Millisecond // Commit batch when idle for that long.
indexingQueueCapacity = 1024 * 16 indexingQueueCapacity = 1024 * 16
) )
var fpLen = len(clientmodel.Fingerprint(0).String()) // Length of a fingerprint as string. var (
fpLen = len(clientmodel.Fingerprint(0).String()) // Length of a fingerprint as string.
byteBufPool = sync.Pool{New: func() interface{} { return make([]byte, 0, 3*chunkLenWithHeader) }}
)
const ( const (
flagHeadChunkPersisted byte = 1 << iota flagHeadChunkPersisted byte = 1 << iota
@ -377,28 +383,37 @@ func (p *persistence) loadChunks(fp clientmodel.Fingerprint, indexes []int, inde
defer f.Close() defer f.Close()
chunks := make([]chunk, 0, len(indexes)) chunks := make([]chunk, 0, len(indexes))
typeBuf := make([]byte, 1) buf := byteBufPool.Get().([]byte)
for _, idx := range indexes { defer func() {
_, err := f.Seek(offsetForChunkIndex(idx+indexOffset), os.SEEK_SET) byteBufPool.Put(buf)
if err != nil { }()
for i := 0; i < len(indexes); i++ {
// This loads chunks in batches. A batch is a streak of
// consecutive chunks, read from disk in one go.
batchSize := 1
if _, err := f.Seek(offsetForChunkIndex(indexes[i]+indexOffset), os.SEEK_SET); err != nil {
return nil, err return nil, err
} }
n, err := f.Read(typeBuf) for ; batchSize < chunkMaxBatchSize &&
if err != nil { i+1 < len(indexes) &&
return nil, err indexes[i]+1 == indexes[i+1]; i, batchSize = i+1, batchSize+1 {
} }
if n != 1 { readSize := batchSize * chunkLenWithHeader
panic("read returned != 1 bytes") if cap(buf) < readSize {
buf = make([]byte, readSize)
} }
buf = buf[:readSize]
_, err = f.Seek(chunkHeaderLen-1, os.SEEK_CUR) if _, err := io.ReadFull(f, buf); err != nil {
if err != nil {
return nil, err return nil, err
} }
chunk := newChunkForEncoding(chunkEncoding(typeBuf[0])) for c := 0; c < batchSize; c++ {
chunk.unmarshal(f) chunk := newChunkForEncoding(chunkEncoding(buf[c*chunkLenWithHeader+chunkHeaderTypeOffset]))
chunks = append(chunks, chunk) chunk.unmarshalFromBuf(buf[c*chunkLenWithHeader+chunkHeaderLen:])
chunks = append(chunks, chunk)
}
} }
chunkOps.WithLabelValues(load).Add(float64(len(chunks))) chunkOps.WithLabelValues(load).Add(float64(len(chunks)))
atomic.AddInt64(&numMemChunks, int64(len(chunks))) atomic.AddInt64(&numMemChunks, int64(len(chunks)))
@ -422,24 +437,23 @@ func (p *persistence) loadChunkDescs(fp clientmodel.Fingerprint, beforeTime clie
if err != nil { if err != nil {
return nil, err return nil, err
} }
totalChunkLen := chunkHeaderLen + chunkLen if fi.Size()%int64(chunkLenWithHeader) != 0 {
if fi.Size()%int64(totalChunkLen) != 0 {
p.setDirty(true) p.setDirty(true)
return nil, fmt.Errorf( return nil, fmt.Errorf(
"size of series file for fingerprint %v is %d, which is not a multiple of the chunk length %d", "size of series file for fingerprint %v is %d, which is not a multiple of the chunk length %d",
fp, fi.Size(), totalChunkLen, fp, fi.Size(), chunkLenWithHeader,
) )
} }
numChunks := int(fi.Size()) / totalChunkLen numChunks := int(fi.Size()) / chunkLenWithHeader
cds := make([]*chunkDesc, 0, numChunks) cds := make([]*chunkDesc, 0, numChunks)
chunkTimesBuf := make([]byte, 16)
for i := 0; i < numChunks; i++ { for i := 0; i < numChunks; i++ {
_, err := f.Seek(offsetForChunkIndex(i)+chunkHeaderFirstTimeOffset, os.SEEK_SET) _, err := f.Seek(offsetForChunkIndex(i)+chunkHeaderFirstTimeOffset, os.SEEK_SET)
if err != nil { if err != nil {
return nil, err return nil, err
} }
chunkTimesBuf := make([]byte, 16)
_, err = io.ReadAtLeast(f, chunkTimesBuf, 16) _, err = io.ReadAtLeast(f, chunkTimesBuf, 16)
if err != nil { if err != nil {
return nil, err return nil, err
@ -799,7 +813,7 @@ func (p *persistence) loadSeriesMapAndHeads() (sm *seriesMap, chunksToPersist in
} }
chunk := newChunkForEncoding(chunkEncoding(encoding)) chunk := newChunkForEncoding(chunkEncoding(encoding))
if err := chunk.unmarshal(r); err != nil { if err := chunk.unmarshal(r); err != nil {
glog.Warning("Could not decode chunk type:", err) glog.Warning("Could not decode chunk:", err)
p.dirty = true p.dirty = true
return sm, chunksToPersist, nil return sm, chunksToPersist, nil
} }
@ -900,7 +914,7 @@ func (p *persistence) dropAndPersistChunks(
return return
} }
headerBuf := make([]byte, chunkHeaderLen) headerBuf := make([]byte, chunkHeaderLen)
_, err = io.ReadAtLeast(f, headerBuf, chunkHeaderLen) _, err = io.ReadFull(f, headerBuf)
if err == io.EOF { if err == io.EOF {
// We ran into the end of the file without finding any chunks that should // We ran into the end of the file without finding any chunks that should
// be kept. Remove the whole file. // be kept. Remove the whole file.
@ -960,7 +974,7 @@ func (p *persistence) dropAndPersistChunks(
if err != nil { if err != nil {
return return
} }
offset = int(written / (chunkHeaderLen + chunkLen)) offset = int(written / chunkLenWithHeader)
if len(chunks) > 0 { if len(chunks) > 0 {
if err = writeChunks(temp, chunks); err != nil { if err = writeChunks(temp, chunks); err != nil {
@ -983,7 +997,7 @@ func (p *persistence) deleteSeriesFile(fp clientmodel.Fingerprint) (int, error)
if err != nil { if err != nil {
return -1, err return -1, err
} }
numChunks := int(fi.Size() / (chunkHeaderLen + chunkLen)) numChunks := int(fi.Size() / chunkLenWithHeader)
if err := os.Remove(fname); err != nil { if err := os.Remove(fname); err != nil {
return -1, err return -1, err
} }
@ -1366,17 +1380,17 @@ loop:
} }
func offsetForChunkIndex(i int) int64 { func offsetForChunkIndex(i int) int64 {
return int64(i * (chunkHeaderLen + chunkLen)) return int64(i * chunkLenWithHeader)
} }
func chunkIndexForOffset(offset int64) (int, error) { func chunkIndexForOffset(offset int64) (int, error) {
if int(offset)%(chunkHeaderLen+chunkLen) != 0 { if int(offset)%chunkLenWithHeader != 0 {
return -1, fmt.Errorf( return -1, fmt.Errorf(
"offset %d is not a multiple of on-disk chunk length %d", "offset %d is not a multiple of on-disk chunk length %d",
offset, chunkHeaderLen+chunkLen, offset, chunkLenWithHeader,
) )
} }
return int(offset) / (chunkHeaderLen + chunkLen), nil return int(offset) / chunkLenWithHeader, nil
} }
func writeChunkHeader(w io.Writer, c chunk) error { func writeChunkHeader(w io.Writer, c chunk) error {
@ -1389,7 +1403,7 @@ func writeChunkHeader(w io.Writer, c chunk) error {
} }
func writeChunks(w io.Writer, chunks []chunk) error { func writeChunks(w io.Writer, chunks []chunk) error {
b := bufio.NewWriterSize(w, len(chunks)*(chunkHeaderLen+chunkLen)) b := bufio.NewWriterSize(w, len(chunks)*chunkLenWithHeader)
for _, chunk := range chunks { for _, chunk := range chunks {
if err := writeChunkHeader(b, chunk); err != nil { if err := writeChunkHeader(b, chunk); err != nil {
return err return err