一、从单体到集群:当你的应用需要更多“分身”

想象一下,你开了一家非常火爆的奶茶店,店里只有你一个店员。顾客(请求)来了,你(应用)得负责点单、制作、收银全套流程。一开始还能应付,但随着顾客排起长队,你一个人就忙不过来了,顾客体验变差,你也累得够呛。

这时候,你决定开分店,或者在同一家店里多招几个伙计。这就是我们常说的应用“横向扩展”——部署多个应用实例来分担压力。在传统的服务器环境里,你可能需要手动告诉顾客:“一号店在A街,二号店在B街”,或者放个前台(比如Nginx)来帮顾客分配去哪个店员那里。

但当你的“分店”开到了Kubernetes这个高度自动化的“超级商业综合体”里,情况就变得复杂又奇妙了。Kubernetes会动态地管理你的应用“分身”(Pod),它们可能因为故障被重启、因为负载变化而增加或减少,甚至被调度到不同的“楼层”(节点)。它们的IP地址和端口就像店员的工牌,是随时可能更换的。

那么,问题来了:顾客(比如一个用户请求,或者另一个微服务)如何才能准确、高效地找到当前正在营业的所有“奶茶店分身”呢?这就是“服务发现”。找到了所有分身,又该如何公平地分配顾客,不让某个店员累死,另一个闲着呢?这就是“负载均衡”。

对于从传统部署迁移到Kubernetes的 .NET Core 应用来说,理解并解决好这两个问题,是实现平滑“云原生”迁移的关键一步。别担心,这个过程并不像听起来那么晦涩,我们一步步来。

二、Kubernetes的“服务”对象:你的智能前台与导购

Kubernetes 非常贴心地为我们提供了一个叫 Service 的核心概念。你可以把它想象成我们奶茶店那个永远在线、信息灵通的智能前台。

这个前台(Service)不直接做奶茶(不运行应用代码),但它做两件至关重要的事:

  1. 服务发现:它有一份实时更新的、健康的分身(Pod)名单。只要分身们戴上标有特定“标签”的工牌(Label),前台就会自动把它们纳入自己的名单。
  2. 负载均衡:当顾客请求到来时,这个智能前台会自动、均匀地把请求分发给名单里健康的店员。

让我们来看一个最基础的例子。假设我们有一个简单的 .NET Core Web API 应用。

技术栈:.NET Core 6, Kubernetes

首先,我们需要为应用分身(Pod)打上标签。这通常在部署(Deployment)中定义:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-dotnet-api
spec:
  replicas: 3 # 告诉K8s,我想要3个应用分身
  selector:
    matchLabels:
      app: my-dotnet-api # 选择器:管理所有带这个标签的Pod
  template:
    metadata:
      labels:
        app: my-dotnet-api # 给每个Pod都贴上这个标签,这是它们“工牌”的核心标识
    spec:
      containers:
      - name: api
        image: myregistry/my-dotnet-api:latest
        ports:
        - containerPort: 80 # 容器内部监听80端口

然后,我们创建那个关键的“智能前台”——Service:

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-dotnet-api-service # 前台的名字,其他服务将通过这个名字找到它
spec:
  selector:
    app: my-dotnet-api # 关键配置:前台只接待和管理贴有 `app: my-dotnet-api` 工牌的Pod
  ports:
    - protocol: TCP
      port: 80        # Service对外暴露的端口,集群内其他服务访问这个端口
      targetPort: 80  # 将请求转发到Pod容器的80端口
  type: ClusterIP     # 这是默认类型,意味着这个“前台”只在Kubernetes集群内部工作

应用这两个配置后,神奇的事情发生了。在集群内部,任何应用(比如另一个微服务)现在只需要访问 http://my-dotnet-api-service 这个固定的域名,就能访问到我们的.NET Core API。Kubernetes 内置的 DNS 服务会自动将这个域名解析到 Service 的虚拟 IP(ClusterIP),然后 Service 负责将请求负载均衡到后端的 3 个 Pod 之一。

你不需要关心 Pod 的具体 IP,哪怕 Pod 重启、IP 变了,Service 的域名和它的“分身名单”都会自动更新。这就是 Kubernetes 内置的服务发现与负载均衡,它是云原生的基石。

三、融入与适配:.NET Core应用需要做什么?

看到这里,你可能会想:“我的 .NET Core 代码需要大改吗?” 好消息是,对于基本的 HTTP/gRPC API 服务,你几乎不需要修改任何业务代码。Kubernetes Service 的负载均衡发生在网络层面(第4层,TCP/UDP),对应用是透明的。你的应用就像在跟一个固定的客户端对话,完全感知不到背后有多个分身。

但是,为了实现真正的“云原生平滑迁移”,让应用在 K8s 里更健康、更易观察,我们通常需要做一些“适配性”工作。这并非为了服务发现本身,而是为了与 K8s 环境更好地协作。

1. 健康检查探针:让前台知道店员是否健康 智能前台需要知道哪个店员(Pod)状态良好可以接客。这通过配置 livenessProbereadinessProbe 实现。

# 在 deployment.yaml 的 container 部分添加
containers:
- name: api
  image: myregistry/my-dotnet-api:latest
  ports: [...]
  livenessProbe:  # 存活探针:判断容器是否“活着”,如果失败K8s会重启容器
    httpGet:
      path: /healthz  # 你的应用需要实现这个健康检查端点
      port: 80
    initialDelaySeconds: 30 # 容器启动后30秒开始检查
    periodSeconds: 10       # 每10秒检查一次
  readinessProbe: # 就绪探针:判断容器是否“准备好”接收流量,如果失败,会从Service的负载均衡列表中暂时移除
    httpGet:
      path: /ready   # 你的应用需要实现这个就绪检查端点
      port: 80
    initialDelaySeconds: 5
    periodSeconds: 5

在你的 .NET Core 应用中,可以使用 Microsoft.Extensions.Diagnostics.HealthChecks 库轻松添加这些端点。

2. 优雅终止:让店员有礼貌地下班 当 K8s 要关闭一个 Pod(例如滚动更新时),它会先发送一个 SIGTERM 信号。你的应用应该捕获这个信号,完成正在处理的请求,然后退出。这在 .NET Core 的 IHostIHostApplicationLifetime 中很容易实现。

// Program.cs 或 Startup.cs 中的简化示例
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureServices((hostContext, services) =>
        {
            // 注册一个后台服务来监听应用终止
            services.AddHostedService<GracefulShutdownService>();
        });

// 优雅终止服务
public class GracefulShutdownService : BackgroundService
{
    private readonly ILogger<GracefulShutdownService> _logger;
    private readonly IHostApplicationLifetime _hostLifetime;

    public GracefulShutdownService(ILogger<GracefulShutdownService> logger, IHostApplicationLifetime hostLifetime)
    {
        _logger = logger;
        _hostLifetime = hostLifetime;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // 监听应用停止信号
        _hostLifetime.ApplicationStopping.Register(() =>
        {
            _logger.LogInformation("收到终止信号,开始优雅关闭...");
            // 这里可以添加等待业务逻辑完成的代码,例如等待请求处理完毕
            Task.Delay(5000).Wait(); // 模拟等待5秒
            _logger.LogInformation("优雅关闭完成。");
        });
        return Task.CompletedTask;
    }
}

3. 配置与状态:适应动态环境 避免在代码中硬编码数据库连接字符串、服务地址等。使用 Kubernetes ConfigMap 和 Secret 来管理配置,并通过环境变量或卷挂载的方式注入到容器中。对于需要发现其他服务的场景,坚持使用 Service 的 DNS 名称(如 http://other-service-name)进行访问,这是云原生的黄金法则。

四、进阶场景:当基础服务不够用时

Kubernetes 自带的 ClusterIP Service 解决了集群内部访问的问题。但现实世界更复杂,我们来看看其他常见场景。

场景一:从集群外访问——NodePort与LoadBalancer 如果你想从外部互联网访问你的 .NET Core API,就需要改变 Service 的类型。

  • NodePort:在每个集群节点上开放一个静态端口(如 30080),访问任何节点的这个端口,请求都会被转发到 Service。
    type: NodePort
    ports:
    - port: 80
      targetPort: 80
      nodePort: 30080 # 可指定范围 30000-32767
    
  • LoadBalancer:在支持云提供商负载均衡器的环境中(如 AWS、Azure、GCP),使用此类型会自动创建一个外部负载均衡器,并分配一个外部 IP。这是将服务暴露到公网的标准方式。
    type: LoadBalancer
    # 云提供商会自动处理其余配置
    

场景二:更智能的路由——Ingress 如果你有多个服务,想通过不同的 URL 路径或域名来访问(例如 api.example.com/v1api.example.com/v2 指向不同服务),或者需要 SSL 终止,那么基础的 Service 就不够了。这时需要 IngressIngress Controller(如 Nginx Ingress Controller)。

Ingress 定义路由规则,Ingress Controller 是实现这些规则的“智能网关”。它位于 Service 之前,提供第7层(HTTP/HTTPS)的更丰富的负载均衡和路由能力。

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: / # 常用的Nginx Ingress注解
spec:
  rules:
  - host: api.mycompany.com # 域名
    http:
      paths:
      - path: /order
        pathType: Prefix
        backend:
          service:
            name: order-service # 指向处理订单的Service
            port:
              number: 80
      - path: /product
        pathType: Prefix
        backend:
          service:
            name: product-service # 指向处理产品的Service
            port:
              number: 80

五、实战迁移清单与避坑指南

假设你现在有一个在虚拟机或物理机上运行的 .NET Core 应用,准备迁移到 Kubernetes。请遵循以下清单:

  1. 容器化:将应用 Docker 化,创建 Dockerfile。确保镜像是无状态的,日志输出到标准输出(stdout)。
  2. 定义部署:编写 Deployment YAML,配置好副本数、资源请求与限制、健康检查探针。
  3. 定义服务:编写 Service YAML,通过标签选择器正确关联到你的 Deployment。内部访问优先使用 ClusterIP
  4. 外部化配置:将应用配置(如连接字符串)移出代码,使用 ConfigMap 和 Secret。
  5. 测试服务发现:在集群内部署一个临时测试 Pod(例如 busybox),使用 nslookupcurl 命令测试是否能通过 Service 名称正确解析和访问你的应用。
  6. 考虑有状态服务:如果你的应用依赖 Redis、SQL Server 等,不要简单地将它们也部署为普通 Deployment。对于生产环境,应使用 StatefulSet 或更佳方案——直接使用云厂商提供的托管数据库服务,或者通过 Operator 管理的专业数据库(如 Redis Operator)。永远记住,Kubernetes 最擅长管理无状态工作负载。
  7. 监控与日志:集成集群监控(如 Prometheus)和集中式日志收集(如 EFK/ELK 栈),这是云原生运维的眼睛。

避坑指南

  • 避免硬编码IP:这是迁移中最常见的错误。所有服务间调用必须使用K8s Service名称。
  • 合理设置资源限制:为容器设置 requestslimits,防止单个应用耗尽节点资源。
  • 理解网络策略:默认情况下,K8s集群内所有Pod是网络互通的。如果需要隔离,要配置 NetworkPolicy。
  • 滚动更新策略:在Deployment中配置 strategy.rollingUpdate,确保更新时服务不中断。

六、总结:云原生之路,始于简单的第一步

将 .NET Core 应用迁移到 Kubernetes,并利用其服务发现与负载均衡能力,听起来高大上,但核心思想很简单:让应用变成一个个声明式的、可被平台自动管理的“零件”

你不再需要手动维护服务器列表、配置复杂的 Nginx 规则。你只需要告诉 Kubernetes:“我需要3个我的API应用实例,它们通过80端口提供服务,并且要健康检查。” 剩下的,Kubernetes 的 Service 和其他组件会帮你自动完成。

从今天开始,你可以尝试将一个简单的、无状态的 .NET Core 辅助服务(比如一个缓存预热作业或一个报表生成API)进行容器化,并部署到测试环境的 Kubernetes 集群中。从这个小目标开始,逐步积累经验,你会发现自己正稳步走在云原生架构的康庄大道上。这个过程,就是所谓的“平滑迁移”。