當前位置:
首頁 > 最新 > 基於netty構建API網關

基於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:點融黑幫 |