Я реализую FUSE файловая система, предназначенная для обеспечения доступа через знакомые вызовы POSIX к файлам, которые фактически хранятся за RESTful API. Файловая система кэширует файлы, как только они были получены в первый раз, чтобы они были более доступны при последующих обращениях.
Я запускаю файловую систему в многопоточном режиме (по умолчанию FUSE), но обнаружил, что вызовы getattr кажутся сериализованными, хотя другие вызовы могут выполняться параллельно.
При открытии файла FUSE всегда сначала вызывает getattr, и клиент, которого я поддерживаю, нуждается в точном размере файла, возвращаемого этим первоначальным вызовом (я не контролирую это поведение). Это означает, что если у меня нет кэшированного файла, мне нужно получить информацию через вызовы RESTful API. Иногда эти вызовы происходят по сети с высокой задержкой, с временем прохождения сигнала туда и обратно примерно 600 мс.
В результате очевидной последовательной природы вызова getattr любой доступ к файлу, который в настоящее время не кэшируется, заставит всю файловую систему блокировать любые новые операции, пока этот getattr обслуживается.
Я придумал несколько способов обойти это, но все они кажутся уродливыми или многословными, на самом деле я просто хочу, чтобы вызовы getattr выполнялись параллельно, как это делают все остальные вызовы.
Глядя на исходный код, я не понимаю, почему getattr должен вести себя так, FUSE блокирует мьютекс tree_lock, но только для чтения, и в это же время не происходит никаких записей.
Чтобы опубликовать что-то простое в этом вопросе, я выбрал невероятно простую реализацию, которая просто поддерживает getattr и позволяет легко продемонстрировать проблему.
#ifndef FUSE_USE_VERSION
#define FUSE_USE_VERSION 22
#endif
#include <fuse.h>
#include <iostream>
static int GetAttr(const char *path, struct stat *stbuf)
{
std::cout << "Before: " << path << std::endl;
sleep(5);
std::cout << "After: " << path << std::endl;
return -1;
}
static struct fuse_operations ops;
int main(int argc, char *argv[])
{
ops.getattr = GetAttr;
return fuse_main(argc, argv, &ops);
}
Использование пары терминалов для вызова ls на пути (примерно) в одно и то же время показывает, что второй вызов getattr начинается только после завершения первого, поэтому второй ls будет занимать ~ 10 секунд вместо 5.
Терминал 1
$ date; sudo ls /mnt/cachefs/file1.ext; date
Tue Aug 27 16:56:34 BST 2013
ls: /mnt/cachefs/file1.ext: Operation not permitted
Tue Aug 27 16:56:39 BST 2013
Терминал 2
$ date; sudo ls /mnt/cachefs/file2.ext; date
Tue Aug 27 16:56:35 BST 2013
ls: /mnt/cachefs/file2.ext: Operation not permitted
Tue Aug 27 16:56:44 BST 2013
Как видите, разница во времени от двух date
выходы до ls
отличается только на одну секунду, но два после ls
отличается на 5 секунд, что соответствует задержке в GetAttr
функция. Это говорит о том, что второй вызов заблокирован где-то глубоко в FUSE.
Выход
$ sudo ./cachefs /mnt/cachefs -f -d
unique: 1, opcode: INIT (26), nodeid: 0, insize: 56
INIT: 7.10
flags=0x0000000b
max_readahead=0x00020000
INIT: 7.8
flags=0x00000000
max_readahead=0x00020000
max_write=0x00020000
unique: 1, error: 0 (Success), outsize: 40
unique: 2, opcode: LOOKUP (1), nodeid: 1, insize: 50
LOOKUP /file1.ext
Before: /file1.ext
After: /file1.ext
unique: 2, error: -1 (Operation not permitted), outsize: 16
unique: 3, opcode: LOOKUP (1), nodeid: 1, insize: 50
LOOKUP /file2.ext
Before: /file2.ext
After: /file2.ext
unique: 3, error: -1 (Operation not permitted), outsize: 16
Приведенный выше код и примеры не похожи на реальное приложение или способ его использования, но демонстрируют то же поведение. Я не показал этого в приведенном выше примере, но обнаружил, что после завершения вызова getattr последующие открытые вызовы могут выполняться параллельно, как я и ожидал.
Я проверил документы, чтобы попытаться объяснить это поведение, и попытался найти кого-то, кто сообщит о подобном опыте, но, похоже, ничего не могу найти. Возможно, потому что большинство реализаций getattr были бы такими быстрыми, что вы не заметили бы и не позаботились бы о том, что они сериализуются, или, может быть, потому, что я делаю что-то глупое в конфигурации. Я использую FUSE версии 2.7.4, поэтому возможно, что это старая ошибка, которая с тех пор была исправлена.
Если у кого-то есть понимание этого, это будет с благодарностью!
Я подписался на список рассылки FUSE, разместил свой вопрос и недавно получил следующий ответ от Миклоша Середи:
Поиск (то есть сначала поиск файла, связанного с именем)
сериализовано в каталог. Это в VFS (общая файловая система
часть в ядре), поэтому в основном любая файловая система подвержена
эта проблема, а не просто предохранитель.
Большое спасибо Миклосу за помощь. Полный поток см. http://fuse.996288.n3.nabble.com/GetAttr-calls-being-serialised-td11741.html.
Я также заметил, что сериализация была для каждого каталога, то есть вышеупомянутый эффект был бы виден, если бы оба файла были в одном каталоге, но не если бы они были в отдельных каталогах. Для моего приложения этого смягчения достаточно для меня, клиенты моей файловой системы используют каталоги, поэтому, хотя я могу ожидать, что многие вызовы getattr будут выполняться последовательно, вероятность того, что они все будут происходить в одном каталоге, достаточно мала, чтобы я не мог беспокоюсь о.
Для тех, для кого этого снижения недостаточно, если ваша файловая система поддерживает список каталогов, вы можете воспользоваться предложением Дэвида Стросса, которое заключается в использовании вызова readdir в качестве триггера для заполнения вашего кэша:
В наших файловых системах мы пытаемся предварительно извлечь и кэшировать атрибут
информация (которая неизбежно будет запрошена) во время чтения, поэтому мы
не нужно поразить бэкэнд для каждого.
Поскольку бэкэнд моей файловой системы не имеет понятия о каталогах, я не смог воспользоваться его предложением, но, надеюсь, это будет полезно для других.
Других решений пока нет …