记录core中GRPC长连接导致负载均衡不均衡问题 二,解决长连接问题
题外话:
1.这几天收到蔚来的面试邀请,但是自己没做准备,并且远程面试,还在上班时间,再加上老东家对我还不错.没想着换工作,导致在自己工位上做算法题不想被人看见,然后非常紧张.估计over了.不过没事,接下来知道哪里不足补哪里继续我的grpc源码解析
2.上期的博客,记录了grpc源码及创建grpc的过程,其实说到底就是围绕GrpcChannel,通过httpclient做长连接处理这次来分析下,具体的实现规律
3.直接上github地址:https://github.com/BestHYC/GRPCHelper
4.大家还是得多做题,不然面试都过不去,都不会看你代码。
一:查看创建HttpClient的源码

public HttpClient CreateClient(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
} HttpMessageHandler handler = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
var client = new HttpClient(handler, disposeHandler: false); HttpClientFactoryOptions options = _optionsMonitor.Get(name);
for (int i = 0; i < options.HttpClientActions.Count; i++)
{
options.HttpClientActions[i](client);
} return client;
}
可以看见,在处理HttpName的时候,通过GetOrAdd来提供HttpClient,那么可以得到一个事实就是,其实AddHttpClient(name),只不过用来标识,其实底层没做特别处理,即使我仅仅AddHttpClient(),在创建HttpClient的时候使用CreateClient("AA"),另外一个地方同样使用CreateClient("AA"),这两个HttpClient在未dispose情况下,还是会共用一个句柄
二:查看Grpc中HttpClinet使用场景
也是查看DefaultGrpcClientFactory创建中var httpClient = _httpClientFactory.CreateClient(name);而由前面源码可知,name可以当成同一个Grpc客户端名称。那么得到,
同一个GrpcClient共用同一个HttpClient,不同的客户端还是会产生2个链接,我们来抓包测试下
StringBuilder sb = new StringBuilder();
for (Int32 i = 0; i < 2; i++)
{
var result = client.SayHelloAsync(new HelloRequest() { Name = i.ToString() }).ResponseAsync.Result;
sb.Append(result.Message);
sb.Append("client1的执行结果");
var result1 = client1.SayHelloAsync(new HelloRequest() { Name = i.ToString() }).ResponseAsync.Result;
sb.AppendLine(result1.Message);
}
return sb.ToString();
如果按照正常逻辑是公用同一个端口号。但是查看可以发现,client两次复用一个端口,Client1两次也是复用一个端口,但是这两个客户端不公用同一个端口号
可以证明我们结合上面代码的逻辑是正确的。
在反证明一次,如果共用一个HttpClient那么端口号相同。那么采用原始创建GrpcChannel方式
[HttpGet("DoubleSamePortByChannel")]
public String DoubleSamePortByChannel()
{
StringBuilder sb = new StringBuilder();
var channel = GrpcChannel.ForAddress("");
for (Int32 i = 0; i < 100; i++)
{
var client = new Greeter.GreeterClient(channel);
var result = client.SayHelloAsync(new HelloRequest() { Name = i.ToString() }).ResponseAsync.Result;
sb.Append(result.Message);
sb.Append("client1的执行结果");
var client1 = new Greeter1.Greeter1Client(channel);
var result1 = client1.SayHelloAsync(new HelloRequest() { Name = i.ToString() }).ResponseAsync.Result;
sb.AppendLine(result1.Message);
}
return sb.ToString();
}
因为共用一个Channel,所以HttpClient是公用的。抓包可以看到,他们复用同一个端口号。
结论:如果共用同一个HttpClient,那么复用同一个端口号,如果使用不同的HttpClient,那么即使是基于Http2.0也是不同的端口号
三:改动源码,解决长连接问题
改动前需要确定几个目的:
1.避免每次AddGrpcClient()注入,随时注入随时启用
2.每次客户端能够复用连接,那么就复用。
3.当请求量比较大的时候,每个端口最多保证10次调用,然后启用新的HttpClient,使用新的http端口号
4.保证可以调用多个站点集合,但是由于正常情况下,大部分站点都是相同的,这里就不做拓展,拓展开来其实都一致。
3.1.解决GrpcClient的注入问题。
难点:1.注入当前站点。2.解决创建Client的注入问题。
3.1.1:注入当前站点,采用最原始的方式,直接GrpcClientFactoryOptions的CurrentValue,而不是通过Option的Get获取通过名称的配置,
缺点是共同使用而不是单点配置,当然完全可以改,这里就不做拓展了。
public static IHttpClientBuilder AddMyGrpcClient<TClient>(this IServiceCollection services, String url)
where TClient : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure<GrpcClientFactoryOptions>(options => options.Address = new Uri(url));
var name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
return services.AddGrpcClientCore<TClient>(name);
}
3.1.2:注入当前GrpcClient,由于看源码得到,所有的Client都是通过DefaultClientActivator<T>创建,修改代码
private void AddClient(Type type)
{
if (type == null) return;
Func<ObjectFactory> result = () => ActivatorUtilities.CreateFactory(type, new Type[] { typeof(CallInvoker), });
if (_createActivator.ContainsKey(type))
{
_createActivator[type] = result;
}
else
{
_createActivator.Add(type, result);
}
}
private Object m_lock = new Object();
public TClient CreateClient<TClient>(CallInvoker callInvoker)
{
if (!_createActivator.ContainsKey(typeof(TClient)))
{
lock (m_lock)
{
if (!_createActivator.ContainsKey(typeof(TClient)))
{
AddClient(typeof(TClient));
}
}
}
return (TClient)Activator(typeof(TClient))(_services, new object[] { callInvoker });
}
增加AddClient,这样在创建新的客户端时候去判断是否存在,不存在就新增,而不是原来只新增注入的Client。
3.1.3.限制请求数量,这种通过注入的方式,留给大家自己扩展吧,因为我发现一个基于GRPCChanel的原始版本
4.不修改注入方式,而是采用直连方式
4.1.创建Client,基于表达式实现
private static Dictionary<String, Func<GrpcChannel, Object>> m_expression = new Dictionary<String, Func<GrpcChannel, Object>>();
private T GetFunc<T>(GrpcChannel channel)
{
String name = typeof(T).FullName;
if (m_expression.ContainsKey(name)) return (T)m_expression[name].Invoke(channel);
var argumentType = new[] { typeof(GrpcChannel) };
var constructor = typeof(T).GetConstructor(
BindingFlags.Instance | BindingFlags.Public,
null,
argumentType,
null);
var param = Expression.Parameter(typeof(GrpcChannel), "channel");
var constructorCallExpression = Expression.New(constructor, param);
var constructorCallingLambda = Expression
.Lambda<Func<GrpcChannel, Object>>(constructorCallExpression, param).Compile();
m_expression.Add(name, constructorCallingLambda);
return (T)constructorCallingLambda(channel);
}
4.2.创建GrpcChannel的代码实现,并且每次请求只允许10次

public T GetHttpClient<T>()
{
lock (m_lock)
{
HttpClient client = null;
foreach (var item in m_httpclients)
{
if (item.Value < 10)
{
m_currentname = item.Key;
break;
}
}
if (String.IsNullOrWhiteSpace(m_currentname))
{
String guid = Guid.NewGuid().ToString();
m_currentname = guid;
m_httpclients.Add(guid, 0);
}
m_httpclients[m_currentname] += 1;
client = m_httpclientfactory.CreateClient(m_currentname);
GrpcChannelOptions options = new GrpcChannelOptions()
{
HttpClient = client
};
var channel = GrpcChannel.ForAddress("http://localhost:6001", options);
var client1 = GetFunc<T>(channel);
return client1;
}
}
public void Dispose()
{
lock (m_lock)
{
if (m_currentname == null) return;
if (m_httpclients.TryGetValue(m_currentname, out Int32 num))
{
if (num <= 0) return;
m_httpclients[m_currentname] = num - 1;
}
}
}
五:测试是否成功
[HttpGet("GrpcHelper")]
public String GetInfotest([FromServices] GrpcHelper grpcHelper)
{
StringBuilder sb = new StringBuilder();
Int32 a = 0;
for (Int32 i = 0; i < 100; i++)
{
Task.Run(() =>
{
using (var factory = grpcHelper.CreateClientFactory())
{
var client = factory.GetHttpClient<Greeter.GreeterClient>();
var result = client.SayHelloAsync(new GrpcService1.HelloRequest() { Name = "hongyichao " + Environment.MachineName }).ResponseAsync.Result.Message;
sb.Append(result);
}
}).ContinueWith(t => Interlocked.Increment(ref a));
}
while (Volatile.Read(ref a) < 100)
{
Thread.Sleep(100);
}
return JsonConvert.SerializeObject(sb);
}
最终100个连接使用了3个端口号就可以解决。这样既解决了只复用单个端口号,又解决了单链接无法复用端口号问题。解决
最终在吐槽下自己,昨天面试渣成啥样了。下一篇开始研究Rabbitmq了。另外,大家得注意,现在都流行代码测试。多做做题。
不然即使像我这种老司机,也在很简单很简单的题目上遭遇滑铁卢。但是也不能一味着写算法,也多多看源码,毕竟这是我们的工作
记录core中GRPC长连接导致负载均衡不均衡问题 二,解决长连接问题的更多相关文章
- .net core中Grpc使用报错:The remote certificate is invalid according to the validation procedure.
因为Grpc采用HTTP/2作为通信协议,默认采用LTS/SSL加密方式传输,比如使用.net core启动一个服务端(被调用方)时: public static IHostBuilder Creat ...
- .net core中Grpc使用报错:The response ended prematurely.
当我们调用Grpc是出现下面的一堆异常时,一般是由于LTS导致的: Call failed with gRPC error status. Status code: 'Unavailable', Me ...
- Asp.net core中由于页面编码导致的中文乱码
问题描述 最近使用asp.net core写了一个简单的网站,在windows系统下完全没有出现问题.后来在linux系统中搭建了docker,并且在linux中自动使用git获取源码,编译,部署一条 ...
- .net core中的分布式缓存和负载均衡
通过减少生成内容所需的工作,缓存可以显著提高应用的性能和可伸缩性,缓存对不经常更改的数据效果最佳,缓存生成的数据副本的返回速度可以比从原始源返回更快.ASP.NET Core 支持多种不同的缓存,最简 ...
- .net core中Grpc使用报错:Request protocol 'HTTP/1.1' is not supported.
显然这个报错是说HTTP/1.1不支持. 首先,我们要知道,Grpc是Google开源的,跨语言的,高性能的远程过程调用框架,它是以HTTP/2作为通信协议的,所以当我启动启用一个服务作为Grpc的服 ...
- SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法
由于项目需要使用SpringCache来做一点缓存,但自己之前没有使用过(其实是没有听过)SpringCache,于是,必须先学习之. 在网上找到一篇文章,比较好,就先学习了,地址是: https:/ ...
- 关于文字内容过长,导致文本内容超出html 标签宽度的解决方法之自动换行
在标签的style 属性中设置 word-break style="word-break:break-all;" 这样就可以实现换行 上截图没设置之前 设置之后 完美解决!!!!! ...
- 9.png(9位图)在android中作为background使用导致居中属性不起作用的解决方法
在使用到9.png的布局上面添加 android:padding="0dip" 比如 <LinearLayout android:layout_widt ...
- EntityFramework Core 3多次Include导致查询性能低之解决方案
前言 上述我们简单讲解了几个小问题,这节我们再来看看如标题EF Core中多次Include导致出现性能的问题,废话少说,直接开门见山. EntityFramework Core 3多次Include ...
随机推荐
- HDOJ 1848(SG函数)
对于SG函数来说,sg[y]=x的意义为,x与y的输赢状态是相同的 sg[y]=mex(y)的定义与n.p点的定义是相同的 #include<iostream>#include<cs ...
- Consonant Fencity Gym - 101612C 暴力二进制枚举 Intelligence in Perpendicularia Gym - 101612I 思维
题意1: 给你一个由小写字母构成的字符串s,你可以其中某些字符变成大写字母.如果s中有字母a,你如果想把a变成大写,那s字符串中的每一个a都要变成A 最后你需要要出来所有的字符对,s[i]和s[i-1 ...
- Happy 2006 POJ - 2773 容斥原理+二分
题意: 找到第k个与m互质的数 题解: 容斥原理求区间(1到r)里面跟n互质的个数时间复杂度O(sqrt(n))- 二分复杂度也是O(log(n)) 容斥原理+二分这个r 代码: 1 #include ...
- POJ1142 Smith Numbers 暴力+分解质因子
题意:题目定义了一个史密斯数,这个数的定义是:一个合数的各个位置上加起来的和等于它的素因数所有位置上的数字加起来的和.比如: 4937775=3∗5∗5∗658374+9+3+7+7+7+5=3+5+ ...
- C语言之库函数的模拟与使用
C语言之库函数的模拟与使用 在我们学习C语言的过程中,难免会遇到这样的一种情况: 我们通常实现一个功能的时候,费尽心血的写出来,却有着满满的错,这时却有人来告诉你说:这个功能可以用相应的库函数来实现. ...
- Redis 穿透 & 击穿 & 雪崩
原文:https://www.cnblogs.com/binghe001/p/13661381.html 缓存穿透 如果在请求数据时,在缓存层和数据库层都没有找到符合条件的数据,也就是说,在缓存层和数 ...
- 鸟哥的linux私房菜——第十六章学习(程序管理与 SELinux 初探)
第十六章.程序管理与 SE Linux 初探 在 Linux 系统当中:"触发任何一个事件时,系统都会将他定义成为一个程序,并且给予这个程序一个 ID ,称为 PID,同时依据启发这个程序的 ...
- 鸟哥的linux私房菜——第七章学习(Linux 磁盘与文件系统管理)
1.1).文件系统特征 我们称呼一个可被挂载的数据为一个文件系统而不是一个分区! 文件系统通常会将这两部份的数据分别存放在不同的区块,权限与属性放置到 inode 中,至于实际数据则放置到 data ...
- IP的地址的划分
IP地址的划分是计算机网络中很重要的一个知识点,曾经考过三级,但是长时间不用就会忘掉,现在重新将IP的地址划分整理一遍. 首先IP地址的编址方法经历了三个阶段:分类的IP地址.子网的划分.构成超网 我 ...
- mark::开源绘图工具graphviz
http://blog.csdn.net/iamljj/article/details/5862930 http://codeforces.com/contest/601/problem/D