Запоминание/кэширование вывода командной строки

Обычно 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 ):

  • dirty_ratio: Содержит в процентах от общей доступной памяти, которая содержит свободные страницы и восстанавливаемые страницы, количество страниц, на которых процесс, производящий запись на диск, сам начнет запись из грязных данных. Общий объем доступной памяти не равен общему объему системной памяти.
  • dirty_background_ratio: Содержит в процентах от общей доступной памяти, которая содержит свободные и восстанавливаемые страницы, количество страниц, на которых фоновые потоки очистки ядра начнут записывать грязные данные.
  • dirty_writeback_centisecs: Потоки очистки ядра периодически просыпаются и записывают "старые" данные на диск. Эта настраиваемая величина выражает интервал между этими пробуждениями в сотых долях секунды. Установка этого значения в ноль полностью отключает периодическую обратную запись.
  • dirty_expire_centisecs: Этот параметр используется для определения того, когда грязные данные достаточно стары, чтобы их можно было записать потоками очистки ядра. Выражается в сотых долях секунды. Данные, которые были загрязнены в памяти дольше этого интервала, будут записаны в следующий раз при пробуждении потока очистки.

Самым простым решением для уменьшения максимальной задержки в таких ситуациях является уменьшение максимального объема грязного дискового кеша и выполнение фоновой задачи для ранней записи. Конечно, это может привести к снижению производительности в ситуациях, когда большой кэш в противном случае вообще предотвратил бы синхронную запись. Например, вы можете настроить следующее в /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.

6
06.05.2016, 13:08
4 ответа

Я только что уговорил ботаника -написать довольно полный сценарий для этого; последняя версия находится по адресуhttps://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.

Преимущества над реализацией оболочки Сиванна:

  • также учитывает envvars при вычислении ключа кэша;
  • полностью избегает условий гонки, используя блокировку, вместо того, чтобы полагаться на случайное ожидание
  • более высокая производительность при вызове в узком цикле из-за меньшего количества разветвлений
  • также кэширует stderr
  • полностью прозрачный :ничего не печатает; не препятствует параллельному выполнению одной и той же команды; просто запускает команду без кеша, если есть проблема с кешем
  • настраивается с помощью envvars и переключателей командной строки
  • может очистить кэш (удалить все устаревшие записи)

Недостаток :написано на 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 $@
7
27.01.2020, 20:23

Реализация существует здесь: https://bitbucket.org/sivann/runcached/src Кэширует путь к исполняемому файлу, вывод, код выхода, запоминает аргументы. Настраиваемый срок действия. Реализовано на bash, C, python, выбирайте то, что вам подходит.

5
27.01.2020, 20:23

Для пользователей 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будут кэшировать результаты и повторно использовать их в последующих вызовах.

См. мой более ранний ответ для более подробной информации.

0
23.04.2020, 23:12

Простое решение на 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
0
01.03.2021, 13:15

Теги

Похожие вопросы