设计
topic和channel
单个nsqd
实例旨在一次处理多个数据流。流称为“主题”,一个主题有 1 个或多个“通道”。每个通道都会收到一个主题的所有消息的_副本_。
主题和通道道_不是_提前配置的。主题是在首次使用时通过发布到指定主题或订阅指定主题的通道来创建的。频道是通过订阅指定频道在首次使用时创建的。
主题和通道都相互独立地缓冲数据,防止缓慢的消费者导致其他通道的积压(同样适用于主题级别)。
一个通道可以并且通常确实连接了多个客户端。假设所有连接的客户端都处于准备接收消息的状态,则每条消息将被传递给随机客户端。
消息是从主题 -> 通道多播的(每个频道都接收该主题的所有消息的副本),但从通道 -> 消费者均匀分布(每个消费者接收该频道的部分消息)。
消息传递保证
NSQ保证一条消息至少被传递一次,尽管重复消息是可能的。消费者应该预料到这一点并进行重复数据删除或执行幂等操作。
工作方式(假设客户端已成功连接并订阅了主题):
- 客户端表示他们已准备好接收消息
- NSQ发送消息并将数据临时存储在本地(在重新排队或超时的情况下)
- 客户端回复 FIN(完成)或 REQ(重新排队)分别指示成功或失败。如果客户端没有回复NSQ将在可配置的持续时间后超时并自动重新排队消息)
这确保了唯一会导致消息丢失的边缘情况是 nsqd
进程的不正常关闭。在这种情况下,内存中的任何消息(或任何未刷新到磁盘的缓冲写入)都将丢失。
完全解决方案是:建立冗余nsqd
对(在不同的主机上)接收相同部分消息的副本。因为您已将消费者编写为幂等,所以对这些消息执行双重时间不会对下游产生影响,并且允许系统承受任何单节点故障而不会丢失消息。
消息持久化
nsqd
提供了一个配置选项--mem-queue-size
,用于确定队列在内存中保留的消息数量。如果队列的深度超过此阈值,消息将透明地写入磁盘。这将给定进程的内存占用限制为 :nsqd mem-queue-size * #_of_channels_and_topics
通过将此值设置为较低的值(如 1 甚至 0),这是一种获得更高交付保证的便捷方法。磁盘支持的队列被设计为在不干净的重启后仍然存在(尽管消息可能会被传递两次)。
此外,与消息传递保证相关,干净关闭(通过向nsqd
进程发送 TERM 信号)能够安全地将当前在内存中、运行中、延迟和各种内部缓冲区中的消息持久保存。
请注意,名称以字符串结尾的主题/频道#ephemeral
的消息不会缓冲到磁盘,而是会在传递mem-queue-size
. 这使得不需要消息保证的消费者能够订阅频道。这些临时通道也将在其最后一个客户端断开连接后消失。对于一个临时主题,这意味着至少有一个频道被创建、消费和删除(通常是一个临时频道)。
消息传输
NSQ旨在通过“类似 memcached”的命令协议与简单的大小前缀响应进行通信。所有消息数据都保存在核心中,包括尝试次数、时间戳等元数据。这消除了从服务器到客户端来回复制数据,这是重新排队消息时先前工具链的固有属性。这也简化了客户端,因为它们不再需要负责维护消息状态。
对于数据协议,我们做出了一个关键的设计决策,即通过将数据推送到客户端而不是等待它拉取来最大化性能和吞吐量。这个概念,我们称之为RDY
状态,本质上是客户端流控制的一种形式。
当客户端连接nsqd
并订阅通道时,它的RDY
状态为 0。这意味着不会向客户端发送任何消息。当客户端准备好接收消息时,它会发送一条命令,将其RDY
状态更新为它准备处理的某个 #,比如 100。如果没有任何其他命令,100 条消息将在可用时推送到客户端(每次递减该客户端的服务器端 RDY 计数)。
客户端库旨在在达到可配置设置的约 25% 时发送更新RDY
计数的命令max-in-flight
(并适当考虑到多个nsqd
实例的连接,适当划分)。
值得注意的是,因为消息既是缓冲的又是基于推送的,能够满足对流(通道)的独立副本的需求,我们制作了一个行为类似于simplequeue
和pubsub
_组合_的守护进程。
nsqd
nsqd
是接收、排队和传递消息给客户端的守护进程。
它可以独立运行,但通常配置在带有nsqlookupd
实例的集群中(在这种情况下,它将宣布主题和发现通道)。
它监听两个 TCP 端口,一个用于客户端,另一个用于 HTTP API。它可以选择在第三个端口上侦听 HTTPS。
nsqd监听两个端口:
4151 HTTP Producer使用HTTP协议的curl等工具生产数据;Consumer使用HTTP协议的curl等工具消费数据;
4150 TCP Producer使用TCP协议的nsq-j等工具生产数据;Consumer使用TCP协议的nsq-j等工具消费数据;
nsqlookupd
nsqlookupd
是管理拓扑信息的守护进程。客户端查询nsqlookupd
以发现 nsqd
特定主题的生产者,nsqd
节点广播主题和频道信息。
有两个端口:用于nsqd
广播的 TCP 端口和用于客户端执行发现和管理操作的 HTTP 接口。
nsqlookupd 监听两个端口:
4160 TCP 用于接收nsqd的广播,记录nsqd的地址以及监听TCP/HTTP端口等。
4161 HTTP 用于接收客户端发送的管理和发现操作请求(增删话题,节点等管理查看性操作等)。当Consumer进行连接时,返回对应存在Topic的nsqd列表。
nsqadmin
nsqadmin监听一个端口
4171 HTTP 用于管理页面
代码分析
/ping
使用了装饰器模式
// http请求的各个handler,从这里创建请求处理的handler------------nsqd.go Main()
httpServer := newHTTPServer(n, false, n.getOpts().TLSRequired == TLSRequired)
router.Handle("GET", "/ping", http_api.Decorate(s.pingHandler, log, http_api.PlainText))
...
// 很多handler
//http_api.Decorate看一下这个方法:
func Decorate(f APIHandler, ds ...Decorator) httprouter.Handle {
decorated := f
for _, decorate := range ds {
decorated = decorate(decorated)
}
return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
decorated(w, req, ps)
}
}
//s.pingHandler
func (s *httpServer) pingHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
health := s.nsqd.GetHealth()
if !s.nsqd.IsHealthy() {
return nil, http_api.Err{500, health}
}
return health, nil
}
//log
func Log(logf lg.AppLogFunc) Decorator {
return func(f APIHandler) APIHandler {
return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
start := time.Now()
response, err := f(w, req, ps)
elapsed := time.Since(start)
status := 20