Python Redis и Celery слишком много клиентов, разные ошибки при каждом выполнении |Задачи подключаются к MySQL с помощью pymsql - PullRequest
0 голосов
/ 15 ноября 2018

В настоящее время я работаю над приложением, которое должно обрабатывать несколько длительных задач. Я использую python 3, flask, celery, redis.

У меня есть рабочее решение на localhost, но на heroku много ошибок, и каждое выполнение приложения вызывает каждый раз различный набор ошибок. Я знаю, что это не может быть случайным, поэтому я пытаюсь понять, с чего начать.

У меня такое ощущение, что с redis что-то не так, и я пытаюсь понять, кто такие клиенты и откуда они берутся, но я не могу найти официальную документацию или объяснения по этой теме.

Вопрос:

Если сервер redis запущен (даже на локальном хосте), многие клиенты подключены, хотя я ничего не сделал. На heroku (я использую heroku-redis ) у меня всегда 6 клиентов, на localhost 11 клиентов.

Я провел некоторые исследования и могу отобразить их с помощью:

if 'DYNO' in os.environ:
    redis_db = redis.StrictRedis(host='HOST', port=15249, password='REDISDBPW')
else:
    redis_db = redis.StrictRedis()

# see what keys are in Redis
all_keys = redis_db.keys()
print (all_keys)

all_clients = redis_db.client_list()
print (all_clients)

Я вижу всех этих клиентов, но информация там мне совсем не помогает. Кто они такие? Почему они там? Откуда они?

У всех надстроек heroku redis есть клиентский лимит, поэтому мне нужно это понять и оптимизировать. Сначала я подумал clientsnumber == tasknumber, но это не так.

Всего у меня определено 12 задач, но сейчас я тестирую 2 задачи (обе заканчиваются менее чем за 30 секунд).

Когда я выполняю задачи на локальном хосте, клиенты увеличиваются с 11 до 16. Если я выполняю еще раз с 16 до 18 и после этого они всегда остаются на 18, не имеет значения, как часто я выполняю задачи.

Так что здесь происходит? У меня есть 2 задачи, почему клиенты увеличиваются с 11 до 16, а затем с 16 до 18? Почему они не закрываются после завершения задания?

Я бьюсь над этой проблемой уже несколько дней (хотя она всегда работает на локальном хосте), поэтому любая помощь или идеи приветствуются. Мне нужно где-то начать искать, поэтому сейчас я пытаюсь понять клиентов.

EDIT:

Я установил flower и попытался отследить 2 задачи на localhost, все выглядит хорошо. Он обрабатывает две задачи, и обе выполняются за несколько секунд. Возвращаемое значение является правильным (но оно всегда отлично работало на localhost).

Тем не менее, проблема в том, что после того, как я начал заниматься цветком, количество клиентов подскочило до 30. Я до сих пор не понимаю: Что такое клиенты ? При количестве сгенерированных мной клиентов мне понадобится надстройка за 100 $, чтобы просто обработать две задачи, для завершения которых требуется несколько секунд, это не может быть правдой, я все еще думаю, что что-то не так с redis, даже на localhost.

Моя настройка redis довольно проста:

if 'DYNO' in os.environ:
    app.config['CELERY_BROKER_URL'] = 'redis://[the full URL from the redis add-on]'
    app.config['CELERY_RESULT_BACKEND'] = 'redis://[the full URL from the redis add-on]'
else:
    app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
    app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost'

celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], backend=app.config['CELERY_RESULT_BACKEND'])

Вот пример задачи:

@celery.task(bind=True)
def get_users_deregistrations_task(self, g_start_date, g_end_date):

    start_date = datetime.strptime(g_start_date, '%d-%m-%Y')
    end_date = datetime.strptime(g_end_date, '%d-%m-%Y')

    a1 = db_session.query(func.sum(UsersTransactionsVK.amount)).filter(UsersTransactionsVK.date_added >= start_date, UsersTransactionsVK.date_added <= end_date, UsersTransactionsVK.payed == 'Yes').scalar()
    a2 = db_session.query(func.sum(UsersTransactionsStripe.amount)).filter(UsersTransactionsStripe.date_added >= start_date, UsersTransactionsStripe.date_added <= end_date, UsersTransactionsStripe.payed == 'Yes').scalar()
    a3 = db_session.query(func.sum(UsersTransactions.amount)).filter(UsersTransactions.date_added >= start_date, UsersTransactions.date_added <= end_date, UsersTransactions.on_hold == 'No').scalar()

    if a1 is None:
        a1 = 0

    if a2 is None:
        a2 = 0

    if a3 is None:
        a3 = 0

    amount = a1 + a2 + a3

    return {'some_value' : amount}

# Selects user deregistrations between selected dates
@app.route('/get-users-deregistration', methods=["POST"])
@basic_auth.required
@check_verified
def get_users_deregistrations():
    if request.method == "POST":

        # init task
        task = get_users_deregistrations_task.apply_async([session['g_start_date'], session['g_end_date']])
        return json.dumps({}), 202, {'Location': url_for('taskstatus_get_users_deregistrations', task_id=task.id)}

@app.route('/status/<task_id>')
def taskstatus_get_users_deregistrations(task_id):
    task = get_users_deregistrations_task.AsyncResult(task_id)
    if task.state == 'PENDING':
        response = {
            'state': task.state,
            'current': 0,
            'total': 1,
            'status': 'Pending...'
        }
    elif task.state != 'FAILURE':
        response = {
            'state': task.state,
            'current': task.info['current'],
            'total': task.info['total'],
            'status': 'Finished',
            'statistic': task.info['statistic'],
            'final_dataset': task.info     
        }
        if 'result' in task.info:
            response['result'] = task.info['result']
    else:
        print ('in else')
        # something went wrong in the background job
        response = {
            'state': task.state,
            'current': 1,
            'total': 1,
            'status': str(task.info),  # this is the exception raised
        }
    return json.dumps(response)

EDIT:

Вот мой профайл для героку:

web: gunicorn stats_main:app
worker: celery worker -A stats_main.celery --loglevel=info

EDIT

Я думаю, что проблема может быть в пуле соединений (на стороне redis), который я не использую должным образом.

Я также нашел несколько конфигураций для сельдерея и добавил их:

celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], backend=app.config['CELERY_RESULT_BACKEND'], redis_max_connections=20, BROKER_TRANSPORT_OPTIONS = {
    'max_connections': 20,
}, broker_pool_limit=None)

Я снова все загрузил в героку с этими конфигурациями. Я все еще тестирую только 2 задачи, которые выполняются быстро.

Я выполнил задания по героку 10 раз подряд, 7 раз они сработали. 3 раза казалось, что они закончили слишком рано: возвращенный результат был неправильным (правильный результат, например, 30000, и он возвратился 3 раза 18000).

Клиенты быстро подскочили до 20, но они никогда не превышали 20, поэтому, по крайней мере, максимальная ошибка клиента и потеря соединения с ошибкой redis устраняются.

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

EDIT

Неважно, ничего не решено, все кажется случайным.Я добавил два print() в одну из задач для дальнейшей отладки и загрузил в heroku.После 2 выполнений я снова вижу, что соединение с redis потеряно, максимальное число клиентов достигло (несмотря на то, что мое дополнение redismonitor показывает, что клиенты никогда не превышали 20)

EDIT

Большое количество клиентов может быть вызвано незанятыми клиентами, которые по какой-то причине никогда не закрываются (можно найти в блоге heroku ):

По умолчанию Redis никогда не будетзакрыть незанятые соединения, что означает, что если вы не закроете свои соединения Redis явно, вы заблокируете себя из своего экземпляра.

Чтобы этого не произошло, Heroku Redis устанавливает время ожидания соединения по умолчанию равное 300секунд.Этот тайм-аут не относится к клиентам без публикации / подписки и другим операциям блокировки.

Я добавил функцию уничтожения для незанятых клиентов прямо перед КАЖДОЙ из моих задач:

def kill_idle_clients():
    if 'DYNO' in os.environ:
        redis_db = redis.StrictRedis(host='HOST', port=15249, password='REDISDBPW')
    else:
        redis_db = redis.StrictRedis()

    all_clients = redis_db.client_list()
    counter = 0
    for client in all_clients:
        if int(client['idle']) >= 15:
            redis_db.client_kill(client['addr'])
            counter += 1

    print ('killing idle clients:', counter)

Перед запуском задачи закрываются все клиенты, которые простаивают более 15 секунд.Он снова работает на localhost (но не удивительно, он всегда работал на localhost).У меня меньше клиентов, но на героку это сработало теперь только 2 раза из 10. 8 раз задания заканчивали слишком рано.Возможно, неработающие клиенты не были в действительности бездействующими, я понятия не имею.

Его также практически невозможно протестировать, поскольку каждое выполнение задач имеет различный результат (теряет соединение с redis, достигло предела клиента, завершается слишком рано, работает отлично).

РЕДАКТИРОВАТЬ

Кажется, настройки сельдерея игнорировались все время.Я все время с подозрением относился к этому и решил проверить это, добавив несколько случайных аргументов и изменив значения до не смысла.Я перезапустил работника сельдерея ofc.

Я ожидал увидеть некоторые ошибки, но он работает, как будто ничего не произошло.

Все работает, как и раньше, с этими бессмысленными конфигурациями:

celery = Celery(app.name, broker=app.config['REDIS_URL'], backend=app.config['REDIS_URL'], redis_max_connections='pups', BROKER_TRANSPORT_OPTIONS = {
    'max_connections': 20,
}, broker_pool_limit=None, broker_connection_timeout='pups', pups="pups")
celery.conf.broker_transport_options = {'visibility_timeout': 'pups'}

РЕДАКТИРОВАТЬ

Я изменил способ загрузки конфигураций для сельдерея (из отдельного файла конфигурации).Кажется, работает сейчас, но проблемы остаются теми же.

celery_task = Celery(broker=app.config['REDIS_URL'], backend=app.config['REDIS_URL'])
celery_task.config_from_object('celeryconfig')

EDIT

С этими конфигурациями мне удалось ограничить количество клиентов на localhost в 18 длявсе задачи (я пробовал все 12 задач).Однако на героку это "как-то" работает.Клиентов меньше, но количество достигло 20 за один раз, хотя я думал, что не смогу превысить 18. (Я тестировал на heroku с 4 задачами).

Тестирование на heroku со всеми 12 задачами вызывает много различных ошибок SQL.Я сейчас более смущен, чем раньше.Кажется, одна и та же задача выполняется несколько раз, но я вижу только 12 URL-адресов задач.

Я думаю, что из-за ошибок SQL, например:

sqlalchemy.exc.InternalError: (pymysql.err.InternalError) Packet sequence number wrong - got 117 expected 1

или

sqlalchemy.exc.InterfaceError: (pymysql.err.InterfaceError) (0, '')

или

Multiple rows were found for one()

Я тестировал несколько раз на героку с 4 заданиями, и были времена, когда результаты заданий возвращались, но результаты были очень странными.

На этот раз заданияне закончил слишком рано, но возвратил увеличенные значения, похоже, что задача A вернула значение 2 раза и суммировала его.

Пример: Задача A должна вернуть 10k, но вернула 20k, поэтому задача была выполненадважды, и результат суммируется.

Вот мои текущие конфигурации.Я все еще не понимаю математику на 100%, но я думаю, что это (для количества клиентов):

max-conncurency * CELERYD_MAX_TASKS_PER_CHILD

На локальном хосте я нашел новую команду CLI для проверки статистики работника, и у меня было max-conncurecy=3 и CELERYD_MAX_TASKS_PER_CHILD=6

Команда CLI:

celery -A stats_main.celery_task inspect stats

Мои текущие конфигурации:

Рабочий запуск:

celery worker -A stats_main.celery_task --loglevel=info --autoscale=10,3

config:

CELERY_REDIS_MAX_CONNECTIONS=20
BROKER_POOL_LIMIT=None
CELERYD_WORKER_LOST_WAIT=20
CELERYD_MAX_TASKS_PER_CHILD=6
BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 18000} # 5 hours
CELERY_RESULT_DB_SHORT_LIVED_SESSIONS = True #useful if: For example, intermittent errors like (OperationalError) (2006, ‘MySQL server has gone away’)

РЕДАКТИРОВАТЬ

Теперь, увидев все эти ошибки SQL, я решил исследовать совершенно другое направление.Моя новая теория заключается в том, что это может быть проблема MySQL.

Я настроил свое соединение с сервером MySQL, как описано в ответе на этот вопрос .

Я также узнал, что у pymsql есть threadsafety=1, я пока не знаю, может ли это быть проблемой, но, похоже, MySQL как-то связан с соединениями и пулами соединений.

На данный момент я тоже могускажем, что память не может быть проблемой, потому что, если пакеты были слишком большими, они не должны работать на localhost, что означает, что я оставил max_allowed_packet со значением по умолчанию, которое составляет около 4 МБ.

У меня естьтакже создал 3 фиктивных задачи, которые делают несколько простых вычислений без подключения к внешней базе данных MySQL.Я выполнил сейчас 5 раз на heroku и ошибок не было, результаты всегда были правильными, поэтому я предполагаю, что проблема не в сельдерее, редисе, а в MySQL, хотя я понятия не имею, почему он будет работать на localhost.Может быть, это комбинация всех 3, что приводит к проблемам на героку.

EDIT

Я настроил свой файл JS.Теперь каждая задача вызывается одна за другой, что означает, что они не асинхронные (я все еще использую apply_async сельдерея, потому что apply не работал)

Так что это сложный обходной путь.Я просто создал var для каждой задачи, например, var task_1_rdy = false;

Я также создал функцию, которая запускается каждые 2 секунды и проверяет, готова ли одна задача, если она готова, запускается следующая задача.Я думаю, что легко понять, что я здесь сделал.

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

РЕДАКТИРОВАТЬ

Я сегодня много тестировал.Время, необходимое для выполнения заданий одинаковое (синхронизация и асинхронность). Я не знаю почему, но оно одинаковое.

Работа со всеми 12 заданиями на герою и выбор огромного диапазона времени (огромный интервал времени =задачи занимают больше времени, потому что обрабатывается больше данных):

Опять же, результаты задачи не точны, возвращаемые значения неправильные, только немного неправильные, но неправильные и, следовательно, ненадежные, например, задача A должна вернуть 20k иГерою вернули 19500. Я не знаю, как это возможно, что данные потеряны / задача возвращается слишком рано, но через 2 недели я сдамся и попытаюсь использовать совершенно другую систему.

Ответы [ 2 ]

0 голосов
/ 17 ноября 2018

Теперь я на 60% уверен, что это ваша задача, которая занимает слишком много времени, и сервер не может ответить в течение времени возврата веб-запроса по умолчанию.Значение 70% / 30% применимо, когда вы находитесь на локальной машине, где сеть очень быстрая.На облачной платформе задержка является проблемой, и иногда она влияет на вашу программу.До этого, если celery worker не удалось, он автоматически создаст другого работника для завершения незавершенного задания из-за gunicon и celery, что вызывает увеличение соединения.

Таким образом, решение:

  • Вариант 1 ускоряет выполнение задачи

  • Вариант 2 сначала возвращает подтверждение, вычисляетв фоновом режиме и сделайте еще один вызов API для отправки результатов

0 голосов
/ 15 ноября 2018

звучит так, как будто вы используете rest-api, используя в качестве очереди сообщений сельдерея. вот список чк:

1 в вашем клиенте вы закрыли соединение после завершения логики

2 сельдерея будут новые рабочие, рабочие могут вызвать проблемы, попробуйте следить за сельдереем с цветком

3 убедитесь, что ваш клиент завершил задачу, попробуйте отладку с распечаткой чего-либо, иногда при постановке и локальных проблемах с сетью, которые мешают вам завершить задачу celery

4, если вы используете redis для очереди сообщений сельдерея, попробуйте отслеживать количество очередей, возможно, они автоматически увеличиваются?

...