基於netty構建API網關
劉石明 資深架構師
專註於互聯網服務化架構,
人生永遠追逐著幻光,但誰把幻光看作幻光,誰便沉入無邊的苦海。
引言
隨著互聯網的快速發展,當前以步入移動互聯、物聯網時代。用戶訪問系統入口也變得多種方式,由原來單一的PC客戶端,變化到PC客戶端、各種瀏覽器、手機移動端及智能終端等。
同時系統之間大部分都不是單獨運行,經常會涉及與其他系統對接、共享數據的需求。所以系統需要升級框架滿足日新月異需求變化,支持業務發展,並將框架升級為微服務架構。「API網關」核心組件是架構用於滿足此些需求,很多互聯網平台已基於網關的設計思路,構建自身平台的API網關,國內主要有京東、攜程、唯品會等,國外主要有Netflix、Amazon等。
業界相關網關框架
業界為了滿足這些需求,已有相關的網關框架。
1、基於nginx平台實現的網關有:kong、umbrella等
2、自研發的網關有:zuul1、zuul2等
但是以上網關框架只能是滿足部分需求,不能滿足企業的所有要求,就我而言,我認為最大的問題是沒有協議轉換及OPS管理控制平台
網關概覽
* 面向 Web 或者移動 App
這類場景,前端應用通過 API 調用後端服務,需要網關具有認證、鑒權、緩存、服務編排、監控告警等功能。
* 面向合作夥伴開放 API
這類場景,主要為了滿足業務形態對外開放,與企業外部合作夥伴建立生態圈,此時的 API 網關注重安全認證、許可權分級、流量管控、緩存等功能。
* 企業內部系統互聯互通
這類場景,主要為了企業內部存在不同部門,而部門之間技術棧的不同,使用的通信協議或框架不同,需要一個通用的網關來支持內部系統互聯及互通,此時API網關會更注重協議轉換的功能,比如說gRPC和Dubbo的協議轉換
另外:對於微服務架構下,如果基於HTTP REST傳輸協議,API網關還承擔了一個內外API甄別的功能,只有在API網關上註冊了的API還能是真正的對外API
網關組成部分
整個網關係統由三個子系統組成:
1. GateWay Proxy:
負責接收客戶端請求、執行過濾器、 路由請求到後端服務,並處理後端服務的請求結果返回給客戶端。
2. GateWay OPS:
提供統一的管理界面,開發人員可在此進行 API、定義過濾器及定義過濾器規則。
3. GateWay Monitor:
主要由Prometheus來拉取GateWay Proxy的系統情況,由運維人員監控整個GateWay Proxy的健康狀況及API統計情況。
網關技術架構
說明:
1) 整個網關基於Netty NIO來實現同步非阻塞是HTTP服務,網關是外部API請求的HTTP服務端,同時是內部服務的客戶端,所以有Netty Server Handler和Netty Client Handler的出現;
2)對於Netty Server Handler來說,當一個HTTP請求進來時,他會把當前連接轉化為ClientToProxyConnection,它是線程安全的,伴隨當前此HTTP請求的生命周期結束,它也負責ClientToProxyConnection的生命周期的維護;
3)對於Netty Client Handler來說,當ClientToProxyConnection需要傳遞請求到內部服務時,會新建(或者獲取原來已建)的ProxyToServerConnection來進行內部的請求,它也是線程安全的;
4)對於Filter來說,他運行在ClientToProxyConnection上,插入請求進來及收到後端請求之間;
Ops管控平台概覽
技術細節
01
網路IO細節
網路模型可以分為阻塞IO、非阻塞IO、IO復用、信號驅動IO和非同步IO
網路IO操作(read/write系統調用)其實分成了兩個步驟:
第一:發起IO請求 ;
第二:實際的IO讀寫(內核態與用戶態的數據拷貝)
阻塞與非阻塞IO的區別在於第一步,發起IO請求的進程是否會被阻塞,如果阻塞直到IO操作完成才返回那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO;
同步IO和非同步IO的區別在於第二步,實際的IO讀寫(內核態與用戶態的數據拷貝)是否需要進程參與,如果需要進程參與則是同步IO,如果不需要進程參與就是非同步IO;
如果實際的IO讀寫需要請求進程參與,那麼就是同步IO。因此阻塞IO、非阻塞IO、IO復用、信號驅動IO都是同步IO
從以上來看HTTP請求是適合同步非阻塞的方式,一個HTTP請求必然有REQUEST和RESPONSE,而在zuul2上實現的真正網路IO也是同步非阻塞,只不過在HttpAsyncEndpoint這個Filter執行HTTP請求時採用了非同步方式,如下:
@Override
public Observable applyAsync(HttpRequestMessage input)
{
if (error != null) {
return Observable.create(subscriber -> {
Throwable t = new RuntimeException("Some error response problem.");
subscriber.onError(t);
});
}
else if (response != null) {
return Observable.just(response);
}
else {
return Observable.just(new HttpResponseMessageImpl(input.getContext(), input, 200));
}
}
從以上分析,網關選擇同步非阻塞方式是一個合適的選擇。
02
協議轉換細節
HTTP ----> gRPC
gRPC基於protobuf來做傳輸,所以整個在gRPC的客戶端來說,必須有protobuf的數據來描述當前此次gRPC請求,而proto文件是一個合適的數據描述方式,[grpc-web](https://github.com/grpc/grpc-web) 也是如此來做http請求的轉換
具體步驟是:
1.將proto文件轉化為protobuf的FileDescriptorSet對象,該對象描述了所有的proto內容,而轉化也很簡單,利用protobuf提供的命令即可轉化,在這裡我使用的是:
com.github.os72
protoc-jar
provided
其中轉化的過程如下:
public FileDescriptorSet invoke() throws ProtocInvocationException {
Path wellKnownTypesInclude;
try {
wellKnownTypesInclude = setupWellKnownTypes();
} catch (IOException e) {
throw new ProtocInvocationException("Unable to extract well known types", e);
}
Path descriptorPath;
try {
descriptorPath = Files.createTempFile("descriptor", ".pb.bin");
} catch (IOException e) {
throw new ProtocInvocationException("Unable to create temporary file", e);
}
ImmutableList protocArgs = ImmutableList.builder()
.addAll(scanProtoFiles(discoveryRoot)).addAll(includePathArgs(wellKnownTypesInclude))
.add("--descriptor_set_out=" + descriptorPath.toAbsolutePath().toString())
.add("--include_imports").build();
invokeBinary(protocArgs);
try {
return FileDescriptorSet.parseFrom(Files.readAllBytes(descriptorPath));
} catch (IOException e) {
throw new ProtocInvocationException("Unable to parse the generated descriptors", e);
}
}
2.根據FileDescriptorSet獲取gRPC的入參和出參描述符,然後再創建gRPC所需要的MethodDescriptor方法描述對象
3.獲取protobuf的MethodDescriptor來獲取入參和出參的Descriptor
private static Pair findDirectyprotobuf(final ApiRpcDO rpcDo) {
byte[ ] protoContent = rpcDo.getProtoContext();
FileDescriptorSet descriptorSet = null;
if (protoContent != null && protoContent.length > 0) {
try {
descriptorSet = FileDescriptorSet.parseFrom(protoContent);
ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(descriptorSet);
ProtoMethodName protoMethodName = ProtoMethodName
.parseFullGrpcMethodName(rpcDo.getServiceName() + "/" + rpcDo.getMethodName());
//MethodDescriptor是protobuf的方法描述對象
MethodDescriptor protoMethodDesc =
serviceResolver.resolveServiceMethod(protoMethodName.getServiceName(),
protoMethodName.getMethodName(), protoMethodName.getPackageName());
return new ImmutablePair(protoMethodDesc.getInputType(),
protoMethodDesc.getOutputType());
} catch (InvalidProtocolBufferException e) {
LOG.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
return null;
}
4.構建gRPC所需要的方法描述對象
private MethodDescriptor createGrpcMethodDescriptor(
String serviceName, String methodName, Descriptor inPutType, Descriptor outPutType) {
String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
//MethodDescriptor是grpc的方法描述對象
return io.grpc.MethodDescriptor.newBuilder()
.setType(MethodType.UNARY)//
.setFullMethodName(fullMethodName)//
.setRequestMarshaller(new DynamicMessageMarshaller(inPutType))//
.setResponseMarshaller(new DynamicMessageMarshaller(outPutType))//
.setSafe(false)//
.setIdempotent(false)//
.build();
}
HTTP ----> dubbo
在dubbo的框架設計中,其中已經包含了泛化調用的設計,所以在這塊,基本上就延用了dubbo的泛化調用來實現http轉dubbo的協議
try {
final String serviceName = rpcDo.getServiceName();
final String methodName = rpcDo.getMethodName();
final String group = rpcDo.getServiceGroup();
final String version = rpcDo.getServiceVersion();
ReferenceConfig reference = new ReferenceConfig();
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig);
reference.setInterface(serviceName);
reference.setGroup(group);
reference.setGeneric(true);
reference.setCheck(false);
reference.setVersion(version);
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
String templateKey = this.cacheTemplate(rpcDo);
Pair typeAndValue = this.transformerData(templateKey, servletRequest);
Object response =
genericService.$invoke(methodName, typeAndValue.getLeft(), typeAndValue.getRight());
return JSON.toJSONString(response);
} catch (Throwable e) {
throw new IllegalArgumentException(String.format(
"service definition is wrong,please check the proto file you update,service is %s, method is %s",
rpcDo.getServiceName(), rpcDo.getMethodName()), e);
}
參 數 裁 剪
利用了[JsonPath](https://github.com/json-path/JsonPath) jsonPath及Freemarker模板可以實現參數裁剪的效果,其設計思路可以參照,[amazon的做法](https://docs.aws.amazon.com/zh_cn/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html),實現效果如下:
[
{
"name": "$",
"mobile": "$",
"idNo": "$"
}
]
API規則編排
重複利用drools和groovy這些動態語言,能夠將規則開放給業務系統來維護。
整個網關目前基本完成並且也開源到GitHub上,
歡迎拍磚及使用:
[tesla](https://github.com/linking12/tesla)
文 章 結 語
GIF
TAG:點融黑幫 |