我有一个node.js应用程序,在任何给定时间运行10k-100k并发setTimeouts。 (它们都是5分钟的持续时间。)回调非常简单,只是redis中的HDECRBY。即使在t2.micro实例上,我还没有遇到任何性能问题。
我知道如果回调函数无法像我设置setTimeouts那样快地执行(显然),我会遇到问题,但是有很多setTimeouts的问题,本身?例如,如果我将其扩展到例如100万并发,我是否会遇到RAM瓶颈? 1000万?
答案 0 :(得分:10)
对于这些类型的问题,了解node.js如何处理source code中的计时器通常很有用。
您将发现node.js保留其自己的内部计时器对象的一个或多个链接列表,并且所有设置为同时发生的计时器共享一个libuv计时器。这意味着在相当具体的时间窗口中设置的数以万计的定时器将不可避免地共享许多触发时间,因此将共享定时器列表,因此将共享许多系统定时器对象。
这使得拥有数以万计的计时器对象成为一个问题。现在,每个计时器对象仍然需要一些内存,并不是计时器实现中的每个操作都是恒定的时间,尽管你可以在下面的评论中看到,他们试图尽可能多地使它们成为允许大量计时器的恒定时间表现还不错。
如果您在定时器触发时不需要绝对精度,则可能会使定时器更频繁地合并并共享定时器对象,方法是仅在特定时间边界(例如偶数100ms)上安排定时器。这将在相同的发射时间内安排更多的数万个计时器,并允许node.js将更多的计时器放入所有共享单个系统计时器的同一列表中。我不知道这对你的计时器是否可行,或者是否需要它,但在研究node.js如何工作时,它会提高效率。 node.js内部的计时器列表更少,libuv中的系统计时器更少。
以下是来自定时器上node.js代码的一些解释性注释,解释了设计的更多方面:
// HOW and WHY the timers implementation works the way it does.
//
// Timers are crucial to Node.js. Internally, any TCP I/O connection creates a
// timer so that we can time out of connections. Additionally, many user
// user libraries and applications also use timers. As such there may be a
// significantly large amount of timeouts scheduled at any given time.
// Therefore, it is very important that the timers implementation is performant
// and efficient.
//
// Note: It is suggested you first read though the lib/internal/linkedlist.js
// linked list implementation, since timers depend on it extensively. It can be
// somewhat counter-intuitive at first, as it is not actually a class. Instead,
// it is a set of helpers that operate on an existing object.
//
// In order to be as performant as possible, the architecture and data
// structures are designed so that they are optimized to handle the following
// use cases as efficiently as possible:
// - Adding a new timer. (insert)
// - Removing an existing timer. (remove)
// - Handling a timer timing out. (timeout)
//
// Whenever possible, the implementation tries to make the complexity of these
// operations as close to constant-time as possible.
// (So that performance is not impacted by the number of scheduled timers.)
//
// Object maps are kept which contain linked lists keyed by their duration in
// milliseconds.
// The linked lists within also have some meta-properties, one of which is a
// TimerWrap C++ handle, which makes the call after the duration to process the
// list it is attached to.
//
//
// ╔════ > Object Map
// ║
// ╠══
// ║ refedLists: { '40': { }, '320': { etc } } (keys of millisecond duration)
// ╚══ ┌─────────┘
// │
// ╔══ │
// ║ TimersList { _idleNext: { }, _idlePrev: (self), _timer: (TimerWrap) }
// ║ ┌────────────────┘
// ║ ╔══ │ ^
// ║ ║ { _idleNext: { }, _idlePrev: { }, _onTimeout: (callback) }
// ║ ║ ┌───────────┘
// ║ ║ │ ^
// ║ ║ { _idleNext: { etc }, _idlePrev: { }, _onTimeout: (callback) }
// ╠══ ╠══
// ║ ║
// ║ ╚════ > Actual JavaScript timeouts
// ║
// ╚════ > Linked List
//
//
// With this, virtually constant-time insertion (append), removal, and timeout
// is possible in the JavaScript layer. Any one list of timers is able to be
// sorted by just appending to it because all timers within share the same
// duration. Therefore, any timer added later will always have been scheduled to
// timeout later, thus only needing to be appended.
// Removal from an object-property linked list is also virtually constant-time
// as can be seen in the lib/internal/linkedlist.js implementation.
// Timeouts only need to process any timers due to currently timeout, which will
// always be at the beginning of the list for reasons stated above. Any timers
// after the first one encountered that does not yet need to timeout will also
// always be due to timeout at a later time.
//
// Less-than constant time operations are thus contained in two places:
// TimerWrap's backing libuv timers implementation (a performant heap-based
// queue), and the object map lookup of a specific list by the duration of
// timers within (or creation of a new list).
// However, these operations combined have shown to be trivial in comparison to
// other alternative timers architectures.
// Object maps containing linked lists of timers, keyed and sorted by their
// duration in milliseconds.
//
// The difference between these two objects is that the former contains timers
// that will keep the process open if they are the only thing left, while the
// latter will not.