github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/mongo/prealloc.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package mongo
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/juju/utils/arch"
    17  )
    18  
    19  const (
    20  	// preallocAlign must divide all preallocated files' sizes.
    21  	preallocAlign = 4096
    22  )
    23  
    24  var (
    25  	runtimeGOOS  = runtime.GOOS
    26  	hostWordSize = arch.Info[arch.HostArch()].WordSize
    27  
    28  	// zeroes is used by preallocFile to write zeroes to
    29  	// preallocated Mongo data files.
    30  	zeroes = make([]byte, 64*1024)
    31  
    32  	minOplogSizeMB = 512
    33  	maxOplogSizeMB = 1024
    34  
    35  	availSpace   = fsAvailSpace
    36  	preallocFile = doPreallocFile
    37  )
    38  
    39  // preallocOplog preallocates the Mongo oplog in the
    40  // specified Mongo datadabase directory.
    41  func preallocOplog(dir string, oplogSizeMB int) error {
    42  	// preallocFiles expects sizes in bytes.
    43  	sizes := preallocFileSizes(oplogSizeMB * 1024 * 1024)
    44  	prefix := filepath.Join(dir, "local.")
    45  	return preallocFiles(prefix, sizes...)
    46  }
    47  
    48  // defaultOplogSize returns the default size in MB for the
    49  // mongo oplog based on the directory of the mongo database.
    50  //
    51  // The size of the oplog is calculated according to the
    52  // formula used by Mongo:
    53  //     http://docs.mongodb.org/manual/core/replica-set-oplog/
    54  //
    55  // NOTE: we deviate from the specified minimum and maximum
    56  //       sizes. Mongo suggests a minimum of 1GB and maximum
    57  //       of 50GB; we set these to 512MB and 1GB respectively.
    58  func defaultOplogSize(dir string) (int, error) {
    59  	if hostWordSize == 32 {
    60  		// "For 32-bit systems, MongoDB allocates about 48 megabytes
    61  		// of space to the oplog."
    62  		return 48, nil
    63  	}
    64  
    65  	// "For 64-bit OS X systems, MongoDB allocates 183 megabytes of
    66  	// space to the oplog."
    67  	if runtimeGOOS == "darwin" {
    68  		return 183, nil
    69  	}
    70  
    71  	// FIXME calculate disk size on Windows like on Linux below.
    72  	if runtimeGOOS == "windows" {
    73  		return minOplogSizeMB, nil
    74  	}
    75  
    76  	avail, err := availSpace(dir)
    77  	if err != nil {
    78  		return -1, err
    79  	}
    80  	size := int(avail * 0.05)
    81  	if size < minOplogSizeMB {
    82  		size = minOplogSizeMB
    83  	} else if size > maxOplogSizeMB {
    84  		size = maxOplogSizeMB
    85  	}
    86  	return size, nil
    87  }
    88  
    89  // fsAvailSpace returns the available space in MB on the
    90  // filesystem containing the specified directory.
    91  func fsAvailSpace(dir string) (avail float64, err error) {
    92  	var stderr bytes.Buffer
    93  	cmd := exec.Command("df", dir)
    94  	cmd.Stderr = &stderr
    95  	out, err := cmd.Output()
    96  	if err != nil {
    97  		err := fmt.Errorf("df failed: %v", err)
    98  		if stderr.Len() > 0 {
    99  			err = fmt.Errorf("%s (%q)", err, stderr.String())
   100  		}
   101  		return -1, err
   102  	}
   103  	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
   104  	if len(lines) < 2 {
   105  		logger.Errorf("unexpected output: %q", out)
   106  		return -1, fmt.Errorf("could not determine available space on %q", dir)
   107  	}
   108  	fields := strings.Fields(lines[1])
   109  	if len(fields) < 4 {
   110  		logger.Errorf("unexpected output: %q", out)
   111  		return -1, fmt.Errorf("could not determine available space on %q", dir)
   112  	}
   113  	kilobytes, err := strconv.Atoi(fields[3])
   114  	if err != nil {
   115  		return -1, err
   116  	}
   117  	return float64(kilobytes) / 1024, err
   118  }
   119  
   120  // preallocFiles preallocates n data files, zeroed to make
   121  // up the specified sizes in bytes. The file sizes must be
   122  // multiples of 4096 bytes.
   123  //
   124  // The filenames are constructed by appending the file index
   125  // to the specified prefix.
   126  func preallocFiles(prefix string, sizes ...int) error {
   127  	var err error
   128  	var createdFiles []string
   129  	for i, size := range sizes {
   130  		var created bool
   131  		filename := fmt.Sprintf("%s%d", prefix, i)
   132  		created, err = preallocFile(filename, size)
   133  		if created {
   134  			createdFiles = append(createdFiles, filename)
   135  		}
   136  		if err != nil {
   137  			break
   138  		}
   139  	}
   140  	if err != nil {
   141  		logger.Debugf("cleaning up after preallocation failure: %v", err)
   142  		for _, filename := range createdFiles {
   143  			if err := os.Remove(filename); err != nil {
   144  				logger.Errorf("failed to remove %q: %v", filename, err)
   145  			}
   146  		}
   147  	}
   148  	return err
   149  }
   150  
   151  // preallocFileSizes returns a slice of file sizes
   152  // that make up the specified total size, exceeding
   153  // the specified total as necessary to pad the
   154  // remainder to a multiple of 4096 bytes.
   155  func preallocFileSizes(totalSize int) []int {
   156  	// Divide the total size into 512MB chunks, and
   157  	// then round up the remaining chunk to a multiple
   158  	// of 4096 bytes.
   159  	const maxChunkSize = 512 * 1024 * 1024
   160  	var sizes []int
   161  	remainder := totalSize % maxChunkSize
   162  	if remainder > 0 {
   163  		aligned := remainder + preallocAlign - 1
   164  		aligned = aligned - (aligned % preallocAlign)
   165  		sizes = []int{aligned}
   166  	}
   167  	for i := 0; i < totalSize/maxChunkSize; i++ {
   168  		sizes = append(sizes, maxChunkSize)
   169  	}
   170  	return sizes
   171  }
   172  
   173  // doPreallocFile creates a file and writes zeroes up to the specified
   174  // extent. If the file exists already, nothing is done and no error
   175  // is returned.
   176  func doPreallocFile(filename string, size int) (created bool, err error) {
   177  	if size%preallocAlign != 0 {
   178  		return false, fmt.Errorf("specified size %v for file %q is not a multiple of %d", size, filename, preallocAlign)
   179  	}
   180  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700)
   181  	if os.IsExist(err) {
   182  		// already exists, don't overwrite
   183  		return false, nil
   184  	}
   185  	if err != nil {
   186  		return false, fmt.Errorf("failed to open mongo prealloc file %q: %v", filename, err)
   187  	}
   188  	defer f.Close()
   189  	for written := 0; written < size; {
   190  		n := len(zeroes)
   191  		if n > (size - written) {
   192  			n = size - written
   193  		}
   194  		n, err := f.Write(zeroes[:n])
   195  		if err != nil {
   196  			return true, fmt.Errorf("failed to write to mongo prealloc file %q: %v", filename, err)
   197  		}
   198  		written += n
   199  	}
   200  	return true, nil
   201  }