嗨,大家好! 我的名字是Sergey Kamardin,我是Mail.Ru的工程师。
引见
首先引见我们的故事的上下文,应该引见几点我们为什么需求这个效劳器。
Mail.Ru有很多有形状的系统。 用户电子邮件存储是其中之一。 跟踪系统中的形状变化和系统事情有几种办法。 这主要是经过活期系统轮询或关于其形状变化的系统通知。
两种方式都有利害。 但是当触及邮件时,用户收到新邮件的速度越快越好。
邮件轮询触及每秒大约50,000个HTTP查询,其中60%前往304形状,这意味着邮箱没有变化。
因此,为了增加效劳器上的负载并加快邮件传递给用户,决议经过编写发布-订阅效劳器,一方面将接纳有关形状更改的通知,另一方面则会收到这种通知的订阅。
先前
如今
第一个方案显示了以前的样子。 阅读器活期轮询API,并查询有关Storage(邮箱效劳)的更改。
第二个方案描画了新架构。 阅读器与通知API树立WebSocket衔接,通知API是Bus效劳器的客户端。收到新的电子邮件后,Storage会向Bus(1)发送一条通知,由Bus发送到订阅者。 API确定衔接以发送接纳到的通知,并将其发送到用户的阅读器(3)。
所以明天我们将讨论API或WebSocket效劳器。 我们的效劳器将有大约300万个在线衔接。
完成方式
让我们看看如何运用Go函数完成效劳器的某些部分,而无需任何优化。
在停止net/http ,我们来谈谈我们如何发送和接纳数据。 站在WebSocket协议(例如JSON对象) 之上的数据在下文中将被称为分组 。
我们末尾完成包含经过WebSocket衔接发送和接纳这些数据包的Channel结构。
channel 结构
// Packet represents application level data.
type Packet struct {
...
}
// Channel wraps user connection.
type Channel struct {
conn net.Conn // WebSocket connection.
send chan Packet // Outgoing packets queue.
}
func NewChannel(conn net.Conn) *Channel {
c := &Channel{
conn: conn,
send: make(chan Packet, N),
}
go c.reader()
go c.writer()
return c
}
留意这里有reader和writer连个goroutines。 每个goroutine都需求本人的内存栈, 依据操作系统和Go版本能够具有2到8 KB的初始大小。
在300万个在线衔接的时分,我们将需求24 GB的内存 (堆栈为4 KB)用于维持一切衔接。 这还没有计算为Channel结构分配的内存,传出的数据包ch.send和其他外部字段消耗的内存。
I/O goroutines
我们来看看“reader”的完成:
func (c *Channel) reader() {
// We make a buffered read to reduce read syscalls.
buf := bufio.NewReader(c.conn)
for {
pkt, _ := readPacket(buf)
c.handle(pkt)
}
}
这里我们运用bufio.Reader来增加read() syscalls的数量,并读取与buf缓冲区大小一样的数量。 在有限循环中,我们等候新数据的到来。 请记住: 估量新数据将会来临。 我们稍后会回来。
我们将分开传入数据包的解析和处置,由于对我们将要讨论的优化不重要。 但是, buf如今值得我们留意:默许状况下,它是4 KB,这意味着我们需求另外12 GB内存。 “writer”有相似的状况:
func (c *Channel) writer() {
// We make buffered write to reduce write syscalls.
buf := bufio.NewWriter(c.conn)
for pkt := range c.send {
_ := writePacket(buf, pkt)
buf.Flush()
}
}
我们遍历c.send ,并将它们写入缓冲区。细心读者曾经猜到的,我们的300万个衔接还将消耗12 GB的内存。
HTTP
我们曾经有一个复杂的Channel完成,如今我们需求一个WebSocket衔接才能运用。
(责任编辑:admin)