代码地址:https://github.com/HDRorz/WorkDayProject
这个 grpc 在 20 年.net core 3.1 release 时就看到官方最新鲜的文档,就着手开始开发了,依旧以工作日服务这个功能实现了服务器端和客户端。然而开发过程并不是一帆风顺的。
protobuf 默认定义的数据类型中并不包含时间,这令人略感意外,这就需要另外进行处理,引入时间类型,这需要在 proto 文件增加一行引用。
import "google/protobuf/timestamp.proto";
这又带来一个新问题,vs 无法编译这个 proto 文件,在通过一连串莫名其妙的操作后(其实是我已经忘了),项目终于恢复正常。
当然这个时间还会有一个新的问题:时区。.net 内 DateTime 默认自带一个 local 的时区,比如说中国是 +8,这个在转换为 timestamp 时,Timestamp.FromDateTime 函数只能接收 UTC 时间,所以只能 ToUniversalTime() 转为 UTC 时间,或者使用 Timestamp.FromDateTimeOffset 函数自动转为 UTC 时间。
//服务器端
return Task.FromResult(new DateTimeReply
{
Date = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTimeHelper.GetCurrWorkDay().ToUniversalTime())
//Date = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeHelper.GetCurrWorkDay())
});
//客户端
var localOffset = DateTimeOffset.Now.Offset;
var replyTask = workDayClient.GetCurrWorkDayAsync(new EmptyRequest(), callOptions);
replyTask.ResponseAsync.Wait();
var workDayReply = new DateTimeOffset(replyTask.ResponseAsync.Result.Date.ToDateTime(), localOffset);
最后在调试中还产生了 HTTPS 协议的问题,因为 asp.net core 的 grpc 是 over HTTP 的,官方建议使用HTTP/2 + HTTPS。实际开发中,我配置了一个自签名的证书,客户端却一直报错,通过开启 grpc 组件内部日志后发现在创建连接时一直产生一个协议识别错误的报警。
//服务端开启日志
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(configureOptions => {
configureOptions.EnableDetailedErrors = true;
});
}
//客户端报错Exception
System.AggregateException: One or more errors occurred. (Status(StatusCode=Internal, Detail="Error starting gRPC call: An error occurred while sending the request."))
---> Grpc.Core.RpcException: Status(StatusCode=Internal, Detail="Error starting gRPC call: An error occurred while sending the request.")
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.Wait()
at WorkDayDotNetGrpc.Client.Program.GrpcCall() in E:\otherproject\WorkDayProject\WorkDayDotNetGrpc.Client\Program.cs:line 88
经过多番尝试和搜索后发现问题的解决方案:需要在客户端对 HTTP/2、HTTPS进行配置,当然也有可能是 win7 的问题(对 HTTP/2 支持不足),同时关闭服务器端 HTTPS。
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true);
这一部分则是测试,因为 grpc over HTTP 并不是常规 HTTP 协议下的 webapi,一些常规的性能测试工具如 apache banchmark、wrk 等工具都无法使用。这里选用了之前二进制测试 .net.tcp 的工具 apache jmeter。
通过抓包发现,grpc 通讯时的二进制难以找到规则,但是我们还可以配置 Java Request,通过继承 org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient,造一个 jar 包塞入 lib\ext 目录下。代码参考了这个项目。
public class WorkDaySampler extends AbstractJavaSamplerClient {
WorkDayClient bclient = null;
@Override
public void setupTest(JavaSamplerContext context){
String host = context.getParameter("host");
String port = context.getParameter("port");
this.bclient = new WorkDayClient(host, Integer.parseInt(port));
super.setupTest(context);
}
@Override
public Arguments getDefaultParameters() {
Arguments defaultParameters = new Arguments();
defaultParameters.addArgument("host", "127.0.0.1");
defaultParameters.addArgument("port", "80");
return defaultParameters;
}
@Override
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
SampleResult result = new SampleResult();
boolean success = true;
LocalDateTime response;
result.sampleStart();
try {
for (int i = 0; i < 1; i++) {
response=bclient.getCurrWorkDay();
}
response=bclient.getCurrWorkDay();
result.sampleEnd();
result.setSuccessful(success);
result.setResponseData(response.toString().getBytes());
result.setResponseMessage( "Successfully performed getCurrWorkDay");
result.setResponseCodeOK(); // 200 code
}
catch (StatusRuntimeException e){
result.sampleEnd(); // stop stopwatch
result.setSuccessful(false);
result.setResponseMessage("Exception: " + e);
success = false;
result.setSuccessful(success);
// get stack trace as a String to return as document data
java.io.StringWriter stringWriter = new java.io.StringWriter();
e.printStackTrace( new java.io.PrintWriter(stringWriter));
result.setResponseData(stringWriter.toString().getBytes());
result.setDataType( org.apache.jmeter.samplers.SampleResult.TEXT );
result.setResponseCode("500");
}
return result;
}
}
然后作者测试了短连接(每个连接调用1次接口)和长连接(每个连接调用1000次接口)的差别。短连接 qps 为 15356.3,长连接则为 21.5(实际约 21500),可见长连接依旧具有优势,可是短连接相比 webapi 看起来则并没有优势(这科学吗?)(有可能测试客户端(开发机)到服务器端的时延造成了这个问题,先前测试时使用的同网段的 2 台服务器)。
通过 dotnet trace 工具导出的日志文件分析,97.5%的 cpu 损耗在 unmanaged code 上,说明确实已经极限了,分析还发现 FileSystemWatch 这部分占用了 10% 的cpu,SemaphoreSlim.Wait(Microsoft.Extensions.Logging.Console模块) 也占用了 10%。
短连接:
长连接(调用1000次):