一、为什么要把DotNetCore应用搬到Serverless上?

Serverless架构就像云计算中的"外卖服务"——你只管点菜(写业务代码),不用操心厨房(服务器管理)。对于DotNetCore应用来说,这种模式特别适合以下场景:

  1. 流量波动大的应用:比如电商秒杀活动,平时可能只需要1台服务器,活动时突然需要100台
  2. 事件驱动型任务:比如用户上传图片后触发的缩略图生成
  3. 后台批处理作业:每天凌晨运行的报表统计任务

我们来看一个典型的电商价格计算函数示例:

// 技术栈:AWS Lambda + DotNetCore 3.1
// 计算商品折扣后的价格
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request)
{
    // 1. 从请求中解析商品ID和数量
    var productId = request.QueryStringParameters["productId"];
    var quantity = int.Parse(request.QueryStringParameters["quantity"]);
    
    // 2. 从数据库获取商品基础信息(模拟)
    var basePrice = await GetProductPrice(productId);
    
    // 3. 应用促销规则计算最终价格
    var finalPrice = ApplyDiscountRules(basePrice, quantity);
    
    // 4. 返回JSON格式结果
    return new APIGatewayProxyResponse {
        StatusCode = 200,
        Body = JsonSerializer.Serialize(new {
            ProductId = productId,
            FinalPrice = finalPrice
        })
    };
}

// 模拟数据库查询
private async Task<decimal> GetProductPrice(string productId) {
    await Task.Delay(50); // 模拟网络延迟
    return productId switch {
        "1001" => 299m,
        "1002" => 599m,
        _ => 999m
    };
}

// 应用折扣规则
private decimal ApplyDiscountRules(decimal basePrice, int quantity) {
    // 满3件打8折
    if(quantity >= 3) return basePrice * 0.8m * quantity;
    // 满2件打9折
    if(quantity >= 2) return basePrice * 0.9m * quantity;
    return basePrice * quantity;
}

这个例子展示了Serverless函数的典型特征:短小精悍、单一职责、无状态。每次调用都是独立的,不需要考虑服务器维护问题。

二、DotNetCore应用需要做哪些改造?

2.1 冷启动优化

Serverless函数的冷启动就像汽车发动——第一次启动时需要预热。对于DotNetCore应用,我们可以这样做:

// 技术栈:Azure Functions + DotNetCore 6.0
// 使用静态变量和Lazy初始化来优化冷启动
public static class DiscountService
{
    // 静态变量在多次调用间保持状态
    private static readonly ConcurrentDictionary<string, Product> _productCache = new();
    
    // 延迟加载促销规则
    private static readonly Lazy<DiscountRuleEngine> _ruleEngine = new(() => {
        Console.WriteLine("首次加载规则引擎...");
        return new DiscountRuleEngine();
    });
    
    [FunctionName("CalculateDiscount")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
    {
        // 使用预热的规则引擎
        var rules = _ruleEngine.Value;
        // ...业务逻辑
    }
}

// 促销规则引擎(模拟)
public class DiscountRuleEngine 
{
    public DiscountRuleEngine() {
        // 这里可能是从数据库加载规则的耗时操作
        Thread.Sleep(1000); 
    }
    
    public decimal ApplyRules(decimal price) { ... }
}

2.2 依赖项瘦身

Serverless环境对部署包大小有限制(通常50MB左右),所以需要精简依赖:

# 使用DotNetCore CLI命令分析依赖树
dotnet list package --include-transitive

# 发布时使用以下命令减小体积
dotnet publish -c Release -r linux-x64 --self-contained false

三、实战中的常见坑与解决方案

3.1 数据库连接管理

Serverless函数是瞬态的,但数据库连接池不是。错误的连接管理会导致连接泄漏:

// 技术栈:阿里云函数计算 + DotNetCore 5.0 + MySQL
public class ProductRepository : IDisposable
{
    private MySqlConnection _connection;
    
    // 正确做法:使用依赖注入管理生命周期
    public ProductRepository(MySqlConnection connection) {
        _connection = connection;
    }
    
    public async Task<Product> GetById(string id) {
        // 使用参数化查询防止SQL注入
        var command = new MySqlCommand(
            "SELECT * FROM products WHERE id = @id", 
            _connection);
        command.Parameters.AddWithValue("@id", id);
        
        await using var reader = await command.ExecuteReaderAsync();
        // ...处理结果
    }
    
    public void Dispose() {
        _connection?.Dispose();
    }
}

// 在Startup中配置
public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder) {
        builder.Services.AddScoped(provider => {
            var conn = new MySqlConnection(Environment.GetEnvironmentVariable("DB_CONN"));
            conn.Open();
            return conn;
        });
        
        builder.Services.AddScoped<ProductRepository>();
    }
}

3.2 超时处理

Serverless函数通常有执行时间限制(如AWS Lambda最多15分钟),长时间任务需要特殊处理:

// 技术栈:Google Cloud Functions + DotNetCore 6.0
public class LongRunningTaskController
{
    [FunctionName("StartExport")]
    public async Task<IActionResult> StartExport(
        [HttpTrigger] HttpRequest req,
        [Queue("export-tasks")] IAsyncCollector<string> queue)
    {
        // 将大任务拆分为小任务放入队列
        var taskId = Guid.NewGuid().ToString();
        await queue.AddAsync(taskId);
        
        return new OkObjectResult(new { taskId });
    }
    
    [FunctionName("ProcessExport")]
    public async Task ProcessExport(
        [QueueTrigger("export-tasks")] string taskId,
        [Table("export-status")] IAsyncCollector<ExportStatus> statusTable)
    {
        // 记录任务状态
        await statusTable.AddAsync(new ExportStatus {
            PartitionKey = "exports",
            RowKey = taskId,
            Status = "Processing"
        });
        
        // 分批次处理(模拟)
        for(int i=0; i<100; i++) {
            await ProcessBatch(i);
            // 定期更新状态防止超时
            if(i % 10 == 0) {
                await statusTable.AddAsync(new ExportStatus {
                    PartitionKey = "exports",
                    RowKey = taskId,
                    Status = $"Processing {i}%"
                });
            }
        }
    }
    
    private async Task ProcessBatch(int batchNumber) {
        await Task.Delay(1000); // 模拟处理耗时
    }
}

四、性能优化进阶技巧

4.1 使用Dapper替代EF Core

在Serverless环境中,轻量级的ORM通常性能更好:

// 技术栈:Tencent SCF + DotNetCore 5.0 + Dapper
public class OrderService
{
    private readonly SqlConnection _connection;
    
    public OrderService(SqlConnection connection) {
        _connection = connection;
    }
    
    public async Task<Order> GetOrderWithDetails(string orderId) {
        var sql = @"
            SELECT * FROM Orders WHERE Id = @orderId;
            SELECT * FROM OrderItems WHERE OrderId = @orderId;
        ";
        
        using var multi = await _connection.QueryMultipleAsync(sql, new { orderId });
        
        var order = await multi.ReadSingleAsync<Order>();
        order.Items = (await multi.ReadAsync<OrderItem>()).ToList();
        
        return order;
    }
}

4.2 合理使用缓存

// 技术栈:AWS Lambda + DotNetCore 6.0 + Redis
public class ProductCatalog
{
    private readonly IDistributedCache _cache;
    private readonly ProductRepository _repository;
    
    public ProductCatalog(IDistributedCache cache, ProductRepository repo) {
        _cache = cache;
        _repository = repo;
    }
    
    public async Task<Product> GetProduct(string id) {
        // 先从缓存读取
        var cached = await _cache.GetStringAsync($"product_{id}");
        if(cached != null) {
            return JsonSerializer.Deserialize<Product>(cached);
        }
        
        // 缓存未命中则查询数据库
        var product = await _repository.GetById(id);
        
        // 写入缓存(设置5分钟过期)
        await _cache.SetStringAsync($"product_{id}", 
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            });
            
        return product;
    }
}

五、监控与调试技巧

Serverless应用的调试与传统应用不同,我们需要依赖云平台提供的工具:

// 技术栈:Azure Functions + DotNetCore 6.0
public class ErrorHandlingDemo
{
    [FunctionName("ProcessPayment")]
    public async Task<IActionResult> Run(
        [HttpTrigger] HttpRequest req,
        [Table("payment-logs")] IAsyncCollector<PaymentLog> logger)
    {
        try {
            var payment = await ParseRequest(req);
            
            // 结构化日志
            using (Logger.BeginScope(new {
                PaymentId = payment.Id,
                UserId = payment.UserId
            })) {
                Logger.LogInformation("开始处理支付请求");
                
                var result = await ProcessPayment(payment);
                
                // 记录审计日志
                await logger.AddAsync(new PaymentLog {
                    PartitionKey = DateTime.UtcNow.ToString("yyyyMMdd"),
                    RowKey = Guid.NewGuid().ToString(),
                    PaymentId = payment.Id,
                    Status = "Completed"
                });
                
                return new OkObjectResult(result);
            }
        }
        catch (PaymentException ex) {
            Logger.LogError(ex, "支付处理失败");
            await logger.AddAsync(new PaymentLog {
                PartitionKey = DateTime.UtcNow.ToString("yyyyMMdd"),
                RowKey = Guid.NewGuid().ToString(),
                PaymentId = payment?.Id ?? "unknown",
                Status = "Failed",
                Error = ex.Message
            });
            return new BadRequestObjectResult(ex.Message);
        }
    }
}

六、总结与最佳实践

经过上面的探索,我们可以总结出以下Serverless适配经验:

  1. 函数设计原则

    • 单一职责:一个函数只做一件事
    • 无状态设计:不依赖本地存储
    • 短时运行:控制在3分钟以内最佳
  2. 性能优化要点

    • 预初始化共享资源
    • 控制依赖项数量
    • 合理使用缓存
  3. 运维建议

    • 实施完善的日志记录
    • 设置适当的超时和重试策略
    • 监控冷启动频率
  4. 成本控制技巧

    • 为不同环境设置不同的内存配置
    • 使用队列解耦耗时任务
    • 定期检查闲置函数

Serverless不是银弹,但对于适合的场景,它能大幅降低运维复杂度。DotNetCore凭借其出色的性能和跨平台能力,在Serverless架构中表现优异。希望这篇指南能帮助你顺利迁移应用!