# 从单体到微服务:C# Web API + HttpClientFactory重构实战
去年接手一个遗留的订单管理系统时,我面对的是一个典型的ASP.NET WebForms单体应用——所有业务逻辑挤在同一个项目里,数据库表关联复杂得像蜘蛛网,每次发布都要全站停机。最要命的是黑五促销期间,订单模块的崩溃直接导致公司损失了15%的潜在营收。这次惨痛教训让我们下定决心进行服务化改造,而C# Web API与HttpClientFactory的组合成为了技术栈的核心选择。
1. 重构规划与边界划分
在动手拆解这个运行了7年的庞然大物前,我们花了三周时间进行领域分析。通过事件风暴工作坊,团队在白板上贴出了287个用户故事便签,最终识别出订单处理、库存管理、支付网关和物流跟踪四个核心子域。
关键决策点: 我们决定优先解耦订单处理模块,因为它的业务规则最复杂且性能瓶颈最明显。但保留用户认证等横切关注点作为共享库,避免过早引入分布式会话的复杂性。
服务拆分后的架构对比:
| 维度 | 原单体架构 | 新微服务架构 |
|---|---|---|
| 部署单元 | 单个IIS应用池 | 4个独立Docker容器 |
| 数据库 | 共享SQL Server实例 | 每个服务独占数据库Schema |
| 事务边界 | 数据库事务 | Saga模式+补偿事务 |
| 团队协作 | 代码合并冲突频繁 | 独立代码仓库+契约测试 |
// 订单服务的领域模型核心 public class Order public string OrderNumber public List
Items public void AddItem(Product product, int quantity) else { Items.Add(new OrderItem(product, quantity)); } } }
> 经验提示:在拆分初期,建议先用命名空间隔离代码而非立即物理拆分,等团队适应了领域驱动设计思维后再进行服务化部署
2. HTTP通信基础设施搭建
选择HttpClientFactory而非裸HttpClient的决定,来自于我们早期原型阶段遇到的端口耗尽问题——在负载测试中,频繁创建HttpClient实例导致系统在3000QPS时就开始抛出SocketException。
2.1 弹性通信管道配置
通过NuGet引入Polly扩展包后,我们为订单服务与库存服务的交互设计了三级防护:
- 重试策略:针对网络抖动,设置指数退避重试
- 熔断器:当库存服务5分钟内错误率超过30%时熔断30秒
- 超时控制:任何跨服务调用必须在800ms内响应
// Startup.cs中的服务注册 services.AddHttpClient("InventoryService") .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))) .AddPolicyHandler(Policy.TimeoutAsync
(TimeSpan.FromMilliseconds(800))) .AddCircuitBreakerPolicy(5, TimeSpan.FromSeconds(30));
实际运行中遇到的坑:
- 序列化冲突:库存服务使用Newtonsoft.Json而订单服务改用System.Text.Json
- 头信息丢失:Authorization头在重试时未被自动携带
- 日志关联:跨服务调用需要注入相同的Request-Id
我们通过统一序列化配置和自定义DelegatingHandler解决了这些问题:
public class CorrelationIdHandler : DelegatingHandler { protected override async Task
SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { request.Headers.TryAddWithoutValidation("X-Correlation-ID", Guid.NewGuid().ToString()); var response = await base.SendAsync(request, cancellationToken); _logger.LogInformation($"调用{request.RequestUri}耗时" + $"{response.Headers.GetValues("X-Elapsed-Time").First()}ms"); return response; } }
3. 服务契约与版本控制
当支付团队突然要求在所有金额字段增加货币类型支持时,我们意识到API版本控制不是可选项而是必选项。经过评估,我们采用了URI路径版本与内容协商的混合方案:
/api/v1/orders/123 → 返回原始金额格式 /api/v2/orders/123 → 返回包含currency字段的新格式
对应的控制器实现:
[ApiVersion("1.0")] [Route("api/v{version:apiVersion}/orders")] public class OrdersV1Controller : ControllerBase { [HttpGet("{id}")] public ActionResult
Get(int id) { // 返回旧版DTO } } [ApiVersion("2.0")] [Route("api/v{version:apiVersion}/orders")] public class OrdersV2Controller : ControllerBase { [HttpGet("{id}")] public ActionResult
Get(int id) { // 返回新版DTO } }
为了平滑迁移,我们在API网关层实现了版本路由:
// Ocelot配置示例 { "DownstreamPathTemplate": "/api/v{version}/orders/{id}", "UpstreamPathTemplate": "/api/orders/{id}", "UpstreamHttpMethod": [ "Get" ], "DownstreamHttpMethod": "GET", "RouteClaimsRequirement": { "version": "1|2" } }
4. 监控与故障诊断
当系统变成分布式后,传统的性能计数器已经不够用了。我们搭建的监控体系包含三个层次:
应用指标监控
- 使用Prometheus收集各服务的HTTP请求指标
- Grafana仪表盘展示P99延迟、错误率等关键数据
- 为HttpClientFactory注入指标收集器
services.AddHttpClient("PaymentService") .AddHttpMessageHandler(() => new PrometheusHttpMessageHandler("order_service"));
分布式追踪
- 通过OpenTelemetry实现跨服务调用链追踪
- Jaeger可视化订单创建的全链路调用
- 在日志中注入SpanId实现日志与追踪关联
业务健康检查
- 每个服务暴露/health端点
- 检查数据库连接、下游服务可达性等
- Kubernetes基于此实现Pod自动恢复
// 健康检查实现示例 public class DatabaseHealthCheck : IHealthCheck { public async Task
CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { using var conn = new SqlConnection(_config.DbConnection); await conn.OpenAsync(cancellationToken); return HealthCheckResult.Healthy(); } catch (Exception ex) { return HealthCheckResult.Unhealthy(ex.Message); } } }
在实施监控后,我们发现了几个关键问题:
- 订单提交时同步调用库存预留是性能瓶颈
- 支付服务在MySQL连接数达到50时开始拒绝请求
- 物流查询API的响应时间波动达600ms
针对这些问题,我们最终将库存操作改为异步事件驱动,为支付服务增加了连接池,并为物流服务引入了本地缓存。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/269197.html