第十九章:部署与DevOps
一、发布准备
1.1 发布模式概述
.NET 应用程序支持多种发布模式,每种模式适用于不同的部署场景:
| 发布模式 |
说明 |
优点 |
缺点 |
| Framework-dependent(框架依赖) |
依赖目标机器上已安装的 .NET 运行时 |
包体小、共享运行时更新 |
需要预安装运行时 |
| Self-contained(独立部署) |
包含 .NET 运行时 |
无需安装运行时、版本隔离 |
包体大(约60-80MB) |
| Single-file(单文件) |
打包为单个可执行文件 |
分发简单 |
首次启动稍慢 |
| ReadyToRun(R2R) |
预编译为原生代码 |
启动更快 |
包体更大 |
| Trimmed(裁剪) |
移除未使用的程序集 |
包体更小 |
可能有兼容问题 |
1.2 发布命令
# 框架依赖发布(默认)
dotnet publish -c Release -o ./publish
# 独立部署(Windows x64)
dotnet publish -c Release -r win-x64 --self-contained true -o ./publish
# 独立部署(Linux x64)
dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish
# 单文件发布
dotnet publish -c Release -r linux-x64 --self-contained true \
-p:PublishSingleFile=true -o ./publish
# 裁剪发布
dotnet publish -c Release -r linux-x64 --self-contained true \
-p:PublishTrimmed=true -o ./publish
# ReadyToRun 发布
dotnet publish -c Release -r linux-x64 --self-contained true \
-p:PublishReadyToRun=true -o ./publish
1.3 项目文件发布配置
<!-- .csproj 文件中的发布配置 -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishTrimmed>false</PublishTrimmed>
<!-- 设置程序集版本 -->
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<!-- 生成 XML 文档(Swagger 需要) -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>
二、IIS 部署
2.1 IIS 环境准备
# 安装 ASP.NET Core Hosting Bundle(Windows Server)
# 下载地址:https://dotnet.microsoft.com/download/dotnet
# 安装 IIS(如果尚未安装)
Install-WindowsFeature -name Web-Server -IncludeManagementTools
# 安装 ASP.NET Core Module
# 下载并安装 .NET Hosting Bundle
2.2 web.config 配置
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModuleV2"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT"
value="Production" />
<environmentVariable name="DOTNET_PRINT_TELEMETRY_MESSAGE"
value="false" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>
2.3 IIS 应用池配置
| 配置项 |
推荐值 |
说明 |
| .NET CLR 版本 |
无托管代码 |
ASP.NET Core 不需要 CLR |
| 托管管道模式 |
集成 |
使用 IIS 集成管道 |
| 启动模式 |
AlwaysRunning |
避免冷启动延迟 |
| 回收间隔 |
0(禁用) |
避免定时回收导致的中断 |
| 闲置超时 |
0(禁用) |
保持应用持续运行 |
| 进程模型 |
InProcess |
性能更好 |
// Program.cs - IIS 集成配置
var builder = WebApplication.CreateBuilder(args);
// 配置 Kestrel 和 IIS 集成
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100MB
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2);
});
var app = builder.Build();
// 在 IIS 后面时,使用转发头部
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto
});
app.Run();
三、Kestrel 独立部署
3.1 Linux 环境部署
# 1. 安装 .NET 运行时(Ubuntu/Debian)
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 aspnetcore-runtime-8.0
# 2. 创建应用目录
sudo mkdir -p /var/www/myapp
sudo chown -R www-data:www-data /var/www/myapp
# 3. 上传发布文件
scp -r ./publish/* user@server:/var/www/myapp/
# 4. 测试运行
cd /var/www/myapp
dotnet MyApp.dll --urls "http://0.0.0.0:5000"
3.2 Systemd 服务配置
# /etc/systemd/system/myapp.service
[Unit]
Description=My Furion Application
After=network.target
[Service]
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll
Restart=always
RestartSec=10
SyslogIdentifier=myapp
User=www-data
Group=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
# 资源限制
LimitNOFILE=65536
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
# 启用并启动服务
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
# 查看服务状态
sudo systemctl status myapp
# 查看日志
sudo journalctl -u myapp -f
3.3 Kestrel 高级配置
// Program.cs - Kestrel 配置
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
// HTTP 端口
options.ListenAnyIP(5000);
// HTTPS 端口
options.ListenAnyIP(5001, listenOptions =>
{
listenOptions.UseHttps("/etc/ssl/certs/myapp.pfx", "password");
});
// 性能配置
options.Limits.MaxConcurrentConnections = 1000;
options.Limits.MaxConcurrentUpgradedConnections = 1000;
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50MB
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
四、Nginx 反向代理配置
4.1 Nginx 基本配置
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com www.example.com;
# 强制 HTTPS 重定向
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL 证书
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 日志
access_log /var/log/nginx/myapp_access.log;
error_log /var/log/nginx/myapp_error.log;
# 请求体大小限制
client_max_body_size 100M;
# 反向代理
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
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_cache_bypass $http_upgrade;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 静态文件缓存
location /static/ {
alias /var/www/myapp/wwwroot/;
expires 7d;
add_header Cache-Control "public, immutable";
}
# 健康检查端点
location /health {
proxy_pass http://127.0.0.1:5000/health;
access_log off;
}
}
4.2 Nginx 负载均衡
# 上游服务器组
upstream myapp_cluster {
least_conn; # 最少连接数策略
server 127.0.0.1:5000 weight=3;
server 127.0.0.1:5001 weight=2;
server 127.0.0.1:5002 weight=1;
# 健康检查
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.com;
location / {
proxy_pass http://myapp_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
五、Docker 容器化部署
5.1 Dockerfile 编写
# 多阶段构建 Dockerfile
# 阶段1:构建
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 复制项目文件并还原依赖(利用 Docker 缓存层)
COPY ["MyApp.Web.Entry/MyApp.Web.Entry.csproj", "MyApp.Web.Entry/"]
COPY ["MyApp.Application/MyApp.Application.csproj", "MyApp.Application/"]
COPY ["MyApp.Core/MyApp.Core.csproj", "MyApp.Core/"]
COPY ["MyApp.EntityFramework.Core/MyApp.EntityFramework.Core.csproj", "MyApp.EntityFramework.Core/"]
RUN dotnet restore "MyApp.Web.Entry/MyApp.Web.Entry.csproj"
# 复制所有源代码并构建
COPY . .
WORKDIR "/src/MyApp.Web.Entry"
RUN dotnet build -c Release -o /app/build
# 阶段2:发布
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
# 阶段3:运行
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 安装中文字体(PDF 生成等场景需要)
RUN apt-get update && apt-get install -y fonts-wqy-zenhei && \
rm -rf /var/lib/apt/lists/*
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=publish /app/publish .
# 设置环境变量
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:80/health || exit 1
USER appuser
ENTRYPOINT ["dotnet", "MyApp.Web.Entry.dll"]
5.2 .dockerignore 文件
**/bin/
**/obj/
**/out/
**/.vs/
**/.vscode/
**/node_modules/
**/*.user
**/*.suo
**/Thumbs.db
.git
.gitignore
README.md
docker-compose*.yml
Dockerfile*
5.3 Docker Compose 编排
# docker-compose.yml
version: '3.8'
services:
# 应用服务
myapp:
build:
context: .
dockerfile: Dockerfile
container_name: myapp
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__Default=Server=db;Port=5432;Database=myapp;User Id=postgres;Password=postgres123;
- Redis__Connection=redis:6379,password=redis123
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- myapp-network
volumes:
- app-logs:/app/logs
- app-uploads:/app/uploads
# PostgreSQL 数据库
db:
image: postgres:16-alpine
container_name: myapp-db
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- myapp-network
# Redis 缓存
redis:
image: redis:7-alpine
container_name: myapp-redis
command: redis-server --requirepass redis123
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- myapp-network
# Nginx 反向代理
nginx:
image: nginx:alpine
container_name: myapp-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
depends_on:
- myapp
restart: unless-stopped
networks:
- myapp-network
volumes:
postgres-data:
redis-data:
app-logs:
app-uploads:
networks:
myapp-network:
driver: bridge
5.4 Docker 常用操作
# 构建镜像
docker build -t myapp:latest .
# 使用 Docker Compose 启动
docker compose up -d
# 查看日志
docker compose logs -f myapp
# 重新构建并启动
docker compose up -d --build
# 停止并清理
docker compose down
# 查看容器资源使用
docker stats
六、Kubernetes 部署
6.1 Deployment 配置
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: registry.example.com/myapp:latest
ports:
- containerPort: 80
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db-connection-string
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
imagePullSecrets:
- name: registry-credentials
6.2 Service 和 Ingress 配置
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
namespace: production
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: myapp-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80
6.3 ConfigMap 和 Secret
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: production
data:
appsettings.Production.json: |
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Redis": {
"Connection": "redis-service:6379"
}
}
---
# k8s/secret.yaml(使用 base64 编码)
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: production
type: Opaque
data:
db-connection-string: U2VydmVyPXBvc3RncmVzLXNlcnZpY2U7... # base64 编码
七、CI/CD 流水线
7.1 GitHub Actions
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: $
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Run tests
run: dotnet test --no-build -c Release --verbosity normal
--collect:"XPlat Code Coverage"
- name: Publish
run: dotnet publish -c Release -o ./publish --no-restore
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-publish
path: ./publish
docker-build:
needs: build-and-test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: $
username: $
password: $
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
$/$:latest
$/$:$
deploy:
needs: docker-build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: $
username: $
key: $
script: |
cd /var/www/myapp
docker compose pull
docker compose up -d --force-recreate
docker image prune -f
7.2 Azure DevOps Pipeline
# azure-pipelines.yml
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
stages:
- stage: Build
displayName: '构建阶段'
jobs:
- job: BuildJob
displayName: '编译和测试'
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- script: dotnet restore
displayName: '还原依赖'
- script: dotnet build --configuration $(buildConfiguration) --no-restore
displayName: '编译项目'
- script: dotnet test --configuration $(buildConfiguration) --no-build
displayName: '运行测试'
- script: dotnet publish --configuration $(buildConfiguration)
--output $(Build.ArtifactStagingDirectory)
displayName: '发布项目'
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: Deploy
displayName: '部署阶段'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployJob
displayName: '部署到生产环境'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- script: echo "部署到生产服务器"
displayName: '执行部署'
八、环境变量与配置管理
8.1 多环境配置
// Program.cs - 环境配置加载
var builder = WebApplication.CreateBuilder(args);
// 配置加载顺序(后加载的覆盖先加载的)
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. 用户机密(仅开发环境)
// 4. 环境变量
// 5. 命令行参数
builder.Configuration
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
// appsettings.json - 默认配置
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": ""
}
}
// appsettings.Development.json - 开发环境
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"ConnectionStrings": {
"Default": "Server=localhost;Database=MyApp_Dev;Uid=root;Pwd=123456;"
},
"Redis": {
"Connection": "localhost:6379"
}
}
// appsettings.Production.json - 生产环境
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"Default": "${DB_CONNECTION_STRING}"
},
"Redis": {
"Connection": "${REDIS_CONNECTION}"
}
}
8.2 配置安全管理
// 使用用户机密(开发环境)
// dotnet user-secrets init
// dotnet user-secrets set "ConnectionStrings:Default" "Server=..."
// 使用 Azure Key Vault(生产环境)
builder.Configuration.AddAzureKeyVault(
new Uri("https://myapp-vault.vault.azure.net/"),
new DefaultAzureCredential());
// 使用环境变量覆盖配置
// export ConnectionStrings__Default="Server=prod-db;..."
// export Redis__Connection="prod-redis:6379"
九、健康检查
9.1 配置健康检查
// Program.cs - 健康检查配置
builder.Services.AddHealthChecks()
// 数据库健康检查
.AddNpgSql(
builder.Configuration.GetConnectionString("Default")!,
name: "postgresql",
tags: new[] { "db", "critical" })
// Redis 健康检查
.AddRedis(
builder.Configuration["Redis:Connection"]!,
name: "redis",
tags: new[] { "cache", "critical" })
// 自定义健康检查
.AddCheck<DiskSpaceHealthCheck>(
"disk-space",
tags: new[] { "infrastructure" })
// 外部 API 健康检查
.AddUrlGroup(
new Uri("https://api.example.com/health"),
name: "external-api",
tags: new[] { "external" });
var app = builder.Build();
// 映射健康检查端点
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = WriteHealthCheckResponse
});
// 就绪检查(仅检查关键依赖)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("critical"),
ResponseWriter = WriteHealthCheckResponse
});
// 存活检查
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // 不检查任何依赖,仅确认应用存活
});
9.2 自定义健康检查
/// <summary>
/// 磁盘空间健康检查
/// </summary>
public class DiskSpaceHealthCheck : IHealthCheck
{
private readonly long _minimumFreeMB;
public DiskSpaceHealthCheck(long minimumFreeMB = 500)
{
_minimumFreeMB = minimumFreeMB;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var drives = DriveInfo.GetDrives()
.Where(d => d.IsReady && d.DriveType == DriveType.Fixed);
foreach (var drive in drives)
{
var freeMB = drive.AvailableFreeSpace / (1024 * 1024);
if (freeMB < _minimumFreeMB)
{
return Task.FromResult(HealthCheckResult.Unhealthy(
$"磁盘 {drive.Name} 空间不足: {freeMB}MB 剩余" +
$"(最低要求: {_minimumFreeMB}MB)"));
}
}
return Task.FromResult(HealthCheckResult.Healthy("磁盘空间充足"));
}
}
/// <summary>
/// 健康检查响应格式化
/// </summary>
static async Task WriteHealthCheckResponse(
HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var response = new
{
Status = report.Status.ToString(),
TotalDuration = report.TotalDuration.TotalMilliseconds + "ms",
Checks = report.Entries.Select(e => new
{
Name = e.Key,
Status = e.Value.Status.ToString(),
Duration = e.Value.Duration.TotalMilliseconds + "ms",
Description = e.Value.Description,
Error = e.Value.Exception?.Message
})
};
await context.Response.WriteAsJsonAsync(response);
}
十、应用监控
10.1 Prometheus + Grafana 监控
// 安装 NuGet 包
// dotnet add package prometheus-net.AspNetCore
// Program.cs - 配置 Prometheus 指标
using Prometheus;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 启用 HTTP 请求指标收集
app.UseHttpMetrics();
// 暴露 Prometheus 指标端点
app.MapMetrics(); // 默认路径: /metrics
app.Run();
10.2 自定义业务指标
/// <summary>
/// 业务指标收集服务
/// </summary>
public class BusinessMetricsService
{
// 订单创建计数器
private static readonly Counter OrderCreatedCounter = Metrics
.CreateCounter("myapp_orders_created_total", "创建的订单总数",
new CounterConfiguration
{
LabelNames = new[] { "payment_method" }
});
// 请求处理时间直方图
private static readonly Histogram RequestDuration = Metrics
.CreateHistogram("myapp_request_duration_seconds", "请求处理时间",
new HistogramConfiguration
{
LabelNames = new[] { "endpoint" },
Buckets = Histogram.LinearBuckets(0.1, 0.1, 10)
});
// 在线用户数仪表盘
private static readonly Gauge OnlineUsers = Metrics
.CreateGauge("myapp_online_users", "当前在线用户数");
public void RecordOrderCreated(string paymentMethod)
{
OrderCreatedCounter.WithLabels(paymentMethod).Inc();
}
public IDisposable MeasureRequestDuration(string endpoint)
{
return RequestDuration.WithLabels(endpoint).NewTimer();
}
public void SetOnlineUsers(int count)
{
OnlineUsers.Set(count);
}
}
十一、日志收集
11.1 Serilog 配置
// 安装 NuGet 包
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.File
// dotnet add package Serilog.Sinks.Seq
// Program.cs - Serilog 配置
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
// 控制台输出
.WriteTo.Console()
// 文件输出(按日期滚动)
.WriteTo.File(
path: "logs/myapp-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} " +
"[{Level:u3}] {Message:lj}{NewLine}{Exception}")
// Seq 集中日志平台
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var app = builder.Build();
// 请求日志中间件
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} 响应 {StatusCode} 耗时 {Elapsed:0.0000}ms";
});
app.Run();
11.2 ELK Stack 集成
// appsettings.json - Elasticsearch Sink 配置
{
"Serilog": {
"WriteTo": [
{
"Name": "Elasticsearch",
"Args": {
"nodeUris": "http://elasticsearch:9200",
"indexFormat": "myapp-logs-{0:yyyy.MM.dd}",
"autoRegisterTemplate": true
}
}
]
}
}
十二、性能优化与压测
12.1 性能优化清单
| 优化项 |
方法 |
预期效果 |
| 启用响应压缩 |
UseResponseCompression |
减少 30-70% 传输量 |
| 启用响应缓存 |
UseResponseCaching |
减少重复计算 |
| 数据库连接池 |
配置连接池大小 |
减少连接开销 |
| 异步编程 |
async/await |
提高并发处理能力 |
| 缓存热点数据 |
Redis/内存缓存 |
减少数据库查询 |
| 启用 HTTP/2 |
Kestrel 配置 |
提高传输效率 |
// 响应压缩配置
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
app.UseResponseCompression();
十三、蓝绿部署与金丝雀发布
13.1 蓝绿部署
# Nginx 蓝绿部署配置
# 通过切换 upstream 实现零停机部署
# 蓝色环境(当前生产)
upstream blue {
server 192.168.1.10:5000;
server 192.168.1.11:5000;
}
# 绿色环境(新版本)
upstream green {
server 192.168.1.20:5000;
server 192.168.1.21:5000;
}
# 当前活跃环境(修改此行切换)
upstream active {
server 192.168.1.10:5000; # 指向蓝色
server 192.168.1.11:5000;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://active;
}
}
13.2 金丝雀发布
# 金丝雀发布 - 按权重分配流量
upstream canary {
server 192.168.1.10:5000 weight=90; # 旧版本 90%
server 192.168.1.20:5000 weight=10; # 新版本 10%
}
十四、安全加固
14.1 安全配置清单
// Program.cs - 安全加固配置
var app = builder.Build();
// HTTPS 重定向
app.UseHttpsRedirection();
// HSTS(HTTP Strict Transport Security)
app.UseHsts();
// 安全头部
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
context.Response.Headers["Content-Security-Policy"] = "default-src 'self'";
context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By");
await next();
});
// 速率限制
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
context => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
app.UseRateLimiter();
十五、国产化环境部署
15.1 麒麟 OS 部署
# 麒麟 V10(ARM64/x86_64)安装 .NET
# 1. 下载 .NET SDK/Runtime(ARM64 版本)
wget https://download.visualstudio.microsoft.com/download/.../dotnet-runtime-8.0.x-linux-arm64.tar.gz
# 2. 解压安装
sudo mkdir -p /usr/share/dotnet
sudo tar zxf dotnet-runtime-8.0.x-linux-arm64.tar.gz -C /usr/share/dotnet
sudo ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet
# 3. 验证安装
dotnet --info
# 4. 发布为指定平台
dotnet publish -c Release -r linux-arm64 --self-contained true -o ./publish
15.2 达梦数据库适配
// 使用达梦数据库(DM8)
// dotnet add package Dm
// 配置达梦数据库连接
builder.Services.AddDbContext<DefaultDbContext>(options =>
{
options.UseDm(builder.Configuration.GetConnectionString("DmDatabase"));
});
// appsettings.json
{
"ConnectionStrings": {
"DmDatabase": "Server=192.168.1.100;Port=5236;Database=MYAPP;User=SYSDBA;Password=SYSDBA001;"
}
}
十六、运维最佳实践
16.1 运维检查清单
| 类别 |
检查项 |
建议 |
| 安全 |
HTTPS 证书有效期 |
自动续期(Let’s Encrypt) |
| 安全 |
依赖漏洞扫描 |
定期执行 dotnet list package --vulnerable |
| 性能 |
数据库慢查询 |
配置慢查询监控告警 |
| 性能 |
内存使用率 |
设置阈值告警(>80%) |
| 可用性 |
健康检查 |
配置自动重启策略 |
| 可用性 |
数据备份 |
每日自动备份 + 异地备份 |
| 日志 |
日志轮转 |
保留30天,自动清理 |
| 日志 |
错误日志告警 |
关键错误实时通知 |
16.2 自动化运维脚本
#!/bin/bash
# deploy.sh - 自动化部署脚本
set -e
APP_NAME="myapp"
DEPLOY_DIR="/var/www/${APP_NAME}"
BACKUP_DIR="/var/backup/${APP_NAME}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "=== 开始部署 ${APP_NAME} ==="
# 1. 备份当前版本
echo ">>> 备份当前版本..."
mkdir -p ${BACKUP_DIR}
cp -r ${DEPLOY_DIR} ${BACKUP_DIR}/${TIMESTAMP}
# 2. 停止服务
echo ">>> 停止服务..."
sudo systemctl stop ${APP_NAME}
# 3. 部署新版本
echo ">>> 部署新版本..."
cp -r ./publish/* ${DEPLOY_DIR}/
# 4. 启动服务
echo ">>> 启动服务..."
sudo systemctl start ${APP_NAME}
# 5. 健康检查
echo ">>> 执行健康检查..."
sleep 5
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/health)
if [ "$HTTP_CODE" = "200" ]; then
echo "=== 部署成功!==="
else
echo "=== 健康检查失败,回滚中... ==="
sudo systemctl stop ${APP_NAME}
cp -r ${BACKUP_DIR}/${TIMESTAMP}/* ${DEPLOY_DIR}/
sudo systemctl start ${APP_NAME}
echo "=== 回滚完成 ==="
exit 1
fi
# 6. 清理旧备份(保留最近5个)
cd ${BACKUP_DIR}
ls -t | tail -n +6 | xargs -r rm -rf
echo "=== 部署完成 ==="
通过本章的学习,你已经全面掌握了 Furion 应用从开发到部署的完整流程,包括多种部署方式、CI/CD 流水线、监控告警、安全加固等关键环节。合理的部署策略和运维实践能够确保应用的高可用性和稳定性。在下一章中,我们将总结最佳实践与常见问题。