Golang 搭建 WebSocket 应用(七) - 性能、可用性

简介: Golang 搭建 WebSocket 应用(七) - 性能、可用性

本文探讨了在WebSocket应用场景中可能遇到的性能问题,如连接数影响的内存消耗、字符串与字节切片转换的性能损失以及过度使用互斥锁导致的并发限制。同时,作者提出了使用读写锁和分段map的改进策略,并讨论了高可用性在集群部署中的挑战及解决方案。

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

在前面的文章中,提到过非功能性需求决定了架构。

今天我们再来考虑一下另外两个非功能性需求:性能和可用性。

前言

关于性能,其实并不是只有我们这个消息推送系统独有的问题。

对于所有的开发者而言,都多多少少会处理过性能相关的问题,比如后端为了减少数据库查询提高并发引入的缓存中间件,如 redis

又或者如前端一次性渲染大量数据的时候,如果让用户体验更加流畅等。

本文会针对 WebSocket 应用场景下去思考一些可能出现的性能问题以及可行的解决方案。

性能

对于性能,有几个可能导致性能问题的地方:

连接数

连接数过多会导致占用的内存过多,因为对于每一个连接,我们都有两个协程,一个读协程,一个写协程;

同时我们的 Client 结构体中的 send 是一个缓冲通道,它的缓冲区大小也直接影响最终占用的内存大小。

比如,我们目前的创建 Client 实例的代码是下面这样的:

client := &Client{hub: hub, conn: conn, send: make(chan Log, 256), uid: uid}

我们在这里直接为 send 分配了 256 的大小,如果 Log 结构体比较大的话,

它占用的内存就会比较大了(因为最终占用内存 = 连接数 * sizeof(Log) * 256)。

在实际中,我们一般没有那么多等待发送的消息,这个其实可以设置为一个非常小的值,比如 16;

设置为一个小的值的负面影响是,当 send 塞满了 16 条 Log 的时候,发送消息的接口会阻塞:

func send(hub *Hub, w http.ResponseWriter, r *http.Request) {
    // ... 其他代码
    // 如果 send 满了,下面这一行会阻塞
  client.send <- messageLog
  hub.pending.Add(int64(1))
}

所以这个数值可能需要根据实际场景来选择一个更加合适的值。

代码本身的问题

比如,我们的代码中其实有一个很常见的性能问题,就是 string[]byte 之间直接强转:

// writePump 方法里面将 string 转 []byte
if err := c.conn.WriteMessage(websocket.TextMessage, []byte(messageLog.Message)); err != nil {
    return
}

至于原因,可以去看看此前的一篇文章《深入理解 go unsafe》 的最后一小节,

简单来说,就是这个转换会产生内存分配,而内存分配会导致一定的性能损耗。而通过 unsafe 就可以实现无损的转换。

除了这个,其他地方也没啥太大的问题了,因为到目前为止,我们的代码还是非常的简单的。

互斥锁

为了保证程序的并发安全,我们在 Hub 中加了一个 sync.Mutex,也就是互斥锁。

在代码中,被 sync.MutexLock 保护的代码,在同一时刻只能有一个协程可以执行。

// 推送消息的接口
func send(hub *Hub, w http.ResponseWriter, r *http.Request) {
    // ... 其他代码
  // 从 hub 中获取 client
  hub.Lock()
  client, ok := hub.userClients[uid]
  hub.Unlock()
    // ... 其他代码
}

对于上面这种只读的操作,也就是没有对 map 进行写操作,我们依然使用了 sync.MutexLock() 来锁定临界区。

这里存在的问题是,其实我们的 hub.userClients 是支持并发读的,只是不能同时读写而已。

所以我们可以考虑将 sync.Mutex 替换为 sync.RWMutex,这样就可以实现并发读了:

// 推送消息的接口
func send(hub *Hub, w http.ResponseWriter, r *http.Request) {
    // ... 其他代码
  // 从 hub 中获取 client
  hub.RLock() // 读锁
  client, ok := hub.userClients[uid]
  hub.RUnlock() // 释放读锁
    // ... 其他代码
}

这样做的好处是,当有多个并发的 send 请求的时候,这些并发的 send 请求并不会相互阻塞;

而使用 sync.Mutex 的时候,并发的 send 请求是会相互阻塞的,也就是会导致 send 变成串行的,这样性能无疑会很差。

除此之外,我们在 Hubrun 方法中也使用了 sync.Mutex

case client := <-h.register:
    h.Lock()
    h.clients[client] = true
    h.userClients[client.uid] = client
    h.Unlock()

也就是说,我们将 Client 注册到 Hub 的操作也是串行的。

对于这种场景,其实也有一种解决方法就是分段 map

也就是将 clientsuserClients 这两个 map 拆分为多个 map

然后对于每一个 map 都有一个对应的 sync.Mutex 互斥锁来保证其读写的安全。

但如果要这样做,单单分段还不够,我们的 registerunregister 还是只有一个,对于这个问题,

我们可能需要将 registerunregister 也分段,最后在 run 方法里面起多个协程来进行处理。

这个实现起来就很复杂了。

其他

由于我们的 Hub 中还有 MessageLogger、错误处理、认证等功能,

在实际中,如果我们有将其替换为自己的实现,可能还得考虑自己的实现中可能存在的性能问题:

type Hub struct {
  messageLogger MessageLogger
  errorHandler  Handler
  authenticator Authenticator
}

可用性

这里主要讨论的是集群部署的情况下,应用存在的一些的问题以及可行的解决方案。关于具体部署上的细节不讨论。

要实现高可用的话,我们就得加机器了,毕竟如果只有一台服务器的话,一旦它宕机了,服务就完全挂了。

由于我们的 WebSocket 应用维持着跟客户端的连接,在单机的时候,客户端连接、推送消息都是在一台机器上的。

这种情况下并没有什么问题,因为推送消息的时候,都可以根据 uid 来找到对应的 WebSocket 连接,从而给客户端推送消息。

而在多台机器的情况下,我们的客户端可能跟不同的服务器产生连接,这个时候一个比较关键的问题是:

如何根据 uid 找到对应的 WebSocket 连接所在的机器?

如果我们推送消息的请求到达的机器上并没有消息关联的 WebSocket 连接,那么我们的消息就无法推送给客户端了。

对于这个问题,一个可行的解决方案是,将 uid 和服务器建立起关联,比如,在用户登录的时候,

就给用户返回一个 WebSocket 服务器的地址,客户端拿到这个地址之后,跟这个服务器建立起 WebSocket 连接,

然后其他应用推送消息的时候,也根据同样的算法将推送消息的请求发送到这个 WebSocket 服务器即可。

总结

最后,再简单回顾一下本文的内容:

  • 具体来说,我们的系统中会有下面几个可能的地方会导致产生性能问题:
  • 连接数:一个连接会有两个协程,另外每一个 Client 结构体也会需要一定的缓冲区来缓冲发送给客户端的消息
  • 代码上的性能问题:如 string[]byte 之间转换带来的性能损耗
  • 互斥锁:某些地方可以使用读写锁来提高读的并发量,另外一个办法就是使用分段 map 配合互斥锁
  • 系统本身预留的扩展点中,用户自行实现的代码中可能会存在性能问题
  • 要实现高可用就得将系统部署到多台机器上,这个时候需要在 uid 和服务器之间建立起某种关联,以便推送消息的时候可以成功推送给客户端。


目录
相关文章
|
1月前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
75 1
|
3月前
|
JSON Go 数据格式
Golang 对象深拷贝的方案以及性能比较
文章探讨了在Go语言中实现对象深拷贝的三种方法:使用gob序列化、JSON序列化和定制化拷贝,并对比了它们在性能和代码复杂度方面的优劣,建议根据具体需求选择最适合的方法。
68 0
|
22天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
20天前
|
Kubernetes Cloud Native JavaScript
为使用WebSocket构建的双向通信应用带来基于服务网格的全链路灰度
介绍如何使用为基于WebSocket的云原生应用构建全链路灰度方案。
|
2月前
|
算法 安全 测试技术
golang 栈数据结构的实现和应用
本文详细介绍了“栈”这一数据结构的特点,并用Golang实现栈。栈是一种FILO(First In Last Out,即先进后出或后进先出)的数据结构。文章展示了如何用slice和链表来实现栈,并通过golang benchmark测试了二者的性能差异。此外,还提供了几个使用栈结构解决的实际算法问题示例,如有效的括号匹配等。
golang 栈数据结构的实现和应用
|
1月前
|
安全 数据安全/隐私保护 UED
优化用户体验:前后端分离架构下Python WebSocket实时通信的性能考量
在当今互联网技术的迅猛发展中,前后端分离架构已然成为主流趋势,它不仅提升了开发效率,也优化了用户体验。然而,在这种架构模式下,如何实现高效的实时通信,特别是利用WebSocket协议,成为了提升用户体验的关键。本文将探讨在前后端分离架构中,使用Python进行WebSocket实时通信时的性能考量,以及与传统轮询方式的比较。
64 2
|
2月前
|
JavaScript 前端开发 UED
WebSocket在Python Web开发中的革新应用:解锁实时通信的新可能
在快速发展的Web应用领域中,实时通信已成为许多现代应用不可或缺的功能。传统的HTTP请求/响应模式在处理实时数据时显得力不从心,而WebSocket技术的出现,为Python Web开发带来了革命性的变化,它允许服务器与客户端之间建立持久的连接,从而实现了数据的即时传输与交换。本文将通过问题解答的形式,深入探讨WebSocket在Python Web开发中的革新应用及其实现方法。
44 3
|
1月前
|
中间件 Go 数据处理
应用golang的管道-过滤器架构风格
【10月更文挑战第1天】本文介绍了一种面向数据流的软件架构设计模式——管道-过滤器(Pipe and Filter),并通过Go语言的Gin框架实现了一个Web应用示例。该模式通过将数据处理流程分解为一系列独立的组件(过滤器),并利用管道连接这些组件,实现了模块化、可扩展性和高效的分布式处理。文中详细讲解了Gin框架的基本使用、中间件的应用以及性能优化方法,展示了如何构建高性能的Web服务。
65 0
|
1月前
|
消息中间件 网络协议 安全
C# 一分钟浅谈:WebSocket 协议应用
【10月更文挑战第6天】在过去的一年中,我参与了一个基于 WebSocket 的实时通信系统项目,该项目不仅提升了工作效率,还改善了用户体验。本文将分享在 C# 中应用 WebSocket 协议的经验和心得,包括基础概念、C# 实现示例、常见问题及解决方案等内容,希望能为广大开发者提供参考。
99 0
|
2月前
|
存储 监控 Go
面向OpenTelemetry的Golang应用无侵入插桩技术
文章主要讲述了阿里云 ARMS 团队与程序语言与编译器团队合作研发的面向OpenTelemetry的Golang应用无侵入插桩技术解决方案,旨在解决Golang应用监控的挑战。