gRPC大数据量消息传递方法 - Go语言中文社区

gRPC大数据量消息传递方法


1.摘要

本文探讨了gRPC中大数据量消息的传输限制及相应的两个解决方法:修改限制值大小和流式数据传输,并给出了gRPC C++版本下采用流式数据传输的示例代码,在该示例中同时说明了如何在Visual Studio下进行proto文件编写、编译以及gRPC项目的配置方法。

2.简介

在项目的实施过程中,给导师提出了使用gRPC构建微服务的方案,这方面我们并没有任何经验,也没有有经验的师兄和老师指导,一切都是摸着石头过河。今天在和项目参与人员讨论服务对接的过程中,突然讨论到:gRPC不支持大数据量的消息传递,并且官方也说如果是涉及到大量数据的交互的服务,建议采用其他的方案。这真是一个致命的问题,之前在方案选型的时候还真是没有注意到说大数据量的问题,看到的关于gRPC等多种RPC服务框架对比的帖子中都说protocol buffer的序列化能大大减少数据量,提高数据的传输效率,但现在看来似乎不太科学啊。于是花了些时间对gRPC传输大数据量数据的消息的情况做了调查和测试。

gRPC默认的消息传输大小为#define GRPC_DEFAULT_MAX_RECV_MESSAGE_LENGTH (4 * 1024 * 1024) byte,也就是4M大小。比如:传递一个数组,数组的元素如果是double,因为double是8byte,所以你传递的数据数组长度最大就只能是1024*512,如果大于该大小,则会提示:Error: grpc: received message larger than max (XXXXX vs. 4194304) 这个XXXXX就是你的真实数据大小。这个只是提示,并不会在编译的时候报错,而是客户端在发起请求的时候通过status的值提示。做这样的限制并不是说gRPC对大数据量传输不支持,而是开发团队认为这样可以提醒gRPC使用者考虑程序中会进行大数据量的传输问题。

4M这个数据量的确太小了,不能满足大多数的RPC服务调用情况,那么有什么办法提升这个数据传输量呢?这里有两个方法:1、在ServerBuilder中,通过SetMaxReceiveMessageSize(int)设置这个最大允许字节长度,因为这里的参数为Int型,所以其最大的字节允许长度也就是INT_MAX=2147483647 (2G)。可能就有朋友看到这里就会问了:谷歌的工程师难道就不能将这个参数设置其他类型吗?unsigned long long之类的多好啊,能提供更大的传输量许可。但是仔细考虑下,这个大小已经很大了,抛开数据在传输中的时间代价,考虑gRPC接收到消息数据后gRPC都是直接在内存中保存数据,也就意味着数据接收到后至少也用掉2G的内存,你的服务单接收数据就使用了2G,算是十分消耗资源了,所以gRPC的开发团队认为没必要再提供更高的数据传输量阈值,如果实在需要更大量的数据传输,那么就是你的算法需要改进了,或可以采用流式处理。所以这里就有了第二个大数据量传输方案。2、采用流式传输。在查找资料时,发现关于gRPC流式传输的帖子很少,采用gRPC C++的更是几乎没看到,所以本文后续的测试采用gRPC C++进行编写,java版本的可参考[5],go版本的可以参考[4]。

3.gRPC流式传输示例

3.1编写proto文件(calculater.proto)

syntax = "proto3";
import "CommonData.proto";
package calc;

service Caltulator {
	rpc LargeDataSetStream(stream Line3D) returns (Response){}
}
message Response {
    int32 sum = 1;
}
message Point3D{
	double	x=1;
	double	y=2;
	double	z=3;
}
message Line3D{
	repeated Point3D	points=1;
}

该rpc服务中通过stream关键字来标识了传入的数据采用的是流式传输,其他的服务消息定义则和普通的定义方式没有区别。

3.2编译proto文件

本示例中使用的编辑平台是Visual Studio,可以在VS的【工具】à【扩展和更新】中安装Protobuf Language Service扩展,该语言服务能提供对proto文件的语法高亮和错误检查,十分方便。

对于已经编写好的proto文件需要定义编译方式以完成服务端和客户端可以使用的C++类文件的生成。首先在【解决方案】列表的proto文件上点击右键à【属性】à【常规】à【项类型】,选择【自定义生成工具】,并点击应用。之后【常规】下面会多出【自定义生成工具】设置,在【自定义生成工具】à【常规】中,命令行写入:protoc --cpp_out=. calculater.proto protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe calculater.proto;然后在【输出】栏中写入:none,如果此处不写入东西的话,编译的时候会因为没有输出而跳过该文件的编译,之后点击确定即可。同时将gRPC和protocol buffer的代码生成工具:protoc.exe和grpc_cpp_plugin.exe两个exe,拷贝到项目文件夹下(就是.vcxproj文件所在的文件夹)。然后编译该项目或者在解决方案文件列表中的proto文件上点击右键选择【编译】,即可完成proto文件的编译。编译后生成的代码在项目文件夹下,但不会被自动加载到项目中,所以自己根据情况手动加载到需要的项目中。

3.3服务端代码

新建一个win32控制台程序,在新建项目的时候,需要:

  1. 项目设置项中去掉“生命周期检查”和“预编译头”的勾选状态(具体名字我忘了,大致是类似的两个选项)。
  2. 在项目【属性】à【配置属性】à【C/C++】à【预处理器】à【预处理器定义】栏中添加_WIN32_WINNT=0x600定义。
  3. 项目中需要链接的gRPC及其依赖的库包括:grpc.lib、grpc++.lib、gpr.lib、libprotobufd.lib、zlibstaticd.lib、ssl.lib、crypto.lib,同时需要链接winsocket2的库文件ws2_32.lib
  4. 需要引用的头文件主要是gRPC和protocol buffer的头文件。
  5. 加载生成的gRPC代码,重写LargeDataSetStream 函数。

服务实现代码为:

class CalcualtorService:public calc::Caltulator::Service
{
public:	
	virtual ::grpc::Status LargeDataSetStream(::grpc::ServerContext* context, ::grpc::ServerReader<::calc::Line3D>* reader, ::calc::Response* response) {
		unsigned long long numID=0;
		system_clock::time_point start_time = system_clock::now();

                ::calc::Line3D oneLine;
		while (reader->Read(&oneLine))
		{
			::google::protobuf::RepeatedPtrField< ::calc::Point3D > points=oneLine.points();
			for (auto& onePoint : points)
			{
				std::cout << ++numID << ": " << onePoint.x() << "t" << onePoint.y() << "t" << onePoint.z() << std::endl;
			}
		}
                system_clock::time_point end_time = system_clock::now();
		auto secs = std::chrono::duration_cast<std::chrono::seconds>(
			end_time - start_time);
		response->set_sum(secs.count());

		std::cout <<"耗时(s):"<<secs.count()<< std::endl;	
	        return grpc::Status::OK;	
};

服务的启动代码为:

int main()
{
	std::string serverAddr("0.0.0.0:8000");

	CalcualtorService service;
	grpc::ServerBuilder builder;
	//builder.SetMaxReceiveMessageSize(INT_MAX);
	builder.AddListeningPort(serverAddr, grpc::InsecureServerCredentials());
	builder.RegisterService(&service);

	std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
	std::cout << "Server listening on " << serverAddr << std::endl;
	server->Wait();
    return 0;
}

3.4客户端代码

新建一个win32项目,设置和配置见3.3。

客户端服务调用代码:

#include "stdafx.h"
#include <memory>
#include "grpc++/grpc++.h"
#include "calculater.grpc.pb.h"

class Client {
public:
	Client(std::shared_ptr<grpc::Channel> channal)
		:stub_(calc::Caltulator::NewStub(channal)){}
	
	void LargeDatasetTest() {
		std::vector<::calc::Line3D*> dataSet;
		int numberOfData = 1024;
		int pointSize = 102400;
		dataSet.reserve(numberOfData);

		for (int i = 0; i < numberOfData; i++)
		{
			::calc::Line3D* oneLine =new ::calc::Line3D;
			for (int k = 0; k < pointSize; k++)
			{
				::calc::Point3D* onePoint = oneLine->add_points();
				onePoint->set_x(1.0);
				onePoint->set_y(2.0);
				onePoint->set_z(3.0);
			}
			dataSet.push_back(oneLine);
		}

		//模拟大体量数据完毕
		calc::Response response;
		grpc::ClientContext context;
		std::unique_ptr< ::grpc::ClientWriter< ::calc::Line3D>> clientStreamWriter=stub_->LargeDataSetStream(&context, &response);

		for (::calc::Line3D* oneLine : dataSet)
		{			
			if(!clientStreamWriter->Write(*oneLine))
				break;	
                }
		clientStreamWriter->WritesDone();
		grpc::Status status = clientStreamWriter->Finish();

		if (status.ok()) {
			std::cout << "数据传输完毕" << std::endl;
			std::cout << "消耗时间为:" <<response.sum()<< std::endl;
		}
		else {
			std::cout << "数据传输失败" << std::endl;
		}	return ;		
	}

private:
	std::unique_ptr<calc::Caltulator::Stub> stub_;
};

客户端启动代码:

int main()
{
	Client clinet(grpc::CreateChannel(
	"192.168.3.91:8000",grpc::InsecureChannelCredentials()));
	clinet.LargeDatasetTest();
    return 0;
}

服务端和客户端启动,服务端接收到数据效果如下:

4.总结与讨论

通过本文的测试可以看到gRPC是可以进行大数据量消息的传输的,当然这里没有去探讨这个传输的效率,不过PB对数据的序列化压缩率很高,如果效率出现问题,那应该是在反序列化阶段,这需要进一步测试才能知道答案。其实在和同学讨论中,我们也提到了第三个解决方案,采用其他的数据传输方案(如:将数据序列化为其他形式并传输),但是在对gRPC和Protocol Buffer进一步了解后,这个方案我感觉可以否定(特别是以json来传输的提议,窃以为应该直接否掉,数据量大后,json这种松散方式数据大小更大,且对象过多,在应用的数据处理中操作更耗时)。无论采用哪种数据序列化方式,在没有考虑额外的压缩的情况下,传输过程中其实只是对数据实体的传输,数据原始是什么就是什么,是三个double构成的对象就是传三个double值(3*8byte),反序列化的时候再用接收到的值构建一个新的对象,所以不外乎就是对象à序列化à供传输的数据à反序列化à对象的过程,那么采用其他的数据传输方式就有两个问题了:不见得有PB高效且框架复杂性增加。就目前看来,采用gRPC提供的解决方案已经够用了。

参考资料

  1. https://developers.google.com/protocol-buffers/docs/techniques
  2. https://github.com/grpc/grpc/issues/7882
  3. https://nanxiao.me/en/message-length-setting-in-grpc/
  4. https://jbrandhorst.com/post/grpc-binary-blob-stream/
  5. https://blog.csdn.net/m0_37595562/article/details/80646099
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/l491453302/article/details/81904067
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 21:55:30
  • 阅读 ( 2314 )
  • 分类:大数据

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢