欢迎您访问高等教育自学考试信息服务网平台!

分布式系统的优缺点

更新时间:2023-11-21 22:44:12作者:自考教育网

说到分布式系统,就不得不说集中式系统。在传统的集中式系统中,整个项目的一切都在一个应用程序中。

网站是一个应用程序。当系统压力很大时,只能横向扩展,增加多个服务器或容器来平衡负载,避免单点故障影响整个系统。集中化最明显的好处就是开发、测试、运维会更方便,不用考虑复杂的分布式环境。缺点也很明显。系统庞大而复杂,难以扩展和维护,所有应用程序每次都必须更新。

分布式系统的优缺点

鉴于集中式系统的缺点,分布式系统应运而生。分布式系统是由背后的一系列计算机组成的,但用户无法感知背后的逻辑。就像访问单机一样,自然避免了单机故障的问题。应用可以按照业务类型拆分成多个应用或服务,再按照结构分为接口层和服务层。我们还可以根据接入定义不同的接口应用入口分,比如移动端和PC端。数据库可以按业务类型拆分成多个实例,单个表可以分为数据库和表。同时增加了分布式缓存、消息队列、非关系数据库、搜索等中间件。分布式系统虽然好,但是增加了系统的复杂性,比如分布式事务、分布式锁、分布式会话、数据一致性等。这些都是分布式系统中需要解决的难题。分布式系统也增加了开发和测试运维的成本,工作量增加。如果管理不好,就会变成负担。

分布式系统拓扑图分布式系统的核心是分布式服务框架。有了分布式服务框架,我们只需要关注各自的业务,而不需要关注那些复杂服务之间的调用过程。

分布式服务框架目前业内比较流行的分布式服务框架有:阿里的Dubbo和Spring Cloud。与其在这里比较这些分布式服务框架,简单的说一下他们都做了什么,可以让我们使用远程服务像调用本地服务一样简单高效。

服务是一个可以向用户输出功能的模块。基于该技术框架,可以实现用户的需求。例如,日志服务、权限管理服务、后台服务、配置服务、缓存服务、存储服务、消息服务等。这些服务可以灵活组合或独立运行。该服务需要一个接口来与系统交互。面向服务的开发应该是分别开发服务,再组合起来。更直接的例子有:历史详情、留言板、评论、评级服务等。它们可以独立运作,也可以组合成一个整体。

注册中心注册中心在整个分布式系统的集成中起着核心作用。它支持对等集群,需要提供CRUD接口,支持订阅和发布机制,可靠性要求非常高。通常,Zookeeper集群被用作注册中心。在分布式环境中,服务提供商的服务将部署在多个服务器上,每个服务器将提供服务提供商标识、服务列表、地址、对应端口、序列化协议等信息。到注册表。注册中心记录服务和服务地址之间的映射关系。通常,一个服务对应多个地址。这个过程称为服务发布或服务注册。服务调用者将获得所需服务的信息(地址和端口信息、序列化协议等。)根据服务标识符和服务列表从注册表中下载,服务列表将在本地缓存。当一个服务需要调用其他服务时,直接在这里找到该服务的地址并调用。这个过程称为服务发现。

下面是Zookeeper作为注册中心的一个简单实现:

/** * 创建结节节点* @ param node * @ param data */public boolean create node(String node,String data){ try { byte[]bytes=data。getbytes();//同步创建临时顺序节点字符串路径=ZK。创造(ZK常数.ZK _ RPC _数据_路径'/'节点'-'字节,ZooDefs .身份证。OPEN_ACL_UNSAFE,CreateMode .短暂_连续);日志。info(' create zookeeper node({ }={ })'path,data);} catch(keeper异常e){ log。错误(“”,e);返回false} catch(中断异常ex){ log。错误(“”,例如);返回false}返回真实}

子节点一

子节点2如下面动物园管理员中写入的临时顺序节点信息:

com。黑色。黑色RPC。测试。你好词:发布服务时对外的名称ZK顺序节点身份证.127.0.0.1:8888,127.0.0.1:8889:服务地址端口原型材料:序列化方式。1.0:权值,负载均衡策略使用。这里使用的是动物园管理员的临时顺序节点,为什么使用临时顺序节点,主要是考虑以下两点:

当服务提供者异常下线时,与动物园管理员的连接会中断,动物园管理员服务器会主动删除临时节点,同步给服务消费者。这样就能避免服务消费者去请求异常的服务器。校稿注:一般消费方也会在实际发起请求前,对当前获取到的服务提供方节点进行心跳,避免请求连接有问题的节点动物园管理员。下面是不允许创建2个名称相同的ZK子节点的,通过顺序节点就能避免创建相同的名称。当然也可以不用顺序节点的方式,直接以com。黑色。黑色RPC。测试。你好词创建节点,在该节点下创建数据节点。下面是ZK的数据同步过程:

/** * 同步节点(通知模式)*同步节点会通过级联方式,在每次看守人被触发后,就会再挂上新的观察者。完成了类似链式触发的功能*/public boolean syncNodes(){ try { List nodeList=ZK。得到孩子(ZK常数.ZK _ RPC _数据_路径,新观察者(){ @覆盖公共空的进程(观察事件事件){ if (event.getType()==事件事件类型。子节点已更改){ sync nodes();} } });map map=new HashMap();for(String node:nodeList){ byte[]bytes=ZK。获取数据(ZK常数.ZK _ RPC _数据_路径'/'节点,false,null);String key=node.substring(0,node.lastIndexOf(ZkConstant .定界_标记));字符串值=新字符串(字节);对象对象=贴图。get(键);如果(反对!=null){((列表)对象)。添加(值);} else { List dataList=new ArrayList();dataList.add(值);map.put(key,dataList);} log.info('节点:[{}]数据:[{}]'节点,新字符串(字节));} /**修改连接的地址缓存*/if(map util。isnotempty(map)){ log。调试('调用服务缓存更新.');invokingservicecache。updatainvokingservicemap(地图);}返回true} catch(keeper异常|中断异常e){ log。错误(e . tostring());返回false} }当数据同步到本地时,一般会写入到本地文件中,防止因动物园管理员集群异常下线而无法获取服务提供者信息。

通信协议服务消费者需要有一个网络连接来传输数据,无论是与注册中心还是与服务提供商,这涉及到通信。作者以前做过这项工作。当时用java BIO只是简单的写一个通讯包。在使用场景中没有太多的并发性,阻塞BIO也没有暴露太多的问题。Java会在建立连接后阻止线程等待数据,这必须是以线程对线程的方式,即当客户端有连接请求时,服务器需要启动一个线程进行处理。连接数太多时,会建立相当数量的线程,性能会直线下降。Java NIO:同步是非阻塞的,服务器实现方式是一个请求一个线程,即客户端发送的所有连接请求都注册在复用器上,复用器只在轮询到I/O请求连接时才启动一个线程进行处理。Java AIO:异步和非阻塞。服务器实现模式是一个线程一个有效请求。客户端的I/O请求都是由OS先完成,然后通知服务器应用启动线程进行处理。BIO、NIO和AIO的适用情景分析;

BIO:用于连接数量少且固定的体系结构。这种方法对服务器资源的要求很高,其并发性仅限于应用程序,但程序直观、简单、易懂。NIO:适用于连接数量多,连接短(轻操作)的架构,比如聊天服务器。并发仅限于应用程序,编程也很复杂。目前主流的通信框架Netty、Apache Mina、Grizzl、NIO框架都是基于它们的实现。AIO:用于连接数量多、连接长(操作量大)的架构,比如镜像服务器、文件传输等。完全调用OS参与并发操作,编程复杂。作为沟通的基石,其实要考虑的东西很多。如:丢包和粘包、心跳机制、断开连接和重新连接、消息缓存重传、优雅释放资源、长连接或短连接等。下面是Netty建立服务器和客户端的简单实现:

导入io。妮蒂。自举。服务器引导;导入io。妮蒂。渠道。通道初始值设定项;导入io。妮蒂。渠道。渠道管道;导入io。妮蒂。渠道。事件循环组;导入io。妮蒂。渠道。nio。nioeventloopgroup导入io。妮蒂。渠道。插座。套接字通道;导入io。妮蒂。渠道。插座。nio。nioserversocketchannel导入io。妮蒂。处理程序。编解码器。lengthfieldbasedframedecoder导入io。妮蒂。处理程序。编解码器。lengthfieldprender导入io。妮蒂。处理程序。编解码器。字节。bytearraydecoder导入io。妮蒂。处理程序。编解码器。字节。bytearray编码器;导入org。slf4j。记录者;导入org。SLF 4j。伐木工厂;/*** * netty tcp服务端* @作者v _王诗雨* */public class NettyTcpService { private static final Logger log=Logger factory。获取记录器(NettyTcpService。类);私有字符串主机;专用(同Internationalorganizations)国际组织端口;公共NettyTcpService(字符串地址)引发异常{ String str[]=地址。拆分('');这个。host=str[0];这个。端口=整数。(str[1])的值;} public NettyTcpService(字符串主机,int端口)抛出异常{ this . host=host this . port=port }/* *用于分配处理业务线程的线程组个数*/private static final int BIZGROUPSIZE=runtime。获取运行时().可用的处理器()* 2;//默认/** 业务出现线程大小*/private static final int BIZTHREADSIZE=4;/* * NioEventLoopGroup实际上就是个线程,* NioEventLoopGroup在后台启动了n个NioEventLoop来处理频道事件, * 每一个NioEventLoop负责处理m个频道,* NioEventLoopGroup从NioEventLoop数组里挨个取出NioEventLoop来处理channel */private静态最终事件loop group boss group=new NioEventLoopGroup(BIZGROUPSIZE);private static final event loop group worker group=new NioEventLoopGroup(BIZTHREADSIZE);公共void开始()引发异常{ log.info('Netty Tcp服务运行.');服务器引导b=新服务器引导();b.group(老板集团、工人集团);b .通道(nioserversocketchannel。类);b .子处理程序(new ChannelInitializer(){ @ Override public void init channel(socket channel ch))抛出异常{通道管道管道=ch。管道();pipeline.addLast('frameDecoder),new LengthFieldBasedFrameDecoder(Integer .MAX_VALUE,0,4,0,4));pipeline.addLast('frameEncoder '新长度字段前置器(4));pipeline.addLast('decoder 'new bytearray decoder());pipeline.addLast('encoder 'new bytearray encoder());//pipeline.addLast(新编码器());//管道。添加last(new Decoder());pipeline.addLast(新TCP服务器处理程序());} });绑定(主机,端口)。sync();log.info('Netty Tcp服务成功!');} /** * 停止服务并释放资源*/public void shut down(){ worker group。优雅关机();boss团。优雅关机();}}导入org。slf4j。记录者;导入org。SLF 4j。伐木工厂;导入io。妮蒂。渠道。channelhandlercontext导入io。妮蒂。渠道。simplechannelinboundhandler/** * 服务端处理器*/公共类TCP服务器处理程序扩展SimpleChannelInboundHandler {私有静态最终记录器log=记录器工厂。获取记录器(TCP服务器处理程序。类);@ Override protected void channel read 0(ChannelHandlerContext CTX,对象消息)抛出异常{ byte[]data=(byte[])msg;}}

导入org。slf4j。记录者;导入org。SLF 4j。伐木工厂;导入io。妮蒂。自举。自举;导入io。妮蒂。渠道。渠道;导入io。妮蒂。渠道。通道初始值设定项;导入io。妮蒂。渠道。频道开发;导入io。妮蒂。渠道。渠道管道;导入io。妮蒂。渠道。事件循环组;导入io。妮蒂。渠道。nio。nioeventloopgroup导入io。妮蒂。渠道。插座。nio。niosocketchannel导入io。妮蒂。处理程序。编解码器。lengthfieldbasedframedecoder导入io。妮蒂。处理程序。编解码器。lengthfieldprender导入io。妮蒂。处理程序。编解码器。字节。bytearraydecoder导入io。妮蒂。处理程序。编解码器。字节。bytearray编码器;导入io。妮蒂。util。并发。未来;/** * netty tcp客户端* @作者v _王诗雨* */public class NettyTcpClient { private static final Logger log=Logger factory。获取记录器(NettyTcpClient。类);私有字符串主机;专用(同Internationalorganizations)国际组织端口;私有自举自举;私信渠道;私有EventLoopGroup组;public NettyTcpClient(String host,int port){ bootstrap=get bootstrap();channel=getChannel(主机,端口);this.host=hostthis.port=port}公共字符串getHost(){ return host;} public int get port(){ return port;} /** * 初始化Bootstrap * @ return */public final Bootstrap get Bootstrap(){ group=new NioEventLoopGroup();Bootstrap b=new Bootstrap();b .集团(集团)。频道(niosocketchannel。类);b . handler(new ChannelInitializer(){ @ Override protected void init Channel(Channel ch))抛出异常{通道管道管道=ch。管道();//pipeline.addLast(新编码器());//管道。添加last(new Decoder());pipeline.addLast('frameDecoder),new LengthFieldBasedFrameDecoder(Integer .MAX_VALUE,0,4,0,4));pipeline.addLast('frameEncoder '新长度字段前置器(4));pipeline.addLast('decoder 'new bytearray decoder());pipeline.addLast('encoder 'new bytearray encoder());pipeline.addLast('handler 'new TcpClientHandler());} });乙。选项(频道开发.SO_KEEPALIVE,true);返回b;} /** * 连接,获取Channel * @ param host * @ param port * @ return */public final Channel get Channel(String host,int port){ Channel Channel=null;试试{ channel=bootstrap.connect(主机,端口)。同步()。channel();返回通道;} catch(异常e) { log.info(String.format('连接服务器(IP[%s],端口[%s])失败!'主机、端口));返回null} } /** *发送消息* @ param msg * @ throws Exception */public boolean send msg(对象消息)抛出异常{如果(频道!=null){ channel。writeandflush(msg).sync();log.debug('消息刷新成功');返回true }否则{ log.debug('msg flush fail,connect为null’);返回false} } /** *连接断开* 并且释放资源* @ return */public boolean disconnect connect(){//channel。关闭().awaitUninterruptibly();未来未来=集团。关机优雅();//优雅地关闭释放所有资源,并且关闭所有当前正在使用的渠道未来。sync interruptible();返回true} }导入org。slf4j。记录者;导入org。SLF 4j。伐木工厂;导入io。妮蒂。渠道。channelhandlercontext导入io。妮蒂。渠道。simplechannelinboundhandler/** * 客户端处理器*/公共类TcpClientHandler扩展SimpleChannelInboundHandler {私有静态最终记录器log=记录器工厂。获取记录器(TcpClientHandler。类);@ Override protected void channel read 0(ChannelHandlerContext CTX,对象消息)抛出异常{ byte[]data=(byte[])msg;}}说到通讯就不能不说协议,通信时所遵守的规则,访问什么,传输的格式等都属于协议。作为一个开发人员,应该都了解传输控制协议协议,它是一个网络通信模型,以及一整套网络传输协议家族,是互联网的基础通信架构。也都应该用过Http(超文本传输协议),Web服务器传输超文本到本地浏览器的传送协议,该协议建立在传输控制协议协议之上。分布式服务框架服务间的调用也会规定协议。

为了支持不同的场景,分布式服务框架中有很多协议,比如Dubbo,它支持七种协议:Dubbo(默认)、RMI、Hessian、HTTP、WebService、Thrift、Memcached和Redis。每个协议处理不同的场景,应该详细处理特定的场景。

路由分布式服务在线时由集群组网部署,集群中会有某个服务的多个实例。消费者如何从服务列表中选择合适的服务提供者进行调用涉及到服务路由。分布式服务框架需要能够满足用户灵活的路由需求。透明路由很多开源RPC框架调用者需要配置服务提供者的地址信息。虽然通过读取数据库中的服务地址列表可以避免硬编码的地址信息,但消费者仍然要感知服务提供者的地址信息,这违反了透明路由原则。基于服务注册中心的服务订阅发布,消费者可以通过主动查询和被动通知的方式获取服务提供者的地址信息,而不需要硬编码。你只需要知道当前系统发布了哪些服务,而不需要知道服务存在于哪里。这是透明路由。负载均衡负载均衡策略是服务的一个重要属性。分布式服务框架通常提供多种负载平衡策略,并支持用户扩展负载平衡策略。一般在对等集群组网中,随机算法用于负载均衡,随机路由算法平均分配消息。JDK提供的java.util.Random或java.security.SecureRandom用于在指定服务提供商列表中生成随机地址。基于消费者随机生成的服务提供商地址的远程调用;

/* * * random */public class random策略实现集群策略{ @ override public remote service base select(list list){ int max _ len=list . size();int index=random util . nextint(MAX _ LEN);返回list . get(index);}}随机性还是有缺点的,部分节点碰撞的概率较高。另外,当硬件配置差异较大时,各个节点的负载也会不均衡。要避免这些问题,需要对服务列表进行加权,性能好的机器接收请求的概率要高于普通机器:

/* * *加权random */public class weighting random strategy实现集群策略{ @ override public remote service base select(List List){//store weighted service provider List List weighting List=new ArrayList();for(remoteservicebase remoteservicebase:list){//放大10倍int weight=(int)(remoteservicebase . getweight()* 10);for(int I=0;I体重;I){ weighting list . add(remoteServiceBase);} } int MAX _ LEN=weighting list . size();int index=random util . nextint(MAX _ LEN);返回weighting list . get(index);}}逐个轮询服务地址,到达边界后继续回绕。缺点:缓慢的提供者积累请求。比如二机慢,但是不挂。当我要求第二台机器时,我被卡住了。随着时间的推移,所有的请求都会在第二台机器上被阻塞。轮询策略的实现非常简单。依次遍历服务提供商列表。到达边界后,复位至零,序列循环继续:

/* * * polling */public class polling策略实现集群策略{//counter private int index=0;私有锁Lock=new reentrant Lock();@ Override public RemoteServiceBase select(List List){ RemoteServiceBase service=null;try { lock.tryLock(10,TimeUnit。毫秒);//如果计数大于服务提供者的数量,则在If(index=list . size()){ index=0;} service=list . get(index);指数;} catch(interrupted exception e){ e . printstacktrace();}最后{ lock . unlock();}//底出,保证程序的健壮性。如果没有获得服务,直接取第一个if(service==null){ service=list . get(0);}退货服务;}}对于加权轮询,您需要为服务地址增加权重:

/* * *加权轮询*/公共类加权轮询策略实现集群策略{//counter private int index=0;//计数器锁私有锁=new reentrant lock();@ Override public RemoteServiceBase select(List List){ RemoteServiceBase service=null;try { lock.tryLock(10,TimeUnit。毫秒);//Store加权服务提供者列表List weighting List=new ArrayList();for(remoteservicebase remoteservicebase:list){//放大10倍int weight=(int)(remoteservicebase . getweight()* 10);for(int I=0;I体重;I){ weighting list . add(remoteServiceBase);} }//如果计数大于服务提供者的数量,则在If(index=weighting list . size()){ index=0;} service=weighting list . get(index);指数;退货服务;} catch(interrupted exception e){ e . printstacktrace();}最后{ lock . unlock();}//确保程序是健壮的。如果没有得到服务,直接取第一个返回list . get(0);}}服务调用延迟消费者缓存所有服务提供者的调用延迟,并定期计算平均服务调用延迟。然后计算服务调用延迟与每个服务提供者平均延迟的差值,并根据差值动态调整权重,以保证服务延迟大的服务提供者收到的消息少,防止消息堆积。该策略的特点是:保证处理能力强的服务接收到更多的消息,通过动态权重分配消除服务调用延迟的振荡范围,使所有服务的调用延迟接近平均值,实现负载均衡。具有相同散列参数的请求总是被发送到统一服务提供商。当一个服务提供者关闭时,最初发送到根提供者的请求基于虚拟节点与其他提供者平均共享,这不会引起剧烈的变化。平台提供默认的虚拟节点数量,虚拟节点数量可以通过配置文件修改。一致哈希环的工作原理如下图所示:

一致哈希路由规则的负载均衡只能保证服务提供商压力的均衡,但在一些业务场景下需要设置一些过滤规则,常用的是带基本表达式的条件路由。通过IP条件表达式配置黑白名单访问控制:consumerIP!=192.168.1.1。只有一些服务提供者被暴露,以防止该集群中的所有服务被淘汰,从而导致其他服务不可用。

例如providerIP=192.168.3*。读写分离:方法=find *、list *、get *、query *=提供者IP=192.168.1.前景与背景分离:app=web=ProviderIP=192.168.1。app=Java=ProviderIP=192.168.2。灰度:将WEB前台应用到新的服务版本:app=WEB app=WEB=provice rip=192 . 168 . 1。*.*.

序列化和反序列化将对象转换为字节序列的过程称为序列化,将字节序列恢复为对象的过程称为反序列化。调用过程时,我们需要先序列化Java对象,然后通过网络和IO传输。当我们到达目的地时,我们将对其进行反序列化以获得所需的结果对象。在分布式系统中,传输的对象会很多,这就需要一种序列化速度快、字节序列小的序列化技术。序列化技术:Serializable,XML,Jackson,MessagePack,FastJson,Protocol Buffer,Thrift,Gson,Avro,Hessian等。Serializable是Java自己的序列化技术,不能跨平台,序列化和反序列化的速度比较慢。XML技术具有良好的跨平台支持,常用于与银行交互的消息,但其字节序列较大,不适合分布式通信框架。FastJson是一个用Java语言编写的高性能Json处理器,由阿里巴巴开发。它的字节序列是JSON字符串,可读性好,序列化速度非常快。协议缓冲区的序列化速度非常快,字节序列也很小,但可读性较差。通用分布式服务框架将有多种内置的序列化协议可供选择。例如,Dubbo支持的七种协议使用不同的序列化技术。

在调用服务的本地环境中,使用一个接口非常简单,直接调用就可以了。在分布式环境中,事情就没那么简单了。消费者只有接口的定义,没有具体的实现。如果要在本地环境中直接调用远程接口,就要费点功夫,需要使用远程代理。这是我偷的照片:

远程通信序列如下:

通信消费者没有具体的实现,所以调用接口时需要动态创建一个代理类。在与Spirng集成的情况下,它在构建Bean时被直接注入到代理类中。下面是如何构建代理类:

导入Java . lang . reflect . proxy;public Class JDK proxy { public static Object getInstance(Class cls){ JdkMethodProxy invocation handler=new JdkMethodProxy();object newProxyInstance=proxy . newProxyInstance(cls . get Class loader(),new Class[] { cls },invocation handler);return(Object)newProxyInstance;}}

导入Java . lang . reflect . invocation handler;导入Java . lang . reflect . method;公共类JdkMethodProxy实现invocation handler { @ Override public Object invoke(Object proxy,Method method,Object[]parameters)Throwable {//如果传入的是实现的具体类If(Object . Class . equals(Method . get declaring Class())){ try { return Method . invoke(this,parameters);} catch(Throwable t){ t . printstacktrace();}//如果传入一个接口} else {//实现接口的核心方法//return remote invoking . invoking(service name,serializationtype,//timeout,loadbalancestrategy,method,parameters);}返回null}}代理会做很多事情,比如序列化被请求服务的名称和参数信息,通过路由选择最合适的服务提供者,建立通信连接发送被请求的信息(或者直接发起Http请求),最后返回得到的结果。当然还有很多问题需要考虑,比如调用超时,请求异常,通信连接的缓存,同步服务调用还是异步服务调用等等。同步服务调用:客户端发起远程服务调用请求。用户线程完成消息序列化后,将消息传递给通信框架,然后同步阻塞。等待通信线程发送请求并收到响应后,唤醒同步等待的用户线程,用户线程收到响应后返回。异步服务调用:基于Java的Future机制,客户端发起远程服务调用请求,会用RequestId进行标记,建立一个与RequestId对应的Future。当客户端通过未来的Get方法得到结果时,就会被阻塞。当服务器收到请求时,它应该返回RequestId,这将解锁相应的Future并设置相应的结果。最后,客户端得到结果。以RequestId为关键字构建未来,并将其放入线程安全映射中。Time Out超时:得到结果时需要写时间,防止结果不返回导致long时间的阻塞。

sync future sync future=new sync future();syncfuturecatch . syncfuturemap . put(RPC request . getrequestid()、sync future);try { RPC response RPC response=sync future . get(time out,TimeUnit。毫秒);返回RPC response . getresult();}catch(异常e){ throw e;} finally { syncfuturecatch . syncfuturemap . remove(RPC request . getrequestid());}当结果返回时,通过返回的RequestId获得对应的未来写响应,未来线程解锁:

log.debug('Tcp客户端接收头:' headAnalysis 'Tcp客户端接收数据:' RPC response);sync future sync future=SyncFuture catch . syncfuturemap . get(RPC response . getrequestid());如果(syncFuture!=null){ sync future . set response(RPC response);}

导入Java。util。并发。countdownlatch导入Java。util。并发。未来;导入Java。util。并发。时间单位;公共类同步未来实现未来{ //因为请求和响应是一一对应的,因此初始化CountDownLatch值为1。private CountDownLatch latch=new CountDownLatch(1);//需要响应线程设置的响应结果私有测试响应;//未来的请求时间,用于计算将来的是否超时私人长开始时间=系统。当前时间毫秒();public sync future(){ } @ Override public boolean cancel(boolean mayInterruptIfRunning){ return false;} @ Override public boolean is canceled(){ return false;} @ Override public boolean isDone(){ if(响应!=null) {返回真实}返回false} //获取响应结果,直到有结果才返回。重写公共T get()抛出中断的异常{ latch。await();返回this.response} //获取响应结果,直到有结果或者超过指定时间就返回。覆盖公共T get(长超时,时间单位单位)抛出中断异常{ if(latch。await(time out,unit)){返回此。回应;}返回null} //用于设置响应结果,并且做倒数计秒操作,通知请求线程公共空集响应(T响应){ this。响应=回应;闩上。倒计时();} public long get begin time(){ return begin time;}}

同步未来同步未来=新同步未来();syncfuturecatch。syncfuturemap。put(RPC请求。getrequestid()、同步未来);RPC响应RPC响应=同步未来。获取(超时,时间单位).毫秒);syncfuturecatch。syncfuturemap。移除(RPC请求。getrequestid());除了同步服务调用,异步服务调用,还有并行服务调用,泛化调用等调用形式。

高可用性简单介绍一下下层分布式服务框架,下面描述一下下层分布式系统的高可用性。一个系统设计开发出来,三天两夜就出了大问题,导致无法使用。那么这个系统就不是一个好系统。业内流传一句话:‘我们的系统支持X 9的可靠性’。X代表一个数字,X ^ 9代表系统1年使用期间时间与总数的比率时间 (1年)。三个9:(1-99.9%)* 365 * 24=8.76小时,表示系统连续运行1年最可能出现的业务中断时间时间为8.76小时,四个9为52.6分钟,五个9为5.26分钟。实现如此高的可靠性是一个巨大的挑战。一个大型的分布式项目可能由几十个或者几百个项目组成,涉及上千个服务,主链中的一个流程需要转移到多个团队维护的项目中。就拿四个9的可靠度来说,平均分摊到各队可能不到10分钟时间。10分钟内,你需要顶住压力,以最快的速度找到并解决问题时间,恢复系统的可用性。先说提高系统可靠性的方案:服务检测:当一台服务器与注册中心的连接中断,其提供的服务没有响应时,系统应该能够主动重启服务,使其能够正常对外提供。隔离:在集群环境中,一个服务器可以对外提供服务,但是由于其他原因,请求结果总是异常。此时,需要主动将节点从集群环境中移除,以免对后续请求造成进一步影响,然后在非高峰时段尝试修复问题。至于机房故障,只能屏蔽整个机房。目前,饿了么在异地做的工作比较多。即使机房一侧挂机,流量也可以完全切换到机房另一侧,保证系统的可用性。监控:包括业务监控、服务异常监控、DB中间件性能监控等。当系统出现异常时,可以及时通知开发人员。等到线下报纸上来的时候,可能影响就大了。电压测量:生产线主要环节的电压测量是必须的。一些高并发性场景不能由集成测试单独覆盖。电压测量可以暴露正常情况下不能出现的问题,也可以直观地显示系统的吞吐能力。当业务激增时,可以考虑直接扩展系统。SOP方案及做法:生产线上随时可能出现问题,绝对不能抱着时间的态度去尝试解决。提前做好相应问题的SOP预案,可以节省很多时间,尽快恢复系统正常。当然平时的演练也是必不可少的。一旦生产线出现故障,我们可以从容应对。除了上述方案,还可以考虑使用服务策略:降级策略:在业务高峰期,为了保证核心服务,需要停止一些不重要的业务。比如双十一期间,不允许发起退款,只允许查看3个月内的历史订单。调用服务接口时,直接返回空结果或异常是分布式系统的降级策略。服务降级是可逆的操作。当系统压力恢复到一定值时,需要消除降级,使服务状态恢复正常。服务降级主要包括屏蔽降级和容错降级:

屏蔽降级:分布式服务框架直接屏蔽对远程接口的请求,不发起对远程服务的调用,直接返回null结果,抛出指定异常,实现本地模拟接口。容错降级:当非核心服务无法调用时,可以接通故障服务,保证主进程不受影响。如请求超时、异常消息解码、异常系统拥塞保护、异常服务提供商系统等。之前笔者也遇到过由于双方的容错和降级失败导致系统失败的情况。下午高峰时间,对方给我们一个非核心接口打电话查询。我们的系统因为Bug问题一直异常,导致对方调用这个界面的页面异常,无法跳转到主流程页面,影响了生产线的生产。当时对方发布了紧急版本,让系统恢复正常。限流策略:说到限流,首先想到的就是尖峰活动。高峰活动的流量可能是正常流量的几百倍到几千倍。这么高的流量系统根本处理不了,只能通过。

过限流来避免系统的崩溃。服务的限流本质和秒杀活动的限流是一样的,都是限制请求的流入,防止服务提供方因大量的请求而崩溃。限流算法:令牌桶、漏桶、计数器算法。上述算法适合单机的限流,但涉及到整个集群的限流时,得考虑使用缓存中间件了。例如:某个服务 1 分钟内只允许请求 2 次,或者一天只允许使用 1000 次。由于负载均衡存在,可能集群内每台机器都会收到请求,这种时候就需要缓存来记录调用方某段时间内的请求次数,再做限流处理。Redis 就很适合做此事。熔断策略:熔断本质上是一种过载保护机制,这一概念来源于电子工程中的断路器,当电流过大时,保险丝会熔断,从而保护整个电路。同样在分布式系统中,当被调用的远程服务无法使用时,如果没有过载保护,就会导致请求的资源阻塞在远程服务器上耗尽资源。很多时候,刚开始可能只是出现了局部小规模的故障,然而由于种种原因,故障影响范围越来越大,最终导致全局性的后果。当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护自己以及系统整体的可用性,可以暂时切断对下游服务的调用。熔断器的设计思路:

Closed:初始状态,熔断器关闭,正常提供服务。Open: 失败次数,失败百分比达到一定的阈值之后,熔断器打开,停止访问服务。Half-Open:熔断一定时间之后,小流量尝试调用服务,如果成功则恢复,熔断器变为 Closed 状态。

数据一致性一个系统设计开发出来,必须保证其运行的数据准确和一致性。拿支付系统来说:用户银行卡已经扣款成功,系统里却显示失败,没有给用户的虚拟帐户充值上,这会引起客诉。说的再严重点,用户发起提现,资金已经转到其银行账户,系统却没扣除对应虚拟帐号的余额,直接导致资金损失了。如果这时候用户一直发起提现,那就酸爽了。

CAP 原则说到数据一致性,就不得不说到 CAP 原则。CAP 原则中指出任何一个分布式系统中,Consistency(一致性 C)、 Availability(可用性 A)、Partition tolerance(分区容错性 P),三者不可兼得。传统单机数据库基于 ACID 特性(原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)) ,放弃了分区容错性,能做到可用性和一致性。对于一个分布式系统而言,分区容错性是一个最基本的要求。既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,会出现节点与节点之间的网络通讯。而网络问题又是一定会出现的异常情况,分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。系统架构师往往需要把精力花在如何根据业务特点在一致性和可用性之间寻求平衡。集中式系统,通过数据库事务的控制,能做到数据的强一致性。但是分布式系统中,涉及多服务间的调用,通过分布式事务的方案:

两阶段提交(2PC)三阶段提交(3PC)补偿事务(TCC)...

虽然能实现数据的强一致,但是都是通过牺牲可用性来实现。

BASE 理论BASE 理论是对 CAP 原则中一致性和可用性权衡的结果:Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)。BASE 理论,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 原则逐步演化而来的。其最核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。基本可用:是指分布式系统在出现不可预知故障的时候,允许损失部分可用性,这不等价于系统不可用。软状态:指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。最终一致性:强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统的事物 ACID 特性是相反的。它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID 特性和 BASE 理论往往又会结合在一起。

结语分布式系统涉及到的东西还有很多,如:分布式锁、定时调度、数据分片、性能问题、各种中间件的使用等,笔者分享只是了解到的那一小部分的知识而已。之前本着学习的目的也写过一个非常简单的分布式服务框架 blackRpc,通过它了解了分布式服务框架内部的一些活动。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

为您推荐

....