Ruby, Python та Go: Практичне порівняння для розробників (Частина 3)

текст перекладу

Обмеження конкурентності в сценаріях високої продуктивності

У цьому третьому випуску нашої серії ми розглядаємо, як Ruby on Rails, Django, Flask та Go (Echo) обробляють конкурентність у реальному сценарії високої продуктивності — системі онлайн-спільного редагування документів. Порівнюючи реалізації фонових завдань, ми підкреслюємо сильні та слабкі сторони моделей конкурентності кожного фреймворку.

Частина I: Конкурентність у контекстах високої продуктивності

Сучасні веб-додатки повинні обробляти все більші обсяги одночасних запитів. Навіть така здавалося б проста функція, як реальне спільне редагування документів, вимагає бекенд-систем, які можуть обробляти численні одночасні оновлення без аварій чи уповільнень.

  1. Ruby on Rails використовує модель потоків Ruby для конкурентності. Однак через Global Interpreter Lock (GIL) в реалізації MRI Ruby тільки один потік може тримати блокування одночасно, що створює вузькі місця при високому використанні процесора.
  2. Python також має GIL, але з версії 3.7 Python ввів бібліотеку asyncio для асинхронного програмування, яка може обробляти велику кількість I/O-завдань в однопоточному циклі подій. Проте завдання, що потребують багато процесорного часу, все ще можуть бути обмежені GIL.
  3. 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: Конкурентність у контекстах високої продуктивності

Сучасні веб-додатки повинні обробляти все більші обсяги одночасних запитів. Навіть така здавалося б проста функція, як реальне спільне редагування документів, вимагає бекенд-систем, які можуть обробляти численні одночасні оновлення без аварій чи уповільнень.

  1. Ruby on Rails використовує модель потоків Ruby для конкурентності. Однак через Global Interpreter Lock (GIL) в реалізації MRI Ruby тільки один потік може тримати блокування одночасно, що створює вузькі місця при високому використанні процесора.
  2. Python також має GIL, але з версії 3.7 Python ввів бібліотеку asyncio для асинхронного програмування, яка може обробляти велику кількість I/O-завдань в однопоточному циклі подій. Проте завдання, що потребують багато процесорного часу, все ще можуть бути обмежені GIL.
  3. 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)

Leave a Reply

Your email address will not be published. Required fields are marked *