Buộc ~ IPC Node.js đồng bộ


8

Tôi có một máy chủ Node tạo ra một tiến trình con bằng fork()cách sử dụng IPC. Tại một số thời điểm, đứa trẻ gửi kết quả trở lại cho cha mẹ ở khoảng 10Hz như là một phần của nhiệm vụ dài hạn. Khi tải trọng được chuyển đến process.send()nhỏ, tất cả đều hoạt động tốt: mọi tin nhắn tôi gửi đều được nhận ~ ngay lập tức và được xử lý bởi phụ huynh.

Tuy nhiên, khi tải trọng là 'Lớn', tôi chưa xác định giới hạn kích thước chính xác, thay vì được phụ huynh nhận ngay lập tức, tất cả các tải trọng được gửi trước và chỉ khi trẻ hoàn thành nhiệm vụ dài hạn thì cha mẹ mới nhận được và xử lý các tin nhắn.

tl; dr trực quan:

Tốt (xảy ra với tải trọng nhỏ):

child:  send()
parent: receive()
child:  send()
parent: receive()
child:  send()
parent: receive()
...

Xấu (xảy ra với tải trọng lớn):

child:  send()
child:  send()
child:  send()
(repeat many times over many seconds)
...
parent: receive()
parent: receive()
parent: receive()
parent: receive()
...
  1. Đây có phải là một lỗi? (Chỉnh sửa: hành vi chỉ xảy ra trên OS X, không phải Windows hoặc Linux)
  2. Có cách nào để tránh điều này, ngoài việc cố gắng giữ cho tải trọng IPC của tôi nhỏ không?

Chỉnh sửa 2 : mã mẫu bên dưới sử dụng cả bộ đếm thời gian và bộ lặp để chọn thời điểm gửi bản cập nhật. (Trong mã thực tế của tôi nó cũng có thể gửi một bản cập nhật sau n lần lặp lại, hoặc sau khi vòng lặp đạt được kết quả nhất định.) Là một viết lại ví dụ của mã để sử dụng setInterval/ setTimeoutthay vì một vòng lặp là một phương sách cuối cùng đối với tôi, vì nó đòi hỏi tôi để loại bỏ các tính năng.

Chỉnh sửa : Đây là mã kiểm tra tái tạo vấn đề. Tuy nhiên, nó chỉ sao chép trên OS X, không phải trên Windows hoặc Linux:

server.js

const opts = {stdio:['inherit', 'inherit', 'inherit', 'ipc']};
const child = require('child_process').fork('worker.js', [], opts);

child.on('message', msg => console.log(`parent: receive() ${msg.data.length} bytes`, Date.now()));

require('http').createServer((req, res) => {
   console.log(req.url);
   const match = /\d+/.exec(req.url);
   if (match) {
      child.send(match[0]*1);
      res.writeHead(200, {'Content-Type':'text/plain'});
      res.end(`Sending packets of size ${match[0]}`);
   } else {
      res.writeHead(404, {'Content-Type':'text/plain'});
      res.end('what?');
   }
}).listen(8080);

worker.js

if (process.send) process.on('message', msg => run(msg));

function run(messageSize) {
   const msg = new Array(messageSize+1).join('x');
   let lastUpdate = Date.now();
   for (let i=0; i<1e7; ++i) {
      const now = Date.now();
      if ((now-lastUpdate)>200 || i%5000==0) {
         console.log(`worker: send()  > ${messageSize} bytes`, now);
         process.send({action:'update', data:msg});
         lastUpdate = Date.now();
      }
      Math.sqrt(Math.random());
   }
   console.log('worker done');
}

Khoảng 8k vấn đề xảy ra. Ví dụ: khi truy vấn http://localhost:8080/15vs.http://localhost:8080/123456

/15
worker: send()  > 15 bytes 1571324249029
parent: receive() 15 bytes 1571324249034
worker: send()  > 15 bytes 1571324249235
parent: receive() 15 bytes 1571324249235
worker: send()  > 15 bytes 1571324249436
parent: receive() 15 bytes 1571324249436
worker done
/123456
worker: send()  > 123456 bytes 1571324276973
worker: send()  > 123456 bytes 1571324277174
worker: send()  > 123456 bytes 1571324277375
child done
parent: receive() 123456 bytes 1571324277391
parent: receive() 123456 bytes 1571324277391
parent: receive() 123456 bytes 1571324277393

Có kinh nghiệm trên cả Node v12.7 và v12.12.


1
Thay vì xếp hàng các tin nhắn trong một vòng lặp chặn, tại sao không sử dụng một setInterval()?
Patrick Roberts

@PatrickRoberts Bạn đang đặt câu hỏi tại sao run()có một whilevòng lặp trong đó? Bạn có gợi ý rằng việc chuyển đổi nó setInterval()sẽ giải quyết vấn đề của tôi? Để trả lời câu hỏi tôi nghĩ bạn đang hỏi: Tôi sử dụng một whilevòng lặp vì chức năng đó là mục đích duy nhất của quy trình nhân viên này và (với tải trọng IPC nhỏ) nó không gây ra bất kỳ vấn đề nào tôi có thể thấy.
Phrogz

1
Chặn như thế không phục vụ mục đích có lợi. Sử dụng cơ chế thời gian không chặn như setInterval()giải phóng vòng lặp sự kiện để thực hiện I / O trong nền. Tôi không nói rằng nó chắc chắn sẽ giải quyết vấn đề này, nhưng có vẻ như một lựa chọn kỳ quặc để viết nó theo cách bạn có, chỉ vì bạn có thể.
Patrick Roberts

@PatrickRoberts Cảm ơn bạn đã nhập. Tôi đã không viết nó theo cách "chỉ vì tôi có thể", mà là vì ban đầu mã được dựa trên bảng điều khiển không có IPC. Một vòng lặp while định kỳ in ra kết quả có vẻ hợp lý tại thời điểm đó, nhưng đang gặp vấn đề này (chỉ trên macOS).
Phrogz

Viết một vòng lặp chặn thăm dò thời gian hiện tại cho đến khi điều kiện dựa trên thời gian được đáp ứng là một phản mẫu trong JavaScript, giai đoạn. Không quan trọng nếu nó có IPC trước hay không. Luôn thích cách tiếp cận không chặn bằng cách sử dụng setTimeout()hoặc setInterval(). Sự thay đổi ở đây là tầm thường.
Patrick Roberts

Câu trả lời:


3

Việc lưu một vòng lặp chạy dài và chặn trong khi kết hợp với các socket hoặc bộ mô tả tệp trong nút luôn là một dấu hiệu cho thấy có gì đó không đúng.

Không thể kiểm tra toàn bộ thiết lập, thật khó để biết liệu yêu cầu của tôi có thực sự chính xác hay không, nhưng các tin nhắn ngắn có thể được chuyển trực tiếp trong một đoạn tới HĐH, sau đó chuyển nó sang quy trình khác. Với các nút tin nhắn lớn hơn sẽ cần đợi cho đến khi HĐH có thể nhận được nhiều dữ liệu hơn, do đó việc gửi được xếp hàng và khi bạn chặn while, việc gửi sẽ được xếp hàng cho đến khi loopkết thúc.

Vì vậy, với câu hỏi của bạn, đó không phải là một lỗi.

Khi bạn sử dụng một phiên bản nodejs gần đây, tôi sẽ sử dụng một awaitasyncthay vì và tạo một không chặn while tương tự như sleeptrong câu trả lời này . Các awaitsẽ cho phép vòng lặp sự kiện nút để đánh chặn nếu processSomelợi nhuận cấp phát Promise.

Đối với mã của bạn không thực sự phản ánh trường hợp sử dụng thực sự, thật khó để nói làm thế nào để giải quyết nó một cách chính xác. Nếu bạn không làm bất cứ điều gì không đồng bộ trong processSomeđó sẽ cho phép I / O chặn thì bạn cần thực hiện thủ công một cách thường xuyên, ví dụ như a await new Promise(setImmediate);.

async function run() {
  let interval = setInterval(() => {
    process.send({action:'update', data:status()});
    console.log('child:  send()');
  }, 1/10)

  while(keepGoing()) {
    await processSome();
  }

  clearInterval(interval)
}

Cảm ơn bạn cho câu trả lời này. Theo chỉnh sửa của tôi cho câu hỏi, mã thực sự của tôi có nhiều điều kiện để gửi một bản cập nhật, chỉ một trong số đó dựa trên thời gian. Có vẻ như bạn đã di chuyển processSome()mã ra khỏi whilevòng lặp. (Hoặc có lẽ tôi đang thiếu một cái gì đó quan trọng liên quan đến lời hứa.)
Phrogz

1
@Phrogz ah ok không, tôi vô tình đọc nhầm niềng răng. Tôi đã cập nhật câu trả lời để process.send({action:'update', data:status()});được giải thích khi nào every10Hzlà đúng và processSomecho mỗi lần lặp lại của while. Việc awaitnày sẽ cho phép nút EvenLoop của nút chặn ngay cả khi processSomekhông trả về Promise. Nhưng lý do cho vấn đề của bạn vẫn là vòng lặp đang chặn.
t.niese

Hai ý kiến ​​về câu trả lời này là như vậy. Nếu processSome()không trả lại lời hứa, thì cách tiếp cận này vẫn chặn I / O (microt Nhiệm vụ như tiếp tục được tạo bởi awaittuyên bố này được xử lý trước IO). Ngoài ra, điều này sẽ khiến phép lặp thực hiện chậm hơn rất nhiều vì microtask được xếp hàng ở mỗi lần lặp.
Patrick Roberts

@PatrickRoberts có bạn đúng, nó phải trả lại một Lời hứa chưa được giải quyết.
t.niese

2

Về câu hỏi đầu tiên của bạn

Đây có phải là một lỗi? (Chỉnh sửa: hành vi chỉ xảy ra trên OS X, không phải Windows hoặc Linux)

Đây chắc chắn không phải là lỗi và tôi có thể sao chép nó trên windows 10 của mình (với kích thước 123456). Phần lớn là do bộ đệm nhân bên dưới và chuyển đổi ngữ cảnh của HĐH, vì hai quy trình riêng biệt (không tách rời) đang giao tiếp qua một bộ mô tả ipc.

Về câu hỏi thứ hai của bạn

Có cách nào để tránh điều này, ngoài việc cố gắng giữ cho tải trọng IPC của tôi nhỏ không?

Nếu tôi hiểu chính xác vấn đề, bạn đang cố gắng giải quyết, đối với mỗi yêu cầu http, mỗi khi nhân viên gửi một đoạn trở lại máy chủ, bạn muốn máy chủ xử lý nó trước khi bạn nhận được đoạn tiếp theo. Đó là cách tôi hiểu khi bạn nói xử lý đồng bộ hóa

Có một cách sử dụng lời hứa, nhưng tôi muốn sử dụng máy phát điện trong công nhân. Tốt hơn là phối hợp dòng chảy trên máy chủ và công nhân

Lưu lượng:

  1. Máy chủ gửi một số nguyên cho nhân viên bất cứ điều gì nó nhận được từ yêu cầu http
  2. Công nhân sau đó tạo và chạy trình tạo để gửi đoạn đầu tiên
  3. Công nhân năng suất sau khi gửi chunk
  4. Yêu cầu máy chủ để biết thêm
  5. Công nhân tạo ra nhiều hơn kể từ khi máy chủ yêu cầu nhiều hơn (chỉ khi có sẵn)
  6. Nếu không còn nữa, công nhân sẽ gửi phần cuối của khối
  7. Máy chủ chỉ ghi nhật ký mà nhân viên đã hoàn thành và không yêu cầu nữa

server.js

const opts = {stdio:['inherit', 'inherit', 'inherit', 'ipc'], detached:false};
const child = require('child_process').fork('worker.js', [], opts);

child.on('message', (msg) => {
   //FLOW 7: Worker is done, just log
   if (msg.action == 'end'){
      console.log(`child ended for a particular request`)
   } else {
      console.log(`parent: receive(${msg.data.iter}) ${msg.data.msg.length} bytes`, Date.now())
      //FLOW 4: Server requests for more
      child.send('more')
   }   

});

require('http').createServer((req, res) => {
   console.log(req.url);
   const match = /\d+/.exec(req.url);   
   if (match) {
      //FLOW 1: Server sends integer to worker
      child.send(match[0]*1);
      res.writeHead(200, {'Content-Type':'text/plain'});
      res.end(`Sending packets of size ${match[0]}`);
   } else {
      res.writeHead(404, {'Content-Type':'text/plain'});
      res.end('what?');
   }
}).listen(8080);

worker.js

let runner
if (process.send) process.on('message', msg => {   
   //FLOW 2: Worker creates and runs a generator to send the first chunk
   if (parseInt(msg)) {
      runner = run(msg)
      runner.next()
   }
   //FLOW 5: Server asked more, so generate more chunks if available
   if (msg == "more") runner.next()

});

//generator function *
function* run(messageSize) {
   const msg = new Array(messageSize+1).join('x');
   let lastUpdate = Date.now();
   for (let i=0; i<1e7; ++i) {
      const now = Date.now();
      if ((now-lastUpdate)>200 || i%5000==0) {
         console.log(`worker: send(${i})  > ${messageSize} bytes`, now);
         let j = i         
         process.send({action:'update', data:{msg, iter:j}});
         //FLOW 3: Worker yields after sending the chunk
         yield
         lastUpdate = Date.now();
      }
      Math.sqrt(Math.random());
   }
   //FLOW 6: If no more, worker sends end signal
   process.send({action:'end'});
   console.log('worker done');
}

Nếu chúng ta biết trường hợp sử dụng chính xác, có thể có những cách tốt hơn để lập trình nó. Đây chỉ là một cách để đồng bộ hóa tiến trình con giữ lại phần lớn mã nguồn ban đầu của bạn.


1

Nếu bạn cần đảm bảo rằng một tin nhắn được nhận trước khi gửi tin nhắn tiếp theo, bạn có thể đợi chủ nhân xác nhận đã nhận. Điều này sẽ trì hoãn việc gửi tin nhắn tiếp theo tất nhiên, nhưng vì logic của bạn phụ thuộc vào cả số lần lặp & số lần lặp để xác định xem có gửi tin nhắn hay không nên có thể phù hợp với trường hợp của bạn.

Việc thực hiện sẽ cần mỗi công nhân tạo một lời hứa cho mỗi tin nhắn được gửi và chờ phản hồi từ chủ trước khi giải quyết lời hứa. Điều này cũng có nghĩa là bạn cần xác định tin nhắn nào được xác nhận dựa trên id tin nhắn hoặc một cái gì đó duy nhất nếu bạn có nhiều hơn một tin nhắn hoặc nhân viên cùng một lúc.

đây là mã sửa đổi

server.js

const opts = {stdio:['inherit', 'inherit', 'inherit', 'ipc']};
const child = require('child_process').fork('worker.js', [], opts);

child.on('message', msg =>  {
    console.log(`parent: receive() ${msg.data.length} bytes`, Date.now())
    // reply to the child with the id
    child.send({ type: 'acknowledge', id: msg.id });
});

...

worker.js

const pendingMessageResolves = {};

if (process.send) process.on('message', msg => { 
    if (msg.type === 'acknowledge') {
        // call the stored resolve function
        pendingMessageResolves[msg.id]();
        // remove the function to allow the memory to be freed
        delete pendingMessageResolves[msg.id]
    } else {
        run(msg) 
    }
});

const sendMessageAndWaitForAcknowledge = (msg) => new Promise(resolve => {
    const id = new uuid(); // or any unique field
    process.send({ action:'update', data: msg, id });
    // store a reference to the resolve function
    pendingMessageResolves[id] = resolve;
})

async function run(messageSize) {
    const msg = new Array(messageSize+1).join('x');
    let lastUpdate = Date.now();
    for (let i=0; i<1e7; ++i) {
        const now = Date.now();
        if ((now-lastUpdate)>200 || i%5000==0) {
            console.log(`worker: send()  > ${messageSize} bytes`, now);
            await sendMessageAndWaitForAcknowledge(msg); // wait until master replies
            lastUpdate = Date.now();
        }
        Math.sqrt(Math.random());
    }
    console.log('worker done');
}

ps Tôi đã không kiểm tra mã để nó có thể cần một số điều chỉnh, nhưng ý tưởng nên giữ.


1

Mặc dù tôi đồng ý với những người khác rằng giải pháp tối ưu sẽ là một trong đó quy trình con có thể tự nguyện từ bỏ quyền kiểm soát ở cuối mỗi vòng lặp, cho phép các quy trình xả bộ đệm chạy, có một cách khắc phục dễ / nhanh / bẩn giúp bạn gần như đồng bộ hành vi, và đó là để làm cho đứa trẻ sendgọi chặn.

Sử dụng giống server.jsnhư trước và gần như giống nhau worker.js, chỉ với một dòng được thêm vào:

worker.js

if (process.send) process.on('message', msg => run(msg));

// cause process.send to block until the message is actually sent                                                                                
process.channel.setBlocking(true);

function run(messageSize) {
   const msg = new Array(messageSize+1).join('x');
   let lastUpdate = Date.now();
   for (let i=0; i<1e6; ++i) {
      const now = Date.now();
      if ((now-lastUpdate)>200 || i%5000==0) {
         console.error(`worker: send()  > ${messageSize} bytes`, now);
         process.send({action:'update', data:msg});
         lastUpdate = Date.now();
      }
      Math.sqrt(Math.random());
   }
   console.log('worker done');
}

Đầu ra:

/123456
worker: send()  > 123456 bytes 1572113820591
worker: send()  > 123456 bytes 1572113820630
parent: receive() 123456 bytes 1572113820629
parent: receive() 123456 bytes 1572113820647
worker: send()  > 123456 bytes 1572113820659
parent: receive() 123456 bytes 1572113820665
worker: send()  > 123456 bytes 1572113820668
parent: receive() 123456 bytes 1572113820678
worker: send()  > 123456 bytes 1572113820678
parent: receive() 123456 bytes 1572113820683
worker: send()  > 123456 bytes 1572113820683
parent: receive() 123456 bytes 1572113820687
worker: send()  > 123456 bytes 1572113820687
worker: send()  > 123456 bytes 1572113820692
parent: receive() 123456 bytes 1572113820692
parent: receive() 123456 bytes 1572113820696
worker: send()  > 123456 bytes 1572113820696
parent: receive() 123456 bytes 1572113820700
worker: send()  > 123456 bytes 1572113820700
parent: receive() 123456 bytes 1572113820703
worker: send()  > 123456 bytes 1572113820703
parent: receive() 123456 bytes 1572113820706
worker: send()  > 123456 bytes 1572113820706
parent: receive() 123456 bytes 1572113820709
worker: send()  > 123456 bytes 1572113820709
parent: receive() 123456 bytes 1572113820713
worker: send()  > 123456 bytes 1572113820714
worker: send()  > 123456 bytes 1572113820721
parent: receive() 123456 bytes 1572113820722
parent: receive() 123456 bytes 1572113820725
worker: send()  > 123456 bytes 1572113820725
parent: receive() 123456 bytes 1572113820727

Xác định câu lệnh chặn trực tiếp trong mã nguồn là một ý tưởng thông minh. Nó sẽ tạo ra một nút cổ chai không thể sửa được. Lý do là mã nguồn được lưu trữ vào ổ cứng khiến việc sử dụng một công cụ quy tắc để thay đổi hành vi một cách khó khăn.
Manuel Rodriguez
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.