分享在Linux下使用OSGi.NET外掛框架快速實現一個分散式服務叢集的方法
在這篇文章我分享瞭如何使用分層與模組化的方法來設計一個分散式服務叢集。這個分散式服務叢集是基於DynamicProxy、WCF和OSGi.NET外掛框架實現的。我將從設計思路、目標和實現三方面來描述。
1 設計思路
首先,我來說明一下設計思路。我們先來看看目前OSGi.NET外掛框架的服務。在這裡,服務不是遠端服務,它是輕量級的服務,由介面和實現類組成,如下圖所示。服務契約外掛定義了服務介面,服務實現外掛向服務匯流排註冊服務,服務呼叫外掛利用服務契約(介面)從服務匯流排獲取實現的服務並呼叫,服務實現外掛和服務呼叫外掛都依賴於服務契約,但二者並未有依賴。服務是外掛間鬆耦合的呼叫方式。
我們希望在不更改現有的通訊機制的情況下,將以前定義的在同一個框架下的服務能夠直接不更改程式碼情況下,變成遠端服務。此時,基於遠端服務的通訊方式變成如下圖所示的方式。
這時候,在不更改服務定義、服務註冊的程式碼下,在OSGi.NET框架中安裝一個遠端服務宿主外掛,它直接將服務匯流排的服務暴露成遠端服務;OSGi.NET外掛框架安裝一個遠端服務客戶端外掛,就可以使用服務契約來獲取並呼叫遠端服務。
接下來,我們希望能更進一步在不需要更改服務定義和註冊程式碼情況下,來實現透明的叢集支援。
在這裡,我們引入了負載均衡外掛。首先,一個OSGi.NET外掛框架安裝了遠端服務負載均衡外掛,它管理所有遠端服務的負載狀況,併為叢集提供了統一的訪問和負載均衡支援;接著,所有安裝了遠端服務宿主外掛的OSGi.NET框架,會安裝一個負載均衡客戶端外掛,它用於將遠端服務註冊到負載均衡器;服務呼叫端安裝了遠端服務客戶端外掛,它通過負載均衡器來呼叫遠端服務。
這個思路可以簡單描述如下:
A)本地服務 = OSGi.NET外掛框架 + 服務契約外掛 + 服務實現外掛 + 服務呼叫外掛;服務實現和服務呼叫在同一個OSGi.NET外掛框架內,在同一個程序。
B)遠端服務實現 = OSGi.NET外掛框架 + 服務契約外掛 + 服務實現外掛 + 遠端服務宿主外掛,遠端服務呼叫 = OSGi.NET外掛框架 + 服務契約外掛 + 遠端服務客戶端外掛;服務實現和服務呼叫可以在不同的OSGi.NET外掛框架,在不同程序內。
C)負載均衡器 = OSGi.NET外掛框架 + 遠端服務負載均衡外掛,負載均衡遠端服務實現 = OSGi.NET外掛框架 + 服務契約外掛 + 服務實現外掛 + 遠端服務宿主外掛 + 負載均衡客戶端外掛,遠端服務呼叫 = OSGi.NET外掛框架 + 服務契約外掛 + 遠端服務客戶端外掛; 負載均衡器、遠端服務、服務呼叫均可以在不同OSGi.NET外掛框架和不同程序,遠端服務可以在多臺機器中,註冊到負載均衡器。
2 設計目標
遠端服務和負載均衡的實現基於模組化思路,如下圖所示。
(1)不更改本地服務的定義、註冊和使用方法;
(2)在本地服務的基礎上,安裝遠端服務宿主外掛,服務就暴露成遠端服務;
(3)在遠端服務的基礎上,安裝負載均衡客戶端外掛和負載均衡器,遠端服務就支援叢集及負載均衡。
以一個簡單的服務ISayHelloService為例,下面將描述如何通過以上方式來實現遠端服務和服務叢集。
2.1 遠端服務示例
2.1.1 遠端服務註冊及實現
如下圖所示,SayHelloServiceContract外掛定義了一個ISayHelloService服務介面,SayHelloService定義了SayHelloServiceImpl服務實現,在OSGi.NET外掛框架安裝了UIShell.RemoteServiceHostPlugin外掛,這樣我們就將本地服務暴露成遠端服務了。
下圖是SayHelloServiceImpl服務的實現和服務註冊。
下圖則是服務註冊。
你可以發現,為了支援遠端服務,我們僅僅是安裝了一個遠端服務宿主外掛,而沒有更改服務的實現和註冊方法。
2.1.2 遠端服務呼叫
遠端服務呼叫如下所示,在OSGi.NET外掛框架安裝了遠端服務客戶端外掛。服務呼叫外掛使用服務契約,來呼叫遠端服務。
呼叫遠端服務的步驟為:
1 使用遠端服務客戶端外掛的IRemoteServiceProxyService來獲取遠端服務;
2 直接呼叫遠端服務的方法。
因此,你可以發現,遠端服務的定義和使用都非常的簡單。接下來我們再看看負載均衡遠端服務的使用。
2.2 負載均衡遠端服務示例
2.2.1 負載均衡器
負載均衡器相當於遠端服務登錄檔,它用於註冊暴露遠端服務的所有機器以及每一個機器每一個服務的負載均衡狀況,並提供負載均衡支援。下圖是負載均衡器的實現。
負載均衡器向外暴露一個固定的IP地址和埠號,用於註冊遠端服務,並提供負載均衡。
2.2.2 負載均衡遠端服務
支援負載均衡的遠端服務需要安裝一個負載均衡客戶端外掛,如下所示。負載均衡客戶端外掛用於將遠端服務註冊到負載均衡器,從而,負載均衡器可以來管理遠端服務的負載情況,當發生故障時可以實現負載轉移和實現負載均衡。
這裡,遠端負載均衡器客戶端外掛會連線到負載均衡伺服器,向其註冊本機器的遠端服務。
2.2.3 負載均衡遠端服務呼叫
呼叫負載均衡遠端服務與直接呼叫遠端服務方法類似,如下圖所示。它使用GetFirstOrDefaultLoadBalancerService介面來訪問負載均衡器,獲取經過負載均衡的遠端服務。
你可以發現,呼叫負載均衡遠端服務的方法也非常簡單。下面,我來介紹一下如何實現。
3 設計實現
首先,我們先來看看遠端服務的實現。
3.1 遠端服務的實現
遠端服務的實現可以歸結為以下幾點:(1)遠端服務宿主外掛用於暴露一個WCF服務,這個WCF服務相當於本地服務的Bridge,即客戶端對遠端服務的呼叫先中專到這個WCF服務,再由WCF服務來呼叫本地服務,然後返回給客戶端;(2)客戶端使用DynamicProxy為服務契約生成一個代理,對這個代理的方法呼叫將會被攔截,然後呼叫遠端服務宿主外掛的WCF服務,將呼叫結果再返回。
3.1.1 遠端服務宿主外掛實現
該外掛首先定義了一個IRemoteServiceInvoker的WCF服務介面,這個介面及引數的定義如下。
它的作用就是通過呼叫這個WCF遠端服務來呼叫OSGi.NET框架本地服務,達到將本地服務暴露成遠端服務的目的。
這個WCF的實現程式碼如下所示,其目的就是對WCF的呼叫轉換成對本地服務方法的呼叫並返回。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Reflection; 7 using System.Threading.Tasks; 8 using fastJSON; 9 using Fasterflect; 10 using UIShell.OSGi.Utility; 11 12 namespace UIShell.RemoteServiceHostPlugin 13 { 14 public class RemoteServiceInvoker : IRemoteServiceInvoker 15 { 16 public static ReaderWriterLock Locker = new ReaderWriterLock(); 17 public static Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> InvokerCache = new Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> (); 18 19 public string InvokeService(RemoteServiceInvocation invocation) 20 { 21 AssertUtility.NotNull(invocation); 22 AssertUtility.ArgumentHasText(invocation.ContractName, "service contract name"); 23 AssertUtility.ArgumentHasText(invocation.MethodName, "service method name"); 24 25 var service = Activator.Context.GetFirstOrDefaultService(invocation.ContractName); 26 string msg = string.Empty; 27 if(service == null) 28 { 29 msg = string.Format ("Remote Service '{0}' not found.", invocation.ContractName); 30 FileLogUtility.Warn (msg); 31 throw new Exception(msg); 32 } 33 34 System.Tuple<MethodInfo, Type[], MethodInvoker> invokerTuple; 35 using (var locker = ReaderWriterLockHelper.CreateReaderLock (Locker)) 36 { 37 InvokerCache.TryGetValue (invocation.Key, out invokerTuple); 38 } 39 40 if (invokerTuple == null) 41 { 42 Type serviceType = service.GetType (); 43 44 var serviceMethodInfo = serviceType.GetMethod (invocation.MethodName); 45 if (serviceMethodInfo == null) 46 { 47 msg = string.Format ("The method '{1}' of the remote service '{0}' not found.", invocation.ContractName, invocation.MethodName); 48 FileLogUtility.Warn (msg); 49 throw new Exception (msg); 50 } 51 52 if (invocation.JsonSerializedParameters == null) { 53 invocation.JsonSerializedParameters = new List<string> (); 54 } 55 56 var parameterInfos = serviceMethodInfo.GetParameters (); 57 if (invocation.JsonSerializedParameters.Count != parameterInfos.Length) 58 { 59 msg = string.Format ("The parameters count is not match with the method '{0}' of service '{1}'. The expected count is {2}, the actual count is {3}.", invocation.MethodName, invocation.ContractName, parameterInfos.Length, invocation.JsonSerializedParameters.Count); 60 FileLogUtility.Warn (msg); 61 throw new Exception (msg); 62 } 63 64 var parameterTypes = new Type[parameterInfos.Length]; 65 for (int i = 0; i < parameterInfos.Length; i++) 66 { 67 parameterTypes [i] = parameterInfos [i].ParameterType; 68 } 69 70 try 71 { 72 var methodInvoker = serviceType.DelegateForCallMethod (invocation.MethodName, parameterTypes); 73 74 invokerTuple = new System.Tuple<MethodInfo, Type[], MethodInvoker> (serviceMethodInfo, parameterTypes, methodInvoker); 75 76 using (var locker = ReaderWriterLockHelper.CreateWriterLock (Locker)) 77 { 78 if (!InvokerCache.ContainsKey (invocation.Key)) 79 { 80 InvokerCache [invocation.Key] = invokerTuple; 81 } 82 } 83 } 84 catch(Exception ex) 85 { 86 msg = string.Format ("Failed to create delegate method for the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName); 87 FileLogUtility.Warn (msg); 88 FileLogUtility.Warn (ex); 89 throw new Exception (msg, ex); 90 } 91 } 92 93 var paramters = new object[invokerTuple.Item2.Length]; 94 95 for(int i = 0; i < invokerTuple.Item2.Length; i++) 96 { 97 try 98 { 99 paramters[i] = JSON.ToObject(invocation.JsonSerializedParameters[i], invokerTuple.Item2[i]); 100 } 101 catch(Exception ex) 102 { 103 msg = string.Format ("Failed to unserialize the '{0}'th parameter for the method '{1}' of service '{2}'.", i + 1, invocation.MethodName, invocation.ContractName); 104 FileLogUtility.Warn (msg); 105 FileLogUtility.Warn (ex); 106 throw new Exception (msg, ex); 107 } 108 } 109 110 try 111 { 112 return JSON.ToJSON(invokerTuple.Item3(service, paramters)); 113 } 114 catch(Exception ex) 115 { 116 msg = string.Format("Failed to invoke the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName); 117 FileLogUtility.Warn (msg); 118 FileLogUtility.Warn (ex); 119 throw new Exception (msg, ex); 120 } 121 } 122 } 123 }
3.1.2 遠端服務客戶端外掛的實現
接下來,我們看看遠端服務客戶端外掛的實現。它定義了一個IRemoteServiceProxyService服務,暴露了兩個介面分別用於對遠端服務和負載均衡遠端服務的呼叫。
該服務的遠端服務獲取實現如下所示。
它僅僅時通過DynamicProxy建立遠端服務代理類,此時,對代理類方法的呼叫會轉換成遠端服務呼叫。下面看看攔截機的實現。
1 class RemoteServiceProxyInterceptor : IInterceptor, IDisposable 2 { 3 private RemoteServiceContext _remoteServiceContext; 4 private RemoteServiceClient _remoteServiceClient; 5 public RemoteServiceProxyInterceptor(RemoteServiceContext context) 6 { 7 _remoteServiceContext = context; 8 _remoteServiceClient = new RemoteServiceClient(_remoteServiceContext.IPAddress, _remoteServiceContext.Port); 9 _remoteServiceClient.Start(); 10 } 11 public void Intercept(IInvocation invocation) 12 { 13 try 14 { 15 var jsonParameters = new List<string>(); 16 foreach (var param in invocation.Arguments) 17 { 18 jsonParameters.Add(JSON.ToJSON(param)); 19 } 20 21 var resultJson = _remoteServiceClient.Invoke(invocation.Method.DeclaringType.FullName, invocation.Method.Name, jsonParameters); 22 23 if (!invocation.Method.ReturnType.FullName.Equals("System.Void")) 24 { 25 invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType); 26 } 27 else 28 { 29 invocation.ReturnValue = null; 30 } 31 } 32 catch(Exception ex) 33 { 34 FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'", 35 invocation.Method.DeclaringType.FullName, invocation.Method.Name)); 36 throw; 37 } 38 } 39 40 public void Dispose() 41 { 42 if (_remoteServiceClient != null) 43 { 44 _remoteServiceClient.Stop(); 45 _remoteServiceClient = null; 46 } 47 } 48 }
攔截機的程式碼很簡單,對遠端服務代理類方法的呼叫將直接轉換成對遠端服務宿主外掛的WCF服務的呼叫。下面看看負載均衡遠端服務的實現。
3.2 負載均衡遠端服務的實現
有了以上的技術,關於負載均衡遠端服務的實現,就簡單多了。負載均衡遠端服務的目的就是將所有遠端服務統一在負載均衡伺服器進行註冊,並實現負載的動態管理。因此,需要在遠端服務基礎上,建立一個負載均衡伺服器和負載均衡客戶端。負載均衡伺服器用於管理所有遠端服務及提供遠端服務的機器,管理所有遠端服務的負載情況,並實現負載均衡及故障轉移;負載均衡客戶端的目的時將遠端服務註冊到負載均衡器,並且當遠端服務關閉時從負載均衡器解除安裝。下面,看看負載均衡器的實現。
3.2.1 負載均衡器實現
負載均衡伺服器暴露了如下WCF服務。這個服務用於提供遠端服務註冊和解除安裝以及負載均衡請求。
這個服務的實現如下所示。
它使用遠端服務登錄檔來實現遠端服務的管理和負載均衡的實現。
3.2.2 負載均衡客戶端的實現
負載均衡客戶端的目的時實現遠端服務的註冊與解除安裝,通過該外掛將遠端服務暴露到負載均衡伺服器。這樣服務呼叫者就可以通過負載均衡器來呼叫遠端服務。
3.2.3 負載均衡遠端服務呼叫
負載均衡遠端服務的呼叫方式的實現和遠端服務類似。它由遠端服務代理服務的GetFirstOrDefaultLoadBalancerService介面來實現。
該介面的實現如下所示,主要時建立代理和方法攔截機。
這個方法呼叫攔截機會將方法呼叫轉化為:(1)從負載均衡伺服器獲取均衡的遠端服務主機;(2)直接呼叫該遠端服務主機的服務,如果呼叫失敗則嘗試進行重新負載均衡。其實現如下所示。
1 class RemoteServiceLoadBalancerProxyInterceptor : IInterceptor, IDisposable 2 { 3 private string LoadBalancerHost 4 { 5 get 6 { 7 return ConfigurationSettings.AppSettings["LoadBalancerHost"]; 8 } 9 } 10 11 private string LoadBalancerPort 12 { 13 get 14 { 15 return ConfigurationSettings.AppSettings["LoadBalancerPort"]; 16 } 17 } 18 19 private LoadBalancerContext _remoteServiceLoadBalancerContext; 20 private RemoteServiceClient _remoteServiceClient; 21 private RemoteServiceLoadBalancerAccessClient _remoteServiceLoadBalancerAccessClient; 22 private bool _initialized; 23 24 public RemoteServiceLoadBalancerProxyInterceptor(LoadBalancerContext context) 25 { 26 _remoteServiceLoadBalancerContext = context; 27 28 if (string.IsNullOrEmpty(LoadBalancerHost)) 29 { 30 throw new Exception("You need to specified the load balancer host (HostName or IP Address) by app setting 'LoadBalancerHost'."); 31 } 32 33 int loadBalancerPortInt; 34 if (!int.TryParse(LoadBalancerPort, out loadBalancerPortInt)) 35 { 36 throw new Exception("You need to specified the load balancer port by app setting 'LoadBalancerPort'."); 37 } 38 39 try 40 { 41 _remoteServiceLoadBalancerAccessClient = new RemoteServiceLoadBalancerAccessClient (LoadBalancerHost, loadBalancerPortInt); 42 _remoteServiceLoadBalancerAccessClient.Start (); 43 } 44 catch(Exception ex) 45 { 46 FileLogUtility.Error (string.Format("Faild to connect to load balancer '{0}'.", _remoteServiceLoadBalancerContext)); 47 FileLogUtility.Error (ex); 48 throw; 49 } 50 } 51 52 private bool Initialize(string serviceContractName) 53 { 54 if(_remoteServiceClient != null) 55 { 56 _remoteServiceClient.Stop(); 57 } 58 59 RemoteServiceHost remoteHost = null; 60 try 61 { 62 remoteHost = _remoteServiceLoadBalancerAccessClient.Balance(serviceContractName); 63 FileLogUtility.Inform(string.Format("Get the remote service host '{0}' by load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext)); 64 } 65 catch(Exception ex) 66 { 67 FileLogUtility.Error (string.Format("Faild to get a remote service host by load balancer '{0}'.", _remoteServiceLoadBalancerContext)); 68 FileLogUtility.Error (ex); 69 return false; 70 } 71 if (remoteHost != null) 72 { 73 _remoteServiceClient = new RemoteServiceClient (remoteHost.IPAddress, remoteHost.Port); 74 try 75 { 76 _remoteServiceClient.Start (); 77 return true; 78 } 79 catch(Exception ex) 80 { 81 FileLogUtility.Error (string.Format("Failed to connect to the remote service host '{0}' by using load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext)); 82 } 83 } 84 85 return false; 86 } 87 88 public void Intercept(IInvocation invocation) 89 { 90 var serviceContractName = invocation.Method.DeclaringType.FullName; 91 if (!_initialized) 92 { 93 _initialized = Initialize (serviceContractName); 94 if (!_initialized) 95 { 96 invocation.ReturnValue = null; 97 return; 98 } 99 } 100 101 var jsonParameters = new List<string>(); 102 foreach (var param in invocation.Arguments) 103 { 104 jsonParameters.Add(JSON.ToJSON(param)); 105 } 106 107 int tryTimes = 1; 108 109 for (int i = 0; i < tryTimes; i ++ ) 110 { 111 try 112 { 113 var resultJson = _remoteServiceClient.Invoke(serviceContractName, invocation.Method.Name, jsonParameters); 114 115 if (!invocation.Method.ReturnType.FullName.Equals("System.Void")) 116 { 117 invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType); 118 } 119 else 120 { 121 invocation.ReturnValue = null; 122 } 123 return; 124 } 125 catch(Exception ex) 126 { 127 FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'", 128 serviceContractName, invocation.Method.Name)); 129 FileLogUtility.Error (ex); 130 if (i == tryTimes) 131 { 132 throw; 133 } 134 if (!((_initialized = Initialize (serviceContractName)) == true)) // 重新Balance 135 { 136 throw; 137 } 138 } 139 } 140 } 141 142 public void Dispose() 143 { 144 if (_remoteServiceClient != null) 145 { 146 _remoteServiceClient.Stop(); 147 _remoteServiceClient = null; 148 } 149 } 150 }
4 小結
在這篇文章,我詳細介紹了支援叢集的遠端服務的實現。你可以發現,整體實現完全按照模組化組裝的方式。你也可以嘗試來考慮以模組化組裝的方法實現一個遠端服務叢集。多謝支援~~。