Đừng chặn vòng lặp sự kiện (Don't block the Event Loop) - Phần 2

Đừng chặn vòng lặp sự kiện (Don't block the Event Loop) - Phần 2

Những mẩu tin ngắn hàng ngày dành cho bạn
  • Đây! Một vấn đề mà từ xưa đến nay mình cứ thắc mắc mãi, và cho đến hôm qua thì mọi thứ đã sáng tỏ.

    Bình thường mọi người dùng height: 100vh để đặt chiều cao bằng với viewport của màn hình. Trên máy tính thì không vấn đề gì, thậm chí giả lập kích thước của điện thoại thông minh thì mọi thứ vẩn ổn. Nhưng khi mở trên điện thoại thì height 100vh lúc nào cũng vượt quá viewport. Ủa!? Là sao???

    Lý giải cho điều này là do trên thiết bị di động có cách tính viewport khác với máy tính. Nó thường bị can thiệp hay ảnh hưởng bởi thanh địa chỉ, thanh điều hướng của nền tảng mà bạn đang sử dụng. Vậy nên nếu muốn 100vh trên di động đúng bằng viewport thì cần phải làm thêm một bước thiết lập lại viewport.

    Dễ lắm, đầu tiên cần tạo một css variable --vh ở ngay thẻ script đầu trang.

    function updateViewportHeight() { const viewportHeight = globalThis.innerHeight; document.documentElement.style.setProperty('--vh', `${viewportHeight * 0.01}px`); } updateViewportHeight(); window.addEventListener('resize', updateViewportHeight);

    Sau đó thay vì dùng height: 100vh thì chuyển thành height: calc(var(--vh, 1vh) * 100). Thế là xong.

    » Xem thêm
  • Cả ngày hôm nay mình dành thời gian để làm giao diện tiếp thị cho gói hội viên của 2coffee.dev. Vậy là cuối cùng thì cũng chính thức đi vào vào con đường mà 5 năm trước cũng không ngờ đến được: "Bán một cái gì đó". Người ta thường nói "Cho đi để nhận lại", bên cạnh đó cũng có câu "Nếu giỏi một cái gì đó, đừng làm nó miễn phí". Nếu theo dõi đủ lâu, bạn đọc sẽ thấy chẳng có gì mình giấu giếm. Biết gì viết nấy, và đôi khi nhờ viết ra mà nhận lại được sự góp ý của độc giả. Từ đó giúp mình hoàn thiện bản thân nhiều hơn.

    Membership là tính năng mà mình sắp sửa giới thiệu. Trở thành hội viên của blog, bạn sẽ có một số đặc quyền nhất định, ví dụ như truy cập vào các bài viết chỉ dành riêng cho hội viên. Các bài viết này về các chủ đề chuyên sâu và được hệ thống hoá sao cho dễ đọc và dễ nắm bắt nhất. Qua đó cung cấp thêm nhiều kiến thức và trau dồi kỹ năng cho bạn đọc.

    Để đạt được đến ngày hôm nay là công rất lớn của các bạn đọc giả, của những người yêu mến 2coffee.dev. Nhờ các bạn mà blog mới có ngày hôm nay. Bên cạnh đó, bản thân mình cũng phải thay đổi liên tục, phải vượt ra khỏi vùng an toàn, làm những điều mà trước nay không dám. Dù sao đi nữa thì đây cũng mới là khởi đầu cho mọi sự gian nan. Nhưng đừng bao giờ nản nha các bạn ơi 😄

    » Xem thêm
  • Ngày nay, 1 triệu (1M) tác vụ đồng thời sẽ tiêu tốn bao nhiêu bộ nhớ? Đó là câu hỏi của hez2010 và anh đã quyết định đi tìm câu trả lời, bằng cách thử nghiệm một chương trình đơn giản trên nhiều ngôn ngữ lập trình khác nhau: How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?

    Tóm tắt lại thì Rust vẫn vô đối, nhưng vị trí thứ 2 mới làm tôi cảm thấy ngạc nhiên 😳

    » Xem thêm

Đừng bao giờ chặn Event Loop

Tất cả các yêu cầu đến cho đến lúc nhận được phản hồi đều đi qua Event Loop. Điều này có nghĩa là nếu Event Loop dành thời gian quá lâu tại bất kỳ thời điểm nào thì tất cả yêu cầu hiện tại và yêu cầu mới sẽ không được xử lý.

Chúng ta nên đảm bảo rằng sẽ không bao giờ chặn Event Loop. Nói cách khác, mỗi hàm callbacks hoàn tất càng nhanh càng tốt. Điều này cũng áp dụng cho await, Promise.then, v.v...

Một cách tốt để đảm bảo điều này là xem xét về "độ phức tạp thuật toán" của các hàm callbacks của bạn. Nếu hàm callback xử lý không quan tâm đến số lượng đầu vào thì chúng ta sẽ đảm bảo được sự "công bằng" cho mỗi yêu cầu. Nếu callbacks thực hiện thực hiện các bước xử lý khác nhau tùy thuộc vào đối số của nó, thì chúng ta nên quan tâm trường hợp xấu nhất là mất bao nhiêu thời gian.

Ví dụ một yêu cầu không quan tâm đầu vào:

app.get('/constant-time', (req, res) => {
  res.sendStatus(200);
});

Còn đây là một yêu cầu mà thời gian xử lý phụ thuộc vào tham số đầu vào.

app.get('/countToN', (req, res) => {
  const n = req.query.n;
  for (let i = 0; i < n; i++) {
    // thực hiện một điều gì đó mỗi lần lặp
  }
  res.sendStatus(200);
});

Node.js sử dụng V8 Engine, công cụ này khá nhanh cho nhiều hoạt động phổ biến. Tuy nhiên, nó cũng có một số trường hợp ngoại lệ đó là làm việc với các biểu thức regexps hoặc JSON.

REDOS: Tấn công từ chối dịch vụ bằng biểu thức regexp

Một cách phổ biến để khiến Event Loop bị chặn là sử dụng một biểu thức regexp "dễ bị tổn thương". Vì thế chúng ta nên tránh việc sử dụng các biểu thức regex dễ bị tổn thương.

Hiểu đơn giản rằng đôi khi chúng ta cần sử dụng regexp để xác định hay tìm kiếm một chuỗi kí tự nào đó. Thật không may trong một số trường hợp, việc kết hợp các chuỗi regexp có thể mất một cơ số thời gian theo cấp số nhân tuỳ thuộc vào chuỗi đầu vào.

Một biểu thức regexp dễ bị tổn thương là một biểu thức regexp có thể mất thời gian theo cấp số nhân, và điều này dẫn đến REDOS. Việc xác định các biểu thức regexp có thực sự mất nhiều thời gian theo cấp số nhân hay không là một câu hỏi khó trả lời, và nó tùy thuộc vào việc bạn đang sử dụng Perl, Python, Ruby, Java, JavaScript v.v... nhưng đây là một số quy tắc áp dụng trên tất cả các ngôn ngữ này:

  • Tránh các định lượng lồng nhau như. Động cơ regexp của V8 có thể xử lý một số trong những một cách nhanh chóng, nhưng những người khác là dễ bị tổn thương.(a+)*
  • Tránh OR với các mệnh đề chồng chéo. Một lần nữa, đây là những đôi khi nhanh chóng.(a|a)*
  • Tránh sử dụng backreferences. Không có động cơ regexp có thể đảm bảo đánh giá những trong thời gian tuyến tính.(a.*) \1
  • Nếu bạn đang thực hiện đối sánh chuỗi đơn giản, hãy sử dụng hoặc tương đương cục bộ. Nó sẽ rẻ hơn và sẽ không bao giờ mất nhiều hơn .indexOf O(n)

Nếu bạn không chắc chắn liệu biểu thức chính quy của mình có dễ bị tổn thương hay không, hãy nhớ rằng Node.js thường không gặp sự cố khi báo cáo kết quả trùng khớp ngay cả đối với regexp dễ bị tổn thương và chuỗi đầu vào dài. Hành vi hàm mũ được kích hoạt khi có một không phù hợp nhưng Node.js không thể chắc chắn cho đến khi nó cố gắng nhiều đường dẫn thông qua chuỗi đầu vào.

Có một số công cụ để kiểm tra độ an toàn của biểu thức regexp:

Tuy nhiên, chúng không hẳn sẽ bắt được tất cả các regexps dễ bị tổn thương.

Một cách tiếp cận khác là sử dụng một công cụ regexp khác nhau. Bạn có thể sử dụng mô-đun node-re2, sử dụng công cụ regexp RE2 nhanh chóng của Google. Nhưng được cảnh báo, RE2 không phải là 100% tương thích với regexps của V8, do đó, kiểm tra hồi quy nếu bạn trao đổi trong mô-đun nút-re2 để xử lý regexps của bạn. Và regexps đặc biệt phức tạp không được hỗ trợ bởi node-re2.

Core modules tiêu tốn nhiều thời gian

Một số modules lõi của Node.js có các API đồng bộ "có chi phí đắt", bao gồm:

  • Encryption
  • Compression
  • File System
  • Child Process

Các API này rất tốn kém, bởi vì chúng liên quan đến tính toán đáng kể (mã hóa, nén), yêu cầu I/O (tệp I/O) hoặc có khả năng cả hai (quá trình con). Các API này được thiết kế để tạo kịch bản thuận tiện, nhưng không dành cho việc sử dụng trong ngữ cảnh máy chủ. Nếu bạn thực thi chúng trên Vòng lặp Sự kiện, chúng sẽ mất nhiều thời gian hơn để hoàn thành so với hướng dẫn JavaScript thông thường, chặn Vòng lặp Sự kiện.

Trong một máy chủ, bạn không nên sử dụng các API đồng bộ sau đây từ các mô-đun này:

Encryption:

  • crypto.randomBytes (sync)
  • crypto.randomFillSync (sync)
  • crypto.pbkdf2Sync (sync)

Bạn cũng nên cẩn thận về việc cung cấp đầu vào lớn cho các thói quen mã hóa và giải mã.

Compression:

  • zlib.inflateSync
  • zlib.deflateSync

File System:

Không sử dụng API hệ thống tệp đồng bộ. Ví dụ, nếu các tập tin bạn truy cập là trong một hệ thống tập tin phân phối như NFS, thời gian truy cập có thể khác nhau rất nhiều.

Child Process:

  • child_process.spawnSync
  • child_process.execSync
  • child_process.execFileSync

JSON DOS

JSON.parse cũng là một hoạt động "có chi phí đắt". Nó phụ thuộc vào độ dài của dữ liệu đầu vào, cho nên có thể mất nhiều thời gian đáng ngạc nhiên. JSON.stringify cũng vậy, chúng có độ phức tạp lên đến O(n)n.

Nếu máy chủ của bạn thao tác các đối tượng JSON, đặc biệt là xử lý dữ liệu nhận từ một từ một client, bạn nên thận trọng về kích thước của chúng.

Ví dụ: Chúng ta tạo ra một đối tượng chuỗi có kích thước 2^21, và sau đó JSON.parse nó. Chuỗi có kích thước là 50MB. Phải mất 0.7 giây để stringify các đối tượng, 0.03 giây để indexOf, và 1.3 giây để parse chuỗi.

var obj = { a: 1 };
var niter = 20;

var before, str, pos, res, took;

for (var i = 0; i < niter; i++) {
  obj = { obj1: obj, obj2: obj };
}

before = process.hrtime();
str = JSON.stringify(obj);
took = process.hrtime(before);
console.log('JSON.stringify took ' + took);

before = process.hrtime();
pos = str.indexOf('nomatch');
took = process.hrtime(before);
console.log('Pure indexof took ' + took);

before = process.hrtime();
res = JSON.parse(str);
took = process.hrtime(before);
console.log('JSON.parse took ' + took);

Để khắc phục tình trạng này, có một số module trên npm cung cấp các API JSON bất đồng bộ như:

  • JSONStream.
  • Big-Friendly JSON, có API stream cũng như các phiên bản bất đồng bộ của các API JSON tiêu chuẩn bằng cách sử dụng mô hình partitioning-on-the-Event-Loop.

Tổng kết

Bài viết trên đây đã đưa ra một số hành vi tưởng chừng như đơn giản nhưng lại gây ảnh hưởng rất lớn đến Event Loop. Ở bài viết sau chúng ta sẽ cũng nhau tìm hiểu những giải pháp để xử lý việc "chặn" Event Loop.

Cao cấp
Hello

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (0)

Nội dung bình luận...