系统设计实战 189:WebSocket服务器

1 阅读9分钟

🚀 系统设计实战 189:WebSocket服务器

摘要:本文深入剖析系统的核心架构关键算法工程实践,提供完整的设计方案和面试要点。

你是否想过,设计WebSocket服务器背后的技术挑战有多复杂?

1. 系统概述

1.1 业务背景

WebSocket服务器提供全双工通信能力,支持实时消息推送、在线聊天、实时协作、游戏等场景。需要处理大量并发连接、消息广播和连接管理。

1.2 核心功能

  • 连接管理:WebSocket连接的建立、维护和清理
  • 消息广播:一对多、多对多的消息分发
  • 房间管理:用户分组和频道管理
  • 心跳机制:连接保活和异常检测
  • 扩展性:水平扩展和负载均衡

1.3 技术挑战

  • 并发连接:支持大量并发WebSocket连接
  • 内存管理:连接状态和消息缓冲的内存优化
  • 消息可靠性:消息传递的可靠性保证
  • 扩展性:多实例间的消息同步
  • 性能优化:低延迟的消息传递

2. 架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                  WebSocket服务器架构                        │
├─────────────────────────────────────────────────────────────┤
│  Client Layer                                               │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ Web客户端   │ │ 移动客户端  │ │ 桌面应用    │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
├─────────────────────────────────────────────────────────────┤
│  WebSocket Server Layer                                     │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ 连接管理    │ │ 消息路由    │ │ 房间管理    │           │
│  │ 心跳检测    │ │ 广播引擎    │ │ 权限控制    │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
├─────────────────────────────────────────────────────────────┤
│  Message Broker                                             │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │ Redis       │ │ RabbitMQ    │ │ Kafka       │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
└─────────────────────────────────────────────────────────────┘

3. 核心组件设计

3.1 WebSocket服务器核心

// 时间复杂度:O(N),空间复杂度:O(1)

type WebSocketServer struct {
    connectionManager *ConnectionManager
    roomManager       *RoomManager
    messageRouter     *MessageRouter
    heartbeatManager  *HeartbeatManager
    messageBroker     MessageBroker
    config           *ServerConfig
    metrics          *ServerMetrics
}

type Connection struct {
    ID          string
    UserID      string
    Socket      *websocket.Conn
    Rooms       map[string]bool
    LastPing    time.Time
    SendChan    chan []byte
    CloseChan   chan struct{}
    Metadata    map[string]interface{}
    mutex       sync.RWMutex
}

type Message struct {
    ID        string
    Type      MessageType
    From      string
    To        string
    Room      string
    Data      interface{}
    Timestamp time.Time
}

func (ws *WebSocketServer) HandleConnection(w http.ResponseWriter, r *http.Request) {
    // 升级HTTP连接为WebSocket
    conn, err := ws.upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("WebSocket upgrade failed: %v", err)
        return
    }
    
    // 创建连接对象
    connection := &Connection{
        ID:        generateConnectionID(),
        Socket:    conn,
        Rooms:     make(map[string]bool),
        LastPing:  time.Now(),
        SendChan:  make(chan []byte, 256),
        CloseChan: make(chan struct{}),
        Metadata:  make(map[string]interface{}),
    }
    
    // 注册连接
    ws.connectionManager.AddConnection(connection)
    
    // 启动连接处理协程
    go ws.handleConnectionRead(connection)
    go ws.handleConnectionWrite(connection)
    
    // 启动心跳检测
    ws.heartbeatManager.StartHeartbeat(connection)
}

func (ws *WebSocketServer) handleConnectionRead(conn *Connection) { defer func() { ws.connectionManager.RemoveConnection(conn.ID) conn.Socket.Close() close(conn.CloseChan) }()

conn.Socket.SetReadLimit(512)
conn.Socket.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.Socket.SetPongHandler(func(string) error {
    conn.LastPing = time.Now()
    conn.Socket.SetReadDeadline(time.Now().Add(60 * time.Second))
    return nil
})

for {
    _, messageData, err := conn.Socket.ReadMessage()
    if err != nil {
        if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
            log.Printf("WebSocket error: %v", err)
        }
        break
    }
    
    // 解析消息
    var message Message
    if err := json.Unmarshal(messageData, &message); err != nil {
        log.Printf("Message parse error: %v", err)
        continue
    }
    
    message.From = conn.ID
    message.Timestamp = time.Now()
    
    // 路由消息
    ws.messageRouter.RouteMessage(&message, conn)
}

}

func (ws *WebSocketServer) handleConnectionWrite(conn *Connection) { ticker := time.NewTicker(54 * time.Second) defer ticker.Stop()

for {
    select {
    case message := <-conn.SendChan:
        conn.Socket.SetWriteDeadline(time.Now().Add(10 * time.Second))
        if err := conn.Socket.WriteMessage(websocket.TextMessage, message); err != nil {
            return
        }
        
    case <-ticker.C:
        conn.Socket.SetWriteDeadline(time.Now().Add(10 * time.Second))
        if err := conn.Socket.WriteMessage(websocket.PingMessage, nil); err != nil {
            return
        }
        
    case <-conn.CloseChan:
        return
    }
}

}

3.2 连接管理器

type ConnectionManager struct {
    connections map[string]*Connection
    userConnections map[string]map[string]*Connection
    mutex       sync.RWMutex
    metrics     *ConnectionMetrics
}

func (cm *ConnectionManager) AddConnection(conn *Connection) {
    cm.mutex.Lock()
    defer cm.mutex.Unlock()
    
    cm.connections[conn.ID] = conn
    
    if conn.UserID != "" {
        if cm.userConnections[conn.UserID] == nil {
            cm.userConnections[conn.UserID] = make(map[string]*Connection)
        }
        cm.userConnections[conn.UserID][conn.ID] = conn
    }
    
    cm.metrics.RecordConnection(conn)
    log.Printf("Connection added: %s, total: %d", conn.ID, len(cm.connections))
}

func (cm *ConnectionManager) RemoveConnection(connID string) {
    cm.mutex.Lock()
    defer cm.mutex.Unlock()
    
    conn, exists := cm.connections[connID]
    if !exists {
        return
    }
    
    delete(cm.connections, connID)
    
    if conn.UserID != "" {
        if userConns, exists := cm.userConnections[conn.UserID]; exists {
            delete(userConns, connID)
            if len(userConns) == 0 {
                delete(cm.userConnections, conn.UserID)
            }
        }
    }
    
    cm.metrics.RecordDisconnection(conn)
    log.Printf("Connection removed: %s, total: %d", connID, len(cm.connections))
}

func (cm *ConnectionManager) GetConnection(connID string) (*Connection, bool) {
    cm.mutex.RLock()
    defer cm.mutex.RUnlock()
    
    conn, exists := cm.connections[connID]
    return conn, exists
}

func (cm *ConnectionManager) GetUserConnections(userID string) []*Connection {
    cm.mutex.RLock()
    defer cm.mutex.RUnlock()
    
    userConns, exists := cm.userConnections[userID]
    if !exists {
        return []*Connection{}
    }
    
    connections := make([]*Connection, 0, len(userConns))
    for _, conn := range userConns {
        connections = append(connections, conn)
    }
    
    return connections
}

func (cm *ConnectionManager) BroadcastToAll(message []byte) {
    cm.mutex.RLock()
    connections := make([]*Connection, 0, len(cm.connections))
    for _, conn := range cm.connections {
        connections = append(connections, conn)
    }
    cm.mutex.RUnlock()
    
    for _, conn := range connections {
        select {
        case conn.SendChan <- message:
        default:
            // 发送缓冲区满,跳过此连接
            log.Printf("Send buffer full for connection: %s", conn.ID)
        }
    }
}

3.3 房间管理器

type RoomManager struct {
    rooms       map[string]*Room
    userRooms   map[string]map[string]bool
    mutex       sync.RWMutex
    maxRoomSize int
}

type Room struct {
    ID          string
    Name        string
    Type        RoomType
    Members     map[string]*Connection
    Metadata    map[string]interface{}
    CreatedAt   time.Time
    mutex       sync.RWMutex
}

func (rm *RoomManager) CreateRoom(roomID, name string, roomType RoomType) (*Room, error) {
    rm.mutex.Lock()
    defer rm.mutex.Unlock()
    
    if _, exists := rm.rooms[roomID]; exists {
        return nil, ErrRoomAlreadyExists
    }
    
    room := &Room{
        ID:        roomID,
        Name:      name,
        Type:      roomType,
        Members:   make(map[string]*Connection),
        Metadata:  make(map[string]interface{}),
        CreatedAt: time.Now(),
    }
    
    rm.rooms[roomID] = room
    return room, nil
}

func (rm *RoomManager) JoinRoom(roomID string, conn *Connection) error {
    rm.mutex.RLock()
    room, exists := rm.rooms[roomID]
    rm.mutex.RUnlock()
    
    if !exists {
        return ErrRoomNotFound
    }
    
    room.mutex.Lock()
    defer room.mutex.Unlock()
    
    if len(room.Members) >= rm.maxRoomSize {
        return ErrRoomFull
    }
    
    room.Members[conn.ID] = conn
    
    conn.mutex.Lock()
    conn.Rooms[roomID] = true
    conn.mutex.Unlock()
    
    // 更新用户房间映射
    rm.mutex.Lock()
    if rm.userRooms[conn.UserID] == nil {
        rm.userRooms[conn.UserID] = make(map[string]bool)
    }
    rm.userRooms[conn.UserID][roomID] = true
    rm.mutex.Unlock()
    
    // 通知房间其他成员
    joinMessage := &Message{
        Type: MessageTypeUserJoined,
        Room: roomID,
        Data: map[string]interface{}{
            "userID": conn.UserID,
            "connID": conn.ID,
        },
        Timestamp: time.Now(),
    }
    
    rm.BroadcastToRoom(roomID, joinMessage, conn.ID)
    return nil
}

func (rm *RoomManager) LeaveRoom(roomID string, conn *Connection) error {
    rm.mutex.RLock()
    room, exists := rm.rooms[roomID]
    rm.mutex.RUnlock()
    
    if !exists {
        return ErrRoomNotFound
    }
    
    room.mutex.Lock()
    delete(room.Members, conn.ID)
    memberCount := len(room.Members)
    room.mutex.Unlock()
    
    conn.mutex.Lock()
    delete(conn.Rooms, roomID)
    conn.mutex.Unlock()
    
    // 更新用户房间映射
    rm.mutex.Lock()
    if userRooms, exists := rm.userRooms[conn.UserID]; exists {
        delete(userRooms, roomID)
        if len(userRooms) == 0 {
            delete(rm.userRooms, conn.UserID)
        }
    }
    rm.mutex.Unlock()
    
    // 如果房间为空,删除房间
    if memberCount == 0 {
        rm.mutex.Lock()
        delete(rm.rooms, roomID)
        rm.mutex.Unlock()
    } else {
        // 通知房间其他成员
        leaveMessage := &Message{
            Type: MessageTypeUserLeft,
            Room: roomID,
            Data: map[string]interface{}{
                "userID": conn.UserID,
                "connID": conn.ID,
            },
            Timestamp: time.Now(),
        }
        
        rm.BroadcastToRoom(roomID, leaveMessage, conn.ID)
    }
    
    return nil
}

func (rm *RoomManager) BroadcastToRoom(roomID string, message *Message, excludeConnID string) error {
    rm.mutex.RLock()
    room, exists := rm.rooms[roomID]
    rm.mutex.RUnlock()
    
    if !exists {
        return ErrRoomNotFound
    }
    
    messageData, err := json.Marshal(message)
    if err != nil {
        return err
    }
    
    room.mutex.RLock()
    members := make([]*Connection, 0, len(room.Members))
    for connID, conn := range room.Members {
        if connID != excludeConnID {
            members = append(members, conn)
        }
    }
    room.mutex.RUnlock()
    
    for _, conn := range members {
        select {
        case conn.SendChan <- messageData:
        default:
            log.Printf("Send buffer full for connection: %s in room: %s", conn.ID, roomID)
        }
    }
    
    return nil
}

### 3.4 消息路由器
```go
type MessageRouter struct {
    handlers    map[MessageType]MessageHandler
    middleware  []MessageMiddleware
    roomManager *RoomManager
    connManager *ConnectionManager
}

type MessageHandler interface {
    Handle(message *Message, conn *Connection) error
}

type MessageMiddleware interface {
    Process(message *Message, conn *Connection) bool
}

func (mr *MessageRouter) RouteMessage(message *Message, conn *Connection) {
    // 应用中间件
    for _, middleware := range mr.middleware {
        if !middleware.Process(message, conn) {
            return
        }
    }
    
    // 查找处理器
    handler, exists := mr.handlers[message.Type]
    if !exists {
        log.Printf("No handler for message type: %v", message.Type)
        return
    }
    
    // 处理消息
    if err := handler.Handle(message, conn); err != nil {
        log.Printf("Message handling error: %v", err)
    }
}

// 私聊消息处理器
type PrivateMessageHandler struct {
    connManager *ConnectionManager
}

func (pmh *PrivateMessageHandler) Handle(message *Message, conn *Connection) error {
    targetConns := pmh.connManager.GetUserConnections(message.To)
    if len(targetConns) == 0 {
        return ErrUserNotOnline
    }
    
    messageData, err := json.Marshal(message)
    if err != nil {
        return err
    }
    
    for _, targetConn := range targetConns {
        select {
        case targetConn.SendChan <- messageData:
        default:
            log.Printf("Send buffer full for user: %s", message.To)
        }
    }
    
    return nil
}

// 房间消息处理器
type RoomMessageHandler struct {
    roomManager *RoomManager
}

func (rmh *RoomMessageHandler) Handle(message *Message, conn *Connection) error {
    return rmh.roomManager.BroadcastToRoom(message.Room, message, conn.ID)
}

// 广播消息处理器
type BroadcastMessageHandler struct {
    connManager *ConnectionManager
}

func (bmh *BroadcastMessageHandler) Handle(message *Message, conn *Connection) error {
    messageData, err := json.Marshal(message)
    if err != nil {
        return err
    }
    
    bmh.connManager.BroadcastToAll(messageData)
    return nil
}

3.5 心跳管理器

type HeartbeatManager struct {
    interval    time.Duration
    timeout     time.Duration
    connections map[string]*Connection
    stopChan    chan struct{}
    mutex       sync.RWMutex
}

func (hm *HeartbeatManager) StartHeartbeat(conn *Connection) {
    hm.mutex.Lock()
    hm.connections[conn.ID] = conn
    hm.mutex.Unlock()
    
    go hm.monitorConnection(conn)
}

func (hm *HeartbeatManager) StopHeartbeat(connID string) {
    hm.mutex.Lock()
    delete(hm.connections, connID)
    hm.mutex.Unlock()
}

func (hm *HeartbeatManager) monitorConnection(conn *Connection) {
    ticker := time.NewTicker(hm.interval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            if time.Since(conn.LastPing) > hm.timeout {
                log.Printf("Connection timeout: %s", conn.ID)
                conn.Socket.Close()
                return
            }
            
        case <-conn.CloseChan:
            hm.StopHeartbeat(conn.ID)
            return
        }
    }
}

### 3.6 消息代理
```go
type MessageBroker interface {
    Publish(topic string, message []byte) error
    Subscribe(topic string, handler func([]byte)) error
    Unsubscribe(topic string) error
}

type RedisMessageBroker struct {
    client    *redis.Client
    pubsub    *redis.PubSub
    handlers  map[string]func([]byte)
    mutex     sync.RWMutex
}

func (rmb *RedisMessageBroker) Publish(topic string, message []byte) error {
    return rmb.client.Publish(context.Background(), topic, message).Err()
}

func (rmb *RedisMessageBroker) Subscribe(topic string, handler func([]byte)) error {
    rmb.mutex.Lock()
    defer rmb.mutex.Unlock()
    
    if rmb.pubsub == nil {
        rmb.pubsub = rmb.client.Subscribe(context.Background())
    }
    
    rmb.handlers[topic] = handler
    return rmb.pubsub.Subscribe(context.Background(), topic)
}

func (rmb *RedisMessageBroker) startMessageLoop() {
    ch := rmb.pubsub.Channel()
    
    for msg := range ch {
        rmb.mutex.RLock()
        handler, exists := rmb.handlers[msg.Channel]
        rmb.mutex.RUnlock()
        
        if exists {
            go handler([]byte(msg.Payload))
        }
    }
}

### 3.7 集群支持
```go
type ClusterManager struct {
    nodeID        string
    nodes         map[string]*ClusterNode
    messageBroker MessageBroker
    connManager   *ConnectionManager
    roomManager   *RoomManager
}

type ClusterNode struct {
    ID       string
    Address  string
    Status   NodeStatus
    LastSeen time.Time
}

func (cm *ClusterManager) Start() {
    // 订阅集群消息
    cm.messageBroker.Subscribe("cluster.broadcast", cm.handleClusterBroadcast)
    cm.messageBroker.Subscribe("cluster.room."+cm.nodeID, cm.handleRoomMessage)
    cm.messageBroker.Subscribe("cluster.user."+cm.nodeID, cm.handleUserMessage)
    
    // 启动节点发现
    go cm.nodeDiscovery()
}

func (cm *ClusterManager) BroadcastMessage(message *Message) error {
    messageData, err := json.Marshal(message)
    if err != nil {
        return err
    }
    
    return cm.messageBroker.Publish("cluster.broadcast", messageData)
}

func (cm *ClusterManager) SendToUser(userID string, message *Message) error {
    // 查找用户所在节点
    nodeID := cm.findUserNode(userID)
    if nodeID == "" {
        return ErrUserNotFound
    }
    
    messageData, err := json.Marshal(message)
    if err != nil {
        return err
    }
    
    topic := fmt.Sprintf("cluster.user.%s", nodeID)
    return cm.messageBroker.Publish(topic, messageData)
}

func (cm *ClusterManager) handleClusterBroadcast(data []byte) {
    var message Message
    if err := json.Unmarshal(data, &message); err != nil {
        log.Printf("Cluster broadcast parse error: %v", err)
        return
    }
    
    messageData, _ := json.Marshal(&message)
    cm.connManager.BroadcastToAll(messageData)
}

func (cm *ClusterManager) handleUserMessage(data []byte) {
    var message Message
    if err := json.Unmarshal(data, &message); err != nil {
        log.Printf("User message parse error: %v", err)
        return
    }
    
    connections := cm.connManager.GetUserConnections(message.To)
    messageData, _ := json.Marshal(&message)
    
    for _, conn := range connections {
        select {
        case conn.SendChan <- messageData:
        default:
            log.Printf("Send buffer full for user: %s", message.To)
        }
    }
}

## 4. 性能优化

### 4.1 连接池优化
```go
type ConnectionPool struct {
    connections chan *Connection
    factory     func() (*Connection, error)
    maxSize     int
    currentSize int32
    mutex       sync.Mutex
}

func (cp *ConnectionPool) Get() (*Connection, error) {
    select {
    case conn := <-cp.connections:
        return conn, nil
    default:
        return cp.factory()
    }
}

func (cp *ConnectionPool) Put(conn *Connection) {
    select {
    case cp.connections <- conn:
    default:
        // 池已满,关闭连接
        conn.Socket.Close()
    }
}

### 4.2 消息批处理
```go
type MessageBatcher struct {
    batchSize    int
    flushInterval time.Duration
    messages     [][]byte
    mutex        sync.Mutex
    flushChan    chan struct{}
}

func (mb *MessageBatcher) AddMessage(message []byte) {
    mb.mutex.Lock()
    defer mb.mutex.Unlock()
    
    mb.messages = append(mb.messages, message)
    
    if len(mb.messages) >= mb.batchSize {
        go mb.flush()
    }
}

func (mb *MessageBatcher) flush() {
    mb.mutex.Lock()
    messages := make([][]byte, len(mb.messages))
    copy(messages, mb.messages)
    mb.messages = mb.messages[:0]
    mb.mutex.Unlock()
    
    // 批量发送消息
    for _, message := range messages {
        // 发送逻辑
    }
}

WebSocket服务器通过高效的连接管理、消息路由和集群支持,为实时通信应用提供了可扩展的解决方案。


🎯 场景引入

你打开App,

你打开手机准备使用设计WebSocket服务器服务。看似简单的操作背后,系统面临三大核心挑战:

  • 挑战一:高并发——如何在百万级 QPS 下保持低延迟?
  • 挑战二:高可用——如何在节点故障时保证服务不中断?
  • 挑战三:数据一致性——如何在分布式环境下保证数据正确?

📈 容量估算

假设 DAU 1000 万,人均日请求 50 次

指标数值
请求 QPS~10 万/秒
P99 延迟< 5ms
并发连接数100 万+
带宽~100 Gbps
节点数20-100
可用性99.99%
日志数据/天~1 TB

❓ 高频面试问题

Q1:WebSocket服务器的核心设计原则是什么?

参考正文中的架构设计部分,核心原则包括:高可用(故障自动恢复)、高性能(低延迟高吞吐)、可扩展(水平扩展能力)、一致性(数据正确性保证)。面试时需结合具体场景展开。

Q2:WebSocket服务器在大规模场景下的主要挑战是什么?

  1. 性能瓶颈:随着数据量和请求量增长,单节点无法承载;2) 一致性:分布式环境下的数据一致性保证;3) 故障恢复:节点故障时的自动切换和数据恢复;4) 运维复杂度:集群管理、监控、升级。

Q3:如何保证WebSocket服务器的高可用?

  1. 多副本冗余(至少 3 副本);2) 自动故障检测和切换(心跳 + 选主);3) 数据持久化和备份;4) 限流降级(防止雪崩);5) 多机房/多活部署。

Q4:WebSocket服务器的性能优化有哪些关键手段?

  1. 缓存(减少重复计算和 IO);2) 异步处理(非关键路径异步化);3) 批量操作(减少网络往返);4) 数据分片(并行处理);5) 连接池复用。

Q5:WebSocket服务器与同类方案相比有什么优劣势?

参考方案对比表格。选型时需考虑:团队技术栈、数据规模、延迟要求、一致性需求、运维成本。没有银弹,需根据业务场景权衡取舍。



| 方案一 | 简单实现 | 低 | 适合小规模 | | 方案二 | 中等复杂度 | 中 | 适合中等规模 | | 方案三 | 高复杂度 ⭐推荐 | 高 | 适合大规模生产环境 |

🚀 架构演进路径

阶段一:单机版 MVP(用户量 < 10 万)

  • 单体应用 + 单机数据库
  • 功能验证优先,快速迭代
  • 适用场景:产品早期验证

阶段二:基础版分布式(用户量 10 万 - 100 万)

  • 应用层水平扩展(无状态服务 + 负载均衡)
  • 数据库主从分离(读写分离)
  • 引入 Redis 缓存热点数据
  • 适用场景:业务增长期

阶段三:生产级高可用(用户量 > 100 万)

  • 微服务拆分,独立部署和扩缩容
  • 数据库分库分表(按业务维度分片)
  • 引入消息队列解耦异步流程
  • 多机房部署,异地容灾
  • 全链路监控 + 自动化运维

✅ 架构设计检查清单

检查项状态说明
高可用多副本部署,自动故障转移,99.9% SLA
可扩展无状态服务水平扩展,数据层分片
数据一致性核心路径强一致,非核心最终一致
安全防护认证授权 + 加密 + 审计日志
监控告警Metrics + Logging + Tracing 三支柱
容灾备份多机房部署,定期备份,RPO < 1 分钟
性能优化多级缓存 + 异步处理 + 连接池
灰度发布支持按用户/地域灰度,快速回滚

⚖️ 关键 Trade-off 分析

🔴 Trade-off 1:一致性 vs 可用性

  • 强一致(CP):适用于金融交易等不能出错的场景
  • 高可用(AP):适用于社交动态等允许短暂不一致的场景
  • 本系统选择:核心路径强一致,非核心路径最终一致

🔴 Trade-off 2:同步 vs 异步

  • 同步处理:延迟低但吞吐受限,适用于核心交互路径
  • 异步处理:吞吐高但增加延迟,适用于后台计算
  • 本系统选择:核心路径同步,非核心路径异步