第35章 云原生开发

章节摘要

本章学习云原生开发的核心概念和实践,掌握如何集成 Azure 和 AWS 的云服务,构建无服务器架构,使用云数据库和存储,实现完整的可观测性(日志、指标、追踪),最后通过部署到云平台的实战项目,学会构建真正适合云环境的现代化应用,充分释放云计算的潜力。

本章目录


开篇引入

云原生应用是专门为云环境设计的应用程序,它们充分利用云平台的弹性、可扩展性、托管服务和按需付费的特性。与传统的"提升与转移"(Lift and Shift)策略不同,云原生开发要求我们从架构设计、开发方式到运维模式都进行根本性的转变。

本章将带你深入云原生开发的世界,学习如何集成 Azure 和 AWS 的云服务,构建无服务器架构,使用云数据库和存储,以及实现完整的可观测性。通过本章的学习,你将能够构建真正适合云环境的现代化应用,充分释放云计算的潜力。


35.1 云原生概念

什么是云原生

云原生(Cloud Native) 是一种构建和运行应用程序的方法,它充分利用云计算模型的优势。云原生计算基金会(CNCF)将云原生定义为:

云原生技术使组织能够在现代动态环境(如公有云、私有云和混合云)中构建和运行可扩展的应用程序。容器、服务网格、微服务、不可变基础设施和声明式 API 是这种方法的典型代表。

核心特征

  1. 容器化(Containerization):应用及其依赖项打包在容器中
  2. 动态编排(Dynamic Orchestration):使用 Kubernetes 等工具自动管理容器
  3. 微服务架构(Microservices):应用拆分为松耦合的小服务
  4. 弹性伸缩(Elastic Scaling):根据负载自动扩缩容
  5. 持续交付(Continuous Delivery):自动化的 CI/CD 流水线
  6. DevOps 文化(DevOps Culture):开发和运维团队紧密协作

云原生 vs 传统应用

特性 传统应用(Lift and Shift) 云原生应用
架构 单体应用 微服务架构
部署 虚拟机 容器 + Kubernetes
扩展 垂直扩展(升级硬件) 水平扩展(增加实例)
状态 有状态(Stateful) 无状态(Stateless)
配置 硬编码或本地配置文件 外部化配置(ConfigMap、Secrets)
数据库 自建数据库 托管云数据库(PaaS)
监控 传统监控工具 云原生可观测性(日志、指标、追踪)
成本模式 固定成本(购买服务器) 按需付费(Pay-as-you-go)

云原生的十二要素应用(The Twelve-Factor App)

这是构建云原生应用的最佳实践指南:

I. 代码库(Codebase)

  • 一个代码库,多个部署环境
# 同一份代码部署到不同环境
git clone https://github.com/myorg/myapp.git
cd myapp

# 部署到开发环境
dotnet publish -c Release -o ./publish
docker build -t myapp:dev .
kubectl apply -f k8s/dev/

# 部署到生产环境(同样的代码)
docker build -t myapp:prod .
kubectl apply -f k8s/prod/

II. 依赖(Dependencies)

  • 显式声明和隔离依赖
<!-- MyApp.csproj - 显式声明所有依赖 -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!-- 所有依赖都在项目文件中明确声明 -->
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" />
    <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
    <PackageReference Include="Azure.Identity" Version="1.10.4" />
  </ItemGroup>
</Project>

III. 配置(Config)

  • 在环境中存储配置,不硬编码
// ❌ 错误:硬编码配置
public class PaymentService
{
    private const string ApiKey = "sk_live_123..."; // 不要这样做!
}

// ✅ 正确:从环境变量读取配置
public class PaymentService
{
    private readonly string _apiKey;
    
    public PaymentService(IConfiguration configuration)
    {
        _apiKey = configuration["Payment:ApiKey"] 
            ?? throw new InvalidOperationException("Payment:ApiKey not configured");
    }
}
# Kubernetes ConfigMap 和 Secret
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  ASPNETCORE_ENVIRONMENT: "Production"
  Logging__LogLevel__Default: "Information"
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  Payment__ApiKey: c2tfbGl2ZV8xMjM...  # base64 编码

IV. 后端服务(Backing Services)

  • 将后端服务视为附加资源
// 通过配置连接后端服务,而非硬编码
var builder = WebApplication.CreateBuilder(args);

// 数据库
var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

// 缓存
var redisConnection = builder.Configuration["Redis:Connection"];
builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = redisConnection);

// 消息队列
var rabbitMqHost = builder.Configuration["RabbitMQ:Host"];
builder.Services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(rabbitMqHost);
    });
});

V. 构建、发布、运行(Build, Release, Run)

  • 严格分离构建、发布和运行阶段
# GitHub Actions - 三个独立阶段
name: Build, Release, Run

jobs:
  # 阶段 1:构建
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build
      run: dotnet build -c Release
    - name: Test
      run: dotnet test
    - name: Publish
      run: dotnet publish -c Release -o ./publish
    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: app-artifact
        path: ./publish

  # 阶段 2:发布(构建 Docker 镜像)
  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/download-artifact@v4
      with:
        name: app-artifact
    - name: Build Docker image
      run: docker build -t myapp:${{ github.sha }} .
    - name: Push to registry
      run: docker push myapp:${{ github.sha }}

  # 阶段 3:运行(部署到 Kubernetes)
  run:
    needs: release
    runs-on: ubuntu-latest
    steps:
    - name: Deploy to K8s
      run: kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}

VI. 进程(Processes)

  • 应用作为无状态进程运行
// ❌ 错误:在进程内存中存储会话状态
public class OrderController : ControllerBase
{
    private static Dictionary<string, Order> _orders = new(); // 不要这样做!
    
    [HttpPost]
    public IActionResult CreateOrder(Order order)
    {
        _orders[order.Id] = order;
        return Ok();
    }
}

// ✅ 正确:使用分布式缓存存储会话状态
public class OrderController : ControllerBase
{
    private readonly IDistributedCache _cache;
    
    public OrderController(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateOrder(Order order)
    {
        var json = JsonSerializer.Serialize(order);
        await _cache.SetStringAsync($"order:{order.Id}", json, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });
        return Ok();
    }
}

VII. 端口绑定(Port Binding)

  • 通过端口绑定导出服务
// Program.cs - 从环境变量读取端口配置
var builder = WebApplication.CreateBuilder(args);

// ✅ 从环境变量读取端口
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(int.Parse(port));
});

var app = builder.Build();
app.Run();
# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0

WORKDIR /app
COPY publish/ .

# 声明应用监听的端口
ENV PORT=8080
EXPOSE 8080

ENTRYPOINT ["dotnet", "MyApp.dll"]

VIII. 并发(Concurrency)

  • 通过进程模型进行扩展
# Kubernetes - 水平扩展
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3  # ✅ 运行多个实例
  selector:
    matchLabels:
      app: myapp
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:1.0.0
---
# 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

IX. 易处理(Disposability)

  • 快速启动和优雅终止
// Program.cs - 优雅关闭
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHostedService<GracefulShutdownService>();

var app = builder.Build();

// 注册应用生命周期事件
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();

lifetime.ApplicationStopping.Register(() =>
{
    Console.WriteLine("应用正在停止,开始清理资源...");
});

lifetime.ApplicationStopped.Register(() =>
{
    Console.WriteLine("应用已停止");
});

app.Run();

// 后台服务实现优雅关闭
public class GracefulShutdownService : BackgroundService
{
    private readonly ILogger<GracefulShutdownService> _logger;
    
    public GracefulShutdownService(ILogger<GracefulShutdownService> logger)
    {
        _logger = logger;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }
        
        // 收到停止信号后,执行清理工作
        _logger.LogInformation("收到停止信号,开始清理...");
        
        // 完成正在处理的请求
        await Task.Delay(TimeSpan.FromSeconds(5)); // 等待最多 5 秒
        
        _logger.LogInformation("清理完成");
    }
}
# Kubernetes - 配置优雅终止
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30  # ✅ 给应用 30 秒时间优雅关闭
      containers:
      - name: myapp
        image: myapp:1.0.0
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]  # 延迟 15 秒,确保负载均衡器已移除此实例

X. 开发环境与生产环境等价(Dev/Prod Parity)

  • 开发、预发布和生产环境保持一致
# Docker Compose - 本地开发环境
version: '3.8'
services:
  api:
    build: .
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Server=db;Database=myapp;User=sa;Password=Pass@word
    depends_on:
      - db
      - redis
  
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest  # ✅ 使用与生产相同的数据库版本
    environment:
      SA_PASSWORD: "Pass@word"
      ACCEPT_EULA: "Y"
  
  redis:
    image: redis:7-alpine  # ✅ 使用与生产相同的 Redis 版本

XI. 日志(Logs)

  • 将日志视为事件流
// Program.cs - 配置日志输出到标准输出
var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();  // ✅ 输出到 stdout
builder.Logging.AddJsonConsole(options =>
{
    options.JsonWriterOptions = new JsonWriterOptions { Indented = false };
});

var app = builder.Build();

// ❌ 不要写入本地文件
// File.AppendAllText("/var/log/app.log", message);

// ✅ 使用 ILogger,自动输出到 stdout
app.Logger.LogInformation("应用启动完成");

app.Run();

XII. 管理进程(Admin Processes)

  • 管理任务作为一次性进程运行
// 数据库迁移作为独立的一次性任务
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

// 如果指定了 --migrate 参数,执行迁移后退出
if (args.Contains("--migrate"))
{
    using var scope = app.Services.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    Console.WriteLine("开始数据库迁移...");
    await dbContext.Database.MigrateAsync();
    Console.WriteLine("迁移完成");
    
    return; // 退出,不启动 Web 服务器
}

app.Run();
# Kubernetes Job - 运行数据库迁移
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: myapp:1.0.0
        command: ["dotnet", "MyApp.dll", "--migrate"]
        env:
        - name: ConnectionStrings__Default
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: db-connection
      restartPolicy: OnFailure
  backoffLimit: 3

云原生成熟度模型

应用的云原生化是一个渐进的过程:

Level 0: 传统部署
├─ 物理服务器或虚拟机
├─ 手动部署和配置
└─ 单体应用

Level 1: 虚拟化
├─ 使用虚拟机
├─ 基础的自动化脚本
└─ 开始模块化

Level 2: 容器化
├─ 应用打包为容器
├─ 使用 Docker
└─ 基础编排(Docker Compose)

Level 3: 编排
├─ Kubernetes 管理容器
├─ 自动扩缩容
└─ 服务发现和负载均衡

Level 4: 云原生
├─ 微服务架构
├─ 完整的 CI/CD
├─ 可观测性(日志、指标、追踪)
├─ 使用云托管服务(数据库、缓存、消息队列)
└─ 混沌工程和弹性设计

Level 5: 无服务器
├─ Serverless 函数(Azure Functions、AWS Lambda)
├─ 事件驱动架构
└─ 按调用次数付费

云原生架构模式

1. 服务网格(Service Mesh)

# 使用 Istio 作为服务网格
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: myapi
spec:
  hosts:
  - myapi
  http:
  - match:
    - headers:
        x-version:
          exact: "v2"
    route:
    - destination:
        host: myapi
        subset: v2
  - route:
    - destination:
        host: myapi
        subset: v1
      weight: 90
    - destination:
        host: myapi
        subset: v2
      weight: 10  # 10% 流量到 v2(金丝雀发布)

2. API 网关模式

// 使用 YARP(Yet Another Reverse Proxy)作为 API 网关
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// 在网关层添加认证、限流等横切关注点
app.UseAuthentication();
app.UseRateLimiter();

app.MapReverseProxy();
app.Run();
// appsettings.json
{
  "ReverseProxy": {
    "Routes": {
      "user-route": {
        "ClusterId": "user-service",
        "Match": {
          "Path": "/api/users/{**catch-all}"
        }
      },
      "order-route": {
        "ClusterId": "order-service",
        "Match": {
          "Path": "/api/orders/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "user-service": {
        "Destinations": {
          "destination1": {
            "Address": "http://user-service:80"
          }
        }
      },
      "order-service": {
        "Destinations": {
          "destination1": {
            "Address": "http://order-service:80"
          }
        }
      }
    }
  }
}

3. 边车模式(Sidecar Pattern)

# Pod 中包含主容器和边车容器
apiVersion: v1
kind: Pod
metadata:
  name: myapp-with-sidecar
spec:
  containers:
  # 主应用容器
  - name: app
    image: myapp:1.0.0
    ports:
    - containerPort: 8080
  
  # 边车容器:日志收集
  - name: log-collector
    image: fluent/fluent-bit:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log
  
  # 边车容器:指标导出
  - name: metrics-exporter
    image: prom/statsd-exporter:latest
    ports:
    - containerPort: 9102
  
  volumes:
  - name: logs
    emptyDir: {}

35.2 Azure 服务集成

Azure 是微软提供的云计算平台,与 .NET 生态系统深度集成,是 C# 开发者的首选云平台。本节将介绍如何在 ASP.NET Core 应用中集成常用的 Azure 服务。

Azure SDK for .NET

Azure 为 .NET 提供了统一的 SDK,所有服务客户端都遵循相同的设计模式。

安装 SDK

# 安装 Azure Identity(统一的身份验证)
dotnet add package Azure.Identity

# 安装具体服务的 SDK
dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Data.Tables
dotnet add package Azure.Messaging.ServiceBus
dotnet add package Azure.Security.KeyVault.Secrets

统一的身份验证

using Azure.Identity;

// DefaultAzureCredential 自动选择最合适的身份验证方式:
// 1. 本地开发:使用 Azure CLI 或 Visual Studio 的登录凭据
// 2. Azure 环境:使用托管标识(Managed Identity)
var credential = new DefaultAzureCredential();

// 所有 Azure 服务客户端都使用相同的凭据
var blobServiceClient = new BlobServiceClient(
    new Uri("https://mystorageaccount.blob.core.windows.net"),
    credential);

var secretClient = new SecretClient(
    new Uri("https://mykeyvault.vault.azure.net"),
    credential);

Azure App Service

Azure App Service 是完全托管的 Web 应用托管平台。

部署到 Azure App Service

# 使用 Azure CLI 部署
az login
az group create --name myapp-rg --location eastus
az appservice plan create --name myapp-plan --resource-group myapp-rg --sku B1 --is-linux
az webapp create --name myapp --resource-group myapp-rg --plan myapp-plan --runtime "DOTNETCORE:8.0"

# 部署代码
dotnet publish -c Release -o ./publish
cd publish
zip -r ../myapp.zip .
az webapp deployment source config-zip --resource-group myapp-rg --name myapp --src ../myapp.zip

配置应用设置

# 设置环境变量
az webapp config appsettings set --resource-group myapp-rg --name myapp --settings \
    ASPNETCORE_ENVIRONMENT=Production \
    ConnectionStrings__Default="Server=tcp:myserver.database.windows.net;..."

使用托管标识访问其他 Azure 资源

# 启用系统分配的托管标识
az webapp identity assign --resource-group myapp-rg --name myapp

# 获取托管标识的对象 ID
$identityId = az webapp identity show --resource-group myapp-rg --name myapp --query principalId -o tsv

# 授予访问 Key Vault 的权限
az keyvault set-policy --name mykeyvault --object-id $identityId --secret-permissions get list
// 应用代码中使用托管标识
var builder = WebApplication.CreateBuilder(args);

// 在 Azure App Service 中,DefaultAzureCredential 自动使用托管标识
var credential = new DefaultAzureCredential();

// 从 Key Vault 加载配置
var keyVaultUrl = builder.Configuration["KeyVault:Url"];
if (!string.IsNullOrEmpty(keyVaultUrl))
{
    var secretClient = new SecretClient(new Uri(keyVaultUrl), credential);
    builder.Configuration.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
}

var app = builder.Build();
app.Run();

Azure SQL Database

Azure SQL Database 是完全托管的关系数据库服务。

创建 Azure SQL Database

# 创建 SQL Server
az sql server create \
    --name myserver \
    --resource-group myapp-rg \
    --location eastus \
    --admin-user sqladmin \
    --admin-password 'P@ssw0rd1234!'

# 配置防火墙(允许 Azure 服务访问)
az sql server firewall-rule create \
    --resource-group myapp-rg \
    --server myserver \
    --name AllowAzureServices \
    --start-ip-address 0.0.0.0 \
    --end-ip-address 0.0.0.0

# 创建数据库
az sql db create \
    --resource-group myapp-rg \
    --server myserver \
    --name myappdb \
    --service-objective S0

使用托管标识连接数据库

// 安装 Azure.Identity 和 Microsoft.Data.SqlClient
// dotnet add package Azure.Identity
// dotnet add package Microsoft.Data.SqlClient

using Azure.Identity;
using Microsoft.Data.SqlClient;

public class DbContextFactory
{
    public static AppDbContext CreateDbContext(IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("Default");
        
        var sqlConnection = new SqlConnection(connectionString);
        
        // 如果在 Azure 环境中,使用托管标识进行身份验证
        if (IsRunningInAzure())
        {
            var credential = new DefaultAzureCredential();
            var token = credential.GetToken(
                new Azure.Core.TokenRequestContext(
                    new[] { "https://database.windows.net/.default" }));
            
            sqlConnection.AccessToken = token.Token;
        }
        
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseSqlServer(sqlConnection);
        
        return new AppDbContext(optionsBuilder.Options);
    }
    
    private static bool IsRunningInAzure()
    {
        return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"));
    }
}
// appsettings.json - 连接字符串不包含密码
{
  "ConnectionStrings": {
    "Default": "Server=tcp:myserver.database.windows.net,1433;Database=myappdb;Authentication=Active Directory Default;"
  }
}

Azure Blob Storage

Azure Blob Storage 是 Azure 的对象存储服务,适用于存储文件、图片、视频等非结构化数据。

创建存储账户

# 创建存储账户
az storage account create \
    --name mystorageaccount \
    --resource-group myapp-rg \
    --location eastus \
    --sku Standard_LRS

# 创建容器
az storage container create \
    --name uploads \
    --account-name mystorageaccount \
    --public-access off

上传和下载文件

using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

public class FileStorageService
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly ILogger<FileStorageService> _logger;

    public FileStorageService(IConfiguration configuration, ILogger<FileStorageService> logger)
    {
        var storageUrl = configuration["Azure:Storage:Url"];
        var credential = new DefaultAzureCredential();
        
        _blobServiceClient = new BlobServiceClient(new Uri(storageUrl), credential);
        _logger = logger;
    }

    // 上传文件
    public async Task<string> UploadFileAsync(string containerName, string fileName, Stream content)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        
        // 确保容器存在
        await containerClient.CreateIfNotExistsAsync(PublicAccessType.None);
        
        var blobClient = containerClient.GetBlobClient(fileName);
        
        // 上传文件
        await blobClient.UploadAsync(content, new BlobHttpHeaders
        {
            ContentType = GetContentType(fileName)
        });
        
        _logger.LogInformation("文件已上传: {FileName} 到容器 {Container}", fileName, containerName);
        
        return blobClient.Uri.ToString();
    }

    // 下载文件
    public async Task<Stream> DownloadFileAsync(string containerName, string fileName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var blobClient = containerClient.GetBlobClient(fileName);
        
        var response = await blobClient.DownloadStreamingAsync();
        return response.Value.Content;
    }

    // 生成临时访问 URL(SAS Token)
    public async Task<string> GetTemporaryUrlAsync(string containerName, string fileName, TimeSpan expiresIn)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var blobClient = containerClient.GetBlobClient(fileName);
        
        // 生成 SAS(共享访问签名)
        var sasBuilder = new BlobSasBuilder
        {
            BlobContainerName = containerName,
            BlobName = fileName,
            Resource = "b", // b = blob
            ExpiresOn = DateTimeOffset.UtcNow.Add(expiresIn)
        };
        
        sasBuilder.SetPermissions(BlobSasPermissions.Read);
        
        // 使用用户委派密钥生成 SAS(更安全)
        var userDelegationKey = await _blobServiceClient.GetUserDelegationKeyAsync(
            DateTimeOffset.UtcNow,
            DateTimeOffset.UtcNow.Add(expiresIn));
        
        var sasToken = sasBuilder.ToSasQueryParameters(
            userDelegationKey.Value,
            _blobServiceClient.AccountName).ToString();
        
        return $"{blobClient.Uri}?{sasToken}";
    }

    // 列出容器中的所有文件
    public async Task<List<string>> ListFilesAsync(string containerName, string? prefix = null)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var files = new List<string>();
        
        await foreach (var blobItem in containerClient.GetBlobsAsync(prefix: prefix))
        {
            files.Add(blobItem.Name);
        }
        
        return files;
    }

    // 删除文件
    public async Task<bool> DeleteFileAsync(string containerName, string fileName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var blobClient = containerClient.GetBlobClient(fileName);
        
        return await blobClient.DeleteIfExistsAsync();
    }

    private static string GetContentType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".pdf" => "application/pdf",
            ".txt" => "text/plain",
            ".json" => "application/json",
            _ => "application/octet-stream"
        };
    }
}

在控制器中使用

[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
    private readonly FileStorageService _storageService;

    public FilesController(FileStorageService storageService)
    {
        _storageService = storageService;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("文件不能为空");

        // 生成唯一文件名
        var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
        
        using var stream = file.OpenReadStream();
        var url = await _storageService.UploadFileAsync("uploads", fileName, stream);
        
        return Ok(new { fileName, url });
    }

    [HttpGet("download/{fileName}")]
    public async Task<IActionResult> Download(string fileName)
    {
        var stream = await _storageService.DownloadFileAsync("uploads", fileName);
        return File(stream, "application/octet-stream", fileName);
    }

    [HttpGet("temporary-url/{fileName}")]
    public async Task<IActionResult> GetTemporaryUrl(string fileName)
    {
        var url = await _storageService.GetTemporaryUrlAsync(
            "uploads", 
            fileName, 
            TimeSpan.FromHours(1));
        
        return Ok(new { url });
    }
}

Azure Service Bus

Azure Service Bus 是企业级消息队列服务,支持发布/订阅模式和队列模式。

创建 Service Bus

# 创建 Service Bus 命名空间
az servicebus namespace create \
    --name myservicebus \
    --resource-group myapp-rg \
    --location eastus \
    --sku Standard

# 创建队列
az servicebus queue create \
    --resource-group myapp-rg \
    --namespace-name myservicebus \
    --name orders

# 创建主题和订阅
az servicebus topic create \
    --resource-group myapp-rg \
    --namespace-name myservicebus \
    --name events

az servicebus topic subscription create \
    --resource-group myapp-rg \
    --namespace-name myservicebus \
    --topic-name events \
    --name email-processor

发送和接收消息

using Azure.Identity;
using Azure.Messaging.ServiceBus;

// Program.cs - 注册 Service Bus 客户端
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(sp =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();
    var serviceBusNamespace = configuration["Azure:ServiceBus:Namespace"];
    var credential = new DefaultAzureCredential();
    
    return new ServiceBusClient(
        $"{serviceBusNamespace}.servicebus.windows.net",
        credential);
});

builder.Services.AddSingleton<OrderMessageService>();

var app = builder.Build();
app.Run();

// OrderMessageService.cs
public class OrderMessageService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly ILogger<OrderMessageService> _logger;

    public OrderMessageService(ServiceBusClient serviceBusClient, ILogger<OrderMessageService> logger)
    {
        _serviceBusClient = serviceBusClient;
        _logger = logger;
    }

    // 发送消息到队列
    public async Task SendOrderMessageAsync(Order order)
    {
        var sender = _serviceBusClient.CreateSender("orders");
        
        try
        {
            var messageBody = JsonSerializer.Serialize(order);
            var message = new ServiceBusMessage(messageBody)
            {
                MessageId = order.Id.ToString(),
                ContentType = "application/json",
                Subject = "NewOrder"
            };
            
            // 添加自定义属性
            message.ApplicationProperties["OrderType"] = order.Type;
            message.ApplicationProperties["Priority"] = order.Priority;
            
            await sender.SendMessageAsync(message);
            
            _logger.LogInformation("订单消息已发送: {OrderId}", order.Id);
        }
        finally
        {
            await sender.DisposeAsync();
        }
    }

    // 批量发送消息
    public async Task SendBatchMessagesAsync(List<Order> orders)
    {
        var sender = _serviceBusClient.CreateSender("orders");
        
        try
        {
            using var messageBatch = await sender.CreateMessageBatchAsync();
            
            foreach (var order in orders)
            {
                var messageBody = JsonSerializer.Serialize(order);
                var message = new ServiceBusMessage(messageBody);
                
                if (!messageBatch.TryAddMessage(message))
                {
                    throw new Exception($"消息太大,无法添加到批次: {order.Id}");
                }
            }
            
            await sender.SendMessagesAsync(messageBatch);
            
            _logger.LogInformation("批量发送了 {Count} 条消息", orders.Count);
        }
        finally
        {
            await sender.DisposeAsync();
        }
    }

    // 发布事件到主题
    public async Task PublishEventAsync(string eventType, object eventData)
    {
        var sender = _serviceBusClient.CreateSender("events");
        
        try
        {
            var messageBody = JsonSerializer.Serialize(eventData);
            var message = new ServiceBusMessage(messageBody)
            {
                Subject = eventType,
                ContentType = "application/json"
            };
            
            await sender.SendMessageAsync(message);
            
            _logger.LogInformation("事件已发布: {EventType}", eventType);
        }
        finally
        {
            await sender.DisposeAsync();
        }
    }
}

// 后台服务接收消息
public class OrderProcessorService : BackgroundService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly ILogger<OrderProcessorService> _logger;
    private ServiceBusProcessor? _processor;

    public OrderProcessorService(
        ServiceBusClient serviceBusClient,
        ILogger<OrderProcessorService> logger)
    {
        _serviceBusClient = serviceBusClient;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor = _serviceBusClient.CreateProcessor("orders", new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 10,
            AutoCompleteMessages = false
        });

        _processor.ProcessMessageAsync += ProcessMessageAsync;
        _processor.ProcessErrorAsync += ProcessErrorAsync;

        await _processor.StartProcessingAsync(stoppingToken);
        
        _logger.LogInformation("订单处理服务已启动");
        
        // 等待停止信号
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
    {
        var body = args.Message.Body.ToString();
        var order = JsonSerializer.Deserialize<Order>(body);
        
        _logger.LogInformation("收到订单消息: {OrderId}", order?.Id);
        
        try
        {
            // 处理订单
            await ProcessOrderAsync(order!);
            
            // 标记消息为已完成
            await args.CompleteMessageAsync(args.Message);
            
            _logger.LogInformation("订单处理完成: {OrderId}", order?.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理订单失败: {OrderId}", order?.Id);
            
            // 如果处理失败,可以选择:
            // 1. 放弃消息(会进入死信队列)
            await args.DeadLetterMessageAsync(args.Message, "ProcessingFailed", ex.Message);
            
            // 2. 延迟重试
            // await args.AbandonMessageAsync(args.Message);
        }
    }

    private Task ProcessErrorAsync(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception, "Service Bus 错误: {ErrorSource}", args.ErrorSource);
        return Task.CompletedTask;
    }

    private async Task ProcessOrderAsync(Order order)
    {
        // 实际的订单处理逻辑
        await Task.Delay(100); // 模拟处理
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_processor != null)
        {
            await _processor.StopProcessingAsync(cancellationToken);
            await _processor.DisposeAsync();
        }
        
        await base.StopAsync(cancellationToken);
    }
}

Azure Key Vault

Azure Key Vault 用于安全地存储和访问密钥、密码、证书等敏感信息。

创建 Key Vault

# 创建 Key Vault
az keyvault create \
    --name mykeyvault \
    --resource-group myapp-rg \
    --location eastus

# 添加密钥
az keyvault secret set \
    --vault-name mykeyvault \
    --name ConnectionString \
    --value "Server=tcp:myserver.database.windows.net;..."

az keyvault secret set \
    --vault-name mykeyvault \
    --name ApiKey \
    --value "sk_live_123..."

在应用中使用 Key Vault

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

// Program.cs - 从 Key Vault 加载配置
var builder = WebApplication.CreateBuilder(args);

// 方式 1:使用 Azure.Extensions.AspNetCore.Configuration.Secrets
if (builder.Environment.IsProduction())
{
    var keyVaultUrl = builder.Configuration["KeyVault:Url"];
    var credential = new DefaultAzureCredential();
    
    builder.Configuration.AddAzureKeyVault(
        new Uri(keyVaultUrl),
        credential);
}

var app = builder.Build();
app.Run();
// 方式 2:直接使用 SecretClient
public class SecretService
{
    private readonly SecretClient _secretClient;
    private readonly ILogger<SecretService> _logger;

    public SecretService(IConfiguration configuration, ILogger<SecretService> logger)
    {
        var keyVaultUrl = configuration["KeyVault:Url"];
        var credential = new DefaultAzureCredential();
        
        _secretClient = new SecretClient(new Uri(keyVaultUrl), credential);
        _logger = logger;
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        try
        {
            var secret = await _secretClient.GetSecretAsync(secretName);
            return secret.Value.Value;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "获取密钥失败: {SecretName}", secretName);
            throw;
        }
    }

    public async Task SetSecretAsync(string secretName, string secretValue)
    {
        await _secretClient.SetSecretAsync(secretName, secretValue);
        _logger.LogInformation("密钥已设置: {SecretName}", secretName);
    }

    // 获取所有版本的密钥
    public async Task<List<SecretProperties>> GetSecretVersionsAsync(string secretName)
    {
        var versions = new List<SecretProperties>();
        
        await foreach (var secretProperties in _secretClient.GetPropertiesOfSecretVersionsAsync(secretName))
        {
            versions.Add(secretProperties);
        }
        
        return versions;
    }
}

密钥轮换

public class SecretRotationService : BackgroundService
{
    private readonly SecretClient _secretClient;
    private readonly ILogger<SecretRotationService> _logger;

    public SecretRotationService(SecretClient secretClient, ILogger<SecretRotationService> logger)
    {
        _secretClient = secretClient;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // 检查密钥是否需要轮换
                var secret = await _secretClient.GetSecretAsync("ApiKey", cancellationToken: stoppingToken);
                
                if (ShouldRotate(secret.Value))
                {
                    _logger.LogInformation("开始轮换密钥: ApiKey");
                    
                    // 生成新密钥
                    var newApiKey = GenerateNewApiKey();
                    
                    // 保存新密钥
                    await _secretClient.SetSecretAsync("ApiKey", newApiKey, stoppingToken);
                    
                    // 通知相关服务更新密钥
                    await NotifyServicesAsync(newApiKey);
                    
                    _logger.LogInformation("密钥轮换完成: ApiKey");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "密钥轮换失败");
            }
            
            // 每天检查一次
            await Task.Delay(TimeSpan.FromDays(1), stoppingToken);
        }
    }

    private bool ShouldRotate(KeyVaultSecret secret)
    {
        // 如果密钥超过 90 天,则需要轮换
        return secret.Properties.CreatedOn.HasValue &&
               (DateTimeOffset.UtcNow - secret.Properties.CreatedOn.Value).TotalDays > 90;
    }

    private string GenerateNewApiKey()
    {
        // 生成新的 API 密钥
        var bytes = new byte[32];
        using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes);
    }

    private Task NotifyServicesAsync(string newApiKey)
    {
        // 通知相关服务更新密钥(例如通过消息队列)
        return Task.CompletedTask;
    }
}

Azure Application Insights

Application Insights 是 Azure 的应用性能监控(APM)服务。

安装和配置

# 安装 NuGet 包
dotnet add package Microsoft.ApplicationInsights.AspNetCore
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 添加 Application Insights
builder.Services.AddApplicationInsightsTelemetry(options =>
{
    options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});

// 添加自定义遥测初始化器
builder.Services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();

var app = builder.Build();

// 自动追踪 HTTP 请求
app.UseHttpsRedirection();
app.MapControllers();

app.Run();

// 自定义遥测初始化器
public class CustomTelemetryInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        // 添加自定义属性
        if (telemetry is ISupportProperties propertyTelemetry)
        {
            propertyTelemetry.Properties["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown";
            propertyTelemetry.Properties["MachineName"] = Environment.MachineName;
        }
    }
}

记录自定义事件和指标

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

public class OrderService
{
    private readonly TelemetryClient _telemetryClient;
    private readonly ILogger<OrderService> _logger;

    public OrderService(TelemetryClient telemetryClient, ILogger<OrderService> logger)
    {
        _telemetryClient = telemetryClient;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        // 记录自定义事件
        _telemetryClient.TrackEvent("OrderCreated", new Dictionary<string, string>
        {
            { "OrderId", order.Id.ToString() },
            { "OrderType", order.Type },
            { "Amount", order.Amount.ToString() }
        });

        // 记录自定义指标
        _telemetryClient.TrackMetric("OrderAmount", order.Amount);

        // 追踪依赖项调用
        var startTime = DateTime.UtcNow;
        var timer = System.Diagnostics.Stopwatch.StartNew();

        try
        {
            // 调用外部 API
            await CallPaymentApiAsync(order);

            _telemetryClient.TrackDependency(
                "HTTP",
                "payment-api.example.com",
                "ProcessPayment",
                startTime,
                timer.Elapsed,
                success: true);

            return order;
        }
        catch (Exception ex)
        {
            _telemetryClient.TrackDependency(
                "HTTP",
                "payment-api.example.com",
                "ProcessPayment",
                startTime,
                timer.Elapsed,
                success: false);

            // 记录异常
            _telemetryClient.TrackException(ex, new Dictionary<string, string>
            {
                { "OrderId", order.Id.ToString() }
            });

            throw;
        }
    }

    private Task CallPaymentApiAsync(Order order)
    {
        // 模拟调用支付 API
        return Task.Delay(100);
    }
}

35.3 AWS 服务集成

AWS(Amazon Web Services)是全球最大的云计算平台。虽然 AWS 与 .NET 的集成不如 Azure 深入,但 AWS SDK for .NET 同样功能强大。

AWS SDK for .NET

安装 SDK

# 安装 AWS SDK Core
dotnet add package AWSSDK.Core

# 安装具体服务的 SDK
dotnet add package AWSSDK.S3                # Amazon S3
dotnet add package AWSSDK.DynamoDBv2        # DynamoDB
dotnet add package AWSSDK.SQS               # Simple Queue Service
dotnet add package AWSSDK.SecretsManager    # Secrets Manager
dotnet add package AWSSDK.Extensions.NETCore.Setup

配置 AWS 凭证

// Program.cs
using Amazon;
using Amazon.Runtime;
using Amazon.Extensions.NETCore.Setup;

var builder = WebApplication.CreateBuilder(args);

// 方式 1:使用默认凭证提供程序(推荐)
// 自动从以下位置查找凭证:
// 1. 环境变量(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY)
// 2. AWS 凭证文件(~/.aws/credentials)
// 3. IAM 角色(在 EC2/ECS 中运行时)
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());

// 方式 2:显式配置
builder.Services.AddDefaultAWSOptions(new AWSOptions
{
    Credentials = new BasicAWSCredentials(
        builder.Configuration["AWS:AccessKey"],
        builder.Configuration["AWS:SecretKey"]),
    Region = RegionEndpoint.USEast1
});

// 注册 AWS 服务客户端
builder.Services.AddAWSService<IAmazonS3>();
builder.Services.AddAWSService<IAmazonDynamoDB>();
builder.Services.AddAWSService<IAmazonSQS>();

var app = builder.Build();
app.Run();

Amazon S3(Simple Storage Service)

S3 是 AWS 的对象存储服务,类似于 Azure Blob Storage。

创建 S3 存储桶

# 使用 AWS CLI
aws s3 mb s3://my-app-bucket --region us-east-1

# 设置存储桶策略(私有)
aws s3api put-bucket-public-access-block \
    --bucket my-app-bucket \
    --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

上传和下载文件

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;

public class S3StorageService
{
    private readonly IAmazonS3 _s3Client;
    private readonly ILogger<S3StorageService> _logger;
    private const string BucketName = "my-app-bucket";

    public S3StorageService(IAmazonS3 s3Client, ILogger<S3StorageService> logger)
    {
        _s3Client = s3Client;
        _logger = logger;
    }

    // 上传文件
    public async Task<string> UploadFileAsync(string fileName, Stream content, string? contentType = null)
    {
        var request = new PutObjectRequest
        {
            BucketName = BucketName,
            Key = fileName,
            InputStream = content,
            ContentType = contentType ?? "application/octet-stream",
            ServerSideEncryptionMethod = ServerSideEncryptionMethod.AES256 // 启用服务端加密
        };

        await _s3Client.PutObjectAsync(request);
        
        _logger.LogInformation("文件已上传到 S3: {FileName}", fileName);
        
        return $"https://{BucketName}.s3.amazonaws.com/{fileName}";
    }

    // 上传大文件(使用 TransferUtility)
    public async Task<string> UploadLargeFileAsync(string fileName, Stream content)
    {
        var transferUtility = new TransferUtility(_s3Client);
        
        var uploadRequest = new TransferUtilityUploadRequest
        {
            BucketName = BucketName,
            Key = fileName,
            InputStream = content,
            ServerSideEncryptionMethod = ServerSideEncryptionMethod.AES256
        };

        // 自动处理分片上传
        await transferUtility.UploadAsync(uploadRequest);
        
        _logger.LogInformation("大文件已上传到 S3: {FileName}", fileName);
        
        return $"https://{BucketName}.s3.amazonaws.com/{fileName}";
    }

    // 下载文件
    public async Task<Stream> DownloadFileAsync(string fileName)
    {
        var request = new GetObjectRequest
        {
            BucketName = BucketName,
            Key = fileName
        };

        var response = await _s3Client.GetObjectAsync(request);
        return response.ResponseStream;
    }

    // 生成预签名 URL(临时访问)
    public string GetPresignedUrl(string fileName, TimeSpan expiresIn)
    {
        var request = new GetPreSignedUrlRequest
        {
            BucketName = BucketName,
            Key = fileName,
            Expires = DateTime.UtcNow.Add(expiresIn),
            Verb = HttpVerb.GET
        };

        return _s3Client.GetPreSignedURL(request);
    }

    // 列出文件
    public async Task<List<string>> ListFilesAsync(string? prefix = null)
    {
        var request = new ListObjectsV2Request
        {
            BucketName = BucketName,
            Prefix = prefix
        };

        var files = new List<string>();
        ListObjectsV2Response response;
        
        do
        {
            response = await _s3Client.ListObjectsV2Async(request);
            files.AddRange(response.S3Objects.Select(obj => obj.Key));
            
            request.ContinuationToken = response.NextContinuationToken;
        } while (response.IsTruncated);

        return files;
    }

    // 删除文件
    public async Task<bool> DeleteFileAsync(string fileName)
    {
        try
        {
            await _s3Client.DeleteObjectAsync(BucketName, fileName);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "删除文件失败: {FileName}", fileName);
            return false;
        }
    }

    // 复制文件
    public async Task CopyFileAsync(string sourceKey, string destinationKey)
    {
        var request = new CopyObjectRequest
        {
            SourceBucket = BucketName,
            SourceKey = sourceKey,
            DestinationBucket = BucketName,
            DestinationKey = destinationKey
        };

        await _s3Client.CopyObjectAsync(request);
        
        _logger.LogInformation("文件已复制: {Source} -> {Destination}", sourceKey, destinationKey);
    }
}

Amazon SQS(Simple Queue Service)

SQS 是 AWS 的消息队列服务,类似于 Azure Service Bus 的队列功能。

创建 SQS 队列

# 创建标准队列
aws sqs create-queue --queue-name orders

# 创建 FIFO 队列(保证顺序和去重)
aws sqs create-queue --queue-name orders.fifo --attributes FifoQueue=true

发送和接收消息

using Amazon.SQS;
using Amazon.SQS.Model;

public class SqsMessageService
{
    private readonly IAmazonSQS _sqsClient;
    private readonly ILogger<SqsMessageService> _logger;
    private const string QueueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/orders";

    public SqsMessageService(IAmazonSQS sqsClient, ILogger<SqsMessageService> logger)
    {
        _sqsClient = sqsClient;
        _logger = logger;
    }

    // 发送消息
    public async Task SendMessageAsync(Order order)
    {
        var messageBody = JsonSerializer.Serialize(order);
        
        var request = new SendMessageRequest
        {
            QueueUrl = QueueUrl,
            MessageBody = messageBody,
            MessageAttributes = new Dictionary<string, MessageAttributeValue>
            {
                { "OrderType", new MessageAttributeValue { DataType = "String", StringValue = order.Type } },
                { "Priority", new MessageAttributeValue { DataType = "Number", StringValue = order.Priority.ToString() } }
            }
        };

        var response = await _sqsClient.SendMessageAsync(request);
        
        _logger.LogInformation("消息已发送到 SQS: {MessageId}", response.MessageId);
    }

    // 批量发送消息
    public async Task SendBatchMessagesAsync(List<Order> orders)
    {
        var entries = orders.Select((order, index) => new SendMessageBatchRequestEntry
        {
            Id = index.ToString(),
            MessageBody = JsonSerializer.Serialize(order),
            MessageAttributes = new Dictionary<string, MessageAttributeValue>
            {
                { "OrderType", new MessageAttributeValue { DataType = "String", StringValue = order.Type } }
            }
        }).ToList();

        var request = new SendMessageBatchRequest
        {
            QueueUrl = QueueUrl,
            Entries = entries
        };

        var response = await _sqsClient.SendMessageBatchAsync(request);
        
        _logger.LogInformation("批量发送了 {Count} 条消息, 成功: {Successful}, 失败: {Failed}",
            orders.Count, response.Successful.Count, response.Failed.Count);
    }

    // 接收消息
    public async Task<List<Message>> ReceiveMessagesAsync(int maxMessages = 10)
    {
        var request = new ReceiveMessageRequest
        {
            QueueUrl = QueueUrl,
            MaxNumberOfMessages = maxMessages,
            WaitTimeSeconds = 20, // 长轮询,减少空请求
            MessageAttributeNames = new List<string> { "All" }
        };

        var response = await _sqsClient.ReceiveMessageAsync(request);
        return response.Messages;
    }

    // 删除消息(处理完成后)
    public async Task DeleteMessageAsync(string receiptHandle)
    {
        await _sqsClient.DeleteMessageAsync(QueueUrl, receiptHandle);
    }

    // 批量删除消息
    public async Task DeleteBatchMessagesAsync(List<string> receiptHandles)
    {
        var entries = receiptHandles.Select((handle, index) => new DeleteMessageBatchRequestEntry
        {
            Id = index.ToString(),
            ReceiptHandle = handle
        }).ToList();

        await _sqsClient.DeleteMessageBatchAsync(QueueUrl, entries);
    }

    // 更改消息可见性(延迟重试)
    public async Task ChangeMessageVisibilityAsync(string receiptHandle, int visibilityTimeoutSeconds)
    {
        await _sqsClient.ChangeMessageVisibilityAsync(
            QueueUrl,
            receiptHandle,
            visibilityTimeoutSeconds);
    }
}

// 后台服务处理消息
public class OrderProcessorService : BackgroundService
{
    private readonly IAmazonSQS _sqsClient;
    private readonly ILogger<OrderProcessorService> _logger;
    private const string QueueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/orders";

    public OrderProcessorService(IAmazonSQS sqsClient, ILogger<OrderProcessorService> logger)
    {
        _sqsClient = sqsClient;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("订单处理服务已启动");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // 接收消息
                var request = new ReceiveMessageRequest
                {
                    QueueUrl = QueueUrl,
                    MaxNumberOfMessages = 10,
                    WaitTimeSeconds = 20,
                    MessageAttributeNames = new List<string> { "All" }
                };

                var response = await _sqsClient.ReceiveMessageAsync(request, stoppingToken);

                foreach (var message in response.Messages)
                {
                    try
                    {
                        var order = JsonSerializer.Deserialize<Order>(message.Body);
                        
                        _logger.LogInformation("收到订单消息: {OrderId}", order?.Id);

                        // 处理订单
                        await ProcessOrderAsync(order!);

                        // 删除消息
                        await _sqsClient.DeleteMessageAsync(QueueUrl, message.ReceiptHandle, stoppingToken);

                        _logger.LogInformation("订单处理完成: {OrderId}", order?.Id);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "处理消息失败: {MessageId}", message.MessageId);
                        
                        // 增加可见性超时,稍后重试
                        await _sqsClient.ChangeMessageVisibilityAsync(
                            QueueUrl,
                            message.ReceiptHandle,
                            300, // 5 分钟后重试
                            stoppingToken);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "接收消息失败");
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
    }

    private async Task ProcessOrderAsync(Order order)
    {
        // 实际的订单处理逻辑
        await Task.Delay(100);
    }
}

Amazon DynamoDB

DynamoDB 是 AWS 的 NoSQL 数据库服务,支持键值和文档数据模型。

创建 DynamoDB 表

# 使用 AWS CLI 创建表
aws dynamodb create-table \
    --table-name Products \
    --attribute-definitions \
        AttributeName=Id,AttributeType=S \
        AttributeName=Category,AttributeType=S \
    --key-schema \
        AttributeName=Id,KeyType=HASH \
    --global-secondary-indexes \
        "IndexName=CategoryIndex,KeySchema=[{AttributeName=Category,KeyType=HASH}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}" \
    --provisioned-throughput \
        ReadCapacityUnits=5,WriteCapacityUnits=5

操作 DynamoDB

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;

// 定义实体
[DynamoDBTable("Products")]
public class Product
{
    [DynamoDBHashKey]
    public string Id { get; set; } = null!;

    [DynamoDBProperty]
    public string Name { get; set; } = null!;

    [DynamoDBProperty]
    public string Category { get; set; } = null!;

    [DynamoDBProperty]
    public decimal Price { get; set; }

    [DynamoDBProperty]
    public int Stock { get; set; }

    [DynamoDBProperty]
    public DateTime CreatedAt { get; set; }
}

public class DynamoDbService
{
    private readonly IAmazonDynamoDB _dynamoDbClient;
    private readonly DynamoDBContext _context;
    private readonly ILogger<DynamoDbService> _logger;

    public DynamoDbService(IAmazonDynamoDB dynamoDbClient, ILogger<DynamoDbService> logger)
    {
        _dynamoDbClient = dynamoDbClient;
        _context = new DynamoDBContext(dynamoDbClient);
        _logger = logger;
    }

    // 保存项目
    public async Task SaveProductAsync(Product product)
    {
        await _context.SaveAsync(product);
        _logger.LogInformation("产品已保存: {ProductId}", product.Id);
    }

    // 获取项目
    public async Task<Product?> GetProductAsync(string productId)
    {
        return await _context.LoadAsync<Product>(productId);
    }

    // 查询(使用全局二级索引)
    public async Task<List<Product>> GetProductsByCategoryAsync(string category)
    {
        var queryConfig = new QueryOperationConfig
        {
            IndexName = "CategoryIndex",
            Filter = new QueryFilter("Category", QueryOperator.Equal, category)
        };

        var search = _context.FromQueryAsync<Product>(queryConfig);
        var products = await search.GetRemainingAsync();
        
        return products;
    }

    // 扫描(全表扫描,慎用)
    public async Task<List<Product>> ScanProductsAsync(decimal minPrice, decimal maxPrice)
    {
        var scanConfig = new ScanOperationConfig
        {
            Filter = new ScanFilter()
        };
        
        scanConfig.Filter.AddCondition("Price", ScanOperator.Between, minPrice, maxPrice);

        var search = _context.FromScanAsync<Product>(scanConfig);
        var products = await search.GetRemainingAsync();
        
        return products;
    }

    // 批量获取
    public async Task<List<Product>> BatchGetProductsAsync(List<string> productIds)
    {
        var batch = _context.CreateBatchGet<Product>();
        
        foreach (var id in productIds)
        {
            batch.AddKey(id);
        }

        await batch.ExecuteAsync();
        return batch.Results;
    }

    // 批量写入
    public async Task BatchSaveProductsAsync(List<Product> products)
    {
        var batch = _context.CreateBatchWrite<Product>();
        
        foreach (var product in products)
        {
            batch.AddPutItem(product);
        }

        await batch.ExecuteAsync();
        _logger.LogInformation("批量保存了 {Count} 个产品", products.Count);
    }

    // 删除项目
    public async Task DeleteProductAsync(string productId)
    {
        await _context.DeleteAsync<Product>(productId);
        _logger.LogInformation("产品已删除: {ProductId}", productId);
    }

    // 条件更新(乐观锁)
    public async Task<bool> UpdateStockAsync(string productId, int quantity)
    {
        try
        {
            var product = await GetProductAsync(productId);
            if (product == null) return false;

            // 检查库存
            if (product.Stock < quantity)
            {
                _logger.LogWarning("库存不足: {ProductId}, 需要: {Quantity}, 库存: {Stock}",
                    productId, quantity, product.Stock);
                return false;
            }

            // 减少库存
            product.Stock -= quantity;

            // 条件更新:只有当库存仍然足够时才更新
            var config = new DynamoDBOperationConfig
            {
                ConditionalExpression = new Expression
                {
                    ExpressionStatement = "Stock >= :quantity",
                    ExpressionAttributeValues = new Dictionary<string, DynamoDBEntry>
                    {
                        { ":quantity", quantity }
                    }
                }
            };

            await _context.SaveAsync(product, config);
            return true;
        }
        catch (ConditionalCheckFailedException)
        {
            _logger.LogWarning("条件更新失败: {ProductId}", productId);
            return false;
        }
    }
}

AWS Secrets Manager

AWS Secrets Manager 类似于 Azure Key Vault,用于安全存储密钥。

using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

public class SecretsManagerService
{
    private readonly IAmazonSecretsManager _secretsManager;
    private readonly ILogger<SecretsManagerService> _logger;

    public SecretsManagerService(IAmazonSecretsManager secretsManager, ILogger<SecretsManagerService> logger)
    {
        _secretsManager = secretsManager;
        _logger = logger;
    }

    // 获取密钥
    public async Task<string> GetSecretAsync(string secretName)
    {
        try
        {
            var request = new GetSecretValueRequest
            {
                SecretId = secretName
            };

            var response = await _secretsManager.GetSecretValueAsync(request);
            
            return response.SecretString;
        }
        catch (ResourceNotFoundException)
        {
            _logger.LogError("密钥不存在: {SecretName}", secretName);
            throw;
        }
    }

    // 创建密钥
    public async Task CreateSecretAsync(string secretName, string secretValue)
    {
        var request = new CreateSecretRequest
        {
            Name = secretName,
            SecretString = secretValue
        };

        await _secretsManager.CreateSecretAsync(request);
        _logger.LogInformation("密钥已创建: {SecretName}", secretName);
    }

    // 更新密钥
    public async Task UpdateSecretAsync(string secretName, string newValue)
    {
        var request = new PutSecretValueRequest
        {
            SecretId = secretName,
            SecretString = newValue
        };

        await _secretsManager.PutSecretValueAsync(request);
        _logger.LogInformation("密钥已更新: {SecretName}", secretName);
    }
}

// Program.cs - 从 Secrets Manager 加载配置
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsProduction())
{
    var secretsManager = builder.Services.BuildServiceProvider()
        .GetRequiredService<IAmazonSecretsManager>();
    
    var secretName = builder.Configuration["AWS:SecretName"];
    
    var request = new GetSecretValueRequest { SecretId = secretName };
    var response = await secretsManager.GetSecretValueAsync(request);
    
    var secrets = JsonSerializer.Deserialize<Dictionary<string, string>>(response.SecretString);
    
    foreach (var (key, value) in secrets!)
    {
        builder.Configuration[key] = value;
    }
}

var app = builder.Build();
app.Run();

35.4 无服务器架构(Azure Functions)

无服务器(Serverless)并不是真的没有服务器,而是开发者不需要管理服务器,只需要编写业务逻辑代码,云平台会自动处理扩展、负载均衡和高可用性。

Azure Functions 概述

Azure Functions 是 Azure 的无服务器计算服务,支持多种触发器:

  • HTTP 触发器(REST API)
  • 定时器触发器(Cron 作业)
  • 队列触发器(Service Bus、Storage Queue)
  • Blob 触发器(文件上传)
  • Event Grid 触发器(事件驱动)

优势

  • 按执行次数付费,空闲时不收费
  • 自动扩展,无需配置
  • 快速开发和部署
  • 与 Azure 服务深度集成

何时使用 Azure Functions

  • 事件驱动的短时任务(图片处理、数据转换)
  • 定时任务(数据同步、报告生成)
  • 轻量级 API(微服务的补充)
  • 异步处理(解耦长时间运行的操作)

创建 Azure Functions 项目

# 安装 Azure Functions Core Tools
npm install -g azure-functions-core-tools@4

# 创建 Functions 项目
func init MyFunctionApp --dotnet
cd MyFunctionApp

# 创建 HTTP 触发器函数
func new --name HttpTriggerFunction --template "HTTP trigger"

# 创建定时器触发器函数
func new --name TimerTriggerFunction --template "Timer trigger"

# 本地运行
func start

HTTP 触发器函数

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class HttpTriggerFunction
{
    private readonly ILogger<HttpTriggerFunction> _logger;

    public HttpTriggerFunction(ILogger<HttpTriggerFunction> logger)
    {
        _logger = logger;
    }

    [Function("HttpTriggerFunction")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "users/{id?}")] HttpRequest req,
        string? id)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        if (req.Method == "GET")
        {
            if (string.IsNullOrEmpty(id))
            {
                // 获取所有用户
                var users = await GetAllUsersAsync();
                return new OkObjectResult(users);
            }
            else
            {
                // 获取单个用户
                var user = await GetUserByIdAsync(id);
                return user != null 
                    ? new OkObjectResult(user)
                    : new NotFoundResult();
            }
        }
        else if (req.Method == "POST")
        {
            // 创建用户
            var body = await new StreamReader(req.Body).ReadToEndAsync();
            var user = JsonSerializer.Deserialize<User>(body);
            
            await CreateUserAsync(user!);
            
            return new CreatedResult($"/api/users/{user!.Id}", user);
        }

        return new BadRequestResult();
    }

    private Task<List<User>> GetAllUsersAsync()
    {
        // 实际实现:从数据库获取
        return Task.FromResult(new List<User>());
    }

    private Task<User?> GetUserByIdAsync(string id)
    {
        // 实际实现:从数据库获取
        return Task.FromResult<User?>(null);
    }

    private Task CreateUserAsync(User user)
    {
        // 实际实现:保存到数据库
        return Task.CompletedTask;
    }
}

public class User
{
    public string Id { get; set; } = null!;
    public string Name { get; set; } = null!;
    public string Email { get; set; } = null!;
}

定时器触发器函数

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class TimerTriggerFunction
{
    private readonly ILogger<TimerTriggerFunction> _logger;
    private readonly HttpClient _httpClient;

    public TimerTriggerFunction(ILogger<TimerTriggerFunction> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient();
    }

    // 每天凌晨 2 点执行
    [Function("DailyReportGenerator")]
    public async Task RunDailyReport(
        [TimerTrigger("0 0 2 * * *")] TimerInfo timerInfo)
    {
        _logger.LogInformation("开始生成每日报告: {Time}", DateTime.UtcNow);

        try
        {
            // 生成报告
            var report = await GenerateReportAsync();

            // 发送邮件
            await SendReportEmailAsync(report);

            _logger.LogInformation("每日报告生成完成");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "生成报告失败");
            throw;
        }
    }

    // 每 5 分钟执行一次
    [Function("HealthCheckMonitor")]
    public async Task RunHealthCheck(
        [TimerTrigger("0 */5 * * * *")] TimerInfo timerInfo)
    {
        _logger.LogInformation("开始健康检查: {Time}", DateTime.UtcNow);

        var endpoints = new[]
        {
            "https://api1.example.com/health",
            "https://api2.example.com/health",
            "https://api3.example.com/health"
        };

        foreach (var endpoint in endpoints)
        {
            try
            {
                var response = await _httpClient.GetAsync(endpoint);
                
                if (!response.IsSuccessStatusCode)
                {
                    _logger.LogWarning("健康检查失败: {Endpoint}, Status: {Status}",
                        endpoint, response.StatusCode);
                    
                    // 发送告警
                    await SendAlertAsync(endpoint, response.StatusCode.ToString());
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "健康检查异常: {Endpoint}", endpoint);
                await SendAlertAsync(endpoint, ex.Message);
            }
        }
    }

    private Task<string> GenerateReportAsync()
    {
        // 实际实现:生成报告
        return Task.FromResult("报告内容");
    }

    private Task SendReportEmailAsync(string report)
    {
        // 实际实现:发送邮件
        return Task.CompletedTask;
    }

    private Task SendAlertAsync(string endpoint, string error)
    {
        // 实际实现:发送告警(邮件、短信、Slack等)
        return Task.CompletedTask;
    }
}

Blob 触发器函数(图片处理)

using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;

public class ImageProcessorFunction
{
    private readonly ILogger<ImageProcessorFunction> _logger;

    public ImageProcessorFunction(ILogger<ImageProcessorFunction> logger)
    {
        _logger = logger;
    }

    [Function("ImageProcessor")]
    public async Task Run(
        [BlobTrigger("uploads/{name}", Connection = "AzureWebJobsStorage")] Stream imageStream,
        string name,
        [BlobInput("thumbnails/{name}", Connection = "AzureWebJobsStorage")] BlobClient outputBlob)
    {
        _logger.LogInformation("开始处理图片: {Name}, 大小: {Size} bytes", name, imageStream.Length);

        try
        {
            // 加载图片
            using var image = await Image.LoadAsync(imageStream);

            // 生成缩略图(最大宽度 300px)
            image.Mutate(x => x.Resize(new ResizeOptions
            {
                Mode = ResizeMode.Max,
                Size = new Size(300, 0)
            }));

            // 保存到 thumbnails 容器
            using var outputStream = new MemoryStream();
            await image.SaveAsJpegAsync(outputStream);
            outputStream.Position = 0;

            await outputBlob.UploadAsync(outputStream, overwrite: true);

            _logger.LogInformation("缩略图已生成: {Name}", name);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "处理图片失败: {Name}", name);
            throw;
        }
    }
}

Queue 触发器函数(订单处理)

using Azure.Storage.Queues.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class OrderProcessorFunction
{
    private readonly ILogger<OrderProcessorFunction> _logger;
    private readonly HttpClient _httpClient;

    public OrderProcessorFunction(ILogger<OrderProcessorFunction> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient();
    }

    [Function("OrderProcessor")]
    public async Task Run(
        [QueueTrigger("orders", Connection = "AzureWebJobsStorage")] QueueMessage message)
    {
        _logger.LogInformation("收到订单消息: {MessageId}", message.MessageId);

        try
        {
            var order = JsonSerializer.Deserialize<Order>(message.MessageText);

            // 1. 验证订单
            await ValidateOrderAsync(order!);

            // 2. 处理支付
            await ProcessPaymentAsync(order!);

            // 3. 更新库存
            await UpdateInventoryAsync(order!);

            // 4. 发送确认邮件
            await SendConfirmationEmailAsync(order!);

            _logger.LogInformation("订单处理完成: {OrderId}", order!.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "订单处理失败: {MessageId}", message.MessageId);
            
            // 重试次数超过限制后,消息会进入毒药队列(poison queue)
            throw;
        }
    }

    private Task ValidateOrderAsync(Order order)
    {
        // 验证订单数据
        if (order.Items == null || !order.Items.Any())
            throw new InvalidOperationException("订单不能为空");
        
        return Task.CompletedTask;
    }

    private async Task ProcessPaymentAsync(Order order)
    {
        // 调用支付 API
        var response = await _httpClient.PostAsJsonAsync("https://payment-api.example.com/charge", new
        {
            amount = order.TotalAmount,
            currency = "USD",
            orderId = order.Id
        });

        response.EnsureSuccessStatusCode();
    }

    private Task UpdateInventoryAsync(Order order)
    {
        // 更新库存
        return Task.CompletedTask;
    }

    private Task SendConfirmationEmailAsync(Order order)
    {
        // 发送确认邮件
        return Task.CompletedTask;
    }
}

public class Order
{
    public string Id { get; set; } = null!;
    public List<OrderItem> Items { get; set; } = new();
    public decimal TotalAmount { get; set; }
}

public class OrderItem
{
    public string ProductId { get; set; } = null!;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Service Bus 触发器函数

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class ServiceBusFunction
{
    private readonly ILogger<ServiceBusFunction> _logger;

    public ServiceBusFunction(ILogger<ServiceBusFunction> logger)
    {
        _logger = logger;
    }

    [Function("ServiceBusQueueTrigger")]
    public async Task ProcessQueueMessage(
        [ServiceBusTrigger("myqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
    {
        _logger.LogInformation("消息 ID: {MessageId}", message.MessageId);
        _logger.LogInformation("消息内容: {Body}", message.Body);

        // 处理消息
        await Task.Delay(100);
    }

    [Function("ServiceBusTopicTrigger")]
    public async Task ProcessTopicMessage(
        [ServiceBusTrigger("mytopic", "mysubscription", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
    {
        _logger.LogInformation("主题消息 ID: {MessageId}", message.MessageId);
        
        // 根据消息类型处理
        var eventType = message.Subject;
        
        switch (eventType)
        {
            case "OrderCreated":
                await HandleOrderCreatedAsync(message.Body.ToString());
                break;
            case "OrderCancelled":
                await HandleOrderCancelledAsync(message.Body.ToString());
                break;
            default:
                _logger.LogWarning("未知的事件类型: {EventType}", eventType);
                break;
        }
    }

    private Task HandleOrderCreatedAsync(string messageBody)
    {
        _logger.LogInformation("处理订单创建事件");
        return Task.CompletedTask;
    }

    private Task HandleOrderCancelledAsync(string messageBody)
    {
        _logger.LogInformation("处理订单取消事件");
        return Task.CompletedTask;
    }
}

Durable Functions(持久函数)

Durable Functions 用于编排长时间运行的工作流。

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;

// 编排器函数
public class OrderWorkflowOrchestrator
{
    [Function("OrderWorkflowOrchestrator")]
    public async Task<string> RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        var orderId = context.GetInput<string>();
        var logger = context.CreateReplaySafeLogger<OrderWorkflowOrchestrator>();

        logger.LogInformation("开始处理订单: {OrderId}", orderId);

        try
        {
            // 步骤 1:验证订单
            await context.CallActivityAsync("ValidateOrder", orderId);
            logger.LogInformation("订单验证完成");

            // 步骤 2:处理支付
            var paymentResult = await context.CallActivityAsync<bool>("ProcessPayment", orderId);
            
            if (!paymentResult)
            {
                logger.LogWarning("支付失败");
                return "PaymentFailed";
            }

            // 步骤 3:更新库存
            await context.CallActivityAsync("UpdateInventory", orderId);

            // 步骤 4:发送确认邮件
            await context.CallActivityAsync("SendEmail", orderId);

            // 步骤 5:等待 24 小时后发送跟进邮件
            var deadline = context.CurrentUtcDateTime.AddHours(24);
            await context.CreateTimer(deadline, CancellationToken.None);

            await context.CallActivityAsync("SendFollowUpEmail", orderId);

            logger.LogInformation("订单工作流完成: {OrderId}", orderId);
            return "Completed";
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "订单工作流失败: {OrderId}", orderId);
            
            // 补偿操作
            await context.CallActivityAsync("CompensateOrder", orderId);
            
            return "Failed";
        }
    }
}

// 活动函数
public class OrderActivities
{
    [Function("ValidateOrder")]
    public Task ValidateOrder([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("ValidateOrder");
        logger.LogInformation("验证订单: {OrderId}", orderId);
        
        // 实际验证逻辑
        return Task.CompletedTask;
    }

    [Function("ProcessPayment")]
    public async Task<bool> ProcessPayment([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("ProcessPayment");
        logger.LogInformation("处理支付: {OrderId}", orderId);
        
        // 模拟支付处理
        await Task.Delay(1000);
        return true;
    }

    [Function("UpdateInventory")]
    public Task UpdateInventory([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("UpdateInventory");
        logger.LogInformation("更新库存: {OrderId}", orderId);
        
        return Task.CompletedTask;
    }

    [Function("SendEmail")]
    public Task SendEmail([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("SendEmail");
        logger.LogInformation("发送确认邮件: {OrderId}", orderId);
        
        return Task.CompletedTask;
    }

    [Function("SendFollowUpEmail")]
    public Task SendFollowUpEmail([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("SendFollowUpEmail");
        logger.LogInformation("发送跟进邮件: {OrderId}", orderId);
        
        return Task.CompletedTask;
    }

    [Function("CompensateOrder")]
    public Task CompensateOrder([ActivityTrigger] string orderId, FunctionContext context)
    {
        var logger = context.GetLogger("CompensateOrder");
        logger.LogInformation("补偿订单: {OrderId}", orderId);
        
        // 回滚操作:退款、恢复库存等
        return Task.CompletedTask;
    }
}

// HTTP 触发器启动编排
public class OrderWorkflowStarter
{
    [Function("StartOrderWorkflow")]
    public async Task<HttpResponseData> StartWorkflow(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
        [DurableClient] DurableTaskClient client,
        FunctionContext context)
    {
        var logger = context.GetLogger("StartOrderWorkflow");

        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var order = JsonSerializer.Deserialize<Order>(body);

        // 启动编排
        var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
            "OrderWorkflowOrchestrator",
            order!.Id);

        logger.LogInformation("已启动工作流实例: {InstanceId}", instanceId);

        // 返回状态查询 URL
        var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
        response.Headers.Add("Location", $"/api/status/{instanceId}");
        
        await response.WriteAsJsonAsync(new { instanceId });
        
        return response;
    }

    [Function("GetWorkflowStatus")]
    public async Task<HttpResponseData> GetStatus(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "status/{instanceId}")] HttpRequestData req,
        [DurableClient] DurableTaskClient client,
        string instanceId)
    {
        // 获取编排状态
        var metadata = await client.GetInstanceAsync(instanceId);

        var response = req.CreateResponse();
        
        if (metadata == null)
        {
            response.StatusCode = System.Net.HttpStatusCode.NotFound;
            return response;
        }

        await response.WriteAsJsonAsync(new
        {
            instanceId,
            runtimeStatus = metadata.RuntimeStatus.ToString(),
            input = metadata.SerializedInput,
            output = metadata.SerializedOutput,
            createdTime = metadata.CreatedAt,
            lastUpdatedTime = metadata.LastUpdatedAt
        });

        return response;
    }
}

部署 Azure Functions

# 创建资源组
az group create --name myfunctions-rg --location eastus

# 创建存储账户(Functions 需要)
az storage account create \
    --name myfunctionsstorage \
    --resource-group myfunctions-rg \
    --location eastus \
    --sku Standard_LRS

# 创建 Function App
az functionapp create \
    --name myfunctionapp \
    --resource-group myfunctions-rg \
    --storage-account myfunctionsstorage \
    --consumption-plan-location eastus \
    --runtime dotnet-isolated \
    --runtime-version 8 \
    --functions-version 4

# 部署代码
func azure functionapp publish myfunctionapp

# 查看日志
func azure functionapp logstream myfunctionapp

35.5 云数据库与存储

云原生应用通常使用托管的数据库服务,而非自建数据库,以降低运维负担并提高可用性。

Azure Cosmos DB

Cosmos DB 是 Azure 的全球分布式多模型数据库,支持 SQL、MongoDB、Cassandra、Gremlin 和 Table API。

安装 SDK

dotnet add package Microsoft.Azure.Cosmos

使用 Cosmos DB SQL API

using Microsoft.Azure.Cosmos;

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(sp =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();
    var endpoint = configuration["CosmosDb:Endpoint"];
    var key = configuration["CosmosDb:Key"];
    
    return new CosmosClient(endpoint, key);
});

builder.Services.AddSingleton<ProductRepository>();

var app = builder.Build();
app.Run();

// ProductRepository.cs
public class ProductRepository
{
    private readonly Container _container;
    private readonly ILogger<ProductRepository> _logger;

    public ProductRepository(CosmosClient cosmosClient, IConfiguration configuration, ILogger<ProductRepository> logger)
    {
        var databaseName = configuration["CosmosDb:DatabaseName"];
        var containerName = configuration["CosmosDb:ContainerName"];
        
        _container = cosmosClient.GetContainer(databaseName, containerName);
        _logger = logger;
    }

    // 创建产品
    public async Task<Product> CreateProductAsync(Product product)
    {
        var response = await _container.CreateItemAsync(product, new PartitionKey(product.Category));
        
        _logger.LogInformation("产品已创建: {ProductId}, RU消耗: {RU}", product.Id, response.RequestCharge);
        
        return response.Resource;
    }

    // 获取产品
    public async Task<Product?> GetProductAsync(string id, string category)
    {
        try
        {
            var response = await _container.ReadItemAsync<Product>(id, new PartitionKey(category));
            return response.Resource;
        }
        catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return null;
        }
    }

    // 查询产品
    public async Task<List<Product>> QueryProductsAsync(string category, decimal minPrice, decimal maxPrice)
    {
        var query = new QueryDefinition(
            "SELECT * FROM c WHERE c.category = @category AND c.price >= @minPrice AND c.price <= @maxPrice")
            .WithParameter("@category", category)
            .WithParameter("@minPrice", minPrice)
            .WithParameter("@maxPrice", maxPrice);

        var iterator = _container.GetItemQueryIterator<Product>(query);
        var products = new List<Product>();

        while (iterator.HasMoreResults)
        {
            var response = await iterator.ReadNextAsync();
            products.AddRange(response);
            
            _logger.LogInformation("查询消耗 RU: {RU}", response.RequestCharge);
        }

        return products;
    }

    // 更新产品
    public async Task<Product> UpdateProductAsync(Product product)
    {
        var response = await _container.ReplaceItemAsync(
            product,
            product.Id,
            new PartitionKey(product.Category));

        return response.Resource;
    }

    // 部分更新(Patch)
    public async Task<Product> UpdatePriceAsync(string id, string category, decimal newPrice)
    {
        var patchOperations = new[]
        {
            PatchOperation.Replace("/price", newPrice),
            PatchOperation.Set("/updatedAt", DateTime.UtcNow)
        };

        var response = await _container.PatchItemAsync<Product>(
            id,
            new PartitionKey(category),
            patchOperations);

        return response.Resource;
    }

    // 删除产品
    public async Task<bool> DeleteProductAsync(string id, string category)
    {
        try
        {
            await _container.DeleteItemAsync<Product>(id, new PartitionKey(category));
            return true;
        }
        catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return false;
        }
    }

    // 事务操作(同一分区键内)
    public async Task TransferInventoryAsync(string fromProductId, string toProductId, string category, int quantity)
    {
        var batch = _container.CreateTransactionalBatch(new PartitionKey(category));

        // 减少来源产品库存
        batch.PatchItem(fromProductId, new[]
        {
            PatchOperation.Increment("/stock", -quantity)
        });

        // 增加目标产品库存
        batch.PatchItem(toProductId, new[]
        {
            PatchOperation.Increment("/stock", quantity)
        });

        using var response = await batch.ExecuteAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"事务失败: {response.StatusCode}");
        }
    }
}

public class Product
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [JsonPropertyName("name")]
    public string Name { get; set; } = null!;

    [JsonPropertyName("category")]
    public string Category { get; set; } = null!;

    [JsonPropertyName("price")]
    public decimal Price { get; set; }

    [JsonPropertyName("stock")]
    public int Stock { get; set; }

    [JsonPropertyName("createdAt")]
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    [JsonPropertyName("updatedAt")]
    public DateTime? UpdatedAt { get; set; }
}

Redis 分布式缓存

在云原生应用中,使用 Redis 作为分布式缓存是常见做法。

Azure Cache for Redis

# 创建 Redis 缓存
az redis create \
    --name myrediscache \
    --resource-group myapp-rg \
    --location eastus \
    --sku Basic \
    --vm-size c0

# 获取访问密钥
az redis list-keys --name myrediscache --resource-group myapp-rg

使用 StackExchange.Redis

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:Connection"];
    options.InstanceName = "MyApp:";
});

builder.Services.AddSingleton<CacheService>();

var app = builder.Build();
app.Run();

// CacheService.cs
public class CacheService
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<CacheService> _logger;

    public CacheService(IDistributedCache cache, ILogger<CacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    // 获取或创建缓存
    public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null)
    {
        var cached = await _cache.GetStringAsync(key);

        if (cached != null)
        {
            _logger.LogInformation("缓存命中: {Key}", key);
            return JsonSerializer.Deserialize<T>(cached)!;
        }

        _logger.LogInformation("缓存未命中: {Key}", key);

        var value = await factory();

        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
        };

        await _cache.SetStringAsync(key, JsonSerializer.Serialize(value), options);

        return value;
    }

    // 删除缓存
    public async Task RemoveAsync(string key)
    {
        await _cache.RemoveAsync(key);
        _logger.LogInformation("缓存已删除: {Key}", key);
    }

    // 删除匹配的键(需要使用 StackExchange.Redis 直接访问)
    public async Task RemoveByPatternAsync(string pattern)
    {
        // 此功能需要直接使用 IConnectionMultiplexer
        _logger.LogWarning("RemoveByPattern 需要直接使用 Redis 连接");
    }
}

35.6 可观测性(日志、指标、追踪)

云原生应用的可观测性包括三个支柱:日志(Logs)、指标(Metrics)和追踪(Traces)。

OpenTelemetry

OpenTelemetry 是云原生计算基金会(CNCF)的可观测性标准。

安装 OpenTelemetry SDK

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient
dotnet add package OpenTelemetry.Extensions.Hosting

配置 OpenTelemetry

using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

var serviceName = "MyCloudNativeApp";
var serviceVersion = "1.0.0";

// 配置 OpenTelemetry
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(serviceName, serviceVersion: serviceVersion)
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = builder.Environment.EnvironmentName,
            ["host.name"] = Environment.MachineName
        }))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.EnrichWithHttpRequest = (activity, httpRequest) =>
            {
                activity.SetTag("http.client_ip", httpRequest.HttpContext.Connection.RemoteIpAddress);
            };
        })
        .AddHttpClientInstrumentation()
        .AddSqlClientInstrumentation(options =>
        {
            options.SetDbStatementForText = true;
            options.RecordException = true;
        })
        .AddConsoleExporter() // 开发环境
        .AddOtlpExporter(options => // 生产环境
        {
            options.Endpoint = new Uri(builder.Configuration["OpenTelemetry:Endpoint"]!);
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddProcessInstrumentation()
        .AddPrometheusExporter());

var app = builder.Build();

// 暴露 Prometheus 指标端点
app.MapPrometheusScrapingEndpoint();

app.Run();

自定义指标

using System.Diagnostics.Metrics;

public class OrderMetrics
{
    private readonly Counter<long> _ordersCreated;
    private readonly Counter<long> _ordersFailed;
    private readonly Histogram<double> _orderAmount;
    private readonly ObservableGauge<int> _activeOrders;

    public OrderMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("MyApp.Orders");

        _ordersCreated = meter.CreateCounter<long>(
            "orders.created",
            unit: "order",
            description: "订单创建数量");

        _ordersFailed = meter.CreateCounter<long>(
            "orders.failed",
            unit: "order",
            description: "订单失败数量");

        _orderAmount = meter.CreateHistogram<double>(
            "orders.amount",
            unit: "USD",
            description: "订单金额分布");

        _activeOrders = meter.CreateObservableGauge<int>(
            "orders.active",
            observeValue: () => GetActiveOrderCount(),
            unit: "order",
            description: "当前活跃订单数");
    }

    public void RecordOrderCreated(string orderType, decimal amount)
    {
        _ordersCreated.Add(1, new KeyValuePair<string, object?>("order.type", orderType));
        _orderAmount.Record((double)amount, new KeyValuePair<string, object?>("order.type", orderType));
    }

    public void RecordOrderFailed(string orderType, string reason)
    {
        _ordersFailed.Add(1,
            new KeyValuePair<string, object?>("order.type", orderType),
            new KeyValuePair<string, object?>("failure.reason", reason));
    }

    private int GetActiveOrderCount()
    {
        // 实际实现:查询数据库或缓存
        return 42;
    }
}

// 在服务中使用
public class OrderService
{
    private readonly OrderMetrics _metrics;
    private readonly ILogger<OrderService> _logger;

    public OrderService(OrderMetrics metrics, ILogger<OrderService> logger)
    {
        _metrics = metrics;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        try
        {
            // 创建订单逻辑
            await Task.Delay(100);

            _metrics.RecordOrderCreated(order.Type, order.Amount);
            _logger.LogInformation("订单已创建: {OrderId}", order.Id);

            return order;
        }
        catch (Exception ex)
        {
            _metrics.RecordOrderFailed(order.Type, ex.Message);
            _logger.LogError(ex, "订单创建失败: {OrderId}", order.Id);
            throw;
        }
    }
}

分布式追踪

using System.Diagnostics;

public class PaymentService
{
    private static readonly ActivitySource ActivitySource = new("MyApp.Payment");
    private readonly HttpClient _httpClient;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(HttpClient httpClient, ILogger<PaymentService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(string orderId, decimal amount)
    {
        // 创建一个新的活动(Span)
        using var activity = ActivitySource.StartActivity("ProcessPayment", ActivityKind.Internal);
        
        activity?.SetTag("order.id", orderId);
        activity?.SetTag("payment.amount", amount);

        try
        {
            // 步骤 1:验证支付信息
            using (var validateActivity = ActivitySource.StartActivity("ValidatePayment", ActivityKind.Internal))
            {
                await ValidatePaymentAsync(orderId, amount);
                validateActivity?.SetTag("validation.result", "success");
            }

            // 步骤 2:调用支付网关
            using (var gatewayActivity = ActivitySource.StartActivity("CallPaymentGateway", ActivityKind.Client))
            {
                gatewayActivity?.SetTag("payment.gateway", "stripe");

                var response = await _httpClient.PostAsJsonAsync("https://payment-gateway.example.com/charge", new
                {
                    orderId,
                    amount,
                    currency = "USD"
                });

                response.EnsureSuccessStatusCode();

                var result = await response.Content.ReadFromJsonAsync<PaymentResult>();
                
                gatewayActivity?.SetTag("payment.transaction_id", result!.TransactionId);
                
                activity?.SetTag("payment.status", "success");
                
                return result;
            }
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            
            _logger.LogError(ex, "支付处理失败: {OrderId}", orderId);
            throw;
        }
    }

    private Task ValidatePaymentAsync(string orderId, decimal amount)
    {
        // 验证逻辑
        return Task.CompletedTask;
    }
}

public class PaymentResult
{
    public string TransactionId { get; set; } = null!;
    public string Status { get; set; } = null!;
}

结构化日志

using Serilog;
using Serilog.Context;

// Program.cs - 配置 Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "MyCloudNativeApp")
    .Enrich.WithProperty("Environment", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown")
    .Enrich.WithMachineName()
    .WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter())
    .WriteTo.ApplicationInsights(
        builder.Configuration["ApplicationInsights:ConnectionString"],
        TelemetryConverter.Traces)
    .CreateLogger();

builder.Host.UseSerilog();

var app = builder.Build();

// 添加请求上下文中间件
app.Use(async (context, next) =>
{
    // 为每个请求添加唯一 ID
    var requestId = context.TraceIdentifier;
    
    using (LogContext.PushProperty("RequestId", requestId))
    using (LogContext.PushProperty("UserAgent", context.Request.Headers["User-Agent"].ToString()))
    using (LogContext.PushProperty("RemoteIp", context.Connection.RemoteIpAddress))
    {
        await next();
    }
});

app.Run();

// 在服务中使用结构化日志
public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        // 结构化日志:使用命名占位符
        _logger.LogInformation(
            "创建订单: {OrderId}, 类型: {OrderType}, 金额: {Amount}, 用户: {UserId}",
            order.Id, order.Type, order.Amount, order.UserId);

        try
        {
            // 处理订单
            await Task.Delay(100);

            _logger.LogInformation("订单创建成功: {OrderId}", order.Id);

            return order;
        }
        catch (Exception ex)
        {
            // 记录异常时附加上下文信息
            _logger.LogError(ex,
                "订单创建失败: {OrderId}, 用户: {UserId}, 错误: {ErrorMessage}",
                order.Id, order.UserId, ex.Message);

            throw;
        }
    }
}

健康检查

using Microsoft.Extensions.Diagnostics.HealthChecks;

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<RedisHealthCheck>("redis")
    .AddCheck<ExternalApiHealthCheck>("external-api");

var app = builder.Build();

// 简单的健康检查端点
app.MapHealthChecks("/health");

// 详细的健康检查端点(包含所有检查项)
app.MapHealthChecks("/health/details", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var result = JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds,
                exception = e.Value.Exception?.Message
            }),
            totalDuration = report.TotalDuration.TotalMilliseconds
        });

        await context.Response.WriteAsync(result);
    }
});

app.Run();

// 自定义健康检查
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly AppDbContext _dbContext;

    public DatabaseHealthCheck(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // 尝试连接数据库
            await _dbContext.Database.CanConnectAsync(cancellationToken);
            
            // 执行简单查询
            var count = await _dbContext.Products.CountAsync(cancellationToken);

            return HealthCheckResult.Healthy($"数据库正常,共 {count} 个产品");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("数据库连接失败", ex);
        }
    }
}

public class RedisHealthCheck : IHealthCheck
{
    private readonly IDistributedCache _cache;

    public RedisHealthCheck(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // 尝试写入和读取
            var key = "health-check";
            var value = DateTime.UtcNow.ToString();

            await _cache.SetStringAsync(key, value, cancellationToken);
            var cached = await _cache.GetStringAsync(key, cancellationToken);

            if (cached == value)
            {
                await _cache.RemoveAsync(key, cancellationToken);
                return HealthCheckResult.Healthy("Redis 正常");
            }

            return HealthCheckResult.Degraded("Redis 读写不一致");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Redis 连接失败", ex);
        }
    }
}

public class ExternalApiHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;

    public ExternalApiHealthCheck(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient();
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var response = await _httpClient.GetAsync(
                "https://api.example.com/health",
                cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                return HealthCheckResult.Healthy("外部 API 正常");
            }

            return HealthCheckResult.Degraded($"外部 API 返回 {response.StatusCode}");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("外部 API 不可用", ex);
        }
    }
}

35.7 实战:部署到云平台

本节将通过一个完整的示例,展示如何将 ASP.NET Core 应用部署到 Azure 云平台。

项目准备

项目结构

MyCloudApp/
├── MyCloudApp.Api/          # Web API 项目
├── MyCloudApp.Functions/    # Azure Functions 项目
├── MyCloudApp.Core/         # 核心业务逻辑
├── MyCloudApp.Infrastructure/ # 基础设施层
├── docker-compose.yml       # 本地开发环境
├── Dockerfile               # API 镜像
└── azure-deploy/            # 部署脚本
    ├── bicep/               # 基础设施即代码
    └── scripts/             # 部署脚本

Bicep 基础设施定义azure-deploy/bicep/main.bicep):

param location string = resourceGroup().location
param appName string = 'mycloudapp'
param environment string = 'prod'

// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: '${appName}-plan-${environment}'
  location: location
  sku: {
    name: 'P1v3'
    tier: 'PremiumV3'
    capacity: 2
  }
  properties: {
    reserved: true // Linux
  }
}

// Web App
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
  name: '${appName}-api-${environment}'
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|8.0'
      alwaysOn: true
      minTlsVersion: '1.2'
      healthCheckPath: '/health'
      appSettings: [
        {
          name: 'ASPNETCORE_ENVIRONMENT'
          value: environment
        }
        {
          name: 'ApplicationInsights__ConnectionString'
          value: applicationInsights.properties.ConnectionString
        }
        {
          name: 'KeyVault__Url'
          value: keyVault.properties.vaultUri
        }
      ]
    }
  }
}

// Azure SQL Server
resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: '${appName}-sql-${environment}'
  location: location
  properties: {
    administratorLogin: 'sqladmin'
    administratorLoginPassword: 'P@ssw0rd1234!' // 实际使用时从 Key Vault 获取
    minimalTlsVersion: '1.2'
  }
}

// Azure SQL Database
resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
  parent: sqlServer
  name: '${appName}-db'
  location: location
  sku: {
    name: 'S1'
    tier: 'Standard'
  }
}

// Azure Cache for Redis
resource redis 'Microsoft.Cache/redis@2023-04-01' = {
  name: '${appName}-redis-${environment}'
  location: location
  properties: {
    sku: {
      name: 'Basic'
      family: 'C'
      capacity: 1
    }
    enableNonSslPort: false
    minimumTlsVersion: '1.2'
  }
}

// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: '${appName}storage${environment}'
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}

// Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
  name: '${appName}-kv-${environment}'
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
  }
}

// Application Insights
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: '${appName}-ai-${environment}'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Flow_Type: 'Bluefield'
  }
}

// Grant Web App access to Key Vault
resource keyVaultRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(keyVault.id, webApp.id, 'SecretsUser')
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalId: webApp.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output keyVaultUrl string = keyVault.properties.vaultUri

部署脚本azure-deploy/scripts/deploy.sh):

#!/bin/bash

# 配置参数
RESOURCE_GROUP="mycloudapp-rg"
LOCATION="eastus"
APP_NAME="mycloudapp"
ENVIRONMENT="prod"

echo "========== 部署 MyCloudApp 到 Azure =========="

# 1. 登录 Azure
echo "步骤 1: 登录 Azure"
az login

# 2. 创建资源组
echo "步骤 2: 创建资源组"
az group create --name $RESOURCE_GROUP --location $LOCATION

# 3. 部署基础设施
echo "步骤 3: 部署基础设施"
az deployment group create \
    --resource-group $RESOURCE_GROUP \
    --template-file ../bicep/main.bicep \
    --parameters appName=$APP_NAME environment=$ENVIRONMENT

# 4. 构建和推送 Docker 镜像
echo "步骤 4: 构建 Docker 镜像"
cd ../../
docker build -t $APP_NAME:latest -f Dockerfile .

# 标记并推送到 Azure Container Registry
ACR_NAME="${APP_NAME}acr${ENVIRONMENT}"
az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic
az acr login --name $ACR_NAME

docker tag $APP_NAME:latest $ACR_NAME.azurecr.io/$APP_NAME:latest
docker push $ACR_NAME.azurecr.io/$APP_NAME:latest

# 5. 配置 Web App 使用容器镜像
echo "步骤 5: 配置 Web App"
WEB_APP_NAME="${APP_NAME}-api-${ENVIRONMENT}"

az webapp config container set \
    --name $WEB_APP_NAME \
    --resource-group $RESOURCE_GROUP \
    --docker-custom-image-name $ACR_NAME.azurecr.io/$APP_NAME:latest \
    --docker-registry-server-url https://$ACR_NAME.azurecr.io

# 6. 存储敏感配置到 Key Vault
echo "步骤 6: 配置 Key Vault"
KEY_VAULT_NAME="${APP_NAME}-kv-${ENVIRONMENT}"

# 获取 SQL 连接字符串
SQL_CONNECTION=$(az sql db show-connection-string \
    --server ${APP_NAME}-sql-${ENVIRONMENT} \
    --name ${APP_NAME}-db \
    --client ado.net \
    --output tsv)

# 存储到 Key Vault
az keyvault secret set --vault-name $KEY_VAULT_NAME --name "ConnectionString" --value "$SQL_CONNECTION"

# 7. 运行数据库迁移
echo "步骤 7: 运行数据库迁移"
dotnet ef database update --project MyCloudApp.Api

# 8. 验证部署
echo "步骤 8: 验证部署"
WEB_APP_URL=$(az webapp show --name $WEB_APP_NAME --resource-group $RESOURCE_GROUP --query defaultHostName -o tsv)

echo "等待应用启动..."
sleep 30

curl -f https://$WEB_APP_URL/health || {
    echo "健康检查失败!"
    exit 1
}

echo "========== 部署完成 =========="
echo "应用 URL: https://$WEB_APP_URL"
echo "Key Vault: https://${KEY_VAULT_NAME}.vault.azure.net"

CI/CD 流水线

GitHub Actions 完整流水线.github/workflows/azure-deploy.yml):

name: Deploy to Azure

on:
  push:
    branches: [ main ]
  workflow_dispatch:

env:
  AZURE_WEBAPP_NAME: mycloudapp-api-prod
  DOTNET_VERSION: '8.0.x'

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --no-restore -c Release
    
    - name: Test
      run: dotnet test --no-build -c Release --logger "trx"
    
    - name: Publish
      run: dotnet publish MyCloudApp.Api/MyCloudApp.Api.csproj -c Release -o ./publish
    
    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: webapp
        path: ./publish

  deploy-infrastructure:
    needs: build-and-test
    runs-on: ubuntu-latest
    outputs:
      webapp-url: ${{ steps.deploy.outputs.webAppUrl }}
    steps:
    - uses: actions/checkout@v4
    
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    - name: Deploy Bicep
      id: deploy
      uses: azure/arm-deploy@v1
      with:
        subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
        resourceGroupName: mycloudapp-rg
        template: ./azure-deploy/bicep/main.bicep
        parameters: 'appName=mycloudapp environment=prod'

  deploy-app:
    needs: deploy-infrastructure
    runs-on: ubuntu-latest
    environment:
      name: production
      url: ${{ needs.deploy-infrastructure.outputs.webapp-url }}
    steps:
    - name: Download artifact
      uses: actions/download-artifact@v4
      with:
        name: webapp
        path: ./publish
    
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    - name: Deploy to Azure Web App
      uses: azure/webapps-deploy@v2
      with:
        app-name: ${{ env.AZURE_WEBAPP_NAME }}
        package: ./publish
    
    - name: Run Database Migration
      run: |
        # 在部署环境中运行迁移
        # 实际实现可能需要使用 Azure CLI 或专门的迁移作业
        echo "Running database migrations..."
    
    - name: Smoke Test
      run: |
        sleep 30
        curl -f ${{ needs.deploy-infrastructure.outputs.webapp-url }}/health || exit 1

  notify:
    needs: deploy-app
    runs-on: ubuntu-latest
    if: always()
    steps:
    - name: Notify Slack
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        text: 'Deployment to Azure completed!'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

常见误区与陷阱

误区1:忽略成本优化

❌ 错误做法

// 不考虑成本,频繁调用云服务
public async Task<string> GetUserNameAsync(string userId)
{
    // 每次都从 Cosmos DB 查询(每次查询消耗 RU)
    var user = await _cosmosContainer.ReadItemAsync<User>(userId, new PartitionKey(userId));
    return user.Resource.Name;
}

问题分析

  • Cosmos DB 按 RU(请求单元)计费,频繁查询成本高
  • 没有使用缓存,浪费资源
  • 未考虑批量操作优化

✅ 正确做法

public class UserService
{
    private readonly Container _cosmosContainer;
    private readonly IDistributedCache _cache;
    private readonly ILogger<UserService> _logger;

    public UserService(Container cosmosContainer, IDistributedCache cache, ILogger<UserService> logger)
    {
        _cosmosContainer = cosmosContainer;
        _cache = cache;
        _logger = logger;
    }

    // 使用缓存减少 Cosmos DB 查询
    public async Task<string> GetUserNameAsync(string userId)
    {
        var cacheKey = $"user:{userId}:name";
        var cached = await _cache.GetStringAsync(cacheKey);

        if (cached != null)
        {
            _logger.LogInformation("缓存命中: {UserId}", userId);
            return cached;
        }

        var user = await _cosmosContainer.ReadItemAsync<User>(userId, new PartitionKey(userId));
        var name = user.Resource.Name;

        // 缓存 1 小时
        await _cache.SetStringAsync(cacheKey, name, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });

        _logger.LogInformation("Cosmos DB 查询消耗 RU: {RU}", user.RequestCharge);

        return name;
    }

    // 批量操作优化
    public async Task<List<User>> GetUsersAsync(List<string> userIds)
    {
        // 使用批量读取减少 RU 消耗
        var tasks = userIds.Select(async id =>
        {
            try
            {
                var response = await _cosmosContainer.ReadItemAsync<User>(id, new PartitionKey(id));
                return response.Resource;
            }
            catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return null;
            }
        });

        var results = await Task.WhenAll(tasks);
        return results.Where(u => u != null).ToList()!;
    }
}

成本优化最佳实践

  1. 使用缓存减少数据库查询
  2. 批量操作替代单个操作
  3. 选择合适的服务层级(不要过度配置)
  4. 设置资源使用告警
  5. 定期审查成本报告

误区2:硬编码区域和端点

❌ 错误做法

// 硬编码 Azure 区域
public class BlobStorageService
{
    private readonly BlobServiceClient _blobClient;

    public BlobStorageService()
    {
        // ❌ 硬编码端点,无法切换区域或环境
        _blobClient = new BlobServiceClient("https://mystorageaccount.blob.core.windows.net");
    }
}

问题分析

  • 无法在不同环境(开发、测试、生产)使用不同账户
  • 无法快速切换到备用区域(灾难恢复)
  • 不支持多区域部署

✅ 正确做法

public class BlobStorageService
{
    private readonly BlobServiceClient _blobClient;
    private readonly ILogger<BlobStorageService> _logger;

    public BlobStorageService(IConfiguration configuration, ILogger<BlobStorageService> logger)
    {
        // ✅ 从配置读取端点
        var storageUrl = configuration["Azure:Storage:Url"];
        var credential = new DefaultAzureCredential();

        _blobClient = new BlobServiceClient(new Uri(storageUrl), credential);
        _logger = logger;
    }
}
// appsettings.Development.json
{
  "Azure": {
    "Storage": {
      "Url": "https://devstorageaccount.blob.core.windows.net"
    }
  }
}

// appsettings.Production.json
{
  "Azure": {
    "Storage": {
      "Url": "https://prodstorageaccount.blob.core.windows.net"
    }
  }
}

误区3:未处理瞬时故障

❌ 错误做法

public async Task<Order> CreateOrderAsync(Order order)
{
    // ❌ 没有重试机制,网络抖动会导致失败
    var response = await _httpClient.PostAsJsonAsync("https://api.example.com/orders", order);
    response.EnsureSuccessStatusCode();
    
    return await response.Content.ReadFromJsonAsync<Order>();
}

问题分析

  • 云服务可能出现瞬时故障(网络抖动、限流)
  • 没有重试机制导致不必要的失败
  • 影响用户体验和系统稳定性

✅ 正确做法

using Polly;
using Polly.Extensions.Http;

// Program.cs - 配置 Polly 重试策略
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<OrderApiClient>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError() // 处理 5xx 和 408
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests) // 处理 429
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 指数退避
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                Console.WriteLine($"重试第 {retryCount} 次,等待 {timespan.TotalSeconds} 秒");
            });
}

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 5,
            durationOfBreak: TimeSpan.FromSeconds(30),
            onBreak: (outcome, timespan) =>
            {
                Console.WriteLine($"断路器打开,持续 {timespan.TotalSeconds} 秒");
            },
            onReset: () =>
            {
                Console.WriteLine("断路器重置");
            });
}

var app = builder.Build();
app.Run();

// OrderApiClient.cs
public class OrderApiClient
{
    private readonly HttpClient _httpClient;

    public OrderApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        // Polly 策略会自动处理重试和断路器
        var response = await _httpClient.PostAsJsonAsync("https://api.example.com/orders", order);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadFromJsonAsync<Order>();
    }
}

误区4:未实现优雅关闭

❌ 错误做法

// 没有优雅关闭逻辑
public class MessageProcessorService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (true) // ❌ 忽略停止令牌
        {
            var messages = await ReceiveMessagesAsync();
            
            foreach (var message in messages)
            {
                await ProcessMessageAsync(message); // 可能正在处理时被强制终止
            }
        }
    }
}

问题分析

  • 容器重启或扩缩容时强制终止,可能丢失正在处理的消息
  • 未给正在执行的操作足够的时间完成
  • 数据可能处于不一致状态

✅ 正确做法

public class MessageProcessorService : BackgroundService
{
    private readonly ILogger<MessageProcessorService> _logger;
    private readonly SemaphoreSlim _processingLock = new(1, 1);

    public MessageProcessorService(ILogger<MessageProcessorService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("消息处理服务已启动");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await _processingLock.WaitAsync(stoppingToken);

                try
                {
                    var messages = await ReceiveMessagesAsync(stoppingToken);

                    foreach (var message in messages)
                    {
                        // ✅ 检查停止令牌
                        if (stoppingToken.IsCancellationRequested)
                        {
                            _logger.LogInformation("收到停止信号,停止处理新消息");
                            break;
                        }

                        await ProcessMessageAsync(message, stoppingToken);
                    }
                }
                finally
                {
                    _processingLock.Release();
                }

                // 短暂延迟,避免空轮询
                await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // 正常取消,不记录错误
                _logger.LogInformation("消息处理服务正在停止");
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理消息时出错");
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("开始优雅关闭消息处理服务");

        // 等待当前正在处理的消息完成(最多 30 秒)
        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

        try
        {
            await _processingLock.WaitAsync(linkedCts.Token);
            _processingLock.Release();
            
            _logger.LogInformation("所有消息处理完成");
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("优雅关闭超时,强制停止");
        }

        await base.StopAsync(cancellationToken);
    }

    private Task<List<string>> ReceiveMessagesAsync(CancellationToken cancellationToken)
    {
        // 接收消息逻辑
        return Task.FromResult(new List<string>());
    }

    private Task ProcessMessageAsync(string message, CancellationToken cancellationToken)
    {
        // 处理消息逻辑
        return Task.CompletedTask;
    }
}

Kubernetes 配置支持优雅关闭

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60  # ✅ 给应用 60 秒时间优雅关闭
      containers:
      - name: app
        image: myapp:1.0.0
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]  # 延迟 15 秒,确保负载均衡器已移除此实例

误区5:过度依赖云服务特性

❌ 错误做法

// 直接使用 Azure 特定 API,无法迁移到其他云
public class NotificationService
{
    private readonly QueueClient _queueClient;

    public NotificationService()
    {
        // ❌ 紧密耦合到 Azure Storage Queue
        _queueClient = new QueueClient("connectionstring", "notifications");
    }

    public async Task SendNotificationAsync(string message)
    {
        await _queueClient.SendMessageAsync(message);
    }
}

问题分析

  • 紧密耦合到特定云平台
  • 难以迁移到其他云或本地环境
  • 测试困难(需要真实的云服务)

✅ 正确做法

// 定义抽象接口
public interface IMessageQueue
{
    Task SendMessageAsync(string queueName, string message);
    Task<List<string>> ReceiveMessagesAsync(string queueName, int maxMessages = 10);
    Task DeleteMessageAsync(string queueName, string messageId);
}

// Azure 实现
public class AzureStorageQueueAdapter : IMessageQueue
{
    private readonly QueueServiceClient _queueServiceClient;

    public AzureStorageQueueAdapter(IConfiguration configuration)
    {
        var connectionString = configuration["Azure:Storage:ConnectionString"];
        _queueServiceClient = new QueueServiceClient(connectionString);
    }

    public async Task SendMessageAsync(string queueName, string message)
    {
        var queueClient = _queueServiceClient.GetQueueClient(queueName);
        await queueClient.CreateIfNotExistsAsync();
        await queueClient.SendMessageAsync(message);
    }

    // 其他方法实现...
}

// AWS 实现
public class AwsSqsAdapter : IMessageQueue
{
    private readonly IAmazonSQS _sqsClient;

    public AwsSqsAdapter(IAmazonSQS sqsClient)
    {
        _sqsClient = sqsClient;
    }

    public async Task SendMessageAsync(string queueName, string message)
    {
        var queueUrl = await GetQueueUrlAsync(queueName);
        await _sqsClient.SendMessageAsync(queueUrl, message);
    }

    // 其他方法实现...
}

// 在 Program.cs 中根据配置选择实现
var builder = WebApplication.CreateBuilder(args);

var cloudProvider = builder.Configuration["CloudProvider"]; // "Azure" 或 "AWS"

if (cloudProvider == "Azure")
{
    builder.Services.AddSingleton<IMessageQueue, AzureStorageQueueAdapter>();
}
else if (cloudProvider == "AWS")
{
    builder.Services.AddSingleton<IMessageQueue, AwsSqsAdapter>();
}

// 业务代码使用抽象接口
public class NotificationService
{
    private readonly IMessageQueue _messageQueue;

    public NotificationService(IMessageQueue messageQueue)
    {
        _messageQueue = messageQueue;
    }

    public async Task SendNotificationAsync(string message)
    {
        await _messageQueue.SendMessageAsync("notifications", message);
    }
}

最佳实践

  1. 使用抽象接口隔离云服务依赖
  2. 支持多云部署策略
  3. 便于本地开发和测试(使用模拟实现)
  4. 降低供应商锁定风险

实战练习

练习 35.1:部署无服务器函数到 Azure

要求
设计并实现一个 Azure Function,用于处理用户上传的图片:

  1. 监听 Blob 容器的文件上传事件
  2. 自动生成缩略图(宽度 200px)
  3. 提取图片元数据(尺寸、格式、大小)
  4. 将元数据存储到 Cosmos DB
  5. 使用 Application Insights 记录处理时间

提示

  • 使用 Blob 触发器
  • 使用 SixLabors.ImageSharp 处理图片
  • 配置 CosmosDB 输出绑定
  • 使用结构化日志记录关键信息
参考答案
# 创建 Functions 项目
func init ImageProcessorFunction --dotnet-isolated
cd ImageProcessorFunction
func new --name ProcessImage --template "Blob trigger"

# 安装依赖
dotnet add package Microsoft.Azure.Functions.Worker
dotnet add package Microsoft.Azure.Functions.Worker.Sdk
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs
dotnet add package Microsoft.Azure.Cosmos
dotnet add package SixLabors.ImageSharp
dotnet add package Microsoft.ApplicationInsights.WorkerService
using Azure.Storage.Blobs;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System.Diagnostics;

public class ImageProcessorFunction
{
    private readonly ILogger<ImageProcessorFunction> _logger;
    private readonly Container _cosmosContainer;
    private readonly BlobServiceClient _blobServiceClient;

    public ImageProcessorFunction(
        ILogger<ImageProcessorFunction> logger,
        CosmosClient cosmosClient,
        BlobServiceClient blobServiceClient)
    {
        _logger = logger;
        _cosmosContainer = cosmosClient.GetContainer("imagedb", "metadata");
        _blobServiceClient = blobServiceClient;
    }

    [Function("ProcessImage")]
    public async Task Run(
        [BlobTrigger("uploads/{name}", Connection = "AzureWebJobsStorage")] Stream imageStream,
        string name,
        FunctionContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        _logger.LogInformation("开始处理图片: {ImageName}, 大小: {Size} bytes", name, imageStream.Length);

        try
        {
            // 1. 加载图片
            using var image = await Image.LoadAsync(imageStream);

            var originalWidth = image.Width;
            var originalHeight = image.Height;
            var format = image.Metadata.DecodedImageFormat?.Name ?? "Unknown";

            _logger.LogInformation("图片信息: {Width}x{Height}, 格式: {Format}",
                originalWidth, originalHeight, format);

            // 2. 生成缩略图
            var thumbnail = image.Clone(ctx => ctx.Resize(new ResizeOptions
            {
                Mode = ResizeMode.Max,
                Size = new Size(200, 0)
            }));

            // 3. 保存缩略图到 thumbnails 容器
            var thumbnailContainerClient = _blobServiceClient.GetBlobContainerClient("thumbnails");
            await thumbnailContainerClient.CreateIfNotExistsAsync();

            var thumbnailBlobClient = thumbnailContainerClient.GetBlobClient(name);

            using var thumbnailStream = new MemoryStream();
            await thumbnail.SaveAsJpegAsync(thumbnailStream);
            thumbnailStream.Position = 0;

            await thumbnailBlobClient.UploadAsync(thumbnailStream, overwrite: true);

            _logger.LogInformation("缩略图已生成: {ThumbnailName}", name);

            // 4. 提取元数据并存储到 Cosmos DB
            var metadata = new ImageMetadata
            {
                Id = Guid.NewGuid().ToString(),
                FileName = name,
                OriginalWidth = originalWidth,
                OriginalHeight = originalHeight,
                Format = format,
                FileSizeBytes = imageStream.Length,
                ThumbnailUrl = thumbnailBlobClient.Uri.ToString(),
                ProcessedAt = DateTime.UtcNow
            };

            await _cosmosContainer.CreateItemAsync(metadata, new PartitionKey(metadata.Id));

            stopwatch.Stop();

            _logger.LogInformation(
                "图片处理完成: {ImageName}, 耗时: {ElapsedMs}ms",
                name, stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            _logger.LogError(ex,
                "图片处理失败: {ImageName}, 耗时: {ElapsedMs}ms",
                name, stopwatch.ElapsedMilliseconds);

            throw;
        }
    }
}

public class ImageMetadata
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = null!;

    [JsonPropertyName("fileName")]
    public string FileName { get; set; } = null!;

    [JsonPropertyName("originalWidth")]
    public int OriginalWidth { get; set; }

    [JsonPropertyName("originalHeight")]
    public int OriginalHeight { get; set; }

    [JsonPropertyName("format")]
    public string Format { get; set; } = null!;

    [JsonPropertyName("fileSizeBytes")]
    public long FileSizeBytes { get; set; }

    [JsonPropertyName("thumbnailUrl")]
    public string ThumbnailUrl { get; set; } = null!;

    [JsonPropertyName("processedAt")]
    public DateTime ProcessedAt { get; set; }
}
// Program.cs - 配置依赖注入
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Azure.Storage.Blobs;
using Azure.Identity;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();

        // 配置 Cosmos DB
        services.AddSingleton(sp =>
        {
            var cosmosEndpoint = Environment.GetEnvironmentVariable("CosmosDb:Endpoint");
            return new CosmosClient(cosmosEndpoint, new DefaultAzureCredential());
        });

        // 配置 Blob Storage
        services.AddSingleton(sp =>
        {
            var storageUrl = Environment.GetEnvironmentVariable("Azure:Storage:Url");
            return new BlobServiceClient(new Uri(storageUrl), new DefaultAzureCredential());
        });
    })
    .Build();

host.Run();
// local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "CosmosDb:Endpoint": "https://your-cosmos-account.documents.azure.com:443/",
    "Azure:Storage:Url": "https://your-storage-account.blob.core.windows.net",
    "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=your-key"
  }
}

测试

# 本地运行
func start

# 上传测试图片
curl -X POST "http://localhost:7071/admin/functions/ProcessImage" \
  -H "Content-Type: application/json" \
  -d '{"input":"test.jpg"}'

# 部署到 Azure
az functionapp create \
    --name imageprocessor \
    --resource-group myrg \
    --storage-account mystorage \
    --consumption-plan-location eastus \
    --runtime dotnet-isolated \
    --runtime-version 8 \
    --functions-version 4

func azure functionapp publish imageprocessor

练习 35.2:实现多云抽象层

要求
设计一个抽象层,使应用能够在 Azure 和 AWS 之间无缝切换:

  1. 定义统一的对象存储接口
  2. 实现 Azure Blob Storage 适配器
  3. 实现 AWS S3 适配器
  4. 支持通过配置切换云提供商
  5. 编写单元测试验证两种实现的行为一致性

提示

  • 使用接口定义抽象
  • 使用工厂模式创建适配器
  • 考虑云服务的差异(如元数据存储方式)
  • 使用 Moq 库编写单元测试
参考答案
// ICloudStorage.cs - 统一接口
public interface ICloudStorage
{
    Task<string> UploadFileAsync(string containerName, string fileName, Stream content, Dictionary<string, string>? metadata = null);
    Task<Stream> DownloadFileAsync(string containerName, string fileName);
    Task<bool> DeleteFileAsync(string containerName, string fileName);
    Task<bool> FileExistsAsync(string containerName, string fileName);
    Task<List<string>> ListFilesAsync(string containerName, string? prefix = null);
    Task<Dictionary<string, string>> GetMetadataAsync(string containerName, string fileName);
}
// AzureBlobStorageAdapter.cs
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

public class AzureBlobStorageAdapter : ICloudStorage
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly ILogger<AzureBlobStorageAdapter> _logger;

    public AzureBlobStorageAdapter(BlobServiceClient blobServiceClient, ILogger<AzureBlobStorageAdapter> logger)
    {
        _blobServiceClient = blobServiceClient;
        _logger = logger;
    }

    public async Task<string> UploadFileAsync(string containerName, string fileName, Stream content, Dictionary<string, string>? metadata = null)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient(fileName);

        var blobHttpHeaders = new BlobHttpHeaders
        {
            ContentType = GetContentType(fileName)
        };

        var uploadOptions = new BlobUploadOptions
        {
            HttpHeaders = blobHttpHeaders,
            Metadata = metadata
        };

        await blobClient.UploadAsync(content, uploadOptions);

        _logger.LogInformation("文件已上传到 Azure: {Container}/{FileName}", containerName, fileName);

        return blobClient.Uri.ToString();
    }

    public async Task<Stream> DownloadFileAsync(string containerName, string fileName)
    {
        var blobClient = _blobServiceClient.GetBlobContainerClient(containerName).GetBlobClient(fileName);
        var response = await blobClient.DownloadAsync();
        
        var memoryStream = new MemoryStream();
        await response.Value.Content.CopyToAsync(memoryStream);
        memoryStream.Position = 0;
        
        return memoryStream;
    }

    public async Task<bool> DeleteFileAsync(string containerName, string fileName)
    {
        var blobClient = _blobServiceClient.GetBlobContainerClient(containerName).GetBlobClient(fileName);
        var response = await blobClient.DeleteIfExistsAsync();
        
        return response.Value;
    }

    public async Task<bool> FileExistsAsync(string containerName, string fileName)
    {
        var blobClient = _blobServiceClient.GetBlobContainerClient(containerName).GetBlobClient(fileName);
        return await blobClient.ExistsAsync();
    }

    public async Task<List<string>> ListFilesAsync(string containerName, string? prefix = null)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var files = new List<string>();

        await foreach (var blobItem in containerClient.GetBlobsAsync(prefix: prefix))
        {
            files.Add(blobItem.Name);
        }

        return files;
    }

    public async Task<Dictionary<string, string>> GetMetadataAsync(string containerName, string fileName)
    {
        var blobClient = _blobServiceClient.GetBlobContainerClient(containerName).GetBlobClient(fileName);
        var properties = await blobClient.GetPropertiesAsync();
        
        return properties.Value.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }

    private string GetContentType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".pdf" => "application/pdf",
            ".txt" => "text/plain",
            ".json" => "application/json",
            _ => "application/octet-stream"
        };
    }
}
// AwsS3Adapter.cs
using Amazon.S3;
using Amazon.S3.Model;

public class AwsS3Adapter : ICloudStorage
{
    private readonly IAmazonS3 _s3Client;
    private readonly ILogger<AwsS3Adapter> _logger;

    public AwsS3Adapter(IAmazonS3 s3Client, ILogger<AwsS3Adapter> logger)
    {
        _s3Client = s3Client;
        _logger = logger;
    }

    public async Task<string> UploadFileAsync(string containerName, string fileName, Stream content, Dictionary<string, string>? metadata = null)
    {
        var request = new PutObjectRequest
        {
            BucketName = containerName,
            Key = fileName,
            InputStream = content,
            ContentType = GetContentType(fileName)
        };

        if (metadata != null)
        {
            foreach (var (key, value) in metadata)
            {
                request.Metadata.Add(key, value);
            }
        }

        await _s3Client.PutObjectAsync(request);

        _logger.LogInformation("文件已上传到 AWS S3: {Bucket}/{Key}", containerName, fileName);

        return $"https://{containerName}.s3.amazonaws.com/{fileName}";
    }

    public async Task<Stream> DownloadFileAsync(string containerName, string fileName)
    {
        var request = new GetObjectRequest
        {
            BucketName = containerName,
            Key = fileName
        };

        var response = await _s3Client.GetObjectAsync(request);
        
        var memoryStream = new MemoryStream();
        await response.ResponseStream.CopyToAsync(memoryStream);
        memoryStream.Position = 0;
        
        return memoryStream;
    }

    public async Task<bool> DeleteFileAsync(string containerName, string fileName)
    {
        try
        {
            await _s3Client.DeleteObjectAsync(containerName, fileName);
            return true;
        }
        catch (AmazonS3Exception)
        {
            return false;
        }
    }

    public async Task<bool> FileExistsAsync(string containerName, string fileName)
    {
        try
        {
            await _s3Client.GetObjectMetadataAsync(containerName, fileName);
            return true;
        }
        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return false;
        }
    }

    public async Task<List<string>> ListFilesAsync(string containerName, string? prefix = null)
    {
        var request = new ListObjectsV2Request
        {
            BucketName = containerName,
            Prefix = prefix
        };

        var files = new List<string>();
        ListObjectsV2Response response;

        do
        {
            response = await _s3Client.ListObjectsV2Async(request);
            files.AddRange(response.S3Objects.Select(obj => obj.Key));
            request.ContinuationToken = response.NextContinuationToken;
        }
        while (response.IsTruncated);

        return files;
    }

    public async Task<Dictionary<string, string>> GetMetadataAsync(string containerName, string fileName)
    {
        var response = await _s3Client.GetObjectMetadataAsync(containerName, fileName);
        return response.Metadata.Keys.ToDictionary(key => key, key => response.Metadata[key]);
    }

    private string GetContentType(string fileName)
    {
        var extension = Path.GetExtension(fileName).ToLowerInvariant();
        return extension switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".pdf" => "application/pdf",
            ".txt" => "text/plain",
            ".json" => "application/json",
            _ => "application/octet-stream"
        };
    }
}
// CloudStorageFactory.cs
public class CloudStorageFactory
{
    public static ICloudStorage Create(string provider, IServiceProvider serviceProvider)
    {
        return provider.ToLower() switch
        {
            "azure" => serviceProvider.GetRequiredService<AzureBlobStorageAdapter>(),
            "aws" => serviceProvider.GetRequiredService<AwsS3Adapter>(),
            _ => throw new ArgumentException($"不支持的云提供商: {provider}")
        };
    }
}
// Program.cs - 配置依赖注入
using Azure.Identity;
using Azure.Storage.Blobs;
using Amazon.S3;

var builder = WebApplication.CreateBuilder(args);

var cloudProvider = builder.Configuration["CloudProvider"]; // "Azure" 或 "AWS"

if (cloudProvider == "Azure")
{
    builder.Services.AddSingleton(sp =>
    {
        var storageUrl = builder.Configuration["Azure:Storage:Url"];
        return new BlobServiceClient(new Uri(storageUrl), new DefaultAzureCredential());
    });
    builder.Services.AddSingleton<AzureBlobStorageAdapter>();
    builder.Services.AddSingleton<ICloudStorage>(sp => sp.GetRequiredService<AzureBlobStorageAdapter>());
}
else if (cloudProvider == "AWS")
{
    builder.Services.AddAWSService<IAmazonS3>();
    builder.Services.AddSingleton<AwsS3Adapter>();
    builder.Services.AddSingleton<ICloudStorage>(sp => sp.GetRequiredService<AwsS3Adapter>());
}

var app = builder.Build();
app.Run();
// CloudStorageTests.cs - 单元测试
using Moq;
using Xunit;

public class CloudStorageTests
{
    [Theory]
    [InlineData("Azure")]
    [InlineData("AWS")]
    public async Task UploadFile_ShouldReturnUrl(string provider)
    {
        // Arrange
        var storage = CreateCloudStorage(provider);
        var content = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));

        // Act
        var url = await storage.UploadFileAsync("test-container", "test.txt", content);

        // Assert
        Assert.NotNull(url);
        Assert.Contains("test.txt", url);
    }

    [Theory]
    [InlineData("Azure")]
    [InlineData("AWS")]
    public async Task FileExists_AfterUpload_ShouldReturnTrue(string provider)
    {
        // Arrange
        var storage = CreateCloudStorage(provider);
        var content = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("test content"));

        // Act
        await storage.UploadFileAsync("test-container", "test.txt", content);
        var exists = await storage.FileExistsAsync("test-container", "test.txt");

        // Assert
        Assert.True(exists);
    }

    [Theory]
    [InlineData("Azure")]
    [InlineData("AWS")]
    public async Task DownloadFile_ShouldReturnOriginalContent(string provider)
    {
        // Arrange
        var storage = CreateCloudStorage(provider);
        var originalContent = "test content";
        var uploadStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(originalContent));

        // Act
        await storage.UploadFileAsync("test-container", "test.txt", uploadStream);
        var downloadStream = await storage.DownloadFileAsync("test-container", "test.txt");
        var downloadedContent = await new StreamReader(downloadStream).ReadToEndAsync();

        // Assert
        Assert.Equal(originalContent, downloadedContent);
    }

    private ICloudStorage CreateCloudStorage(string provider)
    {
        // 实际测试中使用模拟实现或测试容器
        throw new NotImplementedException("需要配置测试环境");
    }
}

练习 35.3:配置完整的可观测性方案

要求
为一个微服务应用配置完整的可观测性系统:

  1. 集成 OpenTelemetry(日志、指标、追踪)
  2. 实现自定义业务指标(订单创建、支付成功率)
  3. 配置分布式追踪(跨服务调用)
  4. 实现结构化日志(使用 Serilog)
  5. 添加健康检查端点(数据库、Redis、外部 API)
  6. 导出到 Prometheus 和 Application Insights

提示

  • 使用 OpenTelemetry SDK
  • 使用 ActivitySource 创建自定义 Span
  • 使用 Meter 创建自定义指标
  • 配置多个导出器(Console、OTLP、Prometheus)
  • 使用 IHealthCheck 接口
参考答案
# 安装依赖包
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Enrichers.Environment
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
dotnet add package AspNetCore.HealthChecks.Redis
dotnet add package AspNetCore.HealthChecks.SqlServer
// Program.cs - 完整的可观测性配置
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
using Serilog.Context;

// 配置 Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
    .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "ObservabilityDemo")
    .Enrich.WithProperty("Environment", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown")
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter())
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);

// 使用 Serilog
builder.Host.UseSerilog();

var serviceName = "ObservabilityDemo";
var serviceVersion = "1.0.0";

// 配置 OpenTelemetry
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(serviceName, serviceVersion: serviceVersion)
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment"] = builder.Environment.EnvironmentName,
            ["host.name"] = Environment.MachineName
        }))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.EnrichWithHttpRequest = (activity, httpRequest) =>
            {
                activity.SetTag("http.client_ip", httpRequest.HttpContext.Connection.RemoteIpAddress);
                activity.SetTag("http.user_agent", httpRequest.Headers["User-Agent"].ToString());
            };
            options.EnrichWithHttpResponse = (activity, httpResponse) =>
            {
                activity.SetTag("http.response_content_length", httpResponse.ContentLength);
            };
        })
        .AddHttpClientInstrumentation(options =>
        {
            options.RecordException = true;
        })
        .AddSqlClientInstrumentation(options =>
        {
            options.SetDbStatementForText = true;
            options.RecordException = true;
        })
        .AddSource("ObservabilityDemo.*") // 自定义 ActivitySource
        .AddConsoleExporter()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(builder.Configuration["OpenTelemetry:Endpoint"] ?? "http://localhost:4317");
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddProcessInstrumentation()
        .AddMeter("ObservabilityDemo.*") // 自定义 Meter
        .AddPrometheusExporter());

// 注册业务服务
builder.Services.AddSingleton<OrderMetrics>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<PaymentService>();

// 配置健康检查
builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "sql-server",
        tags: new[] { "db", "sql" })
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:Connection"]!,
        name: "redis",
        tags: new[] { "cache", "redis" })
    .AddUrlGroup(
        new Uri("https://api.example.com/health"),
        name: "external-api",
        tags: new[] { "external" });

builder.Services.AddControllers();

var app = builder.Build();

// 中间件:添加请求上下文
app.Use(async (context, next) =>
{
    var requestId = context.TraceIdentifier;
    
    using (LogContext.PushProperty("RequestId", requestId))
    using (LogContext.PushProperty("Path", context.Request.Path))
    using (LogContext.PushProperty("Method", context.Request.Method))
    {
        await next();
    }
});

// 暴露 Prometheus 指标端点
app.MapPrometheusScrapingEndpoint();

// 健康检查端点
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
    Predicate = _ => false // 简单的存活检查
});

app.MapControllers();

app.Run();
// OrderMetrics.cs - 自定义业务指标
using System.Diagnostics.Metrics;

public class OrderMetrics
{
    private readonly Counter<long> _ordersCreated;
    private readonly Counter<long> _ordersFailed;
    private readonly Histogram<double> _orderAmount;
    private readonly Histogram<double> _orderProcessingDuration;
    private readonly UpDownCounter<int> _activeOrders;

    public OrderMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("ObservabilityDemo.Orders");

        _ordersCreated = meter.CreateCounter<long>(
            "orders.created",
            unit: "order",
            description: "订单创建总数");

        _ordersFailed = meter.CreateCounter<long>(
            "orders.failed",
            unit: "order",
            description: "订单失败总数");

        _orderAmount = meter.CreateHistogram<double>(
            "orders.amount",
            unit: "USD",
            description: "订单金额分布");

        _orderProcessingDuration = meter.CreateHistogram<double>(
            "orders.processing.duration",
            unit: "ms",
            description: "订单处理耗时");

        _activeOrders = meter.CreateUpDownCounter<int>(
            "orders.active",
            unit: "order",
            description: "当前活跃订单数");
    }

    public void RecordOrderCreated(string orderType, decimal amount)
    {
        _ordersCreated.Add(1, new KeyValuePair<string, object?>("order.type", orderType));
        _orderAmount.Record((double)amount, new KeyValuePair<string, object?>("order.type", orderType));
    }

    public void RecordOrderFailed(string orderType, string reason)
    {
        _ordersFailed.Add(1,
            new KeyValuePair<string, object?>("order.type", orderType),
            new KeyValuePair<string, object?>("failure.reason", reason));
    }

    public void RecordProcessingDuration(double milliseconds, string orderType, bool success)
    {
        _orderProcessingDuration.Record(milliseconds,
            new KeyValuePair<string, object?>("order.type", orderType),
            new KeyValuePair<string, object?>("success", success));
    }

    public void IncrementActiveOrders()
    {
        _activeOrders.Add(1);
    }

    public void DecrementActiveOrders()
    {
        _activeOrders.Add(-1);
    }
}
// OrderService.cs - 带有完整可观测性的服务
using System.Diagnostics;

public class OrderService
{
    private static readonly ActivitySource ActivitySource = new("ObservabilityDemo.Orders");
    
    private readonly OrderMetrics _metrics;
    private readonly PaymentService _paymentService;
    private readonly ILogger<OrderService> _logger;

    public OrderService(
        OrderMetrics metrics,
        PaymentService paymentService,
        ILogger<OrderService> logger)
    {
        _metrics = metrics;
        _paymentService = paymentService;
        _logger = logger;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        // 创建追踪 Span
        using var activity = ActivitySource.StartActivity("CreateOrder", ActivityKind.Server);
        activity?.SetTag("order.id", order.Id);
        activity?.SetTag("order.type", order.Type);
        activity?.SetTag("order.amount", order.Amount);
        activity?.SetTag("order.user_id", order.UserId);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            _metrics.IncrementActiveOrders();

            // 结构化日志
            _logger.LogInformation(
                "开始创建订单: OrderId={OrderId}, Type={OrderType}, Amount={Amount}, UserId={UserId}",
                order.Id, order.Type, order.Amount, order.UserId);

            // 步骤 1:验证订单
            using (var validateActivity = ActivitySource.StartActivity("ValidateOrder"))
            {
                await ValidateOrderAsync(order);
                validateActivity?.SetTag("validation.result", "success");
            }

            // 步骤 2:处理支付
            using (var paymentActivity = ActivitySource.StartActivity("ProcessPayment"))
            {
                var paymentResult = await _paymentService.ProcessPaymentAsync(order.Id, order.Amount);
                
                paymentActivity?.SetTag("payment.transaction_id", paymentResult.TransactionId);
                paymentActivity?.SetTag("payment.status", paymentResult.Status);

                if (paymentResult.Status != "success")
                {
                    throw new PaymentException($"支付失败: {paymentResult.Status}");
                }
            }

            // 步骤 3:保存订单
            using (var saveActivity = ActivitySource.StartActivity("SaveOrder"))
            {
                await SaveOrderAsync(order);
            }

            stopwatch.Stop();

            // 记录指标
            _metrics.RecordOrderCreated(order.Type, order.Amount);
            _metrics.RecordProcessingDuration(stopwatch.Elapsed.TotalMilliseconds, order.Type, true);

            activity?.SetTag("order.status", "success");
            activity?.SetStatus(ActivityStatusCode.Ok);

            _logger.LogInformation(
                "订单创建成功: OrderId={OrderId}, 耗时={ElapsedMs}ms",
                order.Id, stopwatch.ElapsedMilliseconds);

            return order;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            // 记录失败指标
            _metrics.RecordOrderFailed(order.Type, ex.GetType().Name);
            _metrics.RecordProcessingDuration(stopwatch.Elapsed.TotalMilliseconds, order.Type, false);

            // 记录异常到追踪
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);

            // 结构化日志记录异常
            _logger.LogError(ex,
                "订单创建失败: OrderId={OrderId}, Type={OrderType}, UserId={UserId}, 耗时={ElapsedMs}ms",
                order.Id, order.Type, order.UserId, stopwatch.ElapsedMilliseconds);

            throw;
        }
        finally
        {
            _metrics.DecrementActiveOrders();
        }
    }

    private Task ValidateOrderAsync(Order order)
    {
        if (order.Amount <= 0)
        {
            throw new ValidationException("订单金额必须大于0");
        }

        return Task.CompletedTask;
    }

    private Task SaveOrderAsync(Order order)
    {
        // 实际实现:保存到数据库
        return Task.Delay(50); // 模拟数据库操作
    }
}
// PaymentService.cs - 带有分布式追踪的支付服务
using System.Diagnostics;

public class PaymentService
{
    private static readonly ActivitySource ActivitySource = new("ObservabilityDemo.Payment");
    
    private readonly HttpClient _httpClient;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(IHttpClientFactory httpClientFactory, ILogger<PaymentService> logger)
    {
        _httpClient = httpClientFactory.CreateClient();
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(string orderId, decimal amount)
    {
        using var activity = ActivitySource.StartActivity("ProcessPayment", ActivityKind.Client);
        activity?.SetTag("order.id", orderId);
        activity?.SetTag("payment.amount", amount);
        activity?.SetTag("payment.gateway", "stripe");

        _logger.LogInformation("开始处理支付: OrderId={OrderId}, Amount={Amount}", orderId, amount);

        try
        {
            // 调用外部支付网关
            var response = await _httpClient.PostAsJsonAsync("https://payment-gateway.example.com/charge", new
            {
                orderId,
                amount,
                currency = "USD"
            });

            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadFromJsonAsync<PaymentResult>();

            activity?.SetTag("payment.transaction_id", result!.TransactionId);
            activity?.SetStatus(ActivityStatusCode.Ok);

            _logger.LogInformation(
                "支付处理成功: OrderId={OrderId}, TransactionId={TransactionId}",
                orderId, result.TransactionId);

            return result;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);

            _logger.LogError(ex, "支付处理失败: OrderId={OrderId}", orderId);

            throw;
        }
    }
}

public class PaymentResult
{
    public string TransactionId { get; set; } = Guid.NewGuid().ToString();
    public string Status { get; set; } = "success";
}

public class PaymentException : Exception
{
    public PaymentException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public ValidationException(string message) : base(message) { }
}
// OrdersController.cs - API 端点
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;
    private readonly ILogger<OrdersController> _logger;

    public OrdersController(OrderService orderService, ILogger<OrdersController> logger)
    {
        _orderService = orderService;
        _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] Order order)
    {
        try
        {
            var createdOrder = await _orderService.CreateOrderAsync(order);
            return CreatedAtAction(nameof(GetOrder), new { id = createdOrder.Id }, createdOrder);
        }
        catch (ValidationException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
        catch (PaymentException ex)
        {
            return StatusCode(402, new { error = ex.Message });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "创建订单时发生未处理的异常");
            return StatusCode(500, new { error = "Internal server error" });
        }
    }

    [HttpGet("{id}")]
    public IActionResult GetOrder(string id)
    {
        // 实际实现:从数据库获取
        return Ok(new Order { Id = id });
    }
}

public class Order
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Type { get; set; } = "standard";
    public decimal Amount { get; set; }
    public string UserId { get; set; } = null!;
}
# docker-compose.yml - 本地可观测性栈
version: '3.8'

services:
  # Jaeger(分布式追踪)
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"  # Jaeger UI
      - "4317:4317"    # OTLP gRPC
      - "4318:4318"    # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  # Prometheus(指标)
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'

  # Grafana(可视化)
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus

  # 应用服务
  app:
    build: .
    ports:
      - "5000:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - OpenTelemetry__Endpoint=http://jaeger:4317
    depends_on:
      - jaeger
      - prometheus
# prometheus.yml - Prometheus 配置
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'observability-demo'
    static_configs:
      - targets: ['app:8080']
    metrics_path: '/metrics'

测试可观测性

# 启动可观测性栈
docker-compose up -d

# 运行应用
dotnet run

# 创建测试订单
curl -X POST http://localhost:5000/api/orders \
  -H "Content-Type: application/json" \
  -d '{
    "type": "standard",
    "amount": 99.99,
    "userId": "user-123"
  }'

# 访问可观测性端点
# Jaeger UI: http://localhost:16686
# Prometheus: http://localhost:9090
# Grafana: http://localhost:3000
# 应用指标: http://localhost:5000/metrics
# 健康检查: http://localhost:5000/health

更多推荐