分享在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的调用转换成对本地服务方法的调用并返回。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Reflection;
using System.Threading.Tasks;
using fastJSON;
using Fasterflect;
using UIShell.OSGi.Utility; namespace UIShell.RemoteServiceHostPlugin
{
public class RemoteServiceInvoker : IRemoteServiceInvoker
{
public static ReaderWriterLock Locker = new ReaderWriterLock();
public static Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> InvokerCache = new Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> (); public string InvokeService(RemoteServiceInvocation invocation)
{
AssertUtility.NotNull(invocation);
AssertUtility.ArgumentHasText(invocation.ContractName, "service contract name");
AssertUtility.ArgumentHasText(invocation.MethodName, "service method name"); var service = Activator.Context.GetFirstOrDefaultService(invocation.ContractName);
string msg = string.Empty;
if(service == null)
{
msg = string.Format ("Remote Service '{0}' not found.", invocation.ContractName);
FileLogUtility.Warn (msg);
throw new Exception(msg);
} System.Tuple<MethodInfo, Type[], MethodInvoker> invokerTuple;
using (var locker = ReaderWriterLockHelper.CreateReaderLock (Locker))
{
InvokerCache.TryGetValue (invocation.Key, out invokerTuple);
} if (invokerTuple == null)
{
Type serviceType = service.GetType (); var serviceMethodInfo = serviceType.GetMethod (invocation.MethodName);
if (serviceMethodInfo == null)
{
msg = string.Format ("The method '{1}' of the remote service '{0}' not found.", invocation.ContractName, invocation.MethodName);
FileLogUtility.Warn (msg);
throw new Exception (msg);
} if (invocation.JsonSerializedParameters == null) {
invocation.JsonSerializedParameters = new List<string> ();
} var parameterInfos = serviceMethodInfo.GetParameters ();
if (invocation.JsonSerializedParameters.Count != parameterInfos.Length)
{
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);
FileLogUtility.Warn (msg);
throw new Exception (msg);
} var parameterTypes = new Type[parameterInfos.Length];
for (int i = ; i < parameterInfos.Length; i++)
{
parameterTypes [i] = parameterInfos [i].ParameterType;
} try
{
var methodInvoker = serviceType.DelegateForCallMethod (invocation.MethodName, parameterTypes); invokerTuple = new System.Tuple<MethodInfo, Type[], MethodInvoker> (serviceMethodInfo, parameterTypes, methodInvoker); using (var locker = ReaderWriterLockHelper.CreateWriterLock (Locker))
{
if (!InvokerCache.ContainsKey (invocation.Key))
{
InvokerCache [invocation.Key] = invokerTuple;
}
}
}
catch(Exception ex)
{
msg = string.Format ("Failed to create delegate method for the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName);
FileLogUtility.Warn (msg);
FileLogUtility.Warn (ex);
throw new Exception (msg, ex);
}
} var paramters = new object[invokerTuple.Item2.Length]; for(int i = ; i < invokerTuple.Item2.Length; i++)
{
try
{
paramters[i] = JSON.ToObject(invocation.JsonSerializedParameters[i], invokerTuple.Item2[i]);
}
catch(Exception ex)
{
msg = string.Format ("Failed to unserialize the '{0}'th parameter for the method '{1}' of service '{2}'.", i + , invocation.MethodName, invocation.ContractName);
FileLogUtility.Warn (msg);
FileLogUtility.Warn (ex);
throw new Exception (msg, ex);
}
} try
{
return JSON.ToJSON(invokerTuple.Item3(service, paramters));
}
catch(Exception ex)
{
msg = string.Format("Failed to invoke the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName);
FileLogUtility.Warn (msg);
FileLogUtility.Warn (ex);
throw new Exception (msg, ex);
}
}
}
}
3.1.2 远程服务客户端插件的实现
接下来,我们看看远程服务客户端插件的实现。它定义了一个IRemoteServiceProxyService服务,暴露了两个接口分别用于对远程服务和负载均衡远程服务的调用。

该服务的远程服务获取实现如下所示。

它仅仅时通过DynamicProxy创建远程服务代理类,此时,对代理类方法的调用会转换成远程服务调用。下面看看拦截机的实现。
class RemoteServiceProxyInterceptor : IInterceptor, IDisposable
{
private RemoteServiceContext _remoteServiceContext;
private RemoteServiceClient _remoteServiceClient;
public RemoteServiceProxyInterceptor(RemoteServiceContext context)
{
_remoteServiceContext = context;
_remoteServiceClient = new RemoteServiceClient(_remoteServiceContext.IPAddress, _remoteServiceContext.Port);
_remoteServiceClient.Start();
}
public void Intercept(IInvocation invocation)
{
try
{
var jsonParameters = new List<string>();
foreach (var param in invocation.Arguments)
{
jsonParameters.Add(JSON.ToJSON(param));
} var resultJson = _remoteServiceClient.Invoke(invocation.Method.DeclaringType.FullName, invocation.Method.Name, jsonParameters); if (!invocation.Method.ReturnType.FullName.Equals("System.Void"))
{
invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType);
}
else
{
invocation.ReturnValue = null;
}
}
catch(Exception ex)
{
FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'",
invocation.Method.DeclaringType.FullName, invocation.Method.Name));
throw;
}
} public void Dispose()
{
if (_remoteServiceClient != null)
{
_remoteServiceClient.Stop();
_remoteServiceClient = null;
}
}
}
拦截机的代码很简单,对远程服务代理类方法的调用将直接转换成对远程服务宿主插件的WCF服务的调用。下面看看负载均衡远程服务的实现。
3.2 负载均衡远程服务的实现
有了以上的技术,关于负载均衡远程服务的实现,就简单多了。负载均衡远程服务的目的就是将所有远程服务统一在负载均衡服务器进行注册,并实现负载的动态管理。因此,需要在远程服务基础上,创建一个负载均衡服务器和负载均衡客户端。负载均衡服务器用于管理所有远程服务及提供远程服务的机器,管理所有远程服务的负载情况,并实现负载均衡及故障转移;负载均衡客户端的目的时将远程服务注册到负载均衡器,并且当远程服务关闭时从负载均衡器卸载。下面,看看负载均衡器的实现。
3.2.1 负载均衡器实现
负载均衡服务器暴露了如下WCF服务。这个服务用于提供远程服务注册和卸载以及负载均衡请求。

这个服务的实现如下所示。

它使用远程服务注册表来实现远程服务的管理和负载均衡的实现。
3.2.2 负载均衡客户端的实现
负载均衡客户端的目的时实现远程服务的注册与卸载,通过该插件将远程服务暴露到负载均衡服务器。这样服务调用者就可以通过负载均衡器来调用远程服务。

3.2.3 负载均衡远程服务调用
负载均衡远程服务的调用方式的实现和远程服务类似。它由远程服务代理服务的GetFirstOrDefaultLoadBalancerService接口来实现。

该接口的实现如下所示,主要时创建代理和方法拦截机。

这个方法调用拦截机会将方法调用转化为:(1)从负载均衡服务器获取均衡的远程服务主机;(2)直接调用该远程服务主机的服务,如果调用失败则尝试进行重新负载均衡。其实现如下所示。
class RemoteServiceLoadBalancerProxyInterceptor : IInterceptor, IDisposable
{
private string LoadBalancerHost
{
get
{
return ConfigurationSettings.AppSettings["LoadBalancerHost"];
}
} private string LoadBalancerPort
{
get
{
return ConfigurationSettings.AppSettings["LoadBalancerPort"];
}
} private LoadBalancerContext _remoteServiceLoadBalancerContext;
private RemoteServiceClient _remoteServiceClient;
private RemoteServiceLoadBalancerAccessClient _remoteServiceLoadBalancerAccessClient;
private bool _initialized; public RemoteServiceLoadBalancerProxyInterceptor(LoadBalancerContext context)
{
_remoteServiceLoadBalancerContext = context; if (string.IsNullOrEmpty(LoadBalancerHost))
{
throw new Exception("You need to specified the load balancer host (HostName or IP Address) by app setting 'LoadBalancerHost'.");
} int loadBalancerPortInt;
if (!int.TryParse(LoadBalancerPort, out loadBalancerPortInt))
{
throw new Exception("You need to specified the load balancer port by app setting 'LoadBalancerPort'.");
} try
{
_remoteServiceLoadBalancerAccessClient = new RemoteServiceLoadBalancerAccessClient (LoadBalancerHost, loadBalancerPortInt);
_remoteServiceLoadBalancerAccessClient.Start ();
}
catch(Exception ex)
{
FileLogUtility.Error (string.Format("Faild to connect to load balancer '{0}'.", _remoteServiceLoadBalancerContext));
FileLogUtility.Error (ex);
throw;
}
} private bool Initialize(string serviceContractName)
{
if(_remoteServiceClient != null)
{
_remoteServiceClient.Stop();
} RemoteServiceHost remoteHost = null;
try
{
remoteHost = _remoteServiceLoadBalancerAccessClient.Balance(serviceContractName);
FileLogUtility.Inform(string.Format("Get the remote service host '{0}' by load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext));
}
catch(Exception ex)
{
FileLogUtility.Error (string.Format("Faild to get a remote service host by load balancer '{0}'.", _remoteServiceLoadBalancerContext));
FileLogUtility.Error (ex);
return false;
}
if (remoteHost != null)
{
_remoteServiceClient = new RemoteServiceClient (remoteHost.IPAddress, remoteHost.Port);
try
{
_remoteServiceClient.Start ();
return true;
}
catch(Exception ex)
{
FileLogUtility.Error (string.Format("Failed to connect to the remote service host '{0}' by using load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext));
}
} return false;
} public void Intercept(IInvocation invocation)
{
var serviceContractName = invocation.Method.DeclaringType.FullName;
if (!_initialized)
{
_initialized = Initialize (serviceContractName);
if (!_initialized)
{
invocation.ReturnValue = null;
return;
}
} var jsonParameters = new List<string>();
foreach (var param in invocation.Arguments)
{
jsonParameters.Add(JSON.ToJSON(param));
} int tryTimes = ; for (int i = ; i < tryTimes; i ++ )
{
try
{
var resultJson = _remoteServiceClient.Invoke(serviceContractName, invocation.Method.Name, jsonParameters); if (!invocation.Method.ReturnType.FullName.Equals("System.Void"))
{
invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType);
}
else
{
invocation.ReturnValue = null;
}
return;
}
catch(Exception ex)
{
FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'",
serviceContractName, invocation.Method.Name));
FileLogUtility.Error (ex);
if (i == tryTimes)
{
throw;
}
if (!((_initialized = Initialize (serviceContractName)) == true)) // 重新Balance
{
throw;
}
}
}
} public void Dispose()
{
if (_remoteServiceClient != null)
{
_remoteServiceClient.Stop();
_remoteServiceClient = null;
}
}
}
4 小结
在这篇文章,我详细介绍了支持集群的远程服务的实现。你可以发现,整体实现完全按照模块化组装的方式。你也可以尝试来考虑以模块化组装的方法实现一个远程服务集群。多谢支持~~。
分享在Linux下使用OSGi.NET插件框架快速实现一个分布式服务集群的方法的更多相关文章
- Linux下查看内核、CPU、内存及各组件版本的命令和方法
Linux下查看内核.CPU.内存及各组件版本的命令和方法 Linux查看内核版本: uname -a more /etc/*release ...
- 高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群
高性能Linux服务器 第11章 构建高可用的LVS负载均衡集群 libnet软件包<-依赖-heartbeat(包含ldirectord插件(需要perl-MailTools的rpm包)) l ...
- .net core下简单构建高可用服务集群
一说到集群服务相信对普通开发者来说肯定想到很复杂的事情,如zeekeeper ,反向代理服务网关等一系列的搭建和配置等等:总得来说需要有一定经验和规划的团队才能应用起来.在这文章里你能看到在.net ...
- rabbitmq集群搭建方法简介(测试机linux centos)【转】
本文将介绍四台机器搭建rabbitmq集群: rabbitmq IP和主机名(每台机器已安装RabbitMQ 3.5.6, Erlang 18.1) 192.168.87.73 localhost73 ...
- 分享:linux下apache服务器的配置和管理
linux下apache服务器的配置和管理. 一.两个重要目录: Apache有两个重要的目录:1.配置目录/etc/httpd/conf:2.文档目录/var/www: 二.两种配置模式: Apac ...
- elasticsearch插件安装之--linux下安装及head插件
/** * 系统环境: vm12 下的centos 7.2 * 当前安装版本: elasticsearch-2.4.0.tar.gz */ 安装和学习可参照官方文档: 1, 安装 # 下载, 获取不成 ...
- linux下 tar解压 gz解压 bz2等各种解压文件使用方法
http://alex09.iteye.com/blog/647128 大致总结了一下linux下各种格式的压缩包的压缩.解压方法. .tar 解包:tar xvf FileName.tar 打包:t ...
- 转Linux 下用alias 设置命令别名快速切换常用命令
https://blog.csdn.net/u012830148/article/details/80618616 在linux下开发,经常需要切换目录,如果目录很长则切换起来非常的麻烦,针对一些常用 ...
- Linux下使用Vi是方向键变乱码 退格键不能使用的解决方法
在Linux下编辑一些文件.这就涉及到了vi这个编辑器了.在Linux下,初始使用vi的时候有点问题.就是在编辑模式下使用方向键的时候,并不会使光标移动,而是在命令行中出现[A [B [C [D之类的 ...
随机推荐
- Java初始化过程
这篇文章主要讲解Java在创建对象的时候,初始化的顺序.主要从以下几个例子中讲解: 继承关系中初始化顺序 初始化块与构造器的顺序 已经加载过的类的初始化顺序 加载父类,会不会加载子类 创建子类对象会不 ...
- React在开发中的常用结构以及功能详解
一.React什么算法,什么虚拟DOM,什么核心内容网上一大堆,请自行google. 但是能把算法说清楚,虚拟DOM说清楚的聊聊无几.对开发又没卵用,还不如来点干货看看咋用. 二.结构如下: impo ...
- SQL Server-聚焦计算列或计算列持久化查询性能(二十二)
前言 上一节我们详细讲解了计算列以及计算列持久化的问题,本节我们依然如前面讲解来看看二者查询性能问题,简短的内容,深入的理解,Always to review the basics. 持久化计算列比非 ...
- 界面设计技法之css布局
css布局之于页面就如同ECMAScript之于JS一般,细想一番,html就如同语文,css就如同数学,js呢,就是物理,有些扯远,这里就先不展开了. 回到主题,从最开始的css到如今的sass(l ...
- ADO.NET编程之美----数据访问方式(面向连接与面向无连接)
最近,在学习ADO.NET时,其中提到了数据访问方式:面向连接与面向无连接.于是,百度了一下,发现并没有很好的资料,然而,在学校图书馆中发现一本好书(<ASP.NET MVC5 网站开发之美&g ...
- 体验报告:微信小程序在安卓机和苹果机上的区别
很多人可能会问:微信小程序和在微信里面浏览一个网页有什么区别? 首先,小程序的运行是全屏的,界面跟进入了一个APP很像,更为沉浸跟在微信里面访问h5不一样:其次,它的浏览体验更为稳定. 不过,这还不够 ...
- BPM配置故事之案例11-操作外部数据源
小明:可以获取ERP数据了-- 老李:哦,这么快?小伙子,我非常看好你,来来,别急着走,再陪我聊会-- 小明:--您老人家不是又要改流程吧? 老李:没有没有,哎嘿嘿嘿,我们这不都是为公司效率着想嘛,这 ...
- 【SAP业务模式】之ICS(二):基础数据
讲完业务,计划在前台做一下ICS的基本操作,不过在操作之前,得先建立好基本的基础数据. 1.首先创建接单公司LEON,对应工厂是ADA: 2.创建生产公司MXPL,对应工厂是PL01: 3.创建接单公 ...
- 查看mac中磁盘空间占用情况
今天发现磁盘空间不够了,首先要找到那些文件夹占用了磁盘空间. du命令很好使 du -c -d 1 -m | sort -n -c 显示当前文件夹总计占用空间 -d 1 层级为1,即只显示当前目录下一 ...
- 如何手动安装MySql
想安装当然要先有一个MySql的安装包 这里使用的是mysql-5.7.12-winx64 安装包百度云:http://pan.baidu.com/s/1kVAuXuv 密码:hr39 1.要将压缩 ...