Skip to content

那崩溃了怎么办啊

重要提示

  1. 不要等到比赛开始时再来看这一段。比赛前尽量做好压力测试。

  2. 不要过度优化,切勿按照本页说明从上到下全部调整一遍。选择适合服务器当前瓶颈的进行优化,优化后再次测试,观察是否有新的瓶颈,再针对性优化。谨记过犹不及

系统能承载的最大并发数低,很可能是配置错误。我们首先需要检查这些配置是不是正确。

当然,如果配置正确的前提下,很可能就是硬件资源真的不够用了。

Linux 系统优化

1. 检查并调整文件描述符上限

查看当前文件描述符限制:

bash
# 查看软限制和硬限制
ulimit -n
ulimit -Hn

# 查看系统级限制
cat /proc/sys/fs/file-max

修改文件描述符上限,编辑 /etc/security/limits.conf

# 添加以下内容(用户名请根据实际情况修改)
* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536
www-data soft nofile 65536
www-data hard nofile 65536

修改系统级文件描述符上限,编辑 /etc/sysctl.conf

# 添加以下内容
fs.file-max = 1000000

应用配置:

bash
# 重新加载 sysctl 配置
sudo sysctl -p

# 重启服务或重新登录以使 limits.conf 生效

2. 调整网络连接数上限

编辑 /etc/sysctl.conf,添加以下配置:

# 增加端口范围
net.ipv4.ip_local_port_range = 1024 65535

# 增加 TCP 连接队列长度
net.core.somaxconn = 65535

# 调整 TCP 时间戳
net.ipv4.tcp_timestamps = 1

# 快速回收 TIME_WAIT 连接
net.ipv4.tcp_tw_reuse = 1

# 减少 TIME_WAIT 超时时间
net.ipv4.tcp_fin_timeout = 30

# 增加网络缓冲区大小
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 131072 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728

# Redis 优化
vm.overcommit_memory = 1  # 允许内存过量分配

应用配置:

bash
sudo sysctl -p

Nginx 优化配置

编辑 Nginx 配置文件(通常在 /etc/nginx/nginx.conf):

nginx
# 在 main 块中
user www-data;
worker_processes auto;  # 自动设置为 CPU 核心数
worker_rlimit_nofile 65535;  # 工作进程能打开的文件描述符数量

events {
    worker_connections 65535;  # 每个工作进程的最大连接数
    use epoll;  # 使用 epoll 事件模型(Linux 推荐)
    multi_accept on;  # 允许一次接受多个连接
}

http {
    # 基本设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;  # 增加 keep-alive 请求数

    # 客户端请求大小限制
    client_max_body_size 100M;  # 允许上传的最大文件大小
    client_body_buffer_size 128k;
    client_header_buffer_size 32k;
    large_client_header_buffers 4 32k;

    # 超时设置
    client_body_timeout 60s;
    client_header_timeout 60s;
    send_timeout 60s;

    # 压缩设置
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;

    # 缓存设置
    open_file_cache max=10000 inactive=300s;
    open_file_cache_valid 360s;
    open_file_cache_min_uses 2;
    open_file_cache_errors off;

    # 限制请求频率(防止 DDoS)
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;

    # 在 server 块中应用限制
    server {
        # 对登录接口限制
        location /api/auth/ {
            limit_req zone=login burst=10 nodelay;
            # 其他配置...
        }

        # 对 API 接口限制
        location /api/ {
            limit_req zone=api burst=50 nodelay;
            # 其他配置...
        }
    }
}

检验 Nginx 配置并重启:

bash
# 检查配置文件语法
sudo nginx -t

# 重新加载配置
sudo nginx -s reload

# 或重启 Nginx
sudo systemctl restart nginx

数据库优化

MySQL/MariaDB 配置优化

编辑 /etc/mysql/mysql.conf.d/mysqld.cnf/etc/my.cnf

ini
[mysqld]
# 连接数设置
max_connections = 1000
max_user_connections = 800

# 缓冲池设置(根据服务器内存调整)
innodb_buffer_pool_size = 2G  # 设置为内存的 70-80%
innodb_buffer_pool_instances = 8

# 查询缓存
query_cache_type = 1
query_cache_size = 256M

# 连接超时
wait_timeout = 600
interactive_timeout = 600

# 临时表设置
tmp_table_size = 256M
max_heap_table_size = 256M

# 日志设置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

重启 MySQL:

bash
sudo systemctl restart mysql

Redis 配置优化

编辑 /etc/redis/redis.conf/usr/local/etc/redis.conf

ini
# 基本配置
bind 127.0.0.1 ::1  # 绑定本地地址,如需远程访问请修改
port 6379
timeout 300  # 客户端空闲超时时间

# 内存配置
maxmemory 4gb  # 根据服务器内存设置,建议不超过总内存的50%
maxmemory-policy allkeys-lru  # 内存满时的淘汰策略

# 持久化配置(根据需求调整)
# RDB 快照设置
save 900 1      # 900秒内至少1个key变化时保存
save 300 10     # 300秒内至少10个key变化时保存
save 60 10000   # 60秒内至少10000个key变化时保存

# AOF 配置(可选,影响性能但提高数据安全性)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec  # 每秒同步一次

# 网络和连接配置
tcp-keepalive 300
tcp-backlog 511  # TCP连接队列长度
maxclients 10000  # 最大客户端连接数

# 性能优化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

# 慢查询日志
slowlog-log-slower-than 10000  # 记录执行时间超过10ms的命令
slowlog-max-len 128

# 其他优化
stop-writes-on-bgsave-error no  # 后台保存失败时不停止写入
rdbcompression yes  # 启用RDB压缩
rdbchecksum yes     # 启用RDB校验
hz 10              # 后台任务执行频率

应用层优化

Node.js 应用优化

如果使用 PM2 管理 Node.js 应用,可以这样配置:

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'ccxc-sync-server',
    script: './dist/app.js',
    instances: 'max',  // 启动与 CPU 核心数相等的实例
    exec_mode: 'cluster',
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'production',
      UV_THREADPOOL_SIZE: 128  // 增加线程池大小
    }
  }]
}

监控和诊断命令

实时监控系统状态

bash
# 查看当前连接数
ss -tuln | wc -l

# 查看 Nginx 状态
curl http://localhost/nginx_status

# 查看系统负载
top
htop

# 查看内存使用
free -h

# 查看磁盘 I/O
iotop

# 查看网络连接状态
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -n

# 查看文件描述符使用情况
lsof | wc -l
cat /proc/sys/fs/file-nr

重要提醒

  1. 在生产环境修改这些配置前,请务必备份原始配置文件
  2. 根据你的服务器硬件配置(CPU、内存、磁盘)适当调整数值
  3. 建议在内测阶段进行压力测试,验证配置效果
  4. 监控系统资源使用情况,避免过度优化导致资源耗尽

压力测试建议

根据 CCXC Engine 的实际使用场景,压力测试应该重点关注以下核心 API 接口:

主要测试接口

高负载接口

API Base: https://api.yourdomain.com/api

  • POST /v1/start - 比赛开始
  • POST /v1/play/get-main-info - 获取主要信息
  • POST /v1/play/get-puzzle-info - 获取分区信息
  • POST /v1/play/get-detail - 获取题目信息

WebSocket 连接

  • wss://api.yourdomain.com/ws-api/notify - 实时通知

使用 Apache Bench (ab) 进行测试

首先创建认证头部生成脚本:

bash
# create_auth_headers.sh
#!/bin/bash

USER_TOKEN="your_user_token_here"
USER_SK="your_user_secret_key_here"
TS=$(date +%s%3N)  # 毫秒时间戳
BODY_STRING="$1"

# 计算签名
SIGN_DATA="token=${USER_TOKEN}&ts=${TS}&bodyString=${BODY_STRING}"
SIGN=$(echo -n "$SIGN_DATA" | openssl dgst -sha1 -hmac "$USER_SK" -binary | base64)

echo "User-Token: ${USER_TOKEN}"
echo "X-Auth-Token: Ccxc-Auth ${TS} ${SIGN}"
echo "Content-Type: application/json"

生成带认证的测试命令:

bash
# 设置认证信息
USER_TOKEN="your_user_token_here"
USER_SK="your_user_secret_key_here"
TS=$(date +%s%3N)

# 为每个接口生成签名和头部
# 测试启动接口
BODY="{}"
SIGN_DATA="token=${USER_TOKEN}&ts=${TS}&bodyString=${BODY}"
SIGN=$(echo -n "$SIGN_DATA" | openssl dgst -sha1 -hmac "$USER_SK" -binary | base64)
ab -n 5000 -c 200 -p start_payload.json -T application/json \
   -H "User-Token: ${USER_TOKEN}" \
   -H "X-Auth-Token: Ccxc-Auth ${TS} ${SIGN}" \
   https://api.yourdomain.com/api/v1/start

# 测试主信息接口
BODY="{}"
SIGN_DATA="token=${USER_TOKEN}&ts=${TS}&bodyString=${BODY}"
SIGN=$(echo -n "$SIGN_DATA" | openssl dgst -sha1 -hmac "$USER_SK" -binary | base64)
ab -n 10000 -c 300 -p main_info_payload.json -T application/json \
   -H "User-Token: ${USER_TOKEN}" \
   -H "X-Auth-Token: Ccxc-Auth ${TS} ${SIGN}" \
   https://api.yourdomain.com/api/v1/play/get-main-info

# 测试题目信息接口
BODY='{"pgid": 1}'
SIGN_DATA="token=${USER_TOKEN}&ts=${TS}&bodyString=${BODY}"
SIGN=$(echo -n "$SIGN_DATA" | openssl dgst -sha1 -hmac "$USER_SK" -binary | base64)
ab -n 8000 -c 250 -p puzzle_info_payload.json -T application/json \
   -H "User-Token: ${USER_TOKEN}" \
   -H "X-Auth-Token: Ccxc-Auth ${TS} ${SIGN}" \
   https://api.yourdomain.com/api/v1/play/get-puzzle-info

# 测试详细信息接口
BODY='{"pid": 1}'
SIGN_DATA="token=${USER_TOKEN}&ts=${TS}&bodyString=${BODY}"
SIGN=$(echo -n "$SIGN_DATA" | openssl dgst -sha1 -hmac "$USER_SK" -binary | base64)
ab -n 6000 -c 200 -p detail_payload.json -T application/json \
   -H "User-Token: ${USER_TOKEN}" \
   -H "X-Auth-Token: Ccxc-Auth ${TS} ${SIGN}" \
   https://api.yourdomain.com/api/v1/play/get-detail

使用 Node.js 进行测试

javascript
// ab_test.js
const crypto = require('crypto');
const https = require('https');
const { performance } = require('perf_hooks');

// 配置
const USER_TOKEN = "your_user_token_here";
const USER_SK = "your_user_secret_key_here";
const BASE_URL = "https://api.yourdomain.com/api";

// 生成认证头部
function generateAuthHeaders(bodyString) {
    const ts = Date.now();
    const signData = `token=${USER_TOKEN}&ts=${ts}&bodyString=${bodyString}`;
    const sign = crypto.createHmac('sha1', USER_SK).update(signData).digest('base64');
    
    return {
        'User-Token': USER_TOKEN,
        'X-Auth-Token': `Ccxc-Auth ${ts} ${sign}`,
        'Content-Type': 'application/json'
    };
}

// 执行HTTP请求
function makeRequest(path, body, callback) {
    const bodyString = JSON.stringify(body);
    const headers = generateAuthHeaders(bodyString);
    
    const options = {
        hostname: 'api.yourdomain.com',
        port: 443,
        path: path,
        method: 'POST',
        headers: headers
    };
    
    const startTime = performance.now();
    const req = https.request(options, (res) => {
        const endTime = performance.now();
        callback(null, {
            statusCode: res.statusCode,
            responseTime: endTime - startTime
        });
    });
    
    req.on('error', callback);
    req.write(bodyString);
    req.end();
}

// 并发测试函数
async function concurrentTest(path, body, concurrency, totalRequests) {
    const results = [];
    const startTime = performance.now();
    let completed = 0;
    let errors = 0;
    
    return new Promise((resolve) => {
        for (let i = 0; i < concurrency; i++) {
            const requestsPerWorker = Math.floor(totalRequests / concurrency);
            
            const worker = async () => {
                for (let j = 0; j < requestsPerWorker; j++) {
                    makeRequest(BASE_URL + path, body, (err, result) => {
                        completed++;
                        if (err) {
                            errors++;
                        } else {
                            results.push(result);
                        }
                        
                        if (completed >= totalRequests) {
                            const endTime = performance.now();
                            const totalTime = endTime - startTime;
                            
                            resolve({
                                totalRequests,
                                totalTime,
                                errors,
                                successRate: ((totalRequests - errors) / totalRequests * 100).toFixed(2),
                                avgResponseTime: results.reduce((sum, r) => sum + r.responseTime, 0) / results.length
                            });
                        }
                    });
                    
                    // 控制请求频率
                    await new Promise(resolve => setTimeout(resolve, 10));
                }
            };
            
            worker();
        }
    });
}

// 运行测试
async function runTests() {
    console.log('开始压力测试...');
    
    // 测试启动接口
    console.log('\n测试 /v1/start 接口...');
    const startResult = await concurrentTest('/v1/start', {}, 200, 5000);
    console.log('结果:', startResult);
    
    // 测试主信息接口
    console.log('\n测试 /v1/play/get-main-info 接口...');
    const mainInfoResult = await concurrentTest('/v1/play/get-main-info', {}, 300, 10000);
    console.log('结果:', mainInfoResult);
    
    // 测试题目信息接口
    console.log('\n测试 /v1/play/get-puzzle-info 接口...');
    const puzzleInfoResult = await concurrentTest('/v1/play/get-puzzle-info', {pgid: 1}, 250, 8000);
    console.log('结果:', puzzleInfoResult);
    
    // 测试详细信息接口
    console.log('\n测试 /v1/play/get-detail 接口...');
    const detailResult = await concurrentTest('/v1/play/get-detail', {pid: 1}, 200, 6000);
    console.log('结果:', detailResult);
}

// 运行测试
runTests().catch(console.error);

运行 Node.js 压力测试:

bash
node ab_test.js

WebSocket 连接压力测试

使用 Node.js 脚本测试 WebSocket 连接:

javascript
// websocket_test.js
const WebSocket = require('ws');

const CONCURRENT_CONNECTIONS = 1000;
const WS_BASE_URL = 'wss://api.yourdomain.com/ws-api/notify';

// 认证配置
const USER_TOKEN = "your_user_token_here";

let connectedCount = 0;
let failedCount = 0;

// 生成带认证的 WebSocket URL
function generateWebSocketURL() {
    return `${WS_BASE_URL}?sessionId=${USER_TOKEN}`;
}

// 批量创建连接的函数
async function createConnections() {
    for (let i = 0; i < CONCURRENT_CONNECTIONS; i++) {
        try {
            const wsUrl = generateWebSocketURL();
            
            const ws = new WebSocket(wsUrl);

            ws.on('open', () => {
                connectedCount++;
                console.log(`Connected: ${connectedCount}/${CONCURRENT_CONNECTIONS}`);
                
                // 模拟保持连接
                const pingInterval = setInterval(() => {
                    if (ws.readyState === WebSocket.OPEN) {
                        ws.ping();
                    } else {
                        clearInterval(pingInterval);
                    }
                }, 30000);
                
                // 5分钟后关闭连接
                setTimeout(() => {
                    if (ws.readyState === WebSocket.OPEN) {
                        ws.close();
                    }
                    clearInterval(pingInterval);
                }, 300000);
            });

            ws.on('error', (error) => {
                failedCount++;
                console.error(`Connection ${i} failed:`, error.message);
            });

            ws.on('close', (code, reason) => {
                console.log(`Connection ${i} closed: ${code} ${reason}`);
            });

            ws.on('message', (data) => {
                console.log(`Connection ${i} received:`, data.toString());
            });

            // 避免同时发起太多连接,每批50个连接间隔100ms
            if (i % 50 === 0 && i > 0) {
                await new Promise(resolve => setTimeout(resolve, 100));
            }
        } catch (error) {
            failedCount++;
            console.error(`Failed to create connection ${i}:`, error.message);
        }
    }
}

// 开始测试
createConnections().then(() => {
    console.log('所有连接创建请求已发送');
    
    // 每30秒输出一次统计
    const statsInterval = setInterval(() => {
        console.log(`\n=== 统计信息 ===`);
        console.log(`已连接: ${connectedCount}`);
        console.log(`失败: ${failedCount}`);
        console.log(`成功率: ${(connectedCount / CONCURRENT_CONNECTIONS * 100).toFixed(2)}%`);
        console.log(`================\n`);
    }, 30000);
    
    // 10分钟后停止统计并结束程序
    setTimeout(() => {
        clearInterval(statsInterval);
        console.log(`\n=== 最终统计 ===`);
        console.log(`总连接尝试: ${CONCURRENT_CONNECTIONS}`);
        console.log(`成功连接: ${connectedCount}`);
        console.log(`连接失败: ${failedCount}`);
        console.log(`成功率: ${(connectedCount / CONCURRENT_CONNECTIONS * 100).toFixed(2)}%`);
        console.log(`=================`);
        process.exit(0);
    }, 600000);
}).catch(console.error);

运行 WebSocket 测试:

bash
# 安装 WebSocket 依赖
npm install ws

# 运行测试
node websocket_test.js

测试结果评估标准

HTTP 接口性能目标

  • 响应时间:P95 < 500ms,P99 < 1000ms
  • 错误率:< 1%
  • 吞吐量:> 1000 requests/second

WebSocket 连接目标

  • 同时连接数:> 5000
  • 连接建立成功率:> 99%
  • 消息传输延迟:< 100ms

测试建议

  1. 逐步增加负载:从小并发开始,逐步增加到目标值
  2. 持续时间测试:不仅要测试峰值,还要测试持续负载能力
  3. 监控资源使用:同时监控 CPU、内存、磁盘 I/O、网络带宽
  4. 真实数据测试:使用真实的用户认证 token 和数据进行测试

注意事项

  • 替换认证信息:将 your_user_token_hereyour_user_secret_key_here 替换为实际的认证信息
  • 替换测试域名:将 yourdomain.com 替换为实际域名
  • 控制测试强度:避免对生产环境造成实际影响
  • 清理测试数据:测试完成后清理产生的测试数据

测试时注意

  1. User-Tokensk:用户登录后从前端 LocalStorage 中查看
  2. 确保测试账号具有足够的权限访问所有测试接口

应急处理

如果系统在比赛中出现问题,可以采取以下应急措施:

  1. 重启相关服务
bash
sudo systemctl restart nginx
sudo systemctl restart mysql
sudo systemctl restart redis
sudo pm2 restart all
  1. 临时调整限制
bash
# 临时增加文件描述符限制
ulimit -n 100000

# 清理系统缓存(谨慎使用)
echo 3 > /proc/sys/vm/drop_caches
  1. 查看错误日志
bash
# Nginx 错误日志
tail -f /var/log/nginx/error.log

# MySQL 错误日志
tail -f /var/log/mysql/error.log

# 系统日志
journalctl -f

Released under the MIT License. Powered by VitePress.