使用netty構(gòu)建API網(wǎng)關(guān)實踐之路

引言

隨著互聯(lián)網(wǎng)的快速發(fā)展,當前以步入移動互聯(lián)、物聯(lián)網(wǎng)時代。用戶訪問系統(tǒng)入口也變得多種方式,由原來單一的PC客戶端,變化到PC客戶端、各種瀏覽器、手機移動端及智能終端等。同時系統(tǒng)之間大部分都不是單獨運行,經(jīng)常會涉及與其他系統(tǒng)對接、共享數(shù)據(jù)的需求。所以系統(tǒng)需要升級框架滿足日新月異需求變化,支持業(yè)務(wù)發(fā)展,并將框架升級為微服務(wù)架構(gòu)?!癆PI網(wǎng)關(guān)”核心組件是架構(gòu)用于滿足此些需求
很多互聯(lián)網(wǎng)平臺已基于網(wǎng)關(guān)的設(shè)計思路,構(gòu)建自身平臺的API網(wǎng)關(guān),國內(nèi)主要有京東、攜程、唯品會等,國外主要有Netflix、Amazon等。

業(yè)界相關(guān)網(wǎng)關(guān)框架

業(yè)界為了滿足這些需求,已有相關(guān)的網(wǎng)關(guān)框架。
1、基于nginx平臺實現(xiàn)的網(wǎng)關(guān)有:kong、umbrella等
2、自研發(fā)的網(wǎng)關(guān)有:zuul1、zuul2等
但是以上網(wǎng)關(guān)框架只能是滿足部分需求,不能滿足企業(yè)的所有要求,就我而言,我認為最大的問題是沒有協(xié)議轉(zhuǎn)換及OPS管理控制平臺

網(wǎng)關(guān)概覽
overview.png
  • 面向 Web 或者移動 App
    這類場景,前端應(yīng)用通過 API 調(diào)用后端服務(wù),需要網(wǎng)關(guān)具有認證、鑒權(quán)、緩存、服務(wù)編排、監(jiān)控告警等功能。
  • 面向合作伙伴開放 API
    這類場景,主要為了滿足業(yè)務(wù)形態(tài)對外開放,與企業(yè)外部合作伙伴建立生態(tài)圈,此時的 API 網(wǎng)關(guān)注重安全認證、權(quán)限分級、流量管控、緩存等功能。
  • 企業(yè)內(nèi)部系統(tǒng)互聯(lián)互通
    這類場景,主要為了企業(yè)內(nèi)部存在不同部門,而部門之間技術(shù)棧的不同,使用的通信協(xié)議或框架不同,需要一個通用的網(wǎng)關(guān)來支持內(nèi)部系統(tǒng)互聯(lián)及互通,此時API網(wǎng)關(guān)會更注重協(xié)議轉(zhuǎn)換的功能,比如說gRPC和Dubbo的協(xié)議轉(zhuǎn)換

另外:對于微服務(wù)架構(gòu)下,如果基于HTTP REST傳輸協(xié)議,API網(wǎng)關(guān)還承擔了一個內(nèi)外API甄別的功能,只有在API網(wǎng)關(guān)上注冊了的API還能是真正的堆外API

網(wǎng)關(guān)組成部分
apparch.png

整個網(wǎng)關(guān)系統(tǒng)由三個子系統(tǒng)組成:

  1. GateWay Proxy:
    負責接收客戶端請求、執(zhí)行過濾器、 路由請求到后端服務(wù),并處理后端服務(wù)的請求結(jié)果返回給客戶端
  2. GateWay OPS:
    提供統(tǒng)一的管理界面,開發(fā)人員可在此進行 API、定義過濾器及定義過濾器規(guī)則
  3. GateWay Monitor:
    主要由Prometheus來拉取GateWay Proxy的系統(tǒng)情況,由運維人員監(jiān)控整個GateWay Proxy的健康狀況及API統(tǒng)計情況
網(wǎng)關(guān)技術(shù)架構(gòu)
architecture.png

說明:
1) 整個網(wǎng)關(guān)基于Netty NIO來實現(xiàn)同步非阻塞是HTTP服務(wù),網(wǎng)關(guān)是外部API請求的HTTP服務(wù)端,同時是內(nèi)部服務(wù)的客戶端,所以有Netty Server Handler和Netty Client Handler的出現(xiàn);
2)對于Netty Server Handler來說,當一個HTTP請求進來時,他會把當前連接轉(zhuǎn)化為ClientToProxyConnection,它是線程安全的,伴隨當前此HTTP請求的生命周期結(jié)束,它也負責ClientToProxyConnection的生命周期的維護;
3)對于Netty Client Handler來說,當ClientToProxyConnection需要傳遞請求到內(nèi)部服務(wù)時,會新建(或者獲取原來已建)的ProxyToServerConnection來進行內(nèi)部的請求,它也是線程安全的;
4)對于Filter來說,他運行在ClientToProxyConnection上,插入請求進來及收到后端請求之間;

網(wǎng)關(guān)技術(shù)細節(jié)
  • 網(wǎng)絡(luò)IO模型:
    網(wǎng)絡(luò)模型可以分為阻塞IO、非阻塞IO、IO復用、信號驅(qū)動IO和異步IO
    網(wǎng)絡(luò)IO操作(read/write系統(tǒng)調(diào)用)其實分成了兩個步驟:
    第一:發(fā)起IO請求 ;第二:實際的IO讀寫(內(nèi)核態(tài)與用戶態(tài)的數(shù)據(jù)拷貝)
    阻塞與非阻塞IO的區(qū)別在于第一步,發(fā)起IO請求的進程是否會被阻塞,如果阻塞直到IO操作完成才返回那么就是傳統(tǒng)的阻塞IO,如果不阻塞,那么就是非阻塞IO;
    同步IO和異步IO的區(qū)別在于第二步,實際的IO讀寫(內(nèi)核態(tài)與用戶態(tài)的數(shù)據(jù)拷貝)是否需要進程參與,如果需要進程參與則是同步IO,如果不需要進程參與就是異步IO;
    如果實際的IO讀寫需要請求進程參與,那么就是同步IO。因此阻塞IO、非阻塞IO、IO復用、信號驅(qū)動IO都是同步IO
    從以上來看HTTP請求是適合同步非阻塞的方式,一個HTTP請求必然有REQUEST和RESPONSE,而在zuul2上實現(xiàn)的真正網(wǎng)絡(luò)IO也是同步非阻塞,只不過在HttpAsyncEndpoint這個Filter執(zhí)行HTTP請求時采用了異步方式,如下:
 @Override
    public Observable<HttpResponseMessage> 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));
        }
    }

從以上分析,網(wǎng)關(guān)選擇同步非阻塞方式是一個合適的選擇

  • 協(xié)議轉(zhuǎn)換
    1) HTTP ----> gRPC
    gRPC基于protobuf來做傳輸,所以整個在gRPC的客戶端來說,必須有protobuf的數(shù)據(jù)來描述當前此次gRPC請求,而proto文件是一個合適的數(shù)據(jù)描述方式,grpc-web 也是如此來做http請求的轉(zhuǎn)換
    具體步驟是:
    1) 將proto文件轉(zhuǎn)化為protobuf的FileDescriptorSet對象,該對象描述了所有的proto內(nèi)容,而轉(zhuǎn)化也很簡單,利用protobuf提供的命令即可轉(zhuǎn)化,在這里我使用的是:
<dependency>
    <groupId>com.github.os72</groupId>
    <artifactId>protoc-jar</artifactId>
    <scope>provided</scope>
</dependency>

其中轉(zhuǎn)化的過程如下:

 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<String> protocArgs = ImmutableList.<String>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:根據(jù)FileDescriptorSet獲取gRPC的入?yún)⒑统鰠⒚枋龇?,然后再?chuàng)建gRPC所需要的MethodDescriptor方法描述對象

  • 獲取protobuf的MethodDescriptor來獲取入?yún)⒑统鰠⒌腄escriptor
 private static Pair<Descriptor, Descriptor> 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<Descriptor, Descriptor>(protoMethodDesc.getInputType(),
            protoMethodDesc.getOutputType());
      } catch (InvalidProtocolBufferException e) {
        LOG.error(e.getMessage(), e);
        throw new RuntimeException(e);
      }
    }
    return null;
  }
  • 構(gòu)建gRPC所需要的方法描述對象
private MethodDescriptor<DynamicMessage, DynamicMessage> createGrpcMethodDescriptor(
      String serviceName, String methodName, Descriptor inPutType, Descriptor outPutType) {
    String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
    //MethodDescriptor是grpc的方法描述對象
    return io.grpc.MethodDescriptor.<DynamicMessage, DynamicMessage>newBuilder()
        .setType(MethodType.UNARY)//
        .setFullMethodName(fullMethodName)//
        .setRequestMarshaller(new DynamicMessageMarshaller(inPutType))//
        .setResponseMarshaller(new DynamicMessageMarshaller(outPutType))//
        .setSafe(false)//
        .setIdempotent(false)//
        .build();
  }

2) HTTP ----> dubbo
在dubbo的框架設(shè)計中,其中已經(jīng)包含了泛化調(diào)用的設(shè)計,所以在這塊,基本上就延用了dubbo的泛化調(diào)用來實現(xiàn)http轉(zhuǎn)dubbo的協(xié)議,而關(guān)于dubbo的參數(shù)部分,可以指定參數(shù)映射規(guī)范,利用參數(shù)裁剪的技術(shù)對http請求參數(shù)進行抽取,如果dubbo的接口是java類型,則直接抽取,如果是pojo,按照dubbo的用戶文檔,把他組成一個Map的數(shù)據(jù)結(jié)構(gòu)即可,而操作這一步需要映射規(guī)則

  • 參數(shù)裁剪
    利用了JsonPath jsonPath及Freemarker模板可以實現(xiàn)參數(shù)裁剪的效果,其設(shè)計思路可以參照,amazon的做法,實現(xiàn)效果如下:
<#assign json = input.path("$")>
[
    {
      "name": "${json.name}",
      "mobile": "${json.mobile}",
      "idNo": "${json.idNo}"
    }
]
  • API規(guī)則編排
    利用了Drools的drl語言,再利用參數(shù)裁剪,實現(xiàn)API的組裝及編排
package io.github.tesla.gateway.netty.filter.drools

import io.github.tesla.gateway.netty.filter.help.BodyMapping
import io.github.tesla.gateway.netty.filter.help.HeaderMapping
import io.github.tesla.gateway.netty.filter.help.DroolsContext


declare User
    name : String
    mobile : String
    idNo : String
end

rule "condition: call userService to judge user is normal"
no-loop true
when
    $body:BodyMapping()
    $header:HeaderMapping()
    $context:DroolsContext()
then
    User user = new User();
    user.setName($body.json("$.name"));
    user.setMobile($body.json("$.mobile"));
    user.setIdNo($body.json("$.idNo"));
    String userInfo = $context.toJSONString(user);
    String returnUserInfo = $context.callService("tesla","/user",userInfo, "POST");
    User returnUser = $context.parseObject(returnUserInfo,User.class);
    insert(returnUser);
end

rule "condition: judge to jingdong or internal service"
no-loop true
when
     $user:User(name=="test",mobile=="123")
     $context:DroolsContext()
then
     $context.setResponse("www.baidu.com");
end
結(jié)束語

整個網(wǎng)關(guān)目前基本完成并且也開源到GitHub上,歡迎拍磚及使用
tesla

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容