11 KiB
11 KiB
Tài Liệu Kiến Trúc Chat Service
Kiến trúc chi tiết cho Chat Service với SignalR, scalability patterns, và AI integration.
Tổng Quan Kiến Trúc
graph TB
subgraph "Clients"
WEB[Web App]
MOB[Mobile App]
BOT[Bot Client]
end
subgraph "Load Balancer"
LB[NGINX/Traefik]
SS[Sticky Sessions]
end
subgraph "Chat Service Instances"
CS1[Instance 1<br/>SignalR Hub]
CS2[Instance 2<br/>SignalR Hub]
CS3[Instance 3<br/>SignalR Hub]
end
subgraph "Backplane"
RD[(Redis<br/>Pub/Sub)]
end
subgraph "Storage"
PG[(PostgreSQL<br/>Messages)]
RC[(Redis<br/>Cache)]
end
subgraph "AI Service"
AI[OpenAI<br/>GPT-4]
end
WEB & MOB & BOT --> LB
LB --> SS --> CS1 & CS2 & CS3
CS1 & CS2 & CS3 <--> RD
CS1 & CS2 & CS3 --> PG
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
style LB fill:#3498db,stroke:#2980b9,color:#fff
style RD fill:#e74c3c,stroke:#c0392b,color:#fff
style PG fill:#27ae60,stroke:#1e8449,color:#fff
style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
SignalR Hub Architecture
Connection Lifecycle
sequenceDiagram
participant Client
participant Hub as ChatHub
participant Groups
participant DB as Database
participant Redis
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
Hub->>DB: Load user rooms
Hub->>Groups: AddToGroupAsync(roomId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
Note over Client,Hub: User is now online
Client->>Hub: SendMessage(roomId, content)
Hub->>DB: Save message
Hub->>Redis: Publish(NewMessage)
Redis-->>Hub: Broadcast to all instances
Hub->>Groups: Clients.Group(roomId)
Hub-->>Client: ReceiveMessage
Hub Implementation
public class ChatHub : Hub<IChatClient>
{
private readonly IChatRoomRepository _roomRepository;
private readonly IMessageRepository _messageRepository;
private readonly IAIService _aiService;
public async Task JoinRoom(Guid roomId)
{
var userId = Context.UserIdentifier;
// Thêm vào SignalR Group
await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
// Thông báo cho members khác
await Clients.Group(roomId.ToString())
.UserJoined(userId, roomId);
// Lưu trạng thái vào DB
await _roomRepository.AddParticipantAsync(roomId, userId);
}
public async Task SendMessage(Guid roomId, string content)
{
var message = new Message(roomId, Context.UserIdentifier, content);
await _messageRepository.AddAsync(message);
// Broadcast tin nhắn
await Clients.Group(roomId.ToString())
.ReceiveMessage(message.ToDto());
// Kiểm tra AI trigger
if (content.StartsWith("@gpt "))
{
await StreamAIResponse(roomId, content[5..]);
}
}
public async IAsyncEnumerable<string> StreamAIResponse(
Guid roomId,
string prompt,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Lấy lịch sử chat cho context
var history = await _messageRepository.GetHistoryAsync(roomId, 20);
await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
{
yield return chunk;
}
}
}
Scalability Architecture
Redis Backplane
graph LR
subgraph "Instance 1"
C1[Client A] --> H1[Hub 1]
end
subgraph "Instance 2"
C2[Client B] --> H2[Hub 2]
end
subgraph "Redis"
CH[Channel: chat.messages]
end
H1 -->|Publish| CH
CH -->|Subscribe| H2
H2 -->|Deliver| C2
style CH fill:#e74c3c,stroke:#c0392b,color:#fff
Cấu Hình Scaling
| Option | Ưu điểm | Nhược điểm |
|---|---|---|
| Redis Backplane | Đơn giản, on-premise | Cần quản lý Redis cluster |
| Azure SignalR | Serverless, không sticky sessions | Vendor lock-in, chi phí |
| Sticky Sessions | Đơn giản nhất | Không hoàn hảo cho failover |
// Redis Backplane Configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ChannelPrefix =
RedisChannel.Literal("ChatService");
options.Configuration.AbortOnConnectFail = false;
});
// Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = azureSignalRConnectionString;
options.ServerStickyMode = ServerStickyMode.Required;
});
User & Group Management
User Mapping
graph TB
subgraph "User Mapping"
U1[User ID: user-123]
C1[Connection 1<br/>Phone]
C2[Connection 2<br/>Laptop]
C3[Connection 3<br/>Tablet]
end
U1 --> C1 & C2 & C3
style U1 fill:#3498db,stroke:#2980b9,color:#fff
IUserIdProvider
public class ClaimsUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
// Lấy User ID từ JWT Claims
return connection.User?
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
}
// Registration
builder.Services.AddSingleton<IUserIdProvider, ClaimsUserIdProvider>();
Group State Management
// Vấn đề: SignalR Groups không persist khi restart
// Giải pháp: Lưu trữ trong database
public class GroupStateService
{
private readonly IDistributedCache _cache;
private readonly IChatRoomRepository _repository;
public async Task RestoreUserGroups(string userId, string connectionId)
{
var rooms = await _repository.GetUserRoomsAsync(userId);
foreach (var room in rooms)
{
await _hubContext.Groups.AddToGroupAsync(
connectionId,
room.Id.ToString()
);
}
}
}
AI Integration Flow
sequenceDiagram
participant User
participant Hub as ChatHub
participant AI as AIService
participant OpenAI
participant DB as Database
User->>Hub: "@gpt Giải thích DDD"
Hub->>DB: Load history (20 messages)
Hub->>AI: StreamAsync(prompt, history)
AI->>OpenAI: ChatCompletion (stream: true)
loop Streaming
OpenAI-->>AI: Token chunk
AI-->>Hub: yield chunk
Hub-->>User: ReceiveAIChunk
end
Hub->>DB: Save AI response
Hub-->>User: AIResponseComplete
Domain Model
Aggregate Roots
erDiagram
ChatRoom ||--o{ Message : contains
ChatRoom ||--o{ Participant : has
ChatRoom {
uuid id PK
string name
enum type
uuid owner_id
timestamp created_at
}
Message {
uuid id PK
uuid room_id FK
uuid sender_id
string content
enum type
timestamp sent_at
}
Participant {
uuid id PK
uuid room_id FK
uuid user_id
enum role
timestamp joined_at
timestamp last_read
}
Domain Events
| Event | Trigger | Handler |
|---|---|---|
MessageSentEvent |
Tin nhắn mới | Update last_activity, broadcast |
UserJoinedRoomEvent |
User join room | Notify members |
UserLeftRoomEvent |
User leave room | Notify members |
RoomCreatedEvent |
Tạo room mới | Add creator as admin |
MessagePack Protocol
So sánh JSON vs MessagePack
| Metric | JSON | MessagePack |
|---|---|---|
| Size | 100% | ~50-70% |
| Parse time | 1x | 0.5-0.8x |
| Type safety | Weak | Strong |
Cấu Hình
// Server
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData)
.WithCompression(MessagePackCompression.Lz4BlockArray);
});
Resiliency Patterns
Automatic Reconnect
stateDiagram-v2
[*] --> Connected
Connected --> Disconnected: Connection lost
Disconnected --> Reconnecting: Auto retry
Reconnecting --> Connected: Success
Reconnecting --> Reconnecting: Retry with backoff
Reconnecting --> Disconnected: Max retries exceeded
Disconnected --> [*]: User logout
Stateful Reconnect (.NET 8+)
// Server configuration
builder.Services.AddSignalR(options =>
{
options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
});
// Với UseStatefulReconnect
app.MapHub<ChatHub>("/chatHub", options =>
{
options.AllowStatefulReconnects = true;
});
Deployment Architecture
Kubernetes với Sticky Sessions
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatservice
spec:
replicas: 3
template:
spec:
containers:
- name: chatservice
image: chatservice:latest
ports:
- containerPort: 8080
env:
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: chat-secrets
key: redis-url
---
apiVersion: v1
kind: Service
metadata:
name: chatservice
annotations:
service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
spec:
type: LoadBalancer
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600
ports:
- port: 80
targetPort: 8080
Ingress với WebSocket Support
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: chatservice
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
nginx.ingress.kubernetes.io/affinity: "cookie"
spec:
rules:
- host: chat.goodgo.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: chatservice
port:
number: 80
Health Checks
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString, name: "postgresql")
.AddRedis(redisConnectionString, name: "redis")
.AddSignalRHub<ChatHub>(name: "signalr-hub");