1. 程式人生 > >.NET Core微服務之服務間的呼叫方式(REST and RPC)

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

一、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。

參考資料

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

.NET Core服務服務呼叫方式REST and RPC

一、REST or RPC ? 1.1 REST & RPC   微服務之間的介面呼叫通常包含兩個部分,序列化和通訊協議。常見的序列化協議包括json、xml、hession、protobuf、thrift、text、bytes等;通訊比較流行的是http、soap、websockect,RP

.NET Core 3.0深入原始碼理解ObjectPool

寫在前面 物件池是一種比較常用的提高系統性能的軟體設計模式,它維護了一系列相關物件列表的容器物件,這些物件可以隨時重複使用,物件池節省了頻繁建立物件的開銷。 它使用取用/歸還-重複取用的操作模式,如下圖所示: 本文將主要介紹物件池的基本概念、物件池的優勢及其工作機制,下一篇文件將從原始碼角度介紹.NET

.NET Core 3.0深入原始碼理解ObjectPool

寫在前面 前文主要介紹了ObjectPool的一些理論基礎,本文主要從原始碼角度理解Microsoft.Extensions.ObjectPool是如何實現的。下圖為其三大核心元件圖: 核心元件 ObjectPool ObjectPool是一個泛型抽象類,裡面只有兩個抽象方法,Get和Return。它從底

.NET Core 3.0深入原始碼理解HealthCheck

寫在前面 我們的系統可能因為正在部署、服務異常終止或者其他問題導致系統處於非健康狀態,這個時候我們需要知道系統的健康狀況,而健康檢查可以幫助我們快速確定系統是否處於正常狀態。一般情況下,我們會提供公開的HTTP介面,用於專門化健康檢查。 NET Core提供的健康檢查庫包括Microsoft.Extensio

.NET Core 3.1深入原始碼理解HealthCheck

寫在前面前文討論了HealthCheck的理論部分,本文將討論有關HealthCheck的應用內容。可以監視記憶體、磁碟和其他物理伺服器資源的使用情況來了解是否處於正常狀態。執行狀況檢查可以測試應用的依賴項(如資料庫和外部服務終結點)以確認是否可用和正常工作。執行狀況探測可以由容器業務流程協調程式和負載均衡器

ASP.NET Core 簡單實現七牛圖片上傳FormData 和 Base64

private stream public 圖片 ASP.NET Core 簡單實現七牛圖片上傳(FormData 和 Base64)七牛圖片上傳 SDK(.NET 版本):https://developer.qiniu.com/kodo/sdk/1237/csharpUpoladServic

同“窗”的較量:部署在 Windows 上的 .NET Core 版部落格站點發布上線已暫時下線

為了驗證 docker swarm 在高併發下的效能問題,週一我們釋出了使用 docker-compose 部署的 .net core 版部落格站點(博文連結),但由於有1行程式碼請求後端 web api 時沒有使用快取,結果造成大量 web api 請求發向跑後端服務的叢集,悲劇的是這個叢集是用 docke

STL劃分與排序演算法Partions and Sorting

目錄           STL之劃分與排序演算法(Partions and Sorting) 劃分演算法 一、is_partitoned 二、partition 三、partition_copy(beg,

C#LeetCode刷題#771-寶石與石頭Jewels and Stones

問題 給定字串J 代表石頭中寶石的型別,和字串 S代表你擁有的石頭。 S 中每個字元代表了一種你擁有的石頭的型別,你想知道你擁有的石頭中有多少是寶石。 J 中的字母不重複,J 和 S中的所有字元都是字母。字母區分大小寫,因此"a"和"A"是不同型別的石頭。 輸入: J

.NET Core服務開發服務呼叫篇-GRPC

在單體應用中,相互呼叫都是在一個程序內部呼叫,也就是說呼叫發生在本機內部,因此也被叫做本地方法呼叫;在微服務中,服務之間呼叫就變得比較複雜,需要跨網路呼叫,他們之間的呼叫相對於與本地方法呼叫,可稱為遠端過程呼叫,簡稱RPC(Remote procedure call)。 看過上篇API閘道器篇,知道案例中包含

.NET Core服務基於Consul實現服務治理

請求轉發 1.0 asp.net AC port prefix 我們 tle nan 一、Consul基礎介紹   Consul是HashiCorp公司推出的開源工具,用於實現分布式系統的服務發現與配置。與其他分布式服務註冊與發現的方案,比如 Airbnb的Smart

.NET Core服務基於Consul實現服務治理

shell pla code tst 分層 編輯 set req \n 上一篇發布之後,這一篇把上一篇沒有弄到的東西補一下,也算是給各位前來詢問的朋友的一些回復吧。一、Consul服務註冊之配置文件方式1.1 重溫Consul實驗集群  這裏我們有三個Consul Serv

.net core 服務Api閘道器Api Gateway

微服務閘道器目錄 1、 微服務引子 2、使用Nginx作為api閘道器 3、自創api閘道器(重複輪子) 3.1、構建初始化 3.2、構建中介軟體 4、結語

基於Apollo實現.NET Core服務統一配置(測試環境-單機) .NET Core服務基於Apollo實現統一配置中心

一、前言 注:此篇只是為測試環境下的快速入門。後續會給大家帶來生產環境下得實戰開發。 具體的大家可以去看官方推薦。非常的簡單明瞭。以下介紹引用官方內容: Apollo(阿波羅)是攜程框架部門研發的分散式配置中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具

.NET Core服務基於Steeltoe使用Eureka實現服務註冊與發現

一、關於Steeltoe與Spring Cloud    Steeltoe is an open source project that enables .NET developers to implement industry standard best practices when b

.NET Core服務基於Steeltoe整合Zuul實現統一API閘道器

一、關於Spring Cloud Zuul   API Gateway(API GW / API 閘道器),顧名思義,是出現在系統邊界上的一個面向API的、序列集中式的強管控服務,這裡的邊界是企業IT系統的邊界。   Zuul 是Netflix 提供的一個開源元件,致力於在雲平臺上提供動態路由,監

.NET Core服務基於Steeltoe使用Hystrix熔斷保護與監控

一、關於Spring Cloud Hystrix      在微服務架構中,我們將系統拆分為很多個服務,各個服務之間通過註冊與訂閱的方式相互依賴,由於各個服務都是在各自的程序中執行,就有可能由於網路原因或者服務自身的問題導致呼叫故障或延遲,隨著服務的積壓,可能會導致服務崩潰。為了解決這一系列的問題

.NET Core服務基於Steeltoe使用Spring Cloud Config統一管理配置

一、關於Spring Cloud Config   在分散式系統中,每一個功能模組都能拆分成一個獨立的服務,一次請求的完成,可能會呼叫很多個服務協調來完成,為了方便服務配置檔案統一管理,更易於部署、維護,所以就需要分散式配置中心元件了,在Spring Cloud中,就有這麼一個分散式配置中心元件 —

NET Core服務路:自己動手實現Rpc服務框架,基於DotEasy.Rpc服務框架的介紹和整合

本篇內容屬於非實用性(拿來即用)介紹,如對框架設計沒興趣的朋友,請略過。   快一個月沒有寫博文了,最近忙著兩件事;    一:閱讀劉墉先生的《說話的魅力》,以一種微妙的,你我大家都會經常遇見的事物,來建議說話的“藝術和魅力”,對於我們從事軟體開發、不太善於溝通

.NET Core服務路:利用DotNetty實現一個簡單的通訊過程

  上一篇我們已經全面的介紹過《基於gRPC服務發現與服務治理的方案》,我們先複習一下RPC的呼叫過程(筆者會在這一節的幾篇文章中反覆的強調這個過程呼叫方案),看下圖