Обычно Linux использует кэш для асинхронной записи данных на диск. Однако может случиться так, что промежуток времени между запросом на запись и фактической записью или количество незаписанных (грязных) данных станет очень большим. В этой ситуации сбой может привести к огромной потере данных, и по этой причине Linux переключается на синхронную запись, если грязный кеш становится слишком большим или старым. Поскольку порядок записи также должен соблюдаться, вы не можете просто обойти небольшой ввод-вывод, не гарантируя, что малый ввод-вывод полностью независим от всех ранее поставленных в очередь записей. Таким образом, зависимые записи могут вызвать огромную задержку. (Подобные зависимости также могут быть вызваны на уровне файловой системы: см. https://ext4.wiki.kernel.org/index.php/Ext3_Data%3DOrdered_vs_Data%3DWriteback_mode ).
Я предполагаю, что вы испытываете своего рода раздувание буфера в сочетании с зависимой записью. Если вы пишете большой файл и имеете большой дисковый кеш, вы попадаете в ситуации, когда необходимо записать огромный объем данных, прежде чем можно будет выполнить синхронную запись. Есть хорошая статья о LWN, описывающая проблему: https: // lwn.net / Articles / 682582 /
Работа над планировщиками все еще продолжается, и ситуация может улучшиться с новыми версиями ядра. Однако до этого момента: Есть несколько переключателей, которые могут влиять на поведение кэширования в Linux (их больше, см .: https://www.kernel.org/doc/Documentation/sysctl/ vm.txt ):
Самым простым решением для уменьшения максимальной задержки в таких ситуациях является уменьшение максимального объема грязного дискового кеша и выполнение фоновой задачи для ранней записи. Конечно, это может привести к снижению производительности в ситуациях, когда большой кэш в противном случае вообще предотвратил бы синхронную запись. Например, вы можете настроить следующее в /etc/sysctl.conf:
vm.dirty_background_ratio = 1
vm.dirty_ratio = 5
Обратите внимание, что значения, подходящие для вашей системы, зависят от объема доступной оперативной памяти и скорости диска. В экстремальных условиях вышеуказанный грязный рацион может быть слишком большим. Например, если у вас есть 100 ГБ доступной оперативной памяти и вы записываете на диск со скоростью около 100 МБ, указанные выше настройки приведут к максимальному объему грязного кеша 5 ГБ, что может занять около 50 секунд для записи. С помощью dirty_bytes
и dirty_background_bytes
вы также можете установить значения для кэша в абсолютном порядке.
Еще вы можете попробовать переключить планировщик io. В текущих выпусках ядра есть noop, deadline и cfq. Если вы используете более старое ядро, у вас может быть лучшее время реакции с планировщиком крайних сроков по сравнению с cfq. Однако вы должны это проверить. В вашей ситуации следует избегать Noop. Существует также неосновной планировщик BFQ, который утверждает, что снижает задержку по сравнению с CFQ ( http://algo.ing.unimo.it/people/paolo/disk_sched/ ). Однако он не входит во все дистрибутивы. Вы можете проверить и переключить планировщик во время выполнения с помощью:
cat /sys/block/sdX/queue/scheduler
echo > /sys/block/sdX/queue/scheduler
Первая команда предоставит вам также сводку доступных планировщиков и их точные имена.Обратите внимание: настройка теряется после перезагрузки. Чтобы выбрать расписание на постоянной основе, вы можете добавить параметр ядра:
elevator=
Ситуация для NFS аналогична, но включает другие проблемы. Следующие два отчета об ошибках могут дать некоторую информацию об обработке статистики в NFS и о том, почему запись большого файла может привести к очень медленной работе статистики:
https://bugzilla.redhat.com/show_bug.cgi? id = 688232 https://bugzilla.redhat.com/show_bug.cgi?id=469848
Обновление: (14.08.2017)
С ядром 4.10 новые параметры ядра CONFIG_BLK_WBT
и его подопции BLK_WBT_SQ
и CONFIG_BLK_WBT_MQ
были введены. Они предотвращают раздувание буфера, вызванное аппаратными буферами, размер и приоритет которых не может контролироваться ядром:
Enabling this option enables the block layer to throttle buffered
background writeback from the VM, making it more smooth and having
less impact on foreground operations. The throttling is done
dynamically on an algorithm loosely based on CoDel, factoring in
the realtime performance of the disk
Кроме того, BFQ-Scheduler поддерживается ядром 4.12.
Я только что уговорил ботаника -написать довольно полный сценарий для этого; последняя версия находится по адресуhttps://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.
Преимущества над реализацией оболочки Сиванна:
Недостаток :написано на zsh, а не на bash.
#!/bin/zsh
#
# Purpose: run speficied command with specified arguments and cache result. If cache is fresh enough, don't run command again but return cached output.
# Also cache exit status and stderr.
# Copyright (c) 2019-2020 András Korn; License: GPLv3
# Use silly long variable names to avoid clashing with whatever the invoked program might use
RUNCACHED_MAX_AGE=${RUNCACHED_MAX_AGE:-300}
RUNCACHED_IGNORE_ENV=${RUNCACHED_IGNORE_ENV:-0}
RUNCACHED_IGNORE_PWD=${RUNCACHED_IGNORE_PWD:-0}
[[ -n "$HOME" ]] && RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-$HOME/.runcached}
RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-/var/cache/runcached}
function usage() {
echo "Usage: runcached [--ttl <max cache age>] [--cache-dir <cache directory>]"
echo " [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]"
echo " [--] command [arg1 [arg2...]]"
echo
echo "Run 'command' with the specified args and cache stdout, stderr and exit"
echo "status. If you run the same command again and the cache is fresh, cached"
echo "data is returned and the command is not actually run."
echo
echo "Normally, all exported environment variables as well as the current working"
echo "directory are included in the cache key. The --ignore options disable this."
echo "The OLDPWD variable is always ignored."
echo
echo "--prune-cache deletes all cache entries older than the maximum age. There is"
echo "no other mechanism to prevent the cache growing without bounds."
echo
echo "The default cache directory is ${RUNCACHED_CACHE_DIR}."
echo "Maximum cache age defaults to ${RUNCACHED_MAX_AGE}."
echo
echo "CAVEATS:"
echo
echo "Side effects of 'command' are obviously not cached."
echo
echo "There is no cache invalidation logic except cache age (specified in seconds)."
echo
echo "If the cache can't be created, the command is run uncached."
echo
echo "This script is always silent; any output comes from the invoked command. You"
echo "may thus not notice errors creating the cache and such."
echo
echo "stdout and stderr streams are saved separately. When both are written to a"
echo "terminal from cache, they will almost certainly be interleaved differently"
echo "than originally. Ordering of messages within the two streams is preserved."
exit 0
}
while [[ -n "$1" ]]; do
case "$1" in
--ttl) RUNCACHED_MAX_AGE="$2"; shift 2;;
--cache-dir) RUNCACHED_CACHE_DIR="$2"; shift 2;;
--ignore-env) RUNCACHED_IGNORE_ENV=1; shift;;
--ignore-pwd) RUNCACHED_IGNORE_PWD=1; shift;;
--prune-cache) RUNCACHED_PRUNE=1; shift;;
--help) usage;;
--) shift; break;;
*) break;;
esac
done
zmodload zsh/datetime
zmodload zsh/stat
zmodload zsh/system
zmodload zsh/files
# the built-in mv doesn't fall back to copy if renaming fails due to EXDEV;
# since the cache directory is likely on a different fs than the tmp
# directory, this is an important limitation, so we use /bin/mv instead
disable mv
mkdir -p "$RUNCACHED_CACHE_DIR" >/dev/null 2>/dev/null
((RUNCACHED_PRUNE)) && find "$RUNCACHED_CACHE_DIR/." -maxdepth 1 -type f \! -newermt @$[EPOCHSECONDS-RUNCACHED_MAX_AGE] -delete 2>/dev/null
[[ -n "$@" ]] || exit 0 # if no command specified, exit silently
(
# Almost(?) nothing uses OLDPWD, but taking it into account potentially reduces cache efficency.
# Thus, we ignore it for the purpose of coming up with a cache key.
unset OLDPWD
((RUNCACHED_IGNORE_PWD)) && unset PWD
((RUNCACHED_IGNORE_ENV)) || env
echo -E "$@"
) | md5sum | read RUNCACHED_CACHE_KEY RUNCACHED__crap__
# make the cache dir hashed unless a cache file already exists (created by a previous version that didn't use hashed dirs)
if ! [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus ]]; then
RUNCACHED_CACHE_KEY=$RUNCACHED_CACHE_KEY[1,2]/$RUNCACHED_CACHE_KEY[3,4]/$RUNCACHED_CACHE_KEY[5,$]
mkdir -p "$RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h}" >/dev/null 2>/dev/null
fi
# If we can't obtain a lock, we want to run uncached; otherwise
# 'runcached' wouldn't be transparent because it would prevent
# parallel execution of several instances of the same command.
# Locking is necessary to avoid races between the mv(1) command
# below replacing stderr with a newer version and another instance
# of runcached using a newer stdout with the older stderr.
: >>$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null
if zsystem flock -t 0 $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null; then
if [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout ]]; then
if [[ $[EPOCHSECONDS-$(zstat +mtime $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout)] -le $RUNCACHED_MAX_AGE ]]; then
cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout &
cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stderr >&2 &
wait
exit $(<$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus)
else
rm -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.{stdout,stderr,exitstatus} 2>/dev/null
fi
fi
# only reached if cache didn't exist or was too old
if [[ -d $RUNCACHED_CACHE_DIR/. ]]; then
RUNCACHED_tempdir=$(mktemp -d 2>/dev/null)
if [[ -d $RUNCACHED_tempdir/. ]]; then
$@ >&1 >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stdout 2>&2 2>$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stderr
RUNCACHED_ret=$?
echo $RUNCACHED_ret >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.exitstatus 2>/dev/null
mv $RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.{stdout,stderr,exitstatus} $RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h} 2>/dev/null
rmdir $RUNCACHED_tempdir 2>/dev/null
exit $RUNCACHED_ret
fi
fi
fi
# only reached if cache not created successfully or lock couldn't be obtained
exec $@
Реализация существует здесь: https://bitbucket.org/sivann/runcached/src Кэширует путь к исполняемому файлу, вывод, код выхода, запоминает аргументы. Настраиваемый срок действия. Реализовано на bash, C, python, выбирайте то, что вам подходит.
Для пользователей Bash я создал bash -кэш , который предоставляет набор функций, аналогичный подходу András Korn zsh. Он поддерживает env vars, избегает гонок, кэширует stderr и коды выхода, делает недействительными устаревшие данные, поддерживает асинхронное нагревание и блокировку мьютексов и многое другое.
Например, я думаю, вы можете реализовать то, что описываете, вот так:
foo() {
python foo.py <<<"$*" # this writes all arguments to the python stdin
} && bc::cache foo
Который вы затем вызываете как:
foo param1 param2 param3
И foo
будут кэшировать результаты и повторно использовать их в последующих вызовах.
См. мой более ранний ответ для более подробной информации.
Простое решение на Python, основанное на существующих пакетах(joblib.Memory для запоминания):
cmdcache.py:
import sys, os
import joblib
import subprocess
mem = joblib.Memory('.', verbose=False)
@mem.cache
def run_cmd(args, env):
process = subprocess.run(args, capture_output=True, text=True)
return process.stdout, process.stderr, process.returncode
stdout, stderr, returncode = run_cmd(sys.argv[1:], dict(os.environ))
sys.stdout.write(stdout)
sys.stdout.write(stderr)
sys.exit(returncode)
Пример использования:
python cmdcache.py ls