Bảng giá

Tổng quan kiến trúc

Tệp nguồn liên quan

Trang này được tạo dựa trên các tệp nguồn sau:

Dự án gochat là một hệ thống chat thời gian thực được xây dựng theo kiến trúc vi dịch vụ, sử dụng ngôn ngữ Go. Hệ thống được thiết kế để hỗ trợ cả WebSocket và TCP, cho phép mở rộng quy mô và chịu lỗi cao. Kiến trúc phân tách rõ ràng giữa các tầng kết nối, logic nghiệp vụ và xử lý tác vụ, sử dụng etcd để khám phá dịch vụ và Redis làm bộ đệm tin nhắn.

Tổng quan về các thành phần hệ thống

Hệ thống gochat được tổ chức thành các module độc lập, mỗi module chịu trách nhiệm cho một chức năng cụ thể. Điểm khởi đầu của ứng dụng nằm ở file main.go, nơi điều hướng đến các module dựa trên tham số dòng lệnh.

go
1// Ví dụ khởi chạy module
2go run main.go -module logic
3go run main.go -module connect_websocket
4go run main.go -module task

Các module chính bao gồm:

  • logic: Xử lý logic nghiệp vụ và xác thực người dùng
  • connect_websocket/connect_tcp: Quản lý kết nối WebSocket/TCP với client
  • task: Xử lý đẩy tin nhắn và broadcast
  • api: Cung cấp REST API cho các thao tác bên ngoài
  • site: Giao diện web (nếu có)

Theo main.go:20-48, hàm main() sử dụng flag module để xác định module nào sẽ được khởi chạy. Mỗi module có hàm khởi tạo riêng (ví dụ logic.New().Run(), connect.New().Run()) và hệ thống sẽ chờ tín hiệu dừng (SIGINT, SIGTERM) trước khi thoát.

Sơ đồ kiến trúc tổng thể được minh họa trong architecture/gochat.png:1-801, cho thấy sự tương tác giữa các thành phần:

正在加载图表渲染器...

Điểm chính về kiến trúc:

  1. Tách biệt tầng kết nối: Module connect xử lý giao thức WebSocket/TCP, tách biệt với logic nghiệp vụ
  2. Service Discovery: Sử dụng etcd để các service có thể tìm thấy nhau động
  3. Redis làm trung gian: Tin nhắn được đệm trong Redis trước khi được đẩy đến người nhận
  4. Task module độc lập: Xử lý việc đẩy tin nhắn không đồng bộ, giảm tải cho logic layer
  5. API REST riêng biệt: Cung cấp giao diện HTTP cho các tích hợp bên ngoài

Dịch vụ Discovery và RPC

Hệ thống sử dụng etcd làm trung tâm khám phá dịch vụ (service discovery). Khi một connect server khởi động, nó đăng ký địa chỉ với etcd, cho phép các module khác (như task) tìm và gọi RPC đến nó.

Cơ chế RPC được xây dựng dựa trên thư viện rpcx, hỗ trợ nhiều chế độ thất bại (Failtry) và chiến lược chọn lựa (RandomSelect). Theo task/rpc.go:41-149, RpcConnectClient quản lý một map các client RPC được nhóm theo serverId, sử dụng round-robin để cân bằng tải giữa các instance.

go
1// Cấu trúc Instance trong task/rpc.go
2type Instance struct {
3    ServerType string
4    ServerId   string
5    Client     client.XClient
6}

Hàm InitConnectRpcClient() khởi tạo kết nối đến etcd và theo dõi thay đổi dịch vụ thông qua WatchService(). Khi có service mới đăng ký hoặc service cũ ngừng hoạt động, map ServerInsMap được cập nhật động.

Sơ đồ architecture/gochat_discovery.png:1-977 minh họa chi tiết cơ chế này:

正在加载图表渲染器...

Chi tiết triển khai:

  • GetRpcClientByServerId() (task/rpc.go:42-57): Trả về RPC client dựa trên serverId, sử dụng index map để xoay vòng
  • GetAllConnectTypeRpcClient() (task/rpc.go:59-69): Lấy tất cả client đang hoạt động để broadcast
  • watchServicesChange() (task/rpc.go:111-149): Lắng nghe thay đổi từ etcd và cập nhật map client

Connect server khởi tạo RPC server tại connect/rpc.go:80-198, cho phép các module khác gọi các phương thức như push tin nhắn đến client đang kết nối.

Luồng xử lý Logic và Task

Logic Module

Logic module là trung tâm xử lý nghiệp vụ, bao gồm xác thực người dùng, quản lý phòng và xử lý các yêu cầu RPC từ các module khác.

Theo logic/rpc.go:266-305, hai phương thức RPC chính là:

  1. GetRoomInfo: Lấy thông tin phòng từ Redis và đẩy lại cho các thành viên
  2. Connect: Xác thực người dùng dựa trên authToken và đăng ký vào phòng
go
1// Luồng xử lý Connect (logic/rpc.go:285-305)
2func (rpc *RpcLogic) Connect(ctx context.Context, args *proto.ConnectRequest, reply *proto.ConnectReply) {
3    // 1. Lấy session từ Redis bằng authToken
4    key := tools.GetSessionName(args.AuthToken)
5    userInfo, _ := RedisClient.HGetAll(key).Result()
6    
7    // 2. Trích xuất userId từ session
8    reply.UserId, _ = strconv.Atoi(userInfo["userId"])
9    
10    // 3. Đăng ký user vào phòng (lưu vào Redis)
11    roomUserKey := logic.getRoomUserKey(strconv.Itoa(args.RoomId))
12    // ... lưu thông tin user vào phòng
13}

Cấu trúc dữ liệu Redis:

  • Session key: session:{authToken} → Hash chứa userId, username, etc.
  • Room users key: room:{roomId}:users → Hash chứa danh sách user trong phòng

Task Module

Task module chịu trách nhiệm nhận tin nhắn từ Redis channel và đẩy đến các connect server phù hợp. Theo task/push.go:35-65, có 4 loại operation:

OperationMô tảXử lý
OpSingleSendGửi tin nhắn riêngPush qua channel đến serverId cụ thể
OpRoomSendBroadcast phòngGửi đến tất cả connect server
OpRoomCountSendCập nhật số lượng phòngBroadcast count đến tất cả server
OpRoomInfoSendCập nhật thông tin phòngBroadcast room user info
go
1// Xử lý push message (task/push.go:45-65)
2func (task *Task) Push(msg string) {
3    m := &proto.RedisMsg{}
4    json.Unmarshal([]byte(msg), m)
5    
6    switch m.Op {
7    case config.OpSingleSend:
8        // Push đến channel cụ thể
9        pushChannel[rand.Int()%len(pushChannel)] <- &PushParams{
10            ServerId: m.ServerId,
11            UserId:   m.UserId,
12            Msg:      m.Msg,
13        }
14    case config.OpRoomSend:
15        // Broadcast đến tất cả connect server
16        task.broadcastRoomToConnect(m.RoomId, m.Msg)
17    }
18}

Hàm processSinglePush() (task/push.go:36-43) chạy trong goroutine riêng, liên tục đọc từ channel và gọi pushSingleToConnect() để gửi tin nhắn đến connect server.

API Handler và tích hợp

API module cung cấp REST endpoint để tích hợp với hệ thống bên ngoài. Theo api/handler/push.go:130-149, endpoint GetRoomInfo cho phép lấy thông tin phòng:

go
1// Handler GetRoomInfo (api/handler/push.go:131-149)
2func GetRoomInfo(c *gin.Context) {
3    var formRoomInfo FormRoomInfo
4    c.ShouldBindBodyWith(&formRoomInfo, binding.JSON)
5    
6    req := &proto.Send{
7        RoomId: formRoomInfo.RoomId,
8        Op:     config.OpRoomInfoSend,
9    }
10    
11    code, msg := rpc.RpcLogicObj.GetRoomInfo(req)
12    if code == tools.CodeFail {
13        tools.FailWithMsg(c, "rpc get room info fail!")
14        return
15    }
16    tools.SuccessWithMsg(c, "ok", msg)
17}

RPC client được định nghĩa tại api/rpc/rpc.go:127-134, gọi đến logic layer thông qua phương thức GetRoomInfo:

go
1func (rpc *RpcLogic) GetRoomInfo(req *proto.Send) (code int, msg string) {
2    reply := &proto.SuccessReply{}
3    LogicRpcClient.Call(context.Background(), "GetRoomInfo", req, reply)
4    return reply.Code, reply.Msg
5}

Luồng gọi API:

  1. Client gửi POST request đến /api/room/info
  2. API handler parse JSON body lấy roomId
  3. Handler gọi RPC đến logic layer
  4. Logic layer truy vấn Redis và trả về thông tin phòng
  5. API trả response JSON cho client

Sơ đồ Timing và luồng giao tiếp

Sơ đồ timing tại architecture/timing.png:1-986 minh họa trình tự giao tiếp giữa các thành phần khi người dùng tham gia phòng và gửi tin nhắn:

正在加载图表渲染器...

Giải thích các bước:

  1. Kết nối: Client thiết lập WebSocket/TCP đến connect server
  2. Xác thực: Connect server gọi RPC đến logic layer với authToken
  3. Lấy session: Logic layer truy vấn Redis để lấy thông tin user
  4. Đăng ký phòng: User được thêm vào danh sách phòng trong Redis
  5. Gửi tin nhắn: Client gửi tin nhắn qua connection
  6. Queue tin nhắn: Logic layer đẩy tin nhắn vào Redis queue
  7. Xử lý task: Task module lấy tin nhắn từ queue
  8. Khám phá server: Task query etcd để lấy danh sách connect server
  9. Phân phối: Task gọi RPC đến các connect server để đẩy tin nhắn

Các quyết định thiết kế cốt lõi

1. Kiến trúc vi dịch vụ tách biệt

Lý do: Cho phép mở rộng độc lập từng tầng. Connect layer có thể scale theo số lượng connection, task layer scale theo throughput tin nhắn.

Trade-off: Tăng độ phức tạp vận hành, yêu cầu service discovery và monitoring phức tạp hơn.

2. Sử dụng Redis làm message queue

Lý do: Đơn giản, hiệu năng cao, đã có sẵn cho caching. Không cần thêm Kafka/RabbitMQ.

Trade-off: Không đảm bảo exactly-once delivery, cần xử lý duplicate ở application layer.

3. etcd cho service discovery

Lý do: Consistency mạnh, hỗ trợ watch API, tích hợp tốt với Go ecosystem.

Trade-off: Single point of failure nếu không cấu hình cluster, thêm latency cho service lookup.

4. RPC đồng bộ giữa các service

Lý do: Đơn giản để implement, phù hợp với request-response pattern.

Trade-off: Tight coupling, cần circuit breaker và timeout handling cẩn thận.

5. Channel-based push trong Task module

Lý do: Non-blocking, tận dụng goroutine, tự nhiên trong Go.

Trade-off: Cần quản lý channel buffer size, risk of blocking nếu consumer chậm.

Bảng công nghệ sử dụng

Công nghệMục đíchLý do chọnGiải pháp thay thế
GoNgôn ngữ chínhGoroutine cho concurrent, hiệu năng caoRust, Java
GinHTTP frameworkNhẹ, nhanh, middleware ecosystemEcho, Fiber
rpcxRPC frameworkHỗ trợ service discovery, load balancinggRPC, Twirp
etcd v3Service discoveryConsistency, watch APIConsul, ZooKeeper
RedisCache + Message QueueHiệu năng, đa năngMemcached + Kafka
WebSocketReal-time communicationNative browser supportServer-Sent Events
TCPMobile/IoT connectionHiệu năng, controlQUIC
DockerContainerizationConsistency, deploymentPodman

Sơ đồ phụ thuộc module

正在加载图表渲染器...

Phân tích phụ thuộc:

  • main.go: Khởi tạo tất cả module, không có phụ thuộc nghiệp vụ
  • logic: Phụ thuộc Redis để lưu session và room info
  • connect: Đăng ký với etcd, gọi RPC đến logic
  • task: Phụ thuộc Redis để đọc queue, etcd để discover connect servers
  • api: Gọi RPC đến logic layer, không truy cập trực tiếp Redis

Cấu hình và khởi động

Hệ thống sử dụng file cấu hình (thường ở config/ hoặc environment variables) với các section chính:

yaml
1# Ví dụ cấu hình (cần xác nhận từ config file thực tế)
2Common:
3  CommonEtcd:
4    Host: "localhost:2379"
5    BasePath: "/gochat"
6    ServerPathConnect: "connect"
7    ConnectionTimeout: 5
8
9Connect:
10  ConnectRpcAddressWebsockts:
11    Address: "tcp@localhost:6979"
12
13Task:
14  TaskBase:
15    PushChan: 10  # Số channel push song song

Quy trình khởi động:

  1. Khởi động etcd cluster
  2. Khởi động Redis
  3. Chạy logic module trước (xử lý nghiệp vụ)
  4. Chạy connect_websocketconnect_tcp (đăng ký với etcd)
  5. Chạy task module (khám phá connect servers)
  6. Chạy api module (REST endpoint)

Thứ tự này đảm bảo các dependency sẵn sàng trước khi module cần nó khởi động.