.NET Core微服務之服務間的調用方式(REST and RPC)


Tip: 此篇已加入.NET Core微服務基礎系列文章索引

一、REST or RPC ?

1.1 REST & RPC

  微服務之間的接口調用通常包含兩個部分,序列化和通信協議。常見的序列化協議包括json、xml、hession、protobuf、thrift、text、bytes等;通信比較流行的是http、soap、websockect,RPC通常基於TCP實現,常用框架例如dubbo,netty、mina、thrift。

  REST:嚴格意義上說接口很規范,操作對象即為資源,對資源的四種操作(post、get、put、delete),並且參數都放在URL上,但是不嚴格的說Http+json、Http+xml,常見的http api都可以稱為Rest接口。

  RPC:即我們常說的遠程過程調用,就是像調用本地方法一樣調用遠程方法,通信協議大多采用二進制方式。

1.2 HTTP vs 高性能二進制協議

  HTTP相對更規范,更標准,更通用,無論哪種語言都支持HTTP協議。如果你是對外開放API,例如開放平台,外部的編程語言多種多樣,你無法拒絕對每種語言的支持,相應的,如果采用HTTP,無疑在你實現SDK之前,支持了所有語言,所以,現在開源中間件,基本最先支持的幾個協議都包含RESTful。

  RPC協議性能要高的多,例如Protobuf、Thrift、Kyro等,(如果算上序列化)吞吐量大概能達到http的二倍。響應時間也更為出色。千萬不要小看這點性能損耗,公認的,微服務做的比較好的,例如,netflix、阿里,曾經都傳出過為了提升性能而合並服務。如果是交付型的項目,性能更為重要,因為你賣給客戶往往靠的就是性能上微弱的優勢。

  所以,最佳實踐一般是對外REST,對內RPC,但是追求極致的性能會消耗很多額外的成本,所以一般情況下對內一般也REST,但對於個別性能要求較高的接口使用RPC。

二、案例結構

  這里假設有兩個服務,一個ClinetService和一個PaymentService,其中PaymentService有兩部分,一部分是基於REST風格的WebApi部分,它主要是負責一些對性能沒有要求的查詢服務,另一部分是基於TCP的RPC Server,它主要是負責一些對性能要求高的服務,比如支付和支出等涉及到錢的接口。假設User在消費ClientService時需要調用PaymentService根據客戶賬戶獲取Payment History(走REST)以及進行交易事務操作(走RPC)。

三、REST調用

3.1 一個好用的REST Client : WebApiClient

  使用過Java Feign Client的人都知道,一個好的聲明式REST客戶端可以幫我們省不少力。在.NET下,園子里的大大老九就寫了一款類似於Feign Client的REST Client:WebApiClient。WebApiClient是開源在github上的一個httpClient客戶端庫,內部基於HttpClient開發,是一個只需要定義C#接口(interface),並打上相關特性,即可異步調用http-api的框架 ,支持.net framework4.5+、netcoreapp2.0和netstandard2.0。它的GitHub地址是:https://github.com/dotnetcore/WebApiClient

  如何安裝?

NuGet>Install-Package WebApiClient-JIT  

3.2 使用實例:走API Gateway

  Step1.定義HTTP接口

    [HttpHost("http://yourgateway:5000")]
    public interface IPaymentWebApi: IHttpApi
    {
        // GET api/paymentservice/history/edisonzhou
        // Return 原始string內容
        [HttpGet("/api/paymentservice/history/{account}")]
        ITask<IList<string>> GetPaymentHistoryByAccountAsync(string account);
    }

  這里需要注意的是,由於我們要走API網關,所以這里定義的HttpHost地址是一個假的,后面具體調用時會覆蓋掉,當然你也可以直接把地址寫在這里,不過我更傾向於寫到配置文件中,然后把這里的HttpHost設置注釋掉。

  Step2.在Controller中即可異步調用:

    [Route("api/[controller]")]
    public class PaymentController : Controller
    {
        private readonly string gatewayUrl;public PaymentController(IConfiguration _configuration)
        {
            gatewayUrl = _configuration["Gateway:Uri"];
        }

        [HttpGet("{account}")]
        public async Task<IList<string>> Get(string account)
        {
            using (var client = HttpApiClient.Create<IPaymentWebApi>(gatewayUrl))
            {
                var historyList = await client.GetPaymentHistoryByAccountAsync(account);
                // other business logic code here
                // ......
                return historyList;
            }
        }
  }

  當然你也可以在Service啟動時注入一個單例的IPaymentServiceWebApi實例,然后直接在各個Controller中直接使用,這樣更加類似於Feign Client的用法:

  (1)StartUp類注入

    public void ConfigureServices(IServiceCollection services)
    {

        // IoC - WebApiClient
        services.AddSingleton(HttpApiClient.Create<IPaymentServiceWebApi>(Configuration["PaymentService:Url"]));

    }

  (2)Controller中直接使用

    [HttpPost]
    public async Task<string> Post([FromBody]ModelType model, [FromServices]IPaymentServiceWebApi restClient)
    {
        ......
        var result = await restClient.Save(model);
        ......
    }

  這里PaymentService的實現很簡單,就是返回了一個String集合:

    // GET api/history/{account}
    [HttpGet("{account}")]
    public IList<string> Get(string account)
    {
        // some database logic
        // ......
        IList<string> historyList = new List<string>
        {
            "2018-06-10,10000RMB,Chengdu",
            "2018-06-11,11000RMB,Chengdu",
            "2018-06-12,12000RMB,Beijing",
            "2018-06-13,10030RMB,Chengdu",
            "2018-06-20,10400RMB,HongKong"
        };

        return historyList;
    }

  最終調用結果如下:

  

3.3 使用實例:直接訪問具體服務

  在服務眾多,且單個服務就部署了多個實例的情況下,我們可以通過API網關進行中轉,但是當部分場景我們不需要通過API網關進行中轉的時候,比如:性能要求較高,負載壓力較小單個實例足夠等,我們可以直接與要通信的服務進行聯接,也就不用從API網關繞一圈。

  Step1.改一下HTTP接口:

    [HttpHost("http://paymentservice:8880")]
    public interface IPaymentDirectWebApi: IHttpApi
    {
        // GET api/paymentservice/history/edisonzhou
        // Return 原始string內容
        [HttpGet("/api/history/{account}")]
        ITask<IList<string>> GetPaymentHistoryByAccountAsync(string account);
    }

  同理,這里的HttpHost也是后面需要被覆蓋的,原因是我們將其配置到了配置文件中。

  Step2.改一下調用代碼:

    [Route("api/[controller]")]
    public class PaymentController : Controller
    {
        private readonly string gatewayUrl;
        private readonly string paymentServiceUrl;

        public PaymentController(IConfiguration _configuration)
        {
            gatewayUrl = _configuration["Gateway:Uri"];
            paymentServiceUrl = _configuration["PaymentService:Uri"];
        }

        [HttpGet("{account}")]
        public async Task<IList<string>> Get(string account)
        {
            #region v2 directly call PaymentService
            using (var client = HttpApiClient.Create<IPaymentDirectWebApi>(paymentServiceUrl))
            {
                var historyList = await client.GetPaymentHistoryByAccountAsync(account);
                // other business logic code here
                // ......
                return historyList;
            }
            #endregion
        }

  最終調用結果如下:

  

四、RPC調用

4.1 Thrift簡介

  

  Thrift是一個軟件框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Go,Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務。

  當然,還有gRPC也可以選擇,不過從網上的性能測試來看,Thrift性能應該優於gRPC 2倍以上,但是gRPC的文檔方面要比Thrift友好很多。

4.2 Thrift的使用

  (1)下載Thrift (這里選擇Windows版)

  

  下載完成后解壓,這里我將其改名為thrift.exe(去掉了版本號),一會在命令行敲起來更方便一點。

  (2)編寫一個PaymentService.thrift,這是一個IDL中間語言

namespace csharp Manulife.DNC.MSAD.Contracts

service PaymentService { 
    TrxnResult Save(1:TrxnRecord trxn) 
}

enum TrxnResult { 
    SUCCESS = 0, 
    FAILED = 1, 
}

struct TrxnRecord { 
    1: required i64 TrxnId; 
    2: required string TrxnName; 
    3: required i32 TrxnAmount; 
    4: required string TrxnType; 
    5: optional string Remark; 
}

  (3)根據thrift語法規則生成C#代碼

cmd>thrift.exe -gen csharp PaymentService.thrift

  

  (4)創建一個Contracts類庫項目,將生成的C#代碼放進去

  

4.3 增加RPC Server

  (1)新增一個控制台項目,作為我們的Payment Service RPC Server,並引用Contracts類庫項目

  

  (2)引入thrift-netcore包:

NuGet>Install-Package apache-thrift-netcore

  (3)加入一個新增的PaymentService實現類

    public class PaymentServiceImpl : Manulife.DNC.MSAD.Contracts.PaymentService.Iface
    {
        public TrxnResult Save(TrxnRecord trxn)
        {
            // some business logic here
            //Thread.Sleep(1000 * 1);
            Console.WriteLine("Log : TrxnName:{0}, TrxnAmount:{1}, Remark:{2}", trxn.TrxnName, trxn.TrxnAmount, trxn.Remark);
            return TrxnResult.SUCCESS;
        }
    }

  這里輸出日志僅僅是為了測試。

  (4)編寫啟動RPC Server的主程序

    public class Program
    {
        private const int port = 8885;

        public static void Main(string[] args)
        {
            Console.WriteLine("[Welcome] PaymentService RPC Server is lanuched...");
            TServerTransport transport = new TServerSocket(port);
            var processor = new Manulife.DNC.MSAD.Contracts.PaymentService.Processor(new PaymentServiceImpl());
            TServer server = new TThreadedServer(processor, transport);
            // lanuch
            server.Serve();
        }
    }

  (5)如果是多個服務實現的話,也可以如下這樣啟動:

    public static void Main(string[] args)
    {
        Console.WriteLine("[Welcome] PaymentService RPC Server is lanuched...");
        TServerTransport transport = new TServerSocket(port);
        var processor1 = new Manulife.DNC.MSAD.Contracts.PaymentService.Processor(new PaymentServiceImpl());
        var processor2 = new Manulife.DNC.MSAD.Contracts.PayoutService.Processor(new PayoutServiceImpl());
        var processorMulti = new Thrift.Protocol.TMultiplexedProcessor();
        processorMulti.RegisterProcessor("Service1", processor1);
        processorMulti.RegisterProcessor("Service2", processor2);
        TServer server = new TThreadedServer(processorMulti, transport);
        // lanuch
        server.Serve();
    } 

4.4 調用RPC

  在ClientService中也引入apache-thrift-netcore包,然后在調用的地方修改如下:

    [HttpPost]
    public string Post([FromBody]TrxnRecordDTO trxnRecordDto)
    {
        // RPC - use Thrift
        using (TTransport transport = new TSocket(
            configuration["PaymentService:RpcIP"], 
            Convert.ToInt32(configuration["PaymentService:RpcPort"])))
        {
            using (TProtocol protocol = new TBinaryProtocol(transport))
            {
                using (var serviceClient = new PaymentService.Client(protocol))
                {
                    transport.Open();
                    TrxnRecord record = new TrxnRecord
                    {
                        TrxnId = GenerateTrxnId(), 
                        TrxnName = trxnRecordDto.TrxnName,
                        TrxnAmount = trxnRecordDto.TrxnAmount,
                        TrxnType = trxnRecordDto.TrxnType,
                        Remark = trxnRecordDto.Remark
                    };
                    var result = serviceClient.Save(record);

                    return Convert.ToInt32(result) == 0 ? "Trxn Success" : "Trxn Failed";
                }
            }
        }
    }

    private long GenerateTrxnId()
    {
        return 10000001;
    }

  最終測試結果如下:

  

五、小結

  本篇簡單的介紹了下微服務架構下服務之間調用的兩種常用方式:REST與RPC,另外前面介紹的基於消息隊列的發布/訂閱模式也是服務通信的方式之一。本篇基於WebApiClient這個開源庫介紹了如何進行聲明式的REST調用,以及Thrift這個RPC框架介紹了如何進行RPC的通信,最后通過一個小例子來結尾。最后,服務調用的最佳實踐一般是對外REST,對內RPC,但是追求極致的性能會消耗很多額外的成本,所以一般情況下對內一般也REST,但對於個別性能要求較高的接口使用RPC。

參考資料

遠方的行者,《微服務 RPC和REST

楊中科,《.NET Core微服務課程:Thrift高效通訊

醉眼識朦朧,《Thrift入門初探--thrift安裝及java入門實例

focus-lei,《.net core下使用Thrift

寶哥在路上,《Thrift性能測試與分析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM