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:
- architecture/gochat.png
- architecture/gochat_discovery.png
- architecture/timing.png
- main.go
- api/rpc/rpc.go
- api/handler/push.go
- task/rpc.go
- logic/rpc.go
- task/push.go
- connect/rpc.go
- go.mod
- Makefile
- docker/Dockerfile
- architecture/wx.jpg
- architecture/hash.png
- architecture/gochat.gif
- architecture/session.png
- architecture/support.png
- architecture/gochat-new.png
- architecture/gochat_tcp.gif
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.
go1// 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:
- 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ụ
- Service Discovery: Sử dụng etcd để các service có thể tìm thấy nhau động
- Redis làm trung gian: Tin nhắn được đệm trong Redis trước khi được đẩy đến người nhận
- Task module độc lập: Xử lý việc đẩy tin nhắn không đồng bộ, giảm tải cho logic layer
- 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.
go1// 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òngGetAllConnectTypeRpcClient()(task/rpc.go:59-69): Lấy tất cả client đang hoạt động để broadcastwatchServicesChange()(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à:
- GetRoomInfo: Lấy thông tin phòng từ Redis và đẩy lại cho các thành viên
- Connect: Xác thực người dùng dựa trên authToken và đăng ký vào phòng
go1// 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:
| Operation | Mô tả | Xử lý |
|---|---|---|
OpSingleSend | Gửi tin nhắn riêng | Push qua channel đến serverId cụ thể |
OpRoomSend | Broadcast phòng | Gửi đến tất cả connect server |
OpRoomCountSend | Cập nhật số lượng phòng | Broadcast count đến tất cả server |
OpRoomInfoSend | Cập nhật thông tin phòng | Broadcast room user info |
go1// 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:
go1// 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:
go1func (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:
- Client gửi POST request đến
/api/room/info - API handler parse JSON body lấy roomId
- Handler gọi RPC đến logic layer
- Logic layer truy vấn Redis và trả về thông tin phòng
- 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:
- Kết nối: Client thiết lập WebSocket/TCP đến connect server
- Xác thực: Connect server gọi RPC đến logic layer với authToken
- Lấy session: Logic layer truy vấn Redis để lấy thông tin user
- Đăng ký phòng: User được thêm vào danh sách phòng trong Redis
- Gửi tin nhắn: Client gửi tin nhắn qua connection
- Queue tin nhắn: Logic layer đẩy tin nhắn vào Redis queue
- Xử lý task: Task module lấy tin nhắn từ queue
- Khám phá server: Task query etcd để lấy danh sách connect server
- 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 đích | Lý do chọn | Giải pháp thay thế |
|---|---|---|---|
| Go | Ngôn ngữ chính | Goroutine cho concurrent, hiệu năng cao | Rust, Java |
| Gin | HTTP framework | Nhẹ, nhanh, middleware ecosystem | Echo, Fiber |
| rpcx | RPC framework | Hỗ trợ service discovery, load balancing | gRPC, Twirp |
| etcd v3 | Service discovery | Consistency, watch API | Consul, ZooKeeper |
| Redis | Cache + Message Queue | Hiệu năng, đa năng | Memcached + Kafka |
| WebSocket | Real-time communication | Native browser support | Server-Sent Events |
| TCP | Mobile/IoT connection | Hiệu năng, control | QUIC |
| Docker | Containerization | Consistency, deployment | Podman |
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:
yaml1# 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:
- Khởi động etcd cluster
- Khởi động Redis
- Chạy
logicmodule trước (xử lý nghiệp vụ) - Chạy
connect_websocketvàconnect_tcp(đăng ký với etcd) - Chạy
taskmodule (khám phá connect servers) - Chạy
apimodule (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.
