go websocket服务器性能,性能优化实战:百万级WebSockets和Go语言

本文介绍了如何使用Go语言开发一个高负载的WebSocket服务,以减轻服务器负担并提高邮件接收速度。作者Sergey Kamardin分享了在Mail.Ru的工作经验,探讨了如何通过netpoll、限制并发goroutines、零拷贝升级等优化手段处理近3百万个连接,降低内存消耗。文章详细阐述了优化过程和关键代码实现,旨在为Go语言初学者提供性能调优的思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

大家好!我的名字叫Sergey Kamardin。我是来自Mail.Ru的一名工程师。这篇文章将讲述我们是如何用Go语言开发一个高负荷的WebSocket服务。即使你对WebSockets熟悉但对Go语言知之甚少,我还是希望这篇文章里讲到的性能优化的思路和技术对你有所启发。

1. 介绍

作为全文的铺垫,我想先讲一下我们为什么要开发这个服务。

Mail.Ru有许多包含状态的系统。用户的电子邮件存储是其中之一。有很多办法来跟踪这些状态的改变。不外乎通过定期的轮询或者系统通知来得到状态的变化。这两种方法都有它们的优缺点。对邮件这个产品来说,让用户尽快收到新的邮件是一个考量指标。邮件的轮询会产生大概每秒5万个HTTP请求,其中60%的请求会返回304状态(表示邮箱没有变化)。因此,为了减少服务器的负荷并加速邮件的接收,我们决定重写一个publisher-subscriber服务(这个服务通常也会称作bus,message broker或者event-channel)。这个服务负责接收状态更新的通知,然后还处理对这些更新的订阅。

重写publisher-subscriber服务之前:

0_pull.png

现在:

1_push.png

上面第一个图为旧的架构。浏览器(Browser)会定期轮询API服务来获得邮件存储服务(Storage)的更新。

第二张图展示的是新的架构。浏览器(Browser)和通知API服务(notificcation API)建立一个WebSocket连接。通知API服务会发送相关的订阅到Bus服务上。当收到新的电子邮件时,存储服务(Storage)向Bus(1)发送一个通知,Bus又将通知发送给相应的订阅者(2)。API服务为收到的通知找到相应的连接,然后把通知推送到用户的浏览器(3)。

我们今天就来讨论一下这个API服务(也可以叫做WebSocket服务)。在开始之前,我想提一下这个在线服务处理将近3百万个连接。

2. 惯用的做法(The idiomatic way)

首先,我们看一下不做任何优化会如何用Go来实现这个服务的部分功能。在使用net/http

实现具体功能前,让我们先讨论下我们将如何发送和接收数据。这些数据是定义在WebSocket协议之上的(例如JSON对象)。我们在下文中会成他们为packet。

我们先来实现Channel

结构。它包含相应的逻辑来通过WebScoket连接发送和接收packet。

2.1. 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

}

这里我要强调的是读和写这两个goroutines。每个goroutine都需要各自的内存栈。栈的初始大小由操作系统和Go的版本决定,通常在2KB到8KB之间。我们之前提到有3百万个在线连接,如果每个goroutine栈需要4KB的话,所有连接就需要24GB的内存。这还没算上给Channel

结构,发送packet用的ch.send

和其它一些内部字段分配的内存空间。

2.2. 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

。每次都会在buf

大小允许的范围内尽量读取多的字节,从而减少read()

系统调用的次数。在无限循环中,我们期望会接收到新的数据。请记住之前这句话:期望接收到新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值