Масштабируемое отложенное выполнение задачи с помощью Redis

Мне нужно создать масштабируемую систему планирования задач Redis.

Требования:

  • Несколько рабочих процессов.
  • Возможны многие задачи, но возможны длительные периоды бездействия.
  • Разумная временная точность.
  • Минимальный ресурс отходов в режиме ожидания.
  • Должен использовать синхронный API Redis.
  • Должен работать для Redis 2.4 (т.е. нет функций от предстоящего 2.6).
  • Не следует использовать другие средства RPC, кроме Redis.

Псевдо-API: schedule_task(timestamp, task_data). Временная метка находится в целых секундах.

Основная идея:

  • Прослушивание предстоящих задач в списке.
  • Поместите задачи в ведра за метку времени.
  • Сон до ближайшей метки времени.
  • Если появляется новая задача с меткой времени меньше ближайшей, просыпайтесь.
  • Обработать все предстоящие задачи с меткой времени ≤ сейчас, партиями (при условии выполнение этой задачи выполняется быстро).
  • Убедитесь, что параллельный рабочий не будет обрабатывать одни и те же задачи. В то же время убедитесь, что никакие задачи не будут потеряны, если мы потерпим крах при их обработке.

До сих пор я не могу понять, как это сделать в примитивах Redis...

Любые подсказки?

Обратите внимание, что существует аналогичный старый вопрос: Отложенное выполнение/планирование с помощью Redis? В этом новом вопросе я представляю более подробную информацию (самое главное, многие работники). До сих пор я не мог понять, как применять старые ответы здесь - вот, новый вопрос.

Ответ 1

Здесь другое решение, которое основывается на нескольких других [1]. Он использует команду redis WATCH для удаления условия гонки без использования lua в redis 2.6.

Основная схема:

  • Используйте redis zset для запланированных задач и очереди redise для готовых к запуску задач.
  • Попросите диспетчера опросить zset и переместите задачи, которые готовы запустить в очереди redis. Возможно, вам понадобится более 1 диспетчера для резервирования, но вам, вероятно, не нужно или нужно много.
  • Имейте столько рабочих, сколько хотите, которые блокируют всплывающие окна в очереди redis.

Я не тестировал его: -)

Создатель задания foo:

def schedule_task(queue, data, delay_secs):
    # This calculation for run_at isn't great- it won't deal well with daylight
    # savings changes, leap seconds, and other time anomalies. Improvements
    # welcome :-)
    run_at = time.time() + delay_secs

    # If you're using redis-py Redis class and not StrictRedis, swap run_at &
    # the dict.
    redis.zadd(SCHEDULED_ZSET_KEY, run_at, {'queue': queue, 'data': data})

schedule_task('foo_queue', foo_data, 60)

Диспетчер будет выглядеть так:

while working:
    redis.watch(SCHEDULED_ZSET_KEY)
    min_score = 0
    max_score = time.time()
    results = redis.zrangebyscore(
        SCHEDULED_ZSET_KEY, min_score, max_score, start=0, num=1, withscores=False)
    if results is None or len(results) == 0:
        redis.unwatch()
        sleep(1)
    else: # len(results) == 1
        redis.multi()
        redis.rpush(results[0]['queue'], results[0]['data'])
        redis.zrem(SCHEDULED_ZSET_KEY, results[0])
        redis.exec()

Рабочий-foo будет выглядеть так:

while working:
    task_data = redis.blpop('foo_queue', POP_TIMEOUT)
    if task_data:
        foo(task_data)

[1] Это решение основано на not_a_golfer's, один на http://www.saltycrane.com/blog/2011/11/unique-python-redis-based-queue-delay/ и redis docs для транзакций.

Ответ 2

Вы не указали язык, который используете. У вас есть как минимум 3 альтернативы для этого, не вписывая хотя бы одну строку кода в Python.

  • Сельдерей имеет дополнительный брокер redis. http://celeryproject.org/

  • resque - чрезвычайно популярная очередь задач redis с использованием redis. https://github.com/defunkt/resque

  • RQ - простая и маленькая очередь на основе redis, которая направлена ​​на то, чтобы "взять хороший материал из сельдерея и resque" и будет намного проще работать. http://python-rq.org/

Вы можете по крайней мере посмотреть на их дизайн, если вы не можете их использовать.

Но чтобы ответить на ваш вопрос - что вы хотите, можно сделать с помощью redis. Я на самом деле написал более или менее то, что было в прошлом.

EDIT: Что касается моделирования того, что вы хотите на redis, я бы это сделал:

  • Очередь в задание с меткой времени будет выполняться непосредственно клиентом - вы ставите задачу в отсортированном наборе с меткой времени, как оценка, а задача - как значение (см. ZADD).

  • Центральный диспетчер просыпается каждые N секунд, проверяет первые временные метки на этом наборе, и если есть готовые к выполнению задачи, он подталкивает задачу к списку "Выполняется СЕЙЧАС". Это можно сделать с помощью ZREVRANGEBYSCORE в отсортированном наборе "ожидания", получив все элементы с меткой времени <= now, чтобы сразу получить все готовые элементы. нажатие осуществляется с помощью RPUSH.

    Рабочие
  • используют BLPOP в списке "будет исполнено СЕЙЧАС", проснутся, когда есть что-то для работы, и сделайте свою работу. Это безопасно, так как redis является однопоточным, и никакие 2 работника никогда не будут выполнять одну и ту же задачу.

  • как только закончите, рабочие вернут результат в очередь ответов, которая проверяется диспетчером или другим потоком. Вы можете добавить "ожидающее" ведро, чтобы избежать сбоев или чего-то подобного.

поэтому код будет выглядеть примерно так (это просто псевдокод):

клиент:

ZADD "new_tasks" <TIMESTAMP> <TASK_INFO>

диспетчер:

while working:
   tasks = ZREVRANGEBYSCORE "new_tasks" <NOW> 0 #this will only take tasks with timestamp lower/equal than now
   for task in tasks:

       #do the delete and queue as a transaction
       MULTI
       RPUSH "to_be_executed" task
       ZREM "new_tasks" task
       EXEC

   sleep(1)

Я не добавлял обработку очереди ответов, но это более или менее похоже на рабочего:

рабочий:

while working:
   task = BLPOP "to_be_executed" <TIMEOUT>
   if task:
      response = work_on_task(task)
      RPUSH "results" response

EDit: атомный диспетчер без гражданства:

while working:

   MULTI
   ZREVRANGE "new_tasks" 0 1
   ZREMRANGEBYRANK "new_tasks" 0 1
   task = EXEC

   #this is the only risky place - you can solve it by using Lua internall in 2.6
   SADD "tmp" task

   if task.timestamp <= now:
       MULTI
       RPUSH "to_be_executed" task
       SREM "tmp" task
       EXEC
   else:

       MULTI
       ZADD "new_tasks" task.timestamp task
       SREM "tmp" task
       EXEC

   sleep(RESOLUTION)

Ответ 3

Если вы ищете готовое решение на Java. Redisson подходит именно вам. Это позволяет планировать и выполнять задачи (с поддержкой cron-expression) распределенным способом на узлах Redisson, используя знакомые ScheduledExecutorService api и на основе очереди Redis.

Вот пример. Сначала определите задачу с помощью интерфейса java.lang.Runnable. Каждая задача может получить доступ к Redis экземпляру с помощью объекта RedissonClient.

public class RunnableTask implements Runnable {

    @RInject
    private RedissonClient redissonClient;

    @Override
    public void run() throws Exception {
        RMap<String, Integer> map = redissonClient.getMap("myMap");
        Long result = 0;
        for (Integer value : map.values()) {
            result += value;
        }
        redissonClient.getTopic("myMapTopic").publish(result);
    }

}

Теперь он готов отбросить его в ScheduledExecutorService:

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<?> future = executorService.schedule(new CallableTask(), 10, 20, TimeUnit.MINUTES);

future.get();
// or cancel it
future.cancel(true);

Примеры с выражениями cron:

executorService.schedule(new RunnableTask(), CronSchedule.of("10 0/5 * * * ?"));

executorService.schedule(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));

executorService.schedule(new RunnableTask(), CronSchedule.weeklyOnDayAndHourAndMinute(12, 4, Calendar.MONDAY, Calendar.FRIDAY));

Все задачи выполняются на Redisson node.

Ответ 4

Комбинированный подход кажется правдоподобным:

  • Нет новой метки времени задачи может быть меньше текущего времени (зажим, если меньше). Предполагая надежную синхронизацию NTP.

  • Все задачи переходят к ведомым спискам в ключах, с суффиксами с меткой времени задачи.

  • Кроме того, все временные метки задач переходят на выделенный zset (сам ключ и оценка - временная метка).

  • Новые задачи принимаются от клиентов через отдельный список Redis.

  • Loop: вывести самые старые N просроченных временных меток через zrangebyscore... limit.

  • BLPOP с тайм-аутом в списке новых задач и списками для выбранных временных меток.

  • Если вы получили старую задачу, обработайте ее. Если новый - добавьте в bucket и zset.

  • Убедитесь, что обработанные ведра пусты. Если это так - удалите список и введите из zset. Вероятно, не проверяйте очень давно истекшие ведра, чтобы защитить от проблем синхронизации времени. Конечный цикл.

Критика? Комментарии? Альтернативы?