текст перекладу
Обмеження конкурентності в сценаріях високої продуктивності
У цьому третьому випуску нашої серії ми розглядаємо, як Ruby on Rails, Django, Flask та Go (Echo) обробляють конкурентність у реальному сценарії високої продуктивності — системі онлайн-спільного редагування документів. Порівнюючи реалізації фонових завдань, ми підкреслюємо сильні та слабкі сторони моделей конкурентності кожного фреймворку.
Частина I: Конкурентність у контекстах високої продуктивності
Сучасні веб-додатки повинні обробляти все більші обсяги одночасних запитів. Навіть така здавалося б проста функція, як реальне спільне редагування документів, вимагає бекенд-систем, які можуть обробляти численні одночасні оновлення без аварій чи уповільнень.
- Ruby on Rails використовує модель потоків Ruby для конкурентності. Однак через Global Interpreter Lock (GIL) в реалізації MRI Ruby тільки один потік може тримати блокування одночасно, що створює вузькі місця при високому використанні процесора.
- Python також має GIL, але з версії 3.7 Python ввів бібліотеку
asyncio
для асинхронного програмування, яка може обробляти велику кількість I/O-завдань в однопоточному циклі подій. Проте завдання, що потребують багато процесорного часу, все ще можуть бути обмежені GIL. - Go був спроектований з урахуванням конкурентності з самого початку. Goroutines — це легкі «зелені потоки», що робить простим створення та управління тисячами одночасних завдань в одному процесі. Go також ефективно використовує кілька ядер процесора.
У більшості виробничих систем розробники часто використовують спеціалізовані брокери повідомлень (як-от RabbitMQ чи Kafka) та фреймворки для роботи з фоновими завданнями (Sidekiq для Ruby, Celery для Python), щоб ефективно керувати фоновими завданнями. Однак у цій статті ми зосередимося на тому, як кожен фреймворк може обробляти конкурентність «нативно», без використання сторонніх рішень, щоб краще продемонструвати вбудовані можливості конкурентності кожної мови.
Частина II: Приклад використання — Онлайн-спільне редагування документів
Сценарій
Уявіть собі систему онлайн-спільного редагування документів, де кілька користувачів можуть одночасно редагувати один і той самий документ. Для забезпечення зворотного зв'язку в реальному часі кожна подія редагування надсилається на бекенд. Однак негайне записування кожної окремої зміни в базу даних буде дуже неефективним — надмірне записування в базу може стати серйозною проблемою для продуктивності.
Запропоноване рішення
Звичайний підхід полягає в тому, щоб збирати події редагування в пам'яті та вставляти їх у базу даних партіями через визначені інтервали часу. Це значно зменшує навантаження на базу даних. Ми розглянемо, як досягти такого механізму партіонування в чотирьох різних фреймворках:
- Ruby on Rails (без використання Sidekiq чи інших бібліотек для фонових завдань)
- Django (з використанням
asyncio
в Python) - Flask (також із використанням
asyncio
) - Go (Echo), використовуючи нативні goroutines
Частина III: Реалізації коду та аналіз
Нижче наведено прості фрагменти коду, що демонструють, як кожен фреймворк або мова обробляє фонове партіонування операцій редагування. Ці приклади надані для демонстраційних цілей; у реальному середовищі ви, ймовірно, інтегруєте черги повідомлень, бібліотеки для планування чи інші більш надійні рішення.
1. Ruby on Rails: Потоки та процеси
Rails може досягати конкурентності завдяки комбінації багатопоточності та багатозадачності (наприклад, за допомогою сервера Puma). Однак GIL MRI Ruby означає, що лише один потік може виконувати Ruby-код одночасно. У випадках, коли завдання пов'язані з I/O, потоки можуть передавати управління й все ж отримувати певний рівень паралелізму, але завдання, що потребують багато процесорного часу, зокрема обмежені.
текст перекладу
Обмеження конкурентності в сценаріях високої продуктивності
У цьому третьому випуску нашої серії ми розглядаємо, як Ruby on Rails, Django, Flask та Go (Echo) обробляють конкурентність у реальному сценарії високої продуктивності — системі онлайн-спільного редагування документів. Порівнюючи реалізації фонових завдань, ми підкреслюємо сильні та слабкі сторони моделей конкурентності кожного фреймворку.
Частина I: Конкурентність у контекстах високої продуктивності
Сучасні веб-додатки повинні обробляти все більші обсяги одночасних запитів. Навіть така здавалося б проста функція, як реальне спільне редагування документів, вимагає бекенд-систем, які можуть обробляти численні одночасні оновлення без аварій чи уповільнень.
- Ruby on Rails використовує модель потоків Ruby для конкурентності. Однак через Global Interpreter Lock (GIL) в реалізації MRI Ruby тільки один потік може тримати блокування одночасно, що створює вузькі місця при високому використанні процесора.
- Python також має GIL, але з версії 3.7 Python ввів бібліотеку
asyncio
для асинхронного програмування, яка може обробляти велику кількість I/O-завдань в однопоточному циклі подій. Проте завдання, що потребують багато процесорного часу, все ще можуть бути обмежені GIL. - Go був спроектований з урахуванням конкурентності з самого початку. Goroutines — це легкі «зелені потоки», що робить простим створення та управління тисячами одночасних завдань в одному процесі. Go також ефективно використовує кілька ядер процесора.
У більшості виробничих систем розробники часто використовують спеціалізовані брокери повідомлень (як-от RabbitMQ чи Kafka) та фреймворки для роботи з фоновими завданнями (Sidekiq для Ruby, Celery для Python), щоб ефективно керувати фоновими завданнями. Однак у цій статті ми зосередимося на тому, як кожен фреймворк може обробляти конкурентність «нативно», без використання сторонніх рішень, щоб краще продемонструвати вбудовані можливості конкурентності кожної мови.
Частина II: Приклад використання — Онлайн-спільне редагування документів
Сценарій
Уявіть собі систему онлайн-спільного редагування документів, де кілька користувачів можуть одночасно редагувати один і той самий документ. Для забезпечення зворотного зв'язку в реальному часі кожна подія редагування надсилається на бекенд. Однак негайне записування кожної окремої зміни в базу даних буде дуже неефективним — надмірне записування в базу може стати серйозною проблемою для продуктивності.
Запропоноване рішення
Звичайний підхід полягає в тому, щоб збирати події редагування в пам'яті та вставляти їх у базу даних партіями через визначені інтервали часу. Це значно зменшує навантаження на базу даних. Ми розглянемо, як досягти такого механізму партіонування в чотирьох різних фреймворках:
- Ruby on Rails (без використання Sidekiq чи інших бібліотек для фонових завдань)
- Django (з використанням
asyncio
в Python) - Flask (також із використанням
asyncio
) - Go (Echo), використовуючи нативні goroutines
Частина III: Реалізації коду та аналіз
Нижче наведено прості фрагменти коду, що демонструють, як кожен фреймворк або мова обробляє фонове партіонування операцій редагування. Ці приклади надані для демонстраційних цілей; у реальному середовищі ви, ймовірно, інтегруєте черги повідомлень, бібліотеки для планування чи інші більш надійні рішення.
1. Ruby on Rails: Потоки та процеси
Rails може досягати конкурентності завдяки комбінації багатопоточності та багатозадачності (наприклад, за допомогою сервера Puma). Однак GIL MRI Ruby означає, що лише один потік може виконувати Ruby-код одночасно. У випадках, коли завдання пов'язані з I/O, потоки можуть передавати управління й все ж отримувати певний рівень паралелізму, але завдання, що потребують багато процесорного часу, зокрема обмежені.
# config/initializers/batch_saver.rb
class BatchSaver
def initialize
@queue = Queue.new
start_worker
end
def add_edit(edit_data)
@queue.push(edit_data)
end
private
def start_worker
Thread.new do
loop do
batch = []
until @queue.empty?
batch << @queue.pop
end
save_to_db(batch) unless batch.empty?
sleep(5) # Batch save every 5 seconds
end
end
end
def save_to_db(batch)
DocumentUpdate.create(batch)
end
end
# Usage
saver = BatchSaver.new
saver.add_edit({ user_id: 1, content: "Updated content" })
Аналіз
- Без спеціалізованих інструментів, як-от Sidekiq, Rails змушений покладатися на нативні потоки (які обмежені GIL) або запускати кілька процесів.
- У висококонкурентних середовищах з CPU-інтенсивними завданнями продуктивність може знижуватись, якщо не використовувати багатопроцесорні чи альтернативні реалізації Ruby (JRuby тощо).
- Однак для I/O-завдань Rails може ефективно керувати конкурентністю, оскільки потоки можуть передавати управління під час блокуючих операцій введення/виведення.
2. Django (Python): Нативна підтримка асинхронності (3.7+)
З версії 3.7 Django підтримує асинхронні подання та може використовувати бібліотеку Python asyncio
. Ось спрощений приклад, як можна обробляти зміни в документі асинхронно:
# views.py
edits_queue = []
async def add_edit(request):
global edits_queue
edit_data = request.POST.get('content')
edits_queue.append(edit_data)
return JsonResponse({"status": "queued"})
async def batch_save():
global edits_queue
while True:
if edits_queue:
batch = edits_queue.copy()
edits_queue.clear()
await asyncio.sleep(5) # Simulate database write delay
print(f"Saved to DB: {batch}")
await asyncio.sleep(1)
# Start the background task (e.g. in an async-ready environment)
asyncio.create_task(batch_save())
Аналіз
- GIL Python все ще існує, але
asyncio
дозволяє обробляти велику кількість одночасних I/O-завдань. - Для завдань, що потребують багато процесорного часу, зазвичай потрібно використовувати кілька процесів (через multiprocessing), щоб обійти GIL.
- На відміну від Rails, вбудовані асинхронні подання Django дозволяють легко масштабувати I/O-завдання без використання сторонніх планувальників завдань.
3. Flask (Python): Використання asyncio
Хоча Flask не підтримує асинхронні подання нативно, він є легким і гнучким фреймворком. Інтегруючи asyncio
, ми можемо досягти подібного ефекту до прикладу з Django:
app = Flask(__name__)
edits_queue = []
@app.route('/add_edit', methods=['POST'])
async def add_edit():
edit_data = request.form.get('content')
edits_queue.append(edit_data)
return jsonify({"status": "queued"})
async def batch_save():
global edits_queue
while True:
if edits_queue:
batch = edits_queue.copy()
edits_queue.clear()
await asyncio.sleep(5) # Simulate database write
print(f"Saved to DB: {batch}")
await asyncio.sleep(1)
# Start the background task
loop = asyncio.get_event_loop()
loop.create_task(batch_save())
loop.run_forever()
Аналіз
- Дизайн мікрофреймворку Flask
текст перекладу
## Ось проста демонстрація використання стандартного пакетуnet/http
— Echo дотримується тієї ж основної принципової моделі:
var (
editQueue []string
mutex sync.Mutex
)
func addEdit(w http.ResponseWriter, r *http.Request) {
edit := r.FormValue("content")
mutex.Lock()
editQueue = append(editQueue, edit)
mutex.Unlock()
fmt.Fprintf(w, "Queued edit: %s", edit)
}
func batchSave() {
for {
time.Sleep(5 * time.Second)
mutex.Lock()
if len(editQueue) > 0 {
batch := editQueue
editQueue = []string{}
fmt.Printf("Saved to DB: %v\\n", batch)
}
mutex.Unlock()
}
}
func main() {
go batchSave() // Запуск фонової горутини
http.HandleFunc("/add_edit", addEdit)
http.ListenAndServe(":8080", nil)
}
Аналіз
- Горутини дозволяють реалізувати високо масштабовану конкурентність в одному процесі.
- Для синхронізації та спільного доступу до даних використовуються канали (Channels) або м’ютекси (Mutexes).
- Go ефективно використовує кілька ядер процесора, що робить його дуже підходящим для завдань з високою конкуренцією.
Підсумок порівняння
Rails (Ruby)
- Використовує реалізацію потоків Ruby, але GIL обмежує справжню паралельність.
- Може бути достатнім для багатьох I/O-завдань, але для високої конкуренції часто необхідні багатопроцесорні конфігурації або спеціалізовані інструменти, як-от Sidekiq.
Django (Python)
- Нативні асинхронні подання (3.7+) використовують
asyncio
в Python для ефективної обробки I/O-завдань. - Все ще обмежується GIL для завдань, що потребують багато процесорного часу, і зазвичай вимагає використання multiprocessing.
Flask (Python)
- Не має вбудованої асинхронності, але достатньо гнучкий, щоб інтегруватися з
asyncio
або іншими бібліотеками. - Добре масштабується для менших або індивідуальних асинхронних потреб, але для висококонкурентних сценаріїв може знадобитися більше налаштувань для корпоративного рівня.
Echo (Go)
- Вбудовані горутини забезпечують прямолінійну, потужну конкурентність і можуть ефективно використовувати багатоядерні процесори.
- Дуже ефективний і масштабований для високопродуктивних, реальних середовищ.
Для завдань з високою конкуренцією Go часто вирізняється завдяки легким горутинам та здатності ефективно масштабуватися на багатоядерних процесорах. Python-фреймворки також відмінно справляються з обробкою великої кількості I/O-завдань за допомогою asyncio
, хоча GIL може впливати на завдання, що потребують багато процесорного часу. Rails може обробляти багато завдань із відповідними налаштуваннями процесів/потоків та зовнішніми інструментами, як-от Sidekiq, але GIL у MRI Ruby залишається обмеженням для максимальної паралельності.
Найкраще рішення залежить від потреб вашого проєкту, ресурсів для розробки та конкретної природи робочих навантажень (I/O-завдання проти CPU-завдань). Розуміючи різні моделі конкурентності, ви зможете вибрати правильний інструмент для побудови швидких, надійних додатків.
Перекладено з: Ruby, Python, and Go: A Practical Comparison for Developers (Part 3)