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 }