尧图精选

C#微服务Azure实战:从API治理到可观测性的完整落地指南

🕒 发布时间:2026/9/14 23:32:04 📁 来源:尧图网络
1. 前情回顾与本章定位从“能跑”到“能扛”这个系列写到第三篇前两篇我们聊完了整体架构选型、环境搭建以及第一个C#微服务是怎么在Azure上跑起来的。收到不少读者的反馈说照着前两篇能把服务部署上去但一遇到真实流量就露馅跨服务调用超时、日志乱成一锅粥、消息重复消费、配置改完不生效……这些问题单体时代基本不用操心拆成微服务后全冒出来了。这一篇我打算集中解决“微服务落地过程中最扎手的几个问题”不是再铺一层新概念而是把前两篇留下的坑填上。你会看到API网关与版本治理怎么做、异步消息队列到底该在什么时候引入、数据一致性怎么用最朴素的方式保证、线上问题怎么通过可观测性快速定位。整个过程依然围绕C#和Azure这套技术栈展开适合已经有一个基础微服务骨架、正准备往生产环境推的团队参考也适合那些在单体转型微服务过程中被各种“分布式副作用”折磨的开发者。顺便说一句最近热搜词里有很多C#上位机、C#串口通信相关的内容看起来不少搞工控、设备采集的朋友也来关注微服务了。其实思路是通的上位机那边解决的是“怎么把设备数据搬出来”微服务这边解决的是“搬出来之后怎么稳定地处理、存储、对外提供”。你手里那套模块化、解耦、日志埋点的经验换到服务端完全能用只是把串口换成了HTTP和消息队列而已。2. 服务间通信与API治理别让Link调用成为事故现场2.1 为什么不能直接HttpClient满天飞很多从单体转过来的团队第一个微服务往往是从“把Controller拆成独立项目”开始的。服务A要调服务B很自然地就在代码里写了using var httpClient new HttpClient(); var response await httpClient.GetAsync(http://service-b/api/orders/1);这段代码在演示环境跑得好好的上了生产就开始出问题Socket耗尽、DNS解析延迟、下游一抖动整条链路跟着超时……而且你根本不知道service-b这个地址在Kubernetes或Azure Container Apps里到底该填什么。先说结论在Azure上跑C#微服务跨服务调用有三件事必须做——服务发现、弹性策略、统一入口。服务发现解决“service-b到底在哪”弹性策略解决“下游挂了或慢了怎么办”统一入口解决“认证、限流、路由这些横切逻辑放哪”。具体落地可以分两条路走。如果你用的是Azure Kubernetes ServiceK8s自带的Service DNS天然就是服务发现你在代码里写http://service-b没问题但HttpClient必须交给依赖注入管理不能每次new。如果你用的是Azure Container Apps这类PaaS同样有内置的服务发现机制也是用服务名当主机名。关键代码不是写HttpClient而是注册Typed Clientbuilder.Services.AddHttpClientOrderApiClient(client { client.BaseAddress new Uri(http://order-service); }) .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(GetCircuitBreakerPolicy());这里用了Polly做弹性策略这是.NET生态里最成熟的库后来合进了Microsoft.Extensions.Http.Resilience。重试策略要小心只对幂等请求重试。拿订单服务举例查询接口重试没问题但“创建订单”“扣款”这种非幂等接口一旦客户端超时重试就可能产生两笔订单。所以非幂等接口一定要在业务层做幂等校验或者给请求加Idempotency-Key服务端按Key去重。熔断比重试更重要。下游服务如果已经处在过载状态你再怎么重试都只会添乱。Polly的熔断策略会在一段时间内直接快速失败给下游喘息的机会。var circuitBreaker PolicyHttpResponseMessage .HandleHttpRequestException() .OrResult(response (int)response.StatusCode 500) .CircuitBreakerAsync( handledEventsAllowedBeforeBreaking: 5, durationOfBreak: TimeSpan.FromSeconds(30));这个策略的意思是连续5次遇到异常或5xx响应就打开熔断开关30秒内所有请求快速失败30秒后放一个试探请求成功就恢复失败就继续熔断。这个数字不是拍脑袋定的要结合你的下游服务恢复时间。太短了下游还没缓过来又被打爆太长了对用户体验不友好。2.2 API版本管理与Swagger聚合接口改了别让调用方爆炸微服务跑起来以后最头疼的问题之一就是接口升级。今天给Order接口加个字段明天把某个接口的参数从int改成string下游团队跑来问你“你们接口怎么又变了”在微服务架构里API版本管理不是可选项是必选项。ASP.NET Core这边用Asp.Versioning.Http这个库注意老包叫Microsoft.AspNetCore.Mvc.Versioning新版改名了配置很简单builder.Services.AddApiVersioning(options { options.DefaultApiVersion new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified true; options.ReportApiVersions true; options.ApiVersionReader ApiVersionReader.Combine( new UrlSegmentApiVersionReader(), new HeaderApiVersionReader(X-Version)); }).AddApiExplorer();Controller里这样写[ApiController] [Route(api/v{version:apiVersion}/orders)] public class OrdersController : ControllerBase { [HttpGet({id})] public async TaskActionResultOrderDto GetOrder(string id, ApiVersion version) { // 实现 } }版本策略上我踩过不少坑总结下来最实用的原则是小改动升Minor版本破坏性改动升Major版本旧版本留足废弃周期再删。不要懒到所有改动都扔到v2也不要每次都新建Controller副本。对新增字段这类兼容性改动直接在原接口上加就好响应模型里多一个字段对JSON反序列化是透明的。Swagger这一块每个微服务自带一份Swagger文档是基本操作但你不可能让调用方每调一个服务就去翻一个Swagger页面。聚合文档有几个方案如果用了Azure API Management它自带OpenAPI导入能让所有下游接口在一个门户里被检索如果不想引入额外组件简单做法是在网关层做一个代理把各服务的Swagger JSON合并起来。这里提醒一句生产环境千万不要把Swagger端点直接暴露出去。我在客户现场见过不止一次Swagger UI裸奔在公网上接口结构、参数、内部字段全被看光。要么在网关层做身份认证要么只在内部网络开放Azure Container Apps可以直接用内置的ingress限制外部访问。3. 异步消息与后台任务同步调用保不住所有场景3.1 什么时候该上消息队列用尺子量别用感觉微服务面试题里高频出现“同步调用和异步消息怎么选”实际项目里很多人选错了。有个很简单的判断标准用户是否必须立刻拿到结果。用户下单后真正需要同步等待的是“订单创建成功”这个结果至于“发短信通知”“积分累加”“库存预占失败后的补偿”用户其实不关心具体什么时候完成。这类操作如果全部串在请求链路里不仅拖慢响应时间而且任何一个下游服务抖动都会导致下单失败。在Azure上做异步消息首选是Service Bus。它支持队列和主题两种模式队列就是点对点主题可以广播给多个订阅者。订单创建后订单服务往Topic里发一条消息通知服务、积分服务、报表服务各自订阅谁处理失败都不影响别人。发消息的代码很简单public class OrderMessageSender { private readonly ServiceBusSender _sender; public OrderMessageSender(ServiceBusClient client) { _sender client.CreateSender(order-events); } public async Task SendOrderCreatedAsync(OrderCreatedMessage message, CancellationToken cancellationToken) { var json JsonSerializer.Serialize(message); var busMessage new ServiceBusMessage(json) { MessageId message.EventId.ToString(), ContentType application/json }; await _sender.SendMessageAsync(busMessage, cancellationToken); } }这里有两件事必须注意。一是MessageId一定要设置成业务事件的唯一标识比如EventId这样Service Bus自带的消息去重才能生效二是消息体里要包含业务数据和事件元数据不能只放一个“订单ID”就让下游去查查不到的时候下游一脸懵。接收端在Azure Function里写最简单Service Bus触发器自动帮你处理确认和死信逻辑。如果是自己宿主在后台服务里用ServiceBusProcessorawait using var processor client.CreateProcessor(order-events, notifications, new ServiceBusProcessorOptions { AutoCompleteMessages false, MaxConcurrentCalls 10 }); processor.ProcessMessageAsync async args { try { var message args.Message; var orderEvent JsonSerializer.DeserializeOrderCreatedMessage(message.Body.ToString()); await _notificationService.SendAsync(orderEvent); await args.CompleteMessageAsync(message); } catch (Exception ex) { _logger.LogError(ex, 处理订单事件失败); await args.DeadLetterMessageAsync(args.Message, processing-error, ex.Message); } }; processor.ProcessErrorAsync args { _logger.LogError(args.Exception, Service Bus处理器异常); return Task.CompletedTask; }; await processor.StartProcessingAsync();AutoCompleteMessages设成false处理成功才调用CompleteMessage失败了就进死信队列。这里有个经验不要所有异常都重试业务逻辑错误比如数据格式不对重试一百次也是错直接丢死信队列让监控去报警只有网络抖动这类瞬时错误才有重试价值。Service Bus本身支持最大投递次数到了上限会自动进死信队列所以不需要自己写重试循环。3.2 Worker后台任务别再用while(true)Thread.Sleep了热搜词里“C# worker用法”热度很高看来很多人写后台任务还是老一套——一个while(true)循环里塞个Sleep。这样做的问题有两个一是没有优雅停机能力服务发布时任务可能被硬杀二是依赖Sleep的精度没法保证而且Sleep会占着线程不放在高并发场景下容易造成线程池饥饿。正确姿势是BackgroundService搭配PeriodicTimer。举个例子我们要做一个定时扫描超时未支付订单的功能public class OrderTimeoutWorker : BackgroundService { private readonly ILoggerOrderTimeoutWorker _logger; private readonly IServiceScopeFactory _scopeFactory; public OrderTimeoutWorker(ILoggerOrderTimeoutWorker logger, IServiceScopeFactory scopeFactory) { _logger logger; _scopeFactory scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer new PeriodicTimer(TimeSpan.FromMinutes(5)); while (await timer.WaitForNextTickAsync(stoppingToken)) { try { using var scope _scopeFactory.CreateScope(); var dbContext scope.ServiceProvider.GetRequiredServiceOrderDbContext(); var timeoutOrders await dbContext.Orders .Where(o o.Status OrderStatus.Pending o.CreatedAt DateTime.UtcNow.AddMinutes(-15)) .ToListAsync(stoppingToken); // 处理超时订单 } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation(订单超时任务正在停止); break; } catch (Exception ex) { _logger.LogError(ex, 扫描超时订单异常); } } } }两个细节说一下。第一不要用Singleton直接注入DbContext因为DbContext默认是Scoped生命周期从单例的任务里拿Scoped服务会抛异常用IServiceScopeFactory手动创建Scope是对的。第二PeriodicTimer和老的Timer比起来最大的好处是支持CancellationToken服务停止时能干净退出不会出现“明明发布了新版本旧的还在跑”的情况。3.3 分布式事务能不用就不用要用就用最朴素的说到消息队列就绕不开“本地事务和发消息不一致”的问题。经典场景订单表写入成功但消息没发出去下游没收到通知。不少团队的第一反应是引入分布式事务框架在Azure上搞一套Saga编排引擎。我的建议是除非业务量真的到了必须上工作流引擎的程度否则用Outbox模式就够了。Outbox模式说白了就是“把发消息这件事也当成一条数据和业务数据放在同一个数据库事务里”。订单创建时在同一事务里往Order表和Outbox表各插一条记录。然后有一个后台任务扫描Outbox表把状态为Pending的记录读出来发到Service Bus发成功了把状态改成Sent。这是一个把原子性从“跨网络”降维成“本地数据库事务”的思路。好处是不需要额外引入一套分布式事务中间件实现简单几百行代码搞定可靠性由数据库保证。代价是多了Outbox表要清理消息会有一定延迟取决于轮询频率消费端必须做幂等。我用过最简单的实现就是前面说的Worker里加一个扫表逻辑var pendingEvents await context.OutboxEvents .Where(e e.Status OutboxStatus.Pending) .OrderBy(e e.CreatedAt) .Take(200) .ToListAsync(ct); foreach (var eventItem in pendingEvents) { try { await _sender.SendAsync(eventItem.Type, eventItem.Payload, ct); eventItem.Status OutboxStatus.Sent; eventItem.SentAt DateTime.UtcNow; } catch (Exception ex) { _logger.LogError(ex, 发送Outbox消息失败EventId: {EventId}, eventItem.Id); // 这里不要标记为Sent下轮重试但要控制重试次数 eventItem.RetryCount; if (eventItem.RetryCount 10) { eventItem.Status OutboxStatus.Failed; } } } await context.SaveChangesAsync(ct);注意不要在一个事务里处理太多条一次200条的量比较合适。另外一定要给Outbox表加RetryCount字段重试超过阈值直接标记Failed人工排查不然一条坏消息卡在队首后面所有消息都跟着堵车。4. 数据一致性与性能优化数据库是最容易翻车的环节4.1 跨服务查询别让前端为了一个页面调八个接口微服务拆分后最先遇到的问题是单体时代一个JOIN查出来的数据现在分散在好几个服务里。订单列表页要显示订单基本信息、商品信息、物流信息前端得串行调三个接口。这个“接口聚合”的问题如果不解决性能会指数级下降。解决方案按复杂度有三种。第一种是API网关层聚合用Azure API Management的策略或者自定义BFFBackend for Frontend服务专门做编排前端只调一个接口。第二种是数据冗余订单服务里冗余一份商品名称、商品图片的镜像下单时从商品服务同步过来查询时直接查本地库。第三种是CQRS读操作走单独的读模型专门为查询优化。我的建议是中小型团队优先用数据冗余把跨服务联查变成单服务查询。虽然牺牲了一定的数据实时性但换来的性能收益是实打实的。比如下单时把商品快照存到订单表里后续查询订单详情根本不用再调商品服务。商品改名了历史订单还是显示下单时的名字这在业务上反而更合理。4.2 EF Core性能陷阱贪婪加载引发的性能灾难C#微服务里EF Core是主力ORM但用不好就是性能杀手。最常见的问题是导航属性贪婪加载。比如查订单时默认加载商品、物流、优惠券var orders await context.Orders .Include(o o.Items) .Include(o o.Delivery) .ToListAsync();看起来很正常但生成的SQL是三个LEFT JOIN如果订单有50条每条的Items有10条返回的数据量就是50×10×N的笛卡尔积。数据量大了之后这个查询能直接把数据库拖垮。解法有三个手段。第一用AsSplitQuery()把JOIN拆分成多条独立SQL每次查询返回单独的数据集避免笛卡尔爆炸var orders await context.Orders .Include(o o.Items) .AsSplitQuery() .ToListAsync();第二查询列表时用AsNoTracking()只读场景下EF Core不需要做变更跟踪内存开销和查询时间都会明显下降var orders await context.Orders .AsNoTracking() .Where(o o.CustomerId customerId) .OrderByDescending(o o.CreatedAt) .Skip(pageIndex * pageSize) .Take(pageSize) .ToListAsync();第三用ProjectTo投影只查需要的字段var orderDtos await context.Orders .Where(o o.CustomerId customerId) .Select(o new OrderListItemDto { Id o.Id, TotalAmount o.TotalAmount, Status o.Status, ItemCount o.Items.Count }) .ToListAsync();这一条最推荐响应体变小、SQL不用SELECT *、没有导航属性加载问题性能提升最明显。曾经有个客户线上接口P99延迟在800ms左右把Include改成ProjectTo后降到了120ms就是因为返回的数据量小了近10倍。4.3 Redis缓存与分布式锁缓存穿透和击穿怎么防Azure Cache for Redis是微服务架构里标配的组件但很多人应用缓存的方式非常原始查数据库前先查缓存有就返回没有就查库然后写缓存。听起来没问题但高并发下两个经典问题立刻出现。第一是缓存穿透。恶意请求或查询不存在的数据每次都会穿透缓存打到数据库。防的办法是缓存空值比如查一个不存在的订单ID也往缓存里写一个空对象设置较短的过期时间。第二是缓存击穿。某个热点Key过期的一瞬间大量请求同时打到数据库。防的办法是加锁让只有一个请求去查数据库并重建缓存其他请求等锁。在Redis上实现分布式锁StackExchange.Redis也提供了现成封装var cache connection.GetDatabase(); var lockToken Guid.NewGuid().ToString(); var lockAcquired await cache.LockTakeAsync($order:lock:{orderId}, lockToken, TimeSpan.FromSeconds(10)); if (lockAcquired) { try { var order await QueryFromDatabase(orderId); if (order ! null) { await cache.StringSetAsync($order:{orderId}, JsonSerializer.Serialize(order), TimeSpan.FromMinutes(30)); } return order; } finally { await cache.LockReleaseAsync($order:lock:{orderId}, lockToken); } } else { // 没抢到锁稍微等一下再从缓存取 await Task.Delay(100); var cached await cache.StringGetAsync($order:{orderId}); if (cached.HasValue) { return JsonSerializer.DeserializeOrder(cached); } return await QueryFromDatabase(orderId); }锁超时时间一定要比业务执行时间长不然业务还在跑锁就释放了另一个线程拿到锁发现缓存还没重建又查了一次库。保守起见锁的超时时间建议是预估业务耗时的3倍。至于缓存雪崩处理思路是给不同的Key设置不同的过期时间避免大量Key在同一时刻集体失效。可以在基础过期时间上加一个随机数比如30分钟到60分钟之间的随机值让过期时间分散开。5. 可观测性与配置管理出了问题能找到根因才是真“能扛”5.1 结构化日志字符串拼接是排查问题的最大敌人微服务出问题时最气人的不是报错本身而是日志里找不到有效信息。很多团队的日志还是这种写法_logger.LogInformation($订单 {orderId} 创建成功金额 {amount});这样写在排查分布式问题时有几个致命缺陷第一如果orderId或amount是null整个日志直接抛异常把真正的日志信息吞掉了第二无法按字段检索你想查“某个客户的所有订单日志”只能全文搜字符串在海量日志里等于大海捞针第三日志平台无法把orderId提取成维度字段做不了聚合分析。正确写法是结构化日志。Serilog在Azure上配Application Insights的做法Log.Logger new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.ApplicationInsights( instrumentationKey: connectionString, TelemetryConverter.Traces) .CreateLogger();业务代码里用占位符方式记录_logger.LogInformation(订单创建成功 OrderId{OrderId} CustomerId{CustomerId} Amount{Amount}, orderId, customerId, amount);Application Insights能自动识别花括号里的参数名把OrderId、CustomerId变成独立的维度字段。在Azure Portal里可以直接写KQL查询traces | where message contains OrderId | where customDimensions.OrderId ORD-2024-0001十几条日志一下就能揪出来不用像以前一样一行一行翻。5.2 链路追踪一个请求到底经历了哪些服务微服务环境里一个用户请求可能经过网关、订单服务、支付服务、消息队列、Worker任何一环慢了你都得知道是哪一环。链路追踪的核心是给每个请求分配一个TraceId在服务间传递把所有环节的日志串起来。ASP.NET Core这边System.Diagnostics.Activity天生支持Application Insights的SDK会自动做Correlation。但有个很容易被忽视的坑如果你在代码里手动发起HttpClient调用但没有传播TraceId header链路就断了。Azure的Application Insights SDK会自己处理大部分场景但如果你用了自定义的HttpClientHandler要确保没有把默认的TelemetryHandler覆盖掉builder.Services .AddHttpClientOrderApiClient(client client.BaseAddress new Uri(http://order-service)) .AddHttpMessageHandlerHttpClientTelemetryHandler();如果用的是Azure Functions在host.json里开启Application Insights的追踪函数之间传递消息时会自动带上Correlation上下文。Azure Service Bus的SDK也支持在消息头里传播Diagnostic-Id等于说从HTTP到消息队列的链路都能串起来不用手动写代码传播。5.3 配置管理appsettings.json里别放连接字符串微服务上了Azure之后配置管理最容易犯的错是把连接字符串、密钥直接写进appsettings.json甚至写进Docker镜像环境变量。密码一旦被别人获取等于把整个数据库裸奔在公网上。Azure上正确的姿势是Azure Key Vault Azure App Configuration。Key Vault管机密连接字符串、密码、API KeyApp Configuration管普通配置开关、阈值、连接地址。最方便的是在Program.cs里用托管标识拉取var builder WebApplication.CreateBuilder(args); if (builder.Environment.IsProduction()) { var keyVaultUrl new Uri(builder.Configuration[KeyVault:Uri]!); builder.Configuration.AddAzureKeyVault(keyVaultUrl, new DefaultAzureCredential()); }DefaultAzureCredential会自动尝试多种身份认证方式本地开发时用Azure CLI登录的账号部署到Azure时用App Service或Container Apps的托管标识不需要在代码里硬编码任何密钥。Key Vault里的机密可以通过“Key Vault引用”直接注入到App Service的应用设置里代码里不需要感知配置源的切换。这里说说IOptions的问题。很多人在微服务里用了IOptions的模式读取配置但没用对。IOptions是单例的应用启动时读取配置后就缓存了不会感知配置变更。正确做法是IOptionsMonitorpublic class FeatureFlagService { private readonly IOptionsMonitorFeatureFlagOptions _options; public FeatureFlagService(IOptionsMonitorFeatureFlagOptions options) { _options options; } public bool IsEnabled(string flagName) { return _options.CurrentValue.Flags.TryGetValue(flagName, out var enabled) enabled; } }IOptionsMonitor订阅了配置变更事件Azure App Configuration的值一变应用内能实时感知配合Azure App Configuration的Feature Manager还能做功能开关不用重新发布就能控制某个功能在特定环境是否启用。6. 典型问题排查实录这些坑我替你踩过了6.1 HttpClient使用不当导致Socket耗尽现象是服务跑一段时间后所有HTTP请求都开始超时重启之后恢复过几分钟又不行。用netstat一看大量TIME_WAIT连接堆积端口资源耗尽。根因就是代码里到处new HttpClient()。HttpClient底层封装了Socket每次new都会创建一个新的连接池频繁创建导致大量Socket来不及释放。解决办法前面提过用IHttpClientFactory管理生命周期或者用SocketsHttpHandler配置连接池复用builder.Services.AddHttpClient(api) .ConfigurePrimaryHttpMessageHandler(() new SocketsHttpHandler { PooledConnectionLifetime TimeSpan.FromMinutes(5), MaxConnectionsPerServer 50 });PooledConnectionLifetime设置为5分钟是为了让DNS和连接定期刷新避免下游服务更换IP后连接还在用旧地址。6.2 async/await使用不当导致线程池饥饿现象是接口偶发性超时CPU占用并不高但线程池队列越来越长。排查一看代码发现有人在异步方法里写了Task.Result或者.Wait()。这是ASP.NET Core里的头号死锁成因同步阻塞占用了线程池线程异步请求又在等待线程释放互相等待。解决方式只有一条不要在异步代码路径上同步阻塞从入口到出口全链路async/await// 错误的写法 public async TaskOrder GetOrderAsync(string orderId) { var order _repository.GetOrderAsync(orderId).Result; // 阻塞 return order; } // 正确的写法 public async TaskOrder GetOrderAsync(string orderId) { return await _repository.GetOrderAsync(orderId); }6.3 消息消费端重复执行业务逻辑Service Bus的AtLeastOnce语义决定了消息可能被重复投递。场景是这样消费端处理完业务后还没来得及调用CompleteMessage服务宕机了。重启后消息被重新投递业务逻辑又执行了一遍产生重复数据。解决办法是消费端幂等。最简单的做法是利用数据库唯一索引// 建表时把EventId设为唯一索引 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityProcessedEvent() .HasIndex(e e.EventId) .IsUnique(); }处理消息时先尝试插入ProcessedEvent插入失败说明已经处理过直接忽略。这个方法简洁有效代价是多一次数据库写入但对大多数业务场景完全够用。6.4 配置刷新无效还是旧值有读者问“我用了Azure App Configuration改完配置代码里读取到的还是旧值。”这个问题的原因大概率是用了IOptions而不是IOptionsMonitor或者IOptionsMonitor的配置源没有配置UseAzureAppConfiguration的刷新监听。正确的配置在Program.cs里应该是builder.Configuration.AddAzureAppConfiguration(options { options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential()) .Select(Demo:*) .ConfigureRefresh(refreshOptions { refreshOptions.Register(Demo:Settings:Sentinel, refreshAll: true) .SetCacheExpiration(TimeSpan.FromSeconds(30)); }); });这里有个技巧注册一个哨兵KeySentinel每次刷新只检查这个Key是否变化变了才拉取全部配置。如果不加哨兵Key每个Key都会被单独轮询性能和成本都不划算。7. 一点经验沉淀这篇文章写到这里我个人最大的体会是微服务架构的问题从来不是“搭不起来”而是“跑不稳”。C#和Azure这套技术栈给了你很多现成的组件——Service Bus、Redis、Application Insights、Key Vault——但真正决定系统稳不稳的是你在每个环节有没有踩对节奏。API版本管理从第一天做起跨服务调用想清楚幂等和重试消息队列只在异步场景使用配置和密钥一律交给云端托管。这些经验不是看书看来的是一次次线上事故换来的。你按这个思路去搭前期的确会多花一点时间但后面省下的排查时间是十倍百倍。下一篇我打算专门聊聊微服务的规模化部署包括多环境管理、蓝绿发布和金丝雀发布那部分是真正考验运维功力的地方。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →