1011 lines
24 KiB
Markdown
1011 lines
24 KiB
Markdown
# 外卖SaaS系统 - 部署运维
|
||
|
||
## 1. 环境要求
|
||
|
||
### 1.1 开发环境
|
||
- **.NET SDK**:10.0 或更高版本
|
||
- **IDE**:Visual Studio 2022 / JetBrains Rider / VS Code
|
||
- **数据库**:PostgreSQL 16+
|
||
- **缓存**:Redis 7.0+
|
||
- **消息队列**:RabbitMQ 3.12+
|
||
- **Git**:版本控制
|
||
- **Docker Desktop**:容器化开发(可选)
|
||
|
||
### 1.2 生产环境
|
||
- **操作系统**:Linux (Ubuntu 22.04 LTS / CentOS 8+)
|
||
- **运行时**:.NET Runtime 10.0
|
||
- **Web服务器**:Nginx 1.24+
|
||
- **数据库**:PostgreSQL 16+ (主从复制)
|
||
- **缓存**:Redis 7.0+ (哨兵模式)
|
||
- **消息队列**:RabbitMQ 3.12+ (集群模式)
|
||
- **对象存储**:MinIO / 阿里云OSS / 腾讯云COS
|
||
- **监控**:Prometheus + Grafana
|
||
- **日志**:ELK Stack (Elasticsearch + Logstash + Kibana)
|
||
|
||
### 1.3 硬件要求(生产环境)
|
||
- **应用服务器**:4核8GB内存(最低),推荐8核16GB
|
||
- **数据库服务器**:8核16GB内存,SSD存储
|
||
- **Redis服务器**:4核8GB内存
|
||
- **负载均衡器**:2核4GB内存
|
||
|
||
## 2. 本地开发环境搭建
|
||
|
||
### 2.1 安装.NET SDK
|
||
```bash
|
||
# Windows
|
||
# 从官网下载安装:https://dotnet.microsoft.com/download
|
||
|
||
# Linux (Ubuntu)
|
||
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
|
||
sudo dpkg -i packages-microsoft-prod.deb
|
||
sudo apt-get update
|
||
sudo apt-get install -y dotnet-sdk-10.0
|
||
|
||
# 验证安装
|
||
dotnet --version
|
||
```
|
||
|
||
### 2.2 安装PostgreSQL
|
||
```bash
|
||
# Ubuntu
|
||
sudo apt-get update
|
||
sudo apt-get install -y postgresql-16 postgresql-contrib-16
|
||
|
||
# 启动服务
|
||
sudo systemctl start postgresql
|
||
sudo systemctl enable postgresql
|
||
|
||
# 创建数据库
|
||
sudo -u postgres psql
|
||
CREATE DATABASE takeout_saas;
|
||
CREATE USER takeout_user WITH PASSWORD 'your_password';
|
||
GRANT ALL PRIVILEGES ON DATABASE takeout_saas TO takeout_user;
|
||
\q
|
||
```
|
||
|
||
### 2.3 安装Redis
|
||
```bash
|
||
# Ubuntu
|
||
sudo apt-get install -y redis-server
|
||
|
||
# 启动服务
|
||
sudo systemctl start redis-server
|
||
sudo systemctl enable redis-server
|
||
|
||
# 测试连接
|
||
redis-cli ping
|
||
```
|
||
|
||
### 2.4 安装RabbitMQ
|
||
```bash
|
||
# Ubuntu
|
||
sudo apt-get install -y rabbitmq-server
|
||
|
||
# 启动服务
|
||
sudo systemctl start rabbitmq-server
|
||
sudo systemctl enable rabbitmq-server
|
||
|
||
# 启用管理插件
|
||
sudo rabbitmq-plugins enable rabbitmq_management
|
||
|
||
# 创建用户
|
||
sudo rabbitmqctl add_user admin password
|
||
sudo rabbitmqctl set_user_tags admin administrator
|
||
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
||
|
||
# 访问管理界面:http://localhost:15672
|
||
```
|
||
|
||
### 2.5 使用Docker Compose(推荐)
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:16
|
||
container_name: takeout_postgres
|
||
environment:
|
||
POSTGRES_DB: takeout_saas
|
||
POSTGRES_USER: takeout_user
|
||
POSTGRES_PASSWORD: your_password
|
||
ports:
|
||
- "5432:5432"
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
container_name: takeout_redis
|
||
ports:
|
||
- "6379:6379"
|
||
volumes:
|
||
- redis_data:/data
|
||
|
||
rabbitmq:
|
||
image: rabbitmq:3.12-management
|
||
container_name: takeout_rabbitmq
|
||
environment:
|
||
RABBITMQ_DEFAULT_USER: admin
|
||
RABBITMQ_DEFAULT_PASS: password
|
||
ports:
|
||
- "5672:5672"
|
||
- "15672:15672"
|
||
volumes:
|
||
- rabbitmq_data:/var/lib/rabbitmq
|
||
|
||
minio:
|
||
image: minio/minio:latest
|
||
container_name: takeout_minio
|
||
command: server /data --console-address ":9001"
|
||
environment:
|
||
MINIO_ROOT_USER: admin
|
||
MINIO_ROOT_PASSWORD: password123
|
||
ports:
|
||
- "9000:9000"
|
||
- "9001:9001"
|
||
volumes:
|
||
- minio_data:/data
|
||
|
||
volumes:
|
||
postgres_data:
|
||
redis_data:
|
||
rabbitmq_data:
|
||
minio_data:
|
||
```
|
||
|
||
```bash
|
||
# 启动所有服务
|
||
docker-compose up -d
|
||
|
||
# 查看服务状态
|
||
docker-compose ps
|
||
|
||
# 停止服务
|
||
docker-compose down
|
||
```
|
||
|
||
### 2.6 配置项目
|
||
```bash
|
||
# 克隆项目
|
||
git clone https://github.com/your-org/takeout-saas.git
|
||
cd takeout-saas
|
||
|
||
# 还原依赖
|
||
dotnet restore
|
||
|
||
# 配置appsettings.Development.json
|
||
{
|
||
"ConnectionStrings": {
|
||
"DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password"
|
||
},
|
||
"Redis": {
|
||
"Configuration": "localhost:6379"
|
||
},
|
||
"RabbitMQ": {
|
||
"Host": "localhost",
|
||
"Port": 5672,
|
||
"Username": "admin",
|
||
"Password": "password"
|
||
}
|
||
}
|
||
|
||
# 执行数据库迁移
|
||
cd src/TakeoutSaaS.Api
|
||
dotnet ef database update
|
||
|
||
# 运行项目
|
||
dotnet run
|
||
```
|
||
|
||
### 2.7 EF Core 迁移基线
|
||
|
||
现已内置 `dotnet-ef` 本地工具与设计时 DbContext 工厂,可直接在命令行生成/更新数据库。运行前可通过环境变量 `TAKEOUTSAAS_APP_CONNECTION`、`TAKEOUTSAAS_IDENTITY_CONNECTION` 覆盖默认连接串(默认指向本地 PostgreSQL)。
|
||
|
||
```powershell
|
||
# 业务主库(TakeoutAppDbContext,含租户/商户/门店/商品/订单等)
|
||
dotnet tool run dotnet-ef database update `
|
||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext
|
||
|
||
# 身份库(IdentityDbContext)
|
||
dotnet tool run dotnet-ef database update `
|
||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext
|
||
|
||
# 业务/字典库(DictionaryDbContext,归属 AppDatabase)
|
||
dotnet tool run dotnet-ef database update `
|
||
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
|
||
--context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext
|
||
```
|
||
|
||
> Hangfire 使用 Scheduler.ConnectionString 指向的数据库,首次启动服务会自动建表;只需提前创建空数据库并授予账号权限。
|
||
|
||
## 3. Docker部署
|
||
|
||
### 3.1 创建Dockerfile
|
||
```dockerfile
|
||
# Dockerfile
|
||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||
WORKDIR /app
|
||
EXPOSE 80
|
||
EXPOSE 443
|
||
|
||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||
WORKDIR /src
|
||
COPY ["src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj", "src/TakeoutSaaS.Api/"]
|
||
COPY ["src/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/TakeoutSaaS.Application/"]
|
||
COPY ["src/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/TakeoutSaaS.Domain/"]
|
||
COPY ["src/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/TakeoutSaaS.Infrastructure/"]
|
||
COPY ["src/TakeoutSaaS.Shared/TakeoutSaaS.Shared.csproj", "src/TakeoutSaaS.Shared/"]
|
||
RUN dotnet restore "src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj"
|
||
COPY . .
|
||
WORKDIR "/src/src/TakeoutSaaS.Api"
|
||
RUN dotnet build "TakeoutSaaS.Api.csproj" -c Release -o /app/build
|
||
|
||
FROM build AS publish
|
||
RUN dotnet publish "TakeoutSaaS.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||
|
||
FROM base AS final
|
||
WORKDIR /app
|
||
COPY --from=publish /app/publish .
|
||
ENTRYPOINT ["dotnet", "TakeoutSaaS.Api.dll"]
|
||
```
|
||
|
||
### 3.2 构建镜像
|
||
```bash
|
||
# 构建镜像
|
||
docker build -t takeout-saas-api:latest .
|
||
|
||
# 查看镜像
|
||
docker images | grep takeout-saas
|
||
|
||
# 运行容器
|
||
docker run -d \
|
||
--name takeout-api \
|
||
-p 8080:80 \
|
||
-e ASPNETCORE_ENVIRONMENT=Production \
|
||
-e ConnectionStrings__DefaultConnection="Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" \
|
||
takeout-saas-api:latest
|
||
```
|
||
|
||
### 3.3 生产环境Docker Compose
|
||
```yaml
|
||
# docker-compose.prod.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
api:
|
||
image: takeout-saas-api:latest
|
||
container_name: takeout_api
|
||
restart: always
|
||
environment:
|
||
ASPNETCORE_ENVIRONMENT: Production
|
||
ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=${DB_PASSWORD}"
|
||
Redis__Configuration: "redis:6379"
|
||
RabbitMQ__Host: "rabbitmq"
|
||
ports:
|
||
- "8080:80"
|
||
depends_on:
|
||
- postgres
|
||
- redis
|
||
- rabbitmq
|
||
networks:
|
||
- takeout_network
|
||
|
||
nginx:
|
||
image: nginx:latest
|
||
container_name: takeout_nginx
|
||
restart: always
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||
- ./nginx/ssl:/etc/nginx/ssl
|
||
depends_on:
|
||
- api
|
||
networks:
|
||
- takeout_network
|
||
|
||
networks:
|
||
takeout_network:
|
||
driver: bridge
|
||
```
|
||
|
||
## 4. Nginx配置
|
||
|
||
### 4.1 基础配置
|
||
```nginx
|
||
# /etc/nginx/nginx.conf
|
||
user nginx;
|
||
worker_processes auto;
|
||
error_log /var/log/nginx/error.log warn;
|
||
pid /var/run/nginx.pid;
|
||
|
||
events {
|
||
worker_connections 1024;
|
||
}
|
||
|
||
http {
|
||
include /etc/nginx/mime.types;
|
||
default_type application/octet-stream;
|
||
|
||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||
'$status $body_bytes_sent "$http_referer" '
|
||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||
|
||
access_log /var/log/nginx/access.log main;
|
||
|
||
sendfile on;
|
||
tcp_nopush on;
|
||
tcp_nodelay on;
|
||
keepalive_timeout 65;
|
||
types_hash_max_size 2048;
|
||
|
||
# Gzip压缩
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_proxied any;
|
||
gzip_comp_level 6;
|
||
gzip_types text/plain text/css text/xml text/javascript
|
||
application/json application/javascript application/xml+rss
|
||
application/rss+xml font/truetype font/opentype
|
||
application/vnd.ms-fontobject image/svg+xml;
|
||
|
||
# 限流配置
|
||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
|
||
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
||
|
||
include /etc/nginx/conf.d/*.conf;
|
||
}
|
||
```
|
||
|
||
### 4.2 API服务配置
|
||
```nginx
|
||
# /etc/nginx/conf.d/api.conf
|
||
upstream api_backend {
|
||
least_conn;
|
||
server api1:80 weight=1 max_fails=3 fail_timeout=30s;
|
||
server api2:80 weight=1 max_fails=3 fail_timeout=30s;
|
||
keepalive 32;
|
||
}
|
||
|
||
server {
|
||
listen 80;
|
||
server_name api.example.com;
|
||
|
||
# 重定向到HTTPS
|
||
return 301 https://$server_name$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name api.example.com;
|
||
|
||
# SSL证书配置
|
||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
ssl_prefer_server_ciphers on;
|
||
ssl_session_cache shared:SSL:10m;
|
||
ssl_session_timeout 10m;
|
||
|
||
# 安全头
|
||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header X-XSS-Protection "1; mode=block" always;
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
|
||
# 客户端请求体大小限制
|
||
client_max_body_size 10M;
|
||
|
||
# API接口
|
||
location /api/ {
|
||
# 限流
|
||
limit_req zone=api_limit burst=20 nodelay;
|
||
limit_conn conn_limit 10;
|
||
|
||
proxy_pass http://api_backend;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# 超时设置
|
||
proxy_connect_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
proxy_read_timeout 60s;
|
||
|
||
# 缓冲设置
|
||
proxy_buffering on;
|
||
proxy_buffer_size 4k;
|
||
proxy_buffers 8 4k;
|
||
proxy_busy_buffers_size 8k;
|
||
}
|
||
|
||
# WebSocket
|
||
location /ws {
|
||
proxy_pass http://api_backend;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
|
||
# WebSocket超时
|
||
proxy_read_timeout 3600s;
|
||
proxy_send_timeout 3600s;
|
||
}
|
||
|
||
# 健康检查
|
||
location /health {
|
||
proxy_pass http://api_backend;
|
||
access_log off;
|
||
}
|
||
|
||
# 静态文件缓存
|
||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||
proxy_pass http://api_backend;
|
||
expires 30d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 数据库部署
|
||
|
||
### 5.1 PostgreSQL主从复制
|
||
```bash
|
||
# 主库配置 (postgresql.conf)
|
||
listen_addresses = '*'
|
||
wal_level = replica
|
||
max_wal_senders = 10
|
||
wal_keep_size = 64MB
|
||
hot_standby = on
|
||
|
||
# 主库配置 (pg_hba.conf)
|
||
host replication replicator 192.168.1.0/24 md5
|
||
|
||
# 创建复制用户
|
||
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_password';
|
||
|
||
# 从库配置
|
||
# 1. 停止从库
|
||
sudo systemctl stop postgresql
|
||
|
||
# 2. 清空从库数据目录
|
||
rm -rf /var/lib/postgresql/16/main/*
|
||
|
||
# 3. 从主库复制数据
|
||
pg_basebackup -h master_ip -D /var/lib/postgresql/16/main -U replicator -P -v -R -X stream -C -S replica1
|
||
|
||
# 4. 启动从库
|
||
sudo systemctl start postgresql
|
||
|
||
# 5. 验证复制状态
|
||
# 主库执行
|
||
SELECT * FROM pg_stat_replication;
|
||
```
|
||
|
||
### 5.2 数据库备份脚本
|
||
```bash
|
||
#!/bin/bash
|
||
# backup_db.sh
|
||
|
||
BACKUP_DIR="/backup/postgres"
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
DB_NAME="takeout_saas"
|
||
DB_USER="takeout_user"
|
||
RETENTION_DAYS=30
|
||
|
||
# 创建备份目录
|
||
mkdir -p $BACKUP_DIR
|
||
|
||
# 全量备份
|
||
pg_dump -h localhost -U $DB_USER -d $DB_NAME -F c -f $BACKUP_DIR/full_$DATE.dump
|
||
|
||
# 压缩备份
|
||
gzip $BACKUP_DIR/full_$DATE.dump
|
||
|
||
# 删除过期备份
|
||
find $BACKUP_DIR -name "full_*.dump.gz" -mtime +$RETENTION_DAYS -delete
|
||
|
||
# 上传到对象存储(可选)
|
||
# aws s3 cp $BACKUP_DIR/full_$DATE.dump.gz s3://your-bucket/backups/
|
||
|
||
echo "Backup completed: full_$DATE.dump.gz"
|
||
```
|
||
|
||
### 5.3 定时备份(Crontab)
|
||
```bash
|
||
# 编辑crontab
|
||
crontab -e
|
||
|
||
# 每天凌晨2点执行备份
|
||
0 2 * * * /path/to/backup_db.sh >> /var/log/backup.log 2>&1
|
||
```
|
||
|
||
## TODO:基础设施部署脚本
|
||
|
||
- [ ] PostgreSQL 主从:整理主库/从库初始化脚本、basebackup 步骤与故障切换手册。
|
||
- [ ] Redis 哨兵/集群:补充 redis.conf/sentinel.conf 模板以及一主两从搭建命令。
|
||
- [ ] RabbitMQ:编写单节点到镜像队列的安装脚本,记录 VHost、用户、权限、监控等操作。
|
||
- [ ] 腾讯云 COS:整理桶创建、ACL、CDN 绑定与密钥轮换流程,并提供 coscmd/SDK 示例。
|
||
- [ ] Hangfire 存储:确认 PostgreSQL Schema 初始化脚本,补充定期备份、清理、监控的 SOP。
|
||
|
||
## 6. Redis部署
|
||
|
||
### 6.1 Redis哨兵模式
|
||
```bash
|
||
# redis.conf (主节点)
|
||
bind 0.0.0.0
|
||
port 6379
|
||
requirepass your_password
|
||
masterauth your_password
|
||
|
||
# sentinel.conf
|
||
port 26379
|
||
sentinel monitor mymaster 192.168.1.100 6379 2
|
||
sentinel auth-pass mymaster your_password
|
||
sentinel down-after-milliseconds mymaster 5000
|
||
sentinel parallel-syncs mymaster 1
|
||
sentinel failover-timeout mymaster 10000
|
||
```
|
||
|
||
### 6.2 Redis持久化配置
|
||
```bash
|
||
# redis.conf
|
||
# RDB持久化
|
||
save 900 1
|
||
save 300 10
|
||
save 60 10000
|
||
rdbcompression yes
|
||
rdbchecksum yes
|
||
dbfilename dump.rdb
|
||
|
||
# AOF持久化
|
||
appendonly yes
|
||
appendfilename "appendonly.aof"
|
||
appendfsync everysec
|
||
no-appendfsync-on-rewrite no
|
||
auto-aof-rewrite-percentage 100
|
||
auto-aof-rewrite-min-size 64mb
|
||
```
|
||
|
||
## 7. CI/CD配置
|
||
|
||
### 7.1 GitHub Actions
|
||
```yaml
|
||
# .github/workflows/deploy.yml
|
||
name: Deploy to Production
|
||
|
||
on:
|
||
push:
|
||
branches: [ main ]
|
||
|
||
jobs:
|
||
build-and-deploy:
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Setup .NET
|
||
uses: actions/setup-dotnet@v3
|
||
with:
|
||
dotnet-version: '10.0.x'
|
||
|
||
- name: Restore dependencies
|
||
run: dotnet restore
|
||
|
||
- name: Build
|
||
run: dotnet build --configuration Release --no-restore
|
||
|
||
- name: Test
|
||
run: dotnet test --no-build --verbosity normal
|
||
|
||
- name: Publish
|
||
run: dotnet publish src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj -c Release -o ./publish
|
||
|
||
- name: Build Docker image
|
||
run: |
|
||
docker build -t takeout-saas-api:${{ github.sha }} .
|
||
docker tag takeout-saas-api:${{ github.sha }} takeout-saas-api:latest
|
||
|
||
- name: Push to Registry
|
||
run: |
|
||
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||
docker push takeout-saas-api:${{ github.sha }}
|
||
docker push takeout-saas-api:latest
|
||
|
||
- name: Deploy to Server
|
||
uses: appleboy/ssh-action@master
|
||
with:
|
||
host: ${{ secrets.SERVER_HOST }}
|
||
username: ${{ secrets.SERVER_USER }}
|
||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||
script: |
|
||
cd /opt/takeout-saas
|
||
docker-compose pull
|
||
docker-compose up -d
|
||
docker system prune -f
|
||
```
|
||
|
||
### 7.2 GitLab CI
|
||
```yaml
|
||
# .gitlab-ci.yml
|
||
stages:
|
||
- build
|
||
- test
|
||
- deploy
|
||
|
||
variables:
|
||
DOCKER_IMAGE: registry.example.com/takeout-saas-api
|
||
|
||
build:
|
||
stage: build
|
||
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||
script:
|
||
- dotnet restore
|
||
- dotnet build --configuration Release
|
||
artifacts:
|
||
paths:
|
||
- src/*/bin/Release/
|
||
|
||
test:
|
||
stage: test
|
||
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||
script:
|
||
- dotnet test --configuration Release
|
||
|
||
deploy:
|
||
stage: deploy
|
||
image: docker:latest
|
||
services:
|
||
- docker:dind
|
||
script:
|
||
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
|
||
- docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
|
||
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
|
||
- docker push $DOCKER_IMAGE:latest
|
||
only:
|
||
- main
|
||
```
|
||
|
||
## 8. 监控告警
|
||
|
||
### 8.1 Prometheus配置
|
||
```yaml
|
||
# prometheus.yml
|
||
global:
|
||
scrape_interval: 15s
|
||
evaluation_interval: 15s
|
||
|
||
scrape_configs:
|
||
- job_name: 'takeout-api'
|
||
static_configs:
|
||
- targets: ['api:80']
|
||
metrics_path: '/metrics'
|
||
|
||
- job_name: 'postgres'
|
||
static_configs:
|
||
- targets: ['postgres-exporter:9187']
|
||
|
||
- job_name: 'redis'
|
||
static_configs:
|
||
- targets: ['redis-exporter:9121']
|
||
|
||
- job_name: 'node'
|
||
static_configs:
|
||
- targets: ['node-exporter:9100']
|
||
```
|
||
|
||
### 8.2 应用监控指标
|
||
```csharp
|
||
// Program.cs - 添加Prometheus监控
|
||
builder.Services.AddPrometheusMetrics();
|
||
|
||
app.UseMetricServer(); // /metrics端点
|
||
app.UseHttpMetrics(); // HTTP请求指标
|
||
|
||
// 自定义指标
|
||
public class MetricsService
|
||
{
|
||
private static readonly Counter OrderCreatedCounter = Metrics
|
||
.CreateCounter("orders_created_total", "Total orders created");
|
||
|
||
private static readonly Histogram OrderProcessingDuration = Metrics
|
||
.CreateHistogram("order_processing_duration_seconds", "Order processing duration");
|
||
|
||
public void RecordOrderCreated()
|
||
{
|
||
OrderCreatedCounter.Inc();
|
||
}
|
||
|
||
public IDisposable MeasureOrderProcessing()
|
||
{
|
||
return OrderProcessingDuration.NewTimer();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 Grafana仪表板
|
||
```json
|
||
{
|
||
"dashboard": {
|
||
"title": "外卖SaaS系统监控",
|
||
"panels": [
|
||
{
|
||
"title": "API请求速率",
|
||
"targets": [
|
||
{
|
||
"expr": "rate(http_requests_total[5m])"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "订单创建数",
|
||
"targets": [
|
||
{
|
||
"expr": "increase(orders_created_total[1h])"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "数据库连接数",
|
||
"targets": [
|
||
{
|
||
"expr": "pg_stat_activity_count"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4 告警规则
|
||
```yaml
|
||
# alert.rules.yml
|
||
groups:
|
||
- name: takeout_alerts
|
||
interval: 30s
|
||
rules:
|
||
- alert: HighErrorRate
|
||
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
|
||
for: 5m
|
||
labels:
|
||
severity: critical
|
||
annotations:
|
||
summary: "高错误率告警"
|
||
description: "API错误率超过5%"
|
||
|
||
- alert: DatabaseDown
|
||
expr: up{job="postgres"} == 0
|
||
for: 1m
|
||
labels:
|
||
severity: critical
|
||
annotations:
|
||
summary: "数据库宕机"
|
||
description: "PostgreSQL数据库不可用"
|
||
|
||
- alert: HighMemoryUsage
|
||
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9
|
||
for: 5m
|
||
labels:
|
||
severity: warning
|
||
annotations:
|
||
summary: "内存使用率过高"
|
||
description: "内存使用率超过90%"
|
||
```
|
||
|
||
## 9. 日志管理
|
||
|
||
### 9.1 Serilog配置
|
||
```json
|
||
{
|
||
"Serilog": {
|
||
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Elasticsearch"],
|
||
"MinimumLevel": {
|
||
"Default": "Information",
|
||
"Override": {
|
||
"Microsoft": "Warning",
|
||
"System": "Warning"
|
||
}
|
||
},
|
||
"WriteTo": [
|
||
{
|
||
"Name": "Console",
|
||
"Args": {
|
||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||
}
|
||
},
|
||
{
|
||
"Name": "File",
|
||
"Args": {
|
||
"path": "logs/log-.txt",
|
||
"rollingInterval": "Day",
|
||
"retainedFileCountLimit": 30
|
||
}
|
||
},
|
||
{
|
||
"Name": "Elasticsearch",
|
||
"Args": {
|
||
"nodeUris": "http://elasticsearch:9200",
|
||
"indexFormat": "takeout-logs-{0:yyyy.MM.dd}",
|
||
"autoRegisterTemplate": true
|
||
}
|
||
}
|
||
],
|
||
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.2 ELK Stack部署
|
||
```yaml
|
||
# docker-compose.elk.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
elasticsearch:
|
||
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
|
||
environment:
|
||
- discovery.type=single-node
|
||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||
ports:
|
||
- "9200:9200"
|
||
volumes:
|
||
- es_data:/usr/share/elasticsearch/data
|
||
|
||
logstash:
|
||
image: docker.elastic.co/logstash/logstash:8.11.0
|
||
volumes:
|
||
- ./logstash/pipeline:/usr/share/logstash/pipeline
|
||
ports:
|
||
- "5044:5044"
|
||
depends_on:
|
||
- elasticsearch
|
||
|
||
kibana:
|
||
image: docker.elastic.co/kibana/kibana:8.11.0
|
||
ports:
|
||
- "5601:5601"
|
||
environment:
|
||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||
depends_on:
|
||
- elasticsearch
|
||
|
||
volumes:
|
||
es_data:
|
||
```
|
||
|
||
## 10. 安全加固
|
||
|
||
### 10.1 防火墙配置
|
||
```bash
|
||
# UFW防火墙
|
||
sudo ufw enable
|
||
sudo ufw allow 22/tcp # SSH
|
||
sudo ufw allow 80/tcp # HTTP
|
||
sudo ufw allow 443/tcp # HTTPS
|
||
sudo ufw deny 5432/tcp # 禁止外部访问数据库
|
||
sudo ufw deny 6379/tcp # 禁止外部访问Redis
|
||
```
|
||
|
||
### 10.2 SSL证书(Let's Encrypt)
|
||
```bash
|
||
# 安装Certbot
|
||
sudo apt-get install certbot python3-certbot-nginx
|
||
|
||
# 获取证书
|
||
sudo certbot --nginx -d api.example.com
|
||
|
||
# 自动续期
|
||
sudo certbot renew --dry-run
|
||
|
||
# 添加定时任务
|
||
0 3 * * * certbot renew --quiet
|
||
```
|
||
|
||
### 10.3 应用安全配置
|
||
```csharp
|
||
// Program.cs
|
||
builder.Services.AddHsts(options =>
|
||
{
|
||
options.MaxAge = TimeSpan.FromDays(365);
|
||
options.IncludeSubDomains = true;
|
||
options.Preload = true;
|
||
});
|
||
|
||
builder.Services.AddHttpsRedirection(options =>
|
||
{
|
||
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
|
||
options.HttpsPort = 443;
|
||
});
|
||
|
||
// 添加安全头
|
||
app.Use(async (context, next) =>
|
||
{
|
||
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||
context.Response.Headers.Add("X-Frame-Options", "DENY");
|
||
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
|
||
context.Response.Headers.Add("Referrer-Policy", "no-referrer");
|
||
await next();
|
||
});
|
||
```
|
||
|
||
## 11. 性能优化
|
||
|
||
### 11.1 数据库连接池
|
||
```json
|
||
{
|
||
"ConnectionStrings": {
|
||
"DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=user;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=100;ConnectionLifetime=300"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 11.2 Redis连接池
|
||
```csharp
|
||
services.AddStackExchangeRedisCache(options =>
|
||
{
|
||
options.Configuration = configuration["Redis:Configuration"];
|
||
options.InstanceName = "TakeoutSaaS:";
|
||
});
|
||
```
|
||
|
||
### 11.3 响应压缩
|
||
```csharp
|
||
builder.Services.AddResponseCompression(options =>
|
||
{
|
||
options.EnableForHttps = true;
|
||
options.Providers.Add<GzipCompressionProvider>();
|
||
options.Providers.Add<BrotliCompressionProvider>();
|
||
});
|
||
```
|
||
|
||
## 12. 故障恢复
|
||
|
||
### 12.1 数据库恢复
|
||
```bash
|
||
# 从备份恢复
|
||
pg_restore -h localhost -U takeout_user -d takeout_saas -v /backup/full_20240101.dump
|
||
|
||
# PITR恢复到指定时间点
|
||
# 1. 停止数据库
|
||
sudo systemctl stop postgresql
|
||
|
||
# 2. 恢复基础备份
|
||
rm -rf /var/lib/postgresql/16/main/*
|
||
tar -xzf /backup/base_backup.tar.gz -C /var/lib/postgresql/16/main/
|
||
|
||
# 3. 配置recovery.conf
|
||
restore_command = 'cp /backup/wal_archive/%f %p'
|
||
recovery_target_time = '2024-01-01 12:00:00'
|
||
|
||
# 4. 启动数据库
|
||
sudo systemctl start postgresql
|
||
```
|
||
|
||
### 12.2 应用回滚
|
||
```bash
|
||
# Docker回滚到上一个版本
|
||
docker-compose down
|
||
docker-compose up -d --force-recreate --no-deps api
|
||
|
||
# 或使用特定版本
|
||
docker pull takeout-saas-api:previous-version
|
||
docker-compose up -d
|
||
```
|
||
|