├── trim_periodic.sh ├── drop_cache.sh ├── Makefile ├── run.sh ├── README.md ├── LICENSE └── trimtester.cpp /trim_periodic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while true 4 | do 5 | /sbin/fstrim -v / 6 | sleep 60 7 | done 8 | -------------------------------------------------------------------------------- /drop_cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while true 4 | do 5 | echo 3 > /proc/sys/vm/drop_caches 6 | sleep 120 7 | done 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | g++ -g2 trimtester.cpp -lboost_system -lboost_thread -o TrimTester 3 | 4 | clean: 5 | rm -f *~ TrimTester 6 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | chmod +x ./*.sh 4 | chmod +x ./TrimTester 5 | 6 | ./trim_periodic.sh & 7 | ./drop_cache.sh & 8 | 9 | mkdir -p ./test/ 10 | 11 | rm -rf ./test/* 12 | 13 | while true 14 | do 15 | if ./TrimTester ./test/; then 16 | rm -r ./test/* 17 | else 18 | echo "Corruption found" 19 | break 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trimtester 2 | This repository contains the source code of the testing script we have used to investigate this [trim issue](https://blog.algolia.com/when-solid-state-drives-are-not-that-solid/). 3 | 4 | # Warning 5 | This script writes a significant amount of data on the drives. 6 | 7 | # Requirements 8 | * g++ 9 | * libboost-thread 10 | * libboost-system 11 | 12 | # Testing 13 | ```bash 14 | make 15 | bash ./run.sh 16 | ``` 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Algolia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /trimtester.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* Global mutex used to ensure there is no line mixed in output */ 15 | static boost::mutex staticLoggerMutex; 16 | class LoggerMutex { 17 | public: 18 | LoggerMutex() : _lock(staticLoggerMutex) {} 19 | ~LoggerMutex() {} 20 | 21 | private: 22 | boost::mutex::scoped_lock _lock; 23 | 24 | private: 25 | LoggerMutex(const LoggerMutex&); 26 | LoggerMutex& operator=(const LoggerMutex&); 27 | }; 28 | 29 | class MMapedFile 30 | { 31 | public: 32 | MMapedFile(const char* filename) : fd(-1), mmapData(NULL), mmapDataLen(0), initialized(false) { 33 | fd = open(filename, O_RDONLY); 34 | if (fd == -1) { 35 | return; 36 | } 37 | struct stat s; 38 | if (fstat(fd, &s) == -1) { 39 | const char* msg = strerror(errno); 40 | LoggerMutex lock; 41 | std::cerr << " unable to get stats for " << filename << ":" << msg << std::endl; 42 | close(fd); 43 | fd = -1; 44 | return; 45 | } 46 | if (!S_ISREG(s.st_mode)) { 47 | close(fd); 48 | fd = -1; 49 | return; 50 | } 51 | // Handle the empty file case 52 | if (s.st_size == 0) { 53 | close(fd); 54 | fd = -1; 55 | mmapDataLen = 0; 56 | mmapData = NULL; 57 | initialized = true; 58 | return; 59 | } 60 | 61 | mmapData = mmap (0, s.st_size, PROT_READ, MAP_SHARED | MAP_NORESERVE, fd, 0); 62 | if (mmapData == MAP_FAILED) { 63 | const char* msg = strerror(errno); 64 | LoggerMutex lock; 65 | std::cerr << "Unable to mmap " << filename << ":" << msg << std::endl; 66 | mmapData = NULL; 67 | close(fd); 68 | return; 69 | } 70 | madvise(mmapData, s.st_size, MADV_RANDOM); 71 | mmapDataLen = s.st_size; 72 | initialized = true; 73 | } 74 | 75 | ~MMapedFile() 76 | { 77 | if (mmapData != NULL) 78 | munmap(mmapData, mmapDataLen); 79 | if (fd != -1) 80 | close(fd); 81 | } 82 | 83 | bool loaded() const 84 | { 85 | return initialized; 86 | } 87 | 88 | size_t len() const 89 | { 90 | return mmapDataLen; 91 | } 92 | 93 | const char* content() const 94 | { 95 | return (const char*)mmapData; 96 | } 97 | 98 | private: 99 | int fd; 100 | void* mmapData; 101 | size_t mmapDataLen; 102 | bool initialized; 103 | }; 104 | 105 | /** 106 | * Enumerate content of a directory 107 | */ 108 | class DirContentEnumerator 109 | { 110 | public: 111 | DirContentEnumerator(const char* dir) : _dp(NULL), _dir(NULL) { 112 | _dir = opendir(dir); 113 | unsigned len = offsetof(struct dirent, d_name) + pathconf(dir, _PC_NAME_MAX) + 1; 114 | _dp = (struct dirent*)malloc(len); 115 | next(); 116 | } 117 | ~DirContentEnumerator() { 118 | free(_dp); 119 | if (_dir != NULL) { 120 | closedir(_dir); 121 | } 122 | } 123 | 124 | public: 125 | bool end() { 126 | return _dir == NULL || _path.size() == 0; 127 | } 128 | void next() { 129 | _path.resize(0); 130 | _isDir = false; 131 | if (_dir == NULL) { 132 | return; 133 | } 134 | struct dirent* res = NULL; 135 | if (readdir_r(_dir, _dp, &res) != 0 || res == NULL) { 136 | return; 137 | } 138 | if (_dp->d_name[0] == '.' && (_dp->d_name[1] == 0 || 139 | (_dp->d_name[1] == '.' || _dp->d_name[2] == 0))) { 140 | return next(); 141 | } 142 | _isDir = (_dp->d_type & DT_DIR); 143 | _path.insert(_path.end(), _dp->d_name, _dp->d_name + strlen(_dp->d_name) + 1); 144 | } 145 | 146 | // return true if current entry is a directory 147 | bool isDir() const { 148 | return _isDir; 149 | } 150 | // return the name of the current entry 151 | const char* get() const { 152 | return &_path[0]; 153 | } 154 | 155 | private: 156 | struct dirent* _dp; 157 | DIR* _dir; 158 | std::vector _path; 159 | bool _isDir; 160 | 161 | private: 162 | DirContentEnumerator(); 163 | DirContentEnumerator(const DirContentEnumerator&); 164 | DirContentEnumerator& operator=(const DirContentEnumerator&); 165 | }; 166 | 167 | class DetectCorruption 168 | { 169 | public: 170 | DetectCorruption(const std::string &dataDir) : _dataDir(dataDir) { 171 | boost::thread t(boost::bind(DetectCorruption::_mainLoop, this)); 172 | } 173 | 174 | private: 175 | static void _mainLoop(DetectCorruption *self) { 176 | std::string path, path2, path3, file; 177 | path.append(self->_dataDir); 178 | path2.reserve(1024); 179 | path3.reserve(1024); 180 | file.reserve(1024); 181 | 182 | while (true) { 183 | DirContentEnumerator dirEnum(path.c_str()); 184 | 185 | // Scan All Folder 186 | while (!dirEnum.end()) { 187 | if (!dirEnum.isDir()) { 188 | self->_checkFile(path, dirEnum.get(), file); 189 | } else { 190 | path2.resize(0); 191 | path2.append(path); 192 | path2.push_back('/'); 193 | path2.append(dirEnum.get()); 194 | 195 | DirContentEnumerator dirEnum2(path2.c_str()); 196 | // Scan all files 197 | while (!dirEnum2.end()) { 198 | int len = strlen(dirEnum2.get()); 199 | if (len > 4 && memcmp(dirEnum2.get() + len - 4, ".tmp", 4) == 0) { 200 | // ignore .tmp files 201 | } else { 202 | self->_checkFile(path2, dirEnum2.get(), file); 203 | } 204 | dirEnum2.next(); 205 | } 206 | } 207 | dirEnum.next(); 208 | } 209 | sleep(1); 210 | } 211 | } 212 | 213 | void _checkFile(const std::string &path, const char *file, std::string &filename) { 214 | filename.resize(0); 215 | filename.append(path); 216 | filename.push_back('/'); 217 | filename.append(file); 218 | MMapedFile mmap(filename.c_str()); 219 | if (mmap.loaded()) { 220 | bool corrupted = false; 221 | // Detect all 512-bytes page inside the file filled by 0 -> can be caused by a buggy Trim 222 | for (unsigned i = 0; !corrupted && i < mmap.len(); i += 512) { 223 | if (mmap.len() - i > 4) { // only check page > 4-bytes to avoid false positive 224 | bool pagecorrupted = true; 225 | for (unsigned j = i; j < mmap.len() && j < (i + 512); ++j) { 226 | if (mmap.content()[j] != 0) 227 | pagecorrupted = false; 228 | } 229 | if (pagecorrupted) 230 | corrupted = true; 231 | 232 | } 233 | } 234 | if (corrupted) { 235 | std::cerr << "Corrupted file found: " << filename << std::endl; 236 | exit(1); 237 | } 238 | 239 | } 240 | } 241 | 242 | private: 243 | std::string _dataDir; 244 | }; 245 | 246 | bool sync(const char* path) 247 | { 248 | int fd = open(path, O_RDONLY); 249 | if (fd == -1) 250 | return false; 251 | if (fsync(fd) != 0) { 252 | close(fd); 253 | return false; 254 | } 255 | return (close(fd) == 0); 256 | } 257 | 258 | void writeAtomically(const std::string &dataDir, unsigned folder, unsigned file, uint64_t size, unsigned seed) 259 | { 260 | std::stringstream ss; 261 | ss << dataDir << "/" << folder; 262 | std::string folderPath = ss.str(); 263 | mkdir(dataDir.c_str(), 0777); 264 | mkdir(folderPath.c_str(), 0777); 265 | ss << "/" << file; 266 | std::string destFile = ss.str(); 267 | ss << ".tmp"; 268 | std::string tmpFile = ss.str(); 269 | unsigned char buff[65535]; 270 | 271 | for (unsigned i = 0; i < 65536; ++i) { 272 | buff[i] = (seed + i) % 256; 273 | } 274 | { // write file1.bin.tmp 275 | FILE *file = fopen(tmpFile.c_str(), "wb"); 276 | assert(file != NULL); 277 | uint64_t sizeToWrite = size; 278 | 279 | while (sizeToWrite > 0) { 280 | uint64_t toWrite = (sizeToWrite > 65536 ? 65536 : sizeToWrite); 281 | uint64_t nb = fwrite(buff, 1, toWrite, file); 282 | if (nb != toWrite) { 283 | std::cerr << "Disk full, rm folder & restart the test" << std::endl; 284 | exit(0); 285 | } 286 | sizeToWrite -= toWrite; 287 | } 288 | fflush(file); 289 | fsync(fileno(file)); 290 | fclose(file); 291 | } 292 | // be sure meta data of filesystem are sync 293 | sync(folderPath.c_str()); 294 | rename(tmpFile.c_str(), destFile.c_str()); 295 | sync(folderPath.c_str()); 296 | } 297 | 298 | void deleteFile(const std::string &dataDir, unsigned folder, unsigned file) 299 | { 300 | std::stringstream ss; 301 | ss << dataDir << "/" << folder << "/" << file; 302 | unlink(ss.str().c_str()); 303 | } 304 | 305 | class WriteThread 306 | { 307 | public: 308 | WriteThread(const std::string &dataDir, unsigned instance, unsigned nbLoop) { 309 | _dataDir = dataDir; 310 | _instance = instance; 311 | _nbLoop = nbLoop; 312 | _t = boost::thread(boost::bind(WriteThread::_mainLoop, this)); 313 | } 314 | void join() { 315 | _t.join(); 316 | } 317 | private: 318 | static void _mainLoop(WriteThread *self) { 319 | uint64_t size = 100 * 1024 * 1024; // 100MB 320 | unsigned loop = 0; 321 | 322 | // write 2 small files 323 | writeAtomically(self->_dataDir, self->_instance, 10 * self->_nbLoop + 1, 972, self->_instance); 324 | writeAtomically(self->_dataDir, self->_instance, 10 * self->_nbLoop + 2, 148, self->_instance); 325 | 326 | while (loop < self->_nbLoop) { 327 | // write the file 328 | writeAtomically(self->_dataDir, self->_instance, loop + 1, size, self->_instance); 329 | writeAtomically(self->_dataDir, self->_instance, 10 * self->_nbLoop + 3, 528, self->_instance); 330 | writeAtomically(self->_dataDir, self->_instance, 10 * self->_nbLoop + 4, 2789, self->_instance); 331 | deleteFile(self->_dataDir, self->_instance, self->_nbLoop); 332 | size += 100 * 1024 * 1024; // 100MB 333 | ++loop; 334 | } 335 | } 336 | 337 | private: 338 | std::string _dataDir; 339 | unsigned _instance; 340 | unsigned _nbLoop; 341 | boost::thread _t; 342 | }; 343 | 344 | int main(int argc, char **argv) { 345 | if (argc != 2) { 346 | std::cerr << argv[0] << " path" << std::endl; 347 | std::cout << std::endl; 348 | std::cout << "path: path to write the data " << std::endl; 349 | return 1; 350 | } 351 | std::string dataDir(argv[1]); 352 | DetectCorruption detectCorruption(dataDir); 353 | std::vector threads; 354 | 355 | // write 1024 small files 356 | for (unsigned i = 0; i < 1024; ++i) { 357 | writeAtomically(dataDir, i, 1024 /* file 1024 */, 8 + i /* size */, i/* seed for content */); 358 | } 359 | for (unsigned i = 0; i < 8; ++i) { 360 | threads.push_back(new WriteThread(dataDir, i, 10 + 100 * i)); 361 | } 362 | for (unsigned i = 0; i < threads.size(); ++i) { 363 | threads[i]->join(); 364 | } 365 | return 0; 366 | } 367 | --------------------------------------------------------------------------------