вторник, 22 января 2008 г.

«Хитрое» зеркалирование репозиториев на freebsd

Недавно натолкнулся на статью, описывающую создание зеркала репозитория дистрибутива N, способного загружать с основного репозитория только запрашиваемые пользователями файлы. Но, поскольку все это действие предлагалось проводить с linux + fuse + че-то-там-еще, статья не вызвала особого энтузиазма.
Но что-то подобное хотелось иметь и для freebsd. В итоге родилась идея «хитрого» зеркалирования репозитория убунту. Для этого требуется: apache2.0 (mod_cache + mod_proxy + mod_rewrite) и perl.

1. Настройка apache 2.0
1.1. Сперва требуется включить проксирование (mod_proxy) в apache20. Если вы ставите apache2.0 из портов

# cd /usr/ports/www/apache20
# make WITH_PROXY_MODULES=yes all install clean

или в /etc/make.conf добавить
.if ${.CURDIR} == ${PORTSDIR}/www/apache20
WITH_PROXY_MODULES = yes
.endif

и затем
# cd /usr/ports/www/apache20
# make all install clean
1.2. Изменения в файле конфигурации apache2/httpd.conf
# Включаем необходимые модули
LoadModule cache_module modules/mod_cache.so
LoadModule disk_cache_module modules/mod_disk_cache.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
# Создаем виртуальный хост
<VirtualHost *:80>
ServerAdmin webmaster@ubuntu.local
DocumentRoot /www/ubuntu.local
ServerName ubuntu.local
ServerAlias ubuntu.our-office.com
ErrorLog /var/log/apache2/ubuntu.local-error_log
CustomLog /var/log/apache2/ubuntu.local-access_log common
<Directory "/www/ubuntu.local/">
Options FollowSymLinks
Order Deny,Allow
Allow from All
</Directory>

<Directory "/www/ubuntu.local/ubuntu/">
Options Indexes FollowSymLinks
Order Deny,Allow
Allow from All
</Directory>

<LocationMatch "^[^/]+">
Deny from all
</LocationMatch>

# Включаем mod_rewrite
RewriteEngine On
# Ставим уровень детализации логов.
# Настоятельно не рекомендуется ставить больше 2, разве что для отладки.
# По умолчанию в лог не пишется (0).
RewriteLogLevel 1
# Пишем в лог.
RewriteLog "/var/log/apache2/ubuntu.local-rewrite.log"

# Проверяем существование запрашиваемого файла в локальном репозитории
RewriteCond %{SCRIPT_FILENAME} !-f
# Не нашли? Идем с поклоном к вышестоящему репозиторию
RewriteRule ^/ubuntu/pool/(.*)$ http://mirror.ubuntu.optilink.ru/ubuntu/pool/$1 [P,L]

# Настраиваем кеширование (mod_cache)
# Кешируем только .deb файлы.
CacheEnable disk /ubuntu/pool/
# Указываем размещение кеша.
CacheRoot /var/ubuntu.mirror/
# Размер кеша 5 гигов, с копейками (в килобайтах).
CacheSize 5120000
CacheDirLevels 2
CacheDirLength 3
# Задаем максимальное время кеширования файлов (в секундах).
CacheMaxExpire 86400

# Вырубаем левые прокси-запросы
ProxyRequests Off
# Наш местный репозиторий
ProxyPassReverse /ubuntu/pool/ http://mirror.ubuntu.optilink.ru/ubuntu/pool/
# Apache стоит внутри jail и внешний интернет ему доступен только через прокси-сервер.
ProxyRemote * http://127.0.0.2:3128

<Proxy *>
Order Deny,Allow
Deny from all
# Разрешаем запросы только с локальной сети
Allow from 192.168.0.0/24
</Proxy>
</VirtualHost>
1.3. Создаем директории /www/ubuntu.local/ubuntu и /var/ubuntu.mirror. В /www/ubuntu.local/ubuntu/ будет располагаться зеркало репозитория; в /var/ubuntu.mirror - кеш mod_proxy. Владельцем созданных директорий назначаем пользователя, под которым работает веб-сервер (обычно это www) и разрешаем ему писать.

1.4. В /www/ubuntu.local/ubuntu/ создаем dists и pool. В dists из основного репозитория заливаем файлы описаний
# sudo -u www /usr/bin/env http_proxy="127.0.0.2:3128" /usr/local/bin/wget -nH -R 'index.html*' --cut-dirs=2 -r -P/www/ubuntu.local/ubuntu/dists --no-parent http://mirror.ubuntu.optilink.ru/ubuntu/dists/
1.5. Теперь пишем скрипт на перле, копирующий кешированные файлы в зеркало репозитория. За качество кода сильно не бить :) Скрипт сохраним как /usr/local/script/move-from-cache.pl
#!/usr/bin/perl -w

use strict;

# Потрошим файл описания кешированного в mod_proxy объекта.
sub extract_name {
my $data = shift;

# iiIIqqqq
# i1 i1 I1 I1 q1 q1 q1 q1
my ($format,$status,$name_len,
$entity_version,$date,$expire,
$request_time,$response_time, $ext) = unpack('i1i1I1I1q1q1q1q1a*', $data);
my ($name) = unpack ("a$name_len", $ext);

$name =~ s/[?]?$//;
return $name;
}
# Приведенный ниже код подпрограммы взят отсюда.
# Accepts one argument: the full path to a directory.
# Returns: nothing.
sub process_files {
my $path = shift;

# Open the directory.
opendir (DIR, $path) or die "Unable to open $path: $!";

# Read in the files.
# You will not generally want to process the '.' and '..' files,
# so we will use grep() to take them out.
# See any basic Unix filesystem tutorial for an explanation of the +m.
my @files = grep { !/^\.{1,2}$/ } readdir (DIR);

# Close the directory.
closedir (DIR);

# At this point you will have a list of filenames
# without full paths ('filename' rather than
# '/home/count0/filename', for example)
# You will probably have a much easier time if you make
# sure all of these files include the full path,
# so here we will use map() to tack it on.
# (note that this could also be chained with the grep
# mentioned above, during the readdir() ).
@files = map { $path . '/' . $_ } @files;

my @value = ();

for (@files) {

# If the file is a directory
if (-d $_) {
# Here is where we recurse.
# This makes a new call to process_files()
# using a new directory we just found.
push @value, process_files ($_);

# If it isn't a directory, lets just do some
# processing on it.
} else {
# Do whatever you want here =)
# A common example might be to rename the file.
push @value, $_;
}
}
return @value;
}

# Дальше мое сочинение
# С командной строки передаются два параметра:
# Путь к кешу mod_proxy
my $ubuntu_cache = $ARGV[0] if defined $ARGV[0];
# Префикс директории локального репозитория
my $mirror_prefix = $ARGV[1] if defined $ARGV[1];

unless (defined($ubuntu_cache) && defined($mirror_prefix)) {
print "Usage: ./script.pl /path/to/ubuntu/cache /path/mirror/prefix\n\n";
exit;
}

# Формируем список файлов в кеше.
my @files = process_files ($ubuntu_cache);

foreach my $filename (@files)
{
my $file;
my $data = '';
# Если это файл описания, потрошим
if ($filename =~ /^(.*?)\.header$/) {
my $cache_file = $1 . '.data';
open $file, "<${filename}" or die "Error. Can't open file.";
while (!eof($file)) {
my $buf;
$data .= $buf if read ($file, $buf, 4096);
}
close $file;
my @path = split('/', extract_name ($data));
shift (@path);
my $name = join ('/', @path);

my $new_file = $mirror_prefix . '/' . $name;
# Если кешированный файл отсутствует в репозитории, то копируем его
unless (-e $new_file) {
print "$name\n";
my @m_path = split ('/', $new_file);
pop (@m_path);

my $a = '/';
# Проверяем и создаем отсутствующие директории
foreach my $i (@m_path) {
$a .= $i . '/';
mkdir $a unless -e $a;
}
# Копируем кешированный файл куда надо.
system ("/bin/cp $cache_file $new_file");
}
}
}

# Формат файла описания кешированных объектов в mod_proxy.
=pod
/* Our on-disk header format is:
*
* disk_cache_info_t
* entity name (dobj->name) [length is in disk_cache_info_t->name_len]
* r->headers_out (delimited by CRLF)
* CRLF
* r->headers_in (delimited by CRLF)
* CRLF
*/
#define DISK_FORMAT_VERSION 0
typedef struct {
/* Indicates the format of the header struct stored on-disk. */
int format;
/* The HTTP status code returned for this response. */
int status;
/* The size of the entity name that follows. */
apr_size_t name_len;
/* The number of times we've cached this entity. */
apr_size_t entity_version;
/* Miscellaneous time values. */
apr_time_t date;
apr_time_t expire;
apr_time_t request_time;
apr_time_t response_time;
} disk_cache_info_t;
=cut
2. Настраиваем cron
$ sudo crontab -u www -e
# Запускается каждый час
0 * * * * /usr/local/script/move-from-cache.pl /var/ubuntu.mirror/ /www/ubuntu.local/
# Обновление файлов описаний.
# Запускается один раз в день
1 3 * * * /usr/bin/env http_proxy="127.0.0.2:3128" /usr/local/bin/wget -R 'index.html*' -nH --cut-dirs=2 -r -N -P/www/ubuntu.local/ubuntu/dists --no-parent http://mirror.ubuntu.optilink.ru/ubuntu/dists/


3. Вот собственно и все. Не забываем ребутнуть веб-сервер.

5 комментариев:

Анонимный комментирует...

у меня сейчас стоит проксирование через nginx с сохранением на диск (сразу в соответствии со структурой каталогов)
и каши nginx протист не столько сколько апач.

папка pool проксируется, а dist синхронизируется через rsync (в идеале 2 раза в день)

с rsync нужно еще повозиться. Например сначала скачивать только *.pdiff , применять их на списки пакетов. А потом проводить полную синхронизацию dist. но я это еще не реализовал.

конфиг nginx:

server {
listen 80 ;

server_name .mirror.yandex.ru .mirror.tetra.name;

access_log /var/log/nginx/mirror.access.log;

root /media/data/mirror ;
autoindex on;


location /debian/pool/ {

error_page 404 = @fetch;
}

location @fetch {
internal;

proxy_pass http://77.88.19.68:80;
proxy_store on ;
proxy_store_access user:rw group:rw all:r ;
proxy_temp_path /media/data/mirror_t ;

proxy_buffering on ;

proxy_set_header Host $host;
}

}

т.е. веб сервер смотрит в каталог, если файла не обнаруживается то он переходит к проксированию. По хорошему еще нужно прикрутить периодическую проверку контрольных сумм файлов в кеше.

Князь комментирует...

2 timyr-lan: Не понял смысл фразы "и каши nginx протист не столько сколько апач" :) А вот проверка контрольной суммы нужна, так же как и очистка репозитория от старых файлов.

PhoeniX комментирует...

Неплохо, универсально. А при наличии дебиана на зеркале можно использовать apt-cacher, что-то на эту тему было в пакете дня.

Анонимный комментирует...

2 mr.tacitus: описался, "просит" имелся ввиду расход системных ресурсов. Понимаю что при небольших нагрузках не имеет принципиального значения. Но у меня еще со Spectrum привычка экономить ресурсы когда позволяет возможность. Да и в целом телодвижений IMHO меньше.

Князь комментирует...

Не ошибается тот, кто ничего не делает. :)