在Azure上为Micronaut与Python异构服务构建基于Kong的统一gRPC与REST API网关


一个现实的技术挑战摆在面前:一个团队维护着一组基于Micronaut的高性能JVM微服务,负责处理核心的、计算密集的业务逻辑;另一个数据科学团队则使用Python框架(例如FastAPI)快速迭代机器学习模型并提供推理服务。现在,需要将这两组异构服务的能力统一暴露给外部消费者。这些服务间还存在着高性能的内部调用需求。直接将每个服务都暴露出去会迅速导致安全策略碎片化、监控口径不一、客户端调用逻辑复杂化等一系列维护性灾难。

核心矛盾在于,如何在一个统一的入口点上,既能优雅地处理面向外部消费者的RESTful API请求,又能高效地代理服务之间高性能的gRPC通信,同时还要实施一套与具体实现语言无关的认证、授权和流量控制策略。这一切都需要部署在Azure Kubernetes Service (AKS) 这样的生产环境中。

方案A:多入口点与客户端聚合的原始模式

初步的构想往往最直接。我们可以为每个服务栈分别配置入口。比如,为Micronaut服务配置一个Ingress,为Python服务配置另一个。认证逻辑可以部分放在网关,部分放在服务内部。客户端或者一个专用的BFF (Backend for Frontend)层负责调用不同的端点并聚合数据。

graph TD
    subgraph Azure Cloud
        subgraph AKS Cluster
            subgraph JVM Services
                M_SVC1[Micronaut Service 1]
                M_SVC2[Micronaut Service 2]
            end
            subgraph Python Services
                P_SVC1[FastAPI Service 1]
                P_SVC2[FastAPI Service 2]
            end

            Ingress_JVM[Kong Ingress for JVM] --> M_SVC1
            Ingress_JVM --> M_SVC2

            Ingress_PY[Kong Ingress for Python] --> P_SVC1
            Ingress_PY --> P_SVC2
        end
    end

    Client[Mobile/Web Client] --> Ingress_JVM
    Client --> Ingress_PY

    M_SVC1 -- gRPC --> P_SVC1

优势分析:

  1. 快速启动: 部署简单,团队可以独立管理自己的入口配置,互不干扰。
  2. 故障隔离: JVM服务栈的入口问题不会直接影响Python服务栈。

劣势分析:

  1. 策略碎片化: 认证、速率限制、日志记录等横切关注点需要在多个Ingress配置中重复定义,极易产生不一致。一个常见的错误是,安全团队更新了JWT的验证密钥,但只在一个入口上生效了。
  2. 客户端复杂性: 客户端需要知道所有后端的地址,并自行处理服务发现和聚合逻辑。这违反了后端架构对客户端透明的原则。
  3. 可观测性黑洞: 跨服务的完整调用链路难以追踪。日志和指标散落在各个服务和入口点,形成数据孤岛。
  4. 内部调用混乱: 服务间的直接调用(如上图 M_SVC1 --> P_SVC1)绕过了任何网络策略管理,安全性和可靠性完全依赖于服务内部的实现,这在规模化系统中是不可接受的。

在真实项目中,这种方案会在团队规模扩大、服务数量增多后迅速崩溃,最终导致巨大的技术债。

方案B:统一API网关与协议代理

更成熟的架构是引入一个统一的API网关,作为所有流量的唯一入口。Kong在这里不仅仅是一个Ingress Controller,而是整个系统的策略执行点 (Policy Enforcement Point)。所有外部请求,无论是发往Micronaut还是Python服务,都必须经过Kong。服务间的通信也可以被Kong纳管,实现统一的治理。

graph TD
    subgraph Azure Cloud
        subgraph AKS Cluster
            subgraph Unified Gateway
                Kong[Kong API Gateway]
            end

            subgraph JVM Services
                M_SVC[Micronaut Service]
            end
            subgraph Python Services
                P_SVC[FastAPI Service]
            end

            Kong -- REST/gRPC Proxy --> M_SVC
            Kong -- REST/gRPC Proxy --> P_SVC
            M_SVC -- Internal gRPC via K8s Service --> P_SVC
        end
    end
    Client[Mobile/Web Client] -- REST API --> Kong
    InternalClient[Internal gRPC Client] -- gRPC --> Kong

优势分析:

  1. 集中治理: 安全(认证/授权)、流量控制(速率限制/熔断)、可观测性(日志/追踪)等策略在Kong层统一配置,对所有后端服务生效。
  2. 客户端简化: 客户端只与网关的稳定API交互,无需关心后端服务的实现语言、部署位置或通信协议。
  3. 协议灵活性: Kong可以同时代理HTTP/1.1, HTTP/2, gRPC等多种协议,甚至在需要时进行协议转换(例如通过插件将REST请求转换为gRPC)。
  4. 架构解耦: 后端服务可以独立演进,只要遵守与网关约定的API契约。

劣势分析:

  1. 网关的复杂性: Kong的配置和插件管理本身需要专业知识。
  2. 单点风险: 网关成为系统的关键路径,其自身的性能和可用性至关重要,必须进行高可用部署。

最终决策:
方案B是生产环境的唯一选择。它带来的长期可维护性和安全性远超其初始配置的复杂性。我们将采用此架构,并在Azure AKS上进行具体实现。核心任务是配置Kong以统一代理对外的REST API和对内的gRPC流量,并对所有请求强制执行JWT验证。

核心实现

1. 基础设施准备:Azure AKS与Kong部署

我们使用Bicep来定义Azure上的基础设施,包括资源组、容器注册表(ACR)和AKS集群。

infra/main.bicep:

@description('The location for all resources.')
param location string = resourceGroup().location

@description('The base name for all resources.')
param baseName string = 'polyglotgw'

@description('The Kubernetes version for the AKS cluster.')
param kubernetesVersion string = '1.27.7'

resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
  name: '${baseName}acr'
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    adminUserEnabled: true
  }
}

resource aks 'Microsoft.ContainerService/managedClusters@2023-10-01' = {
  name: '${baseName}aks'
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    kubernetesVersion: kubernetesVersion
    dnsPrefix: '${baseName}-dns'
    agentPoolProfiles: [
      {
        name: 'agentpool'
        count: 2
        vmSize: 'Standard_DS2_v2'
        osType: 'Linux'
        mode: 'System'
      }
    ]
    networkProfile: {
      networkPlugin: 'azure'
      networkPolicy: 'azure'
    }
  }
}

// Grant AKS pull access to ACR
resource acrRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(aks.id, acr.id, 'AcrPull')
  scope: acr
  properties: {
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull role
    principalId: aks.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

output acrLoginServer string = acr.properties.loginServer
output aksName string = aks.name

部署完成后,我们使用Helm将Kong部署到AKS中。

# Add Kong's Helm repository
helm repo add kong https://charts.konghq.com
helm repo update

# Install Kong in proxy mode
helm install kong kong/kong \
  --namespace kong \
  --create-namespace \
  --set ingressController.installCRDs=false \
  --set proxy.type=LoadBalancer

2. 定义统一的服务契约:Protobuf

为了实现服务间高效通信,我们选择gRPC。首先定义一个.proto文件,它将成为Micronaut和Python服务之间通信的契约。

protos/user_service.proto:

syntax = "proto3";

package users;

option java_package = "com.example.polyglot.users";
option java_multiple_files = true;

// The user service definition.
service UserService {
  // Retrieves a user by their ID.
  rpc GetUser (UserRequest) returns (UserResponse);
}

// The request message containing the user's ID.
message UserRequest {
  string user_id = 1;
}

// The response message containing the user's details.
message UserResponse {
  string user_id = 1;
  string name = 2;
  string email = 3;
  string role = 4; // e.g., 'admin', 'user'
}

这个文件是跨团队协作的基石,应当在一个独立的Git仓库中进行版本管理。

3. 服务实现:Micronaut (Java)

这个服务将实现gRPC端点,并额外提供一个REST端点作为对照。

build.gradle.kts:

plugins {
    // ... Micronaut plugins
    id("com.google.protobuf") version "0.9.4"
}

// ... repositories and versions

dependencies {
    implementation("io.micronaut.grpc:micronaut-grpc-runtime")
    implementation("jakarta.annotation:jakarta.annotation-api")
    runtimeOnly("io.grpc:grpc-netty")
    // For REST endpoint
    implementation("io.micronaut:micronaut-http-client")
    implementation("io.micronaut.serde:micronaut-serde-jackson")

    // ... other dependencies
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.24.4"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.58.0"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc") {}
            }
        }
    }
}

UserServiceEndpoint.java:

package com.example.polyglot.users;

import io.grpc.stub.StreamObserver;
import jakarta.inject.Singleton;
import io.micronaut.grpc.annotation.GrpcService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
@GrpcService
public class UserServiceEndpoint extends UserServiceGrpc.UserServiceImplBase {

    private static final Logger LOG = LoggerFactory.getLogger(UserServiceEndpoint.class);

    private final Map<String, UserResponse> users = new ConcurrentHashMap<>();

    public UserServiceEndpoint() {
        // Mock data for demonstration
        users.put("101", UserResponse.newBuilder()
                .setUserId("101").setName("Alice (from Micronaut)")
                .setEmail("[email protected]").setRole("admin").build());
        users.put("102", UserResponse.newBuilder()
                .setUserId("102").setName("Bob (from Micronaut)")
                .setEmail("[email protected]").setRole("user").build());
    }

    @Override
    public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
        String userId = request.getUserId();
        LOG.info("Micronaut service received gRPC request for user: {}", userId);

        if (!users.containsKey(userId)) {
            responseObserver.onError(io.grpc.Status.NOT_FOUND
                    .withDescription("User not found with ID: " + userId)
                    .asRuntimeException());
            return;
        }

        responseObserver.onNext(users.get(userId));
        responseObserver.onCompleted();
    }
}

为了便于部署,我们为其编写Dockerfile。

Dockerfile.micronaut:

FROM amazoncorretto:17-alpine-jdk
COPY build/libs/micronaut-service-*-all.jar micronaut-service.jar
EXPOSE 8081 8080
CMD ["java", "-jar", "micronaut-service.jar"]

注意这里暴露了gRPC端口(默认8081)和HTTP端口(默认8080)。

4. 服务实现:FastAPI (Python)

Python服务同样实现gRPC,并提供REST API。

requirements.txt:

fastapi==0.104.1
uvicorn[standard]==0.24.0.post1
grpcio==1.59.2
grpcio-tools==1.59.2
protobuf==4.25.1

首先,使用grpc_tools生成Python代码:

python -m grpc_tools.protoc -I../protos --python_out=. --grpc_python_out=. ../protos/user_service.proto

main.py:

import asyncio
import logging
from concurrent import futures

import grpc
import uvicorn
from fastapi import FastAPI, HTTPException

# Import generated code
import user_service_pb2
import user_service_pb2_grpc

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Mock Data ---
USERS_DB = {
    "201": {
        "user_id": "201",
        "name": "Carlos (from Python)",
        "email": "[email protected]",
        "role": "user"
    },
    "202": {
        "user_id": "202",
        "name": "Diana (from Python)",
        "email": "[email protected]",
        "role": "auditor"
    },
}

# --- gRPC Service Implementation ---
class UserService(user_service_pb2_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        user_id = request.user_id
        logger.info(f"Python service received gRPC request for user: {user_id}")
        
        if user_id not in USERS_DB:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details(f"User not found with ID: {user_id}")
            return user_service_pb2.UserResponse()
            
        user_data = USERS_DB[user_id]
        return user_service_pb2.UserResponse(**user_data)

# --- FastAPI REST Implementation ---
app = FastAPI()

@app.get("/users/{user_id}")
def get_user_rest(user_id: str):
    logger.info(f"Python service received REST request for user: {user_id}")
    if user_id not in USERS_DB:
        raise HTTPException(status_code=404, detail="User not found")
    return USERS_DB[user_id]

# --- Server Startup Logic ---
async def serve():
    grpc_server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
    user_service_pb2_grpc.add_UserServiceServicer_to_server(UserService(), grpc_server)
    grpc_server.add_insecure_port('[::]:50051')
    await grpc_server.start()
    
    config = uvicorn.Config(app, host="0.0.0.0", port=8000, log_level="info")
    server = uvicorn.Server(config)
    
    await asyncio.gather(
        grpc_server.wait_for_termination(),
        server.serve()
    )

if __name__ == "__main__":
    asyncio.run(serve())

Dockerfile.python:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000 50051
CMD ["python", "main.py"]

5. Kubernetes部署与Kong配置

现在,我们将两个服务和Kong的路由规则部署到AKS。

k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: micronaut-user-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: micronaut-user-service
  template:
    metadata:
      labels:
        app: micronaut-user-service
    spec:
      containers:
      - name: service
        image: <your_acr_name>.azurecr.io/micronaut-service:v1
        ports:
        - containerPort: 8080 # REST
        - containerPort: 8081 # gRPC
---
apiVersion: v1
kind: Service
metadata:
  name: micronaut-svc
spec:
  selector:
    app: micronaut-user-service
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: grpc
    port: 8081
    targetPort: 8081
    appProtocol: grpc # Important for gRPC routing
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: python-user-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: python-user-service
  template:
    metadata:
      labels:
        app: python-user-service
    spec:
      containers:
      - name: service
        image: <your_acr_name>.azurecr.io/python-service:v1
        ports:
        - containerPort: 8000 # REST
        - containerPort: 50051 # gRPC
---
apiVersion: v1
kind: Service
metadata:
  name: python-svc
spec:
  selector:
    app: python-user-service
  ports:
  - name: http
    port: 80
    targetPort: 8000
  - name: grpc
    port: 50051
    targetPort: 50051
    appProtocol: grpc

接下来是关键的Kong配置部分。我们将使用Kong的CRDs (Ingress, KongPlugin, TCPIngress) 来声明式地定义路由和策略。

k8s/kong-routing.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: user-services-ingress
  annotations:
    konghq.com/strip-path: 'true'
spec:
  ingressClassName: kong
  rules:
  - http:
      paths:
      - path: /jvm/users
        pathType: Prefix
        backend:
          service:
            name: micronaut-svc
            port:
              name: http
      - path: /py/users
        pathType: Prefix
        backend:
          service:
            name: python-svc
            port:
              name: http
---
# This TCPIngress exposes the Micronaut gRPC service on port 9000
apiVersion: configuration.konghq.com/v1beta1
kind: TCPIngress
metadata:
  name: micronaut-grpc-ingress
  annotations:
    # Apply JWT plugin to this gRPC route
    konghq.com/plugins: jwt-auth-plugin
spec:
  rules:
  - port: 9000
    backend:
      serviceName: micronaut-svc
      servicePort: 8081
    protocol: grpc # Tell Kong this is a gRPC service
---
# Universal JWT Plugin Configuration
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: jwt-auth-plugin
  # Global plugin, but can be attached selectively via annotations
  annotations:
    konghq.com/global: "false" 
config:
  key_claim_name: iss
  secret_is_base64: false
plugin: jwt
---
# Apply the JWT plugin to our HTTP ingress routes
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: user-services-ingress # This must match the name of the Ingress
  annotations:
    # This annotation applies the jwt-auth-plugin to ALL paths in this Ingress
    konghq.com/plugins: jwt-auth-plugin
spec:
  # The spec must be identical to the one in the main Ingress resource
  ingressClassName: kong
  rules:
  - http:
      paths:
      - path: /jvm/users
        pathType: Prefix
        backend:
          service:
            name: micronaut-svc
            port:
              name: http
      - path: /py/users
        pathType: Prefix
        backend:
          service:
            name: python-svc
            port:
              name: http

这段配置完成了以下工作:

  1. 创建了两个Kubernetes DeploymentService,分别用于Micronaut和Python应用。
  2. 定义了一个Ingress资源,将/jvm/users的HTTP流量路由到Micronaut服务,/py/users路由到Python服务。
  3. 定义了一个TCPIngress,这是一个Kong特有的CRD,用于处理非HTTP流量。我们将Kong的9000端口的grpc协议流量代理到Micronaut服务的gRPC端口。
  4. 创建了一个名为jwt-auth-pluginKongPlugin资源。
  5. 通过在IngressTCPIngress上添加konghq.com/plugins: jwt-auth-plugin注解,我们将JWT认证策略统一应用到了所有HTTP和gRPC路由上,无论后端是Java还是Python。

架构的扩展性与局限性

这套架构模式为异构微服务系统提供了一个坚实的治理基础。当需要引入新的Go或Node.js服务时,只需实现其业务逻辑,然后通过几行YAML将其注册到Kong,即可立即获得统一的安全和路由策略,扩展性非常强。

然而,这并非银弹。当前的方案存在一些局限和需要权衡的地方:

  1. 网关的职责边界: Kong承担了路由、安全、流量控制等多种职责。在一个更大规模的系统中,这可能会变得臃肿。服务间的通信(东西向流量)虽然可以通过K8s Service直接调用以获得更低延迟,但这绕过了Kong的治理。更进一步的演进方向是引入Service Mesh(如Istio, Linkerd),将服务间的通信策略下沉到Sidecar代理,让Kong更专注于南北向流量(进出集群的流量)的管理。
  2. 契约版本管理: .proto文件成为了跨团队强依赖的核心资产。必须建立严格的版本控制和向后兼容策略。任何破坏性变更都需要协调所有依赖方同步升级,这是一个组织和流程上的挑战。
  3. gRPC代理的复杂性: 当前我们使用了TCP代理模式来处理gRPC,这在性能上最优,但Kong对请求内容的可见性有限。如果需要基于gRPC的Method或Header进行更精细的路由或应用更复杂的插件,则需要Kong Enterprise的功能或更复杂的开源插件配置,这会增加运维成本。
  4. 可观测性集成: 虽然Kong提供了日志和指标插件,但要构建一个完整的、覆盖从网关到每个服务的分布式追踪链,还需要在所有服务中集成OpenTelemetry等标准,并确保Trace Context能在跨语言、跨协议的调用中正确传播。这需要额外的开发和配置工作。

  目录