一个现实的技术挑战摆在面前:一个团队维护着一组基于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
优势分析:
- 快速启动: 部署简单,团队可以独立管理自己的入口配置,互不干扰。
- 故障隔离: JVM服务栈的入口问题不会直接影响Python服务栈。
劣势分析:
- 策略碎片化: 认证、速率限制、日志记录等横切关注点需要在多个Ingress配置中重复定义,极易产生不一致。一个常见的错误是,安全团队更新了JWT的验证密钥,但只在一个入口上生效了。
- 客户端复杂性: 客户端需要知道所有后端的地址,并自行处理服务发现和聚合逻辑。这违反了后端架构对客户端透明的原则。
- 可观测性黑洞: 跨服务的完整调用链路难以追踪。日志和指标散落在各个服务和入口点,形成数据孤岛。
- 内部调用混乱: 服务间的直接调用(如上图
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
优势分析:
- 集中治理: 安全(认证/授权)、流量控制(速率限制/熔断)、可观测性(日志/追踪)等策略在Kong层统一配置,对所有后端服务生效。
- 客户端简化: 客户端只与网关的稳定API交互,无需关心后端服务的实现语言、部署位置或通信协议。
- 协议灵活性: Kong可以同时代理HTTP/1.1, HTTP/2, gRPC等多种协议,甚至在需要时进行协议转换(例如通过插件将REST请求转换为gRPC)。
- 架构解耦: 后端服务可以独立演进,只要遵守与网关约定的API契约。
劣势分析:
- 网关的复杂性: Kong的配置和插件管理本身需要专业知识。
- 单点风险: 网关成为系统的关键路径,其自身的性能和可用性至关重要,必须进行高可用部署。
最终决策:
方案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
这段配置完成了以下工作:
- 创建了两个Kubernetes
Deployment
和Service
,分别用于Micronaut和Python应用。 - 定义了一个
Ingress
资源,将/jvm/users
的HTTP流量路由到Micronaut服务,/py/users
路由到Python服务。 - 定义了一个
TCPIngress
,这是一个Kong特有的CRD,用于处理非HTTP流量。我们将Kong的9000
端口的grpc
协议流量代理到Micronaut服务的gRPC端口。 - 创建了一个名为
jwt-auth-plugin
的KongPlugin
资源。 - 通过在
Ingress
和TCPIngress
上添加konghq.com/plugins: jwt-auth-plugin
注解,我们将JWT认证策略统一应用到了所有HTTP和gRPC路由上,无论后端是Java还是Python。
架构的扩展性与局限性
这套架构模式为异构微服务系统提供了一个坚实的治理基础。当需要引入新的Go或Node.js服务时,只需实现其业务逻辑,然后通过几行YAML将其注册到Kong,即可立即获得统一的安全和路由策略,扩展性非常强。
然而,这并非银弹。当前的方案存在一些局限和需要权衡的地方:
- 网关的职责边界: Kong承担了路由、安全、流量控制等多种职责。在一个更大规模的系统中,这可能会变得臃肿。服务间的通信(东西向流量)虽然可以通过K8s Service直接调用以获得更低延迟,但这绕过了Kong的治理。更进一步的演进方向是引入Service Mesh(如Istio, Linkerd),将服务间的通信策略下沉到Sidecar代理,让Kong更专注于南北向流量(进出集群的流量)的管理。
- 契约版本管理:
.proto
文件成为了跨团队强依赖的核心资产。必须建立严格的版本控制和向后兼容策略。任何破坏性变更都需要协调所有依赖方同步升级,这是一个组织和流程上的挑战。 - gRPC代理的复杂性: 当前我们使用了TCP代理模式来处理gRPC,这在性能上最优,但Kong对请求内容的可见性有限。如果需要基于gRPC的Method或Header进行更精细的路由或应用更复杂的插件,则需要Kong Enterprise的功能或更复杂的开源插件配置,这会增加运维成本。
- 可观测性集成: 虽然Kong提供了日志和指标插件,但要构建一个完整的、覆盖从网关到每个服务的分布式追踪链,还需要在所有服务中集成OpenTelemetry等标准,并确保Trace Context能在跨语言、跨协议的调用中正确传播。这需要额外的开发和配置工作。