
最近折腾了好几个基于 Azure 的微服务项目,踩了不少坑,这篇把生产环境中真正管用的东西和容易翻车的地方都说清楚。
架构图看起来永远很美好,在真实流量冲击之前。
Azure 上的微服务架构承诺独立部署、团队自治、精细化伸缩、故障隔离。这些好处是真实的——但代价也是实实在在的,教程里很少有人会坦白告诉你:如果不谨慎对待,运维复杂度增长的速度会远超团队增长速度。
这不是一篇微服务入门教程。是我们多个生产系统跑在 Azure 上,总结出的真实经验——平台哪里做得好,哪里会坑你,哪些模式能让系统扛住凌晨三点的压力,哪些模式会让你在那时候焦头烂额。
聊具体模式之前,先说清楚为什么 Azure 是微服务工作负载的合理选择,以及你实际选择了什么。
Azure 的微服务生态主要围绕三个服务构建:
Azure Kubernetes Service(AKS) — 托管版 Kubernetes,负责控制平面升级、节点池管理,与 Azure 全家桶(AAD、ACR、Monitor)集成顺畅。如果跑容器化服务,AKS 是默认选项。
Azure Container Apps — 基于 Kubernetes 和 KEDA 的更高层抽象。控制权比 AKS 少,但运维负担也小得多。适合想要微服务好处但不想投入完整 Kubernetes 的团队。
Azure Service Bus — 服务间异步通信的骨干。比自建队列可靠多了,自带死信队列、消息会话、重复检测。
AKS 和 Container Apps 的选择是第一个需要认真考虑的决定。我们的经验:如果你有专职平台工程师或 SRE,AKS 给你最终会用到的灵活性;如果没有,Container Apps 能让你保持清醒。

最贵的微服务错误不是技术层面的——是划错了边界。
服务切得太细(纳米服务)会制造分布式单体问题:服务在运行时紧耦合,虽然各自独立部署。你会陷入同步调用链,一个慢服务拖垮整个系统。
服务切得太粗又丢掉了架构的优势。运维复杂度上去了,部署独立性却没得到。
一个实用的判断标准:服务应该拥有自己的数据,且能够独立部署,不需要跟其他服务协调。如果 deploy 服务 A 必须同时 deploy 服务 B,那边界划错了。
DDD(领域驱动设计)给了这套逻辑一套术语:边界上下文。每个服务应该对应一个边界上下文——拥有自己的数据模型、自己的语言、自己的规则。支付是个边界上下文,库存是个边界上下文,用户认证是个边界上下文。"API 需要的所有东西"不是。
正确的微服务架构里这条是铁律:每个服务拥有自己的数据库。跨服务边界不能共享数据库。
这看起来很浪费——一个库能搞定的事情为什么要跑多个实例?原因在于共享数据库会在数据层制造耦合,毁掉你想要的独立性。共享数据库里改个 schema,要协调所有读取这个数据的团队。你用 schema 耦合换来了部署耦合。
在 Azure 上,意味着每个服务有自己的 Azure SQL 数据库、Cosmos DB 容器或 PostgreSQL 灵活服务器。是的,成本更高。但这买卖值得。
跨服务查询(对数据库独立模式最常见的质疑),解决方案是物化视图和事件驱动同步——这就把我们带到了消息队列。
服务间同步 REST 调用很诱人,因为大家熟悉。但这同样是微服务系统级联故障的首要原因。
如果服务 A 同步调用服务 B,服务 B 慢或挂了,服务 A 也跟着慢或挂。把这个乘以 15 个服务和同步调用链,你就有了一个脆弱的分布式单体。
我们的原则:需要即时一致性的读操作用同步调用;所有状态变更用异步消息。
Azure Service Bus 是我们做异步消息的默认选择。下面是生产者端的基本模式:
import json
from azure.servicebus import ServiceBusClient, ServiceBusMessage
from azure.identity import DefaultAzureCredential
from dataclasses import dataclass, asdict
from datetime import datetime @dataclass
class OrderPlacedEvent: event_type: str = "order.placed" order_id: str = "" customer_id: str = "" total_amount: float = 0.0 items: list = None placed_at: str = "" def __post_init__(self): if self.items is None: self.items = [] if not self.placed_at: self.placed_at = datetime.utcnow().isoformat() class OrderEventPublisher: def __init__(self, namespace_url: str, topic_name: str): credential = DefaultAzureCredential() self.client = ServiceBusClient(namespace_url, credential) self.topic_name = topic_name def publish_order_placed(self, order: dict) -> str: event = OrderPlacedEvent( order_id=order["id"], customer_id=order["customer_id"], total_amount=order["total"], items=order["items"] ) message = ServiceBusMessage( body=json.dumps(asdict(event)), content_type="application/json", subject=event.event_type, message_id=f"order-placed-{event.order_id}", # Idempotency key ) with self.client.get_topic_sender(self.topic_name) as sender: sender.send_messages(message) return event.order_id
消费者端,带正确错误处理和死信处理:
import logging
from azure.servicebus import ServiceBusClient, ServiceBusReceivedMessage
from azure.identity import DefaultAzureCredential logger = logging.getLogger(__name__) class OrderEventConsumer: def __init__(self, namespace_url: str, topic_name: str, subscription_name: str): credential = DefaultAzureCredential() self.client = ServiceBusClient(namespace_url, credential) self.topic_name = topic_name self.subscription_name = subscription_name self.processed_message_ids = set() # In production: use Redis or DB def process_messages(self, max_messages: int = 10): receiver = self.client.get_subscription_receiver( topic_name=self.topic_name, subscription_name=self.subscription_name, max_wait_time=5 ) with receiver: messages = receiver.receive_messages(max_message_count=max_messages) for message in messages: try: self._handle_message(message, receiver) except Exception as e: logger.error(f"Failed to process message {message.message_id}: {e}") # Dead-letter after max delivery count (configured on Service Bus) receiver.dead_letter_message( message, reason="ProcessingFailed", error_description=str(e) ) def _handle_message(self, message: ServiceBusReceivedMessage, receiver): msg_id = message.message_id # Idempotency check — Service Bus guarantees at-least-once delivery if msg_id in self.processed_message_ids: logger.info(f"Duplicate message {msg_id}, skipping") receiver.complete_message(message) return import json event = json.loads(str(message)) if event["event_type"] == "order.placed": self._handle_order_placed(event) self.processed_message_ids.add(msg_id) receiver.complete_message(message) def _handle_order_placed(self, event: dict): logger.info(f"Processing order {event['order_id']} for customer {event['customer_id']}") # Actual business logic here
上面代码明确了两件教程经常跳过的事情:消息的幂等性 key(Service Bus 保证至少一次投递,所以消费者必须处理重复)和死信路由(处理失败的消息走死信队列,而不是无限重试阻塞队列)。
在 Azure 上,AKS 内部服务间通信靠 Kubernetes DNS。服务通过名字互相调用——http://inventory-service/api/v1/stock——Kubernetes 负责路由。
外部流量,Azure API Management(APIM)是推荐的网关层。它处理:
一个能省很多麻烦的模式:从第一天就给 API 加上版本。所有端点都在 /api/v1/ 下。需要破坏性变更时,加 /api/v2/,迁移期间两个版本并行跑。APIM 层很容易强制执行这个规范。
没有分布式追踪就没法运维微服务系统。一个请求经过 6 个服务才返回结果,靠各服务独立日志根本没法调试——等你把 6 个日志流关联完,值班工程师头发都白了几根。
Azure 原生的答案是开启分布式追踪的 Application Insights。每个服务发出带共享关联 ID 的遥测数据,Azure Monitor 用这个 ID 重建请求跨越服务边界的完整链路。
实操配置:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter def configure_tracing(connection_string: str, service_name: str): """Configure OpenTelemetry with Azure Monitor export.""" exporter = AzureMonitorTraceExporter(connection_string=connection_string) provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) return trace.get_tracer(service_name) tracer = configure_tracing( connection_string="InstrumentationKey=...", service_name="order-service"
) def process_order(order_id: str): with tracer.start_as_current_span("process_order") as span: span.set_attribute("order.id", order_id) with tracer.start_as_current_span("validate_inventory"): # This span will appear as a child in the distributed trace inventory_result = check_inventory(order_id) with tracer.start_as_current_span("charge_payment"): payment_result = process_payment(order_id) return {"order_id": order_id, "status": "processed"}
除了分布式追踪,每个服务还应该发出:
/health/live(进程在跑吗?)和 /health/ready(服务准备好接收流量了吗?)
默认的 Kubernetes 滚动部署会逐个替换 pod,这通常是想要的效果。关键是要加上正确的就绪探针——Kubernetes 在就绪探针通过之前不会往新 pod 路由流量。没有这个,流量会打到还在启动但没准备好服务的 pod 上。
# Excerpt from a Kubernetes deployment manifest
spec: strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 # Never take a pod down before a replacement is ready maxSurge: 1 # Allow one extra pod during rollout template: spec: containers: - name: order-service readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 10
一个 AKS 集群通过命名空间隔离 dev/staging/prod,小团队这样配置是合理的。按环境独立集群更干净但成本也更高。重要的是:生产和非生产 workload 绝不能混在同一个命名空间里,哪怕在不同集群上。
每次部署都应该由 git commit 触发,而不是手动 kubectl apply。我们用 Azure DevOps 流水线,把构建(创建并推送容器镜像)和部署(用新镜像 tag 更新 Kubernetes manifest)分开。Flux 或 ArgoCD 管理 git 状态和集群状态之间的同步。
收尾之前说句实在话:微服务增加的是实实在在的复杂度。如果你是小团队在做早期产品,结构良好的单体架构更合适。跑分布式服务——独立部署、分布式追踪、服务间通信、分布式事务的 saga 模式——运维开销是实打实的。
迁移到微服务的合适时机是你有具体的、已经验证的问题,而这些问题微服务确实能解决:团队因为代码库耦合互相拖后腿,组件有真正不一样的伸缩需求,或者需要用不同运行时做多语言服务。
如果正在评估微服务是否适合你的系统,或者已经走在迁移路上遇到了上述架构挑战,可以聊聊具体场景。
你们在生产环境跑微服务遇到的最大挑战是什么?对我们管用的模式不一定放之四海皆准,很想听听大家的经验。
原文链接:https://dev.to/azure/microservices-with-azure-what-actually-works-in-production-and-what-doesnt-4i5m