Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Concurrency: Mở rộng hiệu quả cho ứng dụng hiệu năng cao

Làm chủ Concurrency

Nhiều người dùng app cùng lúc, và bạn muốn giao app nhanh nhất có thể. Vậy cần cách xử lý concurrency. Đừng lo! Hầu hết web server đã làm mặc định. Nhưng khi cần scale, bạn muốn dùng concurrency hiệu quả nhất.

Các loại Concurrency khác nhau

Có nhiều cách xử lý concurrency: multi-process, multi-threading và event-driven. Mỗi cái có use case, ưu nhược điểm riêng. Bài này bạn học sự khác biệt và khi nào dùng cái nào.

Multi-process (Unicorn)

Cách dễ nhất xử lý concurrency. Master process fork ra nhiều worker processes. Worker xử lý request thực tế, master quản lý workers.

Mỗi worker process nạp toàn bộ codebase vào bộ nhớ. Điều này làm phương pháp này tốn bộ nhớ, và khó scale cho hạ tầng lớn.

Tóm tắt Multi-process
Use caseVí dụ non-Ruby bạn biết: trình duyệt Chrome. Dùng multi-process concurrency để mỗi tab có process riêng. Cho phép một tab crash không làm sập toàn app. Trong trường hợp của nó, cũng giúp isolate exploit trong một tab.
Ưu điểmĐơn giản nhất để triển khai.
Bỏ qua khó khăn về thread safety.
Mỗi worker crash không ảnh hưởng phần còn lại.
Nhược điểmMỗi process nạp toàn bộ codebase vào bộ nhớ. Tốn bộ nhớ.
Do đó, không scale được cho lượng kết nối đồng thời lớn.

Multi-threading (Puma)

Mô hình threading cho phép một process xử lý nhiều request cùng lúc. Làm bằng cách chạy nhiều thread trong một process.

Khác với multi-process, tất cả thread chạy trong cùng một process. Nghĩa là chia sẻ dữ liệu như biến global. Do đó, chỉ dùng thêm một chút bộ nhớ cho mỗi thread.

Global Interpreter Lock (GIL)

Điều này dẫn đến global interpreter lock (GIL) trong MRI. GIL là khóa quanh việc thực thi toàn bộ Ruby code. Dù thread có vẻ chạy song song, tại một thời điểm chỉ có một thread active.

IO hoạt động bên ngoài GIL. Khi thực thi query database chờ kết quả, nó không lock. Thread khác có cơ hội làm việc trong lúc chờ. Nếu làm nhiều toán học và thao tác hash/array trong thread, bạn chỉ dùng một core nếu dùng MRI. Trong hầu hết trường hợp vẫn cần nhiều process để tận dụng máy. Hoặc dùng Rubinius hoặc JRuby, không có GIL.

Thread safety

Nếu dùng nhiều thread, phải cẩn thận viết code thao tác shared data thread-safe. Có thể dùng Mutex khóa shared data structures trước khi thao tác. Điều này đảm bảo thread khác không dựa trên data cũ trong khi bạn đang thay đổi.

Tóm tắt Multi-threaded
Use caseLựa chọn "middle of the road". Dùng cho nhiều web app tiêu chuẩn cần xử lý nhiều request ngắn (web app bận rộn).
Ưu điểmDùng ít bộ nhớ hơn multi-process.
Nhược điểmPhải đảm bảo code thread-safe.
Nếu thread gây crash, có thể sập cả process.
GIL khóa mọi operation trừ I/O.

Event-loop (Thin)

Event-loops dùng khi cần nhiều concurrent I/O operations. Mô hình không ép nhiều request chạy cùng lúc, nhưng là cách hiệu quả xử lý nhiều concurrent users.

Dưới đây event loop đơn giản bằng Ruby. Loop lấy event từ event_queue xử lý. Nếu không có event, sleep và lặp lại xem có event mới không.

loop do if event_queue.any? handle_event(event_queue.pop) else sleep 0.1 end end

Phiên bản minh họa

Trong minh họa này, tiến xa hơn. Event loop khiêu vũ với OS, queue và bộ nhớ.

Bước theo bước

  1. OS theo dõi sẵn sàng network và disk.
  2. Khi OS thấy I/O sẵn sàng, gửi event đến queue.
  3. Queue là danh sách event mà event loop lấy cái đầu tiên.
  4. Event loop xử lý event.
  5. Dùng một chút bộ nhớ lưu metadata về connections.
  6. Có thể gửi event mới trực tiếp vào event queue. Ví dụ, message tắt queue dựa trên nội dung event.
  7. Nếu muốn làm I/O operation, báo OS quan tâm I/O cụ thể. OS theo dõi network/disk (xem [1]) và thêm event khi I/O sẵn sàng.
Tóm tắt Event-loop
Use caseKhi dùng nhiều concurrent connections đến users. Nghĩ đến Slack. Chrome notifications.
Ưu điểmGần như không overhead bộ nhớ mỗi connection.
Scale đến số lượng kết nối song song khổng lồ.
Nhược điểmMô hình tư duy khó hiểu.
Batch sizes phải nhỏ và dự đoán được để tránh queue tích tụ.

Nên dùng cái nào?

Hy vọng bài viết giúp bạn hiểu rõ hơn các mô hình concurrency. Đây là chủ đề khó nắm bắt, nhưng hiểu nó cho bạn công cụ thử nghiệm và dùng setup đúng cho app.

Tóm lại

  • Đa số app threading hợp lý, hệ sinh thái Ruby/Rails dường như (chậm rãi) chuyển hướng này.
  • Nếu chạy app highly concurrent với long-running streams, event-loop cho phép scale.
  • Nếu không phải high traffic, hoặc worker hay break, chọn multi-process truyền thống.

Và, có thể chạy event loop, bên trong thread, bên trong multi-process setup. Vậy là bạn có thể ăn stroopwafel và vẫn còn nó!

Muốn đọc thêm về các mô hình concurrency, xem bài chi tiết về multi-process, multi-threading và event loops.