题外话:

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长连接导致负载均衡不均衡问题 二,解决长连接问题的更多相关文章

  1. .net core中Grpc使用报错:The remote certificate is invalid according to the validation procedure.

    因为Grpc采用HTTP/2作为通信协议,默认采用LTS/SSL加密方式传输,比如使用.net core启动一个服务端(被调用方)时: public static IHostBuilder Creat ...

  2. .net core中Grpc使用报错:The response ended prematurely.

    当我们调用Grpc是出现下面的一堆异常时,一般是由于LTS导致的: Call failed with gRPC error status. Status code: 'Unavailable', Me ...

  3. Asp.net core中由于页面编码导致的中文乱码

    问题描述 最近使用asp.net core写了一个简单的网站,在windows系统下完全没有出现问题.后来在linux系统中搭建了docker,并且在linux中自动使用git获取源码,编译,部署一条 ...

  4. .net core中的分布式缓存和负载均衡

    通过减少生成内容所需的工作,缓存可以显著提高应用的性能和可伸缩性,缓存对不经常更改的数据效果最佳,缓存生成的数据副本的返回速度可以比从原始源返回更快.ASP.NET Core 支持多种不同的缓存,最简 ...

  5. .net core中Grpc使用报错:Request protocol 'HTTP/1.1' is not supported.

    显然这个报错是说HTTP/1.1不支持. 首先,我们要知道,Grpc是Google开源的,跨语言的,高性能的远程过程调用框架,它是以HTTP/2作为通信协议的,所以当我启动启用一个服务作为Grpc的服 ...

  6. SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法

    由于项目需要使用SpringCache来做一点缓存,但自己之前没有使用过(其实是没有听过)SpringCache,于是,必须先学习之. 在网上找到一篇文章,比较好,就先学习了,地址是: https:/ ...

  7. 关于文字内容过长,导致文本内容超出html 标签宽度的解决方法之自动换行

    在标签的style 属性中设置 word-break style="word-break:break-all;" 这样就可以实现换行 上截图没设置之前 设置之后 完美解决!!!!! ...

  8. 9.png(9位图)在android中作为background使用导致居中属性不起作用的解决方法

    在使用到9.png的布局上面添加 android:padding="0dip" 比如 <LinearLayout            android:layout_widt ...

  9. EntityFramework Core 3多次Include导致查询性能低之解决方案

    前言 上述我们简单讲解了几个小问题,这节我们再来看看如标题EF Core中多次Include导致出现性能的问题,废话少说,直接开门见山. EntityFramework Core 3多次Include ...

随机推荐

  1. Codeforces Round #648 (Div. 2) D. Solve The Maze

    这题犯了一个很严重的错误,bfs 应该在入队操作的同时标记访问,而不是每次只标记取出的队首元素. 题目链接:https://codeforces.com/contest/1365/problem/D ...

  2. 【uva 753】A Plug for UNIX(图论--网络流最大流 Dinic)

    题意:有N个插头,M个设备和K种转换器.要求插的设备尽量多,问最少剩几个不匹配的设备. 解法:给读入的各种插头编个号,源点到设备.设备通过转换器到插头.插头到汇点各自建一条容量为1的边.跑一次最大流就 ...

  3. poj 2007 凸包构造和极角排序输出(模板题)

    Scrambled Polygon Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 10841   Accepted: 508 ...

  4. Codeforces Round #582 (Div. 3) F. Unstable String Sort

    传送门 题意: 你需要输出一个长度为n的字符序列(由小写字母组成),且这个字符串中至少包含k个不同的字符.另外题目还有要求:给你两个长度为p和q的序列,设字符序列存在s中 那么就会有s[Pi]< ...

  5. python 迭代器 iter多次消费

    问题 Python 中的迭代器是我们经常使用的迭代工具, 但其只能消费一次,再次消费便会出现 StopIteration 报错. 解决方案 封装了一个类,当迭代器使用完后再次初始化. 代码 class ...

  6. codevs1169传纸条 不相交路径取最大,四维转三维DP

    这个题一个耿直的思路肯定是先模拟.. 但是我们马上发现这是具有后效性的..也就是一个从(1,1)开始走,一个从(n,m)开始走的话 这样在相同的时间点我们就没法判断两个路径是否是相交的 于是在dp写挂 ...

  7. Linux cp command All In One

    Linux cp command All In One $ man cp $ cp -h # 强制 $ cp -f # 递归,复制文件夹 $ cp -r demos cp -fr # ./folder ...

  8. Lua 从入门到放弃

    Lua 从入门到放弃 What is Lua? Lua is a powerful, efficient, lightweight, embeddable scripting language. It ...

  9. WebGL Programming Guide All In One

    WebGL Programming Guide All In One WebGL WebGL Programming Guide All In One Publication date: July 2 ...

  10. 图解 HTTP, 图解 HTTPS, 图解 HTTP/2, 图解 HTTP/3, 图解 QUIC

    图解 HTTP, 图解 HTTPS, 图解 HTTP/2, 图解 HTTP/3, 图解 QUIC HTTP https://en.wikipedia.org/wiki/Hypertext_Transf ...