Администрирование → Дисковая балансировка в Nginx

YourChief 16 августа 2014 в 02:40 20,3k

В этой статье я опишу интересное решение на базе Nginx для случая, когда дисковая система становится узким местом при раздаче контента (например, видео).

Постановка задачи


Имеем задачу: необходимо отправлять клиентам статические файлы (видео) с суммарной полосой раздачи в десятки гигабит в секунду.

Такую полосу по очевидным причинам нельзя раздавать прямо из хранилища, необходимо применить кэширование. Объём контента, составляющий большую часть производимого трафика, на несколько порядков больше объёма оперативной памяти одного сервера, поэтому кэширование в ОЗУ невозможно, хранить кэш придётся на дисках.

Сетевые каналы достаточной ёмкости имеются a priori, иначе задача была бы нерешаема.

Выбор пути решения


В этой ситуации проблемным местом становятся диски: чтобы сервер произвёл 20 гигабит трафика в секунду (два оптоволокна в агрегате), он должен прочитать с дисков ~2400 мегабайт в секунду полезных данных. Вдобавок к этому диски ещё и могут быть заняты записью в кэш.
Для масштабирования производительности дисковой системы применяют RAID-массивы с чередованием блоков. Ставка делается на то, что при чтении файла его блоки окажутся на разных дисках и скорость последовательного чтения файла будет в среднем равна скорости самого медленного диска, умноженного на число чередующихся дисков.
Проблема такого подхода в том, что он работает эффективно только для идеального случая, когда читают достаточно длинный файл (размер файла много больше размера блока чередования), который расположен внутри файловой системы без фрагментирования. Для параллельного чтения многих мелких и/или фрагментированных файлов такой подход не позволяет даже приблизиться к суммарной скорости всех дисков. К примеру, RAID0 из шести ssd дисков при 100% загруженности очереди ввода-вывода давал скорость как у двух дисков.
Практика показала, что выгоднее поделить файлы между дисками целиком, используя раздельные файловые системы. Это гарантирует утилизацию каждого диска, потому что они независимы.

Реализация


Как упомянуто выше, кэшировать будем nginx-ом. Идея в том, чтобы поделить раздаваемые файлы между дисками поровну. Для этого в простейшем случае достаточно хэшированием отобразить множество URL-ов во множество дисков. Примерно так мы и сделаем, но обо всём по порядку.
Определим зоны кэширования по числу дисков. В моём примере их 10.
В секции http:
    proxy_cache_path  /var/www/cache1  levels=1:2  keys_zone=cache1:100m inactive=365d max_size=200g;
    proxy_cache_path  /var/www/cache2  levels=1:2  keys_zone=cache2:100m inactive=365d max_size=200g;
    ...
    proxy_cache_path  /var/www/cache10 levels=1:2  keys_zone=cache10:100m inactive=365d max_size=200g;

В директорию каждой зоны кэширования примонтирован отдельный диск.

Источниками контента будут три апстрима, по два сервера в каждой группе:
upstream src1 {
        server 192.168.1.10;
        server 192.168.1.11;
}
upstream src2 {
        server 192.168.1.12;
        server 192.168.1.13;
}
upstream src3 {
        server 192.168.1.14;
        server 192.168.1.15;
}

Это непринципиальный момент, взято для правдоподобия.

Секция server:

server {
	listen   80 default;
	server_name  localhost.localdomain;
	access_log      /var/log/nginx/video.access.log combined buffer=128k;

	proxy_cache_key $uri;

	set_by_lua_file $cache_zone /etc/nginx/cache_director.lua 10 $uri_without_args;

	proxy_cache_min_uses 0;
	proxy_cache_valid  1y;
	proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_404;

	location ~* ^/site1/.*$ {
		set $be "src1";
		include director;
	}
	location ~* ^/site2/.*$ {
		set $be "src2";
		include director;
	}
	location ~* ^/site3/.*$ {
		set $be "src3";
		include director;
	}

    location @cache1 {
            bytes on;
            proxy_temp_path /var/www/cache1/tmp 1 2;
            proxy_cache cache1;
            proxy_pass           http://$be;
    }
    location @cache2 {
            bytes on;
            proxy_temp_path /var/www/cache2/tmp 1 2;
            proxy_cache cache2;
            proxy_pass           http://$be;
    }
...
    location @cache10 {
            bytes on;
            proxy_temp_path /var/www/cache10/tmp 1 2;
            proxy_cache cache10;
            proxy_pass           http://$be;
    }
}

Директива set_by_lua_file выбирает подходящий диск для этого URL хэшированием. Для условного «сайта» выбирается и запоминается бэкэнд. Затем в файле director происходит перенаправление во внутренний локейшн, который обслуживает запрос из выбранного бэкэнда, сохраняя ответ в определённом для этого URL кэше.

А вот и director:
if ($cache_zone = 0) { return 481; }
if ($cache_zone = 1) { return 482; }
...
if ($cache_zone = 9) { return 490; }
error_page 481 = @cache1;
error_page 482 = @cache2;
...
error_page 490 = @cache10;

Выглядит ужасно, но это единственный способ.

Вся соль конфигурации в хэшировании URL->диск, cache_director.lua:
function shards_vector(base, seed)
    local result = {}
    local shards = {}
    for shard_n=0,base-1 do table.insert(shards, shard_n) end

    for b=base,1,-1 do
        choosen = math.fmod(seed, b)+1
        table.insert(result, shards[choosen])
        table.remove(shards, choosen)
        seed = math.floor(seed / b)
    end
    return result
end

function file_exists(filename)
  local file = io.open(filename)
  if file then
    io.close(file)
    return 1
  else
    return 0
  end
end

disks = ngx.arg[1]
url   = ngx.arg[2]
sum   = 0
for c in url:gmatch"." do
    sum = sum + string.byte(c)
end
sh_v = shards_vector(disks, sum)

for _, v in pairs(sh_v) do
    if file_exists("/var/www/cache" .. (tonumber(v)+1) .. "/ready") == 1 then
        return v
    end
end

В директиве set_by_lua_file, упомянутой выше, этот код получает количество дисков и URL. Идея напрямую отображать URL в диск хороша до тех пор, пока не выйдет из строя хотя бы один диск. Переадресацию URL-ов с проблемного диска на здоровый нужно нужно выполнять одинаково для конкретного URL-а (иначе не будет попаданий в кэш) и при этом же она должна быть разной для разных URL-ов, чтобы не возникало перекоса нагрузки. Оба этих свойства должны сохраняться, если замена замены (и т.д.) тоже выйдет из строя. Поэтому для системы из n дисков отображаю URL во множество всевозможных перестановок этих n дисков и затем по порядку следования этих дисков в расстановке пытаюсь воспользоваться соответствующими кэшами. Критерием активности диска (кэша) считается наличие файла-флага в его директории. Мне приходится chattr-ить эти файлы, чтобы nginx не удалял их.

Результаты


Такое размазывание контента по дискам действительно позволяет использовать всю скорость дисковых устройств. Сервер с 6 недорогими SSD-дисками при рабочей нагрузке смог развить отдачу порядка 1200 МБ/с, что соответствует суммарной скорости дисков. Скорость массива же колебалась в районе 400 МБ/c
Проголосовать:
+28
Сохранить: