Чи безпечні потоки глобальних змінних у колбі? Як розподілити дані між запитами?


101

У моєму додатку стан загального об'єкта змінюється шляхом надсилання запитів, і відповідь залежить від стану.

class SomeObj():
    def __init__(self, param):
        self.param = param
    def query(self):
        self.param += 1
        return self.param

global_obj = SomeObj(0)

@app.route('/')
def home():
    flash(global_obj.query())
    render_template('index.html')

Якщо я запущу це на своєму сервері розробки, я очікую отримати 1, 2, 3 тощо. Якщо запити надходять від 100 різних клієнтів одночасно, чи може щось піти не так? Очікуваним результатом буде те, що 100 різних клієнтів побачать унікальне число від 1 до 100. Або відбудеться щось подібне:

  1. Запити клієнта 1. self.paramзбільшується на 1.
  2. Перш ніж оператор return може бути виконаний, потік перемикається на клієнт 2. self.paramзнову збільшується.
  3. Потік повертається до клієнта 1, і клієнту повертається номер 2, скажімо.
  4. Тепер потік переходить до клієнта 2 і повертає йому номер 3.

Оскільки клієнтів було лише двоє, очікувані результати були 1 і 2, а не 2 і 3. Кількість пропущено.

Чи насправді це станеться, коли я масштабую свою заявку? На які альтернативи глобальній змінній слід подивитися?

Відповіді:


97

Ви не можете використовувати глобальні змінні для зберігання такого роду даних. Це не тільки не безпечно для потоків, це не безпечно для процесів , а сервери WSGI у виробництві породжують кілька процесів. Не тільки ваші підрахунки будуть помилковими, якщо ви використовуєте потоки для обробки запитів, вони також будуть відрізнятися залежно від того, який процес обробляв запит.

Використовуйте джерело даних поза Flask для зберігання глобальних даних. База даних, memcached або redis - це всі відповідні окремі зони зберігання, залежно від ваших потреб. Якщо вам потрібно завантажити та отримати доступ до даних Python, розгляньте multiprocessing.Manager. Ви також можете використовувати сеанс для простих даних для кожного користувача.


Сервер розробки може працювати в одному потоці та обробляти. Ви не побачите поведінку, яку ви описуєте, оскільки кожен запит оброблятиметься синхронно. Увімкніть потоки або процеси, і ви побачите їх. app.run(threaded=True)або app.run(processes=10). (В 1.0 сервер поточно встановлений за замовчуванням.)


Деякі сервери WSGI можуть підтримувати gevent або іншого асинхронного працівника. Глобальні змінні все ще не є безпечними для потоків, оскільки досі немає захисту від більшості расових умов. Ви все ще можете мати сценарій, коли один працівник отримує значення, поступається, інший модифікує його, поступається, тоді перший працівник також змінює його.


Якщо вам потрібно зберегти деякі глобальні дані під час запиту, ви можете використовувати gоб'єкт Flask . Інший поширений випадок - це якийсь об’єкт верхнього рівня, який управляє підключеннями до бази даних. Відмінність для цього типу "глобального" полягає в тому, що він унікальний для кожного запиту, а не використовується між запитами, і є щось, що керує налаштуванням і відключенням ресурсу.


30

Це насправді не відповідь на безпеку потоків у глобальних масштабах.

Але я думаю, що тут важливо згадати сесії. Ви шукаєте спосіб зберігання даних, специфічних для клієнта. Кожне з'єднання повинно мати доступ до власного пулу даних безпечним способом.

Це можливо для сесій на стороні сервера, і вони доступні в дуже акуратному плагіні: https://pythonhosted.org/Flask-Session/

Якщо ви налаштовуєте сеанси, sessionзмінна доступна у всіх ваших маршрутах, і вона поводиться як словник. Дані, що зберігаються в цьому словнику, є індивідуальними для кожного підключається клієнта.

Ось коротка демонстрація:

from flask import Flask, session
from flask_session import Session

app = Flask(__name__)
# Check Configuration section for more details
SESSION_TYPE = 'filesystem'
app.config.from_object(__name__)
Session(app)

@app.route('/')
def reset():
    session["counter"]=0

    return "counter was reset"

@app.route('/inc')
def routeA():
    if not "counter" in session:
        session["counter"]=0

    session["counter"]+=1

    return "counter is {}".format(session["counter"])

@app.route('/dec')
def routeB():
    if not "counter" in session:
        session["counter"] = 0

    session["counter"] -= 1

    return "counter is {}".format(session["counter"])


if __name__ == '__main__':
    app.run()

Після цього pip install Flask-Sessionви зможете запустити це. Спробуйте отримати доступ до нього з різних браузерів, і ви побачите, що лічильник між ними не ділиться.


3

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

... вбудовані типи даних python, і я особисто використовував та тестував глобальні dict, згідно з документами python ( https://docs.python.org/3/glossary.html#term-global-interpreter-lock ) нитка безпечна. Не обробляти безпечно.

Вставки, пошук і читання з такого (глобального сервера) дикту будуть нормальними для кожного (можливо, паралельного) сеансу колби, запущеного на сервері розробки.

Коли такий глобальний дикт вводиться за допомогою унікального ключа сеансу-колби, він може бути досить корисним для зберігання на сервері даних, характерних для сеансу, інакше не вміщуються в файл cookie (максимальний розмір 4k).

Звичайно, такий глобальний сервер сервера слід ретельно охороняти, щоб він не зростав занадто великим, перебуваючи в пам'яті. Якийсь термін дії "старих" пар ключ / значення закінчується може бути закодований під час обробки запиту.

Знову ж таки, не рекомендується для виробничих або масштабованих розгортань, але, можливо, добре для локальних серверів, орієнтованих на завдання, де окремих баз даних занадто багато для даного завдання

...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.