Оптимальный способ хранения большого количества файлов на сервере

Как известно в одной папке не стоит хранить большое количество файлов т.к. очень быстро может произойти сбой в системе или попросту файлы будут очень медленно считываться.

Для решения этой задачи многие программисты берут md5 имени файла f789f789abc898d6892df98d09a8f8, после чего разбивают имя примерно таким образом:

/f7/89/f789abc898d6892df98d09a8f8.jpg

Математика тут очень проста — один символ это 16 вариантов.
Таким образом 2 символа это уже 16*16=256 вариантов.
В нашем случае у нас 2 вложенности по 2 символа, таким образом максимальное количество папок будет 256*256=65536 папок.
Если нам потребуется сохранить 1000000 файлов то число файлов в каждой папке не превысит 1000000/65536=15 файлов.

Да, вариант прост, но что если нам требуется не только хорошо сохранять файлы, но и еще быстро их находить?

Например у нас социальная сеть и мы хотим для каждого пользователя создать отдельную папку с номером его id и хранить в ней файлы которые в свою очередь тоже имеют свой id.
И для нас важно не только сохранить файл но и быстро найти где он лежит по его id.

Для решения этой задачи я написал класс, который позволяет сохранять на сервере большое количество файлов или папок в древовидной структуре папок.

Вот какую структуру создает класс:
image

Чтобы посчитать максимально число файлов которое уместится в этой структуре нужно возвести максимальное количество файлов в папке в степень количества ветвей плюс один.

На изображении мы видим 2 ветви и по 3 файла в каждой папке.
Таким образом 3 нужно возвести в степень 2+1 = 3*3*3=27 файлов.

Для сохранения не более 1000000 файлов в такой структуре нам хватит 2 ветви по 100 файлов в каждой папке (100*100*100).

В класс нужно передать массив параметров — путь к папке где будет строиться дерево, максимальное число файлов в папке, число ветвей, либо можно применить паттерн (параметр pattern) максимального числа файлов, который уже был заранее просчитан — bigint, int, mediumint, smallint:

array('upload_dir'=>Q_PATH.'/uploads/','max_file_count'=>1000,'branches'=>2,'pattern'=>'')

Сам класс:
<?php

//file index
define("Q_PATH",dirname(__FILE__));

//
class Functions {
	
	public static function arr_union(array $def_arr,array $new_arr) {
		foreach($new_arr as $key => $value) {
			if(array_key_exists($key, $def_arr) && is_array($value)) {
				$def_arr[$key]=self::arr_union($def_arr[$key], $new_arr[$key]);
			}
			else {
				$def_arr[$key]=$value;
			}
		}
		return $def_arr;
	}
}

/**
 * Класс построения дерева
 */
class Upload {
    
    public $id;
    private $upload_dir;
    private $max_file_count;
    private $branches;

    public function __construct(array $param=array()) {
        $def_param=array('upload_dir'=>Q_PATH.'/uploads/','max_file_count'=>1000,'branches'=>2,'pattern'=>'');
        $upload_param=Functions::arr_union($def_param,$param);
        $this->upload_dir=$upload_param['upload_dir'];
        $this->max_file_count=$upload_param['max_file_count'];
        $this->branches=$upload_param['branches'];
        //сложность надумана, все зависит от инодов df -i и tune2fs -l /dev/hda1 и df -Ti
        switch($upload_param['pattern']) {
            case 'bigint':
                $this->max_file_count=512;
                $this->branches=6;
            break;
            case 'int':
                $this->max_file_count=216;
                $this->branches=3;
            break;
            case 'mediumint':
                $this->max_file_count=204;
                $this->branches=2;
            break;
            case 'smallint':
                $this->max_file_count=182;
                $this->branches=1;
            break;
        }
        $this->del_id();
    }
    
    public function set_id($id) {
        $this->id=$id;
    }
	
    public function del_id() {
        $this->id=0;
    }
    
    public function find_upload($url) {
        if(is_file($url)) {
            return true;
        }
        else {
            return false;
        }
    }
    
    public function get_upload($id,$fl) {
        $this->set_id($id);
        for($i=$this->branches;$i>=1;$i--) {
            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
        }
        $dir_file_str=implode("/", $dir_file_arr);
        return $this->upload_dir.$dir_file_str.'/'.$this->id.$fl;
    }
    
    public function put_upload($id,$fl,$data) {
        $this->set_id($id);
        for($i=$this->branches;$i>=1;$i--) {
            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
            
            $dir_file_str=implode("/", $dir_file_arr);
            if(!is_dir($this->upload_dir.$dir_file_str)) {
                mkdir($this->upload_dir.$dir_file_str, 0777);
                //chmod($this->upload_dir.$dir_file_str, 0777);
            }
        }
        file_put_contents($this->upload_dir.$dir_file_str.'/'.$this->id.$fl, $data);
        return $this->upload_dir.$dir_file_str.'/'.$this->id.$fl;
    }
    
    public function set_upload($id,$fl) {
        $this->set_id($id);
        for($i=$this->branches;$i>=1;$i--) {
            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
            
            $dir_file_str=implode("/", $dir_file_arr);
            if(!is_dir($this->upload_dir.$dir_file_str)) {
                mkdir($this->upload_dir.$dir_file_str, 0777);
                //chmod($this->upload_dir.$dir_file_str, 0777);
            }
        }
        return $this->upload_dir.$dir_file_str.'/'.$this->id.$fl;
    }
    
    public function get_upload_dir($id) {
        $this->set_id($id);
        for($i=$this->branches;$i>=1;$i--) {
            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
        }
            $dir_file_str=implode("/", $dir_file_arr);
        return $this->upload_dir.$dir_file_str.'/'.$this->id;
    }
    
    public function set_upload_dir($id) {
        $this->set_id($id);
        for($i=$this->branches;$i>=1;$i--) {
            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
            
            $dir_file_str=implode("/", $dir_file_arr);
            if(!is_dir($this->upload_dir.$dir_file_str)) {
                mkdir($this->upload_dir.$dir_file_str, 0777);
                //chmod($this->upload_dir.$dir_file_str, 0777);
            }
        }
            if(!is_dir($this->upload_dir.$dir_file_str.'/'.$this->id)) {
                mkdir($this->upload_dir.$dir_file_str.'/'.$this->id, 0777);
                //chmod($this->upload_dir.$dir_file_str.'/'.$this->id, 0777);
            }
        return $this->upload_dir.$dir_file_str.'/'.$this->id;
    }

}



Скачать в архиве

Для варианта социальной сети описанной выше, требуется 2 раза использовать класс: вначале для построения дерева папок, потом для построения в каждой папке дерева для файлов.

Так же обращаю ваше внимание, что в этом посте я опустил (а не не знал) тему «Максимально допустимое количество файлов на жестком диске».
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 41

    +15
    Библиотеки, даже самые маленькие, лучше распространять через github. А библиотеки на PHP — через github + composer
      –3
      Может вместо написания костылей настроит файловую систему и дисковую подсистему ОС?
        0
        Это скорее не костыли, а внедорожник.
        Вы же верстаете так, что бы поддерживать устаревшие браузеры.
        Можно поставить себе современный браузер, можно настроить ФС.
        Но скорее всего, рано рано или поздно, учитывая текущие реалии, проект может попасть к кому-то, у кого она не настроена и кому по тем или иным причинам от нее что-то будет нужно. И вот тут-то Вас и вспомнят «добрым» словом.
          +1
          это хороший совет, но что делать с 27 миллионами файлов? к которым в любой момент может потребоваться быстрый доступ
            0
            Действительно, как не настраивай файловую систему, но если 6 Терабайт (цифра из реального проекта, средний размер файла пол мегабайта) картинок закинуть в одну папку результат будет плачевный…
              0
              Я правильно понял 6 Тер = 6 000 000 000 0000 и 6 000 000 000 0000 / 500 000 ~ 12 млн. файлов?
              Вот уж не знаю какие вы операционные системы эксплуатируете, но у меня на обычно ноуте живет каталог примерно с 50 млн. файлов каждый по 680 байт.
              Он конечно занимает 200 Гб дискового пространства, но каких либо тормозов я не заметил. Аппликейшин на java, запущенный на этой же машине (где не производилось ни каких оптимизаций) произвольно находит (читает в поток) любой файл в пределах 2-х секунд. Имя файла это sha2-224
                +2
                на Вашем ноутбуке тоже было порядка 650000 запросов в сутки?
                  0
                  7 запросов в секунду? Если бы вы были внимательны то я писал что ноут это система без оптимизации.
                  Но вот только что изменил настройки ФС и застил считывать весь блок с inode в память. Буферы выросли до просто неприемлемых 3 Гб. Скорость доступа выросла всего 20 раз теперь файл читается за 0,1 сек. Работать просто невозможно из 15 Гб памяти что было доступно до оптимизации теперь доступно всего лишь 6 Гб. Пришлось зарезать яву машину до 4 Гб.
                  Так что вы правы лучше использовать странный костыль чем пытаться настроить систем из-за жалкого прироста производительности в 20 раз.
                    0
                    ну он не странный)))
                    а по поводу серверов, там еще куча процессов, и потом, найдя файл сервер начинает его отдавать, занимая ресурсы и памяти, и канала и винта…
            +3
            Далее, имхо, и вполне возможно я ошибаюсь.

            1) Без PSR-0 класс будет использовать неудобно.

            2) Откуда нам брать Functions::arr_union()? Полагаю, тут можно использовать array_merge()

            3) В find_upload(), как я понял, передается путь к файлу или относительный URL, но почему там не учитывается что текущий каталог может быть другой?

            4) Если мы конструируем объект с настройками загрузки, а потом отдельно грузим файл, то по идее это должен быть синглтон. Иначе, почему бы не передавать файл на загрузку сразу в конструктор с настройками загрузки?

            5) Что будет если однажды передать другие настройки сохранения файлов, оставив ту же папку для аплоада?

            6) А как удалять файл?
              +1
              7) Не совсем понимаю зачем нужны get_upload_dir() и set_upload_dir() публично. Хотя они даже в классе нигде не используются. Плюс, set_upload_dir() копирует функционал get_upload_dir(), отличаются только созданием директорий.

              8) Аналогичная ситуация с get_upload() и set_upload().

              9) Самое главное, нигде в коде не увидел move_uploaded_file(). Как класс не может сохранять загруженные файлы? Сначала надо куда-то сохранить и только потом ему «скармливать»?
                0
                Как то сильно похоже на ухудшенную копию… Рад ошибиться.
                Работаем с php классом файлового хранилища.
                TangoPHP файловое хранилище.
                0
                Да, вариант прост, но что если нам требуется не только хорошо сохранять файлы, но и еще быстро их находить?

                Например у нас социальная сеть и мы хотим для каждого пользователя создать отдельную папку с номером его id и хранить в ней файлы которые в свою очередь тоже имеют свой id.
                И для нас важно не только сохранить файл но и быстро найти где он лежит по его id.

                Для этого данные о загруженных файлах можно хранить в БД и не беспокоить админов постоянными тормошениями файловой системы.
                  +3
                  >Для этого данные о загруженных файлах можно хранить в БД
                  NoSQL(наприем редис) — доступ быстрее будет :)
                  и вообще я не вижу надобности в этом классе. Если уж мы проектируем социальную сеть, то файлы должны лежать в статическом хранилище в удобном для быстрого вытаскивания виде. А внешние красивые урлы всегда можно перенаправить на nginx или апачем так как нам нужно.
                  +1
                  давно, давно — Статья на хабре — Кстати в комментариях есть почему ограничено количество файлов в каталоге.
                  Не так давно —
                  Обработка файлов на сервере
                  TangoPHP файловое хранилище.
                  Работаем с php классом файлового хранилища
                    +3
                    Поздравляю, вы изобрели хеш-таблицу с хеш функцией MD5 и хранением бакетов в ФС!
                      0
                      Twig тоже что-то подобное делает:

                      AB/
                      --CD/
                      ----EFABC… (длинный хеш)

                      Только не узнавал что значат первые два уровня, в хеше эти данные вроде-как не содержатся
                        0
                        habrahabr.ru/post/70147/#comment_7727955 я случайно не туда написал. Ответьте.
                          0
                          В посте говорится о простоте поиска которого хоть убейте не вижу!
                          Я храню md5 в таблице и для поиска знаю, что файл лежит по адресу a/b/c/abcdef......gif
                          В Вашем случае путь к файлу все равно нужно где то хранить?
                            0
                            От файловой системы многое зависит. На raiserfs в свое время пробовал — разницы между одной директорией и иерархией не заметил. На ext3 разница была существенная.
                              0
                              Сколько файлов было в одной директории? на примерно терабайте эксперементировал разница одна папка 5-10с иерархия 5-7 мс. средний размер файла 300-500 килобайт. доставал скриптом.
                                0
                                Несколько тысяч. Давно пробовал, точнее не скажу.
                                Файлы делал пустыми.
                                  –1
                                  пустой в смысле нулевого размера? тогда их может и не быть на диске, а они могут быть только в списке файлов, тогда чтение будет занимать доли секунды на любом количестве файлов…
                                    0
                                    Так меня и интересовала скорость lookup, а не чтения. Именно она зависит от количества файлов в директории.
                                      0
                                      не совсем так… список файлов храниться в файле, который разбросан по секторам жесткого диска, под него при создании директории выделяется определенное количество кластеров, которые идут подряд. После заполнения их записями выделяются новые кластеры, но если файлы пустые, и реальной записи на диск не идет, эти кластеры идут подряд. если файлы на диск пишутся то кластеры идут не подряд и головка винта начинает делать лишние проходы для чтения… как то так если на пальцах. SSD по другому.
                                        +1
                                        Управление памятью даже в ext2 более хитрое и чаще всего директории, даже большие, оказываются не фрагментированными.
                                        Разница в скорости происходит за счет представления — имены файлов организованы в линейный массив или в какое-нибудь дерево.
                                          –1
                                          И за счет представления тоже, и за счет того, что каждый раз все равно читать с диска (кроме поиска файлов на реальном серваке куча задач которым нужна память после того, как сервер найдет файл он начинает его читать, что занимает проходы головки, значит следующий файл в другом каталоге, будет искаться медленнее…
                                          а гонять сферитического коня в вакууме с файлами нулевого размера… в чем тогда смысл теста?
                                        0
                                        случайно отправил.
                                        после того как сервер найдет файл он начинает его читать, что занимает проходы головки, значит следующий файл в другом каталоге, будет искаться медленнее…
                                        а гонять сферитического коня в вакууме с файлами нулевого размера… в чем тогда смысл теста?
                                0
                                Эм, и как скачать файл без php, например nginx'ом?
                                  0
                                  Скачать файл nginx? О_О, возможно вы имели в виду раздавать? Он и будеть раздавать.
                                  +5
                                  Хрена вы минусуте-то все? Дайте шанс чуваку. Ну сам переизобрёл велосипед, ну опубликовал. Это же не унылые псевдофилософские размышления на тему «почему HR такие глупые а я такой прекрасный», а вполне себе технический пост (хоть и не супер качественный). Вот такой вот приём разработки. Это же не оффтоп для хабра.
                                    0
                                    Я в любом случае учту все комментарии и переделаю класс.
                                    Для меня важны любые отзывы.
                                    Главное не вылететь с хабра.
                                    Ну и нужно еще думать не обо мне, а о тех кто будет через поисковик искать ответ на вопрос как хранить файлы.
                                    Сам пост (даже и не качественный) и комментарии дадут возможность тому кто ищет быстрее разобраться в вопросе.
                                      0
                                      Не переживайте, есть совсем хреновые статьи и авторы не вылетели. А строительством велосипедов страдали все, с опытом это проходит, но не у всех :)
                                      А вы сделали решение, опубликовали его, стараетесь защитить, только так и можно идти к совершенству. А мелкие огрехи молодости я думаю Хабросообщество простит, всем было 20 лет.
                                        +1
                                        > А вы сделали решение, опубликовали его, стараетесь защитить, только так и можно идти к совершенству. А мелкие огрехи молодости я думаю Хабросообщество простит, всем было 20 лет.

                                        Только «стараетесь защитить» из этого списка надо убрать. Последнее что стоит делать новичку это защищать свой «велосипед». Правильный подход — это внять советам, пройти по предложенным ссылкам и вдохновиться решениями придуманными до нас и обкатанными в продакшене не одной сотней разработчиков. Ну и главный минус тех кто «сам не знаю но учу» это серьезные проблемы у других новичков кои еще не обладают критическим мышлением и по своей неопытности не могут отличить зерна от плевел.
                                        П.С. этот код повторяется 5 раз, (и это далеко не единственная проблема исходников представленных в статье)
                                        for($i=$this->branches;$i>=1;$i--) {
                                            $dir=ceil($this->id/pow($this->max_file_count,$i))%$this->max_file_count;
                                            $dir_file_arr[]=$dir>0?$dir:$this->max_file_count;
                                        

                                        так делать нельзя. такие статьи лишь увеличивают энтропию, и новичку проще затеряться и вместо тру вей он может свернуть в такое болото что потом переучить будет крайне сложно
                                    0
                                    На изображении мы видим 2 ветви и по 3 файла в каждой папке.

                                    Извините, пожалуйста, но я у впор не вижу 2 ветви на изображении.
                                    Есть три уровня в дереве (слоя), (ну, или два уровня, если считать корень обособленной единицей), у каждого узла — по три ветви.
                                      0
                                      Я, возможно, заблуждаюсь, но а что мешает хранить файлы по папкам в виде год/месяц/день/час/хеш_имя? Да хоть до секунды папки создавать)
                                        0
                                        Как их потом искать?
                                          0
                                          Ах предполагается поиск файлов вручную? Думал, что подразумевается запись файлов на сервер, откуда потом они будут доставаться прямыми урлами. А если так, то да, идея отличная.
                                          0
                                          сколько папок будет лет через пять — десять? И какова равномерность распределения фапйлов по каталогам в Вашем примере?
                                            0
                                            Ну если по месяцам — то не так много. Во всяком случае не так много, чтобы плодить какие-либо проблемы.
                                            А плотность — ну если, например, вы примерно уверены, сколько юзеры грузят Вам на сайт (все еще подразумеваю высоконагруженный веб-ресурс), то и примерно распределение будете знать. Ну во всяком случае, плюс-минус 100 файлов погоды не сделают, ни так ли?)
                                              0
                                              я как то переписывал такой проект, первоночально планировалось около тысячи пользователей в день, так и было первые четыре года жизни сайта, а потом что то произошло (я не вникал в маркетинг). И на сайт ломанулось до полумиллиона уников в день. Сообветственно выросли и нагрузки на сервер, и прочее… например оказалось что 32637 комментариев к одной статье это мало))) бывает и больше, а когда проектировали базу данных об этом не подумали, файлы хранились там в папках по дням, во первых накопилось число папок которые уже начали тормозить, во вторых, равномерность была мягко говоря никакая, в одной папке один файл в другой несколько тысяч…

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое