- 单一职责:每个服务只负责一个核心功能
- 独立部署:可以独立启动、停止、扩展
- 清晰边界:服务间通过API通信,避免直接依赖
| 服务 | 端口 | 功能 | 协议 | 启动命令 |
|---|---|---|---|---|
| API | 8080 | 用户接口、业务逻辑、WebSocket | HTTP/WebSocket | go run main.go api |
| Admin | 8081 | 管理接口、后台管理 | HTTP | go run main.go admin |
# 启动单个服务
./scripts/start.sh api # 启动API服务
./scripts/start.sh admin # 启动Admin服务
./scripts/start.sh ws # 启动WebSocket服务
# 启动所有服务
./scripts/start.sh all# 分别启动
go run main.go api
go run main.go admin
go run main.go wsPOST /api/login- 用户登录POST /api/user/register- 用户注册GET /api/user/info- 获取用户信息 (需要认证)POST /api/user/profile- 更新用户资料 (需要认证)POST /api/chat/message/send- 发送消息 (需要认证)GET /api/chat/message/history- 获取消息历史 (需要认证)POST /api/chat/upload- 上传文件 (需要认证)GET /api/friend/list- 获取好友列表 (需要认证)GET /api/friend/detail/:friend_id- 获取好友详情 (需要认证)
POST /admin/user/add- 创建用户GET /admin/user/list- 获取用户列表
GET /ws/connect?token=<token>&user_id=<ID>- 建立WebSocket连接- 消息推送:通过WebSocket实时推送消息给在线用户
| 数据库 | 用途 | 连接信息 |
|---|---|---|
| MySQL | 用户基础数据、关系数据、消息索引 | localhost:3306 |
| MongoDB | 消息存储(主要存储)、历史消息查询 | localhost:27017 |
-
MongoDB(主存储):
- 存储所有聊天消息的完整数据
- 支持高效的分页查询和复杂查询
- 适合存储大量历史消息
- 集合名称:
chat_messages
-
MySQL(辅助存储):
- 存储消息索引和元数据
- 用于快速统计和关系查询
- 与MongoDB同步存储,确保数据一致性
发送消息 → 生成消息ID → 同时写入MongoDB和MySQL → 通过WebSocket实时推送
- 历史消息查询:优先从MongoDB查询
- 分页支持:支持按时间倒序分页查询
- 查询优化:使用索引加速查询(from_user_id, to_user_id, created_at)
# 启动所有服务
./scripts/start.sh all# 分别部署到不同服务器
# API服务 - 负载均衡
# WebSocket服务 - 独立服务器
# Admin服务 - 内网访问- 每个服务都有独立的日志
- 使用健康检查接口监控服务状态
- 建议使用Prometheus + Grafana监控
- 清晰分离:每个服务职责明确
- 独立扩展:可以根据负载独立扩展
- 故障隔离:一个服务故障不影响其他服务
- 技术栈灵活:不同服务可以使用不同技术
- 易于维护:代码结构清晰,便于团队协作
- Hub模式:使用单Hub管理所有WebSocket连接
- Goroutine模型:每个连接使用独立的goroutine处理读写
- 通道通信:使用Go channel实现线程安全的消息传递
目的:检测连接是否存活,及时发现并处理断线情况。
实现方式:
前端(客户端):
- 每30秒自动发送
ping消息 - 监听
pong响应,更新最后心跳时间 - 如果60秒内未收到
pong,主动断开连接并触发重连
后端(服务端):
- 接收客户端的
ping消息,立即回复pong - 使用
SetPongHandler处理WebSocket协议层的pong帧 - 设置读超时(
SetReadDeadline),超时自动断开 - 启动心跳超时检测goroutine,定期检查最后pong时间
- 如果超过配置的超时时间(默认60秒)未收到心跳,主动断开连接
配置参数(config/app.yml):
websocket:
ping_interval: 30 # 心跳ping间隔(秒)
pong_wait: 60 # 心跳超时时间(秒)工作原理:
客户端 服务端
| |
|--- ping (30s) ------->|
|<-- pong --------------|
| |
|--- ping (30s) ------->|
|<-- pong --------------|
| |
|--- ping (30s) ------->|
| (网络断开) |
| | (60秒未收到ping)
| | 主动断开连接
目的:网络波动或服务重启时,自动恢复连接。
实现策略:
指数退避算法(Exponential Backoff):
- 第1次重连:3秒后
- 第2次重连:6秒后(3 × 2¹)
- 第3次重连:12秒后(3 × 2²)
- 第4次重连:24秒后(3 × 2³)
- 最大间隔:30秒(防止间隔过长)
重连次数限制:
- 默认最大重连次数:10次
- 超过最大次数后,停止自动重连,触发
reconnect_failed事件 - 用户可以通过手动调用
connect()重新连接
实现代码(前端):
// 指数退避计算
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
this.maxReconnectInterval
)重连触发条件:
- WebSocket连接意外关闭(
onclose事件) - 心跳超时检测失败
- 网络错误(
onerror事件)
目的:防止连接因网络问题长时间阻塞。
实现方式:
读超时:
- 每次收到消息后,重置读超时为
pong_wait时间 - 如果超时未收到任何数据,自动断开连接
- 使用
SetReadDeadline实现
写超时:
- 每次发送消息前,设置写超时为10秒
- 如果10秒内无法发送,断开连接
- 使用
SetWriteDeadline实现
代码示例(后端):
// 读超时
c.conn.SetReadDeadline(time.Now().Add(c.hub.pongWait))
// 写超时
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))目的:实时了解连接健康状态,便于问题排查。
监控指标:
- 在线用户数量
- 连接建立时间
- 最后心跳时间
- 消息发送成功率
- 重连次数统计
日志记录:
- 连接建立/断开事件
- 心跳超时警告
- 重连尝试记录
- 消息发送失败警告
目的:确保连接关闭时资源正确释放。
关闭流程:
- 停止心跳定时器
- 停止心跳超时检测
- 关闭发送通道(
close(client.send)) - 发送WebSocket关闭帧(
CloseMessage) - 关闭TCP连接
- 从Hub中注销客户端
- 清理相关资源
代码示例:
defer func() {
c.hub.unregister <- c
c.conn.Close()
c.stopHeartbeat()
}()目的:提高系统容错能力,避免单个连接问题影响整体。
处理策略:
- 发送通道满:关闭连接,让客户端重连
- 消息解析失败:记录日志,忽略该消息
- 连接数超限:拒绝新连接,记录警告
- 用户重复连接:替换旧连接,关闭旧连接
代码示例:
// 发送通道满时的处理
select {
case client.send <- message:
// 发送成功
default:
// 通道满,关闭连接
close(client.send)
delete(h.clients, client)
}| 机制 | 前端 | 后端 | 作用 |
|---|---|---|---|
| 心跳检测 | ✅ 发送ping | ✅ 回复pong + 超时检测 | 及时发现断线 |
| 自动重连 | ✅ 指数退避 | - | 自动恢复连接 |
| 读写超时 | - | ✅ SetRead/WriteDeadline | 防止长时间阻塞 |
| 状态监控 | ✅ 连接状态 | ✅ 日志记录 | 便于问题排查 |
| 优雅关闭 | ✅ 清理资源 | ✅ 清理资源 | 防止资源泄漏 |
| 异常处理 | ✅ 错误捕获 | ✅ 容错机制 | 提高系统稳定性 |
最佳实践建议:
- 心跳间隔:建议30秒,平衡及时性和网络开销
- 超时时间:建议60秒(2倍心跳间隔),给网络波动留出缓冲
- 重连策略:使用指数退避,避免频繁重连造成服务器压力
- 监控告警:设置连接数、心跳超时率等监控指标
- 日志记录:记录关键事件,便于问题排查
什么是缓冲通道? 缓冲通道是Go语言中带缓冲区的channel,可以在没有接收者的情况下先存储一定数量的数据,避免发送操作阻塞。
缓冲通道 vs 无缓冲通道:
// 无缓冲通道:发送和接收必须同时准备好,否则阻塞
ch := make(chan int)
// 缓冲通道:可以存储N个元素,只有缓冲区满时才阻塞
ch := make(chan int, 1000)本系统缓冲通道配置:
| 通道名称 | 缓冲大小 | 作用 | 为什么需要缓冲 |
|---|---|---|---|
register |
1000 | 客户端注册队列 | 处理突发连接请求,避免注册操作阻塞 |
unregister |
1000 | 客户端注销队列 | 处理大量同时断开,避免注销操作阻塞 |
broadcast |
1000 | 广播消息队列 | 处理系统通知等广播消息,避免阻塞 |
sendToUser |
10000 | 点对点消息队列 | 核心队列,处理所有用户消息,需要大缓冲应对突发流量 |
client.send |
1024 | 单个客户端发送队列 | 每个连接独立缓冲,处理网络延迟导致的发送堆积 |
缓冲通道的作用:
-
解耦生产者和消费者
- 发送消息的goroutine(生产者)不需要等待接收者立即处理
- 接收消息的goroutine(消费者)可以批量处理,提高效率
-
平滑流量峰值
- 当消息发送速度 > 处理速度时,缓冲通道可以暂存消息
- 避免消息丢失或系统阻塞
- 例如:10000缓冲可以应对短时间内的消息洪峰
-
提高并发性能
- 非阻塞发送:缓冲区未满时,发送操作立即返回,不阻塞
- 减少goroutine等待时间,提高CPU利用率
- 允许更多goroutine并发工作
-
防止系统崩溃
- 当通道满时,使用
select的default分支可以优雅处理 - 记录警告日志,而不是让系统阻塞或崩溃
- 给系统恢复和降级的机会
- 当通道满时,使用
实际应用场景:
// 场景1:突发消息洪峰
// 假设1000个用户同时发送消息
// 如果没有缓冲通道,Hub.Run()处理不过来会导致阻塞
// 有了10000缓冲,可以暂存这些消息,逐步处理
// 场景2:网络延迟
// 客户端网络慢,writePump()发送速度慢
// client.send的1024缓冲可以暂存待发送消息
// 避免因为网络慢导致整个系统阻塞
// 场景3:连接管理
// 大量用户同时上线/下线
// register/unregister的1000缓冲可以平滑处理缓冲大小的选择原则:
-
根据业务量估算
sendToUser: 10000 = 假设每秒1000条消息,缓冲10秒的量client.send: 1024 = 假设网络延迟1秒,每秒1024条消息
-
平衡内存和性能
- 缓冲太大:占用内存多,但性能好
- 缓冲太小:内存占用少,但容易阻塞
- 本系统选择:在内存可接受范围内,尽量大
-
考虑最坏情况
- 峰值流量是平均流量的10倍
- 缓冲大小应该能应对峰值流量
性能指标:
- 并发能力:理论上支持数万并发连接
- 消息处理能力:每秒数万条消息
- 线程安全:使用
sync.RWMutex保证线程安全 - 非阻塞设计:关键路径使用非阻塞操作,避免死锁
每个WebSocket连接的内存占用:
| 组件 | 内存占用 | 说明 |
|---|---|---|
| Client结构体 | ~200字节 | 包含conn、send通道、userID、hub引用 |
| send通道缓冲 | ~200KB | 1024缓冲 × 平均200字节/消息 |
| Goroutine栈 | ~4KB | readPump + writePump,每个约2KB |
| WebSocket连接 | ~5KB | TCP连接缓冲区、WebSocket协议开销 |
| 单连接总计 | ~210KB | 约0.2MB |
Hub全局内存占用:
| 组件 | 内存占用 | 说明 |
|---|---|---|
| clients map | ~8字节/连接 | 指针存储 |
| userClients map | ~16字节/连接 | uint + 指针 |
| 通道缓冲 | ~5MB | register/unregister/broadcast/sendToUser |
| Hub总计 | ~5MB + 24字节/连接 | 可忽略不计 |
系统基础开销:
| 组件 | 内存占用 | 说明 |
|---|---|---|
| Go运行时 | ~50-100MB | GC、调度器等 |
| 数据库连接池 | ~10-20MB | MySQL、MongoDB连接 |
| 应用框架 | ~20-30MB | Gin、中间件等 |
| 系统总计 | ~80-150MB | 基础开销 |
1核2G服务器并发能力分析:
内存计算:
总内存:2GB = 2048MB
系统开销:~150MB(Go运行时 + 数据库 + 框架)
可用内存:~1900MB
单连接内存:~210KB(0.2MB)
理论最大连接数:1900MB ÷ 0.2MB ≈ 9500个连接
CPU计算:
单核CPU限制:
- 每个连接:2个goroutine(readPump + writePump)
- 9500连接 = 19000个goroutine
- Go调度器可以高效调度,但单核CPU是瓶颈
- 实际CPU利用率建议控制在70-80%
- 考虑消息处理、网络I/O等开销
实际建议并发数:
| 场景 | 并发连接数 | 说明 |
|---|---|---|
| 保守估计 | 2000-3000 | 保证系统稳定,CPU利用率60-70% |
| 正常使用 | 3000-5000 | 平衡性能和稳定性,CPU利用率70-80% |
| 极限压测 | 5000-7000 | CPU接近满载,可能出现延迟 |
| 理论最大值 | ~9500 | 内存限制,但CPU会成为瓶颈 |
影响因素:
-
消息频率:
- 低频率(每秒<10条):可支持更多连接
- 高频率(每秒>100条):需要减少连接数
-
消息大小:
- 小消息(<1KB):可支持更多连接
- 大消息(>10KB):需要减少连接数
-
网络带宽:
- 1核2G服务器通常带宽有限(1-10Mbps)
- 带宽可能成为瓶颈
-
数据库性能:
- MongoDB和MySQL的查询性能
- 连接池大小限制
优化建议:
-
连接数限制:
- 已在配置文件中实现:
config/app.yml中的websocket.max_connections - 默认值:5000(可根据服务器配置调整)
- 设置为0表示无限制(不推荐)
- 连接数达到上限时,会拒绝新连接(除非是替换同一用户的旧连接)
- 配置示例:
websocket: max_connections: 3000 # 1核2G服务器建议值
- 已在配置文件中实现:
-
监控指标:
- 监控内存使用率(建议<80%)
- 监控CPU使用率(建议<80%)
- 监控goroutine数量
- 监控消息队列长度
-
降级策略:
- 连接数接近上限时,拒绝新连接
- 消息队列满时,记录日志并丢弃
- CPU使用率过高时,限制新连接
生产环境建议:
- 1核2G服务器:建议并发连接数 2000-3000
- 2核4G服务器:建议并发连接数 5000-8000
- 4核8G服务器:建议并发连接数 10000-15000
发送消息 → SendToUser(userID, message)
→ 入队到sendToUser通道(非阻塞)
→ Hub.Run()从通道取出
→ sendToUserMessage()查找userClients[userID]
→ 找到对应客户端 → 发送到client.send通道
→ writePump() goroutine发送到WebSocket
- 精确映射:使用
userClients map[uint]*Client精确映射用户ID到连接 - 单连接保证:同一用户多个连接时,自动关闭旧连接,只保留最新连接
- ID验证:发送消息前验证用户ID有效性
- 线程安全:所有操作都在Hub的Run goroutine中执行
- 双向推送:消息同时推送给发送者和接收者,确保双方都能实时看到消息
- 离线处理:用户不在线时,消息已存储到数据库,用户上线后可查询历史消息
- 消息格式:
{ "type": "message", "data": { "id": "消息ID", "from_user_id": 发送者ID, "to_user_id": 接收者ID, "message_type": 消息类型, "content": "消息内容", "created_at": 时间戳 } }
- 自动重连:客户端断开后自动清理连接
- 心跳检测:客户端定期发送心跳保持连接
- 异常处理:连接异常时自动注销,防止资源泄漏
- 文本消息 (type=0):普通文本
- 图片消息 (type=1):图片文件
- 文件消息 (type=2):文件附件
- 表情包 (type=3):表情图片
- 系统消息 (type=4):系统通知
- 存储位置:
resources/upload/{fileType}/{date}/ - 访问路径:
/static/upload/{相对路径} - 文件类型:支持图片、文件等
- URL返回:返回完整URL(包含域名)
- 服务发现:生产环境建议使用服务发现机制
- 负载均衡:API服务需要负载均衡
- 消息队列:高并发时考虑引入消息队列(当前已实现缓冲通道)
- 监控告警:建立完善的监控和告警机制
- 消息持久化:当前实现中,用户不在线时消息会存储到数据库,但需要前端主动查询历史消息
- 连接数限制:当前无连接数限制,生产环境建议添加限制防止资源耗尽
- 消息大小限制:单条消息最大1MB,防止内存溢出