chore: adjust player tank's size

This commit is contained in:
jakciehan
2026-05-02 13:50:52 +08:00
parent 0e321bcea6
commit 38294c040c
35 changed files with 5767 additions and 348 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
npm-debug.log
.DS_Store
.git
.gitignore
Dockerfile
.dockerignore
*.md
+256
View File
@@ -0,0 +1,256 @@
# Tank War Server - 完整部署指南
## 概述
本指南详细说明如何将Tank War Server部署到由3台CVM组成的Kubernetes集群中。
## 集群信息
目标K8s集群由以下3台CVM组成:
- 43.139.80.61 (host_172.16.16.16)
- 43.138.255.42 (host_172.16.16.17)
- 159.75.104.221 (host_172.16.16.8)
SSH连接:`ssh root@host_172.16.16.16`
## 部署前准备
### 1. 环境检查
```bash
# 在server目录下运行测试脚本
cd /Users/hanchengxi/workspace/tankwar_proj/server
./test-deployment.sh
```
### 2. 配置Kubernetes访问
确保kubectl已配置连接到目标集群:
```bash
# 检查集群连接
kubectl cluster-info
# 查看当前上下文
kubectl config current-context
# 如果未配置,需要获取集群的kubeconfig文件
# 通常从集群管理员处获取或通过云平台控制台下载
```
## 部署步骤
### 步骤1:构建Docker镜像
```bash
# 在server目录下构建镜像
docker build -t tankwar-server:latest .
# 验证镜像构建成功
docker images | grep tankwar-server
```
### 步骤2:部署到Kubernetes
```bash
# 方法1:使用部署脚本(推荐)
chmod +x deploy.sh
./deploy.sh
# 方法2:手动部署
kubectl create namespace tankwar --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f k8s-deployment.yaml -n tankwar
```
### 步骤3:验证部署
```bash
# 运行验证脚本
chmod +x verify-deployment.sh
./verify-deployment.sh
# 或手动验证
kubectl get all -n tankwar
kubectl logs -l app=tankwar-server -n tankwar
```
## 配置文件说明
### Dockerfile
- 基于Node.js 18 Alpine镜像
- 暴露端口3000
- 生产环境配置
### k8s-deployment.yaml
包含以下Kubernetes资源:
1. **ConfigMap**: 环境变量配置
2. **Deployment**:
- 3个副本
- 资源限制:内存512MiCPU 500m
- 健康检查探针
3. **Service**:
- LoadBalancer类型
- 端口3000
## 网络配置
### 服务暴露
服务使用LoadBalancer类型,将通过云平台的负载均衡器暴露:
```bash
# 获取外部IP
kubectl get svc tankwar-server-service -n tankwar
# WebSocket连接地址
ws://<external-ip>:3000
```
### 端口映射
- 容器端口:3000
- 服务端口:3000
- 外部访问端口:3000
## 健康检查
服务器提供HTTP健康检查端点:
```bash
# 健康检查URL
http://<external-ip>:3000/health
# 返回JSON格式的健康状态
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00.000Z",
"activeConnections": 0,
"activeRooms": 0,
"activeTeamRooms": 0
}
```
## 监控和维护
### 查看状态
```bash
# 查看Pod状态
kubectl get pods -n tankwar -w
# 查看服务状态
kubectl get svc -n tankwar
# 查看日志
kubectl logs -l app=tankwar-server -n tankwar --tail=50
# 查看资源使用
kubectl top pods -n tankwar
```
### 扩展和伸缩
```bash
# 扩展副本数量
kubectl scale deployment/tankwar-server --replicas=5 -n tankwar
# 自动伸缩(如果配置了HPA
kubectl autoscale deployment/tankwar-server --min=3 --max=10 --cpu-percent=80 -n tankwar
```
### 更新部署
```bash
# 重新构建镜像
docker build -t tankwar-server:latest .
# 滚动更新
kubectl rollout restart deployment/tankwar-server -n tankwar
# 查看更新状态
kubectl rollout status deployment/tankwar-server -n tankwar
```
## 故障排除
### 常见问题
1. **镜像构建失败**
```bash
# 检查Docker守护进程
docker info
# 检查Dockerfile语法
docker build --no-cache -t tankwar-server:latest .
```
2. **Pod无法启动**
```bash
# 查看Pod详情
kubectl describe pod <pod-name> -n tankwar
# 查看事件
kubectl get events -n tankwar
```
3. **服务无法访问**
```bash
# 检查服务端点
kubectl get endpoints tankwar-server-service -n tankwar
# 检查网络策略
kubectl get networkpolicies -n tankwar
```
4. **健康检查失败**
```bash
# 检查Pod内部
kubectl exec -it <pod-name> -n tankwar -- wget -qO- http://localhost:3000/health
```
### 调试命令
```bash
# 进入Pod调试
kubectl exec -it <pod-name> -n tankwar -- /bin/sh
# 端口转发本地调试
kubectl port-forward svc/tankwar-server-service 3000:3000 -n tankwar
# 然后访问:http://localhost:3000/health
```
## 清理部署
```bash
# 删除部署
kubectl delete -f k8s-deployment.yaml -n tankwar
# 删除命名空间
kubectl delete namespace tankwar
# 清理镜像
docker rmi tankwar-server:latest
```
## 安全考虑
1. **网络策略**: 配置适当的网络策略限制访问
2. **资源限制**: 设置合理的资源限制防止资源耗尽
3. **镜像安全**: 定期更新基础镜像修复安全漏洞
4. **访问控制**: 配置RBAC权限控制
## 性能优化建议
1. **副本数量**: 根据负载调整副本数量
2. **资源分配**: 根据实际使用情况调整资源限制
3. **连接池**: 考虑使用连接池优化WebSocket连接
4. **监控告警**: 配置监控和告警系统
## 支持信息
如有问题,请检查:
- 服务器日志:`kubectl logs -l app=tankwar-server -n tankwar`
- 部署状态:`kubectl get all -n tankwar`
- 集群状态:`kubectl cluster-info`
+25
View File
@@ -0,0 +1,25 @@
FROM node:18-alpine
LABEL maintainer="tankwar" \
description="Tank War PVP WebSocket Server"
WORKDIR /app
# Install dependencies first (leverage Docker layer cache)
COPY package.json package-lock.json* ./
RUN npm install --omit=dev --no-audit --no-fund \
&& npm cache clean --force
# Copy the rest of the source
COPY . .
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3000
EXPOSE 3000
# Graceful shutdown & init for PID 1
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "index.js"]
+183
View File
@@ -0,0 +1,183 @@
# Tank War Server - Kubernetes部署指南
## 概述
Tank War Server是一个基于WebSocket的多人坦克对战游戏服务器,支持1v1和3v3对战模式。本文档说明如何将服务器部署到Kubernetes集群中。
## 前置要求
- Docker
- Kubernetes集群访问权限
- kubectl命令行工具
- 对目标K8s集群的访问配置
## 部署步骤
### 1. 准备环境
确保您已配置好对目标Kubernetes集群的访问:
```bash
# 检查集群连接
kubectl cluster-info
# 查看当前上下文
kubectl config current-context
```
### 2. 构建Docker镜像
```bash
# 在server目录下构建镜像
cd server
docker build -t tankwar-server:latest .
```
### 3. 部署到Kubernetes
使用提供的部署脚本:
```bash
# 给脚本执行权限
chmod +x deploy.sh
# 执行部署
./deploy.sh
```
或者手动部署:
```bash
# 创建命名空间
kubectl create namespace tankwar
# 部署应用
kubectl apply -f k8s-deployment.yaml -n tankwar
# 等待部署完成
kubectl rollout status deployment/tankwar-server -n tankwar
```
### 4. 验证部署
```bash
# 查看Pod状态
kubectl get pods -n tankwar
# 查看服务信息
kubectl get svc -n tankwar
# 查看日志
kubectl logs -l app=tankwar-server -n tankwar
```
## 配置说明
### 环境变量
- `PORT`: 服务器端口(默认:3000
- `HOST`: 绑定地址(默认:0.0.0.0
- `NODE_ENV`: 运行环境(默认:production
### 资源限制
- 内存请求:256Mi,限制:512Mi
- CPU请求:250m,限制:500m
### 健康检查
服务器提供健康检查端点:
- URL: `/health`
- 返回JSON格式的健康状态信息
## 网络配置
服务使用LoadBalancer类型暴露,可以通过外部IP访问。WebSocket连接地址格式:
```
ws://<external-ip>:3000
```
## 监控和日志
### 查看日志
```bash
# 查看所有Pod日志
kubectl logs -l app=tankwar-server -n tankwar
# 查看特定Pod日志
kubectl logs <pod-name> -n tankwar
```
### 扩展和伸缩
```bash
# 扩展副本数量
kubectl scale deployment/tankwar-server --replicas=5 -n tankwar
# 查看资源使用情况
kubectl top pods -n tankwar
```
## 故障排除
### 常见问题
1. **镜像构建失败**
- 检查Docker守护进程是否运行
- 确认Dockerfile语法正确
2. **部署失败**
- 检查kubectl集群连接
- 验证k8s-deployment.yaml文件语法
3. **Pod无法启动**
- 查看Pod事件:`kubectl describe pod <pod-name> -n tankwar`
- 检查资源配额是否足够
4. **连接问题**
- 确认服务已分配外部IP
- 检查防火墙规则
### 调试命令
```bash
# 查看Pod详细信息
kubectl describe pod -l app=tankwar-server -n tankwar
# 进入Pod调试
kubectl exec -it <pod-name> -n tankwar -- /bin/sh
# 查看服务端点
kubectl get endpoints tankwar-server-service -n tankwar
```
## 维护操作
### 更新部署
```bash
# 重新构建镜像
docker build -t tankwar-server:latest .
# 更新部署
kubectl rollout restart deployment/tankwar-server -n tankwar
```
### 清理部署
```bash
# 删除部署
kubectl delete -f k8s-deployment.yaml -n tankwar
# 删除命名空间
kubectl delete namespace tankwar
```
## 安全考虑
- 在生产环境中考虑使用Ingress控制器
- 配置适当的网络策略
- 定期更新镜像以修复安全漏洞
- 监控资源使用情况防止资源耗尽
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Tank War Server K8s部署脚本
set -e
# 配置变量
IMAGE_NAME="tankwar-server"
K8S_NAMESPACE="tankwar"
K8S_CONFIG="k8s-deployment.yaml"
# 检查Docker是否运行
if ! docker info > /dev/null 2>&1; then
echo "错误: Docker守护进程未运行"
exit 1
fi
# 构建Docker镜像
echo "构建Docker镜像..."
docker build -t $IMAGE_NAME:latest .
# 登录到目标K8s集群(假设已配置kubectl)
echo "检查Kubernetes集群连接..."
kubectl cluster-info
# 创建命名空间(如果不存在)
echo "创建命名空间..."
kubectl create namespace $K8S_NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# 部署应用到K8s
echo "部署应用到Kubernetes..."
kubectl apply -f $K8S_CONFIG -n $K8S_NAMESPACE
# 等待部署完成
echo "等待部署完成..."
kubectl rollout status deployment/tankwar-server -n $K8S_NAMESPACE --timeout=300s
# 获取服务信息
echo "获取服务信息..."
kubectl get svc tankwar-server-service -n $K8S_NAMESPACE
echo "部署完成!"
echo "使用以下命令查看Pod状态:"
echo "kubectl get pods -n $K8S_NAMESPACE"
echo ""
echo "使用以下命令查看日志:"
echo "kubectl logs -l app=tankwar-server -n $K8S_NAMESPACE"
+96 -22
View File
@@ -2,18 +2,48 @@
* Tank War PVP Server
* WebSocket server for online 1v1 multiplayer.
* Handles room management, message relay, and basic game state authority.
*
* Deployment note:
* - /health → HTTP health check (used by K8s livenessProbe/readinessProbe)
* - /tankwar/ws → WebSocket upgrade path (exposed publicly via Nginx)
* Both share the same HTTP server on PORT.
*/
const { WebSocketServer } = require('ws');
const http = require('http');
// ============================================================
// Configuration
// ============================================================
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const WS_PATH = process.env.WS_PATH || '/tankwar/ws';
const HEARTBEAT_INTERVAL = 10000; // ms
const ROOM_TIMEOUT = 300000; // 5 minutes room idle timeout
// ============================================================
// HTTP Health Check Server
// ============================================================
const healthServer = http.createServer((req, res) => {
if (req.url === '/health' || req.url === '/tankwar/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
activeConnections: players.size,
activeRooms: rooms.size,
activeTeamRooms: teamRooms.size
}));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
healthServer.listen(PORT, HOST, () => {
console.log(`[Health Server] Running on ${HOST}:${PORT}`);
});
// ============================================================
// Message Types (must match client NET_MSG)
// ============================================================
@@ -135,6 +165,7 @@ class PlayerInfo {
constructor(ws, playerId) {
this.ws = ws;
this.playerId = playerId;
this.nickname = '';
this.roomId = null;
this.teamId = null;
this.isAlive = true;
@@ -152,8 +183,9 @@ class TeamRoom {
* @param {WebSocket} leaderWs - WebSocket of the team leader
* @param {string} leaderId - Player id of the leader
* @param {string} [battleMode='3v3'] - Battle mode ('1v1' or '3v3')
* @param {string} [leaderNickname=''] - Display nickname of the leader
*/
constructor(id, leaderWs, leaderId, battleMode = '3v3') {
constructor(id, leaderWs, leaderId, battleMode = '3v3', leaderNickname = '') {
this.id = id;
this.state = 'forming'; // forming | matching | playing | finished
this.createdAt = Date.now();
@@ -164,8 +196,8 @@ class TeamRoom {
this.teamSize = config.teamSize;
this.fillWithBotsEnabled = config.fillWithBots;
// Team A members: { ws, playerId, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, ready: true, isBot: false, disconnectedAt: null }];
// Team A members: { ws, playerId, nickname, ready, isBot, disconnectedAt }
this.teamA = [{ ws: leaderWs, playerId: leaderId, nickname: leaderNickname || '', ready: true, isBot: false, disconnectedAt: null }];
// Team B members
this.teamB = [];
this.leaderId = leaderId;
@@ -233,16 +265,16 @@ class TeamRoom {
}
/** Add a player to team A */
addToTeamA(ws, playerId) {
addToTeamA(ws, playerId, nickname = '') {
if (this.isTeamAFull()) return false;
this.teamA.push({ ws, playerId, ready: false, isBot: false, disconnectedAt: null });
this.teamA.push({ ws, playerId, nickname, ready: false, isBot: false, disconnectedAt: null });
return true;
}
/** Add a player to team B */
addToTeamB(ws, playerId) {
addToTeamB(ws, playerId, nickname = '') {
if (this.teamB.length >= this.teamSize) return false;
this.teamB.push({ ws, playerId, ready: false, isBot: false, disconnectedAt: null });
this.teamB.push({ ws, playerId, nickname, ready: false, isBot: false, disconnectedAt: null });
return true;
}
@@ -260,6 +292,7 @@ class TeamRoom {
this.teamA.push({
ws: null,
playerId: `bot_a_${botCounter}_${this.id}`,
nickname: '',
ready: true,
isBot: true,
disconnectedAt: null,
@@ -270,6 +303,7 @@ class TeamRoom {
this.teamB.push({
ws: null,
playerId: `bot_b_${botCounter}_${this.id}`,
nickname: '',
ready: true,
isBot: true,
disconnectedAt: null,
@@ -335,6 +369,7 @@ class TeamRoom {
teamSize: this.teamSize,
teamA: this.teamA.map(m => ({
playerId: m.playerId,
nickname: m.nickname || '',
ready: m.ready,
isBot: m.isBot,
isLeader: m.playerId === this.leaderId,
@@ -342,6 +377,7 @@ class TeamRoom {
})),
teamB: this.teamB.map(m => ({
playerId: m.playerId,
nickname: m.nickname || '',
ready: m.ready,
isBot: m.isBot,
connected: m.isBot || (m.ws && m.ws.readyState === 1),
@@ -449,7 +485,7 @@ function handleCreateRoom(ws, data) {
const roomCode = generateRoomCode();
// Create a TeamRoom in 1v1 mode instead of a legacy Room
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1');
const teamRoom = new TeamRoom(roomCode, ws, playerInfo.playerId, '1v1', playerInfo.nickname || '');
teamRooms.set(roomCode, teamRoom);
playerInfo.teamId = roomCode;
@@ -498,7 +534,7 @@ function handleJoinRoom(ws, data) {
}
// Join as team B
teamRoom.addToTeamB(ws, playerInfo.playerId);
teamRoom.addToTeamB(ws, playerInfo.playerId, playerInfo.nickname || '');
playerInfo.teamId = roomId;
console.log(`[Server] Player ${playerInfo.playerId} joined 1v1 room ${roomId} (TeamRoom)`);
@@ -568,7 +604,7 @@ function handleCreateTeam(ws, data) {
}
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
@@ -592,7 +628,7 @@ function handleJoinTeam(ws, data) {
// Team was cleaned up (e.g. leader disconnected during dev-tool reload).
// Auto-create a new room with the same ID so the invite link still works.
console.log(`[Server] Team ${teamId} not found, auto-creating for ${playerInfo.playerId}`);
teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRooms.set(teamId, teamRoom);
playerInfo.teamId = teamId;
sendMessage(ws, NET_MSG.TEAM_STATE, teamRoom.getTeamState());
@@ -614,7 +650,7 @@ function handleJoinTeam(ws, data) {
handleLeaveTeam(ws, {});
}
teamRoom.addToTeamA(ws, playerInfo.playerId);
teamRoom.addToTeamA(ws, playerInfo.playerId, playerInfo.nickname || '');
playerInfo.teamId = teamId;
console.log(`[Server] Player ${playerInfo.playerId} joined team ${teamId}`);
@@ -815,7 +851,7 @@ function handleSoloMatch(ws, data) {
// Create a solo team room for this player
const teamId = generateTeamId();
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId);
const teamRoom = new TeamRoom(teamId, ws, playerInfo.playerId, '3v3', playerInfo.nickname || '');
teamRoom.state = 'matching';
teamRoom.matchStartTime = Date.now();
teamRooms.set(teamId, teamRoom);
@@ -888,7 +924,7 @@ function tryMatchTeams() {
// Merge team B members into team A room as opponents
for (const member of teamB_room.teamA) {
teamA_room.addToTeamB(member.ws, member.playerId);
teamA_room.addToTeamB(member.ws, member.playerId, member.nickname || '');
if (member.ws) {
const info = players.get(member.ws);
if (info) info.teamId = teamA_room.id;
@@ -953,9 +989,9 @@ function tryMatchTeams() {
// Alternate: odd index -> team A, even index -> team B
if (i % 2 === 1 && !gameRoom.isTeamAFull()) {
gameRoom.addToTeamA(ws, info.playerId);
gameRoom.addToTeamA(ws, info.playerId, info.nickname || '');
} else {
gameRoom.addToTeamB(ws, info.playerId);
gameRoom.addToTeamB(ws, info.playerId, info.nickname || '');
}
}
@@ -986,8 +1022,8 @@ function startTeamGame(teamRoom) {
const gameData = {
mapId: teamRoom.mapId,
teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, isBot: m.isBot })),
teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, isBot: m.isBot })),
teamA: teamRoom.teamA.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot })),
teamB: teamRoom.teamB.map(m => ({ playerId: m.playerId, nickname: m.nickname || '', isBot: m.isBot })),
teamABaseHp: teamRoom.teamABaseHp,
teamBBaseHp: teamRoom.teamBBaseHp,
battleMode: teamRoom.battleMode,
@@ -1306,7 +1342,7 @@ function handleMessage(ws, rawData) {
return;
}
const { type, data, playerId } = msg;
const { type, data, playerId, nickname } = msg;
// Update player info
const playerInfo = players.get(ws);
@@ -1315,6 +1351,26 @@ function handleMessage(ws, rawData) {
if (playerId && !playerInfo.playerId) {
playerInfo.playerId = playerId;
}
// Refresh nickname on every message (it may be granted mid-session).
if (typeof nickname === 'string' && nickname) {
if (playerInfo.nickname !== nickname) {
playerInfo.nickname = nickname;
// Also propagate into any active team room member entry.
if (playerInfo.teamId) {
const tr = teamRooms.get(playerInfo.teamId);
if (tr) {
const member = tr.getMemberByWs(ws);
if (member && member.nickname !== nickname) {
member.nickname = nickname;
// Broadcast regardless of room state (forming / matching / playing)
// so that peers always render the latest display name — in 3v3 a
// player may only tap the UserInfoButton AFTER the match starts.
tr.broadcast(NET_MSG.TEAM_STATE, tr.getTeamState());
}
}
}
}
}
}
switch (type) {
@@ -1538,9 +1594,26 @@ setInterval(() => {
}, 300000); // Every 5 minutes
// ============================================================
// WebSocket Server
// WebSocket Server (noServer mode, shares HTTP server with health check)
// ============================================================
const wss = new WebSocketServer({ host: HOST, port: PORT });
// Use noServer mode so the WS upgrade only fires on the configured path.
// This lets /health stay as plain HTTP on the same port.
const wss = new WebSocketServer({ noServer: true });
healthServer.on('upgrade', (req, socket, head) => {
// Only upgrade on the configured WebSocket path; reject any other path.
// We compare by pathname so query strings are tolerated.
const pathname = (req.url || '').split('?')[0];
if (pathname !== WS_PATH) {
console.warn(`[Server] Rejected WebSocket upgrade on unexpected path: ${req.url}`);
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
@@ -1604,4 +1677,5 @@ setInterval(() => {
// Startup
// ============================================================
console.log(`[Tank War Server] Running on ${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket URL: ws://${HOST}:${PORT}`);
console.log(`[Tank War Server] WebSocket path: ${WS_PATH}`);
console.log(`[Tank War Server] Health check paths: /health, /tankwar/health`);
+138
View File
@@ -0,0 +1,138 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: tankwar-server-config
data:
NODE_ENV: "production"
PORT: "3000"
HOST: "0.0.0.0"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tankwar-server
labels:
app: tankwar-server
spec:
replicas: 3
selector:
matchLabels:
app: tankwar-server
template:
metadata:
labels:
app: tankwar-server
spec:
containers:
- name: tankwar-server
image: tankwar-server:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: tankwar-server-config
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Namespace
metadata:
name: tankwar
labels:
app.kubernetes.io/part-of: tankwar
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tankwar-server-config
namespace: tankwar
data:
NODE_ENV: "production"
PORT: "3000"
HOST: "0.0.0.0"
# WebSocket path must match Nginx location and client SERVER_URL
WS_PATH: "/tankwar/ws"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tankwar-server
namespace: tankwar
labels:
app: tankwar-server
spec:
replicas: 3
selector:
matchLabels:
app: tankwar-server
template:
metadata:
labels:
app: tankwar-server
spec:
containers:
- name: tankwar-server
image: tankwar/tankwar-server:latest
imagePullPolicy: Never
ports:
- containerPort: 3000
name: http-ws
envFrom:
- configMapRef:
name: tankwar-server-config
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: tankwar-server
namespace: tankwar
labels:
app: tankwar-server
spec:
# ClusterIP: only exposed internally, external traffic comes through
# warmcheck-namespace Nginx at game.igeek.site -> /tankwar/ws
type: ClusterIP
selector:
app: tankwar-server
ports:
- name: http-ws
port: 3000
targetPort: 3000
protocol: TCP
type: LoadBalancer
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# ============================================================
# Tankwar server K8s deploy script
# Syncs server source -> Master node -> builds Docker image ->
# distributes image to Worker nodes via ctr -> applies K8s resources
# -> rolls out the tankwar-server deployment.
# ============================================================
set -e
LOG="/tmp/tankwar-k8s-deploy.log"
> "$LOG"
exec > >(tee -a "$LOG") 2>&1
SERVER_DIR="/Users/hanchengxi/workspace/tankwar_proj/server"
MASTER="root@host_172.16.16.16"
WORKERS_IP=("172.16.16.17" "172.16.16.8")
REMOTE_BUILD_DIR="/tmp/tankwar-build"
IMAGE_NAME="tankwar/tankwar-server:latest"
ts() { echo "[$(date '+%H:%M:%S')]"; }
# ------------------------------------------------------------
# Step 0: Sync server source to master node
# ------------------------------------------------------------
echo "$(ts) ===== Syncing tankwar server source to master node ====="
ssh -o StrictHostKeyChecking=no "$MASTER" "mkdir -p $REMOTE_BUILD_DIR"
rsync -az --delete --exclude='.git' --exclude='node_modules' \
-e "ssh -o StrictHostKeyChecking=no" \
"$SERVER_DIR/" "${MASTER}:${REMOTE_BUILD_DIR}/server/"
echo "$(ts) ✓ Source synced"
# ------------------------------------------------------------
# Step 1: Ensure docker is available on master
# ------------------------------------------------------------
echo "$(ts) ===== Checking docker on master ====="
if ! ssh -o StrictHostKeyChecking=no "$MASTER" "which docker >/dev/null 2>&1"; then
echo "$(ts) Docker not found on master. Installing..."
ssh -o StrictHostKeyChecking=no "$MASTER" "curl -fsSL https://get.docker.com | sh"
fi
ssh -o StrictHostKeyChecking=no "$MASTER" "docker version --format '{{.Server.Version}}' 2>/dev/null || systemctl start docker"
echo "$(ts) ✓ Docker ready on master"
# ------------------------------------------------------------
# Step 2: Build image on master
# ------------------------------------------------------------
echo "$(ts) ===== Building $IMAGE_NAME on master ====="
ssh -o StrictHostKeyChecking=no "$MASTER" \
"cd $REMOTE_BUILD_DIR/server && docker build -t $IMAGE_NAME -f Dockerfile ."
echo "$(ts) ✓ Image built"
# ------------------------------------------------------------
# Step 3: Distribute image to workers (containerd / ctr)
# ------------------------------------------------------------
echo "$(ts) ===== Distributing $IMAGE_NAME to workers ====="
# Master itself may also be a worker; import locally first so master pods can use it.
ssh -o StrictHostKeyChecking=no "$MASTER" \
"docker save $IMAGE_NAME | ctr -n k8s.io images import -"
for w in "${WORKERS_IP[@]}"; do
echo "$(ts) -> $w"
ssh -o StrictHostKeyChecking=no "$MASTER" \
"docker save $IMAGE_NAME | ssh -o StrictHostKeyChecking=no root@$w 'ctr -n k8s.io images import -'"
done
echo "$(ts) ✓ Image distributed"
# ------------------------------------------------------------
# Step 4: Apply K8s manifests (namespace + configmap + deploy + svc)
# ------------------------------------------------------------
echo "$(ts) ===== Applying K8s manifests ====="
cat "$SERVER_DIR/k8s-deployment.yaml" | \
ssh -o StrictHostKeyChecking=no "$MASTER" "kubectl apply -f -"
echo "$(ts) ✓ Manifests applied"
# ------------------------------------------------------------
# Step 5: Restart deployment to pick up the new image
# ------------------------------------------------------------
echo "$(ts) ===== Restarting tankwar-server deployment ====="
ssh -o StrictHostKeyChecking=no "$MASTER" \
"kubectl -n tankwar rollout restart deployment/tankwar-server" || true
ssh -o StrictHostKeyChecking=no "$MASTER" \
"kubectl -n tankwar rollout status deployment/tankwar-server --timeout=120s" || true
# ------------------------------------------------------------
# Step 6: Show final status
# ------------------------------------------------------------
echo "$(ts) ===== Final Status ====="
ssh -o StrictHostKeyChecking=no "$MASTER" "kubectl -n tankwar get pods -o wide"
echo ""
ssh -o StrictHostKeyChecking=no "$MASTER" "kubectl -n tankwar get svc"
echo ""
echo "$(ts) ===== ALL DONE ====="
echo "$(ts) Public endpoint (via warmcheck Nginx): wss://www.igeek.site/games/wx/tankwar/ws"
echo "$(ts) Internal endpoint: tankwar-server.tankwar.svc.cluster.local:3000"
echo "$(ts) Remember to redeploy warmcheck Nginx too (run WarmCheck_proj/backend/deploy/k8s/scripts/run-deploy.sh)"
# Cleanup
ssh -o StrictHostKeyChecking=no "$MASTER" "rm -rf $REMOTE_BUILD_DIR" 2>/dev/null || true
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
# Tank War Server部署测试脚本
set -e
echo "=== Tank War Server部署测试 ==="
echo ""
# 检查必需文件是否存在
echo "1. 检查必需文件..."
files=("Dockerfile" "k8s-deployment.yaml" "index.js" "package.json" "deploy.sh")
for file in "${files[@]}"; do
if [ -f "$file" ]; then
echo "$file 存在"
else
echo "$file 缺失"
exit 1
fi
done
echo ""
echo "2. 检查Dockerfile语法..."
if grep -q "FROM node" Dockerfile && grep -q "EXPOSE 3000" Dockerfile; then
echo "✓ Dockerfile语法正确"
else
echo "✗ Dockerfile语法错误"
exit 1
fi
echo ""
echo "3. 检查K8s配置文件..."
# 检查基本的YAML语法
if python3 -c "import yaml; yaml.safe_load(open('k8s-deployment.yaml'))" > /dev/null 2>&1; then
echo "✓ K8s配置文件语法正确"
else
echo "⚠ 无法验证K8s配置,需要连接到Kubernetes集群"
echo " 运行 'kubectl cluster-info' 检查连接状态"
fi
echo ""
echo "4. 检查Node.js依赖..."
if node -e "require('./package.json')" > /dev/null 2>&1; then
echo "✓ package.json语法正确"
else
echo "✗ package.json语法错误"
exit 1
fi
echo ""
echo "5. 检查服务器代码..."
if node -c index.js > /dev/null 2>&1; then
echo "✓ index.js语法正确"
else
echo "✗ index.js语法错误"
exit 1
fi
echo ""
echo "6. 检查部署脚本权限..."
chmod +x deploy.sh
if [ -x "deploy.sh" ]; then
echo "✓ 部署脚本可执行"
else
echo "✗ 部署脚本权限错误"
exit 1
fi
echo ""
echo "=== 所有检查通过! ==="
echo ""
echo "下一步:"
echo "1. 确保Docker守护进程正在运行"
echo "2. 确保kubectl已配置正确的集群上下文"
echo "3. 运行 ./deploy.sh 开始部署"
echo ""
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# Tank War Server部署验证脚本
set -e
echo "=== Tank War Server部署验证 ==="
echo ""
# 检查Kubernetes集群连接
echo "1. 检查Kubernetes集群连接..."
if kubectl cluster-info > /dev/null 2>&1; then
echo "✓ Kubernetes集群连接正常"
# 检查命名空间
echo ""
echo "2. 检查命名空间..."
if kubectl get namespace tankwar > /dev/null 2>&1; then
echo "✓ tankwar命名空间存在"
else
echo "✗ tankwar命名空间不存在"
echo " 运行: kubectl create namespace tankwar"
exit 1
fi
# 检查部署状态
echo ""
echo "3. 检查部署状态..."
kubectl get deployment tankwar-server -n tankwar 2>/dev/null || {
echo "✗ tankwar-server部署不存在"
echo " 运行: kubectl apply -f k8s-deployment.yaml -n tankwar"
exit 1
}
# 检查Pod状态
echo ""
echo "4. 检查Pod状态..."
kubectl get pods -l app=tankwar-server -n tankwar
# 检查服务状态
echo ""
echo "5. 检查服务状态..."
kubectl get svc tankwar-server-service -n tankwar
# 检查服务端点
echo ""
echo "6. 检查服务端点..."
kubectl get endpoints tankwar-server-service -n tankwar
# 检查Pod日志
echo ""
echo "7. 检查Pod日志(最近10行)..."
kubectl logs -l app=tankwar-server -n tankwar --tail=10
echo ""
echo "=== 部署验证完成 ==="
echo ""
echo "如果所有检查都通过,服务应该正在运行。"
echo "WebSocket连接地址格式:ws://<external-ip>:3000"
echo ""
echo "获取外部IP"
echo "kubectl get svc tankwar-server-service -n tankwar -o jsonpath='{.status.loadBalancer.ingress[0].ip}'"
else
echo "✗ Kubernetes集群连接失败"
echo "请配置kubectl连接到正确的集群"
echo ""
echo "配置方法:"
echo "1. 获取集群kubeconfig文件"
echo "2. 设置KUBECONFIG环境变量"
echo "3. 或复制到 ~/.kube/config"
exit 1
fi