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 https://blog.csdn.net/king866/article/details/54174665
杨中科,.NET Core 微服务课程: Thrift 高效通讯 https://pan.baidu.com/s/1rSJgOdNEgo3mhPNqmyXrRw
醉眼识朦胧,Thrift 入门初探 --thrift 安装及 java 入门实例
focus-lei,.net core 下使用 Thrift
宝哥在路上,Thrift 性能测试与分析 https://blog.csdn.net/code52/article/details/21015361
来源: https://www.cnblogs.com/edisonchou/p/microservice_communication_rest_and_rpc_foundation.html