Event-Driven Programming với hệ thống tải cao

7th Jan 2023
Ngày nay, một trong những thách thức lớn nhất với các Developers là phải tối ưu hệ thống của mình
Table of contents

Ngày nay, một trong những thách thức lớn nhất với các Developers là phải tối ưu hệ thống của mình, đặc biệt các hệ thống chịu tải cao, tới từng micro-seconds để đáp ứng số người dùng không ngừng tăng lên. Tuy nhiên, hầu hết các hệ thống lại có một sự lãng phí không hề nhỏ dành cho việc chờ đợi đọc/ghi dữ liệu hoặc các tác vụ khác.

>> Event Driven Architecture - Nguyễn Thế Huy - Laravel Meetup HCM 2022

Hãy nhìn vào thực tế cuộc sống, đó là một “tiến trình bất đồng bộ” giống như ví dụ sau đây:

  • Bạn bước vào quán cafe, nhân viên đứng quầy mời bạn gọi đồ
  • Sau đó, bạn ra bàn ngồi đồng thời lôi smartphone của mình ra check-in trên Facebook
  • Nhân viên làm xong và đưa đồ uống ra bàn cho bạn
  • Bạn thưởng thức tách cafe mà không quên kiểm tra comment tại post của mình

Trong ngữ cảnh hoàn toàn khác, thử tưởng tượng bạn (đặc biệt là vị khách đứng sau bạn) sẽ cảm thấy thế nào khi phải chôn chân đợi nhân viên pha chế xong đồ? Chắc hẳn sẽ rất khó chịu và thấy thật lãng phí thời gian + kém hiệu quả Tương tự vậy, trong phần mềm, hướng tiếp cận “bất đồng bộ” cũng khiến tăng năng lực phục vụ cho hệ thống của bạn.

I. Các khái niệm cơ bản

1. Mô hình Blocking I/O

Đây là cách mà các thread dành thời gian để thực hiện một tác vụ. Trong đó, bao gồm cả việc đợi kết quả trả về từ quá trình IO hoàn thành. Ví dụ như việc chờ đợi I/O của disk, network…

Thread-based vs Event-based

2. Mô hình Non-Blocking I/O

Ngược lại với mô hình Blocking IO, mô hình Non-Blocking cung cấp cách thức khiến các thread không cần thiết chờ đợi việc đọc/ghi IO. Như vậy, thread có thể tiếp tục thực hiện tác vụ khác qua đó giúp ứng dụng giảm bớt độ trễ do chờ đợi không cần thiết.

Thread-based vs Event-based

3. Asynchronous (bất đồng bộ)

Đây là phương thức khiến tác vụ trả về kết quả ngay lập tức (qua 1 đối tuợng như Future, Promise, Deferred) dù chưa thực sự hoàn thành, nhưng sau đó chúng tiếp tục được thực hiện 1 cách song song (parallel) tại 1 thread khác và trả kết quả thực sự thông qua callback. Chính nhờ vậy mà thread sẽ không bị block bởi việc chờ đợi các tác vụ khác.

Khái niệm Asynchronous khá tương đồng với non-blocking. Trong thực tế, nhiều trường hợp 2 khái niệm này hoàn toàn có thể thay thế cho nhau.

II. Thread-based vs Event-based

1. Công nghệ

Ngày nay, phần lớn các lập trình viên thường làm việc với các công nghệ dựa trên Thread. Có thể kể ra đây như:

  • Apache httpd

  • Ruby on Rails

  • Django

  • PHP

Mỗi khi có 1 request được gửi tới, Web Server sẽ tạo ra 1 thread mới và đưa lần lượt vào threadpools (hay còn gọi là worker thread) để xử lý. Sau đó, các ngôn ngữ và framework trên sẽ xử lý các tác vụ này 1 cách độc lập. Thêm vào đó, chúng sẽ bị block (synchronous) đến khi kết quả được trả về client. Do việc khởi tạo ra nhiều thread trong pool khá đắt đỏ (mặc định stack size dành cho 1 thread là 1MB trên JVM 64bit), vì vậy size của threadpool thường được fix cố định.

Thread-based vs Event-based

Tuy nhiên trong hệ thống có traffic lớn, nó lại dẫn chúng ta đến 1 nghịch lý. Đó là có quá nhiều thread trong pool thì hệ thống trở nên khó kiểm soát, cost dành cho memory và context switching tăng lên. Ngược lại, duy trì quá ít thread trong pool lại khiến tăng độ trễ và giảm khả năng đáp ứng của hệ thống -> “game of the threadpools size”.

Thread-based vs Event-based

Thêm nữa, 1 ứng dụng dạng truyền thống xây dựng trên Thread-based thì mọi ý tưởng sẽ xoay quanh 1 thứ đó là callstack. Các chức năng sẽ được gọi và chờ đợi lẫn nhau (blocked) để cùng trả về 1 kết quả cuối cùng. Với các ứng dụng như vậy, callstack sẽ trở nên giống như thế này

Thread-based vs Event-based

Ngược lại với mô hình trên, đó là hướng tiếp cận theo Event-based giúp chúng ta giải quyết các nhược điểm trên. Các công nghệ dựa vào mô hình này như:

  • Play

  • Twisted

  • Node.js

  • Nginx

  • Redis

Event-based sử dụng duy nhất 1 thread (trên mỗi core CPU) và dùng event-loop để xử lý các event trong queue. Mọi xử lý IO đều được gọi một cách bất đồng bộ thông qua callback. Đây cũng là cách non-blocking event handle thường dùng.

Thread-based vs Event-based

Tóm lại, kiến trúc event-based có ưu điểm:

  • Single-thread trên mỗi core CPU

    • Không tốn cost cho context-switching

    • Không lo lắng đến pool size

  • Sử dụng event loop gọi I/O 1 cách bất đồng bộ (sử dụng callback) ~>

    • Không chờ đợi 1 cách vô ích

2. Kỹ thuật lập trình

Multi-thread programming

Trong thực tế thường gặp của Developer, khi cần hoàn thành nhanh chóng 1 tác vụ, chúng ta thường thiết kế chạy nó 1 cách song song (parallel). Đó là một ý tưởng tốt, tuy nhiên, điều này không thực sự đơn giản bởi chúng thường sẽ gặp các vấn đề như:

  • Deadlocks – các thread cùng tranh chấp với nhau 1 resource khi cùng truy cập vào nó.

  • Thread không thể khởi chạy vì tài nguyên dùng chung bị chặn.

Thread-based vs Event-based

Cách giải quyết tốt là sử dụng 1 threadpools .Nhưng khi đó, chúng ta lại rơi vào cái bẫy “nghịch lý threadpool” phía trên

Event-driven programming

Event-driven dựa trên nguyên lý trigger các event. Khi đó các event sẽ được monitor(watched, listened…) bởi 1 hoặc nhiều observers khác nhau. Khác biệt cơ bản đó là việc mỗi khi event được phát ra (broadcast, emit…), các observer sẽ xử lý chúng và chương trình sẽ không bị blocked để chờ đợi kết quả trả về, nhờ đó event-loop sẽ tiếp tục xử lý các request mới. Khi đó ứng dụng của ta trở nên :

  • Có khả năng chịu tải cao hơn (high concurrency)

  • Loose coupling – các thành phần trong hệ thống không còn phụ thuộc lẫn nhau

  • Có khả năng mở rộng (scalable)

Thế nhưng, khi lập trình theo cách này, điều các developers (như NodeJS) thường xuyên gặp phải đó là callback hell khiến các code của ứng dụng trở nên như một mớ bòng bong, rối rắm(spaghetti code) gây khó khăn cho việc mở rộng và maintain của hệ thống. Và việc debug trong hệ thống cũng trở nên khó khăn hơn so với ứng dụng truyền thống.

Thread-based vs Event-based

Nhưng trên quan điểm thiết kế, chúng ta có thể sử dụng Message-Driven Programming(hay Actor-based). Kiến trúc hướng message là sự mở rộng của Event-driven và cũng là nền tảng của 1 ứng dụng Reactive. Nó dựa trên việc truyền tải các message trực tiếp đến các actor(do đó được gọi là actor-based), khác với việc chỉ đơn giản broadcast ra event (có thể là qua 1 trung gian như eventbus) của Event-driven. Hiện này nhiều thư viện support tốt Event/Message-driven programming khiến việc tiếp cận phương pháp này trở nên dễ dàng hơn, trong đó nổi bật là:

  • Akka

  • RabbitMQ

  • Apache Kafka

III. Kết luận

Ngày nay, để giải quyết những thách thức này 1 lớn (như C10k problem và C10M problem), chúng ta cần phải thay đổi nhận thức và tiếp cận bằng phương pháp lập trình mới. Khi đó Event/Message-Driven Programming sẽ là lựa chọn hàng đầu.

Bạn thấy bài viết này như thế nào?
1 reaction

Add new comment

Image CAPTCHA
Enter the characters shown in the image.

Related Articles

Mỗi kết nối cơ sở dữ liệu được định nghĩa trong một mảng, với tên kết nối là khóa của mảng

Eager Loading là một kỹ thuật tối ưu hóa truy vấn cơ sở dữ liệu trong Laravel, giúp tăng tốc độ truy vấn và giảm số lượng truy vấn cần thiết để lấy dữ liệu liên quan đến một bản ghi.

Để sử dụng Eager Loading với điều kiện trong Laravel, bạn có thể sử dụng phương thức whereHas hoặc orWhereHas trong Eloquent Builder.

E hiểu đơn giản vầy nha. auth() hay Auth trong laravel là những function global hay class, nó cũng chỉ là 1 thôi

Xin chào các bạn, tuần này mình sẽ viết một bài về cách xử lý Real Time(thời gian thực) với Laravel và Pusher