記錄core中GRPC長連接導致負載均衡不均衡問題一:查看源碼,看創建過程


一 問題描述:

由來:公司有個功能需要被大量請求,並且中間涉及到多個不同的語言組成(c++/java/c#等),就決定使用grpc來做rpc服務。我是做c#的當然使用grpc for c# 來處理。這里涉及到一個問題,這個底層服務耗費性能,並且只是在一定時間內被大量請求,所以運維啟用監視,當單個容器使用過多時候,便增加新pod,然后通過k8s自己的負載均衡進行協調。大體流程:

  注:1.pod1,pod2是grpcserver

         2.pod會根據容器檢測自動啟用新容器

         3.api層只負責轉發和記錄當前請求內容,不做io處理,所以只需要啟用一個站點,便可以支撐所有請求,(即使真的太多,那么重啟一個服務也是毫無難度的,類似於nginx)

二:問題產生

 從理論來看這種屬於最簡單的流式調用,沒有任何問題。一個pod處理不過來,那么便多啟用幾個pod處理增加速度。一切都很好。但是由於k8s負載均衡只是做個中轉,然后因為grpc的http2.0長連接導致只要連接一個pod,那么在使用當前grpcclient的情況下,一直指向一個pod無法釋放,導致在巔峰期,出現一個pod累成狗,其他pod看熱鬧的情況發生。

 

 最終導致pod1一直重啟。然后連接到pod2,然后pod2掛掉,持續如此

三:問題原因

    由於grpc使用的http2.0長連接(注意與http1.0的長連接,即連接復用one by one方式不一樣),是多個請求可同時在一個連接上並行執行

通過tcpdump抓包可以看出來,44026與6001之間多次連接傳輸數據,並且即使6001沒有回傳數據,44026也會傳輸新請求給6001.這就是http2.0的連接並行

 

 四:解決問題的方式:

1.最簡單的方式是在api層與k8s之間通過一個nginx來處理長連接,但是此處將grpc的長連接強制改為短連接了,此方法pass掉

2.將k8s連接到pod中的方式改為通過k8s自己的負載均衡處理。但是使用的是阿x雲服務器,阿x雲不提供當前方案,自己的運維也不願意在線上折騰,所以此方案pass

3.最傻的解決方式,將api層做負載,每次啟動2個pod,一個api的pod一個grpc的pod,然后在api層做負載,讓api每次使用同一個長連接。大概如下

 

 如圖所示,就知道這種方案有多傻是多傻。但是由於線上緊急,所以使用了此種方式進行處理

5.產生新的問題

   由於只有c#出現這個問題,所以有些人啊,一直在那里說c#垃圾,c#不如java,我那個氣啊。這不行,不爭口香爭口氣。剛好乘着過年好好捋一捋grpc的代碼,看到底啥情況,那么上github看源碼

https://github.com/grpc/grpc-dotnet.git,上面就是官方提供的grpc的連接,一步步來看,首先看我們注入的解決方式

1.先看創建過程AddGrpcClient<TClient>

public static void AddGrpcClient<TClient>(this IServiceCollection services, Action<GrpcClientFactoryOptions> configureClient) where TClient : class
        {
            var name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
            services.Configure(name, configureClient);
            services.TryAddSingleton<GrpcClientFactory, DefaultGrpcClientFactory>();
            services.TryAddSingleton<GrpcCallInvokerFactory>();
            services.TryAddSingleton<DefaultClientActivator<TClient>>();
            services.TryAddSingleton(new GrpcClientMappingRegistry());
            Action<IServiceProvider, HttpClient> configureTypedClient = (s, httpClient) =>
            {
                var os = s.GetRequiredService<IOptionsMonitor<GrpcClientFactoryOptions>>();
                var clientOptions = os.Get(name);
                httpClient.BaseAddress = clientOptions.Address;
                httpClient.Timeout = Timeout.InfiniteTimeSpan;
            };
            services
                .AddHttpClient(name, configureTypedClient)
                .ConfigurePrimaryHttpMessageHandler(() =>
                {
                    var handler = new HttpClientHandler();
                    return handler;
                });
            services.AddTransient<TClient>(s =>
            {
                var clientFactory = s.GetRequiredService<GrpcClientFactory>();
                return clientFactory.CreateClient<TClient>(name);
            });
        }
View Code

整理出最核心代碼,可以發現,生成GrpcClient過程中還是基於HttpClient.這些是注入過程,其中看到一個關鍵的注入方式

Services.AddTransient<TClient>(s =>
            {
                var clientFactory = s.GetRequiredService<GrpcClientFactory>();
                return clientFactory.CreateClient<TClient>(builder.Name);
            });

可以看見,當我注入Greet.GreetClient時候,在ioc獲取的時候 是基於Transient來獲取的

2.在看看創建GrpcClient過程,通過上面的注入方式獲取GrpcClientFactory來獲取Client:

services.TryAddSingleton<GrpcClientFactory, DefaultGrpcClientFactory>();

再來看看DefaultGrpcClientFactory的CreateClient

        public override TClient CreateClient<TClient>(string name) where TClient : class
        {
            var defaultClientActivator = _serviceProvider.GetService<DefaultClientActivator<TClient>>();
            var clientFactoryOptions = _clientFactoryOptionsMonitor.Get(name);
            var httpClient = _httpClientFactory.CreateClient(name);
            var callInvoker = _callInvokerFactory.CreateCallInvoker(httpClient, name, clientFactoryOptions);

            if (clientFactoryOptions.Creator != null)
            {
                var c = clientFactoryOptions.Creator(callInvoker);
                if (c is TClient client)
                {
                    return client;
                }
            }
            else
            {
                return defaultClientActivator.CreateClient(callInvoker);
            }
        }
View Code

有個DefaultClientActivator<TClient>用來生成TClient

private readonly static Func<ObjectFactory> _createActivator = () => ActivatorUtilities.CreateFactory(typeof(TClient), new Type[] { typeof(CallInvoker), });

  var activator = LazyInitializer.EnsureInitialized(ref _activator,ref _initialized,ref _lock,_createActivator);

這個方法查看了注釋,是用來通過Create a delegate that will instantiate a type with constructor arguments provided directly and/or from an System.IServiceProvider.

也就是通過注入IServiceProvider創建一個基於CallInvoker對象生成的Client,但是這點也是我比較奇怪的地方。都已經提供了創建對象的arguments了,為什么還需要通過IServiceProvider來獲取注入的參數,暫時沒有去看這方面的源碼,我就不去猜想這種實現的差異,反正這里目的是創建一個Client,在看看Callinvoke的實現方式

            var clientFactoryOptions = _clientFactoryOptionsMonitor.Get(name);
            var httpClient = _httpClientFactory.CreateClient(name);
            var callInvoker = _callInvokerFactory.CreateCallInvoker(httpClient, name, clientFactoryOptions);

這里面的代碼都比較熟悉,通過IOption注入的GrpcClientFactoryOptions,注入的HttpClient,最后關鍵點的是CallInvoke

            var channelOptions = new GrpcChannelOptions();
            channelOptions.HttpClient = httpClient;
            channelOptions.LoggerFactory = _loggerFactory;

            if (clientFactoryOptions.ChannelOptionsActions.Count > 0)
            {
                foreach (var applyOptions in clientFactoryOptions.ChannelOptionsActions)
                {
                    applyOptions(channelOptions);
                }
            }

            var address = clientFactoryOptions.Address ?? httpClient.BaseAddress;
            var channel = GrpcChannel.ForAddress(address, channelOptions);

            var httpClientCallInvoker = channel.CreateCallInvoker();

可以很清晰的看出來,是通過GrpcChannel.ForAddress(address, channelOptions);來解析所有的參數,只不過有個CallInvoke來當做解析點,在與官方提供的Grpc點比較

        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(cert);
        var httpClient = new HttpClient(handler);

        var channel = GrpcChannel.ForAddress("https://localhost:5001/", new GrpcChannelOptions
        {
            HttpClient = httpClient
        });

        var grpc = new Greeter.GreeterClient(channel);
        var response = await grpc.SayHelloAsync(new HelloRequest { Name = "Bob" });

也就是換個方式來實現new Client的步驟。這就是所有的Grpc生成的源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM